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