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