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