@mrclrchtr/supi-code-intelligence 6.0.0 → 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 (71) hide show
  1. package/README.md +3 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  5. package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  6. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/README.md +7 -1
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-jsonrpc/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.d.ts +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.d.ts +2 -2
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.d.ts +9 -9
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/package.json +3 -3
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.d.ts +2 -2
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.js +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.d.ts +2 -2
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.js +1 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  24. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +2 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +6 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +8 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +1 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  29. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +582 -26
  30. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +2 -3
  31. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +10 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +36 -9
  33. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +2 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +4 -2
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +2 -0
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
  37. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  38. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  39. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
  40. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  41. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  42. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  46. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  48. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  49. package/package.json +5 -5
  50. package/src/analysis/health/recovery.ts +4 -0
  51. package/src/analysis/health/server-status.ts +64 -0
  52. package/src/analysis/health/signals.ts +1 -0
  53. package/src/analysis/provider.ts +2 -1
  54. package/src/analysis/target/symbol.ts +28 -16
  55. package/src/analysis/target/types.ts +1 -0
  56. package/src/session/find-workflow.ts +5 -12
  57. package/src/session/health-refresh.ts +6 -2
  58. package/src/session/health-types.ts +8 -2
  59. package/src/session/health-workflow.ts +5 -1
  60. package/src/session/semantic-demand.ts +36 -0
  61. package/src/session/target-workflow.ts +9 -12
  62. package/src/tool/code_health/guidance.ts +2 -2
  63. package/src/tool/code_health/markdown.ts +15 -3
  64. package/src/tool/code_health/result.ts +14 -5
  65. package/src/tool/code_health/spec.ts +2 -1
  66. package/src/tool/code_health/tui.ts +24 -3
  67. package/src/tool/result/errors.ts +1 -0
  68. package/src/tool/result/types.ts +6 -0
  69. package/src/ui/footer.ts +7 -5
  70. package/src/ui/status-command.ts +31 -0
  71. package/src/ui/status-overlay.ts +69 -23
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Structural AST analysis for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "jiti": "^2.7.0",
38
38
  "web-tree-sitter": "^0.26.8",
39
- "@mrclrchtr/supi-core": "6.0.0",
40
- "@mrclrchtr/supi-code-runtime": "6.0.0"
39
+ "@mrclrchtr/supi-code-runtime": "6.1.0",
40
+ "@mrclrchtr/supi-core": "6.1.0"
41
41
  },
42
42
  "bundledDependencies": [
43
43
  "@mrclrchtr/supi-code-runtime",
@@ -6,7 +6,7 @@
6
6
  "releaseAsset": "https://github.com/fwcd/tree-sitter-kotlin/releases/download/0.3.8/tree-sitter-kotlin.wasm"
7
7
  },
8
8
  "generatedWith": {
9
- "treeSitterCli": "0.26.12"
9
+ "treeSitterCli": "0.26.13"
10
10
  },
11
11
  "sha256": "e08447e36ad5d86f39f91f13defce0636aea1490b8d65dafde6fa72649422061"
12
12
  }
@@ -5,7 +5,7 @@
5
5
  "repository": "https://github.com/derekstride/tree-sitter-sql"
6
6
  },
7
7
  "generatedWith": {
8
- "treeSitterCli": "0.26.12"
8
+ "treeSitterCli": "0.26.13"
9
9
  },
10
10
  "sha256": "9b93a15dc3eade66df4347731f2687b939daf987c8382660d6118e863604ab09",
11
11
  "trust": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-intelligence",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "LSP and AST navigation, search, diagnostics, and refactoring",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "yaml": "^2.9.0",
38
- "@mrclrchtr/supi-code-runtime": "6.0.0",
39
- "@mrclrchtr/supi-lsp": "6.0.0",
40
- "@mrclrchtr/supi-tree-sitter": "6.0.0",
41
- "@mrclrchtr/supi-core": "6.0.0"
38
+ "@mrclrchtr/supi-lsp": "6.1.0",
39
+ "@mrclrchtr/supi-core": "6.1.0",
40
+ "@mrclrchtr/supi-code-runtime": "6.1.0",
41
+ "@mrclrchtr/supi-tree-sitter": "6.1.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
@@ -4,6 +4,7 @@
4
4
  import type { CapabilityState, CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
5
5
  import type {
6
6
  DiagnosticEvidenceSummary,
7
+ ProcessCrashDiagnosticDemand,
7
8
  RecoverDiagnosticsResult,
8
9
  WorkspaceLspRuntime,
9
10
  } from "@mrclrchtr/supi-lsp/api";
@@ -16,6 +17,8 @@ interface RecoverOptions {
16
17
  control?: CodeRequestControl;
17
18
  /** Evidence from the maintenance refresh, so the recovery pass skips its own refresh. */
18
19
  initialEvidence?: DiagnosticEvidenceSummary;
20
+ /** Explicit demand for crashed diagnostic routes before diagnostic recovery. */
21
+ processCrashDemand?: ProcessCrashDiagnosticDemand;
19
22
  }
20
23
 
21
24
  /** Run the runtime's best-effort recovery and preserve its established outcome. */
@@ -26,6 +29,7 @@ export async function recoverDiagnosticRuntime(
26
29
  return opts.service.recoverDiagnostics({
27
30
  restartIfStillStale: true,
28
31
  ...(opts.initialEvidence !== undefined ? { initialEvidence: opts.initialEvidence } : {}),
32
+ ...(opts.processCrashDemand ? { processCrashDemand: opts.processCrashDemand } : {}),
29
33
  control: opts.control,
30
34
  });
31
35
  }
@@ -0,0 +1,64 @@
1
+ import * as path from "node:path";
2
+ import type { ProjectServerInfo, ProjectServerStatusReason } from "@mrclrchtr/supi-lsp/api";
3
+
4
+ /** Workspace-wide counts for non-running LSP routes. */
5
+ export interface ProjectServerRouteStatusCounts {
6
+ recovering: number;
7
+ error: number;
8
+ unavailable: number;
9
+ }
10
+
11
+ /** Count typed route states without changing semantic readiness. */
12
+ export function countProjectServerRouteStatuses(
13
+ servers: readonly Pick<ProjectServerInfo, "status" | "statusReason">[],
14
+ ): ProjectServerRouteStatusCounts {
15
+ return {
16
+ recovering: servers.filter((server) => server.statusReason === "process-crash-recovery-pending")
17
+ .length,
18
+ error: servers.filter(
19
+ (server) =>
20
+ server.status === "error" && server.statusReason !== "process-crash-recovery-pending",
21
+ ).length,
22
+ unavailable: servers.filter((server) => server.status === "unavailable").length,
23
+ };
24
+ }
25
+
26
+ /** Render nonzero workspace route counts for an aggregate status surface. */
27
+ export function formatProjectServerRouteSummary(
28
+ servers: readonly Pick<ProjectServerInfo, "status" | "statusReason">[],
29
+ ): string | null {
30
+ return formatProjectServerRouteStatusCounts(countProjectServerRouteStatuses(servers));
31
+ }
32
+
33
+ /** Render one structured workspace route-count value. */
34
+ export function formatProjectServerRouteStatusCounts(
35
+ counts: ProjectServerRouteStatusCounts,
36
+ ): string | null {
37
+ const parts: string[] = [];
38
+ if (counts.recovering > 0) parts.push(`${counts.recovering} recovering`);
39
+ if (counts.error > 0) parts.push(`${counts.error} error${counts.error === 1 ? "" : "s"}`);
40
+ if (counts.unavailable > 0) parts.push(`${counts.unavailable} unavailable`);
41
+ return parts.length > 0 ? `workspace routes: ${parts.join(", ")}` : null;
42
+ }
43
+
44
+ /** Render an LSP route root relative to its workspace. */
45
+ export function formatProjectServerRoot(cwd: string, root: string): string {
46
+ const relative = path.relative(cwd, root);
47
+ return (relative || ".").replaceAll("\\", "/");
48
+ }
49
+
50
+ /** Render a concise explanation for a process-crash recovery state. */
51
+ export function formatProjectServerStatusReason(
52
+ reason: ProjectServerStatusReason | undefined,
53
+ ): string | null {
54
+ switch (reason) {
55
+ case "process-crashed":
56
+ return "process crashed; next evidence operation will recover";
57
+ case "process-crash-recovery-pending":
58
+ return "process recovery in progress";
59
+ case "process-crash-recovery-exhausted":
60
+ return "process recovery exhausted; reload required";
61
+ default:
62
+ return null;
63
+ }
64
+ }
@@ -15,6 +15,7 @@ export function collectServers(
15
15
  root: server.root,
16
16
  fileTypes: server.fileTypes,
17
17
  status: server.status,
18
+ ...(server.statusReason ? { statusReason: server.statusReason } : {}),
18
19
  ready: server.ready,
19
20
  }));
20
21
  }
@@ -97,7 +97,8 @@ export function withSemanticRequestControl<T extends SemanticProvider>(
97
97
  references: (file, position) => provider.references(file, position, control),
98
98
  implementation: (file, position) => provider.implementation(file, position, control),
99
99
  documentSymbols: (file) => provider.documentSymbols(file, control),
100
- workspaceSymbols: (query) => provider.workspaceSymbols(query, control),
100
+ workspaceSymbols: (query, _requestControl, scopes) =>
101
+ provider.workspaceSymbols(query, control, scopes),
101
102
  hover: provider.hover
102
103
  ? (file, position) => provider.hover?.(file, position, control)
103
104
  : undefined,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import * as path from "node:path";
10
10
  import {
11
+ type CodeQueryResult,
11
12
  type CodeRequestControl,
12
13
  type CodeSymbol,
13
14
  isCodeRequestInterruption,
@@ -114,33 +115,27 @@ export async function resolveSymbolTarget(
114
115
  control?: CodeRequestControl;
115
116
  },
116
117
  ): Promise<TargetOutcome> {
117
- const result = await semantic.workspaceSymbols(symbol);
118
+ const scopePath = options?.path ? normalizePath(options.path, cwd) : null;
119
+ const result = await semantic.workspaceSymbols(
120
+ symbol,
121
+ options?.control,
122
+ scopePath ? [scopePath] : undefined,
123
+ );
118
124
  if (result.kind === "unavailable") {
119
125
  return {
120
- kind: "error",
121
- message: `Symbol discovery for \`${symbol}\` is unavailable: ${result.reason}`,
122
- };
123
- }
124
- if (result.data.length === 0) {
125
- return {
126
- kind: "error",
127
- message: `Symbol not found: \`${symbol}\`. Try \`code_resolve\` to search for it, or use an anchor target (file/line/character) if you know the location.`,
126
+ kind: "unavailable",
127
+ reason: `Symbol discovery for \`${symbol}\` is unavailable: ${result.reason}`,
128
128
  };
129
129
  }
130
+ if (result.data.length === 0) return emptySymbolOutcome(symbol, result, options?.path);
130
131
 
131
- const scopePath = options?.path ? normalizePath(options.path, cwd) : null;
132
132
  const scoped = result.data.filter((candidate) =>
133
133
  scopePath ? isWithinOrEqual(scopePath, candidate.file) : true,
134
134
  );
135
135
  const eligible = options?.exportedOnly
136
136
  ? scoped.filter((candidate) => !NON_EXPORTED_KINDS.has(candidate.kind))
137
137
  : scoped;
138
- if (eligible.length === 0) {
139
- return {
140
- kind: "error",
141
- message: `Symbol not found: \`${symbol}\`${scopePath ? ` in path \`${options?.path}\`` : ""}. Try \`code_resolve\` to search for it, or use an anchor target (file/line/character) if you know the location.`,
142
- };
143
- }
138
+ if (eligible.length === 0) return emptySymbolOutcome(symbol, result, options?.path);
144
139
 
145
140
  const requestedKind = options?.kind;
146
141
  if (requestedKind) {
@@ -248,6 +243,23 @@ type CandidateOutcomeOptions = CandidateOutcomeBase &
248
243
  * exact provider result count for omission disclosure. No candidate is
249
244
  * promoted when the requested provider kind did not match.
250
245
  */
246
+ function emptySymbolOutcome(
247
+ symbol: string,
248
+ result: Exclude<CodeQueryResult<CodeSymbol[]>, { kind: "unavailable" }>,
249
+ scope?: string,
250
+ ): TargetOutcome {
251
+ if (result.kind === "partial") {
252
+ return {
253
+ kind: "unavailable",
254
+ reason: `Symbol discovery for \`${symbol}\` is incomplete: ${result.reason}`,
255
+ };
256
+ }
257
+ return {
258
+ kind: "error",
259
+ message: `Symbol not found: \`${symbol}\`${scope ? ` in path \`${scope}\`` : ""}. Try \`code_resolve\` to search for it, or use an anchor target (file/line/character) if you know the location.`,
260
+ };
261
+ }
262
+
251
263
  async function buildCandidateOutcome(
252
264
  options: CandidateOutcomeOptions,
253
265
  ): Promise<Extract<TargetOutcome, { kind: "disambiguation" | "kind-mismatch" }>> {
@@ -156,4 +156,5 @@ export type TargetOutcome =
156
156
  candidates: DisambiguationCandidateData[];
157
157
  omittedCount: number;
158
158
  }
159
+ | { kind: "unavailable"; reason: string }
159
160
  | { kind: "error"; message: string };
@@ -9,6 +9,7 @@ import { isCodeFindAstKind } from "../tool/code_find/ast-kinds.ts";
9
9
  import type { CapabilityAdapter } from "./capability-adapter.ts";
10
10
  import type { FindWorkflowInput, FindWorkflowOutcome } from "./find-types.ts";
11
11
  import { parseFindWorkflowInput } from "./input/workflows.ts";
12
+ import { resolveWorkspaceSemanticDemand } from "./semantic-demand.ts";
12
13
  import { reportProgress, throwIfAborted, type WorkflowControl } from "./workflow-control.ts";
13
14
 
14
15
  export interface FindWorkflowDeps {
@@ -71,22 +72,14 @@ async function runSemanticSearch(options: {
71
72
  control?: WorkflowControl;
72
73
  }): Promise<FindWorkflowOutcome> {
73
74
  const { query, scopePaths, scopeLabel, maxResults, deps, control } = options;
74
- const readiness = await deps.capability.ensureSemanticReadiness(
75
- deps.cwd,
76
- { kind: "workspace" },
77
- control,
78
- );
79
- if (readiness.kind === "timeout") {
75
+ const semanticDemand = await resolveWorkspaceSemanticDemand(deps.capability, deps.cwd, control);
76
+ if (semanticDemand.kind === "timeout") {
80
77
  return { kind: "unavailable", reason: SEMANTIC_READINESS_TIMEOUT_REASON };
81
78
  }
82
- if (readiness.kind === "unavailable") return readiness;
79
+ if (semanticDemand.kind === "unavailable") return semanticDemand;
83
80
  throwIfAborted(control);
84
81
 
85
- const provider = deps.capability.getSemanticProvider(deps.cwd);
86
- if (!provider?.workspaceSymbols) {
87
- return { kind: "unavailable", reason: "No semantic workspace-symbol provider is active." };
88
- }
89
- const result = await provider.workspaceSymbols(query, control);
82
+ const result = await semanticDemand.provider.workspaceSymbols(query, control, scopePaths);
90
83
  // A cancellation that landed during the request must not publish symbols
91
84
  // the caller no longer awaits.
92
85
  throwIfAborted(control);
@@ -105,9 +105,13 @@ async function collectWorkspaceRefreshAttempt(
105
105
  service: options.runtime,
106
106
  control: options.control,
107
107
  progress: options.reportRecoveryProgress,
108
- // The maintenance pass already refreshed every open document; reuse its
109
- // evidence so the recovery pass skips a redundant second refresh.
108
+ // The maintenance pass already refreshed every running route. Reuse its
109
+ // evidence unless process-crash demand restores a required route; the
110
+ // runtime then performs a fresh pass that includes the replacement.
110
111
  initialEvidence: maintenance.diagnosticEvidence,
112
+ processCrashDemand: {
113
+ ...(workspaceScope?.filter ? { scopes: [workspaceScope.filter] } : {}),
114
+ },
111
115
  });
112
116
  const diagnosticEvidence = mergeDiagnosticEvidence(
113
117
  maintenance.diagnosticEvidence,
@@ -1,4 +1,9 @@
1
- import type { DiagnosticEvidenceSummary, FileScopeDecision } from "@mrclrchtr/supi-lsp/api";
1
+ import type {
2
+ DiagnosticEvidenceSummary,
3
+ FileScopeDecision,
4
+ ProjectServerInfo,
5
+ ProjectServerStatusReason,
6
+ } from "@mrclrchtr/supi-lsp/api";
2
7
  import type { CapabilityWarningReport } from "../analysis/capability/capability-warnings.ts";
3
8
 
4
9
  export type HealthSection = "diagnostics" | "servers";
@@ -22,7 +27,8 @@ export interface HealthServerInfo {
22
27
  readonly name: string;
23
28
  readonly root: string;
24
29
  readonly fileTypes: readonly string[];
25
- readonly status: string;
30
+ readonly status: ProjectServerInfo["status"];
31
+ readonly statusReason?: ProjectServerStatusReason;
26
32
  readonly ready: boolean;
27
33
  }
28
34
 
@@ -59,6 +59,7 @@ export async function runHealthWorkflow(
59
59
  if (prepared.kind === "invalid-input") return prepared;
60
60
  const { request, scopeFilter, included, level } = prepared.value;
61
61
  const semanticRequested = healthNeedsSemantic(included);
62
+ const fileReadinessRequested = included.includes("diagnostics");
62
63
  throwIfAborted(control);
63
64
 
64
65
  reportProgress(control, {
@@ -85,6 +86,7 @@ export async function runHealthWorkflow(
85
86
  });
86
87
  const semanticState = await establishSemanticHealthState({
87
88
  requested: semanticRequested,
89
+ fileReadinessRequested,
88
90
  runtime,
89
91
  scopeFilter,
90
92
  lspState,
@@ -139,6 +141,8 @@ function healthNeedsSemantic(included: readonly HealthSection[]): boolean {
139
141
 
140
142
  interface SemanticHealthStateOptions {
141
143
  requested: boolean;
144
+ /** Whether this call requests file-routed evidence rather than passive inventory. */
145
+ fileReadinessRequested: boolean;
142
146
  runtime: WorkspaceLspRuntime | null;
143
147
  scopeFilter: string | null;
144
148
  lspState: WorkspaceLspRuntimeState;
@@ -150,7 +154,7 @@ async function establishSemanticHealthState(
150
154
  options: SemanticHealthStateOptions,
151
155
  ): Promise<SemanticHealthState | null> {
152
156
  if (!options.requested) return null;
153
- if (options.runtime && isScopedFile(options.scopeFilter)) {
157
+ if (options.fileReadinessRequested && options.runtime && isScopedFile(options.scopeFilter)) {
154
158
  const readiness = await options.runtime.waitUntilReadyForFile(
155
159
  options.scopeFilter,
156
160
  undefined,
@@ -0,0 +1,36 @@
1
+ import type { CodeRequestControl, SemanticProvider } from "@mrclrchtr/supi-code-runtime/api";
2
+ import type { CapabilityAdapter, ReadinessOutcome } from "./capability-adapter.ts";
3
+
4
+ export type WorkspaceSemanticDemandState =
5
+ | { kind: "ready"; provider: SemanticProvider }
6
+ | Exclude<ReadinessOutcome, { kind: "ready" }>;
7
+
8
+ /**
9
+ * Wait for normal workspace startup, but let a registered provider proceed
10
+ * when known crashed routes make readiness recoverable only through demand.
11
+ */
12
+ export async function resolveWorkspaceSemanticDemand(
13
+ capability: CapabilityAdapter,
14
+ cwd: string,
15
+ control?: CodeRequestControl,
16
+ ): Promise<WorkspaceSemanticDemandState> {
17
+ let provider = capability.getSemanticProvider(cwd);
18
+ const readiness = await capability.ensureSemanticReadiness(cwd, { kind: "workspace" }, control);
19
+ if (readiness.kind === "timeout") return readiness;
20
+ if (readiness.kind === "unavailable" && !hasProcessCrashRoute(capability, cwd)) {
21
+ return readiness;
22
+ }
23
+
24
+ provider ??= capability.getSemanticProvider(cwd);
25
+ return provider
26
+ ? { kind: "ready", provider }
27
+ : { kind: "unavailable", reason: "No semantic workspace-symbol provider is active." };
28
+ }
29
+
30
+ function hasProcessCrashRoute(capability: CapabilityAdapter, cwd: string): boolean {
31
+ const lspState = capability.getLspRuntimeState(cwd);
32
+ return (
33
+ lspState.kind === "ready" &&
34
+ lspState.runtime.getProjectServers().some((server) => server.statusReason !== undefined)
35
+ );
36
+ }
@@ -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,