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