1package model
2
3import (
4 "cmp"
5 "fmt"
6 "image"
7
8 "charm.land/lipgloss/v2"
9 "github.com/charmbracelet/crush/internal/ui/common"
10 "github.com/charmbracelet/crush/internal/ui/logo"
11 uv "github.com/charmbracelet/ultraviolet"
12 "github.com/charmbracelet/ultraviolet/layout"
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 len(model.CatwalkCfg.ReasoningLevels) == 0 {
31 if model.ModelCfg.Think {
32 reasoningInfo = "Thinking On"
33 } else {
34 reasoningInfo = "Thinking Off"
35 }
36 } else {
37 reasoningEffort := cmp.Or(model.ModelCfg.ReasoningEffort, model.CatwalkCfg.DefaultReasoningEffort)
38 reasoningInfo = fmt.Sprintf("Reasoning %s", common.FormatReasoningEffort(reasoningEffort))
39 }
40 }
41 }
42 }
43
44 var modelContext *common.ModelContextInfo
45 if model != nil && m.session != nil {
46 modelContext = &common.ModelContextInfo{
47 ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
48 Cost: m.session.Cost,
49 ModelContext: model.CatwalkCfg.ContextWindow,
50 }
51 }
52 var modelName string
53 if model != nil {
54 modelName = model.CatwalkCfg.Name
55 }
56 return common.ModelInfo(m.com.Styles, modelName, providerName, reasoningInfo, modelContext, width)
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.Workspace.WorkingDir(), width)
121 sidebarLogo := m.sidebarLogo
122 if height < logoHeightBreakpoint {
123 sidebarLogo = logo.SmallRender(m.com.Styles, 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 var remainingHeightArea image.Rectangle
141 layout.Vertical(
142 layout.Len(lipgloss.Height(sidebarHeader)),
143 layout.Fill(1),
144 ).Split(m.layout.sidebar).Assign(new(image.Rectangle), &remainingHeightArea)
145 remainingHeight := remainingHeightArea.Dy() - 10
146 maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
147
148 lspSection := m.lspInfo(width, maxLSPs, true)
149 mcpSection := m.mcpInfo(width, maxMCPs, true)
150 filesSection := m.filesInfo(m.com.Workspace.WorkingDir(), width, maxFiles, true)
151
152 uv.NewStyledString(
153 lipgloss.NewStyle().
154 MaxWidth(width).
155 MaxHeight(height).
156 Render(
157 lipgloss.JoinVertical(
158 lipgloss.Left,
159 sidebarHeader,
160 filesSection,
161 "",
162 lspSection,
163 "",
164 mcpSection,
165 ),
166 ),
167 ).Draw(scr, area)
168}