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