pi-lens 3.8.62 → 3.8.64
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.
- package/CHANGELOG.md +37 -0
- package/README.md +12 -9
- package/dist/clients/deadline-utils.js +93 -0
- package/dist/clients/installer/index.js +50 -35
- package/dist/clients/log-cleanup.js +36 -13
- package/dist/clients/lsp/client.js +156 -61
- package/dist/clients/lsp/index.js +218 -33
- package/dist/clients/lsp/interactive-install.js +14 -4
- package/dist/clients/lsp/launch.js +31 -102
- package/dist/clients/lsp/server.js +29 -22
- package/dist/clients/mcp/review.js +7 -4
- package/dist/clients/module-report-lsp.js +1 -16
- package/dist/clients/package-manager.js +223 -0
- package/dist/clients/pipeline.js +113 -39
- package/dist/clients/project-metadata.js +3 -23
- package/dist/clients/read-expansion.js +1 -14
- package/dist/clients/safe-spawn.js +9 -0
- package/dist/clients/tree-sitter-client.js +72 -14
- package/dist/index.js +16 -0
- package/dist/mcp/server.js +4 -2
- package/dist/tools/ast-grep-search.js +10 -6
- package/dist/tools/lens-diagnostics.js +43 -10
- package/dist/tools/lsp-diagnostics.js +23 -4
- package/dist/tools/scan-progress.js +38 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-bash.wasm.json +5 -0
- package/grammars/tree-sitter-css.wasm +0 -0
- package/grammars/tree-sitter-css.wasm.json +5 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-go.wasm.json +5 -0
- package/grammars/tree-sitter-html.wasm +0 -0
- package/grammars/tree-sitter-html.wasm.json +5 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-java.wasm.json +5 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm.json +5 -0
- package/grammars/tree-sitter-json.wasm +0 -0
- package/grammars/tree-sitter-json.wasm.json +5 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-python.wasm.json +5 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm.json +5 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm.json +5 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm.json +5 -0
- package/grammars/tree-sitter-yaml.wasm +0 -0
- package/grammars/tree-sitter-yaml.wasm.json +5 -0
- package/package.json +7 -5
- package/rules/ast-grep-rules/rule-tests/incomplete-string-escaping-js-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/incomplete-string-escaping-test.yml +21 -0
- package/rules/ast-grep-rules/rules/incomplete-string-escaping-js.yml +19 -0
- package/rules/ast-grep-rules/rules/incomplete-string-escaping.yml +19 -0
- package/scripts/analyze-pi-lens-logs.mjs +101 -3
- package/scripts/download-grammars.js +171 -30
- package/scripts/grammars.lock.json +32 -0
- package/skills/write-ast-grep-rule/SKILL.md +36 -26
|
@@ -11,6 +11,7 @@ import { spawn as nodeSpawn } from "node:child_process";
|
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { access, readFile } from "node:fs/promises";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { withTimeout } from "../deadline-utils.js";
|
|
14
15
|
import { logLatency } from "../latency-logger.js";
|
|
15
16
|
// vscode-jsonrpc v9 ships an `exports` map exposing the Node entry as the
|
|
16
17
|
// `./node` subpath (no `.js`); the old `/node.js` file path no longer resolves.
|
|
@@ -76,7 +77,20 @@ export const CLIENT_CAPABILITIES = {
|
|
|
76
77
|
const NAV_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_NAV_REQUEST_TIMEOUT_MS", 10_000); // 10s — per-request ceiling; prevents heavy servers (vue, svelte) from hanging
|
|
77
78
|
const DIAGNOSTICS_WAIT_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_DIAGNOSTICS_WAIT_MS", 10_000);
|
|
78
79
|
const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_RETRY_INTERVAL_MS", 250);
|
|
80
|
+
// Per-request ceiling for pull diagnostics (textDocument/diagnostic), mirroring
|
|
81
|
+
// NAV_REQUEST_TIMEOUT_MS. safeSendRequest only settles on a reply or a *destroyed*
|
|
82
|
+
// stream, so a pull-mode server that is alive but hung (accepts the request, never
|
|
83
|
+
// replies) would await forever — hanging clientWaitForDiagnostics and, upstream,
|
|
84
|
+
// the diagnostics flush. On timeout the request is treated as `unavailable`, which
|
|
85
|
+
// (per #240) is NOT read as clean and falls through to the bounded push backstop.
|
|
86
|
+
const PULL_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_PULL_REQUEST_TIMEOUT_MS", 10_000);
|
|
79
87
|
const SHUTDOWN_REQUEST_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_SHUTDOWN_TIMEOUT_MS", 1000);
|
|
88
|
+
// Anti-deadlock backstop for workspace/executeCommand. Deliberately generous
|
|
89
|
+
// (30s): the command is mutating and legitimately long-running (a real server
|
|
90
|
+
// refactor / organize-imports), so this must not truncate valid work — it only
|
|
91
|
+
// stops a hung server from blocking the caller forever. On timeout the command
|
|
92
|
+
// may still be applying server-side; we surface that rather than pretend it ran.
|
|
93
|
+
const EXECUTE_COMMAND_TIMEOUT_MS = positiveIntFromEnv("PI_LENS_LSP_EXECUTE_COMMAND_TIMEOUT_MS", 30_000);
|
|
80
94
|
const LSP_CRASH_CODES = new Set([
|
|
81
95
|
"ERR_STREAM_DESTROYED",
|
|
82
96
|
"ERR_STREAM_WRITE_AFTER_END",
|
|
@@ -179,18 +193,36 @@ export async function killProcessTree(proc, pid, options = {}) {
|
|
|
179
193
|
}
|
|
180
194
|
return;
|
|
181
195
|
}
|
|
196
|
+
const killPosixProcessGroup = (signal) => {
|
|
197
|
+
if (pid <= 0)
|
|
198
|
+
return false;
|
|
199
|
+
try {
|
|
200
|
+
process.kill(-pid, signal);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const killDirectChild = (signal) => {
|
|
208
|
+
try {
|
|
209
|
+
proc.kill(signal);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// best-effort
|
|
213
|
+
}
|
|
214
|
+
};
|
|
182
215
|
try {
|
|
183
|
-
|
|
216
|
+
if (!killPosixProcessGroup("SIGTERM")) {
|
|
217
|
+
killDirectChild("SIGTERM");
|
|
218
|
+
}
|
|
184
219
|
if (options.fast) {
|
|
185
220
|
const timer = setTimeout(() => {
|
|
186
|
-
|
|
187
|
-
if (!
|
|
188
|
-
|
|
221
|
+
if (!proc.killed) {
|
|
222
|
+
if (!killPosixProcessGroup("SIGKILL")) {
|
|
223
|
+
killDirectChild("SIGKILL");
|
|
189
224
|
}
|
|
190
225
|
}
|
|
191
|
-
catch {
|
|
192
|
-
// best-effort
|
|
193
|
-
}
|
|
194
226
|
}, 1500);
|
|
195
227
|
timer.unref?.();
|
|
196
228
|
proc.unref?.();
|
|
@@ -199,14 +231,11 @@ export async function killProcessTree(proc, pid, options = {}) {
|
|
|
199
231
|
// SIGTERM → 1.5s → SIGKILL escalation.
|
|
200
232
|
// SIGTERM alone can leave zombie processes if the server hangs.
|
|
201
233
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
202
|
-
|
|
203
|
-
if (!
|
|
204
|
-
|
|
234
|
+
if (!proc.killed) {
|
|
235
|
+
if (!killPosixProcessGroup("SIGKILL")) {
|
|
236
|
+
killDirectChild("SIGKILL");
|
|
205
237
|
}
|
|
206
238
|
}
|
|
207
|
-
catch {
|
|
208
|
-
// best-effort
|
|
209
|
-
}
|
|
210
239
|
}
|
|
211
240
|
catch {
|
|
212
241
|
// ignore
|
|
@@ -294,6 +323,7 @@ export function applyDynamicCapabilities(state) {
|
|
|
294
323
|
state.workspaceDiagnosticsSupport = {
|
|
295
324
|
advertised: true,
|
|
296
325
|
mode: "pull",
|
|
326
|
+
workspaceDiagnostics: registeredMethods.has("workspace/diagnostic"),
|
|
297
327
|
diagnosticProviderKind: "dynamic",
|
|
298
328
|
};
|
|
299
329
|
}
|
|
@@ -303,6 +333,7 @@ export function applyDynamicCapabilities(state) {
|
|
|
303
333
|
state.workspaceDiagnosticsSupport = {
|
|
304
334
|
advertised: false,
|
|
305
335
|
mode: "push-only",
|
|
336
|
+
workspaceDiagnostics: false,
|
|
306
337
|
diagnosticProviderKind: "none",
|
|
307
338
|
};
|
|
308
339
|
}
|
|
@@ -439,12 +470,17 @@ function setupConnectionLifecycle(state) {
|
|
|
439
470
|
}
|
|
440
471
|
});
|
|
441
472
|
}
|
|
442
|
-
async function clientRequestPullDiagnostics(state, filePath) {
|
|
473
|
+
async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQUEST_TIMEOUT_MS) {
|
|
443
474
|
if (!isClientAlive(state))
|
|
444
475
|
return { status: "unavailable" };
|
|
445
476
|
const uri = pathToFileURL(filePath).href;
|
|
446
477
|
try {
|
|
447
|
-
|
|
478
|
+
// withTimeout is the backstop against a hung pull-mode server: without it
|
|
479
|
+
// this await never settles unless the stream is destroyed. Bounded by the
|
|
480
|
+
// smaller of the absolute ceiling and the caller's remaining wait budget.
|
|
481
|
+
// On timeout the caught error yields `unavailable` below (never a false
|
|
482
|
+
// `clean`), so it falls through to the push-wait/timeout backstop.
|
|
483
|
+
const report = await withTimeout(safeSendRequest(state.connection, "textDocument/diagnostic", { textDocument: { uri } }), Math.max(1, Math.min(PULL_REQUEST_TIMEOUT_MS, budgetMs)));
|
|
448
484
|
if (!report)
|
|
449
485
|
return { status: "unavailable" };
|
|
450
486
|
const normalizedPath = normalizeMapKey(filePath);
|
|
@@ -472,6 +508,39 @@ async function clientRequestPullDiagnostics(state, filePath) {
|
|
|
472
508
|
return { status: "unavailable" };
|
|
473
509
|
}
|
|
474
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* One project-wide `workspace/diagnostic` pull — a single request that returns
|
|
513
|
+
* diagnostics for every document the server knows, instead of opening N files.
|
|
514
|
+
* Returns per-file reports, or `undefined` on unsupported/dead/timeout/malformed
|
|
515
|
+
* (caller falls back to the per-file path). `unchanged`-kind items carry no
|
|
516
|
+
* diagnostics and are skipped, so a file absent from the result is "clean".
|
|
517
|
+
*/
|
|
518
|
+
export async function clientRequestWorkspaceDiagnostics(state, budgetMs) {
|
|
519
|
+
if (!isClientAlive(state))
|
|
520
|
+
return undefined;
|
|
521
|
+
if (!state.workspaceDiagnosticsSupport.workspaceDiagnostics)
|
|
522
|
+
return undefined;
|
|
523
|
+
try {
|
|
524
|
+
const report = await withTimeout(safeSendRequest(state.connection, "workspace/diagnostic", { previousResultIds: [] }), Math.max(1, budgetMs));
|
|
525
|
+
if (!report || !Array.isArray(report.items))
|
|
526
|
+
return undefined;
|
|
527
|
+
const out = [];
|
|
528
|
+
for (const item of report.items) {
|
|
529
|
+
// Only "full" reports carry items; "unchanged" means "same as last pull"
|
|
530
|
+
// (none, since previousResultIds is empty on this one-shot request).
|
|
531
|
+
if (!item?.uri || item.kind !== "full")
|
|
532
|
+
continue;
|
|
533
|
+
out.push({
|
|
534
|
+
filePath: uriToPath(item.uri),
|
|
535
|
+
diagnostics: normalizeLspDiagnostics(item.items ?? []),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
475
544
|
export async function clientWaitForDiagnostics(state, filePath, timeoutMs, options = {}) {
|
|
476
545
|
const normalizedPath = normalizeMapKey(filePath);
|
|
477
546
|
const minVersion = options.minVersion;
|
|
@@ -499,7 +568,7 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
499
568
|
// `hasFreshDiagnostics()`, which is unconditionally true when there is no
|
|
500
569
|
// version baseline (`minVersion === undefined`), so a failed pull returned
|
|
501
570
|
// 0 and was read as a fresh clean.
|
|
502
|
-
let outcome = await clientRequestPullDiagnostics(state, filePath);
|
|
571
|
+
let outcome = await clientRequestPullDiagnostics(state, filePath, timeoutMs);
|
|
503
572
|
if (outcome.status === "found")
|
|
504
573
|
return;
|
|
505
574
|
let sawClean = outcome.status === "clean";
|
|
@@ -513,7 +582,7 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
513
582
|
// any point is a valid affirmative answer for this touch.
|
|
514
583
|
while (outcome.status !== "found" && Date.now() - startedAt < retryBudgetMs) {
|
|
515
584
|
await new Promise((resolve) => setTimeout(resolve, PULL_DIAGNOSTICS_RETRY_INTERVAL_MS));
|
|
516
|
-
outcome = await clientRequestPullDiagnostics(state, filePath);
|
|
585
|
+
outcome = await clientRequestPullDiagnostics(state, filePath, Math.max(0, retryBudgetMs - (Date.now() - startedAt)));
|
|
517
586
|
if (outcome.status === "clean")
|
|
518
587
|
sawClean = true;
|
|
519
588
|
}
|
|
@@ -714,18 +783,20 @@ async function toWirePosition(state, filePath, line, character) {
|
|
|
714
783
|
function navStaleDropEnabled() {
|
|
715
784
|
return process.env.PI_LENS_LSP_NAV_STALE_DROP !== "0";
|
|
716
785
|
}
|
|
717
|
-
|
|
786
|
+
// Exported for the timeout regression tests (#365). `timeoutMs` overrides the
|
|
787
|
+
// per-request ceiling so a test can bound a hung server quickly.
|
|
788
|
+
export async function navRequest(state, method, params,
|
|
718
789
|
// When provided, the request is dropped if the document's version advances
|
|
719
790
|
// (an edit landed) between send and response. Omit for non-single-file
|
|
720
791
|
// requests (workspaceSymbol, call-hierarchy follow-ups) that have no version.
|
|
721
|
-
staleCheckPath) {
|
|
792
|
+
staleCheckPath, timeoutMs = NAV_REQUEST_TIMEOUT_MS) {
|
|
722
793
|
if (!isClientAlive(state))
|
|
723
794
|
return null;
|
|
724
795
|
const normalizedPath = staleCheckPath !== undefined ? normalizeMapKey(staleCheckPath) : undefined;
|
|
725
796
|
const requestVersion = normalizedPath !== undefined
|
|
726
797
|
? state.documentVersions.get(normalizedPath)
|
|
727
798
|
: undefined;
|
|
728
|
-
const result = (await withTimeout(safeSendRequest(state.connection, method, params),
|
|
799
|
+
const result = (await withTimeout(safeSendRequest(state.connection, method, params), timeoutMs).catch((err) => {
|
|
729
800
|
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
730
801
|
return undefined;
|
|
731
802
|
}
|
|
@@ -743,6 +814,50 @@ staleCheckPath) {
|
|
|
743
814
|
}
|
|
744
815
|
return result;
|
|
745
816
|
}
|
|
817
|
+
// Run an advertised server command via workspace/executeCommand, with the
|
|
818
|
+
// generous EXECUTE_COMMAND_TIMEOUT_MS anti-deadlock backstop. Preserves the
|
|
819
|
+
// hardening invariants: allowlist-by-advertisement (only commands the server
|
|
820
|
+
// declared) and the serverEditsAllowed window that gates server-driven
|
|
821
|
+
// applyEdit to the duration of an explicit call. Exported with an overridable
|
|
822
|
+
// `timeoutMs` for the #365 regression tests.
|
|
823
|
+
export async function runServerCommand(state, command, args, timeoutMs = EXECUTE_COMMAND_TIMEOUT_MS) {
|
|
824
|
+
if (!isClientAlive(state)) {
|
|
825
|
+
return { executed: false, reason: "lsp client not alive" };
|
|
826
|
+
}
|
|
827
|
+
if (!state.advertisedCommands.has(command)) {
|
|
828
|
+
return {
|
|
829
|
+
executed: false,
|
|
830
|
+
reason: `command "${command}" is not advertised by the ${state.serverId} server`,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
state.serverEditsAllowed += 1;
|
|
834
|
+
try {
|
|
835
|
+
let result;
|
|
836
|
+
try {
|
|
837
|
+
result = await withTimeout(safeSendRequest(state.connection, "workspace/executeCommand", {
|
|
838
|
+
command,
|
|
839
|
+
arguments: args ?? [],
|
|
840
|
+
}), timeoutMs);
|
|
841
|
+
}
|
|
842
|
+
catch (err) {
|
|
843
|
+
// Generous backstop only: a timeout means the server is hung (or the
|
|
844
|
+
// command is running longer than the ceiling). Surface it honestly — the
|
|
845
|
+
// command may still be applying — instead of hanging the caller. Real
|
|
846
|
+
// (non-timeout) errors still propagate.
|
|
847
|
+
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
848
|
+
return {
|
|
849
|
+
executed: false,
|
|
850
|
+
reason: `workspace/executeCommand timed out after ${timeoutMs}ms — the command may still be applying server-side`,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
throw err;
|
|
854
|
+
}
|
|
855
|
+
return { executed: true, result };
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
state.serverEditsAllowed -= 1;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
746
861
|
async function resolveCodeActionBestEffort(state, action) {
|
|
747
862
|
if (!isClientAlive(state) || action.edit)
|
|
748
863
|
return action;
|
|
@@ -999,6 +1114,9 @@ export async function createLSPClient(options) {
|
|
|
999
1114
|
getWorkspaceDiagnosticsSupport() {
|
|
1000
1115
|
return state.workspaceDiagnosticsSupport;
|
|
1001
1116
|
},
|
|
1117
|
+
requestWorkspaceDiagnostics(budgetMs) {
|
|
1118
|
+
return clientRequestWorkspaceDiagnostics(state, budgetMs);
|
|
1119
|
+
},
|
|
1002
1120
|
getOperationSupport() {
|
|
1003
1121
|
return state.operationSupport;
|
|
1004
1122
|
},
|
|
@@ -1009,25 +1127,7 @@ export async function createLSPClient(options) {
|
|
|
1009
1127
|
return state.rawCapabilityKeys ?? [];
|
|
1010
1128
|
},
|
|
1011
1129
|
async executeCommand(command, args) {
|
|
1012
|
-
|
|
1013
|
-
return { executed: false, reason: "lsp client not alive" };
|
|
1014
|
-
}
|
|
1015
|
-
// Hardening: allowlist-by-advertisement. Only commands the server
|
|
1016
|
-
// itself declared (static or dynamic) may run — no arbitrary strings.
|
|
1017
|
-
if (!state.advertisedCommands.has(command)) {
|
|
1018
|
-
return {
|
|
1019
|
-
executed: false,
|
|
1020
|
-
reason: `command "${command}" is not advertised by the ${state.serverId} server`,
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
state.serverEditsAllowed += 1;
|
|
1024
|
-
try {
|
|
1025
|
-
const result = await safeSendRequest(state.connection, "workspace/executeCommand", { command, arguments: args ?? [] });
|
|
1026
|
-
return { executed: true, result };
|
|
1027
|
-
}
|
|
1028
|
-
finally {
|
|
1029
|
-
state.serverEditsAllowed -= 1;
|
|
1030
|
-
}
|
|
1130
|
+
return runServerCommand(state, command, args);
|
|
1031
1131
|
},
|
|
1032
1132
|
get diagnosticsVersion() {
|
|
1033
1133
|
return state.diagnosticsVersion;
|
|
@@ -1091,14 +1191,22 @@ export async function createLSPClient(options) {
|
|
|
1091
1191
|
async workspaceSymbol(query) {
|
|
1092
1192
|
if (!isClientAlive(state))
|
|
1093
1193
|
return [];
|
|
1094
|
-
|
|
1194
|
+
// Route through navRequest for the shared withTimeout ceiling — a hung
|
|
1195
|
+
// server would otherwise await forever (safeSendRequest only settles on
|
|
1196
|
+
// a reply or a destroyed stream). No staleCheckPath: not single-file.
|
|
1197
|
+
const result = await navRequest(state, "workspace/symbol", {
|
|
1198
|
+
query,
|
|
1199
|
+
});
|
|
1095
1200
|
return result ?? [];
|
|
1096
1201
|
},
|
|
1097
1202
|
async codeAction(filePath, line, character, endLine, endCharacter) {
|
|
1098
1203
|
if (!isClientAlive(state))
|
|
1099
1204
|
return [];
|
|
1100
1205
|
const uri = pathToFileURL(filePath).href;
|
|
1101
|
-
|
|
1206
|
+
// navRequest adds the shared withTimeout ceiling + single-file
|
|
1207
|
+
// stale-drop (matches documentSymbol); a hung server no longer awaits
|
|
1208
|
+
// forever, and code actions computed against superseded content drop.
|
|
1209
|
+
const result = await navRequest(state, "textDocument/codeAction", {
|
|
1102
1210
|
textDocument: { uri },
|
|
1103
1211
|
range: {
|
|
1104
1212
|
start: await toWirePosition(state, filePath, line, character),
|
|
@@ -1107,7 +1215,7 @@ export async function createLSPClient(options) {
|
|
|
1107
1215
|
context: {
|
|
1108
1216
|
diagnostics: getMergedDiagnosticsForPath(state, normalizeMapKey(filePath)),
|
|
1109
1217
|
},
|
|
1110
|
-
});
|
|
1218
|
+
}, filePath);
|
|
1111
1219
|
if (!result || !Array.isArray(result))
|
|
1112
1220
|
return [];
|
|
1113
1221
|
const actions = result.filter((item) => typeof item === "object" && item !== null && "title" in item);
|
|
@@ -1222,25 +1330,6 @@ function isStreamError(err) {
|
|
|
1222
1330
|
err.code === "EPIPE");
|
|
1223
1331
|
}
|
|
1224
1332
|
// Using shared path utilities from path-utils.ts
|
|
1225
|
-
async function withTimeout(promise, timeoutMs) {
|
|
1226
|
-
let timeout;
|
|
1227
|
-
// Suppress unhandled rejection if `promise` rejects AFTER the timeout
|
|
1228
|
-
// wins the race — Promise.race settles on the first result but the
|
|
1229
|
-
// losing promises still run, and any later rejection would be uncaught.
|
|
1230
|
-
promise.catch(() => { });
|
|
1231
|
-
try {
|
|
1232
|
-
return await Promise.race([
|
|
1233
|
-
promise,
|
|
1234
|
-
new Promise((_, reject) => {
|
|
1235
|
-
timeout = setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
1236
|
-
}),
|
|
1237
|
-
]);
|
|
1238
|
-
}
|
|
1239
|
-
finally {
|
|
1240
|
-
if (timeout)
|
|
1241
|
-
clearTimeout(timeout);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
1333
|
function positiveIntFromEnv(name, fallback) {
|
|
1245
1334
|
const raw = process.env[name];
|
|
1246
1335
|
if (!raw)
|
|
@@ -1259,6 +1348,7 @@ function detectWorkspaceDiagnosticsSupport(initResult) {
|
|
|
1259
1348
|
return {
|
|
1260
1349
|
advertised: false,
|
|
1261
1350
|
mode: "push-only",
|
|
1351
|
+
workspaceDiagnostics: false,
|
|
1262
1352
|
diagnosticProviderKind: "none",
|
|
1263
1353
|
};
|
|
1264
1354
|
}
|
|
@@ -1266,6 +1356,8 @@ function detectWorkspaceDiagnosticsSupport(initResult) {
|
|
|
1266
1356
|
return {
|
|
1267
1357
|
advertised: diagnosticProvider,
|
|
1268
1358
|
mode: diagnosticProvider ? "pull" : "push-only",
|
|
1359
|
+
// The boolean form of diagnosticProvider only signals document pull.
|
|
1360
|
+
workspaceDiagnostics: false,
|
|
1269
1361
|
diagnosticProviderKind: "boolean",
|
|
1270
1362
|
};
|
|
1271
1363
|
}
|
|
@@ -1273,12 +1365,15 @@ function detectWorkspaceDiagnosticsSupport(initResult) {
|
|
|
1273
1365
|
return {
|
|
1274
1366
|
advertised: true,
|
|
1275
1367
|
mode: "pull",
|
|
1368
|
+
workspaceDiagnostics: diagnosticProvider
|
|
1369
|
+
.workspaceDiagnostics === true,
|
|
1276
1370
|
diagnosticProviderKind: "object",
|
|
1277
1371
|
};
|
|
1278
1372
|
}
|
|
1279
1373
|
return {
|
|
1280
1374
|
advertised: false,
|
|
1281
1375
|
mode: "push-only",
|
|
1376
|
+
workspaceDiagnostics: false,
|
|
1282
1377
|
diagnosticProviderKind: typeof diagnosticProvider,
|
|
1283
1378
|
};
|
|
1284
1379
|
}
|