| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- package model
- import (
- "cmp"
- "fmt"
- "charm.land/lipgloss/v2"
- "github.com/charmbracelet/crush/internal/ui/common"
- "github.com/charmbracelet/crush/internal/ui/logo"
- uv "github.com/charmbracelet/ultraviolet"
- "github.com/charmbracelet/ultraviolet/layout"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
- )
- // modelInfo renders the current model information including reasoning
- // settings and context usage/cost for the sidebar.
- func (m *UI) modelInfo(width int) string {
- model := m.selectedLargeModel()
- reasoningInfo := ""
- providerName := ""
- if model != nil {
- // Get provider name first
- providerConfig, ok := m.com.Config().Providers.Get(model.ModelCfg.Provider)
- if ok {
- providerName = providerConfig.Name
- // Only check reasoning if model can reason
- if model.CatwalkCfg.CanReason {
- if len(model.CatwalkCfg.ReasoningLevels) == 0 {
- if model.ModelCfg.Think {
- reasoningInfo = "Thinking On"
- } else {
- reasoningInfo = "Thinking Off"
- }
- } else {
- formatter := cases.Title(language.English, cases.NoLower)
- reasoningEffort := cmp.Or(model.ModelCfg.ReasoningEffort, model.CatwalkCfg.DefaultReasoningEffort)
- reasoningInfo = formatter.String(fmt.Sprintf("Reasoning %s", reasoningEffort))
- }
- }
- }
- }
- var modelContext *common.ModelContextInfo
- if model != nil && m.session != nil {
- modelContext = &common.ModelContextInfo{
- ContextUsed: m.session.CompletionTokens + m.session.PromptTokens,
- Cost: m.session.Cost,
- ModelContext: model.CatwalkCfg.ContextWindow,
- }
- }
- return common.ModelInfo(m.com.Styles, model.CatwalkCfg.Name, providerName, reasoningInfo, modelContext, width)
- }
- // getDynamicHeightLimits will give us the num of items to show in each section based on the hight
- // some items are more important than others.
- func getDynamicHeightLimits(availableHeight int) (maxFiles, maxLSPs, maxMCPs int) {
- const (
- minItemsPerSection = 2
- defaultMaxFilesShown = 10
- defaultMaxLSPsShown = 8
- defaultMaxMCPsShown = 8
- minAvailableHeightLimit = 10
- )
- // If we have very little space, use minimum values
- if availableHeight < minAvailableHeightLimit {
- return minItemsPerSection, minItemsPerSection, minItemsPerSection
- }
- // Distribute available height among the three sections
- // Give priority to files, then LSPs, then MCPs
- totalSections := 3
- heightPerSection := availableHeight / totalSections
- // Calculate limits for each section, ensuring minimums
- maxFiles = max(minItemsPerSection, min(defaultMaxFilesShown, heightPerSection))
- maxLSPs = max(minItemsPerSection, min(defaultMaxLSPsShown, heightPerSection))
- maxMCPs = max(minItemsPerSection, min(defaultMaxMCPsShown, heightPerSection))
- // If we have extra space, give it to files first
- remainingHeight := availableHeight - (maxFiles + maxLSPs + maxMCPs)
- if remainingHeight > 0 {
- extraForFiles := min(remainingHeight, defaultMaxFilesShown-maxFiles)
- maxFiles += extraForFiles
- remainingHeight -= extraForFiles
- if remainingHeight > 0 {
- extraForLSPs := min(remainingHeight, defaultMaxLSPsShown-maxLSPs)
- maxLSPs += extraForLSPs
- remainingHeight -= extraForLSPs
- if remainingHeight > 0 {
- maxMCPs += min(remainingHeight, defaultMaxMCPsShown-maxMCPs)
- }
- }
- }
- return maxFiles, maxLSPs, maxMCPs
- }
- // sidebar renders the chat sidebar containing session title, working
- // directory, model info, file list, LSP status, and MCP status.
- func (m *UI) drawSidebar(scr uv.Screen, area uv.Rectangle) {
- if m.session == nil {
- return
- }
- const logoHeightBreakpoint = 30
- t := m.com.Styles
- width := area.Dx()
- height := area.Dy()
- title := t.Muted.Width(width).MaxHeight(2).Render(m.session.Title)
- cwd := common.PrettyPath(t, m.com.Config().WorkingDir(), width)
- sidebarLogo := m.sidebarLogo
- if height < logoHeightBreakpoint {
- sidebarLogo = logo.SmallRender(m.com.Styles, width)
- }
- blocks := []string{
- sidebarLogo,
- title,
- "",
- cwd,
- "",
- m.modelInfo(width),
- "",
- }
- sidebarHeader := lipgloss.JoinVertical(
- lipgloss.Left,
- blocks...,
- )
- _, remainingHeightArea := layout.SplitVertical(m.layout.sidebar, layout.Fixed(lipgloss.Height(sidebarHeader)))
- remainingHeight := remainingHeightArea.Dy() - 10
- maxFiles, maxLSPs, maxMCPs := getDynamicHeightLimits(remainingHeight)
- lspSection := m.lspInfo(width, maxLSPs, true)
- mcpSection := m.mcpInfo(width, maxMCPs, true)
- filesSection := m.filesInfo(m.com.Config().WorkingDir(), width, maxFiles, true)
- uv.NewStyledString(
- lipgloss.NewStyle().
- MaxWidth(width).
- MaxHeight(height).
- Render(
- lipgloss.JoinVertical(
- lipgloss.Left,
- sidebarHeader,
- filesSection,
- "",
- lspSection,
- "",
- mcpSection,
- ),
- ),
- ).Draw(scr, area)
- }
|