From 5eac07a4104f5ed10a2a26d06f9219f7c11828c4 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Mon, 16 Jun 2025 15:34:57 +0200 Subject: [PATCH] feat: implement lsp errors --- .../tui/components/chat/messages/messages.go | 2 +- .../tui/components/chat/sidebar/sidebar.go | 122 +++++++++++++++++- internal/tui/components/core/helpers.go | 16 ++- internal/tui/page/chat/chat.go | 2 +- internal/tui/styles/crush.go | 2 +- internal/tui/styles/icons.go | 5 +- todos.md | 4 +- 7 files changed, 139 insertions(+), 14 deletions(-) diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index c31ca41691d7329c80d3791ac4928cb3baeb6968..51901308b6a20b65bbaa0779d8e3340675d2b1e5 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -167,7 +167,7 @@ func (m *messageCmp) renderAssistantMessage() string { case message.FinishReasonPermissionDenied: infoMsg = "permission denied" } - assistant := t.S().Muted.Render(fmt.Sprintf("⬡ %s (%s)", models.SupportedModels[m.message.Model].Name, infoMsg)) + assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg)) parts = append(parts, core.Section(assistant, m.textWidth())) } diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index 2e86f6a9750f858a10cbdba02cfcbaf439a1cdfa..9ed5c9a28a4fe65f8b23338ea259e82e5e6ef2f9 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -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() diff --git a/internal/tui/components/core/helpers.go b/internal/tui/components/core/helpers.go index e586b0563278080eb85c7e0bbaa4dbee86e670e9..3621956393a8b73f5f35f659ad9d72ebe77c53aa 100644 --- a/internal/tui/components/core/helpers.go +++ b/internal/tui/components/core/helpers.go @@ -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 { diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index fa374abb0bee72330d840b858b5219d82554b5ed..94fa4dd353043aaf76dbd44f7253574d72f4927d 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -209,7 +209,7 @@ func (p *chatPage) Bindings() []key.Binding { func NewChatPage(app *app.App) ChatPage { sidebarContainer := layout.NewContainer( - sidebar.NewSidebarCmp(), + sidebar.NewSidebarCmp(app.LSPClients), layout.WithPadding(1, 1, 1, 1), ) editorContainer := layout.NewContainer( diff --git a/internal/tui/styles/crush.go b/internal/tui/styles/crush.go index b35008c2a65e9c8b19ec456515d0a72823185c0b..41acdfad103e70b19955a84722a27876575d15b4 100644 --- a/internal/tui/styles/crush.go +++ b/internal/tui/styles/crush.go @@ -32,7 +32,7 @@ func NewCrushTheme() *Theme { // Status Success: charmtone.Guac, Error: charmtone.Sriracha, - Warning: charmtone.Uni, + Warning: charmtone.Zest, Info: charmtone.Malibu, // Colors diff --git a/internal/tui/styles/icons.go b/internal/tui/styles/icons.go index 2b02442437918adbc675bd3ff01b5e5cd71902b7..4f44077bd377fa4211bffedf88a116d7ff6f5813 100644 --- a/internal/tui/styles/icons.go +++ b/internal/tui/styles/icons.go @@ -4,11 +4,12 @@ const ( CheckIcon string = "✓" ErrorIcon string = "×" WarningIcon string = "⚠" - InfoIcon string = "" - HintIcon string = "i" + InfoIcon string = "ⓘ" + HintIcon string = "∵" SpinnerIcon string = "..." LoadingIcon string = "⟳" DocumentIcon string = "🖼" + ModelIcon string = "⬡" // Tool call icons ToolPending string = "●" diff --git a/todos.md b/todos.md index 85ce7a39c019fe86508888c1254049091ff87e2c..9f03c059deecb22d86f42074050e4e7e15038174 100644 --- a/todos.md +++ b/todos.md @@ -3,7 +3,8 @@ - [x] Implement help - [x] Show full help - [x] Make help dependent on the focused pane and page -- [ ] Implement current model in the sidebar +- [x] Implement current model in the sidebar +- [x] Implement LSP errors - [ ] Implement changed files - [ ] Events when tool error - [ ] Support bash commands @@ -23,6 +24,7 @@ - [ ] Address UX issues - [ ] Fix issue with numbers (padding) view tool - [ ] Implement responsive mode +- [ ] Update interactive mode to use the spinner - [ ] Revisit the core list component - [ ] This component has become super complex we might need to fix this. - [ ] Investigate ways to make the spinner less CPU intensive