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