@mrclrchtr/supi-code-intelligence 6.1.0 → 6.2.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 (114) 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-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  6. package/node_modules/@mrclrchtr/supi-lsp/README.md +21 -3
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  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/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  11. package/node_modules/@mrclrchtr/supi-lsp/package.json +4 -5
  12. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -7
  13. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
  15. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
  16. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +17 -5
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
  21. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
  22. package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
  23. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +0 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
  25. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +0 -29
  26. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
  27. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
  29. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
  30. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
  32. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
  33. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
  34. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
  35. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  36. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +275 -99
  39. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +4 -1
  40. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
  41. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
  42. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +26 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
  44. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +83 -19
  45. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +5 -2
  46. package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
  47. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +29 -3
  48. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
  49. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
  50. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +287 -0
  51. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
  52. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  53. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -5
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
  80. package/package.json +5 -5
  81. package/src/analysis/capability/capability-warnings.ts +4 -22
  82. package/src/analysis/search/ast-scan-timing.ts +4 -2
  83. package/src/analysis/search/ast-scan.ts +1 -1
  84. package/src/api.ts +1 -0
  85. package/src/extension.ts +4 -2
  86. package/src/session/health-refresh.ts +24 -2
  87. package/src/session/health-types.ts +19 -0
  88. package/src/session/input/health-refactor.ts +16 -14
  89. package/src/session/refactor-types.ts +14 -1
  90. package/src/session/refactor-workflow.ts +21 -16
  91. package/src/substrate/lsp/lifecycle.ts +0 -3
  92. package/src/substrate/lsp/maintenance.ts +4 -5
  93. package/src/substrate/lsp/settings.ts +6 -9
  94. package/src/substrate/lsp/state.ts +0 -2
  95. package/src/substrate/workspace-provider-host.ts +2 -2
  96. package/src/tool/code_health/file-scope-markdown.ts +43 -0
  97. package/src/tool/code_health/markdown.ts +66 -66
  98. package/src/tool/code_health/refresh-outcome.ts +46 -0
  99. package/src/tool/code_health/refresh-status.ts +166 -23
  100. package/src/tool/code_health/result.ts +1 -0
  101. package/src/tool/code_health/tui.ts +18 -5
  102. package/src/tool/code_refactor_apply/guidance.ts +3 -2
  103. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  104. package/src/tool/register.ts +5 -2
  105. package/src/tool/schemas.ts +21 -11
  106. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
  107. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
@@ -26,13 +26,14 @@ import {
26
26
  isPlanFresh,
27
27
  type RefactorPlan,
28
28
  } from "./refactor-plans.ts";
29
- import type {
30
- PublicSourceRange,
31
- RefactorApplyWorkflowInput,
32
- RefactorApplyWorkflowOutcome,
33
- RefactorOperationInput,
34
- RefactorPlanWorkflowInput,
35
- RefactorPlanWorkflowOutcome,
29
+ import {
30
+ PUBLIC_REFACTOR_OPERATION_NAMES,
31
+ type PublicSourceRange,
32
+ type RefactorApplyWorkflowInput,
33
+ type RefactorApplyWorkflowOutcome,
34
+ type RefactorOperationInput,
35
+ type RefactorPlanWorkflowInput,
36
+ type RefactorPlanWorkflowOutcome,
36
37
  } from "./refactor-types.ts";
37
38
  import { resolveTargetWorkflow, type TargetWorkflowDeps } from "./target-workflow.ts";
38
39
  import { reportProgress, throwIfAborted, type WorkflowControl } from "./workflow-control.ts";
@@ -208,13 +209,11 @@ function parseOperation(input: RefactorOperationInput):
208
209
  | {
209
210
  kind: "ok";
210
211
  operation: RefactorOperation;
211
- newName: string;
212
+ newName?: string;
212
213
  range?: PublicSourceRange;
213
214
  }
214
215
  | { kind: "invalid-input"; message: string } {
215
- const keys = ["rename_symbol", "extract_function", "extract_variable"].filter(
216
- (key) => key in input,
217
- );
216
+ const keys = PUBLIC_REFACTOR_OPERATION_NAMES.filter((key) => key in input);
218
217
  if (keys.length !== 1) {
219
218
  return { kind: "invalid-input", message: "Select exactly one refactor operation." };
220
219
  }
@@ -233,11 +232,17 @@ function parseOperation(input: RefactorOperationInput):
233
232
  range: input.extract_function.range,
234
233
  };
235
234
  }
235
+ if ("extract_variable" in input) {
236
+ return {
237
+ kind: "ok",
238
+ operation: "extract_variable",
239
+ newName: input.extract_variable.newName,
240
+ range: input.extract_variable.range,
241
+ };
242
+ }
236
243
  return {
237
244
  kind: "ok",
238
- operation: "extract_variable",
239
- newName: input.extract_variable.newName,
240
- range: input.extract_variable.range,
245
+ operation: "update_imports" in input ? "update_imports" : "delete_dead_code",
241
246
  };
242
247
  }
243
248
 
@@ -265,13 +270,13 @@ async function planWithProvider(
265
270
  file: string;
266
271
  position: { line: number; character: number };
267
272
  range?: SourceRange;
268
- newName: string;
273
+ newName?: string;
269
274
  },
270
275
  control?: WorkflowControl,
271
276
  ): Promise<RefactorResult> {
272
277
  if (!provider) return { kind: "unavailable", reason: "No semantic provider is active." };
273
278
  if (provider.refactor) return provider.refactor(request, control);
274
- if (request.operation === "rename_symbol" && provider.rename) {
279
+ if (request.operation === "rename_symbol" && request.newName !== undefined && provider.rename) {
275
280
  return provider.rename(request.file, request.position, request.newName, control);
276
281
  }
277
282
  return {
@@ -45,7 +45,6 @@ export function registerLspSessionLifecycle(
45
45
  await state.providerLease?.release();
46
46
  state.providerLease = null;
47
47
  state.controller = null;
48
- state.lspActive = false;
49
48
  state.sentinelSnapshot = new Map();
50
49
 
51
50
  const lease = await acquireWorkspaceProviderHost(ctx.cwd, {
@@ -53,7 +52,6 @@ export function registerLspSessionLifecycle(
53
52
  });
54
53
  state.providerLease = lease;
55
54
  state.controller = lease.lspController;
56
- state.lspActive = lease.lspController?.kind === "ready";
57
55
  state.sentinelSnapshot = lease.sentinelSnapshot;
58
56
  await onStarted?.(ctx);
59
57
  startLifecycleSubscription();
@@ -64,7 +62,6 @@ export function registerLspSessionLifecycle(
64
62
  await state.providerLease?.release();
65
63
  state.providerLease = null;
66
64
  state.controller = null;
67
- state.lspActive = false;
68
65
  state.sentinelSnapshot = new Map();
69
66
  dispatchStateChange();
70
67
  });
@@ -24,7 +24,6 @@ import {
24
24
  isLikelyStaleDiagnostic,
25
25
  isProjectConfigFileName,
26
26
  raceRequestControl,
27
- syncWorkspaceSentinelSnapshot,
28
27
  type WorkspaceLspRuntime,
29
28
  } from "@mrclrchtr/supi-lsp/api";
30
29
  import { mergeDiagnosticEvidence } from "../../diagnostics/evidence.ts";
@@ -56,7 +55,7 @@ export async function refreshLspMaintenance(
56
55
  sentinelSnapshot: Map<string, number>,
57
56
  options: LspMaintenanceOptions = {},
58
57
  ): Promise<WorkspaceLspMaintenanceResult> {
59
- const { snapshot } = await synchronizeSentinels(runtime, cwd, sentinelSnapshot, options);
58
+ const { snapshot } = await synchronizeSentinels(runtime, sentinelSnapshot, options);
60
59
  let diagnosticEvidence = emptyEvidence();
61
60
  let failureReason: string | undefined;
62
61
 
@@ -115,7 +114,7 @@ export async function refreshFileLspMaintenance(options: {
115
114
  const { runtime, cwd, sentinelSnapshot, filePath, control } = options;
116
115
  // A cancelled caller stops before any maintenance work starts.
117
116
  throwIfCodeRequestInterrupted(control);
118
- const { snapshot } = await synchronizeSentinels(runtime, cwd, sentinelSnapshot);
117
+ const { snapshot } = await synchronizeSentinels(runtime, sentinelSnapshot);
119
118
  const target = nodePath.resolve(filePath);
120
119
  const stale = confirmedOutstandingDiagnostics(runtime, cwd).some(
121
120
  (entry) =>
@@ -149,12 +148,11 @@ export async function refreshFileLspMaintenance(options: {
149
148
  */
150
149
  async function synchronizeSentinels(
151
150
  runtime: WorkspaceLspRuntime,
152
- cwd: string,
153
151
  sentinelSnapshot: Map<string, number>,
154
152
  options: LspMaintenanceOptions = {},
155
153
  ) {
156
154
  const primed = sentinelSnapshot.size > 0;
157
- const state = syncWorkspaceSentinelSnapshot(cwd, sentinelSnapshot, {
155
+ const state = runtime.syncWorkspaceSentinelSnapshot(sentinelSnapshot, {
158
156
  includeSourceFiles: true,
159
157
  });
160
158
 
@@ -193,6 +191,7 @@ async function trackCreatedSourceFiles(
193
191
  if (change.type !== FileChangeType.Created) continue;
194
192
  const filePath = uriToFile(change.uri);
195
193
  if (scope && !isWithinOrEqual(scope, filePath)) continue;
194
+ if (!runtime.isSupportedSourceFile(filePath)) continue;
196
195
  // Best-effort: a file no client can serve stays untracked and is
197
196
  // simply absent from evidence until a later explicit request.
198
197
  await runtime.trackFile(filePath);
@@ -1,8 +1,7 @@
1
1
  // LSP settings registration for the code-intelligence umbrella extension.
2
2
  //
3
- // Always-on LSP policy: the global `lsp.enabled` and `lsp.active` keys
4
- // are deprecated and ignored. Per-language disable via
5
- // `lsp.servers.<language>.enabled: false` is the only supported opt-out.
3
+ // Per-language disable via `lsp.servers.<language>.enabled: false` is the
4
+ // supported opt-out.
6
5
  //
7
6
  // Registered fields:
8
7
  // - exclude: stringList
@@ -21,11 +20,9 @@ import {
21
20
  } from "@mrclrchtr/supi-core/settings";
22
21
  import { type LspSettings, loadConfig } from "@mrclrchtr/supi-lsp/api";
23
22
 
24
- const LSP_DEFAULTS: LspSettings = {
25
- enabled: true,
26
- active: [],
27
- exclude: [],
28
- };
23
+ const LSP_DEFAULTS = {
24
+ exclude: [] as string[],
25
+ } satisfies LspSettings;
29
26
 
30
27
  /** Discover configured servers from the defaults + effective LSP config. */
31
28
  function getConfiguredServers(cwd: string): string[] {
@@ -153,7 +150,7 @@ export function registerLspSettings(pi: ExtensionAPI): void {
153
150
  kind: "stringList" as const,
154
151
  key: "exclude",
155
152
  label: "Exclude Patterns",
156
- description: "Gitignore patterns to suppress LSP diagnostics (comma-separated)",
153
+ description: "Gitignore patterns for automatic LSP workspace work (comma-separated)",
157
154
  },
158
155
  {
159
156
  kind: "custom" as const,
@@ -22,7 +22,6 @@ export interface LspAdapterState {
22
22
  controller: LspRuntimeController | null;
23
23
  providerLease: WorkspaceProviderHostLease | null;
24
24
  inspector: LspInspectorState;
25
- lspActive: boolean;
26
25
  /** Snapshot of workspace sentinel files (package.json, tsconfig, lockfiles) for change detection. */
27
26
  sentinelSnapshot: Map<string, number>;
28
27
  /** Fired for runtime lifecycle changes that require a footer render. */
@@ -34,7 +33,6 @@ export function createLspAdapterState(): LspAdapterState {
34
33
  controller: null,
35
34
  providerLease: null,
36
35
  inspector: { handle: null, close: null },
37
- lspActive: false,
38
36
  sentinelSnapshot: new Map(),
39
37
  stateChanges: new EventTarget(),
40
38
  };
@@ -1,7 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
4
- import { LspRuntimeController, scanWorkspaceSentinels } from "@mrclrchtr/supi-lsp/api";
4
+ import { LspRuntimeController } from "@mrclrchtr/supi-lsp/api";
5
5
  import { TreeSitterRuntimeController } from "@mrclrchtr/supi-tree-sitter/api";
6
6
 
7
7
  const HOSTS = Symbol.for("supi-code-intelligence/workspace-provider-hosts");
@@ -77,7 +77,7 @@ class WorkspaceProviderHost {
77
77
  lspController: projectTrusted ? this.#lsp : null,
78
78
  sentinelSnapshot:
79
79
  projectTrusted && this.#lsp?.kind === "ready"
80
- ? scanWorkspaceSentinels(this.cwd)
80
+ ? (this.#lsp.workspaceRuntime?.scanWorkspaceSentinels() ?? new Map())
81
81
  : new Map(),
82
82
  release: async () => {
83
83
  if (released) return;
@@ -0,0 +1,43 @@
1
+ import type { HealthDiagnosticObservation } from "../../session/health-types.ts";
2
+
3
+ /** Render the tsconfig coverage result for a single-file health request. */
4
+ export function renderFileScopeStatus(
5
+ lines: string[],
6
+ observation: Extract<
7
+ HealthDiagnosticObservation,
8
+ { kind: "completed" | "partial" | "unavailable" }
9
+ >,
10
+ cwd: string,
11
+ ): void {
12
+ if (observation.scope.kind !== "file" || !observation.scopeStatus) return;
13
+
14
+ const decision = observation.scopeStatus;
15
+ switch (decision.status) {
16
+ case "included":
17
+ lines.push(
18
+ `**Tsconfig**: covered by ${formatConfigPath(decision.configPath, cwd)} — part of workspace diagnostics.`,
19
+ );
20
+ return;
21
+ case "excluded":
22
+ lines.push(
23
+ `**Tsconfig**: NOT covered by ${formatConfigPath(decision.configPath, cwd)} — not part of workspace diagnostics.`,
24
+ );
25
+ return;
26
+ case "no-config":
27
+ lines.push("**Tsconfig**: no tsconfig.json or jsconfig.json found — nothing filtered.");
28
+ return;
29
+ case "out-of-tree":
30
+ lines.push("**Tsconfig**: outside the project root — not part of workspace diagnostics.");
31
+ }
32
+ }
33
+
34
+ /** Format a config path relative to the current workspace. */
35
+ function formatConfigPath(configPath: string | null, cwd: string): string {
36
+ if (!configPath) return "no config";
37
+ return `\`${makeRelative(cwd, configPath)}\``;
38
+ }
39
+
40
+ /** Format a workspace path without exposing an absolute root. */
41
+ export function makeRelative(cwd: string, file: string): string {
42
+ return file.startsWith(cwd) ? file.slice(cwd.length + 1) : file;
43
+ }
@@ -11,9 +11,15 @@ import {
11
11
  formatProjectServerRouteSummary,
12
12
  formatProjectServerStatusReason,
13
13
  } from "../../analysis/health/server-status.ts";
14
+ import { makeRelative, renderFileScopeStatus } from "./file-scope-markdown.ts";
15
+ import {
16
+ formatProcessCrashRecovery,
17
+ formatRefreshElapsed,
18
+ formatStaleDiagnosticRestarts as formatStaleDiagnosticRestartsText,
19
+ isFileReadinessPending,
20
+ } from "./refresh-outcome.ts";
14
21
  import type {
15
22
  HealthData,
16
- HealthDiagnosticObservation,
17
23
  HealthDiagnosticScope,
18
24
  HealthRefreshAttempt,
19
25
  HealthResultAssembly,
@@ -48,19 +54,25 @@ function renderRefreshStatus(
48
54
  if (!hasDiagnostics) return;
49
55
 
50
56
  switch (data.refresh.kind) {
51
- case "completed":
57
+ case "completed": {
58
+ const fileReadinessPending = isFileReadinessPending(data.refresh, data.semanticState);
52
59
  lines.push(
53
- `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh))}`,
60
+ `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh, fileReadinessPending))}`,
54
61
  );
55
62
  lines.push(`**Stale assessment**: ${asSentence(staleAssessmentText(data.refresh))}`);
56
63
  lines.push("");
57
64
  return;
65
+ }
58
66
  case "failed": {
59
67
  const evidence = data.refresh.diagnosticEvidence
60
68
  ? `; ${formatDiagnosticEvidence(data.refresh.diagnosticEvidence)}`
61
69
  : "";
70
+ const processCrashRecovery = data.refresh.processCrashRecovery
71
+ ? formatProcessCrashRecovery(data.refresh.processCrashRecovery)
72
+ : null;
73
+ const staleRestart = formatStaleDiagnosticRestarts(data.refresh);
62
74
  lines.push(
63
- `**${refreshAttemptLabel(data.refresh)}**: failed — ${data.refresh.reason}${evidence}`,
75
+ `**${refreshAttemptLabel(data.refresh)}**: failed — ${data.refresh.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`,
64
76
  );
65
77
  lines.push("");
66
78
  return;
@@ -84,14 +96,27 @@ function renderRefreshStatus(
84
96
 
85
97
  function completedRefreshText(
86
98
  attempt: Extract<HealthRefreshAttempt, { kind: "completed" }>,
99
+ fileReadinessPending = attempt.fileReadiness === "pending",
87
100
  ): string {
88
- const base =
89
- attempt.attemptedActiveClients === 0 && attempt.restartedClients === 0
101
+ const processCrashRecovery = formatProcessCrashRecovery(attempt.processCrashRecovery);
102
+ const noOp =
103
+ !fileReadinessPending &&
104
+ attempt.attemptedActiveClients === 0 &&
105
+ attempt.restartedClients === 0 &&
106
+ processCrashRecovery === null;
107
+ const base = fileReadinessPending
108
+ ? "readiness pending — LSP may still be warming; retry shortly"
109
+ : noOp
90
110
  ? "completed no-op — no active clients were targeted"
91
- : `completed — ${attempt.attemptedActiveClients} active client${plural(attempt.attemptedActiveClients)} targeted, ${attempt.restartedClients} restarted`;
111
+ : `completed — ${attempt.attemptedActiveClients} active client${plural(attempt.attemptedActiveClients)} targeted`;
112
+ const staleRestart = noOp ? null : formatStaleDiagnosticRestarts(attempt);
113
+ const outcome = [staleRestart, processCrashRecovery].filter(
114
+ (value): value is string => value !== null,
115
+ );
116
+ const withOutcome = outcome.length > 0 ? `${base}; ${outcome.join("; ")}` : base;
92
117
  return attempt.operationScope === "workspace-runtime"
93
- ? `${base}; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
94
- : base;
118
+ ? `${withOutcome}; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
119
+ : withOutcome;
95
120
  }
96
121
 
97
122
  function refreshAttemptLabel(attempt: Pick<HealthRefreshAttempt, "operationScope">): string {
@@ -117,7 +142,7 @@ function renderLastAttempt(lines: string[], attempt: HealthRefreshAttempt, cwd:
117
142
  const outcome = refreshAttemptOutcome(attempt);
118
143
  const label = refreshAttemptLabel(attempt);
119
144
  lines.push(
120
- `**Previous ${label.toLowerCase()}**: ${asSentence(outcome)} Started ${formatElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
145
+ `**Previous ${label.toLowerCase()}**: ${asSentence(outcome)} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
121
146
  );
122
147
  }
123
148
 
@@ -125,24 +150,35 @@ function renderRetainedAttempt(lines: string[], attempt: HealthRefreshAttempt, c
125
150
  const label =
126
151
  attempt.operationScope === "file-runtime" ? "File LSP maintenance" : "Diagnostic refresh";
127
152
  lines.push(
128
- `**${label}**: not requested for this call. Previous attempt ${asSentence(refreshAttemptOutcome(attempt))} Started ${formatElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
153
+ `**${label}**: not requested for this call. Previous attempt ${asSentence(refreshAttemptOutcome(attempt))} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
129
154
  );
130
155
  }
131
156
 
132
157
  function refreshAttemptOutcome(attempt: HealthRefreshAttempt): string {
133
- return attempt.kind === "completed"
134
- ? completedRefreshText(attempt)
135
- : `failed — ${attempt.reason}${attempt.diagnosticEvidence ? `; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}` : ""}`;
158
+ if (attempt.kind === "completed") return completedRefreshText(attempt);
159
+ const processCrashRecovery = attempt.processCrashRecovery
160
+ ? formatProcessCrashRecovery(attempt.processCrashRecovery)
161
+ : null;
162
+ const staleRestart = formatStaleDiagnosticRestarts(attempt);
163
+ const evidence = attempt.diagnosticEvidence
164
+ ? `; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
165
+ : "";
166
+ return `failed — ${attempt.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`;
136
167
  }
137
168
 
138
169
  function refreshOperationScopeText(attempt: HealthRefreshAttempt): string {
139
170
  return attempt.operationScope === "file-runtime" ? "file runtime" : "workspace runtime";
140
171
  }
141
172
 
142
- function formatElapsed(milliseconds: number): string {
143
- const seconds = Math.max(0, Math.round(milliseconds / 1_000));
144
- if (seconds < 60) return `${seconds}s ago`;
145
- return `${Math.round(seconds / 60)}m ago`;
173
+ function formatStaleDiagnosticRestarts(attempt: {
174
+ kind?: HealthRefreshAttempt["kind"];
175
+ operationScope: HealthRefreshAttempt["operationScope"];
176
+ restartedClients?: number;
177
+ }): string | null {
178
+ if (attempt.operationScope !== "workspace-runtime" || attempt.restartedClients === undefined) {
179
+ return null;
180
+ }
181
+ return formatStaleDiagnosticRestartsText(attempt.restartedClients);
146
182
  }
147
183
 
148
184
  function renderCapabilityWarningsSection(
@@ -193,49 +229,6 @@ function renderDiagnosticsSection(lines: string[], data: HealthData, cwd: string
193
229
  lines.push("");
194
230
  }
195
231
 
196
- /**
197
- * Render the tsconfig coverage verdict for a single-file health request.
198
- *
199
- * Written in plain vocabulary (no basis terms) so a reader without tsconfig
200
- * background can act on it: the sentence names the deciding config and states
201
- * whether the file's errors are part of workspace diagnostics. The structured
202
- * decision basis stays in the diagnostics.scope debug event.
203
- */
204
- function renderFileScopeStatus(
205
- lines: string[],
206
- observation: Extract<
207
- HealthDiagnosticObservation,
208
- { kind: "completed" | "partial" | "unavailable" }
209
- >,
210
- cwd: string,
211
- ): void {
212
- if (observation.scope.kind !== "file" || !observation.scopeStatus) return;
213
-
214
- const decision = observation.scopeStatus;
215
- switch (decision.status) {
216
- case "included":
217
- lines.push(
218
- `**Tsconfig**: covered by ${formatConfigPath(decision.configPath, cwd)} — part of workspace diagnostics.`,
219
- );
220
- return;
221
- case "excluded":
222
- lines.push(
223
- `**Tsconfig**: NOT covered by ${formatConfigPath(decision.configPath, cwd)} — not part of workspace diagnostics.`,
224
- );
225
- return;
226
- case "no-config":
227
- lines.push("**Tsconfig**: no tsconfig.json or jsconfig.json found — nothing filtered.");
228
- return;
229
- case "out-of-tree":
230
- lines.push("**Tsconfig**: outside the project root — not part of workspace diagnostics.");
231
- }
232
- }
233
-
234
- function formatConfigPath(configPath: string | null, cwd: string): string {
235
- if (!configPath) return "no config";
236
- return `\`${makeRelative(cwd, configPath)}\``;
237
- }
238
-
239
232
  function formatDiagnosticEvidence(evidence: {
240
233
  requested: number;
241
234
  confirmed: number;
@@ -250,7 +243,14 @@ function renderDiagnosticObservation(lines: string[], data: HealthData, cwd: str
250
243
  const observation = data.diagnostics;
251
244
  if (observation.kind === "not-requested") return;
252
245
  if (observation.kind === "unavailable") {
253
- lines.push(`Diagnostics unavailable — ${asSentence(observation.reason)}`);
246
+ if (
247
+ observation.scope.kind === "file" &&
248
+ isFileReadinessPending(currentRefreshAttempt(data), data.semanticState)
249
+ ) {
250
+ lines.push("Diagnostics pending — LSP may still be warming; retry shortly.");
251
+ } else {
252
+ lines.push(`Diagnostics unavailable — ${asSentence(observation.reason)}`);
253
+ }
254
254
  return;
255
255
  }
256
256
  if (observation.kind === "partial") {
@@ -269,6 +269,10 @@ function renderDiagnosticObservation(lines: string[], data: HealthData, cwd: str
269
269
  renderDiagnosticEntries(lines, data, cwd);
270
270
  }
271
271
 
272
+ function currentRefreshAttempt(data: HealthData): HealthRefreshAttempt | null {
273
+ return data.refresh.kind === "completed" || data.refresh.kind === "failed" ? data.refresh : null;
274
+ }
275
+
272
276
  function asSentence(text: string): string {
273
277
  return /[.!?]$/.test(text) ? text : `${text}.`;
274
278
  }
@@ -371,7 +375,3 @@ function formatDiagnosticScope(scope: HealthDiagnosticScope, cwd: string): strin
371
375
  function plural(count: number): string {
372
376
  return count === 1 ? "" : "s";
373
377
  }
374
-
375
- function makeRelative(cwd: string, file: string): string {
376
- return file.startsWith(cwd) ? file.slice(cwd.length + 1) : file;
377
- }
@@ -0,0 +1,46 @@
1
+ import type { ProcessCrashRecoverySummary } from "@mrclrchtr/supi-lsp/api";
2
+ import type { SemanticHealthState } from "../../session/health-types.ts";
3
+
4
+ /** Report whether a file health result is waiting for semantic readiness. */
5
+ export function isFileReadinessPending(
6
+ attempt: { operationScope?: string; fileReadiness?: string } | null | undefined,
7
+ semanticState: SemanticHealthState | null | undefined,
8
+ ): boolean {
9
+ if (attempt?.operationScope !== undefined) {
10
+ return (
11
+ attempt.operationScope === "file-runtime" &&
12
+ (attempt.fileReadiness === "pending" ||
13
+ (attempt.fileReadiness === undefined && semanticState?.kind === "pending"))
14
+ );
15
+ }
16
+ return semanticState?.kind === "pending";
17
+ }
18
+
19
+ /** Format the outcome of stale-diagnostic client restarts. */
20
+ export function formatStaleDiagnosticRestarts(restartedClients: number): string {
21
+ return `stale diagnostic restarts: ${restartedClients} client${plural(restartedClients)} restarted`;
22
+ }
23
+
24
+ /** Format a non-empty process-crash route recovery outcome. */
25
+ export function formatProcessCrashRecovery(
26
+ summary: ProcessCrashRecoverySummary | null | undefined,
27
+ ): string | null {
28
+ if (
29
+ !summary ||
30
+ (summary.attemptedRoutes === 0 && summary.recoveredRoutes === 0 && summary.failedRoutes === 0)
31
+ ) {
32
+ return null;
33
+ }
34
+ return `process-crash recovery: ${summary.recoveredRoutes} route${plural(summary.recoveredRoutes)} recovered (${summary.attemptedRoutes} attempted, ${summary.failedRoutes} failed)`;
35
+ }
36
+
37
+ /** Format the age of a retained refresh attempt for a status line. */
38
+ export function formatRefreshElapsed(milliseconds: number): string {
39
+ const seconds = Math.max(0, Math.round(milliseconds / 1_000));
40
+ if (seconds < 60) return `${seconds}s ago`;
41
+ return `${Math.round(seconds / 60)}m ago`;
42
+ }
43
+
44
+ function plural(count: number): string {
45
+ return count === 1 ? "" : "s";
46
+ }