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