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