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