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