splash.go

  1package splash
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"time"
  7
  8	"charm.land/bubbles/v2/key"
  9	"charm.land/bubbles/v2/spinner"
 10	tea "charm.land/bubbletea/v2"
 11	"charm.land/lipgloss/v2"
 12	"git.secluded.site/crush/internal/agent"
 13	"git.secluded.site/crush/internal/config"
 14	"git.secluded.site/crush/internal/home"
 15	"git.secluded.site/crush/internal/tui/components/chat"
 16	"git.secluded.site/crush/internal/tui/components/core"
 17	"git.secluded.site/crush/internal/tui/components/core/layout"
 18	"git.secluded.site/crush/internal/tui/components/dialogs/models"
 19	"git.secluded.site/crush/internal/tui/components/logo"
 20	lspcomponent "git.secluded.site/crush/internal/tui/components/lsp"
 21	"git.secluded.site/crush/internal/tui/components/mcp"
 22	"git.secluded.site/crush/internal/tui/exp/list"
 23	"git.secluded.site/crush/internal/tui/styles"
 24	"git.secluded.site/crush/internal/tui/util"
 25	"git.secluded.site/crush/internal/version"
 26	"github.com/charmbracelet/catwalk/pkg/catwalk"
 27)
 28
 29type Splash interface {
 30	util.Model
 31	layout.Sizeable
 32	layout.Help
 33	Cursor() *tea.Cursor
 34	// SetOnboarding controls whether the splash shows model selection UI
 35	SetOnboarding(bool)
 36	// SetProjectInit controls whether the splash shows project initialization prompt
 37	SetProjectInit(bool)
 38
 39	// Showing API key input
 40	IsShowingAPIKey() bool
 41
 42	// IsAPIKeyValid returns whether the API key is valid
 43	IsAPIKeyValid() bool
 44}
 45
 46const (
 47	SplashScreenPaddingY = 1 // Padding Y for the splash screen
 48
 49	LogoGap = 6
 50)
 51
 52// OnboardingCompleteMsg is sent when onboarding is complete
 53type (
 54	OnboardingCompleteMsg struct{}
 55	SubmitAPIKeyMsg       struct{}
 56)
 57
 58type splashCmp struct {
 59	width, height int
 60	keyMap        KeyMap
 61	logoRendered  string
 62
 63	// State
 64	isOnboarding     bool
 65	needsProjectInit bool
 66	needsAPIKey      bool
 67	selectedNo       bool
 68
 69	listHeight    int
 70	modelList     *models.ModelListComponent
 71	apiKeyInput   *models.APIKeyInput
 72	selectedModel *models.ModelOption
 73	isAPIKeyValid bool
 74	apiKeyValue   string
 75}
 76
 77func New() Splash {
 78	keyMap := DefaultKeyMap()
 79	listKeyMap := list.DefaultKeyMap()
 80	listKeyMap.Down.SetEnabled(false)
 81	listKeyMap.Up.SetEnabled(false)
 82	listKeyMap.HalfPageDown.SetEnabled(false)
 83	listKeyMap.HalfPageUp.SetEnabled(false)
 84	listKeyMap.Home.SetEnabled(false)
 85	listKeyMap.End.SetEnabled(false)
 86	listKeyMap.DownOneItem = keyMap.Next
 87	listKeyMap.UpOneItem = keyMap.Previous
 88
 89	modelList := models.NewModelListComponent(listKeyMap, "Find your fave", false)
 90	apiKeyInput := models.NewAPIKeyInput()
 91
 92	return &splashCmp{
 93		width:        0,
 94		height:       0,
 95		keyMap:       keyMap,
 96		logoRendered: "",
 97		modelList:    modelList,
 98		apiKeyInput:  apiKeyInput,
 99		selectedNo:   false,
100	}
101}
102
103func (s *splashCmp) SetOnboarding(onboarding bool) {
104	s.isOnboarding = onboarding
105}
106
107func (s *splashCmp) SetProjectInit(needsInit bool) {
108	s.needsProjectInit = needsInit
109}
110
111// GetSize implements SplashPage.
112func (s *splashCmp) GetSize() (int, int) {
113	return s.width, s.height
114}
115
116// Init implements SplashPage.
117func (s *splashCmp) Init() tea.Cmd {
118	return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
119}
120
121// SetSize implements SplashPage.
122func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
123	wasSmallScreen := s.isSmallScreen()
124	rerenderLogo := width != s.width
125	s.height = height
126	s.width = width
127	if rerenderLogo || wasSmallScreen != s.isSmallScreen() {
128		s.logoRendered = s.logoBlock()
129	}
130	// remove padding, logo height, gap, title space
131	s.listHeight = s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - s.logoGap() - 2
132	listWidth := min(60, width)
133	s.apiKeyInput.SetWidth(width - 2)
134	return s.modelList.SetSize(listWidth, s.listHeight)
135}
136
137// Update implements SplashPage.
138func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
139	switch msg := msg.(type) {
140	case tea.WindowSizeMsg:
141		return s, s.SetSize(msg.Width, msg.Height)
142	case models.APIKeyStateChangeMsg:
143		u, cmd := s.apiKeyInput.Update(msg)
144		s.apiKeyInput = u.(*models.APIKeyInput)
145		if msg.State == models.APIKeyInputStateVerified {
146			return s, tea.Tick(5*time.Second, func(t time.Time) tea.Msg {
147				return SubmitAPIKeyMsg{}
148			})
149		}
150		return s, cmd
151	case SubmitAPIKeyMsg:
152		if s.isAPIKeyValid {
153			return s, s.saveAPIKeyAndContinue(s.apiKeyValue)
154		}
155	case tea.KeyPressMsg:
156		switch {
157		case key.Matches(msg, s.keyMap.Back):
158			if s.isAPIKeyValid {
159				return s, nil
160			}
161			if s.needsAPIKey {
162				// Go back to model selection
163				s.needsAPIKey = false
164				s.selectedModel = nil
165				s.isAPIKeyValid = false
166				s.apiKeyValue = ""
167				s.apiKeyInput.Reset()
168				return s, nil
169			}
170		case key.Matches(msg, s.keyMap.Select):
171			if s.isAPIKeyValid {
172				return s, s.saveAPIKeyAndContinue(s.apiKeyValue)
173			}
174			if s.isOnboarding && !s.needsAPIKey {
175				selectedItem := s.modelList.SelectedModel()
176				if selectedItem == nil {
177					return s, nil
178				}
179				if s.isProviderConfigured(string(selectedItem.Provider.ID)) {
180					cmd := s.setPreferredModel(*selectedItem)
181					s.isOnboarding = false
182					return s, tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
183				} else {
184					// Provider not configured, show API key input
185					s.needsAPIKey = true
186					s.selectedModel = selectedItem
187					s.apiKeyInput.SetProviderName(selectedItem.Provider.Name)
188					return s, nil
189				}
190			} else if s.needsAPIKey {
191				// Handle API key submission
192				s.apiKeyValue = strings.TrimSpace(s.apiKeyInput.Value())
193				if s.apiKeyValue == "" {
194					return s, nil
195				}
196
197				provider, err := s.getProvider(s.selectedModel.Provider.ID)
198				if err != nil || provider == nil {
199					return s, util.ReportError(fmt.Errorf("provider %s not found", s.selectedModel.Provider.ID))
200				}
201				providerConfig := config.ProviderConfig{
202					ID:      string(s.selectedModel.Provider.ID),
203					Name:    s.selectedModel.Provider.Name,
204					APIKey:  s.apiKeyValue,
205					Type:    provider.Type,
206					BaseURL: provider.APIEndpoint,
207				}
208				return s, tea.Sequence(
209					util.CmdHandler(models.APIKeyStateChangeMsg{
210						State: models.APIKeyInputStateVerifying,
211					}),
212					func() tea.Msg {
213						start := time.Now()
214						err := providerConfig.TestConnection(config.Get().Resolver())
215						// intentionally wait for at least 750ms to make sure the user sees the spinner
216						elapsed := time.Since(start)
217						if elapsed < 750*time.Millisecond {
218							time.Sleep(750*time.Millisecond - elapsed)
219						}
220						if err == nil {
221							s.isAPIKeyValid = true
222							return models.APIKeyStateChangeMsg{
223								State: models.APIKeyInputStateVerified,
224							}
225						}
226						return models.APIKeyStateChangeMsg{
227							State: models.APIKeyInputStateError,
228						}
229					},
230				)
231			} else if s.needsProjectInit {
232				return s, s.initializeProject()
233			}
234		case key.Matches(msg, s.keyMap.Tab, s.keyMap.LeftRight):
235			if s.needsAPIKey {
236				u, cmd := s.apiKeyInput.Update(msg)
237				s.apiKeyInput = u.(*models.APIKeyInput)
238				return s, cmd
239			}
240			if s.needsProjectInit {
241				s.selectedNo = !s.selectedNo
242				return s, nil
243			}
244		case key.Matches(msg, s.keyMap.Yes):
245			if s.needsAPIKey {
246				u, cmd := s.apiKeyInput.Update(msg)
247				s.apiKeyInput = u.(*models.APIKeyInput)
248				return s, cmd
249			}
250			if s.isOnboarding {
251				u, cmd := s.modelList.Update(msg)
252				s.modelList = u
253				return s, cmd
254			}
255			if s.needsProjectInit {
256				s.selectedNo = false
257				return s, s.initializeProject()
258			}
259		case key.Matches(msg, s.keyMap.No):
260			if s.needsAPIKey {
261				u, cmd := s.apiKeyInput.Update(msg)
262				s.apiKeyInput = u.(*models.APIKeyInput)
263				return s, cmd
264			}
265			if s.isOnboarding {
266				u, cmd := s.modelList.Update(msg)
267				s.modelList = u
268				return s, cmd
269			}
270			if s.needsProjectInit {
271				s.selectedNo = true
272				return s, s.initializeProject()
273			}
274		default:
275			if s.needsAPIKey {
276				u, cmd := s.apiKeyInput.Update(msg)
277				s.apiKeyInput = u.(*models.APIKeyInput)
278				return s, cmd
279			} else if s.isOnboarding {
280				u, cmd := s.modelList.Update(msg)
281				s.modelList = u
282				return s, cmd
283			}
284		}
285	case tea.PasteMsg:
286		if s.needsAPIKey {
287			u, cmd := s.apiKeyInput.Update(msg)
288			s.apiKeyInput = u.(*models.APIKeyInput)
289			return s, cmd
290		} else if s.isOnboarding {
291			var cmd tea.Cmd
292			s.modelList, cmd = s.modelList.Update(msg)
293			return s, cmd
294		}
295	case spinner.TickMsg:
296		u, cmd := s.apiKeyInput.Update(msg)
297		s.apiKeyInput = u.(*models.APIKeyInput)
298		return s, cmd
299	}
300	return s, nil
301}
302
303func (s *splashCmp) saveAPIKeyAndContinue(apiKey string) tea.Cmd {
304	if s.selectedModel == nil {
305		return nil
306	}
307
308	cfg := config.Get()
309	err := cfg.SetProviderAPIKey(string(s.selectedModel.Provider.ID), apiKey)
310	if err != nil {
311		return util.ReportError(fmt.Errorf("failed to save API key: %w", err))
312	}
313
314	// Reset API key state and continue with model selection
315	s.needsAPIKey = false
316	cmd := s.setPreferredModel(*s.selectedModel)
317	s.isOnboarding = false
318	s.selectedModel = nil
319	s.isAPIKeyValid = false
320
321	return tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
322}
323
324func (s *splashCmp) initializeProject() tea.Cmd {
325	s.needsProjectInit = false
326
327	if err := config.MarkProjectInitialized(); err != nil {
328		return util.ReportError(err)
329	}
330	var cmds []tea.Cmd
331
332	cmds = append(cmds, util.CmdHandler(OnboardingCompleteMsg{}))
333	if !s.selectedNo {
334		initPrompt, err := agent.InitializePrompt(*config.Get())
335		if err != nil {
336			return util.ReportError(err)
337		}
338		cmds = append(cmds,
339			util.CmdHandler(chat.SessionClearedMsg{}),
340			util.CmdHandler(chat.SendMsg{
341				Text: initPrompt,
342			}),
343		)
344	}
345	return tea.Sequence(cmds...)
346}
347
348func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
349	cfg := config.Get()
350	model := cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
351	if model == nil {
352		return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
353	}
354
355	selectedModel := config.SelectedModel{
356		Model:           selectedItem.Model.ID,
357		Provider:        string(selectedItem.Provider.ID),
358		ReasoningEffort: model.DefaultReasoningEffort,
359		MaxTokens:       model.DefaultMaxTokens,
360	}
361
362	err := cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
363	if err != nil {
364		return util.ReportError(err)
365	}
366
367	// Now lets automatically setup the small model
368	knownProvider, err := s.getProvider(selectedItem.Provider.ID)
369	if err != nil {
370		return util.ReportError(err)
371	}
372	if knownProvider == nil {
373		// for local provider we just use the same model
374		err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
375		if err != nil {
376			return util.ReportError(err)
377		}
378	} else {
379		smallModel := knownProvider.DefaultSmallModelID
380		model := cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
381		// should never happen
382		if model == nil {
383			err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
384			if err != nil {
385				return util.ReportError(err)
386			}
387			return nil
388		}
389		smallSelectedModel := config.SelectedModel{
390			Model:           smallModel,
391			Provider:        string(selectedItem.Provider.ID),
392			ReasoningEffort: model.DefaultReasoningEffort,
393			MaxTokens:       model.DefaultMaxTokens,
394		}
395		err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
396		if err != nil {
397			return util.ReportError(err)
398		}
399	}
400	cfg.SetupAgents()
401	return nil
402}
403
404func (s *splashCmp) getProvider(providerID catwalk.InferenceProvider) (*catwalk.Provider, error) {
405	cfg := config.Get()
406	providers, err := config.Providers(cfg)
407	if err != nil {
408		return nil, err
409	}
410	for _, p := range providers {
411		if p.ID == providerID {
412			return &p, nil
413		}
414	}
415	return nil, nil
416}
417
418func (s *splashCmp) isProviderConfigured(providerID string) bool {
419	cfg := config.Get()
420	if _, ok := cfg.Providers.Get(providerID); ok {
421		return true
422	}
423	return false
424}
425
426func (s *splashCmp) View() string {
427	t := styles.CurrentTheme()
428	var content string
429	if s.needsAPIKey {
430		remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
431		apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
432		apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
433			lipgloss.JoinVertical(
434				lipgloss.Left,
435				apiKeyView,
436			),
437		)
438		content = lipgloss.JoinVertical(
439			lipgloss.Left,
440			s.logoRendered,
441			apiKeySelector,
442		)
443	} else if s.isOnboarding {
444		modelListView := s.modelList.View()
445		remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
446		modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
447			lipgloss.JoinVertical(
448				lipgloss.Left,
449				t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("To start, let’s choose a provider and model."),
450				"",
451				modelListView,
452			),
453		)
454		content = lipgloss.JoinVertical(
455			lipgloss.Left,
456			s.logoRendered,
457			modelSelector,
458		)
459	} else if s.needsProjectInit {
460		titleStyle := t.S().Base.Foreground(t.FgBase)
461		pathStyle := t.S().Base.Foreground(t.Success).PaddingLeft(2)
462		bodyStyle := t.S().Base.Foreground(t.FgMuted)
463		shortcutStyle := t.S().Base.Foreground(t.Success)
464
465		initFile := config.Get().Options.InitializeAs
466		initText := lipgloss.JoinVertical(
467			lipgloss.Left,
468			titleStyle.Render("Would you like to initialize this project?"),
469			"",
470			pathStyle.Render(s.cwd()),
471			"",
472			bodyStyle.Render("When I initialize your codebase I examine the project and put the"),
473			bodyStyle.Render(fmt.Sprintf("result into an %s file which serves as general context.", initFile)),
474			"",
475			bodyStyle.Render("You can also initialize anytime via ")+shortcutStyle.Render("ctrl+p")+bodyStyle.Render("."),
476			"",
477			bodyStyle.Render("Would you like to initialize now?"),
478		)
479
480		yesButton := core.SelectableButton(core.ButtonOpts{
481			Text:           "Yep!",
482			UnderlineIndex: 0,
483			Selected:       !s.selectedNo,
484		})
485
486		noButton := core.SelectableButton(core.ButtonOpts{
487			Text:           "Nope",
488			UnderlineIndex: 0,
489			Selected:       s.selectedNo,
490		})
491
492		buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, "  ", noButton)
493		remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
494
495		initContent := t.S().Base.AlignVertical(lipgloss.Bottom).PaddingLeft(1).Height(remainingHeight).Render(
496			lipgloss.JoinVertical(
497				lipgloss.Left,
498				initText,
499				"",
500				buttons,
501			),
502		)
503
504		content = lipgloss.JoinVertical(
505			lipgloss.Left,
506			s.logoRendered,
507			"",
508			initContent,
509		)
510	} else {
511		parts := []string{
512			s.logoRendered,
513			s.infoSection(),
514		}
515		content = lipgloss.JoinVertical(lipgloss.Left, parts...)
516	}
517
518	return t.S().Base.
519		Width(s.width).
520		Height(s.height).
521		PaddingTop(SplashScreenPaddingY).
522		PaddingBottom(SplashScreenPaddingY).
523		Render(content)
524}
525
526func (s *splashCmp) Cursor() *tea.Cursor {
527	if s.needsAPIKey {
528		cursor := s.apiKeyInput.Cursor()
529		if cursor != nil {
530			return s.moveCursor(cursor)
531		}
532	} else if s.isOnboarding {
533		cursor := s.modelList.Cursor()
534		if cursor != nil {
535			return s.moveCursor(cursor)
536		}
537	} else {
538		return nil
539	}
540	return nil
541}
542
543func (s *splashCmp) isSmallScreen() bool {
544	// Consider a screen small if either the width is less than 40 or if the
545	// height is less than 20
546	return s.width < 55 || s.height < 20
547}
548
549func (s *splashCmp) infoSection() string {
550	t := styles.CurrentTheme()
551	infoStyle := t.S().Base.PaddingLeft(2)
552	if s.isSmallScreen() {
553		infoStyle = infoStyle.MarginTop(1)
554	}
555	return infoStyle.Render(
556		lipgloss.JoinVertical(
557			lipgloss.Left,
558			s.cwdPart(),
559			"",
560			s.currentModelBlock(),
561			"",
562			lipgloss.JoinHorizontal(lipgloss.Left, s.lspBlock(), s.mcpBlock()),
563			"",
564		),
565	)
566}
567
568func (s *splashCmp) logoBlock() string {
569	t := styles.CurrentTheme()
570	logoStyle := t.S().Base.Padding(0, 2).Width(s.width)
571	if s.isSmallScreen() {
572		// If the width is too small, render a smaller version of the logo
573		// NOTE: 20 is not correct because [splashCmp.height] is not the
574		// *actual* window height, instead, it is the height of the splash
575		// component and that depends on other variables like compact mode and
576		// the height of the editor.
577		return logoStyle.Render(
578			logo.SmallRender(s.width - logoStyle.GetHorizontalFrameSize()),
579		)
580	}
581	return logoStyle.Render(
582		logo.Render(version.Version, false, logo.Opts{
583			FieldColor:   t.Primary,
584			TitleColorA:  t.Secondary,
585			TitleColorB:  t.Primary,
586			CharmColor:   t.Secondary,
587			VersionColor: t.Primary,
588			Width:        s.width - logoStyle.GetHorizontalFrameSize(),
589		}),
590	)
591}
592
593func (s *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
594	if cursor == nil {
595		return nil
596	}
597	// Calculate the correct Y offset based on current state
598	logoHeight := lipgloss.Height(s.logoRendered)
599	if s.needsAPIKey {
600		infoSectionHeight := lipgloss.Height(s.infoSection())
601		baseOffset := logoHeight + SplashScreenPaddingY + infoSectionHeight
602		remainingHeight := s.height - baseOffset - lipgloss.Height(s.apiKeyInput.View()) - SplashScreenPaddingY
603		offset := baseOffset + remainingHeight
604		cursor.Y += offset
605		cursor.X = cursor.X + 1
606	} else if s.isOnboarding {
607		offset := logoHeight + SplashScreenPaddingY + s.logoGap() + 2
608		cursor.Y += offset
609		cursor.X = cursor.X + 1
610	}
611
612	return cursor
613}
614
615func (s *splashCmp) logoGap() int {
616	if s.height > 35 {
617		return LogoGap
618	}
619	return 0
620}
621
622// Bindings implements SplashPage.
623func (s *splashCmp) Bindings() []key.Binding {
624	if s.needsAPIKey {
625		return []key.Binding{
626			s.keyMap.Select,
627			s.keyMap.Back,
628		}
629	} else if s.isOnboarding {
630		return []key.Binding{
631			s.keyMap.Select,
632			s.keyMap.Next,
633			s.keyMap.Previous,
634		}
635	} else if s.needsProjectInit {
636		return []key.Binding{
637			s.keyMap.Select,
638			s.keyMap.Yes,
639			s.keyMap.No,
640			s.keyMap.Tab,
641			s.keyMap.LeftRight,
642		}
643	}
644	return []key.Binding{}
645}
646
647func (s *splashCmp) getMaxInfoWidth() int {
648	return min(s.width-2, 90) // 2 for left padding
649}
650
651func (s *splashCmp) cwdPart() string {
652	t := styles.CurrentTheme()
653	maxWidth := s.getMaxInfoWidth()
654	return t.S().Muted.Width(maxWidth).Render(s.cwd())
655}
656
657func (s *splashCmp) cwd() string {
658	return home.Short(config.Get().WorkingDir())
659}
660
661func LSPList(maxWidth int) []string {
662	return lspcomponent.RenderLSPList(nil, lspcomponent.RenderOptions{
663		MaxWidth:    maxWidth,
664		ShowSection: false,
665	})
666}
667
668func (s *splashCmp) lspBlock() string {
669	t := styles.CurrentTheme()
670	maxWidth := s.getMaxInfoWidth() / 2
671	section := t.S().Subtle.Render("LSPs")
672	lspList := append([]string{section, ""}, LSPList(maxWidth-1)...)
673	return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
674		lipgloss.JoinVertical(
675			lipgloss.Left,
676			lspList...,
677		),
678	)
679}
680
681func MCPList(maxWidth int) []string {
682	return mcp.RenderMCPList(mcp.RenderOptions{
683		MaxWidth:    maxWidth,
684		ShowSection: false,
685	})
686}
687
688func (s *splashCmp) mcpBlock() string {
689	t := styles.CurrentTheme()
690	maxWidth := s.getMaxInfoWidth() / 2
691	section := t.S().Subtle.Render("MCPs")
692	mcpList := append([]string{section, ""}, MCPList(maxWidth-1)...)
693	return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
694		lipgloss.JoinVertical(
695			lipgloss.Left,
696			mcpList...,
697		),
698	)
699}
700
701func (s *splashCmp) currentModelBlock() string {
702	cfg := config.Get()
703	agentCfg := cfg.Agents[config.AgentCoder]
704	model := config.Get().GetModelByType(agentCfg.Model)
705	if model == nil {
706		return ""
707	}
708	t := styles.CurrentTheme()
709	modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
710	modelName := t.S().Text.Render(model.Name)
711	modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
712	parts := []string{
713		modelInfo,
714	}
715
716	return lipgloss.JoinVertical(
717		lipgloss.Left,
718		parts...,
719	)
720}
721
722func (s *splashCmp) IsShowingAPIKey() bool {
723	return s.needsAPIKey
724}
725
726func (s *splashCmp) IsAPIKeyValid() bool {
727	return s.isAPIKeyValid
728}