@danypops/pi-lector 0.13.5 → 0.13.7
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/extension/src/code-intelligence/operations.ts +23 -1
- package/extension/src/cross-workspace-search/rendering.ts +2 -1
- package/extension/src/editor/index.ts +7 -1
- package/extension/src/editor/modal-editor-component.ts +44 -6
- package/extension/src/index.ts +170 -56
- package/extension/src/mutation-history/operations.ts +57 -10
- package/extension/src/mutation-history/rendering.ts +20 -0
- package/extension/src/package-source/operations.ts +9 -3
- package/extension/src/search/rendering.ts +2 -1
- package/extension/src/tool-output-bounds.ts +36 -0
- package/extension/src/workspace-cache/caching-overlay.ts +7 -4
- package/extension/src/workspace-cache/operations.ts +2 -1
- package/package.json +6 -4
|
@@ -50,9 +50,21 @@ export interface CodeIntelligenceOperations {
|
|
|
50
50
|
/** Never spawns a symbol index -- safe to call opportunistically (e.g. before deciding whether to enrich a result). */
|
|
51
51
|
hasWarmIndex(path: string): Promise<boolean>;
|
|
52
52
|
workspaceMap(path: string, maxNodes: number, maxEdges: number, maxEntries: number, maxBytes: number): Promise<OperationOutputs["workspace.map"]>;
|
|
53
|
+
localizeContext(
|
|
54
|
+
path: string,
|
|
55
|
+
query: string,
|
|
56
|
+
options?: {
|
|
57
|
+
seedSymbols?: readonly string[];
|
|
58
|
+
seedLocations?: readonly { path: string; line: number; character?: number }[];
|
|
59
|
+
maxSymbols?: number;
|
|
60
|
+
maxBytes?: number;
|
|
61
|
+
maxDepth?: number;
|
|
62
|
+
deadlineMs?: number;
|
|
63
|
+
},
|
|
64
|
+
): Promise<OperationOutputs["workspace.localizeContext"]>;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperations {
|
|
67
|
+
export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIntelligenceOperations {
|
|
56
68
|
return {
|
|
57
69
|
async goToDefinition(path, line, character) {
|
|
58
70
|
return withWorkspace(
|
|
@@ -144,6 +156,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
|
|
|
144
156
|
operation: "workspace.populateSymbolGraph",
|
|
145
157
|
input: { workspaceId, maxFiles, maxSymbolsPerFile },
|
|
146
158
|
waitMs,
|
|
159
|
+
...(ownerId ? { ownerId } : {}),
|
|
147
160
|
});
|
|
148
161
|
return job;
|
|
149
162
|
},
|
|
@@ -183,5 +196,14 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
|
|
|
183
196
|
},
|
|
184
197
|
);
|
|
185
198
|
},
|
|
199
|
+
async localizeContext(path, query, options = {}) {
|
|
200
|
+
return withWorkspace(
|
|
201
|
+
() => workspaceForPathOrDirectory(path),
|
|
202
|
+
async ({ workspaceId }) => {
|
|
203
|
+
const client = await lectorClient();
|
|
204
|
+
return client.call("workspace.localizeContext", { workspaceId, query, ...options });
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
},
|
|
186
208
|
};
|
|
187
209
|
}
|
|
@@ -92,7 +92,8 @@ export function formatSearchTextAcrossProjectsResult(
|
|
|
92
92
|
items: outcome.result.matches,
|
|
93
93
|
expanded,
|
|
94
94
|
visibleCount: DEFAULT_VISIBLE_PER_WORKSPACE,
|
|
95
|
-
formatItem: (match) =>
|
|
95
|
+
formatItem: (match) =>
|
|
96
|
+
` ${match.path}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}${match.lineTruncated ? theme.fg("warning", " (line truncated)") : ""}`,
|
|
96
97
|
moreLine: (hidden) => theme.fg("dim", ` ... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
97
98
|
truncationWarning: outcome.result.truncated
|
|
98
99
|
? theme.fg("warning", " (this workspace's search was itself truncated by maxMatches/maxBytes)")
|
|
@@ -38,4 +38,10 @@ export { ExplorerComponent, type ExplorerResult, joinExplorerPath } from "./expl
|
|
|
38
38
|
// exact, already-tested loop without re-deriving it, instead of only this package's own Pi
|
|
39
39
|
// extension entry point being able to.
|
|
40
40
|
export { type ExplorerFlowHost, runExplorerFlow } from "./explorer-flow.ts";
|
|
41
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
type EditorBufferSnapshot,
|
|
43
|
+
type EditorHoverOutcome,
|
|
44
|
+
type EditorHoverRequest,
|
|
45
|
+
ModalEditorComponent,
|
|
46
|
+
type ModalEditorHost,
|
|
47
|
+
} from "./modal-editor-component.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extname } from "node:path";
|
|
2
|
-
import type { HighlightSpan } from "@danypops/lector";
|
|
3
|
-
import { highlightSpans } from "@danypops/lector";
|
|
2
|
+
import type { ContentHash, HighlightSpan } from "@danypops/lector";
|
|
3
|
+
import { contentHashOf, highlightSpans } from "@danypops/lector";
|
|
4
4
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -10,12 +10,30 @@ import type { EditorTheme } from "./editor-theme.ts";
|
|
|
10
10
|
|
|
11
11
|
export type { EditorTheme } from "./editor-theme.ts";
|
|
12
12
|
|
|
13
|
+
export interface EditorBufferSnapshot {
|
|
14
|
+
readonly text: string;
|
|
15
|
+
readonly hash: ContentHash;
|
|
16
|
+
readonly dirty: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface EditorHoverRequest {
|
|
20
|
+
readonly line: number;
|
|
21
|
+
readonly character: number;
|
|
22
|
+
readonly buffer: EditorBufferSnapshot;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type EditorHoverOutcome =
|
|
26
|
+
| { readonly kind: "ready"; readonly hover?: { readonly contents: string } }
|
|
27
|
+
| { readonly kind: "stale-active-buffer"; readonly bufferHash: ContentHash };
|
|
28
|
+
|
|
13
29
|
export interface ModalEditorHost {
|
|
14
30
|
filePath: string;
|
|
15
31
|
/** Saves the buffer's current text through Lector's hash-guarded write. Throws (surfaced as a status message, not a crash) on a genuinely concurrent external change. */
|
|
16
32
|
save(text: string): Promise<void>;
|
|
17
|
-
/**
|
|
33
|
+
/** Resolves hover against the saved file for hosts that do not expose active-buffer semantics. */
|
|
18
34
|
hover(line: number, character: number): Promise<{ contents: string } | undefined>;
|
|
35
|
+
/** Resolves hover against the supplied active-buffer snapshot or reports that semantic evidence is stale. */
|
|
36
|
+
hoverSnapshot?(request: EditorHoverRequest): Promise<EditorHoverOutcome>;
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
const CAPTURE_COLOR: Record<string, ThemeColor> = {
|
|
@@ -111,9 +129,29 @@ export class ModalEditorComponent implements Component {
|
|
|
111
129
|
this.done();
|
|
112
130
|
return;
|
|
113
131
|
case "hover": {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
132
|
+
const text = this.state.buffer.text;
|
|
133
|
+
const request = {
|
|
134
|
+
line: this.state.cursorLine,
|
|
135
|
+
character: this.state.cursorCharacter,
|
|
136
|
+
buffer: { text, hash: contentHashOf(text), dirty: this.state.dirty },
|
|
137
|
+
};
|
|
138
|
+
const outcome = this.host.hoverSnapshot
|
|
139
|
+
? await this.host.hoverSnapshot(request)
|
|
140
|
+
: { kind: "ready" as const, hover: await this.host.hover(request.line, request.character) };
|
|
141
|
+
switch (outcome.kind) {
|
|
142
|
+
case "ready": {
|
|
143
|
+
const firstLine = outcome.hover?.contents.split("\n")[0];
|
|
144
|
+
this.statusMessage = firstLine ?? "no hover info at this position";
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "stale-active-buffer":
|
|
148
|
+
this.statusMessage = "stale active buffer: save or discard changes before semantic queries";
|
|
149
|
+
break;
|
|
150
|
+
default: {
|
|
151
|
+
const exhaustive: never = outcome;
|
|
152
|
+
throw new Error(`Unhandled hover outcome: ${JSON.stringify(exhaustive)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
117
155
|
break;
|
|
118
156
|
}
|
|
119
157
|
default: {
|
package/extension/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import type {
|
|
5
5
|
CachedRepositoryPage,
|
|
6
6
|
ContentHash,
|
|
7
|
+
ContextBundleResult,
|
|
7
8
|
Diagnostic,
|
|
8
9
|
DocumentSymbolEntry,
|
|
9
10
|
EditOutcome,
|
|
@@ -38,10 +39,11 @@ import {
|
|
|
38
39
|
createWriteToolDefinition,
|
|
39
40
|
type ExtensionAPI,
|
|
40
41
|
type ExtensionCommandContext,
|
|
42
|
+
type ToolDefinition,
|
|
41
43
|
} from "@earendil-works/pi-coding-agent";
|
|
42
44
|
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
43
45
|
import { renderBoundedTable, type TextMeasure } from "malevich-tui-components";
|
|
44
|
-
import { Type } from "typebox";
|
|
46
|
+
import { type TSchema, Type } from "typebox";
|
|
45
47
|
|
|
46
48
|
/** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
|
|
47
49
|
const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
@@ -95,7 +97,8 @@ import { formatGitCall, formatGitResult, type GitToolDetails } from "./git/rende
|
|
|
95
97
|
import { nearestGitWorkspaceRoot, setNewWorkspaceObserver } from "./lector-client.ts";
|
|
96
98
|
import { createLectorLineEditOperations } from "./line-edit/operations.ts";
|
|
97
99
|
import { formatLineEditCall, formatLineEditResult } from "./line-edit/rendering.ts";
|
|
98
|
-
import { createMutationHistoryOperations } from "./mutation-history/operations.ts";
|
|
100
|
+
import { createMutationHistoryOperations, type MutationTransactionRevertOutcome } from "./mutation-history/operations.ts";
|
|
101
|
+
import { formatMutationHistoryList, formatMutationTransactionRevert } from "./mutation-history/rendering.ts";
|
|
99
102
|
import { createLectorPackageSourceOperations, type PackageSourceListPage } from "./package-source/operations.ts";
|
|
100
103
|
import {
|
|
101
104
|
buildPackageSourceListTableRows,
|
|
@@ -128,6 +131,7 @@ import { createLectorSearchOperations } from "./search/operations.ts";
|
|
|
128
131
|
import { formatSearchCall, formatSearchResult } from "./search/rendering.ts";
|
|
129
132
|
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation/operations.ts";
|
|
130
133
|
import { formatAnnotationDetail, formatAnnotationListSummary, formatAnnotationSummary } from "./symbol-annotation/rendering.ts";
|
|
134
|
+
import { boundLectorToolText } from "./tool-output-bounds.ts";
|
|
131
135
|
import type { LectorVehicleCall } from "./vehicle-client.ts";
|
|
132
136
|
import { CachingOverlay } from "./workspace-cache/caching-overlay.ts";
|
|
133
137
|
import {
|
|
@@ -168,7 +172,23 @@ function renderIntelligenceSource(body: string, provenance: IntelligenceProvenan
|
|
|
168
172
|
* "start it with `lector serve`" error if none is reachable.
|
|
169
173
|
*/
|
|
170
174
|
export default function (pi: ExtensionAPI) {
|
|
171
|
-
|
|
175
|
+
let cacheOperations = createWorkspaceCacheOperations();
|
|
176
|
+
// Registration is the ownership boundary: every custom Lector tool enters this set
|
|
177
|
+
// automatically, so future tools cannot silently bypass the final model-content cap.
|
|
178
|
+
// Built-in-compatible read/write/edit stay on Pi's own definitions and truncation path.
|
|
179
|
+
const customToolNames = new Set<string>();
|
|
180
|
+
function registerLectorTool<TParams extends TSchema, TDetails = unknown, TState = unknown>(tool: ToolDefinition<TParams, TDetails, TState>): void {
|
|
181
|
+
customToolNames.add(tool.name);
|
|
182
|
+
pi.registerTool(tool);
|
|
183
|
+
}
|
|
184
|
+
pi.on("tool_result", (event) => {
|
|
185
|
+
if (!customToolNames.has(event.toolName)) return;
|
|
186
|
+
const textBlocks = event.content.filter((block): block is Extract<(typeof event.content)[number], { type: "text" }> => block.type === "text");
|
|
187
|
+
if (textBlocks.length === 0) return;
|
|
188
|
+
const bounded = boundLectorToolText(textBlocks.map((block) => block.text).join("\n"));
|
|
189
|
+
if (!bounded.truncation) return;
|
|
190
|
+
return { content: [{ type: "text", text: bounded.text }, ...event.content.filter((block) => block.type !== "text")] };
|
|
191
|
+
});
|
|
172
192
|
// One generation counter shared by every root's monitor loop, not per-root -- a new session
|
|
173
193
|
// (or shutdown) invalidates every previous session's in-flight monitor regardless of which
|
|
174
194
|
// root it tracked, and there is exactly one "current session" at a time.
|
|
@@ -292,6 +312,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
292
312
|
|
|
293
313
|
pi.on("session_start", (_event, ctx) => {
|
|
294
314
|
const { cwd } = ctx;
|
|
315
|
+
const ownerId = ctx.sessionManager.getSessionId();
|
|
316
|
+
cacheOperations = createWorkspaceCacheOperations(ownerId);
|
|
295
317
|
sessionGeneration++;
|
|
296
318
|
cacheStatesByRoot.clear();
|
|
297
319
|
monitoringRoots.clear();
|
|
@@ -299,9 +321,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
299
321
|
uiContext = ctx;
|
|
300
322
|
setNewWorkspaceObserver((root) => startMonitoringRoot(root, ctx));
|
|
301
323
|
if (ctx.hasUI) {
|
|
302
|
-
// The persistent widget counterpart to the single-line "lector-cache" status above
|
|
303
|
-
//
|
|
304
|
-
cachingOverlay
|
|
324
|
+
// The persistent widget counterpart to the single-line "lector-cache" status above.
|
|
325
|
+
// Ownership is the Pi session, not cwd: one session may legitimately touch several roots.
|
|
326
|
+
cachingOverlay = new CachingOverlay(undefined, ownerId);
|
|
305
327
|
cachingOverlay.setUI(ctx.ui);
|
|
306
328
|
void cachingOverlay.refresh();
|
|
307
329
|
cachingOverlay.startPolling();
|
|
@@ -323,7 +345,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
323
345
|
pi.registerTool(createEditToolDefinition(cwd, { operations: createLectorEditOperations() }));
|
|
324
346
|
|
|
325
347
|
const findSymbolsOperations = createLectorFindSymbolsOperations();
|
|
326
|
-
|
|
348
|
+
registerLectorTool({
|
|
327
349
|
name: "find_symbols",
|
|
328
350
|
label: "Find Symbols",
|
|
329
351
|
description:
|
|
@@ -385,7 +407,75 @@ export default function (pi: ExtensionAPI) {
|
|
|
385
407
|
},
|
|
386
408
|
});
|
|
387
409
|
|
|
388
|
-
const codeIntelligenceOperations = createLectorCodeIntelligenceOperations();
|
|
410
|
+
const codeIntelligenceOperations = createLectorCodeIntelligenceOperations(ownerId);
|
|
411
|
+
|
|
412
|
+
registerLectorTool({
|
|
413
|
+
name: "localize_context",
|
|
414
|
+
label: "Localize Context",
|
|
415
|
+
description:
|
|
416
|
+
"Localize a natural-language coding task to a bounded, ranked set of workspace symbols. Combines lexical source matches with the persisted call/reference/containment graph, returns compact signatures and explicit score reasons, and reports incomplete or unavailable graph coverage. The daemon does not invoke an LLM. `directory` selects the project explicitly.",
|
|
417
|
+
promptSnippet: "Localize a coding task to ranked symbols and compact graph-backed context",
|
|
418
|
+
promptGuidelines: [
|
|
419
|
+
"Use localize_context near the start of an unfamiliar implementation or debugging task to get a bounded candidate set before reading files one by one.",
|
|
420
|
+
"Treat every reason as provenance for a retrieval signal, not proof of semantic dataflow; inspect completeness before relying on graph omissions.",
|
|
421
|
+
],
|
|
422
|
+
parameters: Type.Object({
|
|
423
|
+
query: Type.String({ description: "Natural-language coding task or issue text" }),
|
|
424
|
+
directory: Type.String({ description: "Directory of the project to localize, absolute or relative to the current working directory" }),
|
|
425
|
+
seedSymbols: Type.Optional(Type.Array(Type.String(), { description: "Optional exact symbol names that anchor graph expansion", maxItems: 100 })),
|
|
426
|
+
seedLocations: Type.Optional(
|
|
427
|
+
Type.Array(
|
|
428
|
+
Type.Object({
|
|
429
|
+
path: Type.String(),
|
|
430
|
+
line: Type.Number({ description: "1-indexed line" }),
|
|
431
|
+
character: Type.Optional(Type.Number({ description: "Optional 1-indexed declaration character" })),
|
|
432
|
+
}),
|
|
433
|
+
{ description: "Optional declaration locations that anchor graph expansion", maxItems: 100 },
|
|
434
|
+
),
|
|
435
|
+
),
|
|
436
|
+
maxSymbols: Type.Optional(Type.Number({ description: "Maximum candidates returned (default 20, maximum 500)" })),
|
|
437
|
+
maxBytes: Type.Optional(Type.Number({ description: "Maximum serialized candidate bytes (default 30000, maximum 2 MiB)" })),
|
|
438
|
+
maxDepth: Type.Optional(Type.Number({ description: "Maximum call/reference/containment expansion depth (default 2, maximum 5)" })),
|
|
439
|
+
deadlineMs: Type.Optional(Type.Number({ description: "Wall-clock budget in milliseconds (default 5000, maximum 30000)" })),
|
|
440
|
+
}),
|
|
441
|
+
async execute(_toolCallId, params) {
|
|
442
|
+
const directory = resolve(cwd, params.directory);
|
|
443
|
+
const result = await codeIntelligenceOperations.localizeContext(directory, params.query, {
|
|
444
|
+
...(params.seedSymbols ? { seedSymbols: params.seedSymbols } : {}),
|
|
445
|
+
...(params.seedLocations ? { seedLocations: params.seedLocations.map((seed) => ({ ...seed, path: resolve(cwd, seed.path) })) } : {}),
|
|
446
|
+
...(params.maxSymbols !== undefined ? { maxSymbols: params.maxSymbols } : {}),
|
|
447
|
+
...(params.maxBytes !== undefined ? { maxBytes: params.maxBytes } : {}),
|
|
448
|
+
...(params.maxDepth !== undefined ? { maxDepth: params.maxDepth } : {}),
|
|
449
|
+
...(params.deadlineMs !== undefined ? { deadlineMs: params.deadlineMs } : {}),
|
|
450
|
+
});
|
|
451
|
+
const completeness = `lexical=${result.completeness.lexical}, graph=${result.completeness.graph}${result.completeness.deadlineReached ? ", deadline reached" : ""}${result.truncated ? ", truncated" : ""}`;
|
|
452
|
+
const text =
|
|
453
|
+
result.candidates.length === 0
|
|
454
|
+
? `No localization candidates.\nCompleteness: ${completeness}`
|
|
455
|
+
: `Primary candidates\n\n${result.candidates
|
|
456
|
+
.map(
|
|
457
|
+
(candidate, index) =>
|
|
458
|
+
`${index + 1}. ${candidate.name}\n ${candidate.path}:${candidate.line}:${candidate.character}\n ${candidate.signature ?? candidate.kind}\n Reasons:\n${candidate.reasons.map((reason) => ` - ${reason.detail} (+${reason.score})`).join("\n")}`,
|
|
459
|
+
)
|
|
460
|
+
.join("\n\n")}\n\nCompleteness: ${completeness}`;
|
|
461
|
+
return { content: [{ type: "text", text }], details: result };
|
|
462
|
+
},
|
|
463
|
+
renderCall(args, theme) {
|
|
464
|
+
return new Text(theme.fg("toolTitle", `localize ${typeof args.query === "string" ? args.query : "context"}`), 0, 0);
|
|
465
|
+
},
|
|
466
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
467
|
+
if (isPartial) return new Text(theme.fg("warning", "Localizing..."), 0, 0);
|
|
468
|
+
if (context.isError) {
|
|
469
|
+
const errorText = result.content
|
|
470
|
+
.filter((block) => block.type === "text")
|
|
471
|
+
.map((block) => block.text)
|
|
472
|
+
.join("\n");
|
|
473
|
+
return new Text(theme.fg("error", errorText || "localize_context failed"), 0, 0);
|
|
474
|
+
}
|
|
475
|
+
const details = result.details as ContextBundleResult | undefined;
|
|
476
|
+
return new Text(details ? `${details.candidates.length} candidates · graph ${details.completeness.graph}` : "Localization complete", 0, 0);
|
|
477
|
+
},
|
|
478
|
+
});
|
|
389
479
|
|
|
390
480
|
const editorOverlayOptions = { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "center" } } as const;
|
|
391
481
|
|
|
@@ -406,6 +496,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
406
496
|
const result = await codeIntelligenceOperations.hover(absolutePath, line, character);
|
|
407
497
|
return result.hover;
|
|
408
498
|
},
|
|
499
|
+
hoverSnapshot: async (request) => {
|
|
500
|
+
if (request.buffer.dirty) return { kind: "stale-active-buffer", bufferHash: request.buffer.hash };
|
|
501
|
+
const result = await codeIntelligenceOperations.hover(absolutePath, request.line, request.character);
|
|
502
|
+
return { kind: "ready", hover: result.hover };
|
|
503
|
+
},
|
|
409
504
|
};
|
|
410
505
|
return new ModalEditorComponent(tui, theme, host, session.content, () => done(undefined));
|
|
411
506
|
}, editorOverlayOptions);
|
|
@@ -451,7 +546,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
451
546
|
character: Type.Number({ description: "1-indexed character offset within the line" }),
|
|
452
547
|
};
|
|
453
548
|
|
|
454
|
-
|
|
549
|
+
registerLectorTool({
|
|
455
550
|
name: "go_to_definition",
|
|
456
551
|
label: "Go to Definition",
|
|
457
552
|
description: "Find where the symbol at an exact file position is actually declared, across files, through re-exports and aliasing.",
|
|
@@ -485,7 +580,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
485
580
|
},
|
|
486
581
|
});
|
|
487
582
|
|
|
488
|
-
|
|
583
|
+
registerLectorTool({
|
|
489
584
|
name: "go_to_implementation",
|
|
490
585
|
label: "Go to Implementation",
|
|
491
586
|
description:
|
|
@@ -523,7 +618,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
523
618
|
},
|
|
524
619
|
});
|
|
525
620
|
|
|
526
|
-
|
|
621
|
+
registerLectorTool({
|
|
527
622
|
name: "find_references",
|
|
528
623
|
label: "Find References",
|
|
529
624
|
description: "Find every project-wide usage of the symbol at an exact file position.",
|
|
@@ -568,7 +663,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
568
663
|
},
|
|
569
664
|
});
|
|
570
665
|
|
|
571
|
-
|
|
666
|
+
registerLectorTool({
|
|
572
667
|
name: "hover",
|
|
573
668
|
label: "Hover",
|
|
574
669
|
description: "Get type and documentation information for the symbol at an exact file position.",
|
|
@@ -608,7 +703,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
608
703
|
},
|
|
609
704
|
});
|
|
610
705
|
|
|
611
|
-
|
|
706
|
+
registerLectorTool({
|
|
612
707
|
name: "document_symbols",
|
|
613
708
|
label: "Document Symbols",
|
|
614
709
|
description: "List every symbol declared in one file, hierarchically -- an outline of its classes, functions, and their members.",
|
|
@@ -642,7 +737,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
642
737
|
},
|
|
643
738
|
});
|
|
644
739
|
|
|
645
|
-
|
|
740
|
+
registerLectorTool({
|
|
646
741
|
name: "diagnostics",
|
|
647
742
|
label: "Diagnostics",
|
|
648
743
|
description: "List every error and warning a language server currently knows about for one file, as of its last analysis.",
|
|
@@ -679,7 +774,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
679
774
|
},
|
|
680
775
|
});
|
|
681
776
|
|
|
682
|
-
|
|
777
|
+
registerLectorTool({
|
|
683
778
|
name: "call_hierarchy",
|
|
684
779
|
label: "Call Hierarchy",
|
|
685
780
|
description:
|
|
@@ -745,7 +840,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
745
840
|
},
|
|
746
841
|
});
|
|
747
842
|
|
|
748
|
-
|
|
843
|
+
registerLectorTool({
|
|
749
844
|
name: "reference_based_rename",
|
|
750
845
|
label: "Reference-Based Rename",
|
|
751
846
|
description:
|
|
@@ -808,7 +903,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
808
903
|
applied?: OperationOutputs["workspace.rename"];
|
|
809
904
|
}
|
|
810
905
|
|
|
811
|
-
|
|
906
|
+
registerLectorTool({
|
|
812
907
|
name: "rename",
|
|
813
908
|
label: "Rename",
|
|
814
909
|
description:
|
|
@@ -877,7 +972,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
877
972
|
uncontained?: boolean;
|
|
878
973
|
}
|
|
879
974
|
|
|
880
|
-
|
|
975
|
+
registerLectorTool({
|
|
881
976
|
name: "symbol_annotations",
|
|
882
977
|
label: "Symbol Annotations",
|
|
883
978
|
description:
|
|
@@ -1056,7 +1151,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1056
1151
|
},
|
|
1057
1152
|
});
|
|
1058
1153
|
|
|
1059
|
-
|
|
1154
|
+
registerLectorTool({
|
|
1060
1155
|
name: "reachable_from",
|
|
1061
1156
|
label: "Reachable From",
|
|
1062
1157
|
description:
|
|
@@ -1105,7 +1200,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1105
1200
|
},
|
|
1106
1201
|
});
|
|
1107
1202
|
|
|
1108
|
-
|
|
1203
|
+
registerLectorTool({
|
|
1109
1204
|
name: "workspace_map",
|
|
1110
1205
|
label: "Workspace Map",
|
|
1111
1206
|
description:
|
|
@@ -1125,14 +1220,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1125
1220
|
async execute(_toolCallId, params) {
|
|
1126
1221
|
const path = resolve(cwd, params.path);
|
|
1127
1222
|
const result = await codeIntelligenceOperations.workspaceMap(path, params.maxNodes, params.maxEdges, params.maxEntries, params.maxBytes);
|
|
1128
|
-
const
|
|
1223
|
+
const coverage = `Candidate coverage: languages=${result.candidateSelection.representedLanguages.join(",") || "none"}; scopes=${result.candidateSelection.representedScopes.join(",") || "none"}${result.candidateSelection.omittedScopes.length > 0 ? `; omitted=${result.candidateSelection.omittedScopes.join(",")}` : ""}; strategy=${result.candidateSelection.strategy}.`;
|
|
1224
|
+
const text = `${
|
|
1129
1225
|
result.entries.length === 0
|
|
1130
1226
|
? "No ranked symbols (the workspace's symbol graph may still be populating in the background -- retry shortly)."
|
|
1131
1227
|
: result.entries
|
|
1132
1228
|
.map(
|
|
1133
1229
|
(entry) => `${entry.kind} ${entry.name} -- ${entry.path}:${entry.line}:${entry.character}${entry.signature ? ` -- ${entry.signature}` : ""}`,
|
|
1134
1230
|
)
|
|
1135
|
-
.join("\n")
|
|
1231
|
+
.join("\n")
|
|
1232
|
+
}\n${coverage}`;
|
|
1136
1233
|
return { content: [{ type: "text", text }], details: { result } };
|
|
1137
1234
|
},
|
|
1138
1235
|
renderCall(args, theme, context) {
|
|
@@ -1162,7 +1259,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1162
1259
|
readonly job?: JobSnapshot<PopulateSymbolGraphResult>;
|
|
1163
1260
|
}
|
|
1164
1261
|
|
|
1165
|
-
|
|
1262
|
+
registerLectorTool({
|
|
1166
1263
|
name: "workspace_cache",
|
|
1167
1264
|
label: "Workspace Cache",
|
|
1168
1265
|
description:
|
|
@@ -1253,7 +1350,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1253
1350
|
});
|
|
1254
1351
|
|
|
1255
1352
|
const gitOperations = createLectorGitOperations();
|
|
1256
|
-
|
|
1353
|
+
registerLectorTool({
|
|
1257
1354
|
name: "git",
|
|
1258
1355
|
label: "Git",
|
|
1259
1356
|
description:
|
|
@@ -1396,7 +1493,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1396
1493
|
});
|
|
1397
1494
|
|
|
1398
1495
|
const searchOperations = createLectorSearchOperations();
|
|
1399
|
-
|
|
1496
|
+
registerLectorTool({
|
|
1400
1497
|
name: "search_code",
|
|
1401
1498
|
label: "Search Code",
|
|
1402
1499
|
description:
|
|
@@ -1437,7 +1534,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1437
1534
|
});
|
|
1438
1535
|
|
|
1439
1536
|
const findFilesOperations = createLectorFindFilesOperations();
|
|
1440
|
-
|
|
1537
|
+
registerLectorTool({
|
|
1441
1538
|
name: "find_files",
|
|
1442
1539
|
label: "Find Files",
|
|
1443
1540
|
description:
|
|
@@ -1483,7 +1580,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1483
1580
|
});
|
|
1484
1581
|
|
|
1485
1582
|
const lineEditOperations = createLectorLineEditOperations();
|
|
1486
|
-
|
|
1583
|
+
registerLectorTool({
|
|
1487
1584
|
name: "line_edit",
|
|
1488
1585
|
label: "Line Edit",
|
|
1489
1586
|
description:
|
|
@@ -1554,7 +1651,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1554
1651
|
});
|
|
1555
1652
|
|
|
1556
1653
|
const applyPatchOperations = createLectorApplyPatchOperations();
|
|
1557
|
-
|
|
1654
|
+
registerLectorTool({
|
|
1558
1655
|
name: "apply_patch",
|
|
1559
1656
|
label: "Apply Patch",
|
|
1560
1657
|
description:
|
|
@@ -1602,23 +1699,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1602
1699
|
|
|
1603
1700
|
type MutationHistoryToolDetails =
|
|
1604
1701
|
| { readonly action: "list"; readonly entries: readonly MutationHistoryEntry[] }
|
|
1605
|
-
| { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } }
|
|
1702
|
+
| { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } }
|
|
1703
|
+
| { readonly action: "revert-transaction"; readonly reverted: MutationTransactionRevertOutcome };
|
|
1606
1704
|
|
|
1607
1705
|
const mutationHistoryOperations = createMutationHistoryOperations();
|
|
1608
|
-
|
|
1706
|
+
registerLectorTool({
|
|
1609
1707
|
name: "mutation_history",
|
|
1610
1708
|
label: "Mutation History",
|
|
1611
1709
|
description:
|
|
1612
|
-
"List
|
|
1613
|
-
promptSnippet: "List or revert
|
|
1710
|
+
"List a file's recorded edit history, revert a standalone entry, or atomically revert every file in a rename/multi-file transaction. Every successful edit/line_edit/apply_patch is recorded (newest first, bounded, not durable across a daemon restart). Reverts are hash-guarded and further-revertible. A transaction member is never reverted alone: use action=revert-transaction with its transactionId.",
|
|
1711
|
+
promptSnippet: "List or safely revert standalone or transaction-grouped edit history",
|
|
1614
1712
|
promptGuidelines: [
|
|
1615
|
-
"list first
|
|
1713
|
+
"list first. Use action=revert only for a standalone entry; if list reports a transactionId, use action=revert-transaction so every member is restored atomically.",
|
|
1616
1714
|
],
|
|
1617
1715
|
parameters: Type.Object({
|
|
1618
|
-
action: Type.Union([Type.Literal("list"), Type.Literal("revert")]),
|
|
1619
|
-
path: Type.String({ description: "Absolute or workspace-relative path to the
|
|
1716
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("revert"), Type.Literal("revert-transaction")]),
|
|
1717
|
+
path: Type.String({ description: "Absolute or workspace-relative path used to resolve the owning workspace" }),
|
|
1620
1718
|
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return, newest first" })),
|
|
1621
|
-
entryId: Type.Optional(Type.String({ description: "Required for action=revert --
|
|
1719
|
+
entryId: Type.Optional(Type.String({ description: "Required for action=revert -- a standalone entry id returned by list" })),
|
|
1720
|
+
transactionId: Type.Optional(Type.String({ description: "Required for action=revert-transaction -- a transaction id returned by list" })),
|
|
1622
1721
|
}),
|
|
1623
1722
|
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<MutationHistoryToolDetails>> {
|
|
1624
1723
|
const absolutePath = resolve(cwd, params.path);
|
|
@@ -1631,17 +1730,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1631
1730
|
if (params.action === "list") {
|
|
1632
1731
|
if (params.maxResults === undefined) throw new Error("mutation_history action=list requires maxResults");
|
|
1633
1732
|
const entries = await mutationHistoryOperations.list(absolutePath, params.maxResults, vehicleCall);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1733
|
+
return { content: [{ type: "text", text: formatMutationHistoryList(entries) }], details: { action: "list", entries } };
|
|
1734
|
+
}
|
|
1735
|
+
if (params.action === "revert") {
|
|
1736
|
+
if (params.entryId === undefined) throw new Error("mutation_history action=revert requires entryId");
|
|
1737
|
+
const reverted = await mutationHistoryOperations.revert(absolutePath, params.entryId, vehicleCall);
|
|
1738
|
+
return {
|
|
1739
|
+
content: [{ type: "text", text: `${reverted.path} reverted -> ${reverted.newHash ?? "(deleted)"}` }],
|
|
1740
|
+
details: { action: "revert", reverted },
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
if (params.transactionId === undefined) throw new Error("mutation_history action=revert-transaction requires transactionId");
|
|
1744
|
+
const reverted = await mutationHistoryOperations.revertTransaction(absolutePath, params.transactionId, vehicleCall);
|
|
1642
1745
|
return {
|
|
1643
|
-
content: [{ type: "text", text:
|
|
1644
|
-
details: { action: "revert", reverted },
|
|
1746
|
+
content: [{ type: "text", text: formatMutationTransactionRevert(params.transactionId, reverted) }],
|
|
1747
|
+
details: { action: "revert-transaction", reverted },
|
|
1645
1748
|
};
|
|
1646
1749
|
},
|
|
1647
1750
|
renderCall(args, theme, context) {
|
|
@@ -1690,21 +1793,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
1690
1793
|
}
|
|
1691
1794
|
|
|
1692
1795
|
const packageSourceOperations = createLectorPackageSourceOperations();
|
|
1693
|
-
|
|
1796
|
+
registerLectorTool({
|
|
1694
1797
|
name: "package_source",
|
|
1695
1798
|
label: "Package Source",
|
|
1696
1799
|
description:
|
|
1697
|
-
"Resolve, list, remove, or clean bookkeeping for installed npm
|
|
1800
|
+
"Resolve, list, remove, or clean bookkeeping for installed packages (npm, pypi, ...) resolved to verified exact repository source. action=resolve uses the project's own lockfile-family, bounded registry metadata, and an exact Git ref/commit (or, for an editable/direct-VCS/local install, the already-known source directly, no registry lookup needed); registers verified source as a read-only workspace for the other Lector tools. action=list reports every package coordinate already resolved this way -- no re-resolution, no network. action=remove drops one bookkeeping entry by its exact ecosystem/name/resolvedVersion; refuses if it is still a currently-registered workspace. action=clean removes every non-in-use entry, optionally scoped to one ecosystem. Neither remove nor clean deletes the underlying repo_cache disk entry -- use repo_cache(action=evict) for that, since a monorepo can share one checkout across several package coordinates.",
|
|
1698
1801
|
promptSnippet: "Resolve, list, remove, or clean verified package source bookkeeping",
|
|
1699
1802
|
parameters: Type.Object({
|
|
1700
1803
|
action: Type.Optional(Type.Union([Type.Literal("resolve"), Type.Literal("list"), Type.Literal("remove"), Type.Literal("clean")])),
|
|
1701
|
-
directory: Type.Optional(Type.String({ description: "Required for action=resolve -- project directory containing the
|
|
1804
|
+
directory: Type.Optional(Type.String({ description: "Required for action=resolve -- project directory containing the package's own lockfile-family" })),
|
|
1702
1805
|
name: Type.Optional(Type.String({ description: "Required for action=resolve/remove -- installed package name, including scope when present" })),
|
|
1703
1806
|
version: Type.Optional(
|
|
1704
1807
|
Type.String({ description: "action=resolve only -- exact installed version; required when the lockfile contains several versions" }),
|
|
1705
1808
|
),
|
|
1706
|
-
registry: Type.Optional(Type.String({ description: "
|
|
1707
|
-
ecosystem: Type.Optional(
|
|
1809
|
+
registry: Type.Optional(Type.String({ description: "Registry URL; defaults to the ecosystem's own public registry (npm registry / pypi.org)" })),
|
|
1810
|
+
ecosystem: Type.Optional(
|
|
1811
|
+
Type.String({
|
|
1812
|
+
description:
|
|
1813
|
+
"npm/pypi/cargo/go/maven/conan/vcpkg/nuget/swiftpm -- action=resolve defaults to npm; required for action=remove; optional filter for action=list/clean",
|
|
1814
|
+
}),
|
|
1815
|
+
),
|
|
1708
1816
|
resolvedVersion: Type.Optional(Type.String({ description: "Required for action=remove -- the exact resolved version to remove" })),
|
|
1709
1817
|
text: Type.Optional(Type.String({ description: "action=list only -- case-insensitive substring match across ecosystem/name/resolvedVersion" })),
|
|
1710
1818
|
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return in this page" })),
|
|
@@ -1737,7 +1845,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1737
1845
|
}
|
|
1738
1846
|
if (!params.directory || !params.name) throw new Error("package_source action=resolve requires directory and name");
|
|
1739
1847
|
const directory = resolve(cwd, params.directory);
|
|
1740
|
-
const result = await packageSourceOperations.resolve(
|
|
1848
|
+
const result = await packageSourceOperations.resolve(
|
|
1849
|
+
directory,
|
|
1850
|
+
params.name,
|
|
1851
|
+
params.version ?? null,
|
|
1852
|
+
params.registry ?? null,
|
|
1853
|
+
optionalPackageEcosystem(params.ecosystem),
|
|
1854
|
+
);
|
|
1741
1855
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "resolve", result } };
|
|
1742
1856
|
},
|
|
1743
1857
|
renderCall(args, theme, context) {
|
|
@@ -1796,7 +1910,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1796
1910
|
const repoFetchOperations = createLectorRepoFetchOperations();
|
|
1797
1911
|
const repoCacheListOperations = createRepoCacheListOperations();
|
|
1798
1912
|
const repoCacheEvictOperations = createRepoCacheEvictOperations();
|
|
1799
|
-
|
|
1913
|
+
registerLectorTool({
|
|
1800
1914
|
name: "repo_cache",
|
|
1801
1915
|
label: "Repo Cache",
|
|
1802
1916
|
description:
|
|
@@ -1899,7 +2013,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1899
2013
|
| { readonly action: "sourcegraph_code"; readonly result: { candidates: readonly SourcegraphCodeCandidate[] } };
|
|
1900
2014
|
|
|
1901
2015
|
const externalSearchOperations = createExternalSearchOperations();
|
|
1902
|
-
|
|
2016
|
+
registerLectorTool({
|
|
1903
2017
|
name: "external_search",
|
|
1904
2018
|
label: "External Search",
|
|
1905
2019
|
description:
|
|
@@ -1956,7 +2070,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1956
2070
|
});
|
|
1957
2071
|
|
|
1958
2072
|
const crossWorkspaceSearchOperations = createLectorCrossWorkspaceSearchOperations();
|
|
1959
|
-
|
|
2073
|
+
registerLectorTool({
|
|
1960
2074
|
name: "find_symbols_across_projects",
|
|
1961
2075
|
label: "Find Symbols Across Projects",
|
|
1962
2076
|
description:
|
|
@@ -1996,7 +2110,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1996
2110
|
},
|
|
1997
2111
|
});
|
|
1998
2112
|
|
|
1999
|
-
|
|
2113
|
+
registerLectorTool({
|
|
2000
2114
|
name: "search_code_across_projects",
|
|
2001
2115
|
label: "Search Code Across Projects",
|
|
2002
2116
|
description:
|
|
@@ -3,6 +3,13 @@ import { withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
|
3
3
|
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
4
4
|
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
5
5
|
|
|
6
|
+
const MAX_INTERNAL_HISTORY_LOOKUP_RESULTS = 2_000;
|
|
7
|
+
|
|
8
|
+
export interface MutationTransactionRevertOutcome {
|
|
9
|
+
readonly transactionId: string;
|
|
10
|
+
readonly reverted: readonly { readonly path: string; readonly newHash: string | null }[];
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
/** Match MUTATION_HISTORY_READ_PERMISSIONS/MUTATION_HISTORY_WRITE_PERMISSIONS' own declared values server-side (mutation-history/operation-registration.ts). */
|
|
7
14
|
const MUTATION_HISTORY_READ_PERMISSIONS = ["workspace:read"];
|
|
8
15
|
const MUTATION_HISTORY_WRITE_PERMISSIONS = ["workspace:write"];
|
|
@@ -15,32 +22,72 @@ const MUTATION_HISTORY_WRITE_PERMISSIONS = ["workspace:write"];
|
|
|
15
22
|
export interface MutationHistoryOperations {
|
|
16
23
|
list(absolutePath: string, maxResults: number, call: LectorVehicleCall): Promise<readonly MutationHistoryEntry[]>;
|
|
17
24
|
revert(absolutePath: string, entryId: string, call: LectorVehicleCall): Promise<{ path: string; newHash: string | null }>;
|
|
25
|
+
revertTransaction(absolutePath: string, transactionId: string, call: LectorVehicleCall): Promise<MutationTransactionRevertOutcome>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function listResolvedHistory(
|
|
29
|
+
workspaceId: string,
|
|
30
|
+
root: string,
|
|
31
|
+
absolutePath: string,
|
|
32
|
+
maxResults: number,
|
|
33
|
+
call: LectorVehicleCall,
|
|
34
|
+
): Promise<readonly MutationHistoryEntry[]> {
|
|
35
|
+
const relativePath = toWorkspaceRelativePath(root, absolutePath);
|
|
36
|
+
// Single-file edits historically record the caller's workspace-relative path, while LSP
|
|
37
|
+
// WorkspaceEdits record canonical absolute paths. Query both identities until the daemon's
|
|
38
|
+
// stored-history migration can normalize old entries, then deduplicate by immutable entry id.
|
|
39
|
+
const paths = relativePath === absolutePath ? [absolutePath] : [relativePath, absolutePath];
|
|
40
|
+
const pages = await Promise.all(
|
|
41
|
+
paths.map((path) =>
|
|
42
|
+
invokeLectorVehicleOperation<{ entries: readonly MutationHistoryEntry[] }>(
|
|
43
|
+
"workspace.mutationHistory",
|
|
44
|
+
{ workspaceId, path, maxResults },
|
|
45
|
+
MUTATION_HISTORY_READ_PERMISSIONS,
|
|
46
|
+
call,
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
const byId = new Map<string, MutationHistoryEntry>();
|
|
51
|
+
for (const page of pages) for (const entry of page.entries) byId.set(entry.id, entry);
|
|
52
|
+
return [...byId.values()].sort((a, b) => b.timestamp - a.timestamp).slice(0, maxResults);
|
|
18
53
|
}
|
|
19
54
|
|
|
20
55
|
export function createMutationHistoryOperations(): MutationHistoryOperations {
|
|
21
56
|
return {
|
|
22
57
|
list(absolutePath, maxResults, call) {
|
|
58
|
+
return withWorkspace(
|
|
59
|
+
() => workspaceForPath(absolutePath),
|
|
60
|
+
({ workspaceId, root }) => listResolvedHistory(workspaceId, root, absolutePath, maxResults, call),
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
revert(absolutePath, entryId, call) {
|
|
23
64
|
return withWorkspace(
|
|
24
65
|
() => workspaceForPath(absolutePath),
|
|
25
66
|
async ({ workspaceId, root }) => {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
const entries = await listResolvedHistory(workspaceId, root, absolutePath, MAX_INTERNAL_HISTORY_LOOKUP_RESULTS, call);
|
|
68
|
+
const target = entries.find((entry) => entry.id === entryId);
|
|
69
|
+
if (!target) throw new Error(`mutation history entry "${entryId}" is not recorded for "${absolutePath}" -- list that path again before reverting`);
|
|
70
|
+
if (target.transactionId !== null) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`mutation history entry "${entryId}" belongs to multi-file transaction "${target.transactionId}" -- refusing a partial revert; use action=revert-transaction with transactionId`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return invokeLectorVehicleOperation<{ path: string; newHash: string | null }>(
|
|
76
|
+
"workspace.revertMutation",
|
|
77
|
+
{ workspaceId, entryId },
|
|
78
|
+
MUTATION_HISTORY_WRITE_PERMISSIONS,
|
|
31
79
|
call,
|
|
32
80
|
);
|
|
33
|
-
return entries;
|
|
34
81
|
},
|
|
35
82
|
);
|
|
36
83
|
},
|
|
37
|
-
|
|
84
|
+
revertTransaction(absolutePath, transactionId, call) {
|
|
38
85
|
return withWorkspace(
|
|
39
86
|
() => workspaceForPath(absolutePath),
|
|
40
87
|
({ workspaceId }) =>
|
|
41
|
-
invokeLectorVehicleOperation<
|
|
42
|
-
"workspace.
|
|
43
|
-
{ workspaceId,
|
|
88
|
+
invokeLectorVehicleOperation<MutationTransactionRevertOutcome>(
|
|
89
|
+
"workspace.revertMutationTransaction",
|
|
90
|
+
{ workspaceId, transactionId },
|
|
44
91
|
MUTATION_HISTORY_WRITE_PERMISSIONS,
|
|
45
92
|
call,
|
|
46
93
|
),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MutationHistoryEntry } from "@danypops/lector";
|
|
2
|
+
import type { MutationTransactionRevertOutcome } from "./operations.ts";
|
|
3
|
+
|
|
4
|
+
export function formatMutationHistoryList(entries: readonly MutationHistoryEntry[]): string {
|
|
5
|
+
if (entries.length === 0) return "no recorded mutation history for this path";
|
|
6
|
+
return entries
|
|
7
|
+
.map((entry) => {
|
|
8
|
+
const grouping = entry.transactionId === null ? "standalone mutation" : `transaction ${entry.transactionId}`;
|
|
9
|
+
return `${entry.id} ${new Date(entry.timestamp).toISOString()} ${entry.operation} ${grouping}`;
|
|
10
|
+
})
|
|
11
|
+
.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatMutationTransactionRevert(originalTransactionId: string, outcome: MutationTransactionRevertOutcome): string {
|
|
15
|
+
const lines = [
|
|
16
|
+
`${originalTransactionId} reverted atomically; revert recorded as transaction ${outcome.transactionId}`,
|
|
17
|
+
...outcome.reverted.map((entry) => `${entry.path} -> ${entry.newHash ?? "(deleted)"}`),
|
|
18
|
+
];
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
@@ -7,7 +7,13 @@ export interface PackageSourceListPage {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface PackageSourceOperations {
|
|
10
|
-
resolve(
|
|
10
|
+
resolve(
|
|
11
|
+
directory: string,
|
|
12
|
+
name: string,
|
|
13
|
+
requestedVersion: string | null,
|
|
14
|
+
registry: string | null,
|
|
15
|
+
ecosystem?: PackageEcosystem,
|
|
16
|
+
): Promise<PackageSourceOperationResult>;
|
|
11
17
|
list(options: { ecosystem?: PackageEcosystem; text?: string; maxResults: number; cursor?: string }): Promise<PackageSourceListPage>;
|
|
12
18
|
remove(ecosystem: PackageEcosystem, registry: string | null, name: string, resolvedVersion: string): Promise<{ removed: boolean }>;
|
|
13
19
|
clean(ecosystem: PackageEcosystem | undefined): Promise<{ removed: number; skipped: number }>;
|
|
@@ -15,12 +21,12 @@ export interface PackageSourceOperations {
|
|
|
15
21
|
|
|
16
22
|
export function createLectorPackageSourceOperations(): PackageSourceOperations {
|
|
17
23
|
return {
|
|
18
|
-
async resolve(directory, name, requestedVersion, registry) {
|
|
24
|
+
async resolve(directory, name, requestedVersion, registry, ecosystem = "npm") {
|
|
19
25
|
const client = await lectorClient();
|
|
20
26
|
return client.callOnce("package.resolveSource", {
|
|
21
27
|
request: {
|
|
22
28
|
projectRoot: directory,
|
|
23
|
-
coordinate: { ecosystem
|
|
29
|
+
coordinate: { ecosystem, registry, name, requestedVersion },
|
|
24
30
|
},
|
|
25
31
|
bounds: DEFAULT_PACKAGE_SOURCE_BOUNDS,
|
|
26
32
|
});
|
|
@@ -17,7 +17,8 @@ export function formatSearchResult(result: TextSearchResult | undefined, expande
|
|
|
17
17
|
items: result.matches,
|
|
18
18
|
expanded,
|
|
19
19
|
visibleCount: DEFAULT_VISIBLE_MATCHES,
|
|
20
|
-
formatItem: (match) =>
|
|
20
|
+
formatItem: (match) =>
|
|
21
|
+
`${theme.fg("accent", match.path)}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}${match.lineTruncated ? theme.fg("warning", " (line truncated)") : ""}`,
|
|
21
22
|
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
22
23
|
truncationWarning: result.truncated ? theme.fg("warning", "(search itself was truncated by maxMatches/maxBytes -- results are incomplete)") : undefined,
|
|
23
24
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface BoundedLectorToolText {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly truncation: TruncationResult | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function noticeFor(truncation: TruncationResult): string {
|
|
9
|
+
return (
|
|
10
|
+
`[Lector tool output truncated: ${truncation.totalLines} lines total, ${formatSize(truncation.totalBytes)} total; ` +
|
|
11
|
+
`showing ${truncation.outputLines} lines and ${formatSize(truncation.outputBytes)}. Refine the query or lower its scope.]`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Applies Pi's process-level custom-tool output contract independently of Lector's domain bounds.
|
|
17
|
+
* Domain maxBytes values intentionally describe DTO payload fields, not the final serialized tool
|
|
18
|
+
* message; this final guard accounts for separators, provenance, JSON framing, and caller-selected
|
|
19
|
+
* bounds before content enters model context.
|
|
20
|
+
*/
|
|
21
|
+
export function boundLectorToolText(text: string): BoundedLectorToolText {
|
|
22
|
+
const initial = truncateHead(text);
|
|
23
|
+
if (!initial.truncated) return { text, truncation: undefined };
|
|
24
|
+
|
|
25
|
+
let bounded = initial;
|
|
26
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
27
|
+
const notice = noticeFor(bounded);
|
|
28
|
+
const noticeBytes = Buffer.byteLength(`\n\n${notice}`, "utf8");
|
|
29
|
+
bounded = truncateHead(text, {
|
|
30
|
+
maxLines: Math.max(1, DEFAULT_MAX_LINES - 2),
|
|
31
|
+
maxBytes: Math.max(1, DEFAULT_MAX_BYTES - noticeBytes),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const notice = noticeFor(bounded);
|
|
35
|
+
return { text: bounded.content ? `${bounded.content}\n\n${notice}` : notice, truncation: bounded };
|
|
36
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* DoctorOverlay: factory-form ctx.ui.setWidget registration, requestRender on refresh, hides the
|
|
5
5
|
* widget entirely (setWidget(key, undefined)) rather than an empty box once nothing is caching.
|
|
6
6
|
*
|
|
7
|
-
* workspace.activeCachingJobs
|
|
8
|
-
*
|
|
7
|
+
* workspace.activeCachingJobs is filtered by the owning Pi session, while its omitted-owner
|
|
8
|
+
* daemon contract remains available for global administration -- see cache-query-handlers.ts.
|
|
9
9
|
*/
|
|
10
10
|
import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
@@ -39,7 +39,10 @@ export class CachingOverlay {
|
|
|
39
39
|
intervalMs: CACHING_WIDGET_ROTATION_INTERVAL_MS,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
constructor(
|
|
42
|
+
constructor(
|
|
43
|
+
private readonly connect: () => Promise<RetryingLectorClient> = lectorClient,
|
|
44
|
+
private readonly ownerId?: string,
|
|
45
|
+
) {}
|
|
43
46
|
|
|
44
47
|
setUI(ctx: ExtensionUIContext): void {
|
|
45
48
|
if (ctx !== this.uiCtx) {
|
|
@@ -55,7 +58,7 @@ export class CachingOverlay {
|
|
|
55
58
|
async refresh(): Promise<void> {
|
|
56
59
|
try {
|
|
57
60
|
const client = await this.connect();
|
|
58
|
-
const result = await client.call("workspace.activeCachingJobs", {});
|
|
61
|
+
const result = await client.call("workspace.activeCachingJobs", this.ownerId ? { ownerId: this.ownerId } : {});
|
|
59
62
|
this.projection = buildCachingWidgetProjection(result.jobs);
|
|
60
63
|
} catch {
|
|
61
64
|
this.projection = EMPTY_PROJECTION;
|
|
@@ -39,7 +39,7 @@ export interface WorkspaceCacheOperations {
|
|
|
39
39
|
watchJob?(jobId: string, onJob: (job: JobSnapshot<PopulateSymbolGraphResult>) => void): Promise<JobWatchOutcome>;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
|
|
42
|
+
export function createWorkspaceCacheOperations(ownerId?: string): WorkspaceCacheOperations {
|
|
43
43
|
return {
|
|
44
44
|
status(directory, maxFiles, maxSymbolsPerFile) {
|
|
45
45
|
return withWorkspace(
|
|
@@ -59,6 +59,7 @@ export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
|
|
|
59
59
|
operation: "workspace.populateSymbolGraph",
|
|
60
60
|
input: { workspaceId, maxFiles, maxSymbolsPerFile, retryTimeBudgetMs },
|
|
61
61
|
waitMs,
|
|
62
|
+
...(ownerId ? { ownerId } : {}),
|
|
62
63
|
});
|
|
63
64
|
return job;
|
|
64
65
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
4
4
|
"description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
"@danypops/vehicle-client-pi": "^0.45.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@danypops/lector": "^0.
|
|
25
|
+
"@danypops/lector": "^0.20.0",
|
|
26
26
|
"@danypops/vehicle-client": "^0.10.3",
|
|
27
27
|
"@danypops/vehicle-core": "^0.17.1",
|
|
28
28
|
"malevich-tui-components": "^0.32.1",
|
|
29
29
|
"picomatch": "^4.0.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@danypops/
|
|
33
|
-
"@danypops/vehicle-conformance": "^0.4.0",
|
|
32
|
+
"@danypops/pi-eval-harness": "^0.1.0",
|
|
34
33
|
"@danypops/pi-extension-harness": "^0.2.0",
|
|
34
|
+
"@danypops/pi-process-harness": "^0.1.3",
|
|
35
35
|
"@danypops/pi-tui-harness": "^0.0.1",
|
|
36
|
+
"@danypops/vehicle-client-pi": "^0.45.0",
|
|
37
|
+
"@danypops/vehicle-conformance": "^0.4.0",
|
|
36
38
|
"@earendil-works/pi-ai": "^0.81.1",
|
|
37
39
|
"@earendil-works/pi-coding-agent": "^0.81.1",
|
|
38
40
|
"@earendil-works/pi-tui": "^0.81.1",
|