sidebar.go

  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			EstimatedUsage: m.session.EstimatedUsage,
 51		}
 52	}
 53	var modelName string
 54	if model != nil {
 55		modelName = model.CatwalkCfg.Name
 56	}
 57	return common.ModelInfo(m.com.Styles, modelName, providerName, reasoningInfo, modelContext, width, m.hyperCredits)
 58}
 59
 60// getDynamicHeightLimits will give us the num of items to show in each section based on the height
 61// some items are more important than others.
 62func getDynamicHeightLimits(availableHeight, fileCount, lspCount, mcpCount, skillCount int) (maxFiles, maxLSPs, maxMCPs, maxSkills int) {
 63	const (
 64		minItemsPerSection = 2
 65		// Keep these high so dynamic layout uses available sidebar space
 66		// instead of hitting small hard limits.
 67		defaultMaxFilesShown    = 1000
 68		defaultMaxLSPsShown     = 1000
 69		defaultMaxMCPsShown     = 1000
 70		defaultMaxSkillsShown   = 1000
 71		minAvailableHeightLimit = 10
 72	)
 73
 74	if availableHeight < minAvailableHeightLimit {
 75		return minItemsPerSection, minItemsPerSection, minItemsPerSection, minItemsPerSection
 76	}
 77
 78	maxFiles = minItemsPerSection
 79	maxLSPs = minItemsPerSection
 80	maxMCPs = minItemsPerSection
 81	maxSkills = minItemsPerSection
 82
 83	remainingHeight := max(0, availableHeight-(minItemsPerSection*4))
 84
 85	sectionValues := []*int{&maxFiles, &maxLSPs, &maxMCPs, &maxSkills}
 86	sectionCaps := []int{defaultMaxFilesShown, defaultMaxLSPsShown, defaultMaxMCPsShown, defaultMaxSkillsShown}
 87	sectionNeeds := []int{max(0, fileCount-maxFiles), max(0, lspCount-maxLSPs), max(0, mcpCount-maxMCPs), max(0, skillCount-maxSkills)}
 88
 89	for remainingHeight > 0 {
 90		allocated := false
 91		for i, section := range sectionValues {
 92			if remainingHeight == 0 {
 93				break
 94			}
 95			if sectionNeeds[i] == 0 || *section >= sectionCaps[i] {
 96				continue
 97			}
 98			*section = *section + 1
 99			sectionNeeds[i]--
100			remainingHeight--
101			allocated = true
102		}
103		if !allocated {
104			break
105		}
106	}
107
108	for remainingHeight > 0 {
109		allocated := false
110		for i, section := range sectionValues {
111			if remainingHeight == 0 {
112				break
113			}
114			if *section >= sectionCaps[i] {
115				continue
116			}
117			*section = *section + 1
118			remainingHeight--
119			allocated = true
120		}
121		if !allocated {
122			break
123		}
124	}
125
126	return maxFiles, maxLSPs, maxMCPs, maxSkills
127}
128
129// sidebar renders the chat sidebar containing session title, working
130// directory, model info, file list, LSP status, and MCP status.
131func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
132	if m.session == nil {
133		return
134	}
135
136	const logoHeightBreakpoint = 30
137
138	t := m.com.Styles
139	width := area.Dx()
140	height := area.Dy()
141
142	title := t.Sidebar.SessionTitle.Width(width).MaxHeight(2).Render(m.session.Title)
143	cwd := common.PrettyPath(t, m.com.Workspace.WorkingDir(), width)
144	sidebarLogo := m.sidebarLogo
145	if height < logoHeightBreakpoint {
146		sidebarLogo = logo.SmallRender(m.com.Styles, width, logo.Opts{
147			Hyper: m.com.IsHyper(),
148		})
149	}
150	blocks := []string{
151		sidebarLogo,
152		title,
153		"",
154		cwd,
155		"",
156		m.modelInfo(width),
157		"",
158	}
159
160	sidebarHeader := lipgloss.JoinVertical(
161		lipgloss.Left,
162		blocks...,
163	)
164
165	var remainingHeightArea image.Rectangle
166	layout.Vertical(
167		layout.Len(lipgloss.Height(sidebarHeader)),
168		layout.Fill(1),
169	).Split(m.layout.sidebar).Assign(new(image.Rectangle), &remainingHeightArea)
170	remainingHeight := remainingHeightArea.Dy() - 6
171	filesCount := 0
172	for _, f := range m.sessionFiles {
173		if f.Additions == 0 && f.Deletions == 0 {
174			continue
175		}
176		filesCount++
177	}
178
179	lspsCount := len(m.lspStates)
180
181	mcpsCount := 0
182	for _, mcpCfg := range m.com.Config().MCP.Sorted() {
183		if _, ok := m.mcpStates[mcpCfg.Name]; ok {
184			mcpsCount++
185		}
186	}
187
188	skillsCount := len(m.skillStatusItems())
189
190	maxFiles, maxLSPs, maxMCPs, maxSkills := getDynamicHeightLimits(remainingHeight, filesCount, lspsCount, mcpsCount, skillsCount)
191
192	lspSection := m.lspInfo(width, maxLSPs, true)
193	mcpSection := m.mcpInfo(width, maxMCPs, true)
194	skillsSection := m.skillsInfo(width, maxSkills, true)
195	filesSection := m.filesInfo(m.com.Workspace.WorkingDir(), width, maxFiles, true)
196
197	uv.NewStyledString(
198		lipgloss.NewStyle().
199			MaxWidth(width).
200			MaxHeight(height).
201			Render(
202				lipgloss.JoinVertical(
203					lipgloss.Left,
204					sidebarHeader,
205					filesSection,
206					"",
207					lspSection,
208					"",
209					mcpSection,
210					"",
211					skillsSection,
212				),
213			),
214	).Draw(scr, area)
215}