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/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/fur/provider"
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/completions"
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/core/list"
21 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
22 "github.com/charmbracelet/crush/internal/tui/components/logo"
23 "github.com/charmbracelet/crush/internal/tui/styles"
24 "github.com/charmbracelet/crush/internal/tui/util"
25 "github.com/charmbracelet/crush/internal/version"
26 "github.com/charmbracelet/lipgloss/v2"
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 t := styles.CurrentTheme()
90 inputStyle := t.S().Base.Padding(0, 1, 0, 1)
91 modelList := models.NewModelListComponent(listKeyMap, inputStyle, "Find your fave")
92 apiKeyInput := models.NewAPIKeyInput()
93
94 return &splashCmp{
95 width: 0,
96 height: 0,
97 keyMap: keyMap,
98 logoRendered: "",
99 modelList: modelList,
100 apiKeyInput: apiKeyInput,
101 selectedNo: false,
102 }
103}
104
105func (s *splashCmp) SetOnboarding(onboarding bool) {
106 s.isOnboarding = onboarding
107 if onboarding {
108 providers, err := config.Providers()
109 if err != nil {
110 return
111 }
112 filteredProviders := []provider.Provider{}
113 simpleProviders := []string{
114 "anthropic",
115 "openai",
116 "gemini",
117 "xai",
118 "groq",
119 "openrouter",
120 }
121 for _, p := range providers {
122 if slices.Contains(simpleProviders, string(p.ID)) {
123 filteredProviders = append(filteredProviders, p)
124 }
125 }
126 s.modelList.SetProviders(filteredProviders)
127 }
128}
129
130func (s *splashCmp) SetProjectInit(needsInit bool) {
131 s.needsProjectInit = needsInit
132}
133
134// GetSize implements SplashPage.
135func (s *splashCmp) GetSize() (int, int) {
136 return s.width, s.height
137}
138
139// Init implements SplashPage.
140func (s *splashCmp) Init() tea.Cmd {
141 return tea.Batch(s.modelList.Init(), s.apiKeyInput.Init())
142}
143
144// SetSize implements SplashPage.
145func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
146 wasSmallScreen := s.isSmallScreen()
147 rerenderLogo := width != s.width
148 s.height = height
149 s.width = width
150 if rerenderLogo || wasSmallScreen != s.isSmallScreen() {
151 s.logoRendered = s.logoBlock()
152 }
153 // remove padding, logo height, gap, title space
154 s.listHeight = s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - s.logoGap() - 2
155 listWidth := min(60, width)
156 s.apiKeyInput.SetWidth(width - 2)
157 return s.modelList.SetSize(listWidth, s.listHeight)
158}
159
160// Update implements SplashPage.
161func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
162 switch msg := msg.(type) {
163 case tea.WindowSizeMsg:
164 return s, s.SetSize(msg.Width, msg.Height)
165 case models.APIKeyStateChangeMsg:
166 u, cmd := s.apiKeyInput.Update(msg)
167 s.apiKeyInput = u.(*models.APIKeyInput)
168 if msg.State == models.APIKeyInputStateVerified {
169 return s, tea.Tick(5*time.Second, func(t time.Time) tea.Msg {
170 return SubmitAPIKeyMsg{}
171 })
172 }
173 return s, cmd
174 case SubmitAPIKeyMsg:
175 if s.isAPIKeyValid {
176 return s, s.saveAPIKeyAndContinue(s.apiKeyValue)
177 }
178 case tea.KeyPressMsg:
179 switch {
180 case key.Matches(msg, s.keyMap.Back):
181 if s.isAPIKeyValid {
182 return s, nil
183 }
184 if s.needsAPIKey {
185 // Go back to model selection
186 s.needsAPIKey = false
187 s.selectedModel = nil
188 s.isAPIKeyValid = false
189 s.apiKeyValue = ""
190 s.apiKeyInput.Reset()
191 return s, nil
192 }
193 case key.Matches(msg, s.keyMap.Select):
194 if s.isAPIKeyValid {
195 return s, s.saveAPIKeyAndContinue(s.apiKeyValue)
196 }
197 if s.isOnboarding && !s.needsAPIKey {
198 modelInx := s.modelList.SelectedIndex()
199 items := s.modelList.Items()
200 selectedItem := items[modelInx].(completions.CompletionItem).Value().(models.ModelOption)
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
273 if s.needsProjectInit {
274 return s, s.initializeProject()
275 }
276 case key.Matches(msg, s.keyMap.No):
277 if s.needsAPIKey {
278 u, cmd := s.apiKeyInput.Update(msg)
279 s.apiKeyInput = u.(*models.APIKeyInput)
280 return s, cmd
281 }
282
283 s.selectedNo = true
284 return s, s.initializeProject()
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 util.ReportError(fmt.Errorf("no model selected"))
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
331 return tea.Batch(cmd, util.CmdHandler(OnboardingCompleteMsg{}))
332}
333
334func (s *splashCmp) initializeProject() tea.Cmd {
335 s.needsProjectInit = false
336
337 if err := config.MarkProjectInitialized(); err != nil {
338 return util.ReportError(err)
339 }
340 var cmds []tea.Cmd
341
342 cmds = append(cmds, util.CmdHandler(OnboardingCompleteMsg{}))
343 if !s.selectedNo {
344 cmds = append(cmds,
345 util.CmdHandler(chat.SessionClearedMsg{}),
346 util.CmdHandler(chat.SendMsg{
347 Text: prompt.Initialize(),
348 }),
349 )
350 }
351 return tea.Sequence(cmds...)
352}
353
354func (s *splashCmp) setPreferredModel(selectedItem models.ModelOption) tea.Cmd {
355 cfg := config.Get()
356 model := cfg.GetModel(string(selectedItem.Provider.ID), selectedItem.Model.ID)
357 if model == nil {
358 return util.ReportError(fmt.Errorf("model %s not found for provider %s", selectedItem.Model.ID, selectedItem.Provider.ID))
359 }
360
361 selectedModel := config.SelectedModel{
362 Model: selectedItem.Model.ID,
363 Provider: string(selectedItem.Provider.ID),
364 ReasoningEffort: model.DefaultReasoningEffort,
365 MaxTokens: model.DefaultMaxTokens,
366 }
367
368 err := cfg.UpdatePreferredModel(config.SelectedModelTypeLarge, selectedModel)
369 if err != nil {
370 return util.ReportError(err)
371 }
372
373 // Now lets automatically setup the small model
374 knownProvider, err := s.getProvider(selectedItem.Provider.ID)
375 if err != nil {
376 return util.ReportError(err)
377 }
378 if knownProvider == nil {
379 // for local provider we just use the same model
380 err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
381 if err != nil {
382 return util.ReportError(err)
383 }
384 } else {
385 smallModel := knownProvider.DefaultSmallModelID
386 model := cfg.GetModel(string(selectedItem.Provider.ID), smallModel)
387 // should never happen
388 if model == nil {
389 err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, selectedModel)
390 if err != nil {
391 return util.ReportError(err)
392 }
393 return nil
394 }
395 smallSelectedModel := config.SelectedModel{
396 Model: smallModel,
397 Provider: string(selectedItem.Provider.ID),
398 ReasoningEffort: model.DefaultReasoningEffort,
399 MaxTokens: model.DefaultMaxTokens,
400 }
401 err = cfg.UpdatePreferredModel(config.SelectedModelTypeSmall, smallSelectedModel)
402 if err != nil {
403 return util.ReportError(err)
404 }
405 }
406 cfg.SetupAgents()
407 return nil
408}
409
410func (s *splashCmp) getProvider(providerID provider.InferenceProvider) (*provider.Provider, error) {
411 providers, err := config.Providers()
412 if err != nil {
413 return nil, err
414 }
415 for _, p := range providers {
416 if p.ID == providerID {
417 return &p, nil
418 }
419 }
420 return nil, nil
421}
422
423func (s *splashCmp) isProviderConfigured(providerID string) bool {
424 cfg := config.Get()
425 if _, ok := cfg.Providers[providerID]; ok {
426 return true
427 }
428 return false
429}
430
431func (s *splashCmp) View() string {
432 t := styles.CurrentTheme()
433 var content string
434 if s.needsAPIKey {
435 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
436 apiKeyView := t.S().Base.PaddingLeft(1).Render(s.apiKeyInput.View())
437 apiKeySelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
438 lipgloss.JoinVertical(
439 lipgloss.Left,
440 apiKeyView,
441 ),
442 )
443 content = lipgloss.JoinVertical(
444 lipgloss.Left,
445 s.logoRendered,
446 apiKeySelector,
447 )
448 } else if s.isOnboarding {
449 modelListView := s.modelList.View()
450 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2)
451 modelSelector := t.S().Base.AlignVertical(lipgloss.Bottom).Height(remainingHeight).Render(
452 lipgloss.JoinVertical(
453 lipgloss.Left,
454 t.S().Base.PaddingLeft(1).Foreground(t.Primary).Render("Choose a Model"),
455 "",
456 modelListView,
457 ),
458 )
459 content = lipgloss.JoinVertical(
460 lipgloss.Left,
461 s.logoRendered,
462 modelSelector,
463 )
464 } else if s.needsProjectInit {
465 titleStyle := t.S().Base.Foreground(t.FgBase)
466 bodyStyle := t.S().Base.Foreground(t.FgMuted)
467 shortcutStyle := t.S().Base.Foreground(t.Success)
468
469 initText := lipgloss.JoinVertical(
470 lipgloss.Left,
471 titleStyle.Render("Would you like to initialize this project?"),
472 "",
473 bodyStyle.Render("When I initialize your codebase I examine the project and put the"),
474 bodyStyle.Render("result into a CRUSH.md file which serves as general context."),
475 "",
476 bodyStyle.Render("You can also initialize anytime via ")+shortcutStyle.Render("ctrl+p")+bodyStyle.Render("."),
477 "",
478 bodyStyle.Render("Would you like to initialize now?"),
479 )
480
481 yesButton := core.SelectableButton(core.ButtonOpts{
482 Text: "Yep!",
483 UnderlineIndex: 0,
484 Selected: !s.selectedNo,
485 })
486
487 noButton := core.SelectableButton(core.ButtonOpts{
488 Text: "Nope",
489 UnderlineIndex: 0,
490 Selected: s.selectedNo,
491 })
492
493 buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, " ", noButton)
494 infoSection := s.infoSection()
495
496 remainingHeight := s.height - lipgloss.Height(s.logoRendered) - (SplashScreenPaddingY * 2) - lipgloss.Height(infoSection)
497
498 initContent := t.S().Base.AlignVertical(lipgloss.Bottom).PaddingLeft(1).Height(remainingHeight).Render(
499 lipgloss.JoinVertical(
500 lipgloss.Left,
501 initText,
502 "",
503 buttons,
504 ),
505 )
506
507 content = lipgloss.JoinVertical(
508 lipgloss.Left,
509 s.logoRendered,
510 infoSection,
511 initContent,
512 )
513 } else {
514 parts := []string{
515 s.logoRendered,
516 s.infoSection(),
517 }
518 content = lipgloss.JoinVertical(lipgloss.Left, parts...)
519 }
520
521 return t.S().Base.
522 Width(s.width).
523 Height(s.height).
524 PaddingTop(SplashScreenPaddingY).
525 PaddingBottom(SplashScreenPaddingY).
526 Render(content)
527}
528
529func (s *splashCmp) Cursor() *tea.Cursor {
530 if s.needsAPIKey {
531 cursor := s.apiKeyInput.Cursor()
532 if cursor != nil {
533 return s.moveCursor(cursor)
534 }
535 } else if s.isOnboarding {
536 cursor := s.modelList.Cursor()
537 if cursor != nil {
538 return s.moveCursor(cursor)
539 }
540 } else {
541 return nil
542 }
543 return nil
544}
545
546func (s *splashCmp) isSmallScreen() bool {
547 // Consider a screen small if either the width is less than 40 or if the
548 // height is less than 20
549 return s.width < 55 || s.height < 20
550}
551
552func (s *splashCmp) infoSection() string {
553 t := styles.CurrentTheme()
554 infoStyle := t.S().Base.PaddingLeft(2)
555 if s.isSmallScreen() {
556 infoStyle = infoStyle.MarginTop(1)
557 }
558 return infoStyle.Render(
559 lipgloss.JoinVertical(
560 lipgloss.Left,
561 s.cwd(),
562 "",
563 lipgloss.JoinHorizontal(lipgloss.Left, s.lspBlock(), s.mcpBlock()),
564 "",
565 ),
566 )
567}
568
569func (s *splashCmp) logoBlock() string {
570 t := styles.CurrentTheme()
571 logoStyle := t.S().Base.Padding(0, 2).Width(s.width)
572 if s.isSmallScreen() {
573 // If the width is too small, render a smaller version of the logo
574 // NOTE: 20 is not correct because [splashCmp.height] is not the
575 // *actual* window height, instead, it is the height of the splash
576 // component and that depends on other variables like compact mode and
577 // the height of the editor.
578 return logoStyle.Render(
579 logo.SmallRender(s.width - logoStyle.GetHorizontalFrameSize()),
580 )
581 }
582 return logoStyle.Render(
583 logo.Render(version.Version, false, logo.Opts{
584 FieldColor: t.Primary,
585 TitleColorA: t.Secondary,
586 TitleColorB: t.Primary,
587 CharmColor: t.Secondary,
588 VersionColor: t.Primary,
589 Width: s.width - logoStyle.GetHorizontalFrameSize(),
590 }),
591 )
592}
593
594func (s *splashCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
595 if cursor == nil {
596 return nil
597 }
598 // Calculate the correct Y offset based on current state
599 logoHeight := lipgloss.Height(s.logoRendered)
600 if s.needsAPIKey {
601 infoSectionHeight := lipgloss.Height(s.infoSection())
602 baseOffset := logoHeight + SplashScreenPaddingY + infoSectionHeight
603 remainingHeight := s.height - baseOffset - lipgloss.Height(s.apiKeyInput.View()) - SplashScreenPaddingY
604 offset := baseOffset + remainingHeight
605 cursor.Y += offset
606 cursor.X = cursor.X + 1
607 } else if s.isOnboarding {
608 offset := logoHeight + SplashScreenPaddingY + s.logoGap() + 3
609 cursor.Y += offset
610 cursor.X = cursor.X + 1
611 }
612
613 return cursor
614}
615
616func (s *splashCmp) logoGap() int {
617 if s.height > 35 {
618 return LogoGap
619 }
620 return 0
621}
622
623// Bindings implements SplashPage.
624func (s *splashCmp) Bindings() []key.Binding {
625 if s.needsAPIKey {
626 return []key.Binding{
627 s.keyMap.Select,
628 s.keyMap.Back,
629 }
630 } else if s.isOnboarding {
631 return []key.Binding{
632 s.keyMap.Select,
633 s.keyMap.Next,
634 s.keyMap.Previous,
635 }
636 } else if s.needsProjectInit {
637 return []key.Binding{
638 s.keyMap.Select,
639 s.keyMap.Yes,
640 s.keyMap.No,
641 s.keyMap.Tab,
642 s.keyMap.LeftRight,
643 }
644 }
645 return []key.Binding{}
646}
647
648func (s *splashCmp) getMaxInfoWidth() int {
649 return min(s.width-2, 40) // 2 for left padding
650}
651
652func (s *splashCmp) cwd() string {
653 cwd := config.Get().WorkingDir()
654 t := styles.CurrentTheme()
655 homeDir, err := os.UserHomeDir()
656 if err == nil && cwd != homeDir {
657 cwd = strings.ReplaceAll(cwd, homeDir, "~")
658 }
659 maxWidth := s.getMaxInfoWidth()
660 return t.S().Muted.Width(maxWidth).Render(cwd)
661}
662
663func LSPList(maxWidth int) []string {
664 t := styles.CurrentTheme()
665 lspList := []string{}
666 lsp := config.Get().LSP.Sorted()
667 if len(lsp) == 0 {
668 return []string{t.S().Base.Foreground(t.Border).Render("None")}
669 }
670 for _, l := range lsp {
671 iconColor := t.Success
672 if l.LSP.Disabled {
673 iconColor = t.FgMuted
674 }
675 lspList = append(lspList,
676 core.Status(
677 core.StatusOpts{
678 IconColor: iconColor,
679 Title: l.Name,
680 Description: l.LSP.Command,
681 },
682 maxWidth,
683 ),
684 )
685 }
686 return lspList
687}
688
689func (s *splashCmp) lspBlock() string {
690 t := styles.CurrentTheme()
691 maxWidth := s.getMaxInfoWidth() / 2
692 section := t.S().Subtle.Render("LSPs")
693 lspList := append([]string{section, ""}, LSPList(maxWidth-1)...)
694 return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
695 lipgloss.JoinVertical(
696 lipgloss.Left,
697 lspList...,
698 ),
699 )
700}
701
702func MCPList(maxWidth int) []string {
703 t := styles.CurrentTheme()
704 mcpList := []string{}
705 mcps := config.Get().MCP.Sorted()
706 if len(mcps) == 0 {
707 return []string{t.S().Base.Foreground(t.Border).Render("None")}
708 }
709 for _, l := range mcps {
710 iconColor := t.Success
711 if l.MCP.Disabled {
712 iconColor = t.FgMuted
713 }
714 mcpList = append(mcpList,
715 core.Status(
716 core.StatusOpts{
717 IconColor: iconColor,
718 Title: l.Name,
719 Description: l.MCP.Command,
720 },
721 maxWidth,
722 ),
723 )
724 }
725 return mcpList
726}
727
728func (s *splashCmp) mcpBlock() string {
729 t := styles.CurrentTheme()
730 maxWidth := s.getMaxInfoWidth() / 2
731 section := t.S().Subtle.Render("MCPs")
732 mcpList := append([]string{section, ""}, MCPList(maxWidth-1)...)
733 return t.S().Base.Width(maxWidth).PaddingRight(1).Render(
734 lipgloss.JoinVertical(
735 lipgloss.Left,
736 mcpList...,
737 ),
738 )
739}
740
741func (s *splashCmp) IsShowingAPIKey() bool {
742 return s.needsAPIKey
743}
744
745func (s *splashCmp) IsAPIKeyValid() bool {
746 return s.isAPIKeyValid
747}