on main help sign in
feat: implement lsp errors
5eac07aKujtim Hoxha
internal/tui/components/chat/messages/messages.go+1−1
@@ -167,7 +167,7 @@ func (m *messageCmp) renderAssistantMessage() string {
167 case message.FinishReasonPermissionDenied:
167 case message.FinishReasonPermissionDenied:
168 infoMsg = "permission denied"
168 infoMsg = "permission denied"
169 }
169 }
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))
171 parts = append(parts, core.Section(assistant, m.textWidth()))
171 parts = append(parts, core.Section(assistant, m.textWidth()))
172 }
172 }
173
173
internal/tui/components/chat/sidebar/sidebar.go+117−5
@@ -1,11 +1,16 @@
1package sidebar
1package sidebar
2
2
3import (
3import (
4+ "fmt"
4 "os"
5 "os"
5 "strings"
6 "strings"
6
7
7 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/config"
9 "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"
9 "github.com/charmbracelet/crush/internal/pubsub"
14 "github.com/charmbracelet/crush/internal/pubsub"
10 "github.com/charmbracelet/crush/internal/session"
15 "github.com/charmbracelet/crush/internal/session"
11 "github.com/charmbracelet/crush/internal/tui/components/chat"
16 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -32,10 +37,13 @@ type sidebarCmp struct {
32 session session.Session
37 session session.Session
33 logo string
38 logo string
34 cwd string
39 cwd string
40+ lspClients map[string]*lsp.Client
35}
41}
36
42
37−func NewSidebarCmp() Sidebar {
43+func NewSidebarCmp(lspClients map[string]*lsp.Client) Sidebar {
38− return &sidebarCmp{}
44+ return &sidebarCmp{
45+ lspClients: lspClients,
46+ }
39}
47}
40
48
41func (m *sidebarCmp) Init() tea.Cmd {
49func (m *sidebarCmp) Init() tea.Cmd {
@@ -75,6 +83,8 @@ func (m *sidebarCmp) View() tea.View {
75 parts = append(parts,
83 parts = append(parts,
76 m.cwd,
84 m.cwd,
77 "",
85 "",
86+ m.currentModelBlock(),
87+ "",
78 m.lspBlock(),
88 m.lspBlock(),
79 "",
89 "",
80 m.mcpBlock(),
90 m.mcpBlock(),
@@ -137,12 +147,44 @@ func (m *sidebarCmp) lspBlock() string {
137 if l.Disabled {
147 if l.Disabled {
138 iconColor = t.FgMuted
148 iconColor = t.FgMuted
139 }
149 }
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)
140 lspList = append(lspList,
181 lspList = append(lspList,
141 core.Status(
182 core.Status(
142 core.StatusOpts{
183 core.StatusOpts{
143− IconColor: iconColor,
184+ IconColor: iconColor,
144− Title: n,
185+ Title: n,
145− Description: l.Command,
186+ Description: l.Command,
187+ ExtraContent: strings.Join(errs, " "),
146 },
188 },
147 m.width,
189 m.width,
148 ),
190 ),
@@ -195,6 +237,76 @@ func (m *sidebarCmp) mcpBlock() string {
195 )
237 )
196}
238}
197
239
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+
198func cwd() string {
310func cwd() string {
199 cwd := config.WorkingDirectory()
311 cwd := config.WorkingDirectory()
200 t := styles.CurrentTheme()
312 t := styles.CurrentTheme()
internal/tui/components/core/helpers.go+13−3
@@ -42,6 +42,7 @@ type StatusOpts struct {
42 TitleColor color.Color
42 TitleColor color.Color
43 Description string
43 Description string
44 DescriptionColor color.Color
44 DescriptionColor color.Color
45+ ExtraContent string // Additional content to append after the description
45}
46}
46
47
47func Status(ops StatusOpts, width int) string {
48func Status(ops StatusOpts, width int) string {
@@ -67,14 +68,23 @@ func Status(ops StatusOpts, width int) string {
67 icon = t.S().Base.Foreground(iconColor).Render(icon)
68 icon = t.S().Base.Foreground(iconColor).Render(icon)
68 title = t.S().Base.Foreground(titleColor).Render(title)
69 title = t.S().Base.Foreground(titleColor).Render(title)
69 if description != "" {
70 if description != "" {
71+ extraContent := len(ops.ExtraContent)
72+ if extraContent > 0 {
73+ extraContent += 1
74+ }
70− description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
75+ description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContent, "…")
71 }
76 }
72 description = t.S().Base.Foreground(descriptionColor).Render(description)
77 description = t.S().Base.Foreground(descriptionColor).Render(description)
73− return strings.Join([]string{
78+ content := []string{
74 icon,
79 icon,
75 title,
80 title,
76 description,
81 description,
77− }, " ")
82+ }
83+ if ops.ExtraContent != "" {
84+ content = append(content, ops.ExtraContent)
85+ }
86+
87+ return strings.Join(content, " ")
78}
88}
79
89
80type ButtonOpts struct {
90type ButtonOpts struct {
internal/tui/page/chat/chat.go+1−1
@@ -209,7 +209,7 @@ func (p *chatPage) Bindings() []key.Binding {
209
209
210func NewChatPage(app *app.App) ChatPage {
210func NewChatPage(app *app.App) ChatPage {
211 sidebarContainer := layout.NewContainer(
211 sidebarContainer := layout.NewContainer(
212− sidebar.NewSidebarCmp(),
212+ sidebar.NewSidebarCmp(app.LSPClients),
213 layout.WithPadding(1, 1, 1, 1),
213 layout.WithPadding(1, 1, 1, 1),
214 )
214 )
215 editorContainer := layout.NewContainer(
215 editorContainer := layout.NewContainer(
internal/tui/styles/crush.go+1−1
@@ -32,7 +32,7 @@ func NewCrushTheme() *Theme {
32 // Status
32 // Status
33 Success: charmtone.Guac,
33 Success: charmtone.Guac,
34 Error: charmtone.Sriracha,
34 Error: charmtone.Sriracha,
35− Warning: charmtone.Uni,
35+ Warning: charmtone.Zest,
36 Info: charmtone.Malibu,
36 Info: charmtone.Malibu,
37
37
38 // Colors
38 // Colors
internal/tui/styles/icons.go+3−2
@@ -4,11 +4,12 @@ const (
4 CheckIcon string = "✓"
4 CheckIcon string = "✓"
5 ErrorIcon string = "×"
5 ErrorIcon string = "×"
6 WarningIcon string = "⚠"
6 WarningIcon string = "⚠"
7− InfoIcon string = ""
7+ InfoIcon string = "ⓘ"
8− HintIcon string = "i"
8+ HintIcon string = "∵"
9 SpinnerIcon string = "..."
9 SpinnerIcon string = "..."
10 LoadingIcon string = "⟳"
10 LoadingIcon string = "⟳"
11 DocumentIcon string = "🖼"
11 DocumentIcon string = "🖼"
12+ ModelIcon string = "⬡"
12
13
13 // Tool call icons
14 // Tool call icons
14 ToolPending string = "●"
15 ToolPending string = "●"
todos.md+3−1
@@ -3,7 +3,8 @@
3- [x] Implement help
3- [x] Implement help
4 - [x] Show full help
4 - [x] Show full help
5 - [x] Make help dependent on the focused pane and page
5 - [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
7- [ ] Implement changed files
8- [ ] Implement changed files
8- [ ] Events when tool error
9- [ ] Events when tool error
9- [ ] Support bash commands
10- [ ] Support bash commands
@@ -23,6 +24,7 @@
23 - [ ] Address UX issues
24 - [ ] Address UX issues
24 - [ ] Fix issue with numbers (padding) view tool
25 - [ ] Fix issue with numbers (padding) view tool
25- [ ] Implement responsive mode
26- [ ] Implement responsive mode
27+- [ ] Update interactive mode to use the spinner
26- [ ] Revisit the core list component
28- [ ] Revisit the core list component
27 - [ ] This component has become super complex we might need to fix this.
29 - [ ] This component has become super complex we might need to fix this.
28- [ ] Investigate ways to make the spinner less CPU intensive
30- [ ] Investigate ways to make the spinner less CPU intensive