@mrclrchtr/supi-lsp 1.16.0 → 2.0.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 (77) hide show
  1. package/README.md +44 -56
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +6 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
  12. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +3 -2
  16. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +7 -0
  17. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +56 -2
  18. package/node_modules/@mrclrchtr/supi-code-runtime/src/evidence-badge.ts +40 -0
  19. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +5 -0
  20. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +76 -3
  21. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +37 -2
  22. package/node_modules/@mrclrchtr/supi-core/package.json +6 -1
  23. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
  26. package/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
  28. package/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
  31. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
  32. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
  33. package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  34. package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  35. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.d.ts +21 -2
  36. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.js +1 -0
  37. package/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlineValue.d.ts +2 -1
  38. package/node_modules/vscode-languageserver-protocol/package.json +1 -1
  39. package/package.json +4 -31
  40. package/src/api.ts +27 -0
  41. package/src/client/client-refresh.ts +5 -0
  42. package/src/client/client.ts +246 -1
  43. package/src/client/transport.ts +4 -0
  44. package/src/config/capabilities.ts +3 -0
  45. package/src/config/config.ts +4 -2
  46. package/src/config/lsp-settings.ts +102 -0
  47. package/src/config/server-actions.ts +59 -0
  48. package/src/config/server-config.ts +4 -0
  49. package/src/index.ts +11 -0
  50. package/src/manager/capability-index.ts +1 -1
  51. package/src/manager/manager-project-info.ts +2 -1
  52. package/src/manager/manager.ts +115 -26
  53. package/src/provider/lsp-semantic-provider.ts +177 -134
  54. package/src/provider/refactor-planning.ts +214 -0
  55. package/src/session/runtime-controller.ts +320 -0
  56. package/src/session/runtime-registration.ts +26 -5
  57. package/src/session/service-registry.ts +81 -4
  58. package/src/summary.ts +2 -0
  59. package/src/extension.ts +0 -1
  60. package/src/format.ts +0 -373
  61. package/src/handlers/diagnostic-injection.ts +0 -182
  62. package/src/handlers/session-lifecycle.ts +0 -149
  63. package/src/handlers/status-command.ts +0 -38
  64. package/src/handlers/workspace-recovery.ts +0 -92
  65. package/src/lsp.ts +0 -37
  66. package/src/session/lsp-state.ts +0 -93
  67. package/src/session/settings-registration.ts +0 -296
  68. package/src/session/tree-persist.ts +0 -75
  69. package/src/tool/guidance.ts +0 -69
  70. package/src/tool/names.ts +0 -27
  71. package/src/tool/overrides.ts +0 -204
  72. package/src/tool/register-tools.ts +0 -69
  73. package/src/tool/service-actions.ts +0 -399
  74. package/src/tool/tool-specs.ts +0 -248
  75. package/src/ui/renderer.ts +0 -120
  76. package/src/ui/ui.ts +0 -360
  77. package/src/workspace-change.ts +0 -40
@@ -12,6 +12,7 @@ import type {
12
12
  LocationLink,
13
13
  Position,
14
14
  ProjectServerInfo,
15
+ Range,
15
16
  SymbolInformation,
16
17
  WorkspaceEdit,
17
18
  WorkspaceSymbol,
@@ -19,6 +20,10 @@ import type {
19
20
  import type { LspManager } from "../manager/manager.ts";
20
21
  import { resolveSessionPath } from "../utils.ts";
21
22
 
23
+ function isRange(value: Position | Range): value is Range {
24
+ return "start" in value && "end" in value;
25
+ }
26
+
22
27
  /** Workspace diagnostic summary grouped by file. */
23
28
  export interface WorkspaceDiagnosticSummaryEntry {
24
29
  file: string;
@@ -54,6 +59,11 @@ export type SessionLspServiceState =
54
59
  | { kind: "disabled" }
55
60
  | { kind: "unavailable"; reason: string };
56
61
 
62
+ export type SemanticReadinessResult =
63
+ | { kind: "ready" }
64
+ | { kind: "timeout" }
65
+ | { kind: "unavailable"; reason: string };
66
+
57
67
  /**
58
68
  * Public wrapper around {@link LspManager} that exposes stable semantic operations.
59
69
  * File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
@@ -122,20 +132,54 @@ export class SessionLspService {
122
132
  return client.rename(resolvedPath, position, newName);
123
133
  }
124
134
 
125
- async codeActions(filePath: string, position: Position): Promise<CodeAction[] | null> {
135
+ async codeActions(
136
+ filePath: string,
137
+ positionOrRange: Position | Range,
138
+ ): Promise<CodeAction[] | null> {
126
139
  const resolvedPath = this.resolveFilePath(filePath);
127
140
  const client = await this.manager.ensureFileOpen(resolvedPath);
128
141
  if (!client) return null;
129
142
 
130
- const range = { start: position, end: position };
143
+ const range = isRange(positionOrRange)
144
+ ? positionOrRange
145
+ : { start: positionOrRange, end: positionOrRange };
131
146
  const diagnostics = client
132
147
  .getDiagnostics(resolvedPath)
133
- .filter((diagnostic) => diagnostic.range.start.line <= position.line)
134
- .filter((diagnostic) => diagnostic.range.end.line >= position.line);
148
+ .filter((diagnostic) => diagnostic.range.end.line >= range.start.line)
149
+ .filter((diagnostic) => diagnostic.range.start.line <= range.end.line);
135
150
 
136
151
  return client.codeActions(resolvedPath, range, { diagnostics });
137
152
  }
138
153
 
154
+ /**
155
+ * Wait until the LSP client that owns a file is ready for semantic queries.
156
+ * Performs a lightweight semantic warm-up probe before resolving.
157
+ */
158
+ async waitUntilReadyForFile(
159
+ filePath: string,
160
+ options: { timeoutMs?: number } = {},
161
+ ): Promise<SemanticReadinessResult> {
162
+ const resolvedPath = this.resolveFilePath(filePath);
163
+ if (!this.manager.canServeFile(resolvedPath)) {
164
+ return {
165
+ kind: "unavailable",
166
+ reason: "No LSP client can serve this file",
167
+ };
168
+ }
169
+
170
+ return raceReadiness(this.manager.waitUntilFileReady(resolvedPath), options.timeoutMs);
171
+ }
172
+
173
+ /**
174
+ * Wait until all started LSP clients are ready for semantic queries.
175
+ * Performs one representative warm-up probe per client/root.
176
+ */
177
+ async waitUntilReadyForWorkspace(
178
+ options: { timeoutMs?: number } = {},
179
+ ): Promise<SemanticReadinessResult> {
180
+ return raceReadiness(this.manager.waitUntilWorkspaceReady(), options.timeoutMs);
181
+ }
182
+
139
183
  // ── Project / runtime awareness ─────────────────────────────────────
140
184
 
141
185
  getProjectServers(): ProjectServerInfo[] {
@@ -190,8 +234,41 @@ export class SessionLspService {
190
234
  // ── Registry ──────────────────────────────────────────────────────────
191
235
 
192
236
  const WAIT_INTERVAL_MS = 25;
237
+ const DEFAULT_SEMANTIC_READY_TIMEOUT_MS = 15_000;
193
238
  const registry = createSessionStateRegistry<SessionLspServiceState>("supi-lsp/session-registry");
194
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
+ }
271
+
195
272
  /** Publish the LSP service state for a session cwd. */
196
273
  export function setSessionLspServiceState(cwd: string, state: SessionLspServiceState): void {
197
274
  registry.set(cwd, state);
package/src/summary.ts CHANGED
@@ -112,6 +112,8 @@ export function isProjectSource(filePath: string, cwd: string): boolean {
112
112
  }
113
113
 
114
114
  export function shouldIgnoreLspPath(filePath: string, cwd: string): boolean {
115
+ if (!isInProjectTree(filePath, cwd)) return true;
116
+
115
117
  const normalized = normalizeRelevantPath(filePath);
116
118
  if (
117
119
  normalized === "node_modules" ||
package/src/extension.ts DELETED
@@ -1 +0,0 @@
1
- export { default } from "./lsp.ts";
package/src/format.ts DELETED
@@ -1,373 +0,0 @@
1
- // LSP result formatting — converts LSP response types into readable text.
2
-
3
- import * as path from "node:path";
4
- import type {
5
- AnnotatedTextEdit,
6
- CodeAction,
7
- DocumentSymbol,
8
- Hover,
9
- Location,
10
- LocationLink,
11
- MarkedString,
12
- MarkupContent,
13
- SnippetTextEdit,
14
- SymbolInformation,
15
- TextEdit,
16
- WorkspaceEdit,
17
- WorkspaceSymbol,
18
- } from "./config/types.ts";
19
- import { isProjectSource } from "./summary.ts";
20
- import { uriToFile } from "./utils.ts";
21
-
22
- // ── Hover ─────────────────────────────────────────────────────────────
23
-
24
- export function formatHover(hover: Hover): string {
25
- const contents = hover.contents;
26
-
27
- if (typeof contents === "string") return contents;
28
- if ("value" in contents) {
29
- const mc = contents as MarkupContent | { language: string; value: string };
30
- if ("kind" in mc) return mc.value;
31
- return `\`\`\`${mc.language}\n${mc.value}\n\`\`\``;
32
- }
33
- if (Array.isArray(contents)) {
34
- return (contents as MarkedString[])
35
- .map((c) => (typeof c === "string" ? c : `\`\`\`${c.language}\n${c.value}\n\`\`\``))
36
- .join("\n\n");
37
- }
38
-
39
- return String(contents);
40
- }
41
-
42
- // ── Locations ─────────────────────────────────────────────────────────
43
-
44
- function formatLocationLine(loc: Location, cwd: string): string {
45
- const file = relPath(uriToFile(loc.uri), cwd);
46
- const line = loc.range.start.line + 1;
47
- const col = loc.range.start.character + 1;
48
- return `${file}:${line}:${col}`;
49
- }
50
-
51
- function formatLocationList(label: string, locs: Location[], cwd: string): string {
52
- const lines = [`${label} (${locs.length} locations):\n`];
53
- for (const loc of locs) {
54
- lines.push(`- ${formatLocationLine(loc, cwd)}`);
55
- }
56
- return lines.join("\n");
57
- }
58
-
59
- function formatExternalFallback(label: string, locs: Location[], cwd: string): string {
60
- const maxShown = 3;
61
- const shown = locs.slice(0, maxShown);
62
- const hidden = locs.length - maxShown;
63
-
64
- let result: string;
65
- if (shown.length === 1) {
66
- result = `${label}: ${formatLocationLine(shown[0], cwd)}`;
67
- } else {
68
- const lines = [`${label} (${locs.length} locations):\n`];
69
- for (const loc of shown) {
70
- lines.push(`- ${formatLocationLine(loc, cwd)}`);
71
- }
72
- result = lines.join("\n");
73
- }
74
-
75
- if (hidden > 0) {
76
- result += `\n+${hidden} more external ${hidden === 1 ? "location" : "locations"}`;
77
- }
78
-
79
- return result;
80
- }
81
-
82
- function formatExternalSuffix(count: number): string {
83
- return count === 1
84
- ? "+1 external location (node_modules, .pnpm, or out-of-tree)"
85
- : `+${count} external locations (node_modules, .pnpm, or out-of-tree)`;
86
- }
87
-
88
- export function formatLocations(label: string, locations: Location[], cwd: string): string {
89
- const projectLocs: Location[] = [];
90
- const externalLocs: Location[] = [];
91
- for (const loc of locations) {
92
- if (isProjectSource(uriToFile(loc.uri), cwd)) {
93
- projectLocs.push(loc);
94
- } else {
95
- externalLocs.push(loc);
96
- }
97
- }
98
-
99
- let result: string;
100
-
101
- if (projectLocs.length === 1) {
102
- result = `${label}: ${formatLocationLine(projectLocs[0], cwd)}`;
103
- } else if (projectLocs.length > 1) {
104
- result = formatLocationList(label, projectLocs, cwd);
105
- } else if (externalLocs.length > 0) {
106
- return formatExternalFallback(label, externalLocs, cwd);
107
- } else {
108
- return `${label}: No locations found.`;
109
- }
110
-
111
- if (externalLocs.length > 0) {
112
- result += `\n${formatExternalSuffix(externalLocs.length)}`;
113
- }
114
-
115
- return result;
116
- }
117
-
118
- export function normalizeLocations(result: Location | Location[] | LocationLink[]): Location[] {
119
- if (Array.isArray(result)) {
120
- return result.map((item) => {
121
- if ("targetUri" in item) {
122
- const link = item as LocationLink;
123
- return { uri: link.targetUri, range: link.targetSelectionRange };
124
- }
125
- return item as Location;
126
- });
127
- }
128
- return [result as Location];
129
- }
130
-
131
- // ── Symbols ───────────────────────────────────────────────────────────
132
-
133
- export function formatDocumentSymbols(symbols: DocumentSymbol[], indent: number): string {
134
- const lines: string[] = [];
135
- const prefix = " ".repeat(indent);
136
-
137
- for (const sym of symbols) {
138
- const kind = symbolKindName(sym.kind);
139
- const line = sym.selectionRange.start.line + 1;
140
- const detail = sym.detail ? ` — ${sym.detail}` : "";
141
- lines.push(`${prefix}- ${kind} **${sym.name}**${detail} (line ${line})`);
142
-
143
- if (sym.children && sym.children.length > 0) {
144
- lines.push(formatDocumentSymbols(sym.children, indent + 1));
145
- }
146
- }
147
-
148
- return lines.join("\n");
149
- }
150
-
151
- export function formatSymbolInformation(symbols: SymbolInformation[], cwd: string): string {
152
- const projectSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
153
- const externalSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
154
- for (const sym of symbols) {
155
- if (isProjectSource(uriToFile(sym.location.uri), cwd)) {
156
- projectSyms.push(sym);
157
- } else {
158
- externalSyms.push(sym);
159
- }
160
- }
161
-
162
- const lines: string[] = [];
163
- const symbolsToShow = projectSyms.length > 0 ? projectSyms : externalSyms;
164
- for (const sym of symbolsToShow) {
165
- const kind = symbolKindName(sym.kind);
166
- const file = relPath(uriToFile(sym.location.uri), cwd);
167
- const line = "range" in sym.location ? sym.location.range.start.line + 1 : "?";
168
- const container = sym.containerName ? ` (in ${sym.containerName})` : "";
169
- lines.push(`- ${kind} **${sym.name}**${container} — ${file}:${line}`);
170
- }
171
-
172
- if (projectSyms.length > 0 && externalSyms.length > 0) {
173
- const suffix =
174
- externalSyms.length === 1
175
- ? "+1 external symbol (node_modules, .pnpm, or out-of-tree)"
176
- : `+${externalSyms.length} external symbols (node_modules, .pnpm, or out-of-tree)`;
177
- lines.push(`- _${suffix}_`);
178
- }
179
-
180
- return lines.join("\n");
181
- }
182
-
183
- // ── Workspace Edits ───────────────────────────────────────────────────
184
-
185
- interface DisplayEdit {
186
- range: { start: { line: number } };
187
- newText: string;
188
- }
189
-
190
- interface EditEntry {
191
- file: string;
192
- edits: DisplayEdit[];
193
- }
194
-
195
- /**
196
- * Normalize a TextEdit/AnnotatedTextEdit/SnippetTextEdit to a display-friendly form.
197
- *
198
- * @since 3.18.0 — vscode-languageserver-types widened TextDocumentEdit.edits to include
199
- * SnippetTextEdit (guarded by `workspace.workspaceEdit.snippetEditSupport` client capability).
200
- */
201
- function editToDisplay(edit: TextEdit | AnnotatedTextEdit | SnippetTextEdit): DisplayEdit {
202
- if ("snippet" in edit) {
203
- return { range: edit.range, newText: edit.snippet.value };
204
- }
205
- return { range: edit.range, newText: edit.newText };
206
- }
207
-
208
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pre-existing — not introduced by this change
209
- function partitionWorkspaceEdit(
210
- edit: WorkspaceEdit,
211
- cwd: string,
212
- ): { projectChanges: EditEntry[]; externalCount: number } {
213
- const projectChanges: EditEntry[] = [];
214
- let externalCount = 0;
215
-
216
- if (edit.changes) {
217
- for (const [uri, edits] of Object.entries(edit.changes)) {
218
- const filePath = uriToFile(uri);
219
- if (isProjectSource(filePath, cwd)) {
220
- projectChanges.push({
221
- file: relPath(filePath, cwd),
222
- edits: edits.map(editToDisplay),
223
- });
224
- } else {
225
- externalCount++;
226
- }
227
- }
228
- }
229
-
230
- if (edit.documentChanges) {
231
- for (const dc of edit.documentChanges) {
232
- if (!("textDocument" in dc)) continue;
233
- const filePath = uriToFile(dc.textDocument.uri);
234
- if (isProjectSource(filePath, cwd)) {
235
- projectChanges.push({
236
- file: relPath(filePath, cwd),
237
- edits: dc.edits.map(editToDisplay),
238
- });
239
- } else {
240
- externalCount++;
241
- }
242
- }
243
- }
244
-
245
- return { projectChanges, externalCount };
246
- }
247
-
248
- export function formatWorkspaceEdit(edit: WorkspaceEdit, cwd: string): string {
249
- const { projectChanges, externalCount } = partitionWorkspaceEdit(edit, cwd);
250
-
251
- const lines: string[] = ["Rename workspace edit:\n"];
252
- for (const { file, edits } of projectChanges) {
253
- lines.push(`**${file}** (${edits.length} change(s))`);
254
- for (const e of edits) {
255
- const line = e.range.start.line + 1;
256
- lines.push(` Line ${line}: → "${e.newText}"`);
257
- }
258
- }
259
-
260
- if (externalCount > 0) {
261
- const suffix =
262
- externalCount === 1
263
- ? "+1 external file (node_modules, .pnpm, or out-of-tree)"
264
- : `+${externalCount} external files (node_modules, .pnpm, or out-of-tree)`;
265
- lines.push(`\n_${suffix}_`);
266
- }
267
-
268
- return lines.join("\n");
269
- }
270
-
271
- // ── Code Actions ──────────────────────────────────────────────────────
272
-
273
- export function formatCodeActions(actions: CodeAction[]): string {
274
- const lines = [`Available code actions (${actions.length}):\n`];
275
- for (const action of actions) {
276
- const kind = action.kind ? ` [${action.kind}]` : "";
277
- const preferred = action.isPreferred ? " ⭐" : "";
278
- lines.push(`- **${action.title}**${kind}${preferred}`);
279
- if (action.edit) {
280
- const fileCount = action.edit.changes
281
- ? Object.keys(action.edit.changes).length
282
- : (action.edit.documentChanges?.length ?? 0);
283
- lines.push(` Edits ${fileCount} file(s)`);
284
- }
285
- }
286
- return lines.join("\n");
287
- }
288
-
289
- // ── Workspace Symbols ─────────────────────────────────────────────────
290
-
291
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pre-existing — threshold exceeded by widened parameter type for WorkspaceSymbol compatibility
292
- export function formatWorkspaceSymbols(
293
- symbols: (SymbolInformation | WorkspaceSymbol)[],
294
- cwd: string,
295
- ): string {
296
- if (symbols.length === 0) return "No symbols found.";
297
-
298
- const projectSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
299
- const externalSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
300
- for (const sym of symbols) {
301
- if (isProjectSource(uriToFile(sym.location.uri), cwd)) {
302
- projectSyms.push(sym);
303
- } else {
304
- externalSyms.push(sym);
305
- }
306
- }
307
-
308
- if (projectSyms.length === 0 && externalSyms.length > 0) {
309
- return `Workspace symbols: No in-project symbols found.\n+${externalSyms.length} external symbol${externalSyms.length === 1 ? "" : "s"} (node_modules, .pnpm, or out-of-tree).`;
310
- }
311
-
312
- const lines = [`Workspace symbols (${projectSyms.length}):\n`];
313
- for (const sym of projectSyms) {
314
- const kind = symbolKindName(sym.kind);
315
- const file = relPath(uriToFile(sym.location.uri), cwd);
316
- const loc = "range" in sym.location ? sym.location.range : null;
317
- const line = loc ? loc.start.line + 1 : "?";
318
- const col = loc ? loc.start.character + 1 : "?";
319
- const container = sym.containerName ? ` — ${sym.containerName}` : "";
320
- lines.push(`- **${sym.name}** (${kind})${container} — ${file}:${line}:${col}`);
321
- }
322
-
323
- if (externalSyms.length > 0) {
324
- const suffix =
325
- externalSyms.length === 1
326
- ? "+1 external symbol (node_modules, .pnpm, or out-of-tree)"
327
- : `+${externalSyms.length} external symbols (node_modules, .pnpm, or out-of-tree)`;
328
- lines.push(`\n_${suffix}_`);
329
- }
330
-
331
- return lines.join("\n");
332
- }
333
-
334
- // ── Symbol Kind Names ─────────────────────────────────────────────────
335
-
336
- const SYMBOL_KIND_NAMES: Record<number, string> = {
337
- 1: "File",
338
- 2: "Module",
339
- 3: "Namespace",
340
- 4: "Package",
341
- 5: "Class",
342
- 6: "Method",
343
- 7: "Property",
344
- 8: "Field",
345
- 9: "Constructor",
346
- 10: "Enum",
347
- 11: "Interface",
348
- 12: "Function",
349
- 13: "Variable",
350
- 14: "Constant",
351
- 15: "String",
352
- 16: "Number",
353
- 17: "Boolean",
354
- 18: "Array",
355
- 19: "Object",
356
- 20: "Key",
357
- 21: "Null",
358
- 22: "EnumMember",
359
- 23: "Struct",
360
- 24: "Event",
361
- 25: "Operator",
362
- 26: "TypeParameter",
363
- };
364
-
365
- function symbolKindName(kind: number): string {
366
- return SYMBOL_KIND_NAMES[kind] ?? `Kind(${kind})`;
367
- }
368
-
369
- // ── Helpers ───────────────────────────────────────────────────────────
370
-
371
- function relPath(filePath: string, cwd: string): string {
372
- return path.relative(cwd, filePath);
373
- }
@@ -1,182 +0,0 @@
1
- // Diagnostic injection handlers — before_agent_start and context.
2
- //
3
- // Extracted from lsp.ts to keep each orchestration concern in its own module.
4
-
5
- import type {
6
- BeforeAgentStartEvent,
7
- BeforeAgentStartEventResult,
8
- ContextEvent,
9
- ExtensionAPI,
10
- ExtensionContext,
11
- } from "@earendil-works/pi-coding-agent";
12
- import { pruneAndReorderContextMessages, restorePromptContent } from "@mrclrchtr/supi-core/context";
13
- import type { Diagnostic } from "../config/types.ts";
14
- import {
15
- diagnosticsContextFingerprint,
16
- formatDiagnosticsContext,
17
- MAX_DETAILED_DIAGNOSTICS,
18
- } from "../diagnostics/diagnostic-context.ts";
19
- import { formatDiagnosticsDisplayContent } from "../diagnostics/diagnostic-display.ts";
20
- import { assessStaleDiagnostics } from "../diagnostics/stale-diagnostics.ts";
21
- // Force re-open files with module-resolution errors
22
- import { forceResyncStaleModuleFiles } from "../manager/manager-stale-resync.ts";
23
- import type { OutstandingDiagnosticSummaryEntry } from "../manager/manager-types.ts";
24
- import {
25
- ensureLspToolsActive,
26
- type LspRuntimeState,
27
- refreshProjectServers,
28
- removeLspTools,
29
- } from "../session/lsp-state.ts";
30
- import { persistLspInactiveState } from "../session/tree-persist.ts";
31
- import { updateLspUi } from "../ui/ui.ts";
32
- import { refreshWorkspaceSentinels } from "../workspace-change.ts";
33
-
34
- /**
35
- * Register handlers for before_agent_start (diagnostic injection) and context
36
- * (lsp-context message pruning / prompt-content restoration).
37
- */
38
- export function registerDiagnosticInjectionHandlers(
39
- pi: ExtensionAPI,
40
- state: LspRuntimeState,
41
- ): void {
42
- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: before_agent_start coordinates sentinel recovery, pruning, refresh, and diagnostic injection.
43
- pi.on("before_agent_start", async (_event: BeforeAgentStartEvent, ctx: ExtensionContext) => {
44
- if (!state.manager || !state.lspActive) {
45
- removeLspTools(pi);
46
- if (!state.manager && state.lspActive) {
47
- persistLspInactiveState(pi, state);
48
- }
49
- return;
50
- }
51
-
52
- ensureLspToolsActive(pi);
53
-
54
- refreshWorkspaceSentinels(state, ctx.cwd);
55
-
56
- /**
57
- * Two-pass prune/refresh pattern:
58
- *
59
- * 1. Prune files deleted since the last turn and send didClose.
60
- * 2. Re-sync remaining open docs and wait for diagnostics to settle.
61
- * 3. Prune *again* — late publishDiagnostics notifications (already
62
- * in-flight when step 1 ran) may have re-created stale entries for
63
- * files that no longer exist. `getAllDiagnostics()` also filters
64
- * by existence, so this second pass is belt-and-suspenders.
65
- */
66
- state.manager.pruneMissingFiles();
67
- try {
68
- await state.manager.refreshOpenDiagnostics();
69
- } catch {
70
- // Refresh failures must not prevent agent startup
71
- }
72
- state.manager.pruneMissingFiles();
73
-
74
- try {
75
- await forceResyncStaleModuleFiles(state.manager, ctx.cwd);
76
- } catch {
77
- // Best-effort: don't fail the agent turn
78
- }
79
-
80
- refreshProjectServers(state);
81
- updateLspUi(ctx, state.manager, state.inlineSeverity, state.projectServers);
82
-
83
- const diagnostics = state.manager.getOutstandingDiagnosticSummary(state.inlineSeverity);
84
- const totalDiags = diagnostics.reduce((sum, d) => sum + d.total, 0);
85
- const detailed =
86
- totalDiags <= MAX_DETAILED_DIAGNOSTICS
87
- ? state.manager.getOutstandingDiagnostics(state.inlineSeverity)
88
- : undefined;
89
- const staleAssessment = state.staleSuspected
90
- ? assessStaleDiagnostics(state.manager.getOutstandingDiagnostics(4))
91
- : { suspected: false, matchedFiles: [], warning: null };
92
- state.staleSuspected = staleAssessment.suspected;
93
-
94
- const staleWarning = staleAssessment.suspected ? staleAssessment.warning : null;
95
- const content = formatDiagnosticsContext(diagnostics, 3, detailed, staleWarning);
96
- const fingerprint = diagnosticsContextFingerprint(content);
97
-
98
- if (!content) {
99
- state.lastDiagnosticsFingerprint = null;
100
- state.currentContextToken = null;
101
- return;
102
- }
103
-
104
- if (fingerprint === state.lastDiagnosticsFingerprint) {
105
- state.currentContextToken = null;
106
- return;
107
- }
108
-
109
- state.lastDiagnosticsFingerprint = fingerprint;
110
- state.currentContextToken = `lsp-context-${++state.contextCounter}`;
111
-
112
- return buildDiagnosticResult(
113
- diagnostics,
114
- detailed,
115
- state.inlineSeverity,
116
- state.currentContextToken,
117
- staleWarning,
118
- );
119
- });
120
-
121
- pi.on("context", (event: ContextEvent) => {
122
- const messages = pruneAndReorderContextMessages(
123
- event.messages as Array<{
124
- role?: string;
125
- customType?: string;
126
- content?: unknown;
127
- details?: unknown;
128
- }>,
129
- "lsp-context",
130
- state.currentContextToken,
131
- );
132
- const contextMessages = restorePromptContent(
133
- messages,
134
- "lsp-context",
135
- state.currentContextToken,
136
- ) as typeof event.messages;
137
-
138
- if (
139
- contextMessages.length === event.messages.length &&
140
- contextMessages.every((message, index) => message === event.messages[index])
141
- ) {
142
- return;
143
- }
144
- return { messages: contextMessages };
145
- });
146
- }
147
-
148
- /** Build the `lsp-context` custom message used to surface outstanding diagnostics. */
149
- // biome-ignore lint/complexity/useMaxParams: wrapper groups the prompt payload fields in one place.
150
- function buildDiagnosticResult(
151
- diagnostics: OutstandingDiagnosticSummaryEntry[],
152
- detailed:
153
- | {
154
- file: string;
155
- diagnostics: Diagnostic[];
156
- }[]
157
- | undefined,
158
- severity: number,
159
- token: string,
160
- staleWarning?: string | null,
161
- ): BeforeAgentStartEventResult {
162
- return {
163
- message: {
164
- customType: "lsp-context",
165
- content: formatDiagnosticsDisplayContent(diagnostics, detailed),
166
- display: true,
167
- details: {
168
- contextToken: token,
169
- promptContent: formatDiagnosticsContext(diagnostics, 3, detailed, staleWarning),
170
- inlineSeverity: severity,
171
- ...(staleWarning ? { staleWarning } : {}),
172
- diagnostics: diagnostics.map((d) => ({
173
- file: d.file,
174
- errors: d.errors,
175
- warnings: d.warnings,
176
- information: d.information,
177
- hints: d.hints,
178
- })),
179
- },
180
- },
181
- };
182
- }