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)
 57}
 58
 59// getDynamicHeightLimits will give us the num of items to show in each section based on the hight
 60// some items are more important than others.
 61func getDynamicHeightLimits(availableHeight int) (maxFiles, maxLSPs, maxMCPs int) {
 62	const (
 63		minItemsPerSection      = 2
 64		defaultMaxFilesShown    = 10
 65		defaultMaxLSPsShown     = 8
 66		defaultMaxMCPsShown     = 8
 67		minAvailableHeightLimit = 10
 68	)
 69
 70	// If we have very little space, use minimum values
 71	if availableHeight < minAvailableHeightLimit {
 72		return minItemsPerSection, minItemsPerSection, minItemsPerSection
 73	}
 74
 75	// Distribute available height among the three sections
 76	// Give priority to files, then LSPs, then MCPs
 77	totalSections := 3
 78	heightPerSection := availableHeight / totalSections
 79
 80	// Calculate limits for each section, ensuring minimums
 81	maxFiles = max(minItemsPerSection, min(defaultMaxFilesShown, heightPerSection))
 82	maxLSPs = max(minItemsPerSection, min(defaultMaxLSPsShown, heightPerSection))
 83	maxMCPs = max(minItemsPerSection, min(defaultMaxMCPsShown, heightPerSection))
 84
 85	// If we have extra space, give it to files first
 86	remainingHeight := availableHeight - (maxFiles + maxLSPs + maxMCPs)
 87	if remainingHeight > 0 {
 88		extraForFiles := min(remainingHeight, defaultMaxFilesShown-maxFiles)
 89		maxFiles += extraForFiles
 90		remainingHeight -= extraForFiles
 91
 92		if remainingHeight > 0 {
 93			extraForLSPs := min(remainingHeight, defaultMaxLSPsShown-maxLSPs)
 94			maxLSPs += extraForLSPs
 95			remainingHeight -= extraForLSPs
 96
 97			if remainingHeight > 0 {
 98				maxMCPs += min(remainingHeight, defaultMaxMCPsShown-maxMCPs)
 99			}
100		}
101	}
102
103	return maxFiles, maxLSPs, maxMCPs
104}
105
106// sidebar renders the chat sidebar containing session title, working
107// directory, model info, file list, LSP status, and MCP status.
108func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
109	if m.session == nil {
110		return
111	}
112
113	const logoHeightBreakpoint = 30
114
115	t := m.com.Styles
116	width := area.Dx()
117	height := area.Dy()
118
119	title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
120	cwd := common.PrettyPath(t, m.com.Workspace.WorkingDir(), width)
121	sidebarLogo := m.sidebarLogo
122	if height < logoHeightBreakpoint {
123		sidebarLogo = logo.SmallRender(m.com.Styles, width)
124	}
125	blocks := []string{
126		sidebarLogo,
127		title,
128		"",
129		cwd,
130		"",
131		m.modelInfo(width),
132		"",
133	}
134
135	sidebarHeader := lipgloss.JoinVertical(
136		lipgloss.Left,
137		blocks...,
138	)
139
140	var remainingHeightArea image.Rectangle
141	layout.Vertical(
142		layout.Len(lipgloss.Height(sidebarHeader)),
143		layout.Fill(1),
144	).Split(m.layout.sidebar).Assign(new(image.Rectangle), &remainingHeightArea)
145	remainingHeight := remainingHeightArea.Dy() - 10
146	maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
147
148	lspSection := m.lspInfo(width, maxLSPs, true)
149	mcpSection := m.mcpInfo(width, maxMCPs, true)
150	filesSection := m.filesInfo(m.com.Workspace.WorkingDir(), width, maxFiles, true)
151
152	uv.NewStyledString(
153		lipgloss.NewStyle().
154			MaxWidth(width).
155			MaxHeight(height).
156			Render(
157				lipgloss.JoinVertical(
158					lipgloss.Left,
159					sidebarHeader,
160					filesSection,
161					"",
162					lspSection,
163					"",
164					mcpSection,
165				),
166			),
167	).Draw(scr, area)
168}