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}