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 const logoHeightBreakpoint = 30
100
101 t := m.com.Styles
102 width := area.Dx()
103 height := area.Dy()
104
105 title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
106 cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width)
107 sidebarLogo := m.sidebarLogo
108 if height < logoHeightBreakpoint {
109 sidebarLogo = logo.SmallRender(width)
110 }
111 blocks := []string{
112 sidebarLogo,
113 title,
114 "",
115 cwd,
116 "",
117 m.modelInfo(width),
118 "",
119 }
120
121 sidebarHeader := lipgloss.JoinVertical(
122 lipgloss.Left,
123 blocks...,
124 )
125
126 _, remainingHeightArea := uv.SplitVertical(m.layout.sidebar, uv.Fixed(lipgloss.Height(sidebarHeader)))
127 remainingHeight := remainingHeightArea.Dy() - 10
128 maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
129
130 lspSection := m.lspInfo(width, maxLSPs, true)
131 mcpSection := m.mcpInfo(width, maxMCPs, true)
132 filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles)
133
134 uv.NewStyledString(
135 lipgloss.NewStyle().
136 MaxWidth(width).
137 MaxHeight(height).
138 Render(
139 lipgloss.JoinVertical(
140 lipgloss.Left,
141 sidebarHeader,
142 filesSection,
143 "",
144 lspSection,
145 "",
146 mcpSection,
147 ),
148 ),
149 ).Draw(scr, area)
150}