@danypops/pi-lector 0.13.9 → 0.13.11
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 +109 -3
- package/extension/src/code-intelligence/rendering.ts +119 -0
- package/extension/src/cross-workspace-search/rendering.ts +3 -0
- package/extension/src/editor/editor-state.ts +38 -1
- package/extension/src/editor/index.ts +9 -1
- package/extension/src/editor/modal-editor-component.ts +9 -3
- package/extension/src/git/operations.ts +23 -1
- package/extension/src/git/rendering.ts +42 -4
- package/extension/src/index.ts +397 -14
- package/extension/src/search/rendering.ts +4 -2
- package/extension/src/symbol-annotation/operations.ts +8 -4
- package/extension/src/vehicle-client.ts +4 -2
- package/package.json +4 -4
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { JobSnapshot, OperationInputs, OperationOutputs, PopulateSymbolGraphResult, SymbolEdgeKind, SymbolNode } from "@danypops/lector";
|
|
2
2
|
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath, workspaceForPathOrDirectory } from "../lector-client.ts";
|
|
3
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
4
|
+
|
|
5
|
+
const CODE_ACTION_PREVIEW_PERMISSIONS = ["workspace:read"];
|
|
6
|
+
const CODE_ACTION_APPLY_PERMISSIONS = ["workspace:write"];
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Thin wrappers over Lector's code-intelligence operations: goToDefinition,
|
|
@@ -34,9 +38,50 @@ export interface CodeIntelligenceOperations {
|
|
|
34
38
|
hover(path: string, line: number, character: number): Promise<OperationOutputs["workspace.hover"]>;
|
|
35
39
|
documentSymbols(path: string): Promise<OperationOutputs["workspace.documentSymbols"]>;
|
|
36
40
|
diagnostics(path: string): Promise<OperationOutputs["workspace.diagnostics"]>;
|
|
41
|
+
previewCodeActions(
|
|
42
|
+
path: string,
|
|
43
|
+
input: Omit<OperationInputs["workspace.previewCodeActions"], "workspaceId" | "path">,
|
|
44
|
+
call: LectorVehicleCall,
|
|
45
|
+
): Promise<OperationOutputs["workspace.previewCodeActions"]>;
|
|
46
|
+
applyCodeAction(
|
|
47
|
+
path: string,
|
|
48
|
+
previewId: OperationInputs["workspace.applyCodeAction"]["previewId"],
|
|
49
|
+
call: LectorVehicleCall,
|
|
50
|
+
): Promise<OperationOutputs["workspace.applyCodeAction"]>;
|
|
51
|
+
diagnosticDelta(
|
|
52
|
+
path: string,
|
|
53
|
+
source: OperationInputs["workspace.diagnosticDelta"]["source"],
|
|
54
|
+
bounds?: Omit<OperationInputs["workspace.diagnosticDelta"], "workspaceId" | "source">,
|
|
55
|
+
): Promise<OperationOutputs["workspace.diagnosticDelta"]>;
|
|
37
56
|
prepareCallHierarchy(path: string, line: number, character: number): Promise<OperationOutputs["workspace.prepareCallHierarchy"]>;
|
|
38
57
|
incomingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.incomingCalls"]>;
|
|
39
58
|
outgoingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.outgoingCalls"]>;
|
|
59
|
+
prepareTypeHierarchy(
|
|
60
|
+
path: string,
|
|
61
|
+
line: number,
|
|
62
|
+
character: number,
|
|
63
|
+
bounds?: Pick<OperationInputs["workspace.prepareTypeHierarchy"], "maxResults" | "maxBytes" | "deadlineMs">,
|
|
64
|
+
): Promise<OperationOutputs["workspace.prepareTypeHierarchy"]>;
|
|
65
|
+
supertypes(
|
|
66
|
+
path: string,
|
|
67
|
+
line: number,
|
|
68
|
+
character: number,
|
|
69
|
+
bounds?: Pick<OperationInputs["workspace.supertypes"], "maxResults" | "maxBytes" | "deadlineMs">,
|
|
70
|
+
): Promise<OperationOutputs["workspace.supertypes"]>;
|
|
71
|
+
subtypes(
|
|
72
|
+
path: string,
|
|
73
|
+
line: number,
|
|
74
|
+
character: number,
|
|
75
|
+
bounds?: Pick<OperationInputs["workspace.subtypes"], "maxResults" | "maxBytes" | "deadlineMs">,
|
|
76
|
+
): Promise<OperationOutputs["workspace.subtypes"]>;
|
|
77
|
+
impactAnalysis(
|
|
78
|
+
path: string,
|
|
79
|
+
source: OperationInputs["workspace.impactAnalysis"]["source"],
|
|
80
|
+
bounds: Pick<
|
|
81
|
+
OperationInputs["workspace.impactAnalysis"],
|
|
82
|
+
"maxDepth" | "maxNodes" | "maxEdges" | "maxBytes" | "deadlineMs" | "coverage" | "autoPopulate" | "maxFiles" | "maxSymbolsPerFile"
|
|
83
|
+
>,
|
|
84
|
+
): Promise<OperationOutputs["workspace.impactAnalysis"]>;
|
|
40
85
|
/**
|
|
41
86
|
* Not exposed as a standalone Pi tool -- every workspace auto-populates on first touch via
|
|
42
87
|
* monitorWorkspaceCache (workspace-cache/operations.ts). Kept here as an internal capability
|
|
@@ -46,7 +91,14 @@ export interface CodeIntelligenceOperations {
|
|
|
46
91
|
populateSymbolGraph(path: string, maxFiles: number, maxSymbolsPerFile: number, waitMs?: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
47
92
|
/** Not exposed as a standalone Pi tool -- see populateSymbolGraph. */
|
|
48
93
|
jobStatus(jobId: string): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
49
|
-
reachableFrom(
|
|
94
|
+
reachableFrom(
|
|
95
|
+
path: string,
|
|
96
|
+
line: number,
|
|
97
|
+
character: number,
|
|
98
|
+
maxDepth: number,
|
|
99
|
+
kind?: SymbolEdgeKind,
|
|
100
|
+
autoPopulation?: Pick<OperationInputs["workspace.reachableFrom"], "autoPopulate" | "maxFiles" | "maxSymbolsPerFile">,
|
|
101
|
+
): Promise<readonly SymbolNode[]>;
|
|
50
102
|
/** Never spawns a symbol index -- safe to call opportunistically (e.g. before deciding whether to enrich a result). */
|
|
51
103
|
hasWarmIndex(path: string): Promise<boolean>;
|
|
52
104
|
workspaceMap(path: string, maxNodes: number, maxEdges: number, maxEntries: number, maxBytes: number): Promise<OperationOutputs["workspace.map"]>;
|
|
@@ -120,6 +172,36 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
120
172
|
},
|
|
121
173
|
);
|
|
122
174
|
},
|
|
175
|
+
async previewCodeActions(path, input, call) {
|
|
176
|
+
return withWorkspace(
|
|
177
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
178
|
+
({ workspaceId }) =>
|
|
179
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.previewCodeActions"]>(
|
|
180
|
+
"workspace.previewCodeActions",
|
|
181
|
+
{ workspaceId, path, ...input },
|
|
182
|
+
CODE_ACTION_PREVIEW_PERMISSIONS,
|
|
183
|
+
call,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
async applyCodeAction(path, previewId, call) {
|
|
188
|
+
return withWorkspace(
|
|
189
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
190
|
+
({ workspaceId }) =>
|
|
191
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.applyCodeAction"]>(
|
|
192
|
+
"workspace.applyCodeAction",
|
|
193
|
+
{ workspaceId, previewId },
|
|
194
|
+
CODE_ACTION_APPLY_PERMISSIONS,
|
|
195
|
+
call,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
async diagnosticDelta(path, source, bounds) {
|
|
200
|
+
return withWorkspace(
|
|
201
|
+
() => workspaceForPathOrDirectory(path),
|
|
202
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.diagnosticDelta", { workspaceId, source, ...bounds }),
|
|
203
|
+
);
|
|
204
|
+
},
|
|
123
205
|
async prepareCallHierarchy(path, line, character) {
|
|
124
206
|
return withWorkspace(
|
|
125
207
|
() => workspaceForCodeIntelligencePath(path),
|
|
@@ -147,6 +229,30 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
147
229
|
},
|
|
148
230
|
);
|
|
149
231
|
},
|
|
232
|
+
async prepareTypeHierarchy(path, line, character, bounds) {
|
|
233
|
+
return withWorkspace(
|
|
234
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
235
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.prepareTypeHierarchy", { workspaceId, path, line, character, ...bounds }),
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
async supertypes(path, line, character, bounds) {
|
|
239
|
+
return withWorkspace(
|
|
240
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
241
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.supertypes", { workspaceId, path, line, character, ...bounds }),
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
async subtypes(path, line, character, bounds) {
|
|
245
|
+
return withWorkspace(
|
|
246
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
247
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.subtypes", { workspaceId, path, line, character, ...bounds }),
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
async impactAnalysis(path, source, bounds) {
|
|
251
|
+
return withWorkspace(
|
|
252
|
+
() => workspaceForPathOrDirectory(path),
|
|
253
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.impactAnalysis", { workspaceId, source, ...bounds }),
|
|
254
|
+
);
|
|
255
|
+
},
|
|
150
256
|
async populateSymbolGraph(path, maxFiles, maxSymbolsPerFile, waitMs = 500) {
|
|
151
257
|
return withWorkspace(
|
|
152
258
|
() => workspaceForPathOrDirectory(path),
|
|
@@ -167,12 +273,12 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
167
273
|
const { job } = await client.call("job.status", { jobId });
|
|
168
274
|
return job;
|
|
169
275
|
},
|
|
170
|
-
async reachableFrom(path, line, character, maxDepth, kind) {
|
|
276
|
+
async reachableFrom(path, line, character, maxDepth, kind, autoPopulation) {
|
|
171
277
|
return withWorkspace(
|
|
172
278
|
() => workspaceForCodeIntelligencePath(path),
|
|
173
279
|
async ({ workspaceId }) => {
|
|
174
280
|
const client = await lectorClient();
|
|
175
|
-
const { symbols } = await client.call("workspace.reachableFrom", { workspaceId, path, line, character, maxDepth, kind });
|
|
281
|
+
const { symbols } = await client.call("workspace.reachableFrom", { workspaceId, path, line, character, maxDepth, kind, ...autoPopulation });
|
|
176
282
|
return symbols;
|
|
177
283
|
},
|
|
178
284
|
);
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
Hover,
|
|
6
6
|
IncomingCall,
|
|
7
7
|
IntelligenceProvenance,
|
|
8
|
+
OperationOutputs,
|
|
8
9
|
OutgoingCall,
|
|
9
10
|
SymbolNode,
|
|
10
11
|
WorkspaceLocation,
|
|
@@ -27,6 +28,7 @@ const DEFAULT_VISIBLE_LOCATIONS = 8;
|
|
|
27
28
|
const DEFAULT_VISIBLE_SYMBOLS = 12;
|
|
28
29
|
const DEFAULT_VISIBLE_DIAGNOSTICS = 12;
|
|
29
30
|
const DEFAULT_VISIBLE_CALLS = 12;
|
|
31
|
+
const DEFAULT_VISIBLE_CHANGES = 12;
|
|
30
32
|
|
|
31
33
|
const DIAGNOSTIC_SEVERITY_COLOR: Record<Diagnostic["severity"], ThemeColor> = {
|
|
32
34
|
error: "error",
|
|
@@ -171,6 +173,123 @@ export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | und
|
|
|
171
173
|
return lines.join("\n");
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
function formatPathCall(toolName: string, args: { path?: unknown }, theme: LectorTheme, qualifier = ""): string {
|
|
177
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
178
|
+
return `${theme.fg("toolTitle", theme.bold(toolName))}${qualifier ? ` ${theme.fg("muted", qualifier)}` : ""} ${theme.fg("accent", path)}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatCodeActionPreviewCall(
|
|
182
|
+
args: { path?: unknown; startLine?: unknown; startCharacter?: unknown; endLine?: unknown; endCharacter?: unknown },
|
|
183
|
+
theme: LectorTheme,
|
|
184
|
+
): string {
|
|
185
|
+
const range = `${typeof args.startLine === "number" ? args.startLine : "?"}:${typeof args.startCharacter === "number" ? args.startCharacter : "?"}-${typeof args.endLine === "number" ? args.endLine : "?"}:${typeof args.endCharacter === "number" ? args.endCharacter : "?"}`;
|
|
186
|
+
return formatPathCall("code_action_preview", args, theme, range);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function formatCodeActionPreviewResult(
|
|
190
|
+
result: OperationOutputs["workspace.previewCodeActions"] | undefined,
|
|
191
|
+
expanded: boolean,
|
|
192
|
+
theme: LectorTheme,
|
|
193
|
+
): string {
|
|
194
|
+
if (!result || result.actions.length === 0) return theme.fg("dim", "No code actions.");
|
|
195
|
+
return [
|
|
196
|
+
theme.fg("muted", `${result.actions.length} code action${result.actions.length === 1 ? "" : "s"}:`),
|
|
197
|
+
...renderTruncatedList({
|
|
198
|
+
items: result.actions,
|
|
199
|
+
expanded,
|
|
200
|
+
visibleCount: DEFAULT_VISIBLE_CHANGES,
|
|
201
|
+
formatItem: (action) => {
|
|
202
|
+
const state = action.disabledReason
|
|
203
|
+
? theme.fg("warning", `disabled: ${action.disabledReason}`)
|
|
204
|
+
: action.preferred
|
|
205
|
+
? theme.fg("success", "preferred")
|
|
206
|
+
: "";
|
|
207
|
+
const paths = action.affectedPaths.length > 0 ? action.affectedPaths.join(", ") : "command only";
|
|
208
|
+
return ` ${theme.bold(action.title)}${action.kind ? ` (${action.kind})` : ""} -- ${paths}${state ? ` -- ${state}` : ""}`;
|
|
209
|
+
},
|
|
210
|
+
moreLine: moreLine(theme),
|
|
211
|
+
truncationWarning: result.truncated || result.deadlineReached ? theme.fg("warning", "results truncated by the requested bounds") : undefined,
|
|
212
|
+
}),
|
|
213
|
+
].join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function formatCodeActionApplyCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
217
|
+
return formatPathCall("code_action_apply", args, theme);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function formatCodeActionApplyResult(result: OperationOutputs["workspace.applyCodeAction"] | undefined, theme: LectorTheme): string {
|
|
221
|
+
if (!result) return theme.fg("dim", "No code-action result.");
|
|
222
|
+
const lines = result.touchedPaths.map((path) => ` ${path}`);
|
|
223
|
+
if (result.transactionId) lines.push(theme.fg("muted", `transaction ${result.transactionId}`));
|
|
224
|
+
if (result.pendingCommand) lines.push(theme.fg("warning", `pending command ${result.pendingCommand.command}`));
|
|
225
|
+
return lines.length > 0 ? lines.join("\n") : theme.fg("dim", "Code action made no file changes.");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function formatDiagnosticDeltaCall(args: { path?: unknown; source?: unknown }, theme: LectorTheme): string {
|
|
229
|
+
return formatPathCall("diagnostic_delta", args, theme, typeof args.source === "string" ? args.source : "");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function formatDiagnosticDeltaResult(result: OperationOutputs["workspace.diagnosticDelta"] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
233
|
+
if (!result) return theme.fg("dim", "No diagnostic delta.");
|
|
234
|
+
const changes = [
|
|
235
|
+
...result.introduced.map((diagnostic) => ({ label: "introduced", diagnostic })),
|
|
236
|
+
...result.resolved.map((diagnostic) => ({ label: "resolved", diagnostic })),
|
|
237
|
+
...result.changed.map(({ after }) => ({ label: "changed", diagnostic: after })),
|
|
238
|
+
];
|
|
239
|
+
if (changes.length === 0) return theme.fg("success", "No diagnostic changes.");
|
|
240
|
+
return renderTruncatedList({
|
|
241
|
+
items: changes,
|
|
242
|
+
expanded,
|
|
243
|
+
visibleCount: DEFAULT_VISIBLE_CHANGES,
|
|
244
|
+
formatItem: ({ label, diagnostic }) =>
|
|
245
|
+
`${theme.fg(label === "introduced" ? "error" : label === "resolved" ? "success" : "warning", label)} ${formatLocation(theme, diagnostic.range.path, diagnostic.range.start.line, diagnostic.range.start.character)} -- ${diagnostic.message}`,
|
|
246
|
+
moreLine: moreLine(theme),
|
|
247
|
+
truncationWarning: result.truncated ? theme.fg("warning", "results truncated by maxResults/maxBytes") : undefined,
|
|
248
|
+
}).join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function formatTypeHierarchyCall(args: { direction?: unknown; path?: unknown; line?: unknown; character?: unknown }, theme: LectorTheme): string {
|
|
252
|
+
return `${formatPositionalCall("type_hierarchy", args, theme)} ${theme.fg("muted", typeof args.direction === "string" ? args.direction : "")}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function formatTypeHierarchyResult(
|
|
256
|
+
result: OperationOutputs["workspace.prepareTypeHierarchy"] | undefined,
|
|
257
|
+
expanded: boolean,
|
|
258
|
+
theme: LectorTheme,
|
|
259
|
+
): string {
|
|
260
|
+
if (!result || result.items.length === 0) return theme.fg("dim", "No type-hierarchy items found.");
|
|
261
|
+
return renderTruncatedList({
|
|
262
|
+
items: result.items,
|
|
263
|
+
expanded,
|
|
264
|
+
visibleCount: DEFAULT_VISIBLE_CALLS,
|
|
265
|
+
formatItem: (item) => ` ${formatCallHierarchyEntry(item, theme)}${item.detail ? theme.fg("dim", ` -- ${item.detail}`) : ""}`,
|
|
266
|
+
moreLine: moreLine(theme),
|
|
267
|
+
truncationWarning: result.truncated ? theme.fg("warning", "results truncated by the requested bounds") : undefined,
|
|
268
|
+
}).join("\n");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatImpactAnalysisCall(args: { path?: unknown; source?: unknown }, theme: LectorTheme): string {
|
|
272
|
+
return formatPathCall("impact_analysis", args, theme, typeof args.source === "string" ? args.source : "");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function formatImpactAnalysisResult(result: OperationOutputs["workspace.impactAnalysis"] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
276
|
+
if (!result) return theme.fg("dim", "No impact-analysis result.");
|
|
277
|
+
const impacts = [
|
|
278
|
+
...result.changedSymbols.map(({ symbol, side }) => ({ label: `changed ${side}`, symbol })),
|
|
279
|
+
...result.impactedSymbols.map(({ symbol, depth }) => ({ label: `impact depth=${depth}`, symbol })),
|
|
280
|
+
...result.relatedTests.map(({ symbol, evidence }) => ({ label: `test ${evidence.kind}`, symbol })),
|
|
281
|
+
];
|
|
282
|
+
if (impacts.length === 0) return theme.fg("dim", "No changed symbols resolved.");
|
|
283
|
+
return renderTruncatedList({
|
|
284
|
+
items: impacts,
|
|
285
|
+
expanded,
|
|
286
|
+
visibleCount: DEFAULT_VISIBLE_CHANGES,
|
|
287
|
+
formatItem: ({ label, symbol }) => `${theme.fg("muted", label)} ${formatCallHierarchyEntry(symbol, theme)}`,
|
|
288
|
+
moreLine: moreLine(theme),
|
|
289
|
+
truncationWarning: result.truncated || result.deadlineReached ? theme.fg("warning", "analysis truncated by the requested bounds") : undefined,
|
|
290
|
+
}).join("\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
174
293
|
function formatCallHierarchyEntry(entry: { kind: string; name: string; location: WorkspaceLocation }, theme: LectorTheme): string {
|
|
175
294
|
const kind = theme.fg(colorForKind(entry.kind), entry.kind);
|
|
176
295
|
const name = theme.fg("text", theme.bold(entry.name));
|
|
@@ -83,6 +83,9 @@ export function formatSearchTextAcrossProjectsResult(
|
|
|
83
83
|
lines.push(formatOutcomeHeader(entry, theme));
|
|
84
84
|
const { outcome } = entry;
|
|
85
85
|
if (outcome.status !== "ready") continue;
|
|
86
|
+
if (outcome.result.provenance) {
|
|
87
|
+
lines.push(theme.fg("dim", ` lexical via ${outcome.result.provenance.backend} (${outcome.result.provenance.indexState})`));
|
|
88
|
+
}
|
|
86
89
|
if (outcome.result.matches.length === 0) {
|
|
87
90
|
lines.push(theme.fg("dim", " no matches"));
|
|
88
91
|
continue;
|
|
@@ -4,6 +4,26 @@ export type EditorMode = "normal" | "insert" | "command";
|
|
|
4
4
|
|
|
5
5
|
export type EditorAction = { kind: "save" } | { kind: "save-and-quit" } | { kind: "quit" } | { kind: "hover" };
|
|
6
6
|
|
|
7
|
+
export interface EditorSourcePosition {
|
|
8
|
+
/** One-based line number. */
|
|
9
|
+
readonly line: number;
|
|
10
|
+
/** One-based UTF-16 code-unit position within the line. */
|
|
11
|
+
readonly character: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type EditorPositionErrorCode = "line-out-of-range" | "character-out-of-range";
|
|
15
|
+
|
|
16
|
+
/** Reports why a requested source position cannot identify a cursor location in the current buffer. */
|
|
17
|
+
export class EditorPositionError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly code: EditorPositionErrorCode,
|
|
20
|
+
message: string,
|
|
21
|
+
) {
|
|
22
|
+
super(`${code}: ${message}`);
|
|
23
|
+
this.name = "EditorPositionError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
const BACKSPACE_KEYS = new Set(["\x7f", "\b"]);
|
|
8
28
|
|
|
9
29
|
/**
|
|
@@ -27,8 +47,25 @@ export class EditorState {
|
|
|
27
47
|
private insertPendingJ = false;
|
|
28
48
|
private yankedLine: string | undefined;
|
|
29
49
|
|
|
30
|
-
constructor(content: string) {
|
|
50
|
+
constructor(content: string, initialPosition?: EditorSourcePosition) {
|
|
31
51
|
this.buffer = new LiveBuffer(content);
|
|
52
|
+
if (initialPosition) this.moveToInitialPosition(initialPosition);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private moveToInitialPosition(position: EditorSourcePosition): void {
|
|
56
|
+
if (!Number.isSafeInteger(position.line) || position.line < 1 || position.line > this.buffer.lineCount) {
|
|
57
|
+
throw new EditorPositionError("line-out-of-range", `Line ${position.line} is outside this ${this.buffer.lineCount}-line buffer`);
|
|
58
|
+
}
|
|
59
|
+
const lineLength = this.buffer.lineText(position.line).length;
|
|
60
|
+
const maxCharacter = Math.max(lineLength, 1);
|
|
61
|
+
if (!Number.isSafeInteger(position.character) || position.character < 1 || position.character > maxCharacter) {
|
|
62
|
+
throw new EditorPositionError(
|
|
63
|
+
"character-out-of-range",
|
|
64
|
+
`Character ${position.character} is outside line ${position.line}'s ${lineLength} UTF-16 code units`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
this.cursorLine = position.line;
|
|
68
|
+
this.cursorCharacter = position.character;
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
get currentLineText(): string {
|
|
@@ -29,7 +29,14 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
export type { DirectoryExplorerSession } from "./directory-explorer-operations.ts";
|
|
32
|
-
export {
|
|
32
|
+
export {
|
|
33
|
+
type EditorAction,
|
|
34
|
+
type EditorMode,
|
|
35
|
+
EditorPositionError,
|
|
36
|
+
type EditorPositionErrorCode,
|
|
37
|
+
type EditorSourcePosition,
|
|
38
|
+
EditorState,
|
|
39
|
+
} from "./editor-state.ts";
|
|
33
40
|
export type { EditorTheme } from "./editor-theme.ts";
|
|
34
41
|
export { ExplorerComponent, type ExplorerResult, type ExplorerViewState, joinExplorerPath } from "./explorer-component.ts";
|
|
35
42
|
// runExplorerFlow is pure orchestration over the two interfaces above (browse, open a file into
|
|
@@ -44,4 +51,5 @@ export {
|
|
|
44
51
|
type EditorHoverRequest,
|
|
45
52
|
ModalEditorComponent,
|
|
46
53
|
type ModalEditorHost,
|
|
54
|
+
type ModalEditorOptions,
|
|
47
55
|
} from "./modal-editor-component.ts";
|
|
@@ -4,7 +4,7 @@ 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";
|
|
7
|
-
import type { EditorAction } from "./editor-state.ts";
|
|
7
|
+
import type { EditorAction, EditorSourcePosition } from "./editor-state.ts";
|
|
8
8
|
import { EditorState } from "./editor-state.ts";
|
|
9
9
|
import type { EditorTheme } from "./editor-theme.ts";
|
|
10
10
|
|
|
@@ -26,6 +26,11 @@ export type EditorHoverOutcome =
|
|
|
26
26
|
| { readonly kind: "ready"; readonly hover?: { readonly contents: string } }
|
|
27
27
|
| { readonly kind: "stale-active-buffer"; readonly bufferHash: ContentHash };
|
|
28
28
|
|
|
29
|
+
export interface ModalEditorOptions {
|
|
30
|
+
/** Positions the cursor using one-based UTF-16 coordinates and scrolls it into the first viewport. */
|
|
31
|
+
readonly initialPosition?: EditorSourcePosition;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export interface ModalEditorHost {
|
|
30
35
|
filePath: string;
|
|
31
36
|
/** 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. */
|
|
@@ -65,13 +70,14 @@ export class ModalEditorComponent implements Component {
|
|
|
65
70
|
private statusMessage = "";
|
|
66
71
|
private highlightCache: { text: string; spans: readonly HighlightSpan[] } | undefined;
|
|
67
72
|
|
|
68
|
-
constructor(tui: TUI, theme: EditorTheme, host: ModalEditorHost, content: string, done: () => void) {
|
|
73
|
+
constructor(tui: TUI, theme: EditorTheme, host: ModalEditorHost, content: string, done: () => void, options: ModalEditorOptions = {}) {
|
|
69
74
|
this.tui = tui;
|
|
70
75
|
this.theme = theme;
|
|
71
76
|
this.host = host;
|
|
72
77
|
this.done = done;
|
|
73
78
|
this.extension = extname(host.filePath);
|
|
74
|
-
this.state = new EditorState(content);
|
|
79
|
+
this.state = new EditorState(content, options.initialPosition);
|
|
80
|
+
this.scrollToKeepCursorVisible();
|
|
75
81
|
this.refreshHighlightsSafely();
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } from "@danypops/lector";
|
|
1
|
+
import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationInputs, OperationOutputs } from "@danypops/lector";
|
|
2
2
|
import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
|
|
3
3
|
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
4
4
|
|
|
@@ -6,6 +6,8 @@ type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"
|
|
|
6
6
|
type GitWorktreeAddResult = OperationOutputs["workspace.gitWorktreeAdd"];
|
|
7
7
|
type GitWorktreeRemoveResult = OperationOutputs["workspace.gitWorktreeRemove"];
|
|
8
8
|
type GitGrepResult = OperationOutputs["workspace.gitGrep"];
|
|
9
|
+
type GitHistoryGrepResult = OperationOutputs["workspace.gitGrepHistory"];
|
|
10
|
+
type GitHistoryGrepBounds = Omit<OperationInputs["workspace.gitGrepHistory"], "workspaceId" | "pattern" | "pathspecs">;
|
|
9
11
|
type GitListFilesResult = OperationOutputs["workspace.gitListFiles"];
|
|
10
12
|
|
|
11
13
|
/** Matches GIT_READ_PERMISSIONS' own declared value server-side (git/operation-registration.ts). */
|
|
@@ -42,6 +44,14 @@ export interface GitOperations {
|
|
|
42
44
|
maxBytes: number,
|
|
43
45
|
call: LectorVehicleCall,
|
|
44
46
|
): Promise<GitGrepResult>;
|
|
47
|
+
/** Bounded extended-regex search across commit trees reachable from every ref, with deterministic topological paging and exact-match deduplication. */
|
|
48
|
+
grepHistory(
|
|
49
|
+
directory: string,
|
|
50
|
+
pattern: string,
|
|
51
|
+
pathspecs: readonly string[] | undefined,
|
|
52
|
+
bounds: GitHistoryGrepBounds,
|
|
53
|
+
call: LectorVehicleCall,
|
|
54
|
+
): Promise<GitHistoryGrepResult>;
|
|
45
55
|
/** Every file path in `ref`'s own tree, no checkout -- pathspecs narrows the listing (prefix-based, not glob-based like grep's). */
|
|
46
56
|
listFiles(directory: string, ref: string, pathspecs: readonly string[] | undefined, maxResults: number, call: LectorVehicleCall): Promise<GitListFilesResult>;
|
|
47
57
|
/** True iff ancestorRef is a real ancestor of (or the exact same commit as) ref -- the backport/reachability check "was this fix ported to this branch" actually needs. */
|
|
@@ -139,6 +149,18 @@ export function createLectorGitOperations(): GitOperations {
|
|
|
139
149
|
),
|
|
140
150
|
);
|
|
141
151
|
},
|
|
152
|
+
async grepHistory(directory, pattern, pathspecs, bounds, call) {
|
|
153
|
+
return withWorkspace(
|
|
154
|
+
() => workspaceForDirectory(directory),
|
|
155
|
+
({ workspaceId }) =>
|
|
156
|
+
invokeLectorVehicleOperation<GitHistoryGrepResult>(
|
|
157
|
+
"workspace.gitGrepHistory",
|
|
158
|
+
{ workspaceId, pattern, pathspecs, ...bounds },
|
|
159
|
+
GIT_READ_PERMISSIONS,
|
|
160
|
+
call,
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
},
|
|
142
164
|
async listFiles(directory, ref, pathspecs, maxResults, call) {
|
|
143
165
|
return withWorkspace(
|
|
144
166
|
() => workspaceForDirectory(directory),
|
|
@@ -11,12 +11,24 @@ const DEFAULT_VISIBLE_FILES = 20;
|
|
|
11
11
|
const DEFAULT_VISIBLE_COMMITS = 10;
|
|
12
12
|
const DEFAULT_VISIBLE_DIFF_LINES = 60;
|
|
13
13
|
|
|
14
|
-
export type GitAction =
|
|
14
|
+
export type GitAction =
|
|
15
|
+
| "status"
|
|
16
|
+
| "log"
|
|
17
|
+
| "diff"
|
|
18
|
+
| "compare-symbol"
|
|
19
|
+
| "worktree-add"
|
|
20
|
+
| "worktree-remove"
|
|
21
|
+
| "show"
|
|
22
|
+
| "grep-ref"
|
|
23
|
+
| "grep-history"
|
|
24
|
+
| "ls-ref"
|
|
25
|
+
| "is-ancestor";
|
|
15
26
|
|
|
16
27
|
type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"];
|
|
17
28
|
type GitWorktreeAddResult = OperationOutputs["workspace.gitWorktreeAdd"];
|
|
18
29
|
type GitWorktreeRemoveResult = OperationOutputs["workspace.gitWorktreeRemove"];
|
|
19
30
|
type GitGrepResult = OperationOutputs["workspace.gitGrep"];
|
|
31
|
+
type GitHistoryGrepResult = OperationOutputs["workspace.gitGrepHistory"];
|
|
20
32
|
type GitListFilesResult = OperationOutputs["workspace.gitListFiles"];
|
|
21
33
|
|
|
22
34
|
export interface GitToolDetails {
|
|
@@ -29,6 +41,7 @@ export interface GitToolDetails {
|
|
|
29
41
|
readonly worktreeRemove?: GitWorktreeRemoveResult;
|
|
30
42
|
readonly showFile?: { readonly ref: string; readonly path: string; readonly content: string | undefined };
|
|
31
43
|
readonly grep?: GitGrepResult;
|
|
44
|
+
readonly historyGrep?: GitHistoryGrepResult;
|
|
32
45
|
readonly listFiles?: GitListFilesResult;
|
|
33
46
|
readonly isAncestor?: { readonly ancestorRef: string; readonly ref: string; readonly result: boolean };
|
|
34
47
|
}
|
|
@@ -61,10 +74,10 @@ export function formatGitCall(
|
|
|
61
74
|
const ref = typeof args.ref === "string" ? args.ref : "";
|
|
62
75
|
return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)} ${ancestorRef} -> ${ref}`;
|
|
63
76
|
}
|
|
64
|
-
if (action === "grep-ref") {
|
|
65
|
-
const ref = typeof args.ref === "string" ? args.ref : "";
|
|
77
|
+
if (action === "grep-ref" || action === "grep-history") {
|
|
78
|
+
const ref = action === "grep-ref" && typeof args.ref === "string" ? `${args.ref} ` : "";
|
|
66
79
|
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
67
|
-
return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)} ${ref}
|
|
80
|
+
return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)} ${ref}"${pattern}"`;
|
|
68
81
|
}
|
|
69
82
|
if (action === "show") {
|
|
70
83
|
const ref = typeof args.ref === "string" ? args.ref : "";
|
|
@@ -105,6 +118,30 @@ function formatGitGrepResult(result: GitGrepResult | undefined, expanded: boolea
|
|
|
105
118
|
return lines.join("\n");
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
function formatGitHistoryGrepResult(result: GitHistoryGrepResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
122
|
+
if (!result || result.matches.length === 0) {
|
|
123
|
+
if (result?.deadlineReached) return theme.fg("warning", "No matches before the deadline.");
|
|
124
|
+
return theme.fg("dim", "No historical matches.");
|
|
125
|
+
}
|
|
126
|
+
const lines = renderTruncatedList({
|
|
127
|
+
items: result.matches,
|
|
128
|
+
expanded,
|
|
129
|
+
visibleCount: DEFAULT_VISIBLE_FILES,
|
|
130
|
+
formatItem: (match) => {
|
|
131
|
+
const occurrences = match.occurrences > 1 ? ` (${match.occurrences} commits)` : "";
|
|
132
|
+
return `${theme.fg("muted", match.commit.slice(0, 8))} ${theme.fg("accent", `${match.path}:${match.line}`)}:${match.text}${occurrences}`;
|
|
133
|
+
},
|
|
134
|
+
moreLine: moreLine(theme),
|
|
135
|
+
truncationWarning: result.deadlineReached
|
|
136
|
+
? theme.fg("warning", "(deadline reached)")
|
|
137
|
+
: result.truncated
|
|
138
|
+
? theme.fg("warning", "(bounded by maxMatches/maxBytes)")
|
|
139
|
+
: undefined,
|
|
140
|
+
});
|
|
141
|
+
if (result.nextCommitOffset !== undefined) lines.push(theme.fg("muted", `next commit offset: ${result.nextCommitOffset}`));
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
108
145
|
function formatGitListFilesResult(result: GitListFilesResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
109
146
|
if (!result || result.paths.length === 0) return theme.fg("dim", "No files.");
|
|
110
147
|
const lines = renderTruncatedList({
|
|
@@ -133,6 +170,7 @@ export function formatGitResult(details: GitToolDetails | undefined, expanded: b
|
|
|
133
170
|
if (details.action === "worktree-remove") return formatGitWorktreeRemoveResult(details.worktreeRemove, theme);
|
|
134
171
|
if (details.action === "show") return formatGitShowFileResult(details.showFile, theme);
|
|
135
172
|
if (details.action === "grep-ref") return formatGitGrepResult(details.grep, expanded, theme);
|
|
173
|
+
if (details.action === "grep-history") return formatGitHistoryGrepResult(details.historyGrep, expanded, theme);
|
|
136
174
|
if (details.action === "ls-ref") return formatGitListFilesResult(details.listFiles, expanded, theme);
|
|
137
175
|
if (details.action === "is-ancestor") return formatGitIsAncestorResult(details.isAncestor, theme);
|
|
138
176
|
return formatGitDiffResult(details.result, expanded, theme);
|
package/extension/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ import type {
|
|
|
31
31
|
WorkspaceLocation,
|
|
32
32
|
WorkspaceMapResult,
|
|
33
33
|
} from "@danypops/lector";
|
|
34
|
-
import { DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS, PACKAGE_ECOSYSTEMS } from "@danypops/lector";
|
|
34
|
+
import { codeActionPreviewId, DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS, PACKAGE_ECOSYSTEMS } from "@danypops/lector";
|
|
35
35
|
import {
|
|
36
36
|
type AgentToolResult,
|
|
37
37
|
createEditToolDefinition,
|
|
@@ -56,6 +56,12 @@ import {
|
|
|
56
56
|
type CallHierarchyToolDetails,
|
|
57
57
|
formatCallHierarchyCall,
|
|
58
58
|
formatCallHierarchyResult,
|
|
59
|
+
formatCodeActionApplyCall,
|
|
60
|
+
formatCodeActionApplyResult,
|
|
61
|
+
formatCodeActionPreviewCall,
|
|
62
|
+
formatCodeActionPreviewResult,
|
|
63
|
+
formatDiagnosticDeltaCall,
|
|
64
|
+
formatDiagnosticDeltaResult,
|
|
59
65
|
formatDiagnosticsCall,
|
|
60
66
|
formatDiagnosticsResult,
|
|
61
67
|
formatDocumentSymbolsCall,
|
|
@@ -68,8 +74,12 @@ import {
|
|
|
68
74
|
formatGoToImplementationResult,
|
|
69
75
|
formatHoverCall,
|
|
70
76
|
formatHoverResult,
|
|
77
|
+
formatImpactAnalysisCall,
|
|
78
|
+
formatImpactAnalysisResult,
|
|
71
79
|
formatReachableFromCall,
|
|
72
80
|
formatReachableFromResult,
|
|
81
|
+
formatTypeHierarchyCall,
|
|
82
|
+
formatTypeHierarchyResult,
|
|
73
83
|
formatWorkspaceMapCall,
|
|
74
84
|
formatWorkspaceMapResult,
|
|
75
85
|
} from "./code-intelligence/rendering.ts";
|
|
@@ -774,6 +784,186 @@ export default function (pi: ExtensionAPI) {
|
|
|
774
784
|
},
|
|
775
785
|
});
|
|
776
786
|
|
|
787
|
+
registerLectorTool({
|
|
788
|
+
name: "code_action_preview",
|
|
789
|
+
label: "Code Action Preview",
|
|
790
|
+
description:
|
|
791
|
+
"Request bounded language-server code actions for one range. Returns opaque preview ids, affected files, exact edits, provenance, and truncation status. This operation is read-only; use code_action_apply separately to mutate files.",
|
|
792
|
+
promptSnippet: "Preview bounded language-server fixes for one range",
|
|
793
|
+
promptGuidelines: ["Preview first, inspect every affected path/edit, then pass exactly one returned previewId to code_action_apply."],
|
|
794
|
+
parameters: Type.Object({
|
|
795
|
+
path: Type.String({ description: "Absolute or cwd-relative file path" }),
|
|
796
|
+
startLine: Type.Number({ description: "1-indexed range start line" }),
|
|
797
|
+
startCharacter: Type.Number({ description: "1-indexed range start character" }),
|
|
798
|
+
endLine: Type.Number({ description: "1-indexed range end line" }),
|
|
799
|
+
endCharacter: Type.Number({ description: "1-indexed range end character" }),
|
|
800
|
+
only: Type.Optional(Type.Array(Type.String(), { maxItems: 20, description: "Code-action kinds such as quickfix" })),
|
|
801
|
+
includeCommandActions: Type.Optional(Type.Boolean({ description: "Include command-only actions in preview; guarded apply still denies them" })),
|
|
802
|
+
maxActions: Type.Number({ description: "Maximum actions" }),
|
|
803
|
+
maxEdits: Type.Number({ description: "Maximum edits per action" }),
|
|
804
|
+
maxFiles: Type.Number({ description: "Maximum affected files per action" }),
|
|
805
|
+
maxBytes: Type.Number({ description: "Maximum response JSON bytes" }),
|
|
806
|
+
deadlineMs: Type.Number({ description: "Wall-clock deadline in milliseconds" }),
|
|
807
|
+
}),
|
|
808
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<OperationOutputs["workspace.previewCodeActions"]>> {
|
|
809
|
+
const path = resolve(cwd, params.path);
|
|
810
|
+
const vehicleCall: LectorVehicleCall = { toolName: "code_action_preview", toolCallId, signal, context: ctx };
|
|
811
|
+
const result = await codeIntelligenceOperations.previewCodeActions(
|
|
812
|
+
path,
|
|
813
|
+
{
|
|
814
|
+
range: {
|
|
815
|
+
start: { line: params.startLine, character: params.startCharacter },
|
|
816
|
+
end: { line: params.endLine, character: params.endCharacter },
|
|
817
|
+
},
|
|
818
|
+
...(params.only ? { only: params.only } : {}),
|
|
819
|
+
...(params.includeCommandActions !== undefined ? { includeCommandActions: params.includeCommandActions } : {}),
|
|
820
|
+
maxActions: params.maxActions,
|
|
821
|
+
maxEdits: params.maxEdits,
|
|
822
|
+
maxFiles: params.maxFiles,
|
|
823
|
+
maxBytes: params.maxBytes,
|
|
824
|
+
deadlineMs: params.deadlineMs,
|
|
825
|
+
},
|
|
826
|
+
vehicleCall,
|
|
827
|
+
);
|
|
828
|
+
const text = result.actions
|
|
829
|
+
.map((action) => `${action.id} ${action.kind ?? "action"} ${action.title} -- ${action.affectedPaths.join(", ") || "command only"}`)
|
|
830
|
+
.join("\n");
|
|
831
|
+
return { content: [{ type: "text", text: text || "No code actions." }], details: result };
|
|
832
|
+
},
|
|
833
|
+
renderCall(args, theme, context) {
|
|
834
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
835
|
+
text.setText(formatCodeActionPreviewCall(args, theme));
|
|
836
|
+
return text;
|
|
837
|
+
},
|
|
838
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
839
|
+
if (isPartial) return new Text(theme.fg("warning", "Finding code actions..."), 0, 0);
|
|
840
|
+
if (context.isError) {
|
|
841
|
+
const errorText = result.content
|
|
842
|
+
.filter((block) => block.type === "text")
|
|
843
|
+
.map((block) => block.text)
|
|
844
|
+
.join("\n");
|
|
845
|
+
return new Text(theme.fg("error", errorText || "code_action_preview failed"), 0, 0);
|
|
846
|
+
}
|
|
847
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
848
|
+
text.setText(formatCodeActionPreviewResult(result.details, expanded, theme));
|
|
849
|
+
return text;
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
registerLectorTool({
|
|
854
|
+
name: "code_action_apply",
|
|
855
|
+
label: "Apply Code Action",
|
|
856
|
+
description:
|
|
857
|
+
"Apply one previously previewed language-server WorkspaceEdit atomically with workspace containment, document-version and content-hash guards. Returns a transaction id for exact transaction-aware revert. Command-only actions are denied.",
|
|
858
|
+
promptSnippet: "Atomically apply one previewed language-server edit",
|
|
859
|
+
promptGuidelines: ["Use only after inspecting code_action_preview; retain transactionId for diagnostic_delta and revert."],
|
|
860
|
+
parameters: Type.Object({
|
|
861
|
+
path: Type.String({ description: "The same file path used to obtain the preview" }),
|
|
862
|
+
previewId: Type.String({ description: "Opaque id returned by code_action_preview" }),
|
|
863
|
+
}),
|
|
864
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<OperationOutputs["workspace.applyCodeAction"]>> {
|
|
865
|
+
const vehicleCall: LectorVehicleCall = { toolName: "code_action_apply", toolCallId, signal, context: ctx };
|
|
866
|
+
const result = await codeIntelligenceOperations.applyCodeAction(resolve(cwd, params.path), codeActionPreviewId(params.previewId), vehicleCall);
|
|
867
|
+
const text = [
|
|
868
|
+
...result.touchedPaths,
|
|
869
|
+
...(result.transactionId ? [`transaction ${result.transactionId}`] : []),
|
|
870
|
+
...(result.pendingCommand ? [`pending command ${result.pendingCommand.command}`] : []),
|
|
871
|
+
].join("\n");
|
|
872
|
+
return { content: [{ type: "text", text: text || "Code action made no file changes." }], details: result };
|
|
873
|
+
},
|
|
874
|
+
renderCall(args, theme, context) {
|
|
875
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
876
|
+
text.setText(formatCodeActionApplyCall(args, theme));
|
|
877
|
+
return text;
|
|
878
|
+
},
|
|
879
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
880
|
+
if (isPartial) return new Text(theme.fg("warning", "Applying code action..."), 0, 0);
|
|
881
|
+
if (context.isError) {
|
|
882
|
+
const errorText = result.content
|
|
883
|
+
.filter((block) => block.type === "text")
|
|
884
|
+
.map((block) => block.text)
|
|
885
|
+
.join("\n");
|
|
886
|
+
return new Text(theme.fg("error", errorText || "code_action_apply failed"), 0, 0);
|
|
887
|
+
}
|
|
888
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
889
|
+
text.setText(formatCodeActionApplyResult(result.details, theme));
|
|
890
|
+
return text;
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
registerLectorTool({
|
|
895
|
+
name: "diagnostic_delta",
|
|
896
|
+
label: "Diagnostic Delta",
|
|
897
|
+
description:
|
|
898
|
+
"Return only diagnostics introduced, resolved, or changed by one atomic mutation transaction or git-ref diff across its bounded affected-file cone, with completeness and provenance. Transaction results include the exact safe revert operation.",
|
|
899
|
+
promptSnippet: "Inspect diagnostic changes caused by a transaction or git diff",
|
|
900
|
+
promptGuidelines: [
|
|
901
|
+
"Use source=transaction with the id returned by an atomic mutation, or source=git with a baseline ref; unchanged diagnostics are omitted.",
|
|
902
|
+
],
|
|
903
|
+
parameters: Type.Object({
|
|
904
|
+
path: Type.String({ description: "Project directory or a path inside it" }),
|
|
905
|
+
source: Type.String({ description: "transaction | git" }),
|
|
906
|
+
sourceId: Type.String({ description: "Mutation transaction id or git baseline ref" }),
|
|
907
|
+
maxResults: Type.Optional(Type.Number({ description: "Maximum results per delta class" })),
|
|
908
|
+
maxBytes: Type.Optional(Type.Number({ description: "Maximum response JSON bytes" })),
|
|
909
|
+
maxDepth: Type.Optional(Type.Number({ description: "Git source: maximum impact depth" })),
|
|
910
|
+
maxNodes: Type.Optional(Type.Number({ description: "Git source: maximum graph nodes" })),
|
|
911
|
+
maxEdges: Type.Optional(Type.Number({ description: "Git source: maximum graph edges" })),
|
|
912
|
+
deadlineMs: Type.Optional(Type.Number({ description: "Git source: wall-clock deadline" })),
|
|
913
|
+
maxFiles: Type.Optional(Type.Number({ description: "Git source: maximum graph files" })),
|
|
914
|
+
maxSymbolsPerFile: Type.Optional(Type.Number({ description: "Git source: maximum declarations per file" })),
|
|
915
|
+
autoPopulate: Type.Optional(Type.Boolean({ description: "Git source: populate a missing/stale graph" })),
|
|
916
|
+
}),
|
|
917
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<OperationOutputs["workspace.diagnosticDelta"]>> {
|
|
918
|
+
const source =
|
|
919
|
+
params.source === "transaction"
|
|
920
|
+
? ({ kind: "transaction", transactionId: params.sourceId } as const)
|
|
921
|
+
: params.source === "git"
|
|
922
|
+
? ({ kind: "git", ref: params.sourceId } as const)
|
|
923
|
+
: undefined;
|
|
924
|
+
if (!source) throw new TypeError("diagnostic_delta source must be transaction or git");
|
|
925
|
+
const result = await codeIntelligenceOperations.diagnosticDelta(resolve(cwd, params.path), source, {
|
|
926
|
+
maxResults: params.maxResults,
|
|
927
|
+
maxBytes: params.maxBytes,
|
|
928
|
+
maxDepth: params.maxDepth,
|
|
929
|
+
maxNodes: params.maxNodes,
|
|
930
|
+
maxEdges: params.maxEdges,
|
|
931
|
+
deadlineMs: params.deadlineMs,
|
|
932
|
+
maxFiles: params.maxFiles,
|
|
933
|
+
maxSymbolsPerFile: params.maxSymbolsPerFile,
|
|
934
|
+
autoPopulate: params.autoPopulate,
|
|
935
|
+
});
|
|
936
|
+
const text = [
|
|
937
|
+
...result.introduced.map(
|
|
938
|
+
(diagnostic) => `introduced ${diagnostic.severity} ${diagnostic.range.path}:${diagnostic.range.start.line} -- ${diagnostic.message}`,
|
|
939
|
+
),
|
|
940
|
+
...result.resolved.map(
|
|
941
|
+
(diagnostic) => `resolved ${diagnostic.severity} ${diagnostic.range.path}:${diagnostic.range.start.line} -- ${diagnostic.message}`,
|
|
942
|
+
),
|
|
943
|
+
...result.changed.map(({ before, after }) => `changed ${after.range.path}:${after.range.start.line} -- ${before.message} -> ${after.message}`),
|
|
944
|
+
].join("\n");
|
|
945
|
+
return { content: [{ type: "text", text: text || "No diagnostic changes." }], details: result };
|
|
946
|
+
},
|
|
947
|
+
renderCall(args, theme, context) {
|
|
948
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
949
|
+
text.setText(formatDiagnosticDeltaCall(args, theme));
|
|
950
|
+
return text;
|
|
951
|
+
},
|
|
952
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
953
|
+
if (isPartial) return new Text(theme.fg("warning", "Comparing diagnostics..."), 0, 0);
|
|
954
|
+
if (context.isError) {
|
|
955
|
+
const errorText = result.content
|
|
956
|
+
.filter((block) => block.type === "text")
|
|
957
|
+
.map((block) => block.text)
|
|
958
|
+
.join("\n");
|
|
959
|
+
return new Text(theme.fg("error", errorText || "diagnostic_delta failed"), 0, 0);
|
|
960
|
+
}
|
|
961
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
962
|
+
text.setText(formatDiagnosticDeltaResult(result.details, expanded, theme));
|
|
963
|
+
return text;
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
|
|
777
967
|
registerLectorTool({
|
|
778
968
|
name: "call_hierarchy",
|
|
779
969
|
label: "Call Hierarchy",
|
|
@@ -840,6 +1030,137 @@ export default function (pi: ExtensionAPI) {
|
|
|
840
1030
|
},
|
|
841
1031
|
});
|
|
842
1032
|
|
|
1033
|
+
registerLectorTool({
|
|
1034
|
+
name: "type_hierarchy",
|
|
1035
|
+
label: "Type Hierarchy",
|
|
1036
|
+
description:
|
|
1037
|
+
"Resolve a type at an exact file position, or list its direct supertypes or subtypes. ACTIONS: prepare, supertypes, subtypes. Capability-unavailable is reported distinctly from an empty hierarchy.",
|
|
1038
|
+
promptSnippet: "Resolve a type hierarchy at an exact position",
|
|
1039
|
+
promptGuidelines: ["Use supertypes/subtypes instead of grepping for extends or implements; the language server resolves semantic relationships."],
|
|
1040
|
+
parameters: Type.Object({
|
|
1041
|
+
direction: Type.String({ description: "prepare | supertypes | subtypes" }),
|
|
1042
|
+
...positionParameters,
|
|
1043
|
+
maxResults: Type.Optional(Type.Number({ description: "Maximum hierarchy items (default 1000)" })),
|
|
1044
|
+
maxBytes: Type.Optional(Type.Number({ description: "Maximum JSON bytes (default 1 MiB)" })),
|
|
1045
|
+
deadlineMs: Type.Optional(Type.Number({ description: "Wall-clock deadline in milliseconds (default 10000, maximum 120000)" })),
|
|
1046
|
+
}),
|
|
1047
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<OperationOutputs["workspace.prepareTypeHierarchy"]>> {
|
|
1048
|
+
const path = resolve(cwd, params.path);
|
|
1049
|
+
const bounds = { maxResults: params.maxResults, maxBytes: params.maxBytes, deadlineMs: params.deadlineMs };
|
|
1050
|
+
const result =
|
|
1051
|
+
params.direction === "prepare"
|
|
1052
|
+
? await codeIntelligenceOperations.prepareTypeHierarchy(path, params.line, params.character, bounds)
|
|
1053
|
+
: params.direction === "supertypes"
|
|
1054
|
+
? await codeIntelligenceOperations.supertypes(path, params.line, params.character, bounds)
|
|
1055
|
+
: params.direction === "subtypes"
|
|
1056
|
+
? await codeIntelligenceOperations.subtypes(path, params.line, params.character, bounds)
|
|
1057
|
+
: undefined;
|
|
1058
|
+
if (!result) throw new Error(`unknown type_hierarchy direction: ${String(params.direction)}`);
|
|
1059
|
+
const text =
|
|
1060
|
+
result.items.length === 0
|
|
1061
|
+
? "No type-hierarchy items found."
|
|
1062
|
+
: result.items.map((item) => `${item.kind} ${item.name} -- ${item.location.path}:${item.location.line}:${item.location.character}`).join("\n");
|
|
1063
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(result.provenance)}\n${text}` }], details: result };
|
|
1064
|
+
},
|
|
1065
|
+
renderCall(args, theme, context) {
|
|
1066
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1067
|
+
text.setText(formatTypeHierarchyCall(args, theme));
|
|
1068
|
+
return text;
|
|
1069
|
+
},
|
|
1070
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1071
|
+
if (isPartial) return new Text(theme.fg("warning", "Resolving type hierarchy..."), 0, 0);
|
|
1072
|
+
if (context.isError) {
|
|
1073
|
+
const errorText = result.content
|
|
1074
|
+
.filter((block) => block.type === "text")
|
|
1075
|
+
.map((block) => block.text)
|
|
1076
|
+
.join("\n");
|
|
1077
|
+
return new Text(theme.fg("error", errorText || "type_hierarchy failed"), 0, 0);
|
|
1078
|
+
}
|
|
1079
|
+
const details = result.details;
|
|
1080
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1081
|
+
text.setText(renderIntelligenceSource(formatTypeHierarchyResult(details, expanded, theme), details.provenance, theme));
|
|
1082
|
+
return text;
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
registerLectorTool({
|
|
1087
|
+
name: "impact_analysis",
|
|
1088
|
+
label: "Impact Analysis",
|
|
1089
|
+
description:
|
|
1090
|
+
"Map a git diff or mutation transaction to changed declarations, semantic callers/references, package boundaries, diagnostics, and related tests. Associations preserve semantic-edge versus filename-heuristic evidence.",
|
|
1091
|
+
promptSnippet: "Find symbols and tests affected by a change",
|
|
1092
|
+
promptGuidelines: [
|
|
1093
|
+
"Use after a mutation or against a git ref to choose targeted verification from explicit evidence rather than filename guesses alone.",
|
|
1094
|
+
],
|
|
1095
|
+
parameters: Type.Object({
|
|
1096
|
+
path: Type.String({ description: "Project directory or a path inside it" }),
|
|
1097
|
+
source: Type.String({ description: "git | mutation" }),
|
|
1098
|
+
ref: Type.Optional(Type.String({ description: "Git ref for source=git" })),
|
|
1099
|
+
transactionId: Type.Optional(Type.String({ description: "Mutation transaction id for source=mutation" })),
|
|
1100
|
+
maxDepth: Type.Number({ description: "Maximum reverse-graph hops" }),
|
|
1101
|
+
maxNodes: Type.Number({ description: "Maximum graph nodes" }),
|
|
1102
|
+
maxEdges: Type.Number({ description: "Maximum graph edges" }),
|
|
1103
|
+
maxBytes: Type.Number({ description: "Maximum response JSON bytes" }),
|
|
1104
|
+
deadlineMs: Type.Number({ description: "Wall-clock deadline in milliseconds" }),
|
|
1105
|
+
maxFiles: Type.Number({ description: "Maximum source files for graph freshness/population" }),
|
|
1106
|
+
maxSymbolsPerFile: Type.Number({ description: "Maximum declarations per source file" }),
|
|
1107
|
+
autoPopulate: Type.Optional(Type.Boolean({ description: "Populate once when no complete graph exists" })),
|
|
1108
|
+
coverage: Type.Optional(
|
|
1109
|
+
Type.Array(
|
|
1110
|
+
Type.Object({
|
|
1111
|
+
testPath: Type.String(),
|
|
1112
|
+
coveredPaths: Type.Array(Type.String(), { maxItems: 1000 }),
|
|
1113
|
+
}),
|
|
1114
|
+
{ maxItems: 1000, description: "Optional test-to-covered-source evidence" },
|
|
1115
|
+
),
|
|
1116
|
+
),
|
|
1117
|
+
}),
|
|
1118
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<OperationOutputs["workspace.impactAnalysis"]>> {
|
|
1119
|
+
const source =
|
|
1120
|
+
params.source === "git"
|
|
1121
|
+
? ({ kind: "git", ...(params.ref !== undefined ? { ref: params.ref } : {}) } as const)
|
|
1122
|
+
: params.source === "mutation" && params.transactionId
|
|
1123
|
+
? ({ kind: "mutation", transactionId: params.transactionId } as const)
|
|
1124
|
+
: undefined;
|
|
1125
|
+
if (!source) throw new TypeError("impact_analysis requires source=git or source=mutation with transactionId");
|
|
1126
|
+
const result = await codeIntelligenceOperations.impactAnalysis(resolve(cwd, params.path), source, {
|
|
1127
|
+
maxDepth: params.maxDepth,
|
|
1128
|
+
maxNodes: params.maxNodes,
|
|
1129
|
+
maxEdges: params.maxEdges,
|
|
1130
|
+
maxBytes: params.maxBytes,
|
|
1131
|
+
deadlineMs: params.deadlineMs,
|
|
1132
|
+
maxFiles: params.maxFiles,
|
|
1133
|
+
maxSymbolsPerFile: params.maxSymbolsPerFile,
|
|
1134
|
+
...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
|
|
1135
|
+
...(params.coverage !== undefined ? { coverage: params.coverage } : {}),
|
|
1136
|
+
});
|
|
1137
|
+
const text = [
|
|
1138
|
+
...result.changedSymbols.map(({ symbol, side }) => `changed ${side} ${symbol.kind} ${symbol.name} -- ${symbol.location.path}`),
|
|
1139
|
+
...result.impactedSymbols.map(({ symbol, depth }) => `impact depth=${depth} ${symbol.kind} ${symbol.name} -- ${symbol.location.path}`),
|
|
1140
|
+
...result.relatedTests.map(({ symbol, evidence }) => `test ${evidence.kind} -- ${symbol.location.path}`),
|
|
1141
|
+
].join("\n");
|
|
1142
|
+
return { content: [{ type: "text", text: text || "No changed symbols resolved." }], details: result };
|
|
1143
|
+
},
|
|
1144
|
+
renderCall(args, theme, context) {
|
|
1145
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1146
|
+
text.setText(formatImpactAnalysisCall(args, theme));
|
|
1147
|
+
return text;
|
|
1148
|
+
},
|
|
1149
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1150
|
+
if (isPartial) return new Text(theme.fg("warning", "Analyzing impact..."), 0, 0);
|
|
1151
|
+
if (context.isError) {
|
|
1152
|
+
const errorText = result.content
|
|
1153
|
+
.filter((block) => block.type === "text")
|
|
1154
|
+
.map((block) => block.text)
|
|
1155
|
+
.join("\n");
|
|
1156
|
+
return new Text(theme.fg("error", errorText || "impact_analysis failed"), 0, 0);
|
|
1157
|
+
}
|
|
1158
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1159
|
+
text.setText(formatImpactAnalysisResult(result.details, expanded, theme));
|
|
1160
|
+
return text;
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
|
|
843
1164
|
registerLectorTool({
|
|
844
1165
|
name: "reference_based_rename",
|
|
845
1166
|
label: "Reference-Based Rename",
|
|
@@ -1012,6 +1333,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1012
1333
|
childId: Type.Optional(Type.String({ description: "The contained annotation's id -- required for contain/uncontain" })),
|
|
1013
1334
|
rootId: Type.Optional(Type.String({ description: "The subtree's root annotation id -- required for tree" })),
|
|
1014
1335
|
maxDepth: Type.Optional(Type.Number({ description: "Maximum containment hops from rootId to include -- required for tree" })),
|
|
1336
|
+
autoPopulate: Type.Optional(Type.Boolean({ description: "For create/refresh: populate a genuinely not-cached graph once before resolving anchors" })),
|
|
1337
|
+
maxFiles: Type.Optional(Type.Number({ description: "For create/refresh autoPopulate: explicit maximum files to scan" })),
|
|
1338
|
+
maxSymbolsPerFile: Type.Optional(Type.Number({ description: "For create/refresh autoPopulate: explicit maximum declarations per file" })),
|
|
1015
1339
|
}),
|
|
1016
1340
|
async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<SymbolAnnotationToolDetails>> {
|
|
1017
1341
|
const path = resolve(cwd, params.path);
|
|
@@ -1034,6 +1358,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1034
1358
|
params.body,
|
|
1035
1359
|
resolveAnchorInputs(params.anchors),
|
|
1036
1360
|
vehicleCall,
|
|
1361
|
+
{
|
|
1362
|
+
...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
|
|
1363
|
+
...(params.maxFiles !== undefined ? { maxFiles: params.maxFiles } : {}),
|
|
1364
|
+
...(params.maxSymbolsPerFile !== undefined ? { maxSymbolsPerFile: params.maxSymbolsPerFile } : {}),
|
|
1365
|
+
},
|
|
1037
1366
|
);
|
|
1038
1367
|
details.annotation = annotation;
|
|
1039
1368
|
text = formatAnnotationDetail(annotation);
|
|
@@ -1063,6 +1392,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1063
1392
|
params.body,
|
|
1064
1393
|
resolveAnchorInputs(params.anchors),
|
|
1065
1394
|
vehicleCall,
|
|
1395
|
+
{
|
|
1396
|
+
...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
|
|
1397
|
+
...(params.maxFiles !== undefined ? { maxFiles: params.maxFiles } : {}),
|
|
1398
|
+
...(params.maxSymbolsPerFile !== undefined ? { maxSymbolsPerFile: params.maxSymbolsPerFile } : {}),
|
|
1399
|
+
},
|
|
1066
1400
|
);
|
|
1067
1401
|
details.annotation = annotation;
|
|
1068
1402
|
text = annotation ? formatAnnotationDetail(annotation) : `no annotation "${params.id}"`;
|
|
@@ -1169,10 +1503,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1169
1503
|
description: "Restrict to one edge kind; omit for any kind",
|
|
1170
1504
|
}),
|
|
1171
1505
|
),
|
|
1506
|
+
autoPopulate: Type.Optional(Type.Boolean({ description: "Populate a genuinely not-cached graph once before traversing" })),
|
|
1507
|
+
maxFiles: Type.Optional(Type.Number({ description: "For autoPopulate: explicit maximum files to scan" })),
|
|
1508
|
+
maxSymbolsPerFile: Type.Optional(Type.Number({ description: "For autoPopulate: explicit maximum declarations per file" })),
|
|
1172
1509
|
}),
|
|
1173
1510
|
async execute(_toolCallId, params) {
|
|
1174
1511
|
const path = resolve(cwd, params.path);
|
|
1175
|
-
const symbols = await codeIntelligenceOperations.reachableFrom(path, params.line, params.character, params.maxDepth, params.kind
|
|
1512
|
+
const symbols = await codeIntelligenceOperations.reachableFrom(path, params.line, params.character, params.maxDepth, params.kind, {
|
|
1513
|
+
...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
|
|
1514
|
+
...(params.maxFiles !== undefined ? { maxFiles: params.maxFiles } : {}),
|
|
1515
|
+
...(params.maxSymbolsPerFile !== undefined ? { maxSymbolsPerFile: params.maxSymbolsPerFile } : {}),
|
|
1516
|
+
});
|
|
1176
1517
|
const text =
|
|
1177
1518
|
symbols.length === 0
|
|
1178
1519
|
? "Nothing reachable at this position."
|
|
@@ -1354,18 +1695,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1354
1695
|
name: "git",
|
|
1355
1696
|
label: "Git",
|
|
1356
1697
|
description:
|
|
1357
|
-
"Working tree status, recent commit log, unified diff, one symbol's own declaration diff across two versions, ref-scoped blob/text/ancestry queries with no checkout, and a real disposable checkout at another ref, for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes), compare-symbol (a named symbol's own declaration text diffed between fromRef and toRef, or fromRef and the current working tree when toRef is omitted -- tree-sitter syntactic tier only, TypeScript/JavaScript files only, no project-aware cross-reference resolution), show (a path's exact blob content at `ref`, no checkout), grep-ref (text search across `ref`'s own tree, no checkout -- the ref-scoped equivalent of search_code), ls-ref (every file path in `ref`'s own tree, no checkout), is-ancestor (is `ancestorRef` a real ancestor of, or the same commit as, `ref` -- the backport/reachability check \"was this fix ported to this branch\" actually needs), worktree-add (materializes `ref` as a real, read-only project via a detached git worktree and returns its own `directory` -- pass that straight to find_symbols/search_code/this tool itself for full semantic queries against another branch/commit, not just text), worktree-remove (releases and deletes a worktree-add-created checkout -- `directory` is that checkout's own returned directory, not the source repo's).",
|
|
1698
|
+
"Working tree status, recent commit log, unified diff, one symbol's own declaration diff across two versions, ref-scoped blob/text/ancestry queries with no checkout, and a real disposable checkout at another ref, for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes), compare-symbol (a named symbol's own declaration text diffed between fromRef and toRef, or fromRef and the current working tree when toRef is omitted -- tree-sitter syntactic tier only, TypeScript/JavaScript files only, no project-aware cross-reference resolution), show (a path's exact blob content at `ref`, no checkout), grep-ref (text search across `ref`'s own tree, no checkout -- the ref-scoped equivalent of search_code), grep-history (bounded extended-regex search across commit trees reachable from all refs, with topological paging and commit provenance), ls-ref (every file path in `ref`'s own tree, no checkout), is-ancestor (is `ancestorRef` a real ancestor of, or the same commit as, `ref` -- the backport/reachability check \"was this fix ported to this branch\" actually needs), worktree-add (materializes `ref` as a real, read-only project via a detached git worktree and returns its own `directory` -- pass that straight to find_symbols/search_code/this tool itself for full semantic queries against another branch/commit, not just text), worktree-remove (releases and deletes a worktree-add-created checkout -- `directory` is that checkout's own returned directory, not the source repo's).",
|
|
1358
1699
|
promptSnippet:
|
|
1359
1700
|
"Show a repository's status, log, diff, one symbol's diff across versions, a ref-scoped blob/text/ancestry query, or a real checkout at another ref",
|
|
1360
1701
|
promptGuidelines: [
|
|
1361
|
-
"maxCount is required for action=log; maxBytes is required for action=diff/compare-symbol/grep-ref -- every bounded query needs its bound stated explicitly, never defaulted silently.",
|
|
1702
|
+
"maxCount is required for action=log; maxBytes is required for action=diff/compare-symbol/grep-ref/grep-history -- every bounded query needs its bound stated explicitly, never defaulted silently.",
|
|
1362
1703
|
"path, symbol, and fromRef are required for action=compare-symbol; toRef is optional and means 'the current working tree' when omitted.",
|
|
1363
1704
|
"ref is required for action=worktree-add. A repeated worktree-add for the same (directory, ref) reuses the existing checkout unless forceRefresh is set -- use that when ref is a branch that may have moved.",
|
|
1364
1705
|
"action=worktree-remove's directory is worktree-add's own returned directory, never the source repo's -- always call it once done with a worktree to reclaim disk.",
|
|
1365
|
-
"ref and path are required for action=show; ref and pattern (maxMatches, maxBytes) are required for action=grep-ref; ref (maxResults) is required for action=ls-ref; ancestorRef and ref are required for action=is-ancestor.
|
|
1706
|
+
"ref and path are required for action=show; ref and pattern (maxMatches, maxBytes) are required for action=grep-ref; pattern, commitOffset, maxCommits, maxMatches, maxBytes, and deadlineMs are required for action=grep-history; ref (maxResults) is required for action=ls-ref; ancestorRef and ref are required for action=is-ancestor. These actions inspect Git objects directly with no checkout.",
|
|
1366
1707
|
],
|
|
1367
1708
|
parameters: Type.Object({
|
|
1368
|
-
action: Type.String({
|
|
1709
|
+
action: Type.String({
|
|
1710
|
+
description: "status | log | diff | compare-symbol | show | grep-ref | grep-history | ls-ref | is-ancestor | worktree-add | worktree-remove",
|
|
1711
|
+
}),
|
|
1369
1712
|
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
1370
1713
|
maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return, most recent first -- required for action=log" })),
|
|
1371
1714
|
ref: Type.Optional(
|
|
@@ -1376,7 +1719,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1376
1719
|
),
|
|
1377
1720
|
maxBytes: Type.Optional(
|
|
1378
1721
|
Type.Number({
|
|
1379
|
-
|
|
1722
|
+
minimum: 1,
|
|
1723
|
+
maximum: 8 * 1024 * 1024,
|
|
1724
|
+
description: "Maximum diff/comparison/grep output size in bytes before truncating -- required for action=diff/compare-symbol/grep-ref/grep-history",
|
|
1380
1725
|
}),
|
|
1381
1726
|
),
|
|
1382
1727
|
path: Type.Optional(
|
|
@@ -1392,14 +1737,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
1392
1737
|
description: "action=worktree-add only: recreate an already-reused worktree at ref's current tip instead of returning the existing one",
|
|
1393
1738
|
}),
|
|
1394
1739
|
),
|
|
1395
|
-
pattern: Type.Optional(
|
|
1740
|
+
pattern: Type.Optional(
|
|
1741
|
+
Type.String({ maxLength: 4096, description: "Pattern to search for -- required for grep-ref; an extended regular expression for grep-history" }),
|
|
1742
|
+
),
|
|
1396
1743
|
pathspecs: Type.Optional(
|
|
1397
|
-
Type.Array(Type.String(), {
|
|
1744
|
+
Type.Array(Type.String({ minLength: 1, maxLength: 1024 }), {
|
|
1745
|
+
maxItems: 64,
|
|
1398
1746
|
description:
|
|
1399
|
-
'Narrows action=grep-ref (glob-based, e.g. "*.go") or action=ls-ref (prefix-based, e.g. "pkg/dpll"); omitted searches/lists the whole tree',
|
|
1747
|
+
'Narrows action=grep-ref/grep-history (glob-based, e.g. "*.go") or action=ls-ref (prefix-based, e.g. "pkg/dpll"); omitted searches/lists the whole tree',
|
|
1400
1748
|
}),
|
|
1401
1749
|
),
|
|
1402
|
-
maxMatches: Type.Optional(
|
|
1750
|
+
maxMatches: Type.Optional(
|
|
1751
|
+
Type.Number({ minimum: 1, maximum: 10_000, description: "Maximum grep matches to return -- required for action=grep-ref/grep-history" }),
|
|
1752
|
+
),
|
|
1753
|
+
commitOffset: Type.Optional(
|
|
1754
|
+
Type.Number({ minimum: 0, maximum: 1_000_000, description: "Number of topologically ordered commits to skip -- required for action=grep-history" }),
|
|
1755
|
+
),
|
|
1756
|
+
maxCommits: Type.Optional(Type.Number({ minimum: 1, maximum: 512, description: "Maximum commit trees to search -- required for action=grep-history" })),
|
|
1757
|
+
deadlineMs: Type.Optional(Type.Number({ minimum: 1, maximum: 120_000, description: "Wall-clock budget -- required for action=grep-history" })),
|
|
1403
1758
|
maxResults: Type.Optional(Type.Number({ description: "Maximum file paths to return -- required for action=ls-ref" })),
|
|
1404
1759
|
ancestorRef: Type.Optional(Type.String({ description: "The candidate ancestor ref -- required for action=is-ancestor" })),
|
|
1405
1760
|
}),
|
|
@@ -1456,6 +1811,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
1456
1811
|
const details: GitToolDetails = { action: "grep-ref", grep };
|
|
1457
1812
|
return { content: [{ type: "text", text: JSON.stringify(grep) }], details };
|
|
1458
1813
|
}
|
|
1814
|
+
if (params.action === "grep-history") {
|
|
1815
|
+
if (!params.pattern) throw new Error("git action=grep-history requires pattern");
|
|
1816
|
+
if (
|
|
1817
|
+
params.commitOffset === undefined ||
|
|
1818
|
+
params.maxCommits === undefined ||
|
|
1819
|
+
params.maxMatches === undefined ||
|
|
1820
|
+
params.maxBytes === undefined ||
|
|
1821
|
+
params.deadlineMs === undefined
|
|
1822
|
+
)
|
|
1823
|
+
throw new Error("git action=grep-history requires commitOffset, maxCommits, maxMatches, maxBytes, and deadlineMs");
|
|
1824
|
+
const historyGrep = await gitOperations.grepHistory(
|
|
1825
|
+
directory,
|
|
1826
|
+
params.pattern,
|
|
1827
|
+
params.pathspecs,
|
|
1828
|
+
{
|
|
1829
|
+
commitOffset: params.commitOffset,
|
|
1830
|
+
maxCommits: params.maxCommits,
|
|
1831
|
+
maxMatches: params.maxMatches,
|
|
1832
|
+
maxBytes: params.maxBytes,
|
|
1833
|
+
deadlineMs: params.deadlineMs,
|
|
1834
|
+
},
|
|
1835
|
+
vehicleCall,
|
|
1836
|
+
);
|
|
1837
|
+
const details: GitToolDetails = { action: "grep-history", historyGrep };
|
|
1838
|
+
return { content: [{ type: "text", text: JSON.stringify(historyGrep) }], details };
|
|
1839
|
+
}
|
|
1459
1840
|
if (params.action === "ls-ref") {
|
|
1460
1841
|
if (!params.ref) throw new Error("git action=ls-ref requires ref");
|
|
1461
1842
|
if (params.maxResults === undefined) throw new Error("git action=ls-ref requires maxResults");
|
|
@@ -1497,7 +1878,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1497
1878
|
name: "search_code",
|
|
1498
1879
|
label: "Search Code",
|
|
1499
1880
|
description:
|
|
1500
|
-
"Multi-file text/regex search scoped to a real project directory
|
|
1881
|
+
"Multi-file text/regex search scoped to a real project directory. Uses a bounded resident FFF index when ready and explicit ripgrep loading/stale/degraded fallback otherwise; respects ignore rules and skips generated dependency output. Bounded by maxMatches and maxBytes; results include lexical provenance.",
|
|
1501
1882
|
promptSnippet: "Search a project's files for a pattern",
|
|
1502
1883
|
parameters: Type.Object({
|
|
1503
1884
|
directory: Type.String({ description: "Directory inside the project to search, absolute or relative to the current working directory" }),
|
|
@@ -1508,8 +1889,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1508
1889
|
async execute(_toolCallId, params) {
|
|
1509
1890
|
const directory = resolve(cwd, params.directory);
|
|
1510
1891
|
const result = await searchOperations.search(params.query, directory, params.maxMatches, params.maxBytes);
|
|
1511
|
-
const
|
|
1892
|
+
const matches =
|
|
1512
1893
|
result.matches.length === 0 ? "No matches found." : result.matches.map((m) => `${m.path}:${m.lineNumber}: ${m.line.replace(/\n$/, "")}`).join("\n");
|
|
1894
|
+
const source = result.provenance ? `lexical via ${result.provenance.backend} (${result.provenance.indexState})\n` : "";
|
|
1895
|
+
const text = `${source}${matches}`;
|
|
1513
1896
|
return { content: [{ type: "text", text }], details: { result } };
|
|
1514
1897
|
},
|
|
1515
1898
|
renderCall(args, theme, context) {
|
|
@@ -2114,7 +2497,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2114
2497
|
name: "search_code_across_projects",
|
|
2115
2498
|
label: "Search Code Across Projects",
|
|
2116
2499
|
description:
|
|
2117
|
-
"Fans out
|
|
2500
|
+
"Fans out bounded indexed lexical text/regex search with explicit ripgrep fallback provenance across several explicitly-named project directories at once and reports one outcome per project. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions. Each directory resolves to its OWN nearest project root (package.json/tsconfig.json/go.mod/Cargo.toml/...), not the outer repo's git root -- sibling packages under one monorepo stay distinct scopes rather than collapsing into one. A result's collapsedWith lists any other requested directories that genuinely did resolve to the same workspace; empty means it got its own.",
|
|
2118
2501
|
promptSnippet: "Search for a pattern across several projects at once",
|
|
2119
2502
|
parameters: Type.Object({
|
|
2120
2503
|
directories: Type.Array(Type.String(), { description: "Project directories to search, each absolute or relative to the current working directory" }),
|
|
@@ -12,7 +12,9 @@ export function formatSearchCall(args: { directory?: unknown; query?: unknown },
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function formatSearchResult(result: TextSearchResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
15
|
-
if (!result
|
|
15
|
+
if (!result) return theme.fg("dim", "No matches found.");
|
|
16
|
+
const provenance = result.provenance ? theme.fg("dim", `lexical via ${result.provenance.backend} (${result.provenance.indexState})`) : undefined;
|
|
17
|
+
if (result.matches.length === 0) return [provenance, theme.fg("dim", "No matches found.")].filter((line) => line !== undefined).join("\n");
|
|
16
18
|
const lines = renderTruncatedList({
|
|
17
19
|
items: result.matches,
|
|
18
20
|
expanded,
|
|
@@ -22,5 +24,5 @@ export function formatSearchResult(result: TextSearchResult | undefined, expande
|
|
|
22
24
|
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
23
25
|
truncationWarning: result.truncated ? theme.fg("warning", "(search itself was truncated by maxMatches/maxBytes -- results are incomplete)") : undefined,
|
|
24
26
|
});
|
|
25
|
-
return lines.join("\n");
|
|
27
|
+
return [provenance, ...lines].filter((line) => line !== undefined).join("\n");
|
|
26
28
|
}
|
|
@@ -13,6 +13,8 @@ export interface AnnotationAnchorInput {
|
|
|
13
13
|
readonly character: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export type AnnotationAutoPopulationOptions = Pick<OperationInputs["workspace.createAnnotation"], "autoPopulate" | "maxFiles" | "maxSymbolsPerFile">;
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* Thin wrappers over Lector's annotation operations. Every operation resolves its workspace from
|
|
18
20
|
* its own `path` parameter via workspaceForAnnotationPath -- a real project directory or an
|
|
@@ -29,6 +31,7 @@ export interface SymbolAnnotationOperations {
|
|
|
29
31
|
body: string,
|
|
30
32
|
anchors: readonly AnnotationAnchorInput[],
|
|
31
33
|
call: LectorVehicleCall,
|
|
34
|
+
autoPopulation?: AnnotationAutoPopulationOptions,
|
|
32
35
|
): Promise<OperationOutputs["workspace.createAnnotation"]>;
|
|
33
36
|
get(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.getAnnotation"]>;
|
|
34
37
|
list(
|
|
@@ -44,6 +47,7 @@ export interface SymbolAnnotationOperations {
|
|
|
44
47
|
body: string,
|
|
45
48
|
anchors: readonly AnnotationAnchorInput[],
|
|
46
49
|
call: LectorVehicleCall,
|
|
50
|
+
autoPopulation?: AnnotationAutoPopulationOptions,
|
|
47
51
|
): Promise<OperationOutputs["workspace.refreshAnnotation"]>;
|
|
48
52
|
scrub(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.scrubAnnotation"]>;
|
|
49
53
|
restore(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.restoreAnnotation"]>;
|
|
@@ -54,13 +58,13 @@ export interface SymbolAnnotationOperations {
|
|
|
54
58
|
|
|
55
59
|
export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperations {
|
|
56
60
|
return {
|
|
57
|
-
async create(path, subtype, title, body, anchors, call) {
|
|
61
|
+
async create(path, subtype, title, body, anchors, call, autoPopulation) {
|
|
58
62
|
return withWorkspace(
|
|
59
63
|
() => workspaceForAnnotationPath(path),
|
|
60
64
|
({ workspaceId }) =>
|
|
61
65
|
invokeLectorVehicleOperation<OperationOutputs["workspace.createAnnotation"]>(
|
|
62
66
|
"workspace.createAnnotation",
|
|
63
|
-
{ workspaceId, subtype, title, body, anchors },
|
|
67
|
+
{ workspaceId, subtype, title, body, anchors, ...autoPopulation },
|
|
64
68
|
ANNOTATION_WRITE_PERMISSIONS,
|
|
65
69
|
call,
|
|
66
70
|
),
|
|
@@ -90,13 +94,13 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
|
|
|
90
94
|
),
|
|
91
95
|
);
|
|
92
96
|
},
|
|
93
|
-
async refresh(path, id, subtype, title, body, anchors, call) {
|
|
97
|
+
async refresh(path, id, subtype, title, body, anchors, call, autoPopulation) {
|
|
94
98
|
return withWorkspace(
|
|
95
99
|
() => workspaceForAnnotationPath(path),
|
|
96
100
|
({ workspaceId }) =>
|
|
97
101
|
invokeLectorVehicleOperation<OperationOutputs["workspace.refreshAnnotation"]>(
|
|
98
102
|
"workspace.refreshAnnotation",
|
|
99
|
-
{ workspaceId, id, subtype, title, body, anchors },
|
|
103
|
+
{ workspaceId, id, subtype, title, body, anchors, ...autoPopulation },
|
|
100
104
|
ANNOTATION_WRITE_PERMISSIONS,
|
|
101
105
|
call,
|
|
102
106
|
),
|
|
@@ -29,9 +29,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
29
29
|
|
|
30
30
|
type VehicleClientConnector = () => Promise<VehicleClient>;
|
|
31
31
|
|
|
32
|
-
function connectLectorVehicleClient(): Promise<VehicleClient> {
|
|
32
|
+
async function connectLectorVehicleClient(): Promise<VehicleClient> {
|
|
33
33
|
const { host, port, token } = resolveLectorDaemonConnection();
|
|
34
|
-
|
|
34
|
+
const client = new RemoteVehicleClient({ baseUrl: `http://${host}:${port}`, token });
|
|
35
|
+
await client.negotiate({ minimumVersion: 1, maximumVersion: 1, requiredCapabilities: [], optionalCapabilities: [] });
|
|
36
|
+
return client;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function resolveLectorVehicleIdentity() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.11",
|
|
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,9 +22,9 @@
|
|
|
22
22
|
"@danypops/vehicle-client-pi": "^0.45.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@danypops/lector": "^0.20.
|
|
26
|
-
"@danypops/vehicle-client": "^0.10.
|
|
27
|
-
"@danypops/vehicle-core": "^0.
|
|
25
|
+
"@danypops/lector": "^0.20.4",
|
|
26
|
+
"@danypops/vehicle-client": "^0.10.8",
|
|
27
|
+
"@danypops/vehicle-core": "^0.19.1",
|
|
28
28
|
"malevich-tui-components": "^0.32.1",
|
|
29
29
|
"picomatch": "^4.0.5"
|
|
30
30
|
},
|