on main help sign in
feat(lsp): show user-configured LSPs in the UI (#2192)

* feat(lsp): show user-configured LSPs in the UI This will show the user-configured LSPs as stopped in the UI. Maybe we should have a different state for "waiting"? Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * chore(lsp): mark "unstarted" LSPs as such, use named styles * fix: add unstarted state Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Christian Rocha <christian@rocha.is>

commit5179b28efd13e48cb3419fede83958930a065682 authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateWed 11 Feb 2026, 17:46 (8 months ago) parentbaedc28
6 files changed+79−47
internal/app/app.go+5 internal/lsp/client.go+2−1 internal/lsp/manager.go+18 internal/ui/model/lsp.go+25−25 internal/ui/model/mcp.go+13−13 internal/ui/styles/styles.go+16−8
internal/app/app.go+5
@@ -126,9 +126,14 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
126126
127127 // Set up callback for LSP state updates.
128128 app.LSPManager.SetCallback(func(name string, client *lsp.Client) {
129+ if client == nil {
130+ updateLSPState(name, lsp.StateUnstarted, nil, nil, 0)
131+ return
132+ }
129133 client.SetDiagnosticsCallback(updateLSPDiagnostics)
130134 updateLSPState(name, client.GetServerState(), nil, client, 0)
131135 })
136+ go app.LSPManager.TrackConfigured()
132137
133138 return app, nil
134139}
internal/lsp/client.go+2−1
@@ -241,10 +241,11 @@ func (c *Client) Restart() error {
241241type ServerState int
242242
243243const (
244− StateStopped ServerState = iota
244+ StateUnstarted ServerState = iota
245245 StateStarting
246246 StateReady
247247 StateError
248+ StateStopped
248249 StateDisabled
249250)
250251
internal/lsp/manager.go+18
@@ -77,6 +77,24 @@ func (s *Manager) SetCallback(cb func(name string, client *Client)) {
7777 s.callback = cb
7878}
7979
80+// TrackConfigured will callback the user-configured LSPs, but will not create
81+// any clients.
82+func (s *Manager) TrackConfigured() {
83+ s.mu.Lock()
84+ defer s.mu.Unlock()
85+
86+ var wg sync.WaitGroup
87+ for name := range s.manager.GetServers() {
88+ if !s.isUserConfigured(name) {
89+ continue
90+ }
91+ wg.Go(func() {
92+ s.callback(name, nil)
93+ })
94+ }
95+ wg.Wait()
96+}
97+
8098// Start starts an LSP server that can handle the given file path.
8199// If an appropriate LSP is already running, this is a no-op.
82100func (s *Manager) Start(ctx context.Context, path string) {
internal/ui/model/lsp.go+25−25
@@ -31,26 +31,23 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
3131
3232 var lsps []LSPInfo
3333 for _, state := range states {
34− client, ok := m.com.App.LSPManager.Clients().Get(state.Name)
35− if !ok {
36− continue
37− }
38− counts := client.GetDiagnosticCounts()
39− lspErrs := map[protocol.DiagnosticSeverity]int{
40− protocol.SeverityError: counts.Error,
41− protocol.SeverityWarning: counts.Warning,
42− protocol.SeverityHint: counts.Hint,
43− protocol.SeverityInformation: counts.Information,
34+ lspErrs := map[protocol.DiagnosticSeverity]int{}
35+ if client, ok := m.com.App.LSPManager.Clients().Get(state.Name); ok {
36+ counts := client.GetDiagnosticCounts()
37+ lspErrs[protocol.SeverityError] = counts.Error
38+ lspErrs[protocol.SeverityWarning] = counts.Warning
39+ lspErrs[protocol.SeverityHint] = counts.Hint
40+ lspErrs[protocol.SeverityInformation] = counts.Information
4441 }
4542
4643 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
4744 }
4845
49− title := t.Subtle.Render("LSPs")
46+ title := t.ResourceGroupTitle.Render("LSPs")
5047 if isSection {
5148 title = common.Section(t, title, width)
5249 }
53− list := t.Subtle.Render("None")
50+ list := t.ResourceAdditionalText.Render("None")
5451 if len(lsps) > 0 {
5552 list = lspList(t, lsps, width, maxItems)
5653 }
@@ -85,30 +82,33 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
8582 var renderedLsps []string
8683 for _, l := range lsps {
8784 var icon string
88− title := l.Name
85+ title := t.ResourceName.Render(l.Name)
8986 var description string
9087 var diagnostics string
9188 switch l.State {
89+ case lsp.StateUnstarted:
90+ icon = t.ResourceOfflineIcon.String()
91+ description = t.ResourceStatus.Render("unstarted")
9292 case lsp.StateStopped:
93− icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
94− description = t.Subtle.Render("stopped")
93+ icon = t.ResourceOfflineIcon.String()
94+ description = t.ResourceStatus.Render("stopped")
9595 case lsp.StateStarting:
96− icon = t.ItemBusyIcon.String()
97− description = t.Subtle.Render("starting...")
96+ icon = t.ResourceBusyIcon.String()
97+ description = t.ResourceStatus.Render("starting...")
9898 case lsp.StateReady:
99− icon = t.ItemOnlineIcon.String()
99+ icon = t.ResourceOnlineIcon.String()
100100 diagnostics = lspDiagnostics(t, l.Diagnostics)
101101 case lsp.StateError:
102− icon = t.ItemErrorIcon.String()
103− description = t.Subtle.Render("error")
102+ icon = t.ResourceErrorIcon.String()
103+ description = t.ResourceStatus.Render("error")
104104 if l.Error != nil {
105− description = t.Subtle.Render(fmt.Sprintf("error: %s", l.Error.Error()))
105+ description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", l.Error.Error()))
106106 }
107107 case lsp.StateDisabled:
108− icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
109− description = t.Subtle.Render("disabled")
108+ icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
109+ description = t.ResourceStatus.Render("disabled")
110110 default:
111− icon = t.ItemOfflineIcon.String()
111+ icon = t.ResourceOfflineIcon.String()
112112 }
113113 renderedLsps = append(renderedLsps, common.Status(t, common.StatusOpts{
114114 Icon: icon,
@@ -121,7 +121,7 @@ func lspList(t *styles.Styles, lsps []LSPInfo, width, maxItems int) string {
121121 if len(renderedLsps) > maxItems {
122122 visibleItems := renderedLsps[:maxItems-1]
123123 remaining := len(renderedLsps) - maxItems
124− visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
124+ visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
125125 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
126126 }
127127 return lipgloss.JoinVertical(lipgloss.Left, renderedLsps...)
internal/ui/model/mcp.go+13−13
@@ -22,11 +22,11 @@ func (m *UI) mcpInfo(width, maxItems int, isSection bool) string {
2222 }
2323 }
2424
25− title := t.Subtle.Render("MCPs")
25+ title := t.ResourceGroupTitle.Render("MCPs")
2626 if isSection {
2727 title = common.Section(t, title, width)
2828 }
29− list := t.Subtle.Render("None")
29+ list := t.ResourceAdditionalText.Render("None")
3030 if len(mcps) > 0 {
3131 list = mcpList(t, mcps, width, maxItems)
3232 }
@@ -59,28 +59,28 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
5959
6060 for _, m := range mcps {
6161 var icon string
62− title := m.Name
62+ title := t.ResourceName.Render(m.Name)
6363 var description string
6464 var extraContent string
6565
6666 switch m.State {
6767 case mcp.StateStarting:
68− icon = t.ItemBusyIcon.String()
69− description = t.Subtle.Render("starting...")
68+ icon = t.ResourceBusyIcon.String()
69+ description = t.ResourceStatus.Render("starting...")
7070 case mcp.StateConnected:
71− icon = t.ItemOnlineIcon.String()
71+ icon = t.ResourceOnlineIcon.String()
7272 extraContent = mcpCounts(t, m.Counts)
7373 case mcp.StateError:
74− icon = t.ItemErrorIcon.String()
75− description = t.Subtle.Render("error")
74+ icon = t.ResourceErrorIcon.String()
75+ description = t.ResourceStatus.Render("error")
7676 if m.Error != nil {
77− description = t.Subtle.Render(fmt.Sprintf("error: %s", m.Error.Error()))
77+ description = t.ResourceStatus.Render(fmt.Sprintf("error: %s", m.Error.Error()))
7878 }
7979 case mcp.StateDisabled:
80− icon = t.ItemOfflineIcon.Foreground(t.Muted.GetBackground()).String()
81− description = t.Subtle.Render("disabled")
80+ icon = t.ResourceOfflineIcon.Foreground(t.Muted.GetBackground()).String()
81+ description = t.ResourceStatus.Render("disabled")
8282 default:
83− icon = t.ItemOfflineIcon.String()
83+ icon = t.ResourceOfflineIcon.String()
8484 }
8585
8686 renderedMcps = append(renderedMcps, common.Status(t, common.StatusOpts{
@@ -94,7 +94,7 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin
9494 if len(renderedMcps) > maxItems {
9595 visibleItems := renderedMcps[:maxItems-1]
9696 remaining := len(renderedMcps) - maxItems
97− visibleItems = append(visibleItems, t.Subtle.Render(fmt.Sprintf("…and %d more", remaining)))
97+ visibleItems = append(visibleItems, t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)))
9898 return lipgloss.JoinVertical(lipgloss.Left, visibleItems...)
9999 }
100100 return lipgloss.JoinVertical(lipgloss.Left, renderedMcps...)
internal/ui/styles/styles.go+16−8
@@ -109,10 +109,14 @@ type Styles struct {
109109 TextSelection lipgloss.Style
110110
111111 // LSP and MCP status indicators
112− ItemOfflineIcon lipgloss.Style
113− ItemBusyIcon lipgloss.Style
114− ItemErrorIcon lipgloss.Style
115− ItemOnlineIcon lipgloss.Style
112+ ResourceGroupTitle lipgloss.Style
113+ ResourceOfflineIcon lipgloss.Style
114+ ResourceBusyIcon lipgloss.Style
115+ ResourceErrorIcon lipgloss.Style
116+ ResourceOnlineIcon lipgloss.Style
117+ ResourceName lipgloss.Style
118+ ResourceStatus lipgloss.Style
119+ ResourceAdditionalText lipgloss.Style
116120
117121 // Markdown & Chroma
118122 Markdown ansi.StyleConfig
@@ -1199,10 +1203,14 @@ func DefaultStyles() Styles {
11991203 s.Initialize.Accent = s.Base.Foreground(greenDark)
12001204
12011205 // LSP and MCP status.
1202− s.ItemOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Squid).SetString("●")
1203− s.ItemBusyIcon = s.ItemOfflineIcon.Foreground(charmtone.Citron)
1204− s.ItemErrorIcon = s.ItemOfflineIcon.Foreground(charmtone.Coral)
1205− s.ItemOnlineIcon = s.ItemOfflineIcon.Foreground(charmtone.Guac)
1206+ s.ResourceGroupTitle = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1207+ s.ResourceOfflineIcon = lipgloss.NewStyle().Foreground(charmtone.Iron).SetString("●")
1208+ s.ResourceBusyIcon = s.ResourceOfflineIcon.Foreground(charmtone.Citron)
1209+ s.ResourceErrorIcon = s.ResourceOfflineIcon.Foreground(charmtone.Coral)
1210+ s.ResourceOnlineIcon = s.ResourceOfflineIcon.Foreground(charmtone.Guac)
1211+ s.ResourceName = lipgloss.NewStyle().Foreground(charmtone.Squid)
1212+ s.ResourceStatus = lipgloss.NewStyle().Foreground(charmtone.Oyster)
1213+ s.ResourceAdditionalText = lipgloss.NewStyle().Foreground(charmtone.Oyster)
12061214
12071215 // LSP
12081216 s.LSP.ErrorDiagnostic = s.Base.Foreground(redDark)