pi-lens 3.8.24 → 3.8.26
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 +14 -0
- package/clients/dispatch/runners/architect.ts +5 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +12 -4
- package/clients/dispatch/runners/similarity.ts +42 -18
- package/clients/dispatch/runners/tree-sitter.ts +17 -3
- package/clients/lsp/client.ts +1 -0
- package/clients/lsp/index.ts +193 -21
- package/clients/lsp/launch.ts +2 -0
- package/clients/lsp/server.ts +21 -9
- package/clients/metrics-history.ts +20 -0
- package/clients/runtime-session.ts +54 -36
- package/clients/tree-sitter-client.ts +6 -2
- package/commands/booboo.ts +4 -24
- package/index.ts +22 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.8.26] - 2026-04-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Silent crash on unhandled promise rejection** — the LSP crash guard's `unhandledRejection` handler was swallowing all non-ignorable rejections without rethrowing, causing silent process exits. The handler now rethrows so non-ignorable rejections surface as `uncaughtException` and are properly reported. Triggered most visibly when editing JSON files while Biome or another LSP server was active.
|
|
11
|
+
|
|
12
|
+
## [3.8.25] - 2026-04-13
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Go LSP PATH augmentation on Windows** — LSP subprocess PATH now includes common Go install directories (`C:\Program Files\Go\bin`, `C:\Go\bin`) to prevent `gopls` startup/runtime failures when `go` is not in inherited shell PATH.
|
|
16
|
+
- **Similarity runner cold-start behavior** — similarity now skips fast when no cached project index exists and for tiny/trivial files, reducing write/edit pipeline tail latency and eliminating frequent 30s timeout noise in scratch-file workflows.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Non-git workspace commit lookup noise** — metrics snapshot commit detection now pre-checks repository context before invoking Git, preventing `fatal: not a git repository` terminal noise in non-repo folders.
|
|
20
|
+
|
|
7
21
|
## [3.8.24] - 2026-04-12
|
|
8
22
|
|
|
9
23
|
### Changed
|
|
@@ -47,7 +47,11 @@ const architectRunner: RunnerDefinition = {
|
|
|
47
47
|
|
|
48
48
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
49
49
|
const relPath = path.relative(ctx.cwd, ctx.filePath).replace(/\\/g, "/");
|
|
50
|
-
const
|
|
50
|
+
const contentFromFacts = ctx.facts.getFileFact<string | null>(
|
|
51
|
+
ctx.filePath,
|
|
52
|
+
"file.content",
|
|
53
|
+
);
|
|
54
|
+
const content = contentFromFacts ?? readFileContent(ctx.filePath);
|
|
51
55
|
|
|
52
56
|
if (!content) {
|
|
53
57
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
@@ -423,10 +423,18 @@ const astGrepNapiRunner: RunnerDefinition = {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
let content: string;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
const contentFromFacts = ctx.facts.getFileFact<string | null>(
|
|
427
|
+
ctx.filePath,
|
|
428
|
+
"file.content",
|
|
429
|
+
);
|
|
430
|
+
if (contentFromFacts !== undefined && contentFromFacts !== null) {
|
|
431
|
+
content = contentFromFacts;
|
|
432
|
+
} else {
|
|
433
|
+
try {
|
|
434
|
+
content = fs.readFileSync(ctx.filePath, "utf-8");
|
|
435
|
+
} catch {
|
|
436
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
437
|
+
}
|
|
430
438
|
}
|
|
431
439
|
|
|
432
440
|
let root: import("@ast-grep/napi").SgRoot;
|
|
@@ -9,8 +9,8 @@ import * as nodeFs from "node:fs";
|
|
|
9
9
|
import * as fs from "node:fs/promises";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import * as ts from "typescript";
|
|
12
|
-
import { EXCLUDED_DIRS } from "../../file-utils.js";
|
|
13
12
|
import { NativeRustCoreClient } from "../../native-rust-client.js";
|
|
13
|
+
import { collectSourceFiles } from "../../source-filter.js";
|
|
14
14
|
import {
|
|
15
15
|
buildProjectIndex,
|
|
16
16
|
findSimilarFunctions,
|
|
@@ -40,6 +40,7 @@ const CONFIG = {
|
|
|
40
40
|
SIMILARITY_THRESHOLD: 0.96, // align with booboo: stricter to reduce boilerplate false positives
|
|
41
41
|
MIN_TRANSITIONS: 40, // stronger signal floor for structural comparisons
|
|
42
42
|
MIN_FUNCTION_LINES: 8, // Ignore tiny helpers/wrappers
|
|
43
|
+
MIN_FILE_CHARS: 140, // Skip tiny/trivial files early
|
|
43
44
|
MAX_TRANSITION_RATIO: 1.8, // Skip pairs with highly mismatched complexity/size
|
|
44
45
|
MAX_SUGGESTIONS: 3, // Max 3 suggestions per file
|
|
45
46
|
MAX_PER_TARGET_NAME: 1, // Avoid one-to-many spam for the same target utility
|
|
@@ -115,12 +116,26 @@ const similarityRunner: RunnerDefinition = {
|
|
|
115
116
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
const lineCount = content.split(/\r?\n/).length;
|
|
120
|
+
if (
|
|
121
|
+
content.trim().length < CONFIG.MIN_FILE_CHARS ||
|
|
122
|
+
lineCount < CONFIG.MIN_FUNCTION_LINES + 2 ||
|
|
123
|
+
!/(\bfunction\b|=>)/.test(content)
|
|
124
|
+
) {
|
|
125
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
// Find project root and load index
|
|
119
129
|
const projectRoot = await findProjectRoot(filePath);
|
|
120
130
|
if (!projectRoot) {
|
|
121
131
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
const cachedIndex = await loadCachedIndex(projectRoot);
|
|
135
|
+
if (!cachedIndex || cachedIndex.entries.size === 0) {
|
|
136
|
+
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
// ── Rust fast-path ─────────────────────────────────────────────────────
|
|
125
140
|
// Try Rust for file scanning + similarity detection. If the Rust binary
|
|
126
141
|
// is available, use it. On any failure, fall through to the pure-TS path.
|
|
@@ -139,10 +154,7 @@ const similarityRunner: RunnerDefinition = {
|
|
|
139
154
|
}
|
|
140
155
|
// ── TypeScript fallback ─────────────────────────────────────────────────
|
|
141
156
|
|
|
142
|
-
const index =
|
|
143
|
-
if (!index || index.entries.size === 0) {
|
|
144
|
-
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
145
|
-
}
|
|
157
|
+
const index = cachedIndex;
|
|
146
158
|
|
|
147
159
|
// Parse the file
|
|
148
160
|
const sourceFile = ts.createSourceFile(
|
|
@@ -512,30 +524,42 @@ async function loadOrBuildIndex(
|
|
|
512
524
|
}
|
|
513
525
|
|
|
514
526
|
// Build new index
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
cwd: projectRoot,
|
|
525
|
-
ignore: ignorePatterns,
|
|
527
|
+
const absoluteFiles = collectSourceFiles(projectRoot, {
|
|
528
|
+
extensions: [".ts"],
|
|
529
|
+
}).filter((filePath) => {
|
|
530
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
531
|
+
return (
|
|
532
|
+
!normalized.endsWith(".test.ts") &&
|
|
533
|
+
!normalized.endsWith(".spec.ts") &&
|
|
534
|
+
!normalized.endsWith(".poc.test.ts")
|
|
535
|
+
);
|
|
526
536
|
});
|
|
527
537
|
|
|
528
|
-
if (
|
|
538
|
+
if (absoluteFiles.length === 0) {
|
|
529
539
|
return null;
|
|
530
540
|
}
|
|
531
541
|
|
|
532
|
-
const absoluteFiles = files.map((f) => path.join(projectRoot, f));
|
|
533
542
|
const index = await buildProjectIndex(projectRoot, absoluteFiles);
|
|
534
543
|
|
|
535
544
|
indexCache.set(projectRoot, index);
|
|
536
545
|
return index;
|
|
537
546
|
}
|
|
538
547
|
|
|
548
|
+
async function loadCachedIndex(projectRoot: string): Promise<ProjectIndex | null> {
|
|
549
|
+
const cached = indexCache.get(projectRoot);
|
|
550
|
+
if (cached) {
|
|
551
|
+
return cached;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const existing = await loadIndex(projectRoot);
|
|
555
|
+
if (!existing) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
indexCache.set(projectRoot, existing);
|
|
560
|
+
return existing;
|
|
561
|
+
}
|
|
562
|
+
|
|
539
563
|
// ============================================================================
|
|
540
564
|
// Testing Helper
|
|
541
565
|
// ============================================================================
|
|
@@ -519,13 +519,27 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
const diagnostics: Diagnostic[] = [];
|
|
522
|
+
const contentFromFacts = ctx.facts.getFileFact<string | null>(
|
|
523
|
+
filePath,
|
|
524
|
+
"file.content",
|
|
525
|
+
);
|
|
526
|
+
const contentOverride =
|
|
527
|
+
contentFromFacts !== undefined && contentFromFacts !== null
|
|
528
|
+
? contentFromFacts
|
|
529
|
+
: undefined;
|
|
522
530
|
|
|
523
531
|
// Run each query against the file
|
|
524
532
|
for (const query of effectiveQueries) {
|
|
525
533
|
try {
|
|
526
|
-
const matches = await client.runQueryOnFile(
|
|
527
|
-
|
|
528
|
-
|
|
534
|
+
const matches = await client.runQueryOnFile(
|
|
535
|
+
query,
|
|
536
|
+
filePath,
|
|
537
|
+
languageId,
|
|
538
|
+
{
|
|
539
|
+
maxResults: 10,
|
|
540
|
+
},
|
|
541
|
+
contentOverride,
|
|
542
|
+
);
|
|
529
543
|
|
|
530
544
|
for (const match of matches) {
|
|
531
545
|
// Get line/column from match (already 0-indexed from tree-sitter)
|
package/clients/lsp/client.ts
CHANGED
package/clients/lsp/index.ts
CHANGED
|
@@ -31,6 +31,20 @@ export interface LSPState {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const BROKEN_RETRY_COOLDOWN_MS = 15_000;
|
|
34
|
+
const OPTIONAL_LSP_RETRY_COOLDOWN_MS = 5 * 60_000;
|
|
35
|
+
const OPTIONAL_LSP_SERVER_IDS = new Set(["biome-lsp"]);
|
|
36
|
+
const NAV_CLIENT_WAIT_TIMEOUT_MS = Math.max(
|
|
37
|
+
0,
|
|
38
|
+
Number.parseInt(
|
|
39
|
+
process.env.PI_LENS_LSP_NAV_CLIENT_WAIT_MS ?? "700",
|
|
40
|
+
10,
|
|
41
|
+
) || 700,
|
|
42
|
+
);
|
|
43
|
+
const TOUCH_DEBOUNCE_MS = Math.max(
|
|
44
|
+
0,
|
|
45
|
+
Number.parseInt(process.env.PI_LENS_LSP_TOUCH_DEBOUNCE_MS ?? "1500", 10) ||
|
|
46
|
+
1500,
|
|
47
|
+
);
|
|
34
48
|
const SESSIONSTART_LOG_DIR = path.join(os.homedir(), ".pi-lens");
|
|
35
49
|
const SESSIONSTART_LOG = path.join(SESSIONSTART_LOG_DIR, "sessionstart.log");
|
|
36
50
|
|
|
@@ -57,6 +71,12 @@ export class LSPService {
|
|
|
57
71
|
private workspaceProbeLogged = new Set<string>();
|
|
58
72
|
private warmStartLogged = new Set<string>();
|
|
59
73
|
private emitConsoleLspErrors = process.env.PI_LENS_CONSOLE_LSP === "1";
|
|
74
|
+
private optionalFailureLogged = new Set<string>();
|
|
75
|
+
private optionalDisabled = new Set<string>();
|
|
76
|
+
private recentTouches = new Map<
|
|
77
|
+
string,
|
|
78
|
+
{ fingerprint: string; touchedAt: number; clientScope: "primary" | "all" }
|
|
79
|
+
>();
|
|
60
80
|
|
|
61
81
|
constructor() {
|
|
62
82
|
this.state = {
|
|
@@ -67,11 +87,57 @@ export class LSPService {
|
|
|
67
87
|
};
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
private fingerprintContent(content: string): string {
|
|
91
|
+
if (content.length <= 96) {
|
|
92
|
+
return `${content.length}:${content}`;
|
|
93
|
+
}
|
|
94
|
+
return `${content.length}:${content.slice(0, 48)}:${content.slice(-48)}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private shouldSkipTouch(
|
|
98
|
+
filePath: string,
|
|
99
|
+
content: string,
|
|
100
|
+
clientScope: "primary" | "all",
|
|
101
|
+
waitForDiagnostics: boolean,
|
|
102
|
+
): boolean {
|
|
103
|
+
if (waitForDiagnostics || TOUCH_DEBOUNCE_MS <= 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const key = `${normalizeMapKey(filePath)}:${clientScope}`;
|
|
108
|
+
const previous = this.recentTouches.get(key);
|
|
109
|
+
if (!previous) return false;
|
|
110
|
+
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
if (now - previous.touchedAt > TOUCH_DEBOUNCE_MS) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return previous.fingerprint === this.fingerprintContent(content);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private markTouched(
|
|
120
|
+
filePath: string,
|
|
121
|
+
content: string,
|
|
122
|
+
clientScope: "primary" | "all",
|
|
123
|
+
): void {
|
|
124
|
+
const key = `${normalizeMapKey(filePath)}:${clientScope}`;
|
|
125
|
+
this.recentTouches.set(key, {
|
|
126
|
+
fingerprint: this.fingerprintContent(content),
|
|
127
|
+
touchedAt: Date.now(),
|
|
128
|
+
clientScope,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
70
132
|
/**
|
|
71
133
|
* Get or create LSP client for a file
|
|
72
134
|
* Prevents duplicate client creation via in-flight promise tracking
|
|
73
135
|
*/
|
|
74
|
-
async getClientForFile(
|
|
136
|
+
async getClientForFile(
|
|
137
|
+
filePath: string,
|
|
138
|
+
maxWaitMs?: number,
|
|
139
|
+
): Promise<SpawnedServer | undefined> {
|
|
140
|
+
const withBudget = async (): Promise<SpawnedServer | undefined> => {
|
|
75
141
|
const servers = getServersForFileWithConfig(filePath);
|
|
76
142
|
if (servers.length === 0) return undefined;
|
|
77
143
|
|
|
@@ -105,6 +171,32 @@ export class LSPService {
|
|
|
105
171
|
});
|
|
106
172
|
|
|
107
173
|
return undefined;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!maxWaitMs || maxWaitMs <= 0) {
|
|
177
|
+
return withBudget();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const timeoutResult = await Promise.race<SpawnedServer | undefined>([
|
|
181
|
+
withBudget(),
|
|
182
|
+
new Promise<undefined>((resolve) =>
|
|
183
|
+
setTimeout(() => resolve(undefined), maxWaitMs),
|
|
184
|
+
),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
if (!timeoutResult) {
|
|
188
|
+
logLatency({
|
|
189
|
+
type: "phase",
|
|
190
|
+
phase: "lsp_client_wait_timeout",
|
|
191
|
+
filePath,
|
|
192
|
+
durationMs: maxWaitMs,
|
|
193
|
+
metadata: {
|
|
194
|
+
maxWaitMs,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return timeoutResult;
|
|
108
200
|
}
|
|
109
201
|
|
|
110
202
|
/**
|
|
@@ -131,6 +223,11 @@ export class LSPService {
|
|
|
131
223
|
|
|
132
224
|
const normalizedRoot = normalizeMapKey(root);
|
|
133
225
|
const key = `${server.id}:${normalizedRoot}`;
|
|
226
|
+
const isOptionalServer = OPTIONAL_LSP_SERVER_IDS.has(server.id);
|
|
227
|
+
|
|
228
|
+
if (isOptionalServer && this.optionalDisabled.has(key)) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
134
231
|
|
|
135
232
|
const existing = this.state.clients.get(key);
|
|
136
233
|
if (existing) {
|
|
@@ -228,6 +325,7 @@ export class LSPService {
|
|
|
228
325
|
filePath: string,
|
|
229
326
|
allowInstall: boolean,
|
|
230
327
|
): Promise<SpawnedServer | undefined> {
|
|
328
|
+
const isOptionalServer = OPTIONAL_LSP_SERVER_IDS.has(server.id);
|
|
231
329
|
const startedAt = Date.now();
|
|
232
330
|
logSessionStart(
|
|
233
331
|
`lsp spawn ${server.id}: start root=${root} policy=${server.installPolicy ?? "unknown"} install=${allowInstall ? "enabled" : "disabled"} file=${filePath}`,
|
|
@@ -258,6 +356,10 @@ export class LSPService {
|
|
|
258
356
|
};
|
|
259
357
|
|
|
260
358
|
this.state.clients.set(key, client);
|
|
359
|
+
if (isOptionalServer) {
|
|
360
|
+
this.optionalDisabled.delete(key);
|
|
361
|
+
this.optionalFailureLogged.delete(key);
|
|
362
|
+
}
|
|
261
363
|
logSessionStart(
|
|
262
364
|
`lsp spawn ${server.id}: success source=${spawned.source ?? server.installPolicy ?? "unknown"} (${Date.now() - startedAt}ms)`,
|
|
263
365
|
);
|
|
@@ -269,11 +371,16 @@ export class LSPService {
|
|
|
269
371
|
}
|
|
270
372
|
return { client, info: server };
|
|
271
373
|
} catch (err) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
374
|
+
if (!isOptionalServer || !this.optionalFailureLogged.has(key)) {
|
|
375
|
+
logSessionStart(
|
|
376
|
+
`lsp spawn ${server.id}: failed (${Date.now() - startedAt}ms) error=${err instanceof Error ? err.message : String(err)}`,
|
|
377
|
+
);
|
|
378
|
+
if (isOptionalServer) {
|
|
379
|
+
this.optionalFailureLogged.add(key);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
275
382
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
276
|
-
if (this.emitConsoleLspErrors) {
|
|
383
|
+
if (this.emitConsoleLspErrors && !isOptionalServer) {
|
|
277
384
|
if (errorMsg.includes("Timeout")) {
|
|
278
385
|
console.error(
|
|
279
386
|
`[lsp] ${server.id} timed out during initialization (${errorMsg}). The server may be downloading or the project is large. Skipping.`,
|
|
@@ -290,7 +397,13 @@ export class LSPService {
|
|
|
290
397
|
console.error(`[lsp] Failed to spawn ${server.id}:`, err);
|
|
291
398
|
}
|
|
292
399
|
}
|
|
293
|
-
this.state.broken.set(
|
|
400
|
+
this.state.broken.set(
|
|
401
|
+
key,
|
|
402
|
+
Date.now() + (isOptionalServer ? OPTIONAL_LSP_RETRY_COOLDOWN_MS : BROKEN_RETRY_COOLDOWN_MS),
|
|
403
|
+
);
|
|
404
|
+
if (isOptionalServer) {
|
|
405
|
+
this.optionalDisabled.add(key);
|
|
406
|
+
}
|
|
294
407
|
return undefined;
|
|
295
408
|
}
|
|
296
409
|
}
|
|
@@ -326,12 +439,14 @@ export class LSPService {
|
|
|
326
439
|
waitForDiagnostics = false,
|
|
327
440
|
source = "unknown",
|
|
328
441
|
useAllClients = false,
|
|
442
|
+
maxClientWaitMs?: number,
|
|
329
443
|
): Promise<void> {
|
|
330
444
|
const startedAt = Date.now();
|
|
331
445
|
const normalizedPath = normalizeMapKey(filePath);
|
|
446
|
+
const clientScope = useAllClients ? "all" : "primary";
|
|
332
447
|
const spawned = useAllClients
|
|
333
448
|
? await this.getClientsForFile(filePath)
|
|
334
|
-
: await this.getClientForFile(filePath).then((entry) =>
|
|
449
|
+
: await this.getClientForFile(filePath, maxClientWaitMs).then((entry) =>
|
|
335
450
|
entry ? [entry] : [],
|
|
336
451
|
);
|
|
337
452
|
if (spawned.length === 0) {
|
|
@@ -342,13 +457,32 @@ export class LSPService {
|
|
|
342
457
|
durationMs: Date.now() - startedAt,
|
|
343
458
|
metadata: {
|
|
344
459
|
serverCountReady: 0,
|
|
345
|
-
clientScope
|
|
460
|
+
clientScope,
|
|
346
461
|
failureKind: "no_clients",
|
|
347
462
|
},
|
|
348
463
|
});
|
|
349
464
|
return;
|
|
350
465
|
}
|
|
351
466
|
|
|
467
|
+
if (this.shouldSkipTouch(filePath, content, clientScope, waitForDiagnostics)) {
|
|
468
|
+
logLatency({
|
|
469
|
+
type: "phase",
|
|
470
|
+
phase: "lsp_touch_file",
|
|
471
|
+
filePath: normalizedPath,
|
|
472
|
+
durationMs: Date.now() - startedAt,
|
|
473
|
+
metadata: {
|
|
474
|
+
serverCountReady: spawned.length,
|
|
475
|
+
clientScope,
|
|
476
|
+
waitForDiagnostics,
|
|
477
|
+
source,
|
|
478
|
+
failureKind: "success",
|
|
479
|
+
skipped: true,
|
|
480
|
+
reason: "debounced_unchanged_content",
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
352
486
|
const languageId = getLanguageId(filePath) ?? "plaintext";
|
|
353
487
|
await Promise.all(
|
|
354
488
|
spawned.map((entry) => entry.client.notify.open(filePath, content, languageId)),
|
|
@@ -362,6 +496,8 @@ export class LSPService {
|
|
|
362
496
|
);
|
|
363
497
|
}
|
|
364
498
|
|
|
499
|
+
this.markTouched(filePath, content, clientScope);
|
|
500
|
+
|
|
365
501
|
logLatency({
|
|
366
502
|
type: "phase",
|
|
367
503
|
phase: "lsp_touch_file",
|
|
@@ -369,7 +505,7 @@ export class LSPService {
|
|
|
369
505
|
durationMs: Date.now() - startedAt,
|
|
370
506
|
metadata: {
|
|
371
507
|
serverCountReady: spawned.length,
|
|
372
|
-
clientScope
|
|
508
|
+
clientScope,
|
|
373
509
|
waitForDiagnostics,
|
|
374
510
|
source,
|
|
375
511
|
failureKind: "success",
|
|
@@ -471,7 +607,10 @@ export class LSPService {
|
|
|
471
607
|
* Navigation: go to definition
|
|
472
608
|
*/
|
|
473
609
|
async definition(filePath: string, line: number, character: number) {
|
|
474
|
-
const spawned = await this.getClientForFile(
|
|
610
|
+
const spawned = await this.getClientForFile(
|
|
611
|
+
filePath,
|
|
612
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
613
|
+
);
|
|
475
614
|
if (!spawned) return [];
|
|
476
615
|
return spawned.client.definition(filePath, line, character);
|
|
477
616
|
}
|
|
@@ -485,7 +624,10 @@ export class LSPService {
|
|
|
485
624
|
character: number,
|
|
486
625
|
includeDeclaration = true,
|
|
487
626
|
) {
|
|
488
|
-
const spawned = await this.getClientForFile(
|
|
627
|
+
const spawned = await this.getClientForFile(
|
|
628
|
+
filePath,
|
|
629
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
630
|
+
);
|
|
489
631
|
if (!spawned) return [];
|
|
490
632
|
return spawned.client.references(
|
|
491
633
|
filePath,
|
|
@@ -499,7 +641,10 @@ export class LSPService {
|
|
|
499
641
|
* Navigation: hover info
|
|
500
642
|
*/
|
|
501
643
|
async hover(filePath: string, line: number, character: number) {
|
|
502
|
-
const spawned = await this.getClientForFile(
|
|
644
|
+
const spawned = await this.getClientForFile(
|
|
645
|
+
filePath,
|
|
646
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
647
|
+
);
|
|
503
648
|
if (!spawned) return null;
|
|
504
649
|
return spawned.client.hover(filePath, line, character);
|
|
505
650
|
}
|
|
@@ -508,7 +653,10 @@ export class LSPService {
|
|
|
508
653
|
* Navigation: signature help at cursor position
|
|
509
654
|
*/
|
|
510
655
|
async signatureHelp(filePath: string, line: number, character: number) {
|
|
511
|
-
const spawned = await this.getClientForFile(
|
|
656
|
+
const spawned = await this.getClientForFile(
|
|
657
|
+
filePath,
|
|
658
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
659
|
+
);
|
|
512
660
|
if (!spawned) return null;
|
|
513
661
|
return spawned.client.signatureHelp(filePath, line, character);
|
|
514
662
|
}
|
|
@@ -517,7 +665,10 @@ export class LSPService {
|
|
|
517
665
|
* Navigation: symbols in document
|
|
518
666
|
*/
|
|
519
667
|
async documentSymbol(filePath: string) {
|
|
520
|
-
const spawned = await this.getClientForFile(
|
|
668
|
+
const spawned = await this.getClientForFile(
|
|
669
|
+
filePath,
|
|
670
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
671
|
+
);
|
|
521
672
|
if (!spawned) return [];
|
|
522
673
|
return spawned.client.documentSymbol(filePath);
|
|
523
674
|
}
|
|
@@ -527,7 +678,10 @@ export class LSPService {
|
|
|
527
678
|
*/
|
|
528
679
|
async workspaceSymbol(query: string, filePath?: string) {
|
|
529
680
|
if (filePath) {
|
|
530
|
-
const spawned = await this.getClientForFile(
|
|
681
|
+
const spawned = await this.getClientForFile(
|
|
682
|
+
filePath,
|
|
683
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
684
|
+
);
|
|
531
685
|
if (!spawned) return [];
|
|
532
686
|
return spawned.client.workspaceSymbol(query);
|
|
533
687
|
}
|
|
@@ -592,7 +746,10 @@ export class LSPService {
|
|
|
592
746
|
endLine: number,
|
|
593
747
|
endCharacter: number,
|
|
594
748
|
) {
|
|
595
|
-
const spawned = await this.getClientForFile(
|
|
749
|
+
const spawned = await this.getClientForFile(
|
|
750
|
+
filePath,
|
|
751
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
752
|
+
);
|
|
596
753
|
if (!spawned) return [];
|
|
597
754
|
return spawned.client.codeAction(
|
|
598
755
|
filePath,
|
|
@@ -612,7 +769,10 @@ export class LSPService {
|
|
|
612
769
|
character: number,
|
|
613
770
|
newName: string,
|
|
614
771
|
) {
|
|
615
|
-
const spawned = await this.getClientForFile(
|
|
772
|
+
const spawned = await this.getClientForFile(
|
|
773
|
+
filePath,
|
|
774
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
775
|
+
);
|
|
616
776
|
if (!spawned) return null;
|
|
617
777
|
return spawned.client.rename(filePath, line, character, newName);
|
|
618
778
|
}
|
|
@@ -621,7 +781,10 @@ export class LSPService {
|
|
|
621
781
|
* Navigation: go to implementation
|
|
622
782
|
*/
|
|
623
783
|
async implementation(filePath: string, line: number, character: number) {
|
|
624
|
-
const spawned = await this.getClientForFile(
|
|
784
|
+
const spawned = await this.getClientForFile(
|
|
785
|
+
filePath,
|
|
786
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
787
|
+
);
|
|
625
788
|
if (!spawned) return [];
|
|
626
789
|
return spawned.client.implementation(filePath, line, character);
|
|
627
790
|
}
|
|
@@ -634,7 +797,10 @@ export class LSPService {
|
|
|
634
797
|
line: number,
|
|
635
798
|
character: number,
|
|
636
799
|
) {
|
|
637
|
-
const spawned = await this.getClientForFile(
|
|
800
|
+
const spawned = await this.getClientForFile(
|
|
801
|
+
filePath,
|
|
802
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
803
|
+
);
|
|
638
804
|
if (!spawned) return [];
|
|
639
805
|
return spawned.client.prepareCallHierarchy(filePath, line, character);
|
|
640
806
|
}
|
|
@@ -643,7 +809,10 @@ export class LSPService {
|
|
|
643
809
|
* Navigation: find incoming calls (callers)
|
|
644
810
|
*/
|
|
645
811
|
async incomingCalls(item: import("./client.js").LSPCallHierarchyItem) {
|
|
646
|
-
const spawned = await this.getClientForFile(
|
|
812
|
+
const spawned = await this.getClientForFile(
|
|
813
|
+
uriToPath(item.uri),
|
|
814
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
815
|
+
);
|
|
647
816
|
if (!spawned) return [];
|
|
648
817
|
return spawned.client.incomingCalls(item);
|
|
649
818
|
}
|
|
@@ -652,7 +821,10 @@ export class LSPService {
|
|
|
652
821
|
* Navigation: find outgoing calls (callees)
|
|
653
822
|
*/
|
|
654
823
|
async outgoingCalls(item: import("./client.js").LSPCallHierarchyItem) {
|
|
655
|
-
const spawned = await this.getClientForFile(
|
|
824
|
+
const spawned = await this.getClientForFile(
|
|
825
|
+
uriToPath(item.uri),
|
|
826
|
+
NAV_CLIENT_WAIT_TIMEOUT_MS,
|
|
827
|
+
);
|
|
656
828
|
if (!spawned) return [];
|
|
657
829
|
return spawned.client.outgoingCalls(item);
|
|
658
830
|
}
|
package/clients/lsp/launch.ts
CHANGED
|
@@ -35,6 +35,8 @@ function buildAugmentedPath(basePath?: string): string {
|
|
|
35
35
|
candidates.push(path.join(userProfile, ".cargo", "bin"));
|
|
36
36
|
candidates.push(path.join(userProfile, "go", "bin"));
|
|
37
37
|
}
|
|
38
|
+
candidates.push(path.join("C:\\", "Program Files", "Go", "bin"));
|
|
39
|
+
candidates.push(path.join("C:\\", "Go", "bin"));
|
|
38
40
|
candidates.push(path.join("C:\\", "Ruby34-x64", "bin"));
|
|
39
41
|
candidates.push(path.join("C:\\", "Ruby33-x64", "bin"));
|
|
40
42
|
|
package/clients/lsp/server.ts
CHANGED
|
@@ -124,23 +124,35 @@ async function launchWithDirectOrPackageManager(
|
|
|
124
124
|
args: string[],
|
|
125
125
|
options: { cwd: string; env?: NodeJS.ProcessEnv; allowInstall?: boolean },
|
|
126
126
|
): Promise<{ process: LSPProcess; source: "direct" | "package-manager" } | undefined> {
|
|
127
|
+
let lastDirectError: unknown;
|
|
128
|
+
|
|
127
129
|
for (const command of directCommands) {
|
|
128
130
|
try {
|
|
129
131
|
const process = await launchLSP(command, args, options);
|
|
130
132
|
return { process, source: "direct" };
|
|
131
133
|
} catch (error) {
|
|
132
|
-
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
134
|
+
lastDirectError = error;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
try {
|
|
139
|
+
const process = await launchViaPackageManagerWithPolicy(packageName, args, {
|
|
140
|
+
cwd: options.cwd,
|
|
141
|
+
allowInstall: options.allowInstall,
|
|
142
|
+
});
|
|
143
|
+
if (!process) {
|
|
144
|
+
if (lastDirectError && !isCommandNotFoundError(lastDirectError)) {
|
|
145
|
+
throw lastDirectError;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return { process, source: "package-manager" };
|
|
150
|
+
} catch (packageManagerError) {
|
|
151
|
+
if (lastDirectError && !isCommandNotFoundError(lastDirectError)) {
|
|
152
|
+
throw lastDirectError;
|
|
153
|
+
}
|
|
154
|
+
throw packageManagerError;
|
|
155
|
+
}
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
type InitializationConfig = Record<string, unknown>;
|
|
@@ -45,10 +45,30 @@ const MAX_HISTORY_PER_FILE = 20;
|
|
|
45
45
|
|
|
46
46
|
// --- Git Helpers ---
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Check whether cwd is inside a Git worktree.
|
|
50
|
+
*/
|
|
51
|
+
function isInsideGitRepo(startDir: string): boolean {
|
|
52
|
+
let dir = path.resolve(startDir);
|
|
53
|
+
while (true) {
|
|
54
|
+
if (fs.existsSync(path.join(dir, ".git"))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) break;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
/**
|
|
49
65
|
* Get current git commit hash (short)
|
|
50
66
|
*/
|
|
51
67
|
function getCurrentCommit(): string {
|
|
68
|
+
if (!isInsideGitRepo(process.cwd())) {
|
|
69
|
+
return "unknown";
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
try {
|
|
53
73
|
return execSync("git rev-parse --short HEAD", {
|
|
54
74
|
encoding: "utf-8",
|
|
@@ -10,13 +10,13 @@ import { getKnipIgnorePatterns } from "./file-utils.js";
|
|
|
10
10
|
import type { GoClient } from "./go-client.js";
|
|
11
11
|
import type { JscpdClient } from "./jscpd-client.js";
|
|
12
12
|
import type { KnipClient } from "./knip-client.js";
|
|
13
|
+
import { canRunStartupHeavyScans } from "./language-policy.js";
|
|
13
14
|
import {
|
|
14
15
|
detectProjectLanguageProfile,
|
|
15
16
|
getDefaultStartupTools,
|
|
16
17
|
hasLanguage,
|
|
17
18
|
isLanguageConfigured,
|
|
18
19
|
} from "./language-profile.js";
|
|
19
|
-
import { canRunStartupHeavyScans } from "./language-policy.js";
|
|
20
20
|
import type { MetricsClient } from "./metrics-client.js";
|
|
21
21
|
import {
|
|
22
22
|
buildProjectIndex,
|
|
@@ -64,7 +64,10 @@ interface SessionStartDeps {
|
|
|
64
64
|
|
|
65
65
|
type StartupMode = "full" | "minimal" | "quick";
|
|
66
66
|
|
|
67
|
-
function isCommandAvailable(
|
|
67
|
+
function isCommandAvailable(
|
|
68
|
+
command: string,
|
|
69
|
+
args: string[] = ["--version"],
|
|
70
|
+
): boolean {
|
|
68
71
|
const result = safeSpawn(command, args, { timeout: 5000 });
|
|
69
72
|
return !result.error && result.status === 0;
|
|
70
73
|
}
|
|
@@ -97,7 +100,9 @@ function getLanguageInstallHints(
|
|
|
97
100
|
};
|
|
98
101
|
|
|
99
102
|
if (hasStrongSignal("go") && !isCommandAvailable("gopls")) {
|
|
100
|
-
hints.push(
|
|
103
|
+
hints.push(
|
|
104
|
+
"Go detected: install gopls (`go install golang.org/x/tools/gopls@latest`).",
|
|
105
|
+
);
|
|
101
106
|
}
|
|
102
107
|
if (hasStrongSignal("rust") && !isCommandAvailable("rust-analyzer")) {
|
|
103
108
|
hints.push(
|
|
@@ -174,6 +179,25 @@ export async function handleSessionStart(
|
|
|
174
179
|
delete process.env.PI_LENS_DISABLE_LSP_INSTALL;
|
|
175
180
|
}
|
|
176
181
|
|
|
182
|
+
const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
|
|
183
|
+
const cwd = ctxCwd ?? process.cwd();
|
|
184
|
+
if (quickMode) {
|
|
185
|
+
runtime.projectRoot = cwd;
|
|
186
|
+
const quickTools: string[] = [];
|
|
187
|
+
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
188
|
+
quickTools.push("LSP Service");
|
|
189
|
+
}
|
|
190
|
+
log(`Active tools: ${quickTools.join(", ")}`);
|
|
191
|
+
dbg(
|
|
192
|
+
`session_start tools: ${quickTools.join(", ") || "deferred (quick mode)"}`,
|
|
193
|
+
);
|
|
194
|
+
dbg(
|
|
195
|
+
"session_start: quick mode active - skipping slow tool probes, language profiling, preinstall, scans, and error debt baseline",
|
|
196
|
+
);
|
|
197
|
+
dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
177
201
|
const tools: string[] = [];
|
|
178
202
|
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
179
203
|
tools.push("LSP Service");
|
|
@@ -200,17 +224,6 @@ export async function handleSessionStart(
|
|
|
200
224
|
}
|
|
201
225
|
}
|
|
202
226
|
|
|
203
|
-
const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
|
|
204
|
-
const cwd = ctxCwd ?? process.cwd();
|
|
205
|
-
if (quickMode) {
|
|
206
|
-
runtime.projectRoot = cwd;
|
|
207
|
-
dbg(
|
|
208
|
-
"session_start: quick mode active - skipping language profiling, preinstall, scans, and error debt baseline",
|
|
209
|
-
);
|
|
210
|
-
dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
227
|
const startupScan = resolveStartupScanContext(cwd);
|
|
215
228
|
const scanRoot = startupScan.projectRoot ?? cwd;
|
|
216
229
|
const useScanRootForSignals =
|
|
@@ -236,18 +249,20 @@ export async function handleSessionStart(
|
|
|
236
249
|
}
|
|
237
250
|
|
|
238
251
|
const lensLspEnabled = !!getFlag("lens-lsp") && !getFlag("no-lsp");
|
|
239
|
-
const startupDefaults = getDefaultStartupTools(languageProfile).filter(
|
|
240
|
-
|
|
241
|
-
(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
const startupDefaults = getDefaultStartupTools(languageProfile).filter(
|
|
253
|
+
(tool) => {
|
|
254
|
+
if (
|
|
255
|
+
(tool === "typescript-language-server" || tool === "pyright") &&
|
|
256
|
+
!lensLspEnabled
|
|
257
|
+
) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (tool === "ruff" && getFlag("no-autofix-ruff")) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
);
|
|
251
266
|
|
|
252
267
|
if (!allowBootstrapTasks) {
|
|
253
268
|
dbg("session_start: skipping tool preinstall (startup mode)");
|
|
@@ -369,7 +384,9 @@ export async function handleSessionStart(
|
|
|
369
384
|
runtime.markStartupScanInFlight(name, sessionGeneration);
|
|
370
385
|
void task()
|
|
371
386
|
.then(() => {
|
|
372
|
-
dbg(
|
|
387
|
+
dbg(
|
|
388
|
+
`session_start task ${name}: success (${Date.now() - startedAt}ms)`,
|
|
389
|
+
);
|
|
373
390
|
})
|
|
374
391
|
.catch((err) => {
|
|
375
392
|
dbg(`session_start: ${name} background scan failed: ${err}`);
|
|
@@ -391,7 +408,9 @@ export async function handleSessionStart(
|
|
|
391
408
|
dbg(
|
|
392
409
|
`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
|
|
393
410
|
);
|
|
394
|
-
dbg(
|
|
411
|
+
dbg(
|
|
412
|
+
`session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`,
|
|
413
|
+
);
|
|
395
414
|
} else {
|
|
396
415
|
const canRunJsTsHeavyScans = canRunStartupHeavyScans(
|
|
397
416
|
languageProfile,
|
|
@@ -401,9 +420,7 @@ export async function handleSessionStart(
|
|
|
401
420
|
if (canRunJsTsHeavyScans) {
|
|
402
421
|
scanNames.push("knip", "jscpd", "ast-grep exports", "project index");
|
|
403
422
|
}
|
|
404
|
-
dbg(
|
|
405
|
-
`session_start: launching background scans (${scanNames.join(", ")})`,
|
|
406
|
-
);
|
|
423
|
+
dbg(`session_start: launching background scans (${scanNames.join(", ")})`);
|
|
407
424
|
|
|
408
425
|
runStartupTask("todo", async () => {
|
|
409
426
|
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
@@ -458,10 +475,9 @@ export async function handleSessionStart(
|
|
|
458
475
|
runStartupTask("jscpd", async () => {
|
|
459
476
|
if (await jscpdClient.ensureAvailable()) {
|
|
460
477
|
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
461
|
-
const cached = cacheManager.readCache<
|
|
462
|
-
"
|
|
463
|
-
|
|
464
|
-
);
|
|
478
|
+
const cached = cacheManager.readCache<
|
|
479
|
+
ReturnType<JscpdClient["scan"]>
|
|
480
|
+
>("jscpd", analysisRoot);
|
|
465
481
|
if (cached) {
|
|
466
482
|
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
467
483
|
dbg("session_start jscpd: cache hit");
|
|
@@ -527,7 +543,9 @@ export async function handleSessionStart(
|
|
|
527
543
|
);
|
|
528
544
|
} else {
|
|
529
545
|
if (!runtime.isCurrentSession(sessionGeneration)) return;
|
|
530
|
-
dbg(
|
|
546
|
+
dbg(
|
|
547
|
+
`session_start: skipped project index (${tsFiles.length} files)`,
|
|
548
|
+
);
|
|
531
549
|
}
|
|
532
550
|
}
|
|
533
551
|
});
|
|
@@ -283,6 +283,7 @@ export class TreeSitterClient {
|
|
|
283
283
|
async parseFile(
|
|
284
284
|
filePath: string,
|
|
285
285
|
languageId: string,
|
|
286
|
+
contentOverride?: string,
|
|
286
287
|
): Promise<TreeSitterTree | null> {
|
|
287
288
|
this.dbg(`Parsing ${filePath} with language ${languageId}`);
|
|
288
289
|
const parser = await this.getParser(languageId);
|
|
@@ -292,7 +293,7 @@ export class TreeSitterClient {
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
try {
|
|
295
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
296
|
+
const content = contentOverride ?? fs.readFileSync(filePath, "utf-8");
|
|
296
297
|
this.dbg(`File content length: ${content.length}`);
|
|
297
298
|
|
|
298
299
|
// Check cache first
|
|
@@ -469,6 +470,7 @@ export class TreeSitterClient {
|
|
|
469
470
|
filePath: string,
|
|
470
471
|
languageId: string,
|
|
471
472
|
options: { maxResults?: number } = {},
|
|
473
|
+
contentOverride?: string,
|
|
472
474
|
): Promise<StructuralMatch[]> {
|
|
473
475
|
if (!this.initialized) {
|
|
474
476
|
const ok = await this.init();
|
|
@@ -493,6 +495,7 @@ export class TreeSitterClient {
|
|
|
493
495
|
queryDef.id,
|
|
494
496
|
compiled.postFilter,
|
|
495
497
|
compiled.postFilterParams,
|
|
498
|
+
contentOverride,
|
|
496
499
|
);
|
|
497
500
|
|
|
498
501
|
const maxResults = options.maxResults ?? 50;
|
|
@@ -727,8 +730,9 @@ export class TreeSitterClient {
|
|
|
727
730
|
postFilter?: string,
|
|
728
731
|
// biome-ignore lint/suspicious/noExplicitAny: Post filter params
|
|
729
732
|
postFilterParams?: any,
|
|
733
|
+
contentOverride?: string,
|
|
730
734
|
): Promise<StructuralMatch[]> {
|
|
731
|
-
const tree = await this.parseFile(filePath, languageId);
|
|
735
|
+
const tree = await this.parseFile(filePath, languageId, contentOverride);
|
|
732
736
|
if (!tree) return [];
|
|
733
737
|
|
|
734
738
|
const matches: StructuralMatch[] = [];
|
package/commands/booboo.ts
CHANGED
|
@@ -369,33 +369,13 @@ export async function handleBooboo(
|
|
|
369
369
|
// Runner 3: Semantic similarity
|
|
370
370
|
await tracker.run("semantic similarity (Amain)", async () => {
|
|
371
371
|
try {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
ignore: [
|
|
376
|
-
"**/node_modules/**",
|
|
377
|
-
"**/*.test.ts",
|
|
378
|
-
"**/*.test.tsx",
|
|
379
|
-
"**/*.spec.ts",
|
|
380
|
-
"**/*.spec.tsx",
|
|
381
|
-
"**/*.poc.test.ts",
|
|
382
|
-
"**/*.poc.test.tsx",
|
|
383
|
-
"**/test-utils.ts",
|
|
384
|
-
"**/test-*.ts",
|
|
385
|
-
"**/__tests__/**",
|
|
386
|
-
"**/tests/**",
|
|
387
|
-
"**/dist/**",
|
|
388
|
-
],
|
|
389
|
-
});
|
|
372
|
+
const absoluteFiles = collectSourceFiles(targetPath, {
|
|
373
|
+
extensions: [".ts"],
|
|
374
|
+
}).filter(shouldIncludeFile);
|
|
390
375
|
|
|
391
|
-
if (
|
|
376
|
+
if (absoluteFiles.length === 0) {
|
|
392
377
|
return { findings: 0, status: "done" };
|
|
393
378
|
}
|
|
394
|
-
|
|
395
|
-
// Filter out test files using centralized exclusion
|
|
396
|
-
const absoluteFiles = sourceFiles
|
|
397
|
-
.map((f) => path.join(targetPath, f))
|
|
398
|
-
.filter(shouldIncludeFile);
|
|
399
379
|
const index = await buildProjectIndex(targetPath, absoluteFiles);
|
|
400
380
|
const topPairs = findTopSimilarPairs(index, 10);
|
|
401
381
|
|
package/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
getLatencyReports,
|
|
14
14
|
resetDispatchBaselines,
|
|
15
15
|
} from "./clients/dispatch/integration.js";
|
|
16
|
-
import { extractFunctions } from "./clients/dispatch/runners/similarity.js";
|
|
17
16
|
import { resetFormatService } from "./clients/format-service.js";
|
|
18
17
|
import { evaluateGitGuard, isGitCommitOrPushAttempt } from "./clients/git-guard.js";
|
|
19
18
|
import { ensureTool } from "./clients/installer/index.js";
|
|
@@ -60,6 +59,11 @@ function dbg(msg: string) {
|
|
|
60
59
|
let _verbose = false;
|
|
61
60
|
const runtime = new RuntimeCoordinator();
|
|
62
61
|
const _lspConfigInitializedCwds = new Set<string>();
|
|
62
|
+
const LSP_TOOLCALL_NAV_TOUCH_BUDGET_MS = Math.max(
|
|
63
|
+
0,
|
|
64
|
+
Number.parseInt(process.env.PI_LENS_TOOLCALL_NAV_TOUCH_MS ?? "700", 10) ||
|
|
65
|
+
700,
|
|
66
|
+
);
|
|
63
67
|
|
|
64
68
|
function log(msg: string) {
|
|
65
69
|
if (_verbose) console.error(`[pi-lens] ${msg}`);
|
|
@@ -506,7 +510,8 @@ pi.on("session_start", async (event, ctx) => {
|
|
|
506
510
|
testRunnerClient,
|
|
507
511
|
goClient,
|
|
508
512
|
rustClient,
|
|
509
|
-
ensureTool
|
|
513
|
+
ensureTool: async (name: string) =>
|
|
514
|
+
(await import("./clients/installer/index.js")).ensureTool(name),
|
|
510
515
|
cleanStaleTsBuildInfo,
|
|
511
516
|
resetDispatchBaselines,
|
|
512
517
|
resetLSPService,
|
|
@@ -576,8 +581,17 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
576
581
|
if (shouldAutoTouch) {
|
|
577
582
|
try {
|
|
578
583
|
const fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
584
|
+
const maxClientWaitMs =
|
|
585
|
+
toolName === "lsp_navigation" ? LSP_TOOLCALL_NAV_TOUCH_BUDGET_MS : undefined;
|
|
579
586
|
void getLSPService()
|
|
580
|
-
.touchFile(
|
|
587
|
+
.touchFile(
|
|
588
|
+
filePath,
|
|
589
|
+
fileContent,
|
|
590
|
+
false,
|
|
591
|
+
`tool_call:${toolName}`,
|
|
592
|
+
false,
|
|
593
|
+
maxClientWaitMs,
|
|
594
|
+
)
|
|
581
595
|
.catch((err) => dbg(`lsp auto-touch failed for ${filePath}: ${err}`));
|
|
582
596
|
} catch {
|
|
583
597
|
// Best effort only; never block tool calls.
|
|
@@ -594,6 +608,9 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
594
608
|
const baseline = complexityClient.analyzeFile(filePath);
|
|
595
609
|
if (baseline) {
|
|
596
610
|
runtime.complexityBaselines.set(filePath, baseline);
|
|
611
|
+
const { captureSnapshot } = await import(
|
|
612
|
+
"./clients/metrics-history.js"
|
|
613
|
+
);
|
|
597
614
|
captureSnapshot(filePath, {
|
|
598
615
|
maintainabilityIndex: baseline.maintainabilityIndex,
|
|
599
616
|
cognitiveComplexity: baseline.cognitiveComplexity,
|
|
@@ -659,6 +676,8 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
659
676
|
ts.ScriptTarget.Latest,
|
|
660
677
|
true,
|
|
661
678
|
);
|
|
679
|
+
const { extractFunctions } = await import("./clients/dispatch/runners/similarity.js");
|
|
680
|
+
const { findSimilarFunctions } = await import("./clients/project-index.js");
|
|
662
681
|
const newFunctions = extractFunctions(sourceFile, newContent);
|
|
663
682
|
const simWarnings: string[] = [];
|
|
664
683
|
let simHintsTruncated = false;
|