@@ -1,11 +1,16 @@
package sidebar
import (
+ "fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/config"
+ "github.com/charmbracelet/crush/internal/llm/models"
+ "github.com/charmbracelet/crush/internal/logging"
+ "github.com/charmbracelet/crush/internal/lsp"
+ "github.com/charmbracelet/crush/internal/lsp/protocol"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -32,10 +37,13 @@ type sidebarCmp struct {
session session.Session
logo string
cwd string
+ lspClients map[string]*lsp.Client
}
-func NewSidebarCmp() Sidebar {
- return &sidebarCmp{}
+func NewSidebarCmp(lspClients map[string]*lsp.Client) Sidebar {
+ return &sidebarCmp{
+ lspClients: lspClients,
+ }
}
func (m *sidebarCmp) Init() tea.Cmd {
@@ -75,6 +83,8 @@ func (m *sidebarCmp) View() tea.View {
parts = append(parts,
m.cwd,
"",
+ m.currentModelBlock(),
+ "",
m.lspBlock(),
"",
m.mcpBlock(),
@@ -137,12 +147,44 @@ func (m *sidebarCmp) lspBlock() string {
if l.Disabled {
iconColor = t.FgMuted
}
+ lspErrs := map[protocol.DiagnosticSeverity]int{
+ protocol.SeverityError: 0,
+ protocol.SeverityWarning: 0,
+ protocol.SeverityHint: 0,
+ protocol.SeverityInformation: 0,
+ }
+ if client, ok := m.lspClients[n]; ok {
+ for _, diagnostics := range client.GetDiagnostics() {
+ for _, diagnostic := range diagnostics {
+ if severity, ok := lspErrs[diagnostic.Severity]; ok {
+ lspErrs[diagnostic.Severity] = severity + 1
+ }
+ }
+ }
+ }
+
+ errs := []string{}
+ if lspErrs[protocol.SeverityError] > 0 {
+ errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s%d", styles.ErrorIcon, lspErrs[protocol.SeverityError])))
+ }
+ if lspErrs[protocol.SeverityWarning] > 0 {
+ errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s%d", styles.WarningIcon, lspErrs[protocol.SeverityWarning])))
+ }
+ if lspErrs[protocol.SeverityHint] > 0 {
+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.HintIcon, lspErrs[protocol.SeverityHint])))
+ }
+ if lspErrs[protocol.SeverityInformation] > 0 {
+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
+ }
+
+ logging.Info("LSP Errors", "errors", errs)
lspList = append(lspList,
core.Status(
core.StatusOpts{
- IconColor: iconColor,
- Title: n,
- Description: l.Command,
+ IconColor: iconColor,
+ Title: n,
+ Description: l.Command,
+ ExtraContent: strings.Join(errs, " "),
},
m.width,
),
@@ -195,6 +237,76 @@ func (m *sidebarCmp) mcpBlock() string {
)
}
+func formatTokensAndCost(tokens, contextWindow int64, cost float64) string {
+ t := styles.CurrentTheme()
+ // Format tokens in human-readable format (e.g., 110K, 1.2M)
+ var formattedTokens string
+ switch {
+ case tokens >= 1_000_000:
+ formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
+ case tokens >= 1_000:
+ formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
+ default:
+ formattedTokens = fmt.Sprintf("%d", tokens)
+ }
+
+ // Remove .0 suffix if present
+ if strings.HasSuffix(formattedTokens, ".0K") {
+ formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
+ }
+ if strings.HasSuffix(formattedTokens, ".0M") {
+ formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
+ }
+
+ percentage := (float64(tokens) / float64(contextWindow)) * 100
+
+ baseStyle := t.S().Base
+
+ formattedCost := baseStyle.Foreground(t.FgMuted).Render(fmt.Sprintf("$%.2f", cost))
+
+ formattedTokens = baseStyle.Foreground(t.FgMuted).Render(fmt.Sprintf("(%s)", formattedTokens))
+ formattedPercentage := baseStyle.Foreground(t.FgSubtle).Render(fmt.Sprintf("%d%%", int(percentage)))
+ formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
+ if percentage > 80 {
+ // add the warning icon
+ formattedTokens = fmt.Sprintf("%s %s", styles.WarningIcon, formattedTokens)
+ }
+
+ return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
+}
+
+func (s *sidebarCmp) currentModelBlock() string {
+ cfg := config.Get()
+ agentCfg := cfg.Agents[config.AgentCoder]
+ selectedModelID := agentCfg.Model
+ model := models.SupportedModels[selectedModelID]
+
+ t := styles.CurrentTheme()
+
+ modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
+ modelName := t.S().Text.Render(model.Name)
+ modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
+ parts := []string{
+ // section,
+ // "",
+ modelInfo,
+ }
+ if s.session.ID != "" {
+ parts = append(
+ parts,
+ " "+formatTokensAndCost(
+ s.session.CompletionTokens+s.session.PromptTokens,
+ model.ContextWindow,
+ s.session.Cost,
+ ),
+ )
+ }
+ return lipgloss.JoinVertical(
+ lipgloss.Left,
+ parts...,
+ )
+}
+
func cwd() string {
cwd := config.WorkingDirectory()
t := styles.CurrentTheme()
@@ -42,6 +42,7 @@ type StatusOpts struct {
TitleColor color.Color
Description string
DescriptionColor color.Color
+ ExtraContent string // Additional content to append after the description
}
func Status(ops StatusOpts, width int) string {
@@ -67,14 +68,23 @@ func Status(ops StatusOpts, width int) string {
icon = t.S().Base.Foreground(iconColor).Render(icon)
title = t.S().Base.Foreground(titleColor).Render(title)
if description != "" {
- description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
+ extraContent := len(ops.ExtraContent)
+ if extraContent > 0 {
+ extraContent += 1
+ }
+ description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContent, "…")
}
description = t.S().Base.Foreground(descriptionColor).Render(description)
- return strings.Join([]string{
+ content := []string{
icon,
title,
description,
- }, " ")
+ }
+ if ops.ExtraContent != "" {
+ content = append(content, ops.ExtraContent)
+ }
+
+ return strings.Join(content, " ")
}
type ButtonOpts struct {