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