sidebar.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package model
  2. import (
  3. "cmp"
  4. "fmt"
  5. "charm.land/lipgloss/v2"
  6. "github.com/charmbracelet/crush/internal/ui/common"
  7. "github.com/charmbracelet/crush/internal/ui/logo"
  8. uv "github.com/charmbracelet/ultraviolet"
  9. "github.com/charmbracelet/ultraviolet/layout"
  10. "golang.org/x/text/cases"
  11. "golang.org/x/text/language"
  12. )
  13. // modelInfo renders the current model information including reasoning
  14. // settings and context usage/cost for the sidebar.
  15. func (m *UI) modelInfo(width int) string {
  16. model := m.selectedLargeModel()
  17. reasoningInfo := ""
  18. providerName := ""
  19. if model != nil {
  20. // Get provider name first
  21. providerConfig, ok := m.com.Config().Providers.Get(model.ModelCfg.Provider)
  22. if ok {
  23. providerName = providerConfig.Name
  24. // Only check reasoning if model can reason
  25. if model.CatwalkCfg.CanReason {
  26. if len(model.CatwalkCfg.ReasoningLevels) == 0 {
  27. if model.ModelCfg.Think {
  28. reasoningInfo = "Thinking On"
  29. } else {
  30. reasoningInfo = "Thinking Off"
  31. }
  32. } else {
  33. formatter := cases.Title(language.English, cases.NoLower)
  34. reasoningEffort := cmp.Or(model.ModelCfg.ReasoningEffort, model.CatwalkCfg.DefaultReasoningEffort)
  35. reasoningInfo = formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))
  36. }
  37. }
  38. }
  39. }
  40. var modelContext *common.ModelContextInfo
  41. if model != nil && m.session != nil {
  42. modelContext = &common.ModelContextInfo{
  43. ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
  44. Cost: m.session.Cost,
  45. ModelContext: model.CatwalkCfg.ContextWindow,
  46. }
  47. }
  48. return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
  49. }
  50. // getDynamicHeightLimits will give us the num of items to show in each section based on the hight
  51. // some items are more important than others.
  52. func getDynamicHeightLimits(availableHeight int) (maxFiles, maxLSPs, maxMCPs int) {
  53. const (
  54. minItemsPerSection = 2
  55. defaultMaxFilesShown = 10
  56. defaultMaxLSPsShown = 8
  57. defaultMaxMCPsShown = 8
  58. minAvailableHeightLimit = 10
  59. )
  60. // If we have very little space, use minimum values
  61. if availableHeight < minAvailableHeightLimit {
  62. return minItemsPerSection, minItemsPerSection, minItemsPerSection
  63. }
  64. // Distribute available height among the three sections
  65. // Give priority to files, then LSPs, then MCPs
  66. totalSections := 3
  67. heightPerSection := availableHeight / totalSections
  68. // Calculate limits for each section, ensuring minimums
  69. maxFiles = max(minItemsPerSection, min(defaultMaxFilesShown, heightPerSection))
  70. maxLSPs = max(minItemsPerSection, min(defaultMaxLSPsShown, heightPerSection))
  71. maxMCPs = max(minItemsPerSection, min(defaultMaxMCPsShown, heightPerSection))
  72. // If we have extra space, give it to files first
  73. remainingHeight := availableHeight - (maxFiles + maxLSPs + maxMCPs)
  74. if remainingHeight > 0 {
  75. extraForFiles := min(remainingHeight, defaultMaxFilesShown-maxFiles)
  76. maxFiles += extraForFiles
  77. remainingHeight -= extraForFiles
  78. if remainingHeight > 0 {
  79. extraForLSPs := min(remainingHeight, defaultMaxLSPsShown-maxLSPs)
  80. maxLSPs += extraForLSPs
  81. remainingHeight -= extraForLSPs
  82. if remainingHeight > 0 {
  83. maxMCPs += min(remainingHeight, defaultMaxMCPsShown-maxMCPs)
  84. }
  85. }
  86. }
  87. return maxFiles, maxLSPs, maxMCPs
  88. }
  89. // sidebar renders the chat sidebar containing session title, working
  90. // directory, model info, file list, LSP status, and MCP status.
  91. func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
  92. if m.session == nil {
  93. return
  94. }
  95. const logoHeightBreakpoint = 30
  96. t := m.com.Styles
  97. width := area.Dx()
  98. height := area.Dy()
  99. title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
  100. cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width)
  101. sidebarLogo := m.sidebarLogo
  102. if height < logoHeightBreakpoint {
  103. sidebarLogo = logo.SmallRender(m.com.Styles, width)
  104. }
  105. blocks := []string{
  106. sidebarLogo,
  107. title,
  108. "",
  109. cwd,
  110. "",
  111. m.modelInfo(width),
  112. "",
  113. }
  114. sidebarHeader := lipgloss.JoinVertical(
  115. lipgloss.Left,
  116. blocks...,
  117. )
  118. _, remainingHeightArea := layout.SplitVertical(m.layout.sidebar, layout.Fixed(lipgloss.Height(sidebarHeader)))
  119. remainingHeight := remainingHeightArea.Dy() - 10
  120. maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
  121. lspSection := m.lspInfo(width, maxLSPs, true)
  122. mcpSection := m.mcpInfo(width, maxMCPs, true)
  123. filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true)
  124. uv.NewStyledString(
  125. lipgloss.NewStyle().
  126. MaxWidth(width).
  127. MaxHeight(height).
  128. Render(
  129. lipgloss.JoinVertical(
  130. lipgloss.Left,
  131. sidebarHeader,
  132. filesSection,
  133. "",
  134. lspSection,
  135. "",
  136. mcpSection,
  137. ),
  138. ),
  139. ).Draw(scr, area)
  140. }