splash.go 19 KB

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