@@ -2,6 +2,7 @@ package splash
7 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
@@ -48,10 +49,12 @@ type splashCmp struct {
49 isOnboarding bool
50 isOnboarding bool
50 needsProjectInit bool
51 needsProjectInit bool
51 selectedNo bool
53 selectedNo bool
53− modelList *models.ModelListComponent
55+ modelList *models.ModelListComponent
54− cursorRow, cursorCol int
56+ apiKeyInput *models.APIKeyInput
57+ selectedModel *models.ModelOption
57func New() Splash {
60func New() Splash {
@@ -69,12 +72,15 @@ func New() Splash {
69 t := styles.CurrentTheme()
72 t := styles.CurrentTheme()
70 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
73 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
71 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
74 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
75+ apiKeyInput := models.NewAPIKeyInput()
72 return &splashCmp{
77 return &splashCmp{
74 height: 0,
79 height: 0,
75 keyMap: keyMap,
80 keyMap: keyMap,
76 logoRendered: "",
81 logoRendered: "",
77 modelList: modelList,
82 modelList: modelList,
83+ apiKeyInput: apiKeyInput,
78 selectedNo: false,
84 selectedNo: false,
@@ -114,7 +120,7 @@ func (s *splashCmp) GetSize() (int, int) {
115// Init implements SplashPage.
121// Init implements SplashPage.
116func (s *splashCmp) Init() tea.Cmd {
122func (s *splashCmp) Init() tea.Cmd {
117− return s.modelList.Init()
123+ return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
120// SetSize implements SplashPage.
126// SetSize implements SplashPage.
@@ -125,8 +131,6 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
125 listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
131 listHeigh := min(40, height-(SplashScreenPaddingY*2)-lipgloss.Height(s.logoRendered)-2) // -1 for the title
126 listWidth := min(60, width-(SplashScreenPaddingX*2))
132 listWidth := min(60, width-(SplashScreenPaddingX*2))
128− // Calculate the cursor position based on the height and logo size
129− s.cursorRow = height - listHeigh
130 return s.modelList.SetSize(listWidth, listHeigh)
134 return s.modelList.SetSize(listWidth, listHeigh)
@@ -137,8 +141,16 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
137 return s, s.SetSize(msg.Width, msg.Height)
141 return s, s.SetSize(msg.Width, msg.Height)
138 case tea.KeyPressMsg:
142 case tea.KeyPressMsg:
144+ case key.Matches(msg, s.keyMap.Back):
145+ slog.Info("Back key pressed in splash screen")
147+ // Go back to model selection
148+ s.needsAPIKey = false
149+ s.selectedModel = nil
140 case key.Matches(msg, s.keyMap.Select):
152 case key.Matches(msg, s.keyMap.Select):
141− if s.isOnboarding {
153+ if s.isOnboarding && !s.needsAPIKey {
142 modelInx := s.modelList.SelectedIndex()
154 modelInx := s.modelList.SelectedIndex()
143 items := s.modelList.Items()
155 items := s.modelList.Items()
144 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
156 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
@@ -146,6 +158,18 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
146 cmd := s.setPreferredModel(selectedItem)
158 cmd := s.setPreferredModel(selectedItem)
147 s.isOnboarding = false
159 s.isOnboarding = false
148 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
160 return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
162+ // Provider not configured, show API key input
163+ s.needsAPIKey = true
164+ s.selectedModel = &selectedItem
165+ s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
168+ } else if s.needsAPIKey {
169+ // Handle API key submission
170+ apiKey := s.apiKeyInput.Value()
172+ return s, s.saveAPIKeyAndContinue(apiKey)
150 } else if s.needsProjectInit {
174 } else if s.needsProjectInit {
151 return s, s.initializeProject()
175 return s, s.initializeProject()
@@ -165,16 +189,50 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
165 return s, util.CmdHandler(OnboardingCompleteMsg{})
189 return s, util.CmdHandler(OnboardingCompleteMsg{})
193+ u, cmd := s.apiKeyInput.Update(msg)
194+ s.apiKeyInput = u.(*models.APIKeyInput)
168− if s.isOnboarding {
196+ } else if s.isOnboarding {
169 u, cmd := s.modelList.Update(msg)
197 u, cmd := s.modelList.Update(msg)
170 s.modelList = u
198 s.modelList = u
171 return s, cmd
199 return s, cmd
204+ u, cmd := s.apiKeyInput.Update(msg)
205+ s.apiKeyInput = u.(*models.APIKeyInput)
207+ } else if s.isOnboarding {
209+ s.modelList, cmd = s.modelList.Update(msg)
175 return s, nil
213 return s, nil
216+func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
217+ if s.selectedModel == nil {
218+ return util.ReportError(fmt.Errorf("no model selected"))
222+ err := cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
224+ return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
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
233+ return tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
178func (s *splashCmp) initializeProject() tea.Cmd {
236func (s *splashCmp) initializeProject() tea.Cmd {
179 s.needsProjectInit = false
237 s.needsProjectInit = false
180 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
238 prompt := `Please analyze this codebase and create a CRUSH.md file containing:
@@ -283,7 +341,21 @@ func (s *splashCmp) View() string {
283 t := styles.CurrentTheme()
341 t := styles.CurrentTheme()
285 var content string
343 var content string
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(
353+ content = lipgloss.JoinVertical(
286− if s.isOnboarding {
358+ } else if s.isOnboarding {
287 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
359 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
288 modelListView := s.modelList.View()
360 modelListView := s.modelList.View()
289 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
361 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
@@ -363,7 +435,12 @@ func (s *splashCmp) View() string {
365func (s *splashCmp) Cursor() *tea.Cursor {
437func (s *splashCmp) Cursor() *tea.Cursor {
439+ cursor := s.apiKeyInput.Cursor()
441+ return s.moveCursor(cursor)
366− if s.isOnboarding {
443+ } else if s.isOnboarding {
367 cursor := s.modelList.Cursor()
444 cursor := s.modelList.Cursor()
368 if cursor != nil {
445 if cursor != nil {
369 return s.moveCursor(cursor)
446 return s.moveCursor(cursor)
@@ -391,15 +468,38 @@ func (m *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
391 if cursor == nil {
468 if cursor == nil {
392 return nil
469 return nil
394− offset := m.cursorRow
471+
472+ // Calculate the correct Y offset based on current state
473+ logoHeight := lipgloss.Height(m.logoRendered)
474+ baseOffset := logoHeight + SplashScreenPaddingY
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())
395− cursor.Y += offset
480+ cursor.Y += offset
481+ // API key input already includes prompt in its cursor positioning
396− cursor.X = cursor.X + 3 // 3 for padding
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
488+ // Model list doesn't have a prompt, so add padding + space for list styling
489+ cursor.X = cursor.X + SplashScreenPaddingX + 1
397 return cursor
492 return cursor
400// Bindings implements SplashPage.
495// Bindings implements SplashPage.
401func (s *splashCmp) Bindings() []key.Binding {
496func (s *splashCmp) Bindings() []key.Binding {
498+ return []key.Binding{
402− if s.isOnboarding {
502+ } else if s.isOnboarding {
403 return []key.Binding{
503 return []key.Binding{
404 s.keyMap.Select,
504 s.keyMap.Select,
405 s.keyMap.Next,
505 s.keyMap.Next,