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	if m.session == nil {
100		return
101	}
102
103	const logoHeightBreakpoint = 30
104
105	t := m.com.Styles
106	width := area.Dx()
107	height := area.Dy()
108
109	title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
110	cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width)
111	sidebarLogo := m.sidebarLogo
112	if height < logoHeightBreakpoint {
113		sidebarLogo = logo.SmallRender(width)
114	}
115	blocks := []string{
116		sidebarLogo,
117		title,
118		"",
119		cwd,
120		"",
121		m.modelInfo(width),
122		"",
123	}
124
125	sidebarHeader := lipgloss.JoinVertical(
126		lipgloss.Left,
127		blocks...,
128	)
129
130	_, remainingHeightArea := uv.SplitVertical(m.layout.sidebar, uv.Fixed(lipgloss.Height(sidebarHeader)))
131	remainingHeight := remainingHeightArea.Dy() - 10
132	maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
133
134	lspSection := m.lspInfo(width, maxLSPs, true)
135	mcpSection := m.mcpInfo(width, maxMCPs, true)
136	filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles)
137
138	uv.NewStyledString(
139		lipgloss.NewStyle().
140			MaxWidth(width).
141			MaxHeight(height).
142			Render(
143				lipgloss.JoinVertical(
144					lipgloss.Left,
145					sidebarHeader,
146					filesSection,
147					"",
148					lspSection,
149					"",
150					mcpSection,
151				),
152			),
153	).Draw(scr, area)
154}