on main help sign in
feat: implement lsp errors
commit5eac07a4104f5ed10a2a26d06f9219f7c11828c4 authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 16 Jun 2025, 15:34 (1 year ago) parentd9577ec
7 files changed+139−14
internal/tui/components/chat/messages/messages.go+1−1 internal/tui/components/chat/sidebar/sidebar.go+117−5 internal/tui/components/core/helpers.go+13−3 internal/tui/page/chat/chat.go+1−1 internal/tui/styles/crush.go+1−1 internal/tui/styles/icons.go+3−2 todos.md+3−1
internal/tui/components/chat/messages/messages.go+1−1
@@ -167,7 +167,7 @@ func (m *messageCmp) renderAssistantMessage() string {
167167 case message.FinishReasonPermissionDenied:
168168 infoMsg = "permission denied"
169169 }
170− assistant := t.S().Muted.Render(fmt.Sprintf("⬡ %s (%s)", models.SupportedModels[m.message.Model].Name, infoMsg))
170+ assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg))
171171 parts = append(parts, core.Section(assistant, m.textWidth()))
172172 }
173173
internal/tui/components/chat/sidebar/sidebar.go+117−5
@@ -1,11 +1,16 @@
11package sidebar
22
33import (
4+ "fmt"
45 "os"
56 "strings"
67
78 tea "github.com/charmbracelet/bubbletea/v2"
89 "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/llm/models"
11+ "github.com/charmbracelet/crush/internal/logging"
12+ "github.com/charmbracelet/crush/internal/lsp"
13+ "github.com/charmbracelet/crush/internal/lsp/protocol"
914 "github.com/charmbracelet/crush/internal/pubsub"
1015 "github.com/charmbracelet/crush/internal/session"
1116 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -32,10 +37,13 @@ type sidebarCmp struct {
3237 session session.Session
3338 logo string
3439 cwd string
40+ lspClients map[string]*lsp.Client
3541}
3642
37−func NewSidebarCmp() Sidebar {
38− return &sidebarCmp{}
43+func NewSidebarCmp(lspClients map[string]*lsp.Client) Sidebar {
44+ return &sidebarCmp{
45+ lspClients: lspClients,
46+ }
3947}
4048
4149func (m *sidebarCmp) Init() tea.Cmd {
@@ -75,6 +83,8 @@ func (m *sidebarCmp) View() tea.View {
7583 parts = append(parts,
7684 m.cwd,
7785 "",
86+ m.currentModelBlock(),
87+ "",
7888 m.lspBlock(),
7989 "",
8090 m.mcpBlock(),
@@ -137,12 +147,44 @@ func (m *sidebarCmp) lspBlock() string {
137147 if l.Disabled {
138148 iconColor = t.FgMuted
139149 }
150+ lspErrs := map[protocol.DiagnosticSeverity]int{
151+ protocol.SeverityError: 0,
152+ protocol.SeverityWarning: 0,
153+ protocol.SeverityHint: 0,
154+ protocol.SeverityInformation: 0,
155+ }
156+ if client, ok := m.lspClients[n]; ok {
157+ for _, diagnostics := range client.GetDiagnostics() {
158+ for _, diagnostic := range diagnostics {
159+ if severity, ok := lspErrs[diagnostic.Severity]; ok {
160+ lspErrs[diagnostic.Severity] = severity + 1
161+ }
162+ }
163+ }
164+ }
165+
166+ errs := []string{}
167+ if lspErrs[protocol.SeverityError] > 0 {
168+ errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s%d", styles.ErrorIcon, lspErrs[protocol.SeverityError])))
169+ }
170+ if lspErrs[protocol.SeverityWarning] > 0 {
171+ errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s%d", styles.WarningIcon, lspErrs[protocol.SeverityWarning])))
172+ }
173+ if lspErrs[protocol.SeverityHint] > 0 {
174+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.HintIcon, lspErrs[protocol.SeverityHint])))
175+ }
176+ if lspErrs[protocol.SeverityInformation] > 0 {
177+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
178+ }
179+
180+ logging.Info("LSP Errors", "errors", errs)
140181 lspList = append(lspList,
141182 core.Status(
142183 core.StatusOpts{
143− IconColor: iconColor,
144− Title: n,
145− Description: l.Command,
184+ IconColor: iconColor,
185+ Title: n,
186+ Description: l.Command,
187+ ExtraContent: strings.Join(errs, " "),
146188 },
147189 m.width,
148190 ),
@@ -195,6 +237,76 @@ func (m *sidebarCmp) mcpBlock() string {
195237 )
196238}
197239
240+func formatTokensAndCost(tokens, contextWindow int64, cost float64) string {
241+ t := styles.CurrentTheme()
242+ // Format tokens in human-readable format (e.g., 110K, 1.2M)
243+ var formattedTokens string
244+ switch {
245+ case tokens >= 1_000_000:
246+ formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
247+ case tokens >= 1_000:
248+ formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
249+ default:
250+ formattedTokens = fmt.Sprintf("%d", tokens)
251+ }
252+
253+ // Remove .0 suffix if present
254+ if strings.HasSuffix(formattedTokens, ".0K") {
255+ formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
256+ }
257+ if strings.HasSuffix(formattedTokens, ".0M") {
258+ formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
259+ }
260+
261+ percentage := (float64(tokens) / float64(contextWindow)) * 100
262+
263+ baseStyle := t.S().Base
264+
265+ formattedCost := baseStyle.Foreground(t.FgMuted).Render(fmt.Sprintf("$%.2f", cost))
266+
267+ formattedTokens = baseStyle.Foreground(t.FgMuted).Render(fmt.Sprintf("(%s)", formattedTokens))
268+ formattedPercentage := baseStyle.Foreground(t.FgSubtle).Render(fmt.Sprintf("%d%%", int(percentage)))
269+ formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
270+ if percentage > 80 {
271+ // add the warning icon
272+ formattedTokens = fmt.Sprintf("%s %s", styles.WarningIcon, formattedTokens)
273+ }
274+
275+ return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
276+}
277+
278+func (s *sidebarCmp) currentModelBlock() string {
279+ cfg := config.Get()
280+ agentCfg := cfg.Agents[config.AgentCoder]
281+ selectedModelID := agentCfg.Model
282+ model := models.SupportedModels[selectedModelID]
283+
284+ t := styles.CurrentTheme()
285+
286+ modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
287+ modelName := t.S().Text.Render(model.Name)
288+ modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
289+ parts := []string{
290+ // section,
291+ // "",
292+ modelInfo,
293+ }
294+ if s.session.ID != "" {
295+ parts = append(
296+ parts,
297+ " "+formatTokensAndCost(
298+ s.session.CompletionTokens+s.session.PromptTokens,
299+ model.ContextWindow,
300+ s.session.Cost,
301+ ),
302+ )
303+ }
304+ return lipgloss.JoinVertical(
305+ lipgloss.Left,
306+ parts...,
307+ )
308+}
309+
198310func cwd() string {
199311 cwd := config.WorkingDirectory()
200312 t := styles.CurrentTheme()
internal/tui/components/core/helpers.go+13−3
@@ -42,6 +42,7 @@ type StatusOpts struct {
4242 TitleColor color.Color
4343 Description string
4444 DescriptionColor color.Color
45+ ExtraContent string // Additional content to append after the description
4546}
4647
4748func Status(ops StatusOpts, width int) string {
@@ -67,14 +68,23 @@ func Status(ops StatusOpts, width int) string {
6768 icon = t.S().Base.Foreground(iconColor).Render(icon)
6869 title = t.S().Base.Foreground(titleColor).Render(title)
6970 if description != "" {
70− description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
71+ extraContent := len(ops.ExtraContent)
72+ if extraContent > 0 {
73+ extraContent += 1
74+ }
75+ description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContent, "…")
7176 }
7277 description = t.S().Base.Foreground(descriptionColor).Render(description)
73− return strings.Join([]string{
78+ content := []string{
7479 icon,
7580 title,
7681 description,
77− }, " ")
82+ }
83+ if ops.ExtraContent != "" {
84+ content = append(content, ops.ExtraContent)
85+ }
86+
87+ return strings.Join(content, " ")
7888}
7989
8090type ButtonOpts struct {
internal/tui/page/chat/chat.go+1−1
@@ -209,7 +209,7 @@ func (p *chatPage) Bindings() []key.Binding {
209209
210210func NewChatPage(app *app.App) ChatPage {
211211 sidebarContainer := layout.NewContainer(
212− sidebar.NewSidebarCmp(),
212+ sidebar.NewSidebarCmp(app.LSPClients),
213213 layout.WithPadding(1, 1, 1, 1),
214214 )
215215 editorContainer := layout.NewContainer(
internal/tui/styles/crush.go+1−1
@@ -32,7 +32,7 @@ func NewCrushTheme() *Theme {
3232 // Status
3333 Success: charmtone.Guac,
3434 Error: charmtone.Sriracha,
35− Warning: charmtone.Uni,
35+ Warning: charmtone.Zest,
3636 Info: charmtone.Malibu,
3737
3838 // Colors
internal/tui/styles/icons.go+3−2
@@ -4,11 +4,12 @@ const (
44 CheckIcon string = "✓"
55 ErrorIcon string = "×"
66 WarningIcon string = "⚠"
7− InfoIcon string = ""
8− HintIcon string = "i"
7+ InfoIcon string = "ⓘ"
8+ HintIcon string = "∵"
99 SpinnerIcon string = "..."
1010 LoadingIcon string = "⟳"
1111 DocumentIcon string = "🖼"
12+ ModelIcon string = "⬡"
1213
1314 // Tool call icons
1415 ToolPending string = "●"
todos.md+3−1
@@ -3,7 +3,8 @@
33- [x] Implement help
44 - [x] Show full help
55 - [x] Make help dependent on the focused pane and page
6−- [ ] Implement current model in the sidebar
6+- [x] Implement current model in the sidebar
7+- [x] Implement LSP errors
78- [ ] Implement changed files
89- [ ] Events when tool error
910- [ ] Support bash commands
@@ -23,6 +24,7 @@
2324 - [ ] Address UX issues
2425 - [ ] Fix issue with numbers (padding) view tool
2526- [ ] Implement responsive mode
27+- [ ] Update interactive mode to use the spinner
2628- [ ] Revisit the core list component
2729 - [ ] This component has become super complex we might need to fix this.
2830- [ ] Investigate ways to make the spinner less CPU intensive