@@ -2,6 +2,7 @@ package splash
22
33import (
44 "fmt"
5+ "log/slog"
56 "slices"
67
78 "github.com/charmbracelet/bubbles/v2/key"
@@ -48,10 +49,12 @@ type splashCmp struct {
4849 // State
4950 isOnboarding bool
5051 needsProjectInit bool
52+ needsAPIKey bool
5153 selectedNo bool
5254
53− modelList *models.ModelListComponent
54− cursorRow, cursorCol int
55+ modelList *models.ModelListComponent
56+ apiKeyInput *models.APIKeyInput
57+ selectedModel *models.ModelOption
5558}
5659
5760func New() Splash {
@@ -69,12 +72,15 @@ func New() Splash {
6972 t := styles.CurrentTheme()
7073 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
7174 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
75+ apiKeyInput := models.NewAPIKeyInput()
76+
7277 return &splashCmp{
7378 width: 0,
7479 height: 0,
7580 keyMap: keyMap,
7681 logoRendered: "",
7782 modelList: modelList,
83+ apiKeyInput: apiKeyInput,
7884 selectedNo: false,
7985 }
8086}
@@ -114,7 +120,7 @@ func (s *splashCmp) GetSize() (int, int) {
114120
115121// Init implements SplashPage.
116122func (s *splashCmp) Init() tea.Cmd {
117− return s.modelList.Init()
123+ return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
118124}
119125
120126// SetSize implements SplashPage.
@@ -125,8 +131,6 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
125131 listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
126132 listWidth := min(60, width-(SplashScreenPaddingX*2))
127133
128− // Calculate the cursor position based on the height and logo size
129− s.cursorRow = height - listHeigh
130134 return s.modelList.SetSize(listWidth, listHeigh)
131135}
132136
@@ -137,8 +141,16 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137141 return s, s.SetSize(msg.Width, msg.Height)
138142 case tea.KeyPressMsg:
139143 switch {
144+ case key.Matches(msg, s.keyMap.Back):
145+ slog.Info("Back key pressed in splash screen")
146+ if s.needsAPIKey {
147+ // Go back to model selection
148+ s.needsAPIKey = false
149+ s.selectedModel = nil
150+ return s, nil
151+ }
140152 case key.Matches(msg, s.keyMap.Select):
141− if s.isOnboarding {
153+ if s.isOnboarding && !s.needsAPIKey {
142154 modelInx := s.modelList.SelectedIndex()
143155 items := s.modelList.Items()
144156 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
@@ -146,6 +158,18 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
146158 cmd := s.setPreferredModel(selectedItem)
147159 s.isOnboarding = false
148160 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
161+ } else {
162+ // Provider not configured, show API key input
163+ s.needsAPIKey = true
164+ s.selectedModel = &selectedItem
165+ s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
166+ return s, nil
167+ }
168+ } else if s.needsAPIKey {
169+ // Handle API key submission
170+ apiKey := s.apiKeyInput.Value()
171+ if apiKey != "" {
172+ return s, s.saveAPIKeyAndContinue(apiKey)
149173 }
150174 } else if s.needsProjectInit {
151175 return s, s.initializeProject()
@@ -165,16 +189,50 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
165189 return s, util.CmdHandler(OnboardingCompleteMsg{})
166190 }
167191 default:
168− if s.isOnboarding {
192+ if s.needsAPIKey {
193+ u, cmd := s.apiKeyInput.Update(msg)
194+ s.apiKeyInput = u.(*models.APIKeyInput)
195+ return s, cmd
196+ } else if s.isOnboarding {
169197 u, cmd := s.modelList.Update(msg)
170198 s.modelList = u
171199 return s, cmd
172200 }
173201 }
202+ case tea.PasteMsg:
203+ if s.needsAPIKey {
204+ u, cmd := s.apiKeyInput.Update(msg)
205+ s.apiKeyInput = u.(*models.APIKeyInput)
206+ return s, cmd
207+ } else if s.isOnboarding {
208+ var cmd tea.Cmd
209+ s.modelList, cmd = s.modelList.Update(msg)
210+ return s, cmd
211+ }
174212 }
175213 return s, nil
176214}
177215
216+func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
217+ if s.selectedModel == nil {
218+ return util.ReportError(fmt.Errorf("no model selected"))
219+ }
220+
221+ cfg := config.Get()
222+ err := cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
223+ if err != nil {
224+ return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
225+ }
226+
227+ // Reset API key state and continue with model selection
228+ s.needsAPIKey = false
229+ cmd := s.setPreferredModel(*s.selectedModel)
230+ s.isOnboarding = false
231+ s.selectedModel = nil
232+
233+ return tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
234+}
235+
178236func (s *splashCmp) initializeProject() tea.Cmd {
179237 s.needsProjectInit = false
180238 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
@@ -283,7 +341,21 @@ func (s *splashCmp) View() string {
283341 t := styles.CurrentTheme()
284342
285343 var content string
286− if s.isOnboarding {
344+ if s.needsAPIKey {
345+ remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
346+ apiKeyView := s.apiKeyInput.View()
347+ apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
348+ lipgloss.JoinVertical(
349+ lipgloss.Left,
350+ apiKeyView,
351+ ),
352+ )
353+ content = lipgloss.JoinVertical(
354+ lipgloss.Left,
355+ s.logoRendered,
356+ apiKeySelector,
357+ )
358+ } else if s.isOnboarding {
287359 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
288360 modelListView := s.modelList.View()
289361 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -363,7 +435,12 @@ func (s *splashCmp) View() string {
363435}
364436
365437func (s *splashCmp) Cursor() *tea.Cursor {
366− if s.isOnboarding {
438+ if s.needsAPIKey {
439+ cursor := s.apiKeyInput.Cursor()
440+ if cursor != nil {
441+ return s.moveCursor(cursor)
442+ }
443+ } else if s.isOnboarding {
367444 cursor := s.modelList.Cursor()
368445 if cursor != nil {
369446 return s.moveCursor(cursor)
@@ -391,15 +468,38 @@ func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
391468 if cursor == nil {
392469 return nil
393470 }
394− offset := m.cursorRow
395− cursor.Y += offset
396− cursor.X = cursor.X + 3 // 3 for padding
471+
472+ // Calculate the correct Y offset based on current state
473+ logoHeight := lipgloss.Height(m.logoRendered)
474+ baseOffset := logoHeight + SplashScreenPaddingY
475+
476+ if m.needsAPIKey {
477+ // For API key input, position at the bottom of the remaining space
478+ remainingHeight := m.height - logoHeight - (SplashScreenPaddingY * 2)
479+ offset := baseOffset + remainingHeight - lipgloss.Height(m.apiKeyInput.View())
480+ cursor.Y += offset
481+ // API key input already includes prompt in its cursor positioning
482+ cursor.X = cursor.X + SplashScreenPaddingX
483+ } else if m.isOnboarding {
484+ // For model list, use the original calculation
485+ listHeight := min(40, m.height-(SplashScreenPaddingY*2)-logoHeight-2)
486+ offset := m.height - listHeight
487+ cursor.Y += offset
488+ // Model list doesn't have a prompt, so add padding + space for list styling
489+ cursor.X = cursor.X + SplashScreenPaddingX + 1
490+ }
491+
397492 return cursor
398493}
399494
400495// Bindings implements SplashPage.
401496func (s *splashCmp) Bindings() []key.Binding {
402− if s.isOnboarding {
497+ if s.needsAPIKey {
498+ return []key.Binding{
499+ s.keyMap.Select,
500+ s.keyMap.Back,
501+ }
502+ } else if s.isOnboarding {
403503 return []key.Binding{
404504 s.keyMap.Select,
405505 s.keyMap.Next,