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