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