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