sidebar.go

  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	providerName := ""
 22
 23	if model != nil {
 24		// Get provider name first
 25		providerConfig, ok := m.com.Config().Providers.Get(model.ModelCfg.Provider)
 26		if ok {
 27			providerName = providerConfig.Name
 28
 29			// Only check reasoning if model can reason
 30			if model.CatwalkCfg.CanReason {
 31				switch providerConfig.Type {
 32				case catwalk.TypeAnthropic:
 33					if model.ModelCfg.Think {
 34						reasoningInfo = "Thinking On"
 35					} else {
 36						reasoningInfo = "Thinking Off"
 37					}
 38				default:
 39					formatter := cases.Title(language.English, cases.NoLower)
 40					reasoningEffort := cmp.Or(model.ModelCfg.ReasoningEffort, model.CatwalkCfg.DefaultReasoningEffort)
 41					reasoningInfo = formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))
 42				}
 43			}
 44		}
 45	}
 46
 47	var modelContext *common.ModelContextInfo
 48	if model != nil && m.session != nil {
 49		modelContext = &common.ModelContextInfo{
 50			ContextUsed:  m.session.CompletionTokens + m.session.PromptTokens,
 51			Cost:         m.session.Cost,
 52			ModelContext: model.CatwalkCfg.ContextWindow,
 53		}
 54		return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
 55	}
 56	return ""
 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.Config().WorkingDir(), width)
121	sidebarLogo := m.sidebarLogo
122	if height < logoHeightBreakpoint {
123		sidebarLogo = logo.SmallRender(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	_, remainingHeightArea := uv.SplitVertical(m.layout.sidebar, uv.Fixed(lipgloss.Height(sidebarHeader)))
141	remainingHeight := remainingHeightArea.Dy() - 10
142	maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
143
144	lspSection := m.lspInfo(width, maxLSPs, true)
145	mcpSection := m.mcpInfo(width, maxMCPs, true)
146	filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true)
147
148	uv.NewStyledString(
149		lipgloss.NewStyle().
150			MaxWidth(width).
151			MaxHeight(height).
152			Render(
153				lipgloss.JoinVertical(
154					lipgloss.Left,
155					sidebarHeader,
156					filesSection,
157					"",
158					lspSection,
159					"",
160					mcpSection,
161				),
162			),
163	).Draw(scr, area)
164}