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