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 model != nil && 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 return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
55 }
56 return ""
57}
58
59// getDynamicHeightLimits will give us the num of items to show in each section based on the hight
60// some items are more important than others.
61func getDynamicHeightLimits(availableHeight int) (maxFiles, maxLSPs, maxMCPs int) {
62 const (
63 minItemsPerSection = 2
64 defaultMaxFilesShown = 10
65 defaultMaxLSPsShown = 8
66 defaultMaxMCPsShown = 8
67 minAvailableHeightLimit = 10
68 )
69
70 // If we have very little space, use minimum values
71 if availableHeight < minAvailableHeightLimit {
72 return minItemsPerSection, minItemsPerSection, minItemsPerSection
73 }
74
75 // Distribute available height among the three sections
76 // Give priority to files, then LSPs, then MCPs
77 totalSections := 3
78 heightPerSection := availableHeight / totalSections
79
80 // Calculate limits for each section, ensuring minimums
81 maxFiles = max(minItemsPerSection, min(defaultMaxFilesShown, heightPerSection))
82 maxLSPs = max(minItemsPerSection, min(defaultMaxLSPsShown, heightPerSection))
83 maxMCPs = max(minItemsPerSection, min(defaultMaxMCPsShown, heightPerSection))
84
85 // If we have extra space, give it to files first
86 remainingHeight := availableHeight - (maxFiles + maxLSPs + maxMCPs)
87 if remainingHeight > 0 {
88 extraForFiles := min(remainingHeight, defaultMaxFilesShown-maxFiles)
89 maxFiles += extraForFiles
90 remainingHeight -= extraForFiles
91
92 if remainingHeight > 0 {
93 extraForLSPs := min(remainingHeight, defaultMaxLSPsShown-maxLSPs)
94 maxLSPs += extraForLSPs
95 remainingHeight -= extraForLSPs
96
97 if remainingHeight > 0 {
98 maxMCPs += min(remainingHeight, defaultMaxMCPsShown-maxMCPs)
99 }
100 }
101 }
102
103 return maxFiles, maxLSPs, maxMCPs
104}
105
106// sidebar renders the chat sidebar containing session title, working
107// directory, model info, file list, LSP status, and MCP status.
108func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
109 if m.session == nil {
110 return
111 }
112
113 const logoHeightBreakpoint = 30
114
115 t := m.com.Styles
116 width := area.Dx()
117 height := area.Dy()
118
119 title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
120 cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width)
121 sidebarLogo := m.sidebarLogo
122 if height < logoHeightBreakpoint {
123 sidebarLogo = logo.SmallRender(width)
124 }
125 blocks := []string{
126 sidebarLogo,
127 title,
128 "",
129 cwd,
130 "",
131 m.modelInfo(width),
132 "",
133 }
134
135 sidebarHeader := lipgloss.JoinVertical(
136 lipgloss.Left,
137 blocks...,
138 )
139
140 _, remainingHeightArea := uv.SplitVertical(m.layout.sidebar, uv.Fixed(lipgloss.Height(sidebarHeader)))
141 remainingHeight := remainingHeightArea.Dy() - 10
142 maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
143
144 lspSection := m.lspInfo(width, maxLSPs, true)
145 mcpSection := m.mcpInfo(width, maxMCPs, true)
146 filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true)
147
148 uv.NewStyledString(
149 lipgloss.NewStyle().
150 MaxWidth(width).
151 MaxHeight(height).
152 Render(
153 lipgloss.JoinVertical(
154 lipgloss.Left,
155 sidebarHeader,
156 filesSection,
157 "",
158 lspSection,
159 "",
160 mcpSection,
161 ),
162 ),
163 ).Draw(scr, area)
164}