sidebar.go 4.6 KB

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