sidebar.go

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