@mrclrchtr/supi-lsp 3.2.0 → 4.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 (58) hide show
  1. package/README.md +72 -62
  2. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +8 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -4
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  12. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
  13. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +10 -1
  14. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +19 -29
  15. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +7 -1
  16. package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +42 -0
  17. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +15 -10
  18. package/node_modules/@mrclrchtr/supi-core/package.json +2 -4
  19. package/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
  20. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
  21. package/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
  22. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
  23. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
  24. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +1 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +9 -0
  26. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  27. package/package.json +4 -5
  28. package/src/api.ts +9 -9
  29. package/src/client/client.ts +70 -41
  30. package/src/client/transport.ts +16 -13
  31. package/src/config/config.ts +2 -2
  32. package/src/config/lsp-settings.ts +0 -2
  33. package/src/index.ts +6 -6
  34. package/src/manager/manager-diagnostics.ts +5 -95
  35. package/src/manager/manager-workspace-recovery.ts +4 -3
  36. package/src/manager/manager-workspace-symbol.ts +44 -6
  37. package/src/manager/manager.ts +47 -65
  38. package/src/provider/lsp-semantic-provider.ts +132 -79
  39. package/src/provider/refactor-planning.ts +6 -14
  40. package/src/session/readiness.ts +38 -0
  41. package/src/session/runtime-controller.ts +87 -75
  42. package/src/session/runtime-registration.ts +5 -5
  43. package/src/session/{service-registry.ts → runtime-registry.ts} +163 -71
  44. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
  45. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
  46. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
  47. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
  48. package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
  49. package/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
  50. package/src/diagnostics/diagnostic-augmentation.ts +0 -82
  51. package/src/diagnostics/diagnostic-context.ts +0 -116
  52. package/src/diagnostics/diagnostic-display.ts +0 -69
  53. package/src/manager/capability-index.ts +0 -24
  54. package/src/manager/client-pool.ts +0 -33
  55. package/src/manager/diagnostic-store.ts +0 -51
  56. package/src/manager/manager-stale-resync.ts +0 -47
  57. package/src/manager/recovery-coordinator.ts +0 -37
  58. package/src/manager/workspace-router.ts +0 -44
@@ -1,12 +1,16 @@
1
- // Shared session-scoped LSP service registry.
2
- // Peer extensions can import `getSessionLspService` from the package root
3
- // to reuse the active LSP runtime without starting duplicate servers.
1
+ // Shared session-scoped LSP service registry reused by peer extensions.
4
2
 
3
+ import {
4
+ type CodeQueryResult,
5
+ mapCodeQueryResult,
6
+ unavailableCodeQuery,
7
+ } from "@mrclrchtr/supi-code-runtime/api";
5
8
  import { createSessionStateRegistry } from "@mrclrchtr/supi-core/session";
6
9
  import type {
7
10
  CodeAction,
8
11
  Diagnostic,
9
12
  DocumentSymbol,
13
+ FileEvent,
10
14
  Hover,
11
15
  Location,
12
16
  LocationLink,
@@ -19,11 +23,16 @@ import type {
19
23
  } from "../config/types.ts";
20
24
  import type { LspManager } from "../manager/manager.ts";
21
25
  import { resolveSessionPath } from "../utils.ts";
26
+ import { raceReadinessValue } from "./readiness.ts";
22
27
 
23
28
  function isRange(value: Position | Range): value is Range {
24
29
  return "start" in value && "end" in value;
25
30
  }
26
31
 
32
+ function unavailableFileQuery<T>(operation: string, file: string): CodeQueryResult<T> {
33
+ return unavailableCodeQuery(`No routed LSP client could complete ${operation} for ${file}.`);
34
+ }
35
+
27
36
  /** Workspace diagnostic summary grouped by file. */
28
37
  export interface WorkspaceDiagnosticSummaryEntry {
29
38
  file: string;
@@ -43,7 +52,8 @@ export interface OutstandingDiagnosticSummaryEntry {
43
52
 
44
53
  /** Result from a workspace diagnostic recovery pass. */
45
54
  export interface RecoverDiagnosticsResult {
46
- refreshedClients: number;
55
+ /** Active clients targeted by the best-effort refresh, not confirmed successful refreshes. */
56
+ attemptedClients: number;
47
57
  restartedClients: number;
48
58
  staleAssessment: {
49
59
  suspected: boolean;
@@ -52,9 +62,9 @@ export interface RecoverDiagnosticsResult {
52
62
  };
53
63
  }
54
64
 
55
- export type SessionLspServiceState =
56
- | { kind: "ready"; service: SessionLspService }
57
- | { kind: "inactive"; service: SessionLspService }
65
+ export type WorkspaceLspRuntimeState =
66
+ | { kind: "ready"; runtime: WorkspaceLspRuntime }
67
+ | { kind: "inactive"; runtime: WorkspaceLspRuntime }
58
68
  | { kind: "pending" }
59
69
  | { kind: "disabled" }
60
70
  | { kind: "unavailable"; reason: string };
@@ -65,59 +75,116 @@ export type SemanticReadinessResult =
65
75
  | { kind: "unavailable"; reason: string };
66
76
 
67
77
  /**
68
- * Public wrapper around {@link LspManager} that exposes stable semantic operations.
78
+ * Workspace-scoped LSP interface that owns routing, readiness, semantic operations,
79
+ * diagnostics, and recovery without exposing clients or the mutable manager.
69
80
  * File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
70
81
  * to match pi's built-in path-tool convention. Position arguments use raw 0-based LSP
71
82
  * coordinates; use `toLspPosition()` from `@mrclrchtr/supi-lsp/api` when starting from
72
83
  * user-facing 1-based line and character values.
73
84
  */
74
- export class SessionLspService {
85
+ export interface WorkspaceLspRuntime {
86
+ hover(filePath: string, position: Position): Promise<CodeQueryResult<Hover | null>>;
87
+ definition(
88
+ filePath: string,
89
+ position: Position,
90
+ ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>>;
91
+ references(filePath: string, position: Position): Promise<CodeQueryResult<Location[]>>;
92
+ implementation(
93
+ filePath: string,
94
+ position: Position,
95
+ ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>>;
96
+ documentSymbols(
97
+ filePath: string,
98
+ ): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>>;
99
+ workspaceSymbol(query: string): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[]>>;
100
+ rename(filePath: string, position: Position, newName: string): Promise<WorkspaceEdit | null>;
101
+ codeActions(filePath: string, positionOrRange: Position | Range): Promise<CodeAction[] | null>;
102
+ /** Succeeds only when the concrete routed client exists and is query-ready. */
103
+ waitUntilReadyForFile(
104
+ filePath: string,
105
+ options?: { timeoutMs?: number },
106
+ ): Promise<SemanticReadinessResult>;
107
+ /** Succeeds only when at least one concrete workspace client is active and query-ready. */
108
+ waitUntilReadyForWorkspace(options?: { timeoutMs?: number }): Promise<SemanticReadinessResult>;
109
+ getProjectServers(): ProjectServerInfo[];
110
+ isSupportedSourceFile(filePath: string): boolean;
111
+ trackFile(filePath: string): Promise<boolean>;
112
+ closeFile(filePath: string): void;
113
+ pruneMissingFiles(): readonly string[];
114
+ noteWorkspaceChanges(changes: FileEvent[]): void;
115
+ fileDiagnostics(filePath: string, maxSeverity?: number): Promise<CodeQueryResult<Diagnostic[]>>;
116
+ refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void>;
117
+ getWorkspaceDiagnosticSummary(): WorkspaceDiagnosticSummaryEntry[];
118
+ getOutstandingDiagnostics(
119
+ maxSeverity?: number,
120
+ ): Array<{ file: string; diagnostics: Diagnostic[] }>;
121
+ getOutstandingDiagnosticSummary(maxSeverity?: number): OutstandingDiagnosticSummaryEntry[];
122
+ recoverDiagnostics(options?: {
123
+ restartIfStillStale?: boolean;
124
+ maxWaitMs?: number;
125
+ quietMs?: number;
126
+ }): Promise<RecoverDiagnosticsResult>;
127
+ }
128
+
129
+ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
75
130
  constructor(private readonly manager: LspManager) {}
76
131
 
132
+ static createOwner(manager: LspManager) {
133
+ const runtime = new DefaultWorkspaceLspRuntime(manager);
134
+ return { runtime: runtime as WorkspaceLspRuntime, shutdown: () => runtime.#shutdown() };
135
+ }
136
+
77
137
  // ── Semantic lookups ────────────────────────────────────────────────
78
138
 
79
- async hover(filePath: string, position: Position): Promise<Hover | null> {
139
+ async hover(filePath: string, position: Position): Promise<CodeQueryResult<Hover | null>> {
80
140
  const resolvedPath = this.resolveFilePath(filePath);
81
141
  const client = await this.manager.ensureFileOpen(resolvedPath);
82
- if (!client) return null;
142
+ if (!client) return unavailableFileQuery("hover", resolvedPath);
83
143
  return client.hover(resolvedPath, position);
84
144
  }
85
145
 
86
146
  async definition(
87
147
  filePath: string,
88
148
  position: Position,
89
- ): Promise<Location | Location[] | LocationLink[] | null> {
149
+ ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
90
150
  const resolvedPath = this.resolveFilePath(filePath);
91
151
  const client = await this.manager.ensureFileOpen(resolvedPath);
92
- if (!client) return null;
152
+ if (!client) return unavailableFileQuery("definition", resolvedPath);
93
153
  return client.definition(resolvedPath, position);
94
154
  }
95
155
 
96
- async references(filePath: string, position: Position): Promise<Location[] | null> {
156
+ async references(filePath: string, position: Position): Promise<CodeQueryResult<Location[]>> {
97
157
  const resolvedPath = this.resolveFilePath(filePath);
98
158
  const client = await this.manager.ensureFileOpen(resolvedPath);
99
- if (!client) return null;
100
- return client.references(resolvedPath, position);
159
+ if (!client) return unavailableFileQuery("references", resolvedPath);
160
+ return mapCodeQueryResult(
161
+ await client.references(resolvedPath, position),
162
+ (data) => data ?? [],
163
+ );
101
164
  }
102
165
 
103
166
  async implementation(
104
167
  filePath: string,
105
168
  position: Position,
106
- ): Promise<Location | Location[] | LocationLink[] | null> {
169
+ ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
107
170
  const resolvedPath = this.resolveFilePath(filePath);
108
171
  const client = await this.manager.ensureFileOpen(resolvedPath);
109
- if (!client) return null;
172
+ if (!client) return unavailableFileQuery("implementation", resolvedPath);
110
173
  return client.implementation(resolvedPath, position);
111
174
  }
112
175
 
113
- async documentSymbols(filePath: string): Promise<DocumentSymbol[] | SymbolInformation[] | null> {
176
+ async documentSymbols(
177
+ filePath: string,
178
+ ): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>> {
114
179
  const resolvedPath = this.resolveFilePath(filePath);
115
180
  const client = await this.manager.ensureFileOpen(resolvedPath);
116
- if (!client) return null;
117
- return client.documentSymbols(resolvedPath);
181
+ if (!client) return unavailableFileQuery("document symbols", resolvedPath);
182
+ return mapCodeQueryResult(await client.documentSymbols(resolvedPath), (data) => data ?? []);
118
183
  }
119
184
 
120
- async workspaceSymbol(query: string): Promise<SymbolInformation[] | WorkspaceSymbol[] | null> {
185
+ async workspaceSymbol(
186
+ query: string,
187
+ ): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[]>> {
121
188
  return this.manager.workspaceSymbol(query);
122
189
  }
123
190
 
@@ -167,7 +234,18 @@ export class SessionLspService {
167
234
  };
168
235
  }
169
236
 
170
- return raceReadiness(this.manager.waitUntilFileReady(resolvedPath), options.timeoutMs);
237
+ const readiness = await raceReadinessValue(
238
+ this.manager.waitUntilFileReady(resolvedPath),
239
+ options.timeoutMs,
240
+ );
241
+ if (readiness.kind !== "resolved") return readiness;
242
+ if (readiness.value === null) {
243
+ return {
244
+ kind: "unavailable",
245
+ reason: "The routed LSP client could not be started for this file",
246
+ };
247
+ }
248
+ return { kind: "ready" };
171
249
  }
172
250
 
173
251
  /**
@@ -177,11 +255,20 @@ export class SessionLspService {
177
255
  async waitUntilReadyForWorkspace(
178
256
  options: { timeoutMs?: number } = {},
179
257
  ): Promise<SemanticReadinessResult> {
180
- return raceReadiness(this.manager.waitUntilWorkspaceReady(), options.timeoutMs);
258
+ const readiness = await raceReadinessValue(
259
+ this.manager.waitUntilWorkspaceReady(),
260
+ options.timeoutMs,
261
+ );
262
+ if (readiness.kind !== "resolved") return readiness;
263
+ if (readiness.value === 0) {
264
+ return {
265
+ kind: "unavailable",
266
+ reason: "No active LSP clients are ready for this workspace",
267
+ };
268
+ }
269
+ return { kind: "ready" };
181
270
  }
182
271
 
183
- // ── Project / runtime awareness ─────────────────────────────────────
184
-
185
272
  getProjectServers(): ProjectServerInfo[] {
186
273
  return this.manager.getKnownProjectServers([]);
187
274
  }
@@ -191,15 +278,51 @@ export class SessionLspService {
191
278
  return this.manager.canServeFile(this.resolveFilePath(filePath));
192
279
  }
193
280
 
194
- // ── Diagnostics ─────────────────────────────────────────────────────
281
+ /** Track a file in its routed client without exposing that client. */
282
+ async trackFile(filePath: string): Promise<boolean> {
283
+ const resolvedPath = this.resolveFilePath(filePath);
284
+ return (await this.manager.ensureFileOpen(resolvedPath)) !== null;
285
+ }
286
+
287
+ /** Stop tracking a file and clear its cached diagnostics. */
288
+ closeFile(filePath: string): void {
289
+ this.manager.closeFile(this.resolveFilePath(filePath));
290
+ }
291
+
292
+ /** Remove missing files from runtime tracking. */
293
+ pruneMissingFiles(): readonly string[] {
294
+ return this.manager.pruneMissingFiles();
295
+ }
296
+
297
+ /** Notify routed clients of workspace file changes and reset pull state. */
298
+ noteWorkspaceChanges(changes: FileEvent[]): void {
299
+ this.manager.clearAllPullResultIds();
300
+ this.manager.notifyWorkspaceFileChanges(changes);
301
+ }
302
+
303
+ async #shutdown(): Promise<void> {
304
+ await this.manager.shutdownAll();
305
+ }
306
+
307
+ // ── Diagnostics and recovery ────────────────────────────────────────
195
308
 
196
309
  /** Sync a file through LSP and return diagnostics up to the supplied severity threshold. */
197
- async fileDiagnostics(filePath: string, maxSeverity: number = 4): Promise<Diagnostic[] | null> {
310
+ async fileDiagnostics(
311
+ filePath: string,
312
+ maxSeverity: number = 4,
313
+ ): Promise<CodeQueryResult<Diagnostic[]>> {
198
314
  const resolvedPath = this.resolveFilePath(filePath);
199
- if (!this.manager.canServeFile(resolvedPath)) return null;
315
+ if (!this.manager.canServeFile(resolvedPath)) {
316
+ return unavailableFileQuery("diagnostics", resolvedPath);
317
+ }
200
318
  return this.manager.syncFileAndGetDiagnostics(resolvedPath, maxSeverity);
201
319
  }
202
320
 
321
+ /** Re-sync every open document and wait for diagnostics to settle. */
322
+ async refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void> {
323
+ await this.manager.refreshOpenDiagnostics(options);
324
+ }
325
+
203
326
  /** Get a lightweight workspace diagnostic summary for all tracked files. */
204
327
  getWorkspaceDiagnosticSummary(): WorkspaceDiagnosticSummaryEntry[] {
205
328
  return this.manager.getDiagnosticSummary();
@@ -231,51 +354,20 @@ export class SessionLspService {
231
354
  }
232
355
  }
233
356
 
234
- // ── Registry ──────────────────────────────────────────────────────────
357
+ export function createWorkspaceLspRuntimeOwner(manager: LspManager) {
358
+ return DefaultWorkspaceLspRuntime.createOwner(manager);
359
+ }
235
360
 
236
361
  const WAIT_INTERVAL_MS = 25;
237
- const DEFAULT_SEMANTIC_READY_TIMEOUT_MS = 15_000;
238
- const registry = createSessionStateRegistry<SessionLspServiceState>("supi-lsp/session-registry");
239
-
240
- async function raceReadiness(
241
- readiness: Promise<unknown>,
242
- timeoutMs: number | undefined,
243
- ): Promise<SemanticReadinessResult> {
244
- const effectiveTimeoutMs = timeoutMs ?? DEFAULT_SEMANTIC_READY_TIMEOUT_MS;
245
- let timer: ReturnType<typeof setTimeout> | null = null;
246
-
247
- try {
248
- await Promise.race([
249
- readiness,
250
- new Promise<never>((_, reject) => {
251
- timer = setTimeout(
252
- () => reject(new Error("semantic-readiness-timeout")),
253
- effectiveTimeoutMs,
254
- );
255
- }),
256
- ]);
257
- return { kind: "ready" };
258
- } catch (error) {
259
- if (timer) clearTimeout(timer);
260
- if (error instanceof Error && error.message === "semantic-readiness-timeout") {
261
- return { kind: "timeout" };
262
- }
263
- return {
264
- kind: "unavailable",
265
- reason: error instanceof Error ? error.message : String(error),
266
- };
267
- } finally {
268
- if (timer) clearTimeout(timer);
269
- }
270
- }
362
+ const registry = createSessionStateRegistry<WorkspaceLspRuntimeState>("supi-lsp/session-registry");
271
363
 
272
364
  /** Publish the LSP service state for a session cwd. */
273
- export function setSessionLspServiceState(cwd: string, state: SessionLspServiceState): void {
365
+ export function setWorkspaceLspRuntimeState(cwd: string, state: WorkspaceLspRuntimeState): void {
274
366
  registry.set(cwd, state);
275
367
  }
276
368
 
277
369
  /** Acquire the LSP service state for a session cwd. */
278
- export function getSessionLspService(cwd: string): SessionLspServiceState {
370
+ export function getWorkspaceLspRuntime(cwd: string): WorkspaceLspRuntimeState {
279
371
  return (
280
372
  registry.get(cwd) ?? {
281
373
  kind: "unavailable",
@@ -285,22 +377,22 @@ export function getSessionLspService(cwd: string): SessionLspServiceState {
285
377
  }
286
378
 
287
379
  /** Wait briefly for a pending session-scoped LSP service to become ready. */
288
- export async function waitForSessionLspService(
380
+ export async function waitForWorkspaceLspRuntime(
289
381
  cwd: string,
290
382
  timeoutMs: number = 250,
291
- ): Promise<SessionLspServiceState> {
383
+ ): Promise<WorkspaceLspRuntimeState> {
292
384
  const deadline = Date.now() + Math.max(0, timeoutMs);
293
- let state = getSessionLspService(cwd);
385
+ let state = getWorkspaceLspRuntime(cwd);
294
386
 
295
387
  while (state.kind === "pending" && Date.now() < deadline) {
296
388
  await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS));
297
- state = getSessionLspService(cwd);
389
+ state = getWorkspaceLspRuntime(cwd);
298
390
  }
299
391
 
300
392
  return state;
301
393
  }
302
394
 
303
395
  /** Remove the LSP service state for a session cwd. */
304
- export function clearSessionLspService(cwd: string): void {
396
+ export function clearWorkspaceLspRuntime(cwd: string): void {
305
397
  registry.clear(cwd);
306
398
  }
@@ -1,31 +0,0 @@
1
- /**
2
- * Abort controller/signal utility helpers.
3
- *
4
- * @module
5
- */
6
-
7
- /**
8
- * Combine a caller-provided abort controller with a generation timeout signal.
9
- *
10
- * Returns the combined signal and a cleanup function that removes both
11
- * event listeners. Callers must invoke cleanup in a `finally` block
12
- * to avoid listener leaks.
13
- */
14
- export function combineAbortSignals(
15
- abort: AbortController,
16
- timeoutMs: number,
17
- ): { signal: AbortSignal; cleanup: () => void } {
18
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
19
- const combinedController = new AbortController();
20
- const onAbort = () => combinedController.abort();
21
- abort.signal.addEventListener("abort", onAbort, { once: true });
22
- timeoutSignal.addEventListener("abort", onAbort, { once: true });
23
-
24
- return {
25
- signal: combinedController.signal,
26
- cleanup: () => {
27
- abort.signal.removeEventListener("abort", onAbort);
28
- timeoutSignal.removeEventListener("abort", onAbort);
29
- },
30
- };
31
- }
@@ -1,11 +0,0 @@
1
- /** 0-based position used by LSP and code-intelligence internally. */
2
- export interface CodePosition {
3
- line: number;
4
- character: number;
5
- }
6
-
7
- /** Normalized location — flat replacement for LSP's nested Location/range shape. */
8
- export interface CodeLocation {
9
- uri: string;
10
- range: { start: CodePosition; end: CodePosition };
11
- }
@@ -1,2 +0,0 @@
1
- // supi-core types domain — shared substrate types (zero dependencies, pure types).
2
- export type { CodeLocation, CodePosition } from "./substrate-types.ts";
@@ -1,31 +0,0 @@
1
- /**
2
- * Abort controller/signal utility helpers.
3
- *
4
- * @module
5
- */
6
-
7
- /**
8
- * Combine a caller-provided abort controller with a generation timeout signal.
9
- *
10
- * Returns the combined signal and a cleanup function that removes both
11
- * event listeners. Callers must invoke cleanup in a `finally` block
12
- * to avoid listener leaks.
13
- */
14
- export function combineAbortSignals(
15
- abort: AbortController,
16
- timeoutMs: number,
17
- ): { signal: AbortSignal; cleanup: () => void } {
18
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
19
- const combinedController = new AbortController();
20
- const onAbort = () => combinedController.abort();
21
- abort.signal.addEventListener("abort", onAbort, { once: true });
22
- timeoutSignal.addEventListener("abort", onAbort, { once: true });
23
-
24
- return {
25
- signal: combinedController.signal,
26
- cleanup: () => {
27
- abort.signal.removeEventListener("abort", onAbort);
28
- timeoutSignal.removeEventListener("abort", onAbort);
29
- },
30
- };
31
- }
@@ -1,11 +0,0 @@
1
- /** 0-based position used by LSP and code-intelligence internally. */
2
- export interface CodePosition {
3
- line: number;
4
- character: number;
5
- }
6
-
7
- /** Normalized location — flat replacement for LSP's nested Location/range shape. */
8
- export interface CodeLocation {
9
- uri: string;
10
- range: { start: CodePosition; end: CodePosition };
11
- }
@@ -1,2 +0,0 @@
1
- // supi-core types domain — shared substrate types (zero dependencies, pure types).
2
- export type { CodeLocation, CodePosition } from "./substrate-types.ts";
@@ -1,82 +0,0 @@
1
- import type { Diagnostic, Hover, MarkedString, MarkupContent } from "../config/types.ts";
2
- import type { LspManager } from "../manager/manager.ts";
3
- import { resolveSessionPath } from "../utils.ts";
4
-
5
- const AUGMENT_TIMEOUT_MS = 500;
6
-
7
- /**
8
- * Augment diagnostics with LSP hover and code_actions at the first severity-1 error.
9
- * Silently returns null if LSP is unavailable, times out, or there are no errors.
10
- */
11
- export async function augmentDiagnostics(
12
- filePath: string,
13
- diags: Diagnostic[],
14
- manager: LspManager,
15
- cwd: string,
16
- ): Promise<string | null> {
17
- const firstError = diags.find((d) => d.severity === 1);
18
- if (!firstError) return null;
19
-
20
- const resolvedPath = resolveSessionPath(cwd, filePath);
21
- const client = await manager.getClientForFile(resolvedPath);
22
- if (!client) return null;
23
-
24
- const pos = firstError.range.start;
25
-
26
- const [hoverResult, codeActionsResult] = await Promise.all([
27
- withTimeout(client.hover(resolvedPath, pos), AUGMENT_TIMEOUT_MS),
28
- withTimeout(
29
- client.codeActions(resolvedPath, { start: pos, end: pos }, { diagnostics: [firstError] }),
30
- AUGMENT_TIMEOUT_MS,
31
- ),
32
- ]);
33
-
34
- const parts: string[] = [];
35
-
36
- if (hoverResult) {
37
- const hoverText = formatHoverForDiagnostics(hoverResult);
38
- if (hoverText) parts.push(`💡 Hover info:\n${hoverText}`);
39
- }
40
-
41
- if (codeActionsResult && codeActionsResult.length > 0) {
42
- const titles = codeActionsResult.map((a) => a.title).join(", ");
43
- parts.push(`💡 Available fix: ${titles}`);
44
- }
45
-
46
- return parts.length > 0 ? parts.join("\n") : null;
47
- }
48
-
49
- /**
50
- * Extract raw hover text for inline diagnostic augmentation.
51
- * Intentionally strips markdown code-block framing (unlike formatHover)
52
- * to keep augmentation concise and readable inside diagnostic output.
53
- */
54
- function formatHoverForDiagnostics(hover: Hover): string {
55
- const contents = hover.contents;
56
- let text = "";
57
-
58
- if (typeof contents === "string") {
59
- text = contents;
60
- } else if ("value" in contents) {
61
- const mc = contents as MarkupContent | { language: string; value: string };
62
- if ("kind" in mc) text = mc.value;
63
- else text = mc.value;
64
- } else if (Array.isArray(contents)) {
65
- text = (contents as MarkedString[])
66
- .map((c) => (typeof c === "string" ? c : c.value))
67
- .join("\n");
68
- }
69
-
70
- const lines = text.split("\n").slice(0, 3);
71
- return lines.join("\n");
72
- }
73
-
74
- async function withTimeout<T>(promise: Promise<T | null>, ms: number): Promise<T | null> {
75
- let timer: ReturnType<typeof setTimeout> | null = null;
76
- const timeoutPromise = new Promise<null>((resolve) => {
77
- timer = setTimeout(() => resolve(null), ms);
78
- });
79
- const result = await Promise.race([promise, timeoutPromise]);
80
- if (timer) clearTimeout(timer);
81
- return result;
82
- }
@@ -1,116 +0,0 @@
1
- // Diagnostic context formatting for the LSP extension.
2
- // Extracted from guidance.ts to keep prompt surfaces separate from formatting logic.
3
-
4
- import type { Diagnostic } from "../config/types.ts";
5
- import type { OutstandingDiagnosticSummaryEntry } from "../manager/manager-types.ts";
6
- import { splitSuppressionDiagnostics } from "./suppression-diagnostics.ts";
7
-
8
- export const MAX_DETAILED_DIAGNOSTICS = 5;
9
- const MAX_DETAIL_LINES_PER_FILE = 3;
10
-
11
- interface DetailedDiagnostics {
12
- file: string;
13
- diagnostics: Diagnostic[];
14
- }
15
-
16
- export function formatDiagnosticsContext(
17
- diagnostics: OutstandingDiagnosticSummaryEntry[],
18
- maxFiles: number = 3,
19
- detailed?: DetailedDiagnostics[],
20
- staleWarning?: string | null,
21
- ): string | null {
22
- if (diagnostics.length === 0) return null;
23
-
24
- const totalDiags = diagnostics.reduce((sum, d) => sum + d.total, 0);
25
- const detailMap = buildDetailMap(totalDiags, detailed);
26
-
27
- const lines: string[] = [];
28
- if (staleWarning) lines.push(staleWarning);
29
- const visible = diagnostics.slice(0, maxFiles);
30
-
31
- for (const entry of visible) {
32
- lines.push(`- ${entry.file}: ${formatCounts(entry)}`);
33
- appendDetailLines(lines, detailMap?.get(entry.file));
34
- }
35
-
36
- const remaining = diagnostics.length - visible.length;
37
- if (remaining > 0) {
38
- lines.push(`- +${remaining} more file${remaining === 1 ? "" : "s"}`);
39
- }
40
-
41
- appendSuppressionCleanup(
42
- lines,
43
- visible.map((entry) => entry.file),
44
- detailMap,
45
- );
46
-
47
- return [
48
- '<extension-context source="supi-lsp">',
49
- "Outstanding diagnostics — fix these before proceeding:",
50
- ...lines,
51
- "</extension-context>",
52
- ].join("\n");
53
- }
54
-
55
- function buildDetailMap(
56
- totalDiags: number,
57
- detailed?: DetailedDiagnostics[],
58
- ): Map<string, Diagnostic[]> | null {
59
- if (totalDiags > MAX_DETAILED_DIAGNOSTICS || !detailed || detailed.length === 0) return null;
60
- return new Map(detailed.map((d) => [d.file, d.diagnostics]));
61
- }
62
-
63
- function appendDetailLines(lines: string[], details?: Diagnostic[]): void {
64
- if (!details) return;
65
- for (const d of details.slice(0, MAX_DETAIL_LINES_PER_FILE)) {
66
- const line = d.range.start.line + 1;
67
- const char = d.range.start.character + 1;
68
- const source = d.source ? ` ${d.source}` : "";
69
- const msg = typeof d.message === "string" ? d.message : d.message.value;
70
- lines.push(` L${line} C${char}${source}: ${msg}`);
71
- }
72
- if (details.length > MAX_DETAIL_LINES_PER_FILE) {
73
- const extra = details.length - MAX_DETAIL_LINES_PER_FILE;
74
- lines.push(` +${extra} more`);
75
- }
76
- }
77
-
78
- function appendSuppressionCleanup(
79
- lines: string[],
80
- visibleFiles: string[],
81
- detailMap: Map<string, Diagnostic[]> | null,
82
- ): void {
83
- if (!detailMap) return;
84
-
85
- const suppressionLines: string[] = [];
86
- for (const file of visibleFiles) {
87
- const diagnostics = detailMap.get(file);
88
- if (!diagnostics) continue;
89
-
90
- const { suppressions } = splitSuppressionDiagnostics(diagnostics, 1);
91
- if (suppressions.length === 0) continue;
92
-
93
- suppressionLines.push(`- ${file}`);
94
- appendDetailLines(suppressionLines, suppressions);
95
- }
96
-
97
- if (suppressionLines.length === 0) return;
98
- lines.push("", "Stale suppression comments — clean these up:", ...suppressionLines);
99
- }
100
-
101
- export function diagnosticsContextFingerprint(content: string | null): string | null {
102
- return content;
103
- }
104
-
105
- function formatCounts(entry: OutstandingDiagnosticSummaryEntry): string {
106
- const counts: string[] = [];
107
- if (entry.errors > 0) counts.push(pluralize(entry.errors, "error"));
108
- if (entry.warnings > 0) counts.push(pluralize(entry.warnings, "warning"));
109
- if (entry.information > 0) counts.push(pluralize(entry.information, "info"));
110
- if (entry.hints > 0) counts.push(pluralize(entry.hints, "hint"));
111
- return counts.join(", ");
112
- }
113
-
114
- function pluralize(count: number, word: string): string {
115
- return `${count} ${word}${count === 1 ? "" : "s"}`;
116
- }