@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12

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 (152) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
  3. package/dist/cli.js +10795 -10791
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/config/custom-models.d.ts +31 -0
  6. package/dist/types/config/model-config-values.d.ts +19 -0
  7. package/dist/types/config/model-patch.d.ts +93 -0
  8. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  9. package/dist/types/config/model-registry.d.ts +5 -52
  10. package/dist/types/config/settings.d.ts +5 -3
  11. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  12. package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
  13. package/dist/types/discovery/contained-path.d.ts +2 -3
  14. package/dist/types/eval/executor-base.d.ts +8 -2
  15. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  16. package/dist/types/export/share.d.ts +1 -1
  17. package/dist/types/lsp/diagnostics.d.ts +96 -0
  18. package/dist/types/lsp/index.d.ts +7 -128
  19. package/dist/types/lsp/servers.d.ts +72 -0
  20. package/dist/types/lsp/tool.d.ts +42 -0
  21. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  22. package/dist/types/lsp/writethrough.d.ts +33 -0
  23. package/dist/types/mcp/transports/header-policy.d.ts +2 -1
  24. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  25. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  26. package/dist/types/modes/theme/color.d.ts +19 -0
  27. package/dist/types/modes/theme/loader.d.ts +19 -0
  28. package/dist/types/modes/theme/schema.d.ts +175 -0
  29. package/dist/types/modes/theme/symbols.d.ts +28 -0
  30. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  31. package/dist/types/modes/theme/theme.d.ts +9 -280
  32. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  33. package/dist/types/registry/agent-registry.d.ts +1 -3
  34. package/dist/types/secrets/index.d.ts +3 -1
  35. package/dist/types/secrets/message-transform.d.ts +40 -0
  36. package/dist/types/secrets/obfuscator.d.ts +0 -108
  37. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  38. package/dist/types/secrets/placeholder.d.ts +94 -0
  39. package/dist/types/secrets/replacement.d.ts +82 -0
  40. package/dist/types/session/agent-session-types.d.ts +6 -0
  41. package/dist/types/session/agent-session.d.ts +1 -1
  42. package/dist/types/session/session-handoff.d.ts +3 -3
  43. package/dist/types/session/session-manager.d.ts +32 -0
  44. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  45. package/dist/types/session/turn-recovery.d.ts +2 -2
  46. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  47. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  48. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  49. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  50. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  51. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  52. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  53. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  54. package/dist/types/tools/gh-common.d.ts +69 -0
  55. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  56. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  57. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  58. package/dist/types/tools/gh-search.d.ts +61 -0
  59. package/dist/types/tools/gh-types.d.ts +340 -0
  60. package/dist/types/tools/gh-view.d.ts +88 -0
  61. package/dist/types/tools/gh.d.ts +4 -201
  62. package/dist/types/tools/read-archive.d.ts +16 -0
  63. package/dist/types/tools/read-format.d.ts +104 -0
  64. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  65. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  66. package/dist/types/tools/read-renderer.d.ts +24 -0
  67. package/dist/types/tools/read-selector.d.ts +27 -0
  68. package/dist/types/tools/read-sqlite.d.ts +16 -0
  69. package/dist/types/tools/read-summary.d.ts +19 -0
  70. package/dist/types/tools/read.d.ts +1 -23
  71. package/dist/types/tools/shell-tokenize.d.ts +2 -1
  72. package/dist/types/utils/changelog.d.ts +0 -5
  73. package/package.json +13 -13
  74. package/src/advisor/runtime.ts +12 -7
  75. package/src/config/custom-models.ts +188 -0
  76. package/src/config/model-config-values.ts +128 -0
  77. package/src/config/model-patch.ts +252 -0
  78. package/src/config/model-provider-discovery.ts +132 -0
  79. package/src/config/model-registry.ts +64 -704
  80. package/src/config/settings.ts +7 -3
  81. package/src/debug/raw-sse-buffer.ts +20 -0
  82. package/src/discovery/agent-plugin-format.ts +7 -7
  83. package/src/discovery/contained-path.ts +2 -5
  84. package/src/edit/hashline/diff.ts +5 -1
  85. package/src/eval/executor-base.ts +39 -6
  86. package/src/eval/jl/executor.ts +82 -371
  87. package/src/eval/kernel-session-registry.ts +398 -0
  88. package/src/eval/py/executor.ts +53 -261
  89. package/src/eval/rb/executor.ts +36 -279
  90. package/src/export/share.ts +2 -1
  91. package/src/lsp/diagnostics.ts +516 -0
  92. package/src/lsp/index.ts +20 -2819
  93. package/src/lsp/servers.ts +296 -0
  94. package/src/lsp/tool.ts +1352 -0
  95. package/src/lsp/workspace-diagnostics.ts +170 -0
  96. package/src/lsp/writethrough.ts +561 -0
  97. package/src/mcp/transports/header-policy.ts +1 -1
  98. package/src/modes/components/agent-hub-projection.ts +2 -2
  99. package/src/modes/components/agent-hub-renderer.ts +3 -7
  100. package/src/modes/controllers/command-controller.ts +4 -1
  101. package/src/modes/theme/color.ts +133 -0
  102. package/src/modes/theme/loader.ts +178 -0
  103. package/src/modes/theme/schema.ts +263 -0
  104. package/src/modes/theme/symbols.ts +1000 -0
  105. package/src/modes/theme/theme-class.ts +611 -0
  106. package/src/modes/theme/theme.ts +27 -2453
  107. package/src/modes/theme/tui-adapters.ts +279 -0
  108. package/src/registry/agent-registry.ts +2 -2
  109. package/src/secrets/index.ts +6 -12
  110. package/src/secrets/message-transform.ts +287 -0
  111. package/src/secrets/obfuscator.ts +39 -1303
  112. package/src/secrets/placeholder-scan.ts +506 -0
  113. package/src/secrets/placeholder.ts +309 -0
  114. package/src/secrets/replacement.ts +216 -0
  115. package/src/session/agent-session-types.ts +6 -0
  116. package/src/session/agent-session.ts +113 -7
  117. package/src/session/indexed-session-storage.ts +7 -2
  118. package/src/session/session-advisors.ts +32 -34
  119. package/src/session/session-handoff.ts +39 -20
  120. package/src/session/session-manager.ts +67 -3
  121. package/src/session/session-provider-boundary.ts +3 -6
  122. package/src/session/turn-recovery.ts +21 -12
  123. package/src/slash-commands/builtin-collaboration.ts +504 -0
  124. package/src/slash-commands/builtin-completions.ts +291 -0
  125. package/src/slash-commands/builtin-control.ts +78 -0
  126. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  127. package/src/slash-commands/builtin-marketplace.ts +567 -0
  128. package/src/slash-commands/builtin-modes.ts +518 -0
  129. package/src/slash-commands/builtin-registry.ts +21 -3000
  130. package/src/slash-commands/builtin-session.ts +592 -0
  131. package/src/task/executor.ts +17 -14
  132. package/src/tools/gh-common.ts +288 -0
  133. package/src/tools/gh-pr-checkout.ts +679 -0
  134. package/src/tools/gh-pr-diff.ts +473 -0
  135. package/src/tools/gh-run-watch.ts +1015 -0
  136. package/src/tools/gh-search.ts +500 -0
  137. package/src/tools/gh-types.ts +379 -0
  138. package/src/tools/gh-view.ts +612 -0
  139. package/src/tools/gh.ts +109 -3821
  140. package/src/tools/read-archive.ts +209 -0
  141. package/src/tools/read-format.ts +593 -0
  142. package/src/tools/read-path-resolution.ts +36 -0
  143. package/src/tools/read-pdf-images.ts +250 -0
  144. package/src/tools/read-renderer.ts +289 -0
  145. package/src/tools/read-selector.ts +84 -0
  146. package/src/tools/read-sqlite.ts +215 -0
  147. package/src/tools/read-summary.ts +199 -0
  148. package/src/tools/read.ts +77 -1820
  149. package/src/tools/shell-tokenize.ts +1 -1
  150. package/src/utils/changelog.ts +1 -1
  151. package/src/utils/title-generator.ts +3 -1
  152. package/src/web/search/providers/zai.ts +12 -3
@@ -0,0 +1,516 @@
1
+ import * as fs from "node:fs";
2
+ import path from "node:path";
3
+ import { logger } from "@oh-my-pi/pi-utils";
4
+ import { formatPathRelativeToCwd } from "../tools/path-utils";
5
+ import { throwIfAborted } from "../tools/tool-errors";
6
+ import { getOrCreateClient, sendRequest, supportsDocumentDiagnostics, waitForProjectLoaded } from "./client";
7
+ import { getLinterClient } from "./clients";
8
+ import { hasRootMarkerAncestor } from "./config";
9
+ import { applyTextEditsToString } from "./edits";
10
+ import { resolveFormatOptions } from "./format-options";
11
+ import { isProjectAwareLspServer } from "./servers";
12
+ import type {
13
+ Diagnostic,
14
+ Location,
15
+ LocationLink,
16
+ LspClient,
17
+ Position,
18
+ PublishedDiagnostics,
19
+ ServerConfig,
20
+ TextEdit,
21
+ } from "./types";
22
+ import {
23
+ fileToUri,
24
+ formatDiagnostic,
25
+ formatDiagnosticsSummary,
26
+ formatLocation,
27
+ readLocationContext,
28
+ sortDiagnostics,
29
+ uriToFile,
30
+ } from "./utils";
31
+
32
+ const DIAGNOSTIC_MESSAGE_LIMIT = 50;
33
+ export const SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 3000;
34
+ export const BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS = 400;
35
+ const DIAGNOSTICS_POLL_MS = 100;
36
+ const DIAGNOSTICS_SETTLE_MS = 250;
37
+ /**
38
+ * How long the edit/write writethrough blocks inline waiting for fresh
39
+ * diagnostics before handing slow servers off to the deferred late-injection
40
+ * channel. Keeps the common fast-server case inline while letting an edit
41
+ * return promptly when a server (e.g. a large-monorepo tsserver) is slow to
42
+ * publish fresh diagnostics.
43
+ */
44
+ export const INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS = 500;
45
+ /**
46
+ * Inner per-server diagnostics wait budget for the background/deferred fetch.
47
+ * Longer than the inline cap (and the old 3s default) so a slow server still
48
+ * delivers late instead of giving up before it ever publishes.
49
+ */
50
+ export const DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS = 12_000;
51
+ export const MAX_GLOB_DIAGNOSTIC_TARGETS = 20;
52
+ export const WORKSPACE_SYMBOL_LIMIT = 200;
53
+ export const PROJECT_INDEXED_ACTIONS: ReadonlySet<string> = new Set([
54
+ "definition",
55
+ "type_definition",
56
+ "implementation",
57
+ "references",
58
+ "rename",
59
+ "hover",
60
+ ]);
61
+
62
+ const RUST_WORKSPACE_MARKERS = ["Cargo.toml", "rust-analyzer.toml"] as const;
63
+
64
+ export function hasRustWorkspaceAncestor(filePath: string): boolean {
65
+ let dir = path.dirname(filePath);
66
+ while (true) {
67
+ for (const marker of RUST_WORKSPACE_MARKERS) {
68
+ if (fs.existsSync(path.join(dir, marker))) {
69
+ return true;
70
+ }
71
+ }
72
+ const parent = path.dirname(dir);
73
+ if (parent === dir) {
74
+ return false;
75
+ }
76
+ dir = parent;
77
+ }
78
+ }
79
+
80
+ export function limitDiagnosticMessages(messages: string[]): string[] {
81
+ if (messages.length <= DIAGNOSTIC_MESSAGE_LIMIT) {
82
+ return messages;
83
+ }
84
+ return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
85
+ }
86
+
87
+ const ORPHAN_TYPESCRIPT_PROJECT_DIAGNOSTIC_CODES: Record<number, true> = {
88
+ 1375: true,
89
+ 1378: true,
90
+ 2307: true,
91
+ 2580: true,
92
+ 2591: true,
93
+ 2792: true,
94
+ 2867: true,
95
+ };
96
+
97
+ function diagnosticCodeNumber(diagnostic: Diagnostic): number | null {
98
+ if (typeof diagnostic.code === "number") return diagnostic.code;
99
+ if (typeof diagnostic.code === "string" && /^\d+$/.test(diagnostic.code)) return Number(diagnostic.code);
100
+ return null;
101
+ }
102
+ function isTypeScriptProjectDiagnostic(serverName: string, diagnostic: Diagnostic): boolean {
103
+ if (diagnostic.source !== "typescript" && !serverName.toLowerCase().includes("typescript")) {
104
+ return false;
105
+ }
106
+ const code = diagnosticCodeNumber(diagnostic);
107
+ return code !== null && ORPHAN_TYPESCRIPT_PROJECT_DIAGNOSTIC_CODES[code] === true;
108
+ }
109
+
110
+ function filterOrphanProjectDiagnostics(
111
+ absolutePath: string,
112
+ serverName: string,
113
+ serverConfig: ServerConfig,
114
+ diagnostics: Diagnostic[],
115
+ ): Diagnostic[] {
116
+ if (!serverConfig.rootMarkers.length || hasRootMarkerAncestor(absolutePath, serverConfig.rootMarkers)) {
117
+ return diagnostics;
118
+ }
119
+ return diagnostics.filter(diagnostic => !isTypeScriptProjectDiagnostic(serverName, diagnostic));
120
+ }
121
+
122
+ const LOCATION_CONTEXT_LINES = 1;
123
+ export const REFERENCE_CONTEXT_LIMIT = 50;
124
+
125
+ export const REFERENCES_RETRY_COUNT = 2;
126
+ export const REFERENCES_RETRY_DELAY_MS = 250;
127
+
128
+ function comparePosition(a: Position, b: Position): number {
129
+ return a.line === b.line ? a.character - b.character : a.line - b.line;
130
+ }
131
+
132
+ function rangeContainsPosition(range: Location["range"], position: Position): boolean {
133
+ return comparePosition(range.start, position) <= 0 && comparePosition(position, range.end) <= 0;
134
+ }
135
+
136
+ export function isOnlyQueriedDeclaration(locations: Location[], uri: string, position: Position): boolean {
137
+ return locations.length === 1 && locations[0]?.uri === uri && rangeContainsPosition(locations[0].range, position);
138
+ }
139
+
140
+ export function normalizeLocationResult(
141
+ result: Location | Location[] | LocationLink | LocationLink[] | null,
142
+ ): Location[] {
143
+ if (!result) return [];
144
+ const raw = Array.isArray(result) ? result : [result];
145
+ return raw.flatMap(loc => {
146
+ if ("uri" in loc) {
147
+ return [loc as Location];
148
+ }
149
+ if ("targetUri" in loc) {
150
+ const link = loc as LocationLink;
151
+ return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
152
+ }
153
+ return [];
154
+ });
155
+ }
156
+
157
+ export async function formatLocationWithContext(location: Location, cwd: string): Promise<string> {
158
+ const header = ` ${formatLocation(location, cwd)}`;
159
+ const context = await readLocationContext(
160
+ uriToFile(location.uri),
161
+ location.range.start.line + 1,
162
+ LOCATION_CONTEXT_LINES,
163
+ );
164
+ if (context.length === 0) {
165
+ return header;
166
+ }
167
+ return `${header}\n${context.map(lineText => ` ${lineText}`).join("\n")}`;
168
+ }
169
+
170
+ interface WaitForDiagnosticsOptions {
171
+ timeoutMs?: number;
172
+ signal?: AbortSignal;
173
+ minVersion?: number;
174
+ expectedDocumentVersion?: number;
175
+ /**
176
+ * Quiescence window (ms). typescript-language-server never echoes the document
177
+ * version (issue #983) and emits diagnostics from several sources at different
178
+ * times, so there is no single "complete, version-matched" publish to gate on.
179
+ * When the server does not exact-version-match, accept the latest publish only
180
+ * after no newer one has arrived for this long, letting an in-flight pre-edit
181
+ * publish be superseded by the fresh one.
182
+ */
183
+ settleMs?: number;
184
+ }
185
+
186
+ function requestDocumentDiagnostics(
187
+ client: LspClient,
188
+ uri: string,
189
+ signal: AbortSignal | undefined,
190
+ timeoutMs: number,
191
+ ): Promise<Diagnostic[] | undefined> {
192
+ return sendRequest(client, "textDocument/diagnostic", { textDocument: { uri } }, signal, timeoutMs)
193
+ .then(report => {
194
+ if (!report || typeof report !== "object" || !("kind" in report) || report.kind !== "full") {
195
+ return undefined;
196
+ }
197
+ if (!("items" in report) || !Array.isArray(report.items)) return undefined;
198
+ return report.items;
199
+ })
200
+ .catch(err => {
201
+ if (!signal?.aborted) {
202
+ logger.debug("LSP document diagnostic pull failed", { server: client.name, uri, error: String(err) });
203
+ }
204
+ return undefined;
205
+ });
206
+ }
207
+
208
+ export async function waitForDiagnostics(
209
+ client: LspClient,
210
+ uri: string,
211
+ options: WaitForDiagnosticsOptions = {},
212
+ ): Promise<Diagnostic[]> {
213
+ const { timeoutMs = 3000, signal, minVersion, expectedDocumentVersion, settleMs = DIAGNOSTICS_SETTLE_MS } = options;
214
+ const deadline = Date.now() + timeoutMs;
215
+ let pullAttempted = false;
216
+ let pullResultPromise: Promise<{ diagnostics: Diagnostic[] | undefined }> | undefined;
217
+ let pulled: Diagnostic[] | undefined;
218
+ let settledRef: PublishedDiagnostics | undefined;
219
+ let settledAt = 0;
220
+ while (Date.now() < deadline) {
221
+ throwIfAborted(signal);
222
+ if (!pullAttempted && supportsDocumentDiagnostics(client)) {
223
+ pullAttempted = true;
224
+ pullResultPromise = requestDocumentDiagnostics(client, uri, signal, Math.max(1, deadline - Date.now())).then(
225
+ diagnostics => ({ diagnostics }),
226
+ );
227
+ }
228
+
229
+ const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
230
+ const published = client.diagnostics.get(uri);
231
+ if (published && versionOk) {
232
+ // Server honored our exact document version → authoritative, accept now.
233
+ if (expectedDocumentVersion !== undefined && published.version === expectedDocumentVersion) {
234
+ return published.diagnostics;
235
+ }
236
+ // Unversioned/mismatched publish: wait for the stream to go quiet so an
237
+ // in-flight publish for the pre-edit content is superseded by the fresh one.
238
+ if (published !== settledRef) {
239
+ settledRef = published;
240
+ settledAt = Date.now();
241
+ } else if (Date.now() - settledAt >= settleMs) {
242
+ return published.diagnostics;
243
+ }
244
+ }
245
+
246
+ const pollMs = Math.min(DIAGNOSTICS_POLL_MS, Math.max(0, deadline - Date.now()));
247
+ if (!pullResultPromise) {
248
+ await Bun.sleep(pollMs);
249
+ continue;
250
+ }
251
+ const pullResult = await Promise.race([pullResultPromise, Bun.sleep(pollMs).then(() => undefined)]);
252
+ if (pullResult) {
253
+ pullResultPromise = undefined;
254
+ pulled = pullResult.diagnostics;
255
+ if (pulled !== undefined) break;
256
+ }
257
+ }
258
+
259
+ const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
260
+ const published = client.diagnostics.get(uri);
261
+ if (published && versionOk) {
262
+ return published.diagnostics;
263
+ }
264
+ if (pullResultPromise) {
265
+ pulled = (await pullResultPromise).diagnostics;
266
+ }
267
+ throwIfAborted(signal);
268
+ if (pulled === undefined) return [];
269
+ client.diagnostics.set(uri, {
270
+ diagnostics: pulled,
271
+ version: expectedDocumentVersion ?? client.openFiles.get(uri)?.version ?? null,
272
+ });
273
+ client.diagnosticsVersion += 1;
274
+ return pulled;
275
+ }
276
+
277
+ /** Result from getDiagnosticsForFile */
278
+ export interface FileDiagnosticsResult {
279
+ /** Name of the LSP server used (if available) */
280
+ server?: string;
281
+ /** Formatted diagnostic messages */
282
+ messages: string[];
283
+ /** Summary string (e.g., "2 error(s), 1 warning(s)") */
284
+ summary: string;
285
+ /** Whether there are any errors (severity 1) */
286
+ errored: boolean;
287
+ /** Whether the file was formatted */
288
+ formatter?: FileFormatResult;
289
+ }
290
+
291
+ export type ServerVersionMap = Map<string, number>;
292
+
293
+ interface GetDiagnosticsForFileOptions {
294
+ signal?: AbortSignal;
295
+ minVersions?: ServerVersionMap;
296
+ expectedDocumentVersions?: ServerVersionMap;
297
+ /** Per-server wait budget (ms). Defaults to {@link SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS}. */
298
+ timeoutMs?: number;
299
+ }
300
+
301
+ /**
302
+ * Capture current diagnostic versions for all LSP servers.
303
+ * Call this BEFORE syncing content to detect stale diagnostics later.
304
+ */
305
+ export async function captureDiagnosticVersions(
306
+ cwd: string,
307
+ servers: Array<[string, ServerConfig]>,
308
+ initTimeoutMs?: number,
309
+ signal?: AbortSignal,
310
+ ): Promise<ServerVersionMap> {
311
+ const versions = new Map<string, number>();
312
+ await Promise.allSettled(
313
+ servers.map(async ([serverName, serverConfig]) => {
314
+ if (serverConfig.createClient) return;
315
+ const client = await getOrCreateClient(serverConfig, cwd, initTimeoutMs, signal);
316
+ versions.set(serverName, client.diagnosticsVersion);
317
+ }),
318
+ );
319
+ return versions;
320
+ }
321
+
322
+ export async function captureOpenFileVersions(
323
+ absolutePath: string,
324
+ cwd: string,
325
+ servers: Array<[string, ServerConfig]>,
326
+ signal?: AbortSignal,
327
+ ): Promise<ServerVersionMap> {
328
+ const uri = fileToUri(absolutePath);
329
+ const versions = new Map<string, number>();
330
+ await Promise.allSettled(
331
+ servers.map(async ([serverName, serverConfig]) => {
332
+ const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
333
+ const version = client.openFiles.get(uri)?.version;
334
+ if (version !== undefined) {
335
+ versions.set(serverName, version);
336
+ }
337
+ }),
338
+ );
339
+ return versions;
340
+ }
341
+
342
+ /**
343
+ * Get diagnostics for a file using LSP or custom linter client.
344
+ *
345
+ * @param absolutePath - Absolute path to the file
346
+ * @param cwd - Working directory for LSP config resolution
347
+ * @param servers - Servers to query diagnostics for
348
+ * @param minVersions - Minimum diagnostic versions per server (to detect stale results)
349
+ * @returns Diagnostic results or undefined if no servers
350
+ */
351
+ export async function getDiagnosticsForFile(
352
+ absolutePath: string,
353
+ cwd: string,
354
+ servers: Array<[string, ServerConfig]>,
355
+ options: GetDiagnosticsForFileOptions = {},
356
+ ): Promise<FileDiagnosticsResult | undefined> {
357
+ const { signal, minVersions, expectedDocumentVersions, timeoutMs } = options;
358
+ if (servers.length === 0) {
359
+ return undefined;
360
+ }
361
+
362
+ const uri = fileToUri(absolutePath);
363
+ const relPath = formatPathRelativeToCwd(absolutePath, cwd);
364
+ const allDiagnostics: Diagnostic[] = [];
365
+ const serverNames: string[] = [];
366
+
367
+ // Wait for diagnostics from all servers in parallel
368
+ const results = await Promise.allSettled(
369
+ servers.map(async ([serverName, serverConfig]) => {
370
+ throwIfAborted(signal);
371
+ // Use custom linter client if configured
372
+ if (serverConfig.createClient) {
373
+ const linterClient = getLinterClient(serverName, serverConfig, cwd);
374
+ const diagnostics = await linterClient.lint(absolutePath);
375
+ return { serverName, serverConfig, diagnostics };
376
+ }
377
+
378
+ // Default: use LSP
379
+ const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
380
+ throwIfAborted(signal);
381
+ if (isProjectAwareLspServer(serverConfig)) {
382
+ await waitForProjectLoaded(client, signal);
383
+ throwIfAborted(signal);
384
+ }
385
+ // Content already synced + didSave sent, wait for fresh diagnostics
386
+ const minVersion = minVersions?.get(serverName);
387
+ const expectedDocumentVersion = expectedDocumentVersions?.get(serverName);
388
+ const diagnostics = await waitForDiagnostics(client, uri, {
389
+ timeoutMs: timeoutMs ?? SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS,
390
+ signal,
391
+ minVersion,
392
+ expectedDocumentVersion,
393
+ });
394
+ return { serverName, serverConfig, diagnostics };
395
+ }),
396
+ );
397
+
398
+ for (const result of results) {
399
+ if (result.status === "fulfilled") {
400
+ serverNames.push(result.value.serverName);
401
+ allDiagnostics.push(
402
+ ...filterOrphanProjectDiagnostics(
403
+ absolutePath,
404
+ result.value.serverName,
405
+ result.value.serverConfig,
406
+ result.value.diagnostics,
407
+ ),
408
+ );
409
+ }
410
+ }
411
+
412
+ if (serverNames.length === 0) {
413
+ return undefined;
414
+ }
415
+
416
+ if (allDiagnostics.length === 0) {
417
+ return {
418
+ server: serverNames.join(", "),
419
+ messages: [],
420
+ summary: "OK",
421
+ errored: false,
422
+ };
423
+ }
424
+
425
+ // Deduplicate diagnostics by range + message (different servers might report similar issues)
426
+ const seen = new Set<string>();
427
+ const uniqueDiagnostics: Diagnostic[] = [];
428
+ for (const d of allDiagnostics) {
429
+ const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
430
+ if (!seen.has(key)) {
431
+ seen.add(key);
432
+ uniqueDiagnostics.push(d);
433
+ }
434
+ }
435
+
436
+ sortDiagnostics(uniqueDiagnostics);
437
+ const formatted = uniqueDiagnostics.map(d => formatDiagnostic(d, relPath));
438
+ const limited = limitDiagnosticMessages(formatted);
439
+ const summary = formatDiagnosticsSummary(uniqueDiagnostics);
440
+ const hasErrors = uniqueDiagnostics.some(d => d.severity === 1);
441
+
442
+ return {
443
+ server: serverNames.join(", "),
444
+ messages: limited,
445
+ summary,
446
+ errored: hasErrors,
447
+ };
448
+ }
449
+
450
+ export enum FileFormatResult {
451
+ UNCHANGED = "unchanged",
452
+ FORMATTED = "formatted",
453
+ }
454
+
455
+ /**
456
+ * Format content using LSP or custom linter client.
457
+ *
458
+ * @param absolutePath - Absolute path (for URI)
459
+ * @param content - Content to format
460
+ * @param cwd - Working directory for LSP config resolution
461
+ * @param servers - Servers to try formatting with
462
+ * @returns Formatted content, or original if no formatter available
463
+ */
464
+ export async function formatContent(
465
+ absolutePath: string,
466
+ content: string,
467
+ cwd: string,
468
+ servers: Array<[string, ServerConfig]>,
469
+ signal?: AbortSignal,
470
+ ): Promise<string> {
471
+ if (servers.length === 0) {
472
+ return content;
473
+ }
474
+
475
+ const uri = fileToUri(absolutePath);
476
+
477
+ for (const [serverName, serverConfig] of servers) {
478
+ try {
479
+ throwIfAborted(signal);
480
+ // Use custom linter client if configured
481
+ if (serverConfig.createClient) {
482
+ const linterClient = getLinterClient(serverName, serverConfig, cwd);
483
+ return await linterClient.format(absolutePath, content);
484
+ }
485
+
486
+ // Default: use LSP
487
+ const client = await getOrCreateClient(serverConfig, cwd, undefined, signal);
488
+ throwIfAborted(signal);
489
+
490
+ const caps = client.serverCapabilities;
491
+ if (!caps?.documentFormattingProvider) {
492
+ continue;
493
+ }
494
+
495
+ // Request formatting (content already synced)
496
+ const edits = (await sendRequest(
497
+ client,
498
+ "textDocument/formatting",
499
+ {
500
+ textDocument: { uri },
501
+ options: resolveFormatOptions(absolutePath, content),
502
+ },
503
+ signal,
504
+ )) as TextEdit[] | null;
505
+
506
+ if (!edits || edits.length === 0) {
507
+ return content;
508
+ }
509
+
510
+ // Apply edits in-memory and return
511
+ return applyTextEditsToString(content, edits);
512
+ } catch {}
513
+ }
514
+
515
+ return content;
516
+ }