@mrclrchtr/supi-code-intelligence 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  4. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-lsp/README.md +5 -1
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-jsonrpc/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.d.ts +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.d.ts +2 -2
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.d.ts +9 -9
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/package.json +3 -3
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.d.ts +2 -2
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.js +1 -1
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.d.ts +2 -2
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.js +1 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/package.json +1 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  20. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +2 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +6 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +8 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +1 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  25. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +582 -26
  26. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +2 -3
  27. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +10 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +36 -9
  29. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +2 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +4 -2
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  37. package/package.json +5 -5
  38. package/src/analysis/health/recovery.ts +4 -0
  39. package/src/analysis/health/server-status.ts +64 -0
  40. package/src/analysis/health/signals.ts +1 -0
  41. package/src/analysis/provider.ts +2 -1
  42. package/src/analysis/target/symbol.ts +28 -16
  43. package/src/analysis/target/types.ts +1 -0
  44. package/src/session/find-workflow.ts +5 -12
  45. package/src/session/health-refresh.ts +6 -2
  46. package/src/session/health-types.ts +8 -2
  47. package/src/session/health-workflow.ts +5 -1
  48. package/src/session/semantic-demand.ts +36 -0
  49. package/src/session/target-workflow.ts +9 -12
  50. package/src/tool/code_health/guidance.ts +2 -2
  51. package/src/tool/code_health/markdown.ts +15 -3
  52. package/src/tool/code_health/result.ts +14 -5
  53. package/src/tool/code_health/spec.ts +2 -1
  54. package/src/tool/code_health/tui.ts +24 -3
  55. package/src/tool/result/errors.ts +1 -0
  56. package/src/tool/result/types.ts +6 -0
  57. package/src/ui/footer.ts +7 -5
  58. package/src/ui/status-command.ts +31 -0
  59. package/src/ui/status-overlay.ts +69 -23
@@ -23,6 +23,7 @@ import type {
23
23
  } from "../analysis/target/types.ts";
24
24
  import type { CapabilityAdapter } from "./capability-adapter.ts";
25
25
  import { parseTargetInput } from "./input/common.ts";
26
+ import { resolveWorkspaceSemanticDemand } from "./semantic-demand.ts";
26
27
  import { registerTargetCandidates, type TargetWorkflowCandidate } from "./target-candidates.ts";
27
28
  import type { TargetInput, TargetSymbolKind } from "./target-input.ts";
28
29
  import {
@@ -201,22 +202,15 @@ async function resolveSymbolWorkflow(
201
202
  scope = resolved.path;
202
203
  }
203
204
 
204
- const readiness = await deps.capability.ensureSemanticReadiness(
205
- deps.cwd,
206
- { kind: "workspace" },
207
- control,
208
- );
209
- if (readiness.kind === "timeout") {
205
+ const semanticDemand = await resolveWorkspaceSemanticDemand(deps.capability, deps.cwd, control);
206
+ if (semanticDemand.kind === "timeout") {
210
207
  return { kind: "unavailable", reason: "LSP readiness timed out. Retry shortly." };
211
208
  }
212
- if (readiness.kind === "unavailable") {
213
- return { kind: "unavailable", reason: readiness.reason };
209
+ if (semanticDemand.kind === "unavailable") {
210
+ return { kind: "unavailable", reason: semanticDemand.reason };
214
211
  }
215
212
 
216
- const provider = withSemanticRequestControl(
217
- deps.capability.getSemanticProvider(deps.cwd),
218
- control,
219
- );
213
+ const provider = withSemanticRequestControl(semanticDemand.provider, control);
220
214
  if (!provider) {
221
215
  return {
222
216
  kind: "unavailable",
@@ -291,6 +285,9 @@ function toWorkflowOutcome(
291
285
  policy: TargetWorkflowPolicy,
292
286
  deps: TargetWorkflowDeps,
293
287
  ): TargetWorkflowOutcome {
288
+ if (outcome.kind === "unavailable") {
289
+ return { kind: "unavailable", reason: outcome.reason };
290
+ }
294
291
  if (outcome.kind === "error") {
295
292
  return { kind: "invalid-input", message: outcome.message };
296
293
  }
@@ -1,8 +1,8 @@
1
1
  export const toolDescription =
2
- "Report live diagnostics as observations, language-server status, and final semantic health state. Tracked-file snapshots do not prove workspace completeness; server inventory is workspace-wide.";
2
+ "Report live diagnostics as observations, language-server route status, and final semantic health state. Tracked-file snapshots do not prove workspace completeness; server inventory and route issue counts are workspace-wide. Server-only calls are passive.";
3
3
 
4
4
  export const promptSnippet = "live workspace health observations";
5
5
 
6
6
  export const promptGuidelines = [
7
- "Use code_health with refresh:true before relying on potentially stale diagnostics.",
7
+ "Use code_health with refresh:true before relying on potentially stale diagnostics; explicit refresh can recover crashed routes with tracked files in scope.",
8
8
  ];
@@ -6,6 +6,11 @@
6
6
  */
7
7
 
8
8
  import type { CapabilityWarningReport } from "../../analysis/capability/capability-warnings.ts";
9
+ import {
10
+ formatProjectServerRoot,
11
+ formatProjectServerRouteSummary,
12
+ formatProjectServerStatusReason,
13
+ } from "../../analysis/health/server-status.ts";
9
14
  import type {
10
15
  HealthData,
11
16
  HealthDiagnosticObservation,
@@ -28,7 +33,9 @@ export function renderHealthResult(result: HealthResultAssembly, cwd: string): s
28
33
  renderRefreshStatus(lines, data, hasDiagnostics, cwd);
29
34
  if (hasDiagnostics) renderDiagnosticsSection(lines, data, cwd);
30
35
  if (semanticRequested) renderCapabilityWarningsSection(lines, result.details.capabilityWarnings);
31
- if (hasSection("servers")) renderServersSection(lines, data, sectionStatus(result, "servers"));
36
+ if (hasSection("servers")) {
37
+ renderServersSection(lines, data, sectionStatus(result, "servers"), cwd);
38
+ }
32
39
  return lines.join("\n");
33
40
  }
34
41
 
@@ -158,7 +165,8 @@ function renderCapabilityWarningsSection(
158
165
  function renderStatusLine(lines: string[], data: HealthData, semanticRequested: boolean): void {
159
166
  if (!semanticRequested) return;
160
167
 
161
- lines.push(`**LSP**: ${displaySemanticStatus(data)}`);
168
+ const routeSummary = formatProjectServerRouteSummary(data.servers);
169
+ lines.push(`**LSP**: ${displaySemanticStatus(data)}${routeSummary ? ` — ${routeSummary}` : ""}`);
162
170
  if (data.structuralStatus) lines.push(`**Structural**: ${data.structuralStatus}`);
163
171
  lines.push("");
164
172
  }
@@ -316,6 +324,7 @@ function renderServersSection(
316
324
  lines: string[],
317
325
  data: HealthData,
318
326
  status: "complete" | "partial" | "unavailable" | undefined,
327
+ cwd: string,
319
328
  ): void {
320
329
  lines.push("### Servers");
321
330
  lines.push("");
@@ -329,8 +338,11 @@ function renderServersSection(
329
338
  } else {
330
339
  for (const server of data.servers) {
331
340
  const statusIcon = server.status === "running" ? "✓" : "✗";
341
+ const reason = server.statusReason
342
+ ? ` — ${formatProjectServerStatusReason(server.statusReason)}`
343
+ : "";
332
344
  lines.push(
333
- `- ${statusIcon} **${server.name}** (${server.fileTypes.join(", ")}) — ${server.status}`,
345
+ `- ${statusIcon} **${server.name}** @ \`${formatProjectServerRoot(cwd, server.root)}\` (${server.fileTypes.join(", ")}) — ${server.status}${reason}`,
334
346
  );
335
347
  }
336
348
  }
@@ -1,4 +1,9 @@
1
1
  import type { ConfidenceMode } from "@mrclrchtr/supi-code-runtime/api";
2
+ import {
3
+ countProjectServerRouteStatuses,
4
+ formatProjectServerRoot,
5
+ formatProjectServerStatusReason,
6
+ } from "../../analysis/health/server-status.ts";
2
7
  import type { HealthData, HealthSection } from "../../session/health-types.ts";
3
8
  import {
4
9
  assembleToolResult,
@@ -36,7 +41,7 @@ const SECTION_TITLES: Record<HealthSection, string> = {
36
41
  };
37
42
 
38
43
  /** Assemble public code_health evidence and details before presentation adapters render it. */
39
- export function assembleHealthResult(data: HealthData): HealthResultAssembly {
44
+ export function assembleHealthResult(data: HealthData, cwd?: string): HealthResultAssembly {
40
45
  const projections = data.includedSections.map((section) => projectSection(section, data));
41
46
  const sections = projections.map((projection) => projection.section);
42
47
  const sectionDetails = projections.map((projection) => projection.details);
@@ -55,7 +60,7 @@ export function assembleHealthResult(data: HealthData): HealthResultAssembly {
55
60
  provenance,
56
61
  });
57
62
 
58
- const displaySections = buildHealthDisplaySections(data);
63
+ const displaySections = buildHealthDisplaySections(data, cwd);
59
64
 
60
65
  return {
61
66
  data,
@@ -78,12 +83,13 @@ export function assembleHealthResult(data: HealthData): HealthResultAssembly {
78
83
  capabilityWarnings: data.capabilityWarnings ?? null,
79
84
  diagnosticFileCount: data.diagnostics.entries.length,
80
85
  serverCount: data.servers.length,
86
+ serverRouteStatusCounts: countProjectServerRouteStatuses(data.servers),
81
87
  evidenceLists: [...assembled.evidenceLists],
82
88
  },
83
89
  };
84
90
  }
85
91
 
86
- function buildHealthDisplaySections(data: HealthData): ToolDisplaySection[] {
92
+ function buildHealthDisplaySections(data: HealthData, cwd?: string): ToolDisplaySection[] {
87
93
  const sections: ToolDisplaySection[] = [];
88
94
 
89
95
  if (data.includedSections.includes("diagnostics") && data.diagnostics.entries.length > 0) {
@@ -107,7 +113,10 @@ function buildHealthDisplaySections(data: HealthData): ToolDisplaySection[] {
107
113
  title: "Servers",
108
114
  items: data.servers,
109
115
  totalCount: data.servers.length,
110
- format: (server) => `${server.name} (${server.fileTypes.join(", ")}) — ${server.status}`,
116
+ format: (server) => {
117
+ const root = cwd ? formatProjectServerRoot(cwd, server.root) : server.root;
118
+ return `${server.name} @ ${root} (${server.fileTypes.join(", ")}) — ${server.status}${server.statusReason ? ` — ${formatProjectServerStatusReason(server.statusReason)}` : ""}`;
119
+ },
111
120
  }),
112
121
  );
113
122
  }
@@ -255,7 +264,7 @@ export function finishHealthResult(outcome: HealthOutcome, cwd: string): CodeInt
255
264
  return healthErrorResult(`**Error:** ${outcome.message}`, outcome.message);
256
265
  }
257
266
 
258
- const assembly = assembleHealthResult(outcome.data);
267
+ const assembly = assembleHealthResult(outcome.data, cwd);
259
268
  return {
260
269
  content: renderHealthResult(assembly, cwd),
261
270
  details: {
@@ -17,7 +17,8 @@ export const codeHealthSpec = {
17
17
  scope: Type.Optional(ScopeParam),
18
18
  refresh: Type.Optional(
19
19
  Type.Boolean({
20
- description: "Attempt diagnostic recovery before collecting; result reports the outcome.",
20
+ description:
21
+ "Attempt diagnostic and required process-crash recovery before collecting; result reports the outcome.",
21
22
  }),
22
23
  ),
23
24
  include: Type.Optional(
@@ -4,6 +4,10 @@
4
4
 
5
5
  import type { Theme } from "@earendil-works/pi-coding-agent";
6
6
  import { Container, Spacer, Text } from "@earendil-works/pi-tui";
7
+ import {
8
+ formatProjectServerRouteStatusCounts,
9
+ type ProjectServerRouteStatusCounts,
10
+ } from "../../analysis/health/server-status.ts";
7
11
  import {
8
12
  type EvidenceEntry,
9
13
  formatCallValue,
@@ -132,8 +136,11 @@ function buildCompactSummary(data: Record<string, unknown> | null, theme: Theme)
132
136
  );
133
137
  if (semanticRequested) {
134
138
  const semanticStatus = readSemanticStatus(data);
139
+ const routeSummary = readServerRouteStatusSummary(data);
135
140
  const statusColor = semanticStatus.startsWith("ready") ? "success" : "warning";
136
- segments.push(`${theme.fg("dim", "lsp")} ${theme.fg(statusColor, semanticStatus)}`);
141
+ segments.push(
142
+ `${theme.fg("dim", "lsp")} ${theme.fg(statusColor, semanticStatus)}${routeSummary ? ` ${theme.fg("warning", routeSummary)}` : ""}`,
143
+ );
137
144
  }
138
145
  const previousRefresh = readPreviousRefreshStatus(data);
139
146
  if (previousRefresh) segments.push(theme.fg("dim", previousRefresh));
@@ -179,7 +186,10 @@ function buildStatusBar(data: Record<string, unknown> | null, theme: Theme): Tex
179
186
  const lspColor = semanticStatus.startsWith("ready") ? "success" : "warning";
180
187
  const structuralColor = structuralStatus === "ready" ? "success" : "muted";
181
188
 
182
- const lines: string[] = [`LSP: ${theme.fg(lspColor, semanticStatus)}`];
189
+ const routeSummary = readServerRouteStatusSummary(data);
190
+ const lines: string[] = [
191
+ `LSP: ${theme.fg(lspColor, semanticStatus)}${routeSummary ? ` — ${theme.fg("warning", routeSummary)}` : ""}`,
192
+ ];
183
193
  if (structuralStatus) {
184
194
  lines.push(`Tree-sitter: ${theme.fg(structuralColor, structuralStatus)}`);
185
195
  }
@@ -287,7 +297,8 @@ function buildHealthSectionSummary(data: Record<string, unknown> | null, theme:
287
297
  if (section.status === "unavailable" || !section.available) {
288
298
  return `${label} unavailable`;
289
299
  }
290
- return `${label} ${section.itemCount}`;
300
+ const routeSummary = section.key === "servers" ? readServerRouteStatusSummary(data) : null;
301
+ return `${label} ${section.itemCount}${routeSummary ? ` — ${routeSummary}` : ""}`;
291
302
  });
292
303
  return new Text(theme.fg("muted", lines.join(" ")), 0, 0);
293
304
  }
@@ -345,6 +356,16 @@ function readSemanticStatus(data: Record<string, unknown> | null): string {
345
356
  return formatSemanticHealthState(readSemanticHealthState(data?.semanticState));
346
357
  }
347
358
 
359
+ function readServerRouteStatusSummary(data: Record<string, unknown> | null): string | null {
360
+ const counts = readRecord(data?.serverRouteStatusCounts);
361
+ if (!counts) return null;
362
+ return formatProjectServerRouteStatusCounts({
363
+ recovering: typeof counts.recovering === "number" ? counts.recovering : 0,
364
+ error: typeof counts.error === "number" ? counts.error : 0,
365
+ unavailable: typeof counts.unavailable === "number" ? counts.unavailable : 0,
366
+ } satisfies ProjectServerRouteStatusCounts);
367
+ }
368
+
348
369
  function readString(data: Record<string, unknown> | null, key: string): string | null {
349
370
  const value = data?.[key];
350
371
  return typeof value === "string" ? value : null;
@@ -100,6 +100,7 @@ export function healthErrorResult(content: string, reason?: string): CodeIntelRe
100
100
  capabilityWarnings: null,
101
101
  diagnosticFileCount: 0,
102
102
  serverCount: 0,
103
+ serverRouteStatusCounts: { recovering: 0, error: 0, unavailable: 0 },
103
104
  },
104
105
  status: "invalid-input",
105
106
  message: reason ?? content,
@@ -251,5 +251,11 @@ export interface HealthDetails {
251
251
  capabilityWarnings: CapabilityWarningReport | null;
252
252
  diagnosticFileCount: number;
253
253
  serverCount: number;
254
+ /** Workspace-wide non-running LSP route counts, separate from semantic readiness. */
255
+ serverRouteStatusCounts: {
256
+ recovering: number;
257
+ error: number;
258
+ unavailable: number;
259
+ };
254
260
  evidenceLists?: EvidenceListMetadata[];
255
261
  }
package/src/ui/footer.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
10
  import { footerContributions } from "@mrclrchtr/supi-core/footer-registry";
11
+ import { countProjectServerRouteStatuses } from "../analysis/health/server-status.ts";
11
12
  import { LSP_STATE_CHANGE_EVENT, type LspAdapterState } from "../substrate/lsp/state.ts";
12
13
 
13
14
  /** Build the LSP status text: "λ lsp • 3 ✓ • 1 ⟳ • 1 ✗ • 2 open files" */
@@ -22,17 +23,18 @@ export function buildLspStatusText(lspState: LspAdapterState): string | undefine
22
23
  // Aggregate server states
23
24
  const ready = servers.filter((s) => s.status === "running" && s.ready).length;
24
25
  const starting = servers.filter((s) => s.status === "running" && !s.ready).length;
25
- const error = servers.filter((s) => s.status === "error").length;
26
- const unavailable = servers.filter((s) => s.status === "unavailable").length;
26
+ const routeCounts = countProjectServerRouteStatuses(servers);
27
27
 
28
- const hasServers = ready + starting + error + unavailable > 0;
28
+ const hasServers =
29
+ ready + starting + routeCounts.recovering + routeCounts.error + routeCounts.unavailable > 0;
29
30
  if (!hasServers && openFiles === 0) return undefined;
30
31
 
31
32
  const parts = ["λ lsp"];
32
33
  if (ready > 0) parts.push(`${ready} ✓`);
33
34
  if (starting > 0) parts.push(`${starting} ⟳`);
34
- if (error > 0) parts.push(`${error} ✗`);
35
- if (unavailable > 0) parts.push(`${unavailable} ⊘`);
35
+ if (routeCounts.recovering > 0) parts.push(`${routeCounts.recovering} ↻`);
36
+ if (routeCounts.error > 0) parts.push(`${routeCounts.error} ✗`);
37
+ if (routeCounts.unavailable > 0) parts.push(`${routeCounts.unavailable} ⊘`);
36
38
  if (openFiles > 0) parts.push(`${openFiles} ${openFiles === 1 ? "open file" : "open files"}`);
37
39
  return parts.join(" • ");
38
40
  }
@@ -9,6 +9,7 @@ import {
9
9
  evaluateCapabilityWarnings,
10
10
  gatherCapabilityWarningInput,
11
11
  } from "../analysis/capability/capability-warnings.ts";
12
+ import { countProjectServerRouteStatuses } from "../analysis/health/server-status.ts";
12
13
  import { type CiStatusData, createCiStatusDialog } from "./status-overlay.ts";
13
14
 
14
15
  const STATUS_KEY = "code-intelligence";
@@ -120,6 +121,7 @@ async function gatherCiStatusData(cwd: string, pi: ExtensionAPI): Promise<CiStat
120
121
  const capabilityWarnings = evaluateCapabilityWarnings(gatherCapabilityWarningInput(cwd, null));
121
122
 
122
123
  return {
124
+ workspaceRoot: cwd,
123
125
  servers,
124
126
  diagnostics,
125
127
  serverInventoryAvailable,
@@ -202,12 +204,40 @@ function renderFooterLine(ctx: ExtensionContext, data: CiStatusData): string {
202
204
  if (totalWarnings > 0) {
203
205
  parts.push(t.fg("warning", `${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`));
204
206
  }
207
+ addProjectServerRouteCounts(parts, data, t);
205
208
  const structKind = data.capabilities.structural.kind;
206
209
  if (structKind === "ready") parts.push(t.fg("success", "✓ ts"));
207
210
  if (parts.length === 0) return t.fg("dim", "CI — no data available");
208
211
  return parts.join(t.fg("dim", " "));
209
212
  }
210
213
 
214
+ function addProjectServerRouteCounts(
215
+ parts: string[],
216
+ data: CiStatusData,
217
+ theme: ExtensionContext["ui"]["theme"],
218
+ ): void {
219
+ const counts = countProjectServerRouteStatuses(data.servers);
220
+ if (counts.recovering > 0) {
221
+ parts.push(
222
+ theme.fg(
223
+ "warning",
224
+ `${counts.recovering} route${counts.recovering === 1 ? "" : "s"} recovering`,
225
+ ),
226
+ );
227
+ }
228
+ if (counts.error > 0) {
229
+ parts.push(theme.fg("error", `${counts.error} route error${counts.error === 1 ? "" : "s"}`));
230
+ }
231
+ if (counts.unavailable > 0) {
232
+ parts.push(
233
+ theme.fg(
234
+ "warning",
235
+ `${counts.unavailable} route${counts.unavailable === 1 ? "" : "s"} unavailable`,
236
+ ),
237
+ );
238
+ }
239
+ }
240
+
211
241
  function truncateFooterLine(line: string, width: number): string {
212
242
  return truncateToWidth(line, width, "…");
213
243
  }
@@ -230,6 +260,7 @@ function updateStatusAndWidget(ctx: ExtensionContext, data: CiStatusData): void
230
260
  if (totalWarnings > 0) {
231
261
  parts.push(t.fg("warning", `${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`));
232
262
  }
263
+ addProjectServerRouteCounts(parts, data, t);
233
264
  const structKind = data.capabilities.structural.kind;
234
265
  if (structKind === "ready") parts.push(t.fg("success", "✓ ts"));
235
266
 
@@ -18,6 +18,11 @@ import type {
18
18
  ProjectServerInfo,
19
19
  } from "@mrclrchtr/supi-lsp/api";
20
20
  import type { CapabilityWarningReport } from "../analysis/capability/capability-warnings.ts";
21
+ import {
22
+ countProjectServerRouteStatuses,
23
+ formatProjectServerRoot,
24
+ formatProjectServerStatusReason,
25
+ } from "../analysis/health/server-status.ts";
21
26
  import { diagnosticMessageString } from "../substrate/lsp/utils.ts";
22
27
 
23
28
  /**
@@ -32,6 +37,8 @@ export interface CiDialogTheme {
32
37
  }
33
38
 
34
39
  export interface CiStatusData {
40
+ /** Canonical workspace root used to display LSP route roots. */
41
+ workspaceRoot: string;
35
42
  servers: ProjectServerInfo[];
36
43
  /** Whether the server list is complete status evidence. */
37
44
  serverInventoryAvailable: boolean;
@@ -263,31 +270,65 @@ export class CiStatusDialog {
263
270
  }
264
271
 
265
272
  private renderSummaryLine(width: number): string {
266
- const t = this.theme;
267
- const running = this.data.servers.filter((s) => s.status === "running").length;
268
- const totalErrors = this.data.diagnostics.reduce((sum, d) => sum + d.errors, 0);
269
- const totalWarnings = this.data.diagnostics.reduce((sum, d) => sum + d.warnings, 0);
270
- const sep = t.fg("dim", " · ");
271
-
272
- const parts: string[] = [];
273
- if (running > 0) {
274
- parts.push(t.fg("text", `${running} server${running === 1 ? "" : "s"}`));
275
- }
276
- if (totalErrors > 0) {
277
- parts.push(t.fg("error", `${totalErrors} error${totalErrors === 1 ? "" : "s"}`));
278
- }
279
- if (totalWarnings > 0) {
280
- parts.push(t.fg("warning", `${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`));
281
- }
282
- const structKind = this.data.capabilities.structural.kind;
283
- if (structKind === "ready") parts.push(t.fg("success", "✓ ts ready"));
284
- else if (structKind === "pending") parts.push(t.fg("dim", "ts pending…"));
285
- else parts.push(t.fg("error", "ts unavailable"));
286
-
287
- const joined = parts.length > 0 ? parts.join(sep) : t.fg("dim", "(no data)");
273
+ const parts = [
274
+ ...this.serverSummaryParts(),
275
+ ...this.diagnosticSummaryParts(),
276
+ ...this.routeSummaryParts(),
277
+ this.structuralSummaryPart(),
278
+ ];
279
+ const joined =
280
+ parts.length > 0
281
+ ? parts.join(this.theme.fg("dim", " · "))
282
+ : this.theme.fg("dim", "(no data)");
288
283
  return truncateToWidth(joined, width);
289
284
  }
290
285
 
286
+ private serverSummaryParts(): string[] {
287
+ const running = this.data.servers.filter((server) => server.status === "running").length;
288
+ return running > 0
289
+ ? [this.theme.fg("text", `${running} server${running === 1 ? "" : "s"}`)]
290
+ : [];
291
+ }
292
+
293
+ private diagnosticSummaryParts(): string[] {
294
+ const errors = this.data.diagnostics.reduce((sum, entry) => sum + entry.errors, 0);
295
+ const warnings = this.data.diagnostics.reduce((sum, entry) => sum + entry.warnings, 0);
296
+ return [
297
+ errors > 0 ? this.theme.fg("error", `${errors} error${errors === 1 ? "" : "s"}`) : null,
298
+ warnings > 0
299
+ ? this.theme.fg("warning", `${warnings} warning${warnings === 1 ? "" : "s"}`)
300
+ : null,
301
+ ].filter((part): part is string => part !== null);
302
+ }
303
+
304
+ private routeSummaryParts(): string[] {
305
+ const counts = countProjectServerRouteStatuses(this.data.servers);
306
+ return [
307
+ counts.recovering > 0
308
+ ? this.theme.fg(
309
+ "warning",
310
+ `${counts.recovering} route${counts.recovering === 1 ? "" : "s"} recovering`,
311
+ )
312
+ : null,
313
+ counts.error > 0
314
+ ? this.theme.fg("error", `${counts.error} route error${counts.error === 1 ? "" : "s"}`)
315
+ : null,
316
+ counts.unavailable > 0
317
+ ? this.theme.fg(
318
+ "warning",
319
+ `${counts.unavailable} route${counts.unavailable === 1 ? "" : "s"} unavailable`,
320
+ )
321
+ : null,
322
+ ].filter((part): part is string => part !== null);
323
+ }
324
+
325
+ private structuralSummaryPart(): string {
326
+ const kind = this.data.capabilities.structural.kind;
327
+ if (kind === "ready") return this.theme.fg("success", "✓ ts ready");
328
+ if (kind === "pending") return this.theme.fg("dim", "ts pending…");
329
+ return this.theme.fg("error", "ts unavailable");
330
+ }
331
+
291
332
  private addServerSection(container: Container): void {
292
333
  const t = this.theme;
293
334
  container.addChild(new Text(t.fg("accent", t.bold(" Servers")), 1, 0));
@@ -328,7 +369,7 @@ export class CiStatusDialog {
328
369
  " " +
329
370
  t.fg(statusColor, icon) +
330
371
  " " +
331
- t.fg("dim", `root: ${server.root}`),
372
+ t.fg("dim", `root: ${formatProjectServerRoot(this.data.workspaceRoot, server.root)}`),
332
373
  0,
333
374
  0,
334
375
  ),
@@ -337,6 +378,11 @@ export class CiStatusDialog {
337
378
  const fileTypes = server.fileTypes.map((f) => `.${f}`).join(" ");
338
379
  container.addChild(new Text(t.fg("dim", ` files: ${fileTypes || "(none)"}`), 0, 0));
339
380
 
381
+ const statusReason = formatProjectServerStatusReason(server.statusReason);
382
+ if (statusReason) {
383
+ container.addChild(new Text(t.fg("warning", ` ${statusReason}`), 0, 0));
384
+ }
385
+
340
386
  if (server.openFiles.length > 0) {
341
387
  const shown = server.openFiles.slice(0, MAX_OPEN_FILES_SHOWN);
342
388
  const remaining = server.openFiles.length - shown.length;