@danypops/pi-lector 0.2.2 → 0.4.0
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-rendering.ts +26 -13
- package/extension/src/git-rendering.ts +23 -16
- package/extension/src/index.ts +491 -258
- package/extension/src/lector-client.ts +28 -6
- package/extension/src/mutation-history-operations.ts +34 -0
- package/extension/src/nearest-workspace-root.ts +19 -3
- package/extension/src/reference-based-rename-operations.ts +26 -0
- package/extension/src/rename-operations.ts +36 -0
- package/extension/src/repo-cache-list-operations.ts +24 -0
- package/extension/src/symbol-annotation-operations.ts +38 -2
- package/package.json +3 -3
package/extension/src/index.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type {
|
|
3
|
-
CallHierarchyEntry,
|
|
4
3
|
ContentHash,
|
|
5
4
|
Diagnostic,
|
|
6
5
|
DocumentSymbolEntry,
|
|
7
6
|
EditOutcome,
|
|
8
7
|
FindFilesResult,
|
|
9
|
-
GitDiffResult,
|
|
10
|
-
GitLogEntry,
|
|
11
|
-
GitStatusSummary,
|
|
12
8
|
Hover,
|
|
13
|
-
IncomingCall,
|
|
14
9
|
IntelligenceProvenance,
|
|
15
10
|
JobSnapshot,
|
|
16
11
|
LineEdit,
|
|
17
12
|
LineEditOutcome,
|
|
18
|
-
|
|
13
|
+
MutationHistoryEntry,
|
|
14
|
+
OperationOutputs,
|
|
19
15
|
PackageSourceOperationResult,
|
|
20
16
|
PopulateSymbolGraphResult,
|
|
21
17
|
RepoFetchResult,
|
|
@@ -27,14 +23,23 @@ import type {
|
|
|
27
23
|
WorkspaceMapResult,
|
|
28
24
|
WorkspaceQueryOutcome,
|
|
29
25
|
} from "@danypops/lector";
|
|
30
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
type AgentToolResult,
|
|
28
|
+
createEditToolDefinition,
|
|
29
|
+
createReadToolDefinition,
|
|
30
|
+
createWriteToolDefinition,
|
|
31
|
+
type ExtensionAPI,
|
|
32
|
+
} from "@earendil-works/pi-coding-agent";
|
|
31
33
|
import { Text } from "@earendil-works/pi-tui";
|
|
32
34
|
import { Type } from "typebox";
|
|
33
35
|
import { createLectorApplyPatchOperations } from "./apply-patch-operations.ts";
|
|
34
36
|
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch-rendering.ts";
|
|
35
37
|
import { createLectorCodeIntelligenceOperations } from "./code-intelligence-operations.ts";
|
|
36
38
|
import {
|
|
39
|
+
type CallHierarchyToolDetails,
|
|
37
40
|
describePopulateSymbolGraphJob,
|
|
41
|
+
formatCallHierarchyCall,
|
|
42
|
+
formatCallHierarchyResult,
|
|
38
43
|
formatDiagnosticsCall,
|
|
39
44
|
formatDiagnosticsResult,
|
|
40
45
|
formatDocumentSymbolsCall,
|
|
@@ -47,14 +52,8 @@ import {
|
|
|
47
52
|
formatGoToImplementationResult,
|
|
48
53
|
formatHoverCall,
|
|
49
54
|
formatHoverResult,
|
|
50
|
-
formatIncomingCallsCall,
|
|
51
|
-
formatIncomingCallsResult,
|
|
52
|
-
formatOutgoingCallsCall,
|
|
53
|
-
formatOutgoingCallsResult,
|
|
54
55
|
formatPopulateSymbolGraphCall,
|
|
55
56
|
formatPopulateSymbolGraphResult,
|
|
56
|
-
formatPrepareCallHierarchyCall,
|
|
57
|
-
formatPrepareCallHierarchyResult,
|
|
58
57
|
formatReachableFromCall,
|
|
59
58
|
formatReachableFromResult,
|
|
60
59
|
formatWorkspaceMapCall,
|
|
@@ -68,13 +67,18 @@ import { formatFindFilesCall, formatFindFilesResult } from "./find-files-renderi
|
|
|
68
67
|
import { createLectorFindSymbolsOperations } from "./find-symbols-operations.ts";
|
|
69
68
|
import { describeFindSymbolSources, formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols-rendering.ts";
|
|
70
69
|
import { createLectorGitOperations } from "./git-operations.ts";
|
|
71
|
-
import {
|
|
70
|
+
import { formatGitCall, formatGitResult, type GitToolDetails } from "./git-rendering.ts";
|
|
71
|
+
import { setNewWorkspaceObserver } from "./lector-client.ts";
|
|
72
72
|
import { createLectorLineEditOperations } from "./line-edit-operations.ts";
|
|
73
73
|
import { formatLineEditCall, formatLineEditResult } from "./line-edit-rendering.ts";
|
|
74
|
+
import { createMutationHistoryOperations } from "./mutation-history-operations.ts";
|
|
74
75
|
import { nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
75
76
|
import { createLectorPackageSourceOperations } from "./package-source-operations.ts";
|
|
76
77
|
import { formatPackageSourceCall, formatPackageSourceResult } from "./package-source-rendering.ts";
|
|
77
78
|
import { createLectorReadOperations } from "./read-operations.ts";
|
|
79
|
+
import { createReferenceBasedRenameOperations } from "./reference-based-rename-operations.ts";
|
|
80
|
+
import { createRenameOperations } from "./rename-operations.ts";
|
|
81
|
+
import { createRepoCacheListOperations } from "./repo-cache-list-operations.ts";
|
|
78
82
|
import { createLectorRepoFetchOperations } from "./repo-fetch-operations.ts";
|
|
79
83
|
import { formatRepoFetchCall, formatRepoFetchResult } from "./repo-fetch-rendering.ts";
|
|
80
84
|
import { createLectorSearchOperations } from "./search-operations.ts";
|
|
@@ -118,63 +122,115 @@ function renderIntelligenceSource(body: string, provenance: IntelligenceProvenan
|
|
|
118
122
|
*/
|
|
119
123
|
export default function (pi: ExtensionAPI) {
|
|
120
124
|
const cacheOperations = createWorkspaceCacheOperations();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
// One generation counter shared by every root's monitor loop, not per-root -- a new session
|
|
126
|
+
// (or shutdown) invalidates every previous session's in-flight monitor regardless of which
|
|
127
|
+
// root it tracked, and there is exactly one "current session" at a time.
|
|
128
|
+
let sessionGeneration = 0;
|
|
129
|
+
// Every workspace root actually touched so far this session, not just one fixed cwd root --
|
|
130
|
+
// populated by setNewWorkspaceObserver below, the real "first touch" trigger.
|
|
131
|
+
const cacheStatesByRoot = new Map<string, CachePresentationState>();
|
|
132
|
+
// Roots already monitored this session -- guards against starting the SAME root's monitor
|
|
133
|
+
// twice: session_start's own direct kick-off for the cwd root itself calls
|
|
134
|
+
// cacheOperations.status(), which registers that root via workspace.registerPath, which fires
|
|
135
|
+
// setNewWorkspaceObserver for it a moment later -- without this guard that would start a
|
|
136
|
+
// second, redundant concurrent monitor loop for the exact same root.
|
|
137
|
+
const monitoringRoots = new Set<string>();
|
|
138
|
+
let lastInjectedSummary: string | undefined;
|
|
139
|
+
let uiContext: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1] | undefined;
|
|
140
|
+
|
|
141
|
+
function combinedSummary(): string {
|
|
142
|
+
const states = [...cacheStatesByRoot.values()];
|
|
143
|
+
if (states.length === 0) return "";
|
|
144
|
+
const [only] = states;
|
|
145
|
+
if (states.length === 1 && only) return describeCacheState(only);
|
|
146
|
+
const counts = new Map<string, number>();
|
|
147
|
+
for (const state of states) counts.set(state.status, (counts.get(state.status) ?? 0) + 1);
|
|
148
|
+
return [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function refreshStatusBar(): void {
|
|
152
|
+
if (!uiContext) return;
|
|
153
|
+
const summary = combinedSummary();
|
|
154
|
+
if (!summary) {
|
|
155
|
+
uiContext.ui.setStatus("lector-cache", undefined);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const states = [...cacheStatesByRoot.values()];
|
|
159
|
+
const worst = states.some((state) => state.status === "not-cached" || state.status === "caching")
|
|
160
|
+
? "warning"
|
|
161
|
+
: states.every((state) => state.status === "cached")
|
|
162
|
+
? "success"
|
|
163
|
+
: "accent";
|
|
164
|
+
uiContext.ui.setStatus("lector-cache", uiContext.ui.theme.fg(worst, `Lector: ${summary}`));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Starts (or restarts, on a stale generation) monitoring one workspace root's cache lifecycle -- shared by session_start's own cwd root and every later root a tool call first touches. */
|
|
168
|
+
function startMonitoringRoot(root: string, ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]): void {
|
|
169
|
+
if (monitoringRoots.has(root)) return;
|
|
170
|
+
monitoringRoots.add(root);
|
|
171
|
+
const thisGeneration = sessionGeneration;
|
|
172
|
+
void monitorWorkspaceCache(cacheOperations, {
|
|
173
|
+
directory: root,
|
|
174
|
+
maxFiles: 500,
|
|
175
|
+
maxSymbolsPerFile: 100,
|
|
176
|
+
pollIntervalMs: 1_000,
|
|
177
|
+
maxPolls: 300,
|
|
178
|
+
shouldContinue: () => sessionGeneration === thisGeneration,
|
|
179
|
+
onState: (state) => {
|
|
180
|
+
if (sessionGeneration !== thisGeneration) return;
|
|
181
|
+
cacheStatesByRoot.set(root, state);
|
|
182
|
+
if (state.status === "finished-caching") {
|
|
183
|
+
if (ctx.hasUI) ctx.ui.notify(`Lector finished caching ${root}`, "info");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
refreshStatusBar();
|
|
187
|
+
},
|
|
188
|
+
}).catch((error: unknown) => {
|
|
189
|
+
if (sessionGeneration !== thisGeneration) return;
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
cacheStatesByRoot.delete(root);
|
|
192
|
+
refreshStatusBar();
|
|
193
|
+
if (ctx.hasUI) ctx.ui.notify(`Lector cache failed for ${root}: ${message}`, "error");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
124
196
|
|
|
125
197
|
pi.on("before_agent_start", () => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
198
|
+
const summary = combinedSummary();
|
|
199
|
+
if (!summary || summary === lastInjectedSummary) return;
|
|
200
|
+
lastInjectedSummary = summary;
|
|
201
|
+
const messages = [...cacheStatesByRoot.entries()]
|
|
202
|
+
.filter(([, state]) => state.status !== "cached")
|
|
203
|
+
.map(([root, state]) => `${root}: ${cacheContextMessage(state)}`);
|
|
204
|
+
if (messages.length === 0) return;
|
|
130
205
|
return {
|
|
131
206
|
message: {
|
|
132
207
|
customType: "lector-cache-status",
|
|
133
|
-
content:
|
|
208
|
+
content: messages.join("\n"),
|
|
134
209
|
display: false,
|
|
135
210
|
},
|
|
136
211
|
};
|
|
137
212
|
});
|
|
138
213
|
|
|
139
214
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
215
|
+
sessionGeneration++;
|
|
216
|
+
cacheStatesByRoot.clear();
|
|
217
|
+
monitoringRoots.clear();
|
|
218
|
+
lastInjectedSummary = undefined;
|
|
219
|
+
uiContext = undefined;
|
|
143
220
|
ctx.ui.setStatus("lector-cache", undefined);
|
|
144
221
|
});
|
|
145
222
|
|
|
146
223
|
pi.on("session_start", (_event, ctx) => {
|
|
147
224
|
const { cwd } = ctx;
|
|
225
|
+
sessionGeneration++;
|
|
226
|
+
cacheStatesByRoot.clear();
|
|
227
|
+
monitoringRoots.clear();
|
|
228
|
+
lastInjectedSummary = undefined;
|
|
229
|
+
uiContext = ctx;
|
|
230
|
+
setNewWorkspaceObserver((root) => startMonitoringRoot(root, ctx));
|
|
148
231
|
const projectRoot = nearestGitRoot(cwd);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
lastInjectedCacheState = undefined;
|
|
152
|
-
if (projectRoot) {
|
|
153
|
-
void monitorWorkspaceCache(cacheOperations, {
|
|
154
|
-
directory: projectRoot,
|
|
155
|
-
maxFiles: 500,
|
|
156
|
-
maxSymbolsPerFile: 100,
|
|
157
|
-
pollIntervalMs: 1_000,
|
|
158
|
-
maxPolls: 300,
|
|
159
|
-
shouldContinue: () => cacheRun === thisRun,
|
|
160
|
-
onState: (state) => {
|
|
161
|
-
cacheState = state;
|
|
162
|
-
if (state.status === "finished-caching") {
|
|
163
|
-
if (ctx.hasUI) ctx.ui.notify(`Lector finished caching ${projectRoot}`, "info");
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const color = state.status === "cached" ? "success" : state.status === "caching" ? "accent" : "warning";
|
|
167
|
-
ctx.ui.setStatus("lector-cache", ctx.ui.theme.fg(color, `Lector: ${describeCacheState(state)}`));
|
|
168
|
-
},
|
|
169
|
-
}).catch((error: unknown) => {
|
|
170
|
-
if (cacheRun !== thisRun) return;
|
|
171
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
-
ctx.ui.setStatus("lector-cache", ctx.ui.theme.fg("error", "Lector: cache error"));
|
|
173
|
-
if (ctx.hasUI) ctx.ui.notify(`Lector cache failed: ${message}`, "error");
|
|
174
|
-
});
|
|
175
|
-
} else {
|
|
176
|
-
ctx.ui.setStatus("lector-cache", undefined);
|
|
177
|
-
}
|
|
232
|
+
if (projectRoot) startMonitoringRoot(projectRoot, ctx);
|
|
233
|
+
else ctx.ui.setStatus("lector-cache", undefined);
|
|
178
234
|
|
|
179
235
|
pi.registerTool(createReadToolDefinition(cwd, { operations: createLectorReadOperations() }));
|
|
180
236
|
pi.registerTool(createWriteToolDefinition(cwd, { operations: createLectorWriteOperations() }));
|
|
@@ -221,7 +277,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
221
277
|
},
|
|
222
278
|
renderCall(args, theme, context) {
|
|
223
279
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
224
|
-
text.setText(formatFindSymbolsCall(args
|
|
280
|
+
text.setText(formatFindSymbolsCall(args, theme));
|
|
225
281
|
return text;
|
|
226
282
|
},
|
|
227
283
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -236,7 +292,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
292
|
return new Text(theme.fg("error", errorText || "find_symbols failed"), 0, 0);
|
|
237
293
|
}
|
|
238
294
|
const details = result.details as SymbolSearchResult | undefined;
|
|
239
|
-
const query = typeof context.args
|
|
295
|
+
const query = typeof context.args.query === "string" ? context.args.query : "";
|
|
240
296
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
241
297
|
text.setText(formatFindSymbolsResult(details, query, expanded, theme));
|
|
242
298
|
return text;
|
|
@@ -244,6 +300,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
244
300
|
});
|
|
245
301
|
|
|
246
302
|
const codeIntelligenceOperations = createLectorCodeIntelligenceOperations();
|
|
303
|
+
const referenceBasedRenameOperations = createReferenceBasedRenameOperations();
|
|
304
|
+
const renameOperations = createRenameOperations();
|
|
247
305
|
const positionParameters = {
|
|
248
306
|
path: Type.String({ description: "Absolute or cwd-relative path to the file" }),
|
|
249
307
|
line: Type.Number({ description: "1-indexed line number" }),
|
|
@@ -265,7 +323,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
265
323
|
},
|
|
266
324
|
renderCall(args, theme, context) {
|
|
267
325
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
268
|
-
text.setText(formatGoToDefinitionCall(args
|
|
326
|
+
text.setText(formatGoToDefinitionCall(args, theme));
|
|
269
327
|
return text;
|
|
270
328
|
},
|
|
271
329
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -303,7 +361,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
303
361
|
},
|
|
304
362
|
renderCall(args, theme, context) {
|
|
305
363
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
306
|
-
text.setText(formatGoToImplementationCall(args
|
|
364
|
+
text.setText(formatGoToImplementationCall(args, theme));
|
|
307
365
|
return text;
|
|
308
366
|
},
|
|
309
367
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -348,7 +406,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
348
406
|
},
|
|
349
407
|
renderCall(args, theme, context) {
|
|
350
408
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
351
|
-
text.setText(formatFindReferencesCall(args
|
|
409
|
+
text.setText(formatFindReferencesCall(args, theme));
|
|
352
410
|
return text;
|
|
353
411
|
},
|
|
354
412
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -388,7 +446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
446
|
},
|
|
389
447
|
renderCall(args, theme, context) {
|
|
390
448
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
391
|
-
text.setText(formatHoverCall(args
|
|
449
|
+
text.setText(formatHoverCall(args, theme));
|
|
392
450
|
return text;
|
|
393
451
|
},
|
|
394
452
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -422,7 +480,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
422
480
|
},
|
|
423
481
|
renderCall(args, theme, context) {
|
|
424
482
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
425
|
-
text.setText(formatDocumentSymbolsCall(args
|
|
483
|
+
text.setText(formatDocumentSymbolsCall(args, theme));
|
|
426
484
|
return text;
|
|
427
485
|
},
|
|
428
486
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -459,7 +517,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
459
517
|
},
|
|
460
518
|
renderCall(args, theme, context) {
|
|
461
519
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
462
|
-
text.setText(formatDiagnosticsCall(args
|
|
520
|
+
text.setText(formatDiagnosticsCall(args, theme));
|
|
463
521
|
return text;
|
|
464
522
|
},
|
|
465
523
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -479,118 +537,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
479
537
|
});
|
|
480
538
|
|
|
481
539
|
pi.registerTool({
|
|
482
|
-
name: "
|
|
483
|
-
label: "
|
|
484
|
-
description:
|
|
485
|
-
|
|
540
|
+
name: "call_hierarchy",
|
|
541
|
+
label: "Call Hierarchy",
|
|
542
|
+
description:
|
|
543
|
+
"Resolve the symbol at an exact file position to its call-hierarchy root, or find its real callers/callees, project-wide, in one tool. ACTIONS: prepare (confirm what the position resolves to), incoming (who actually calls it, as distinct from find_references which also finds non-call usages), outgoing (what it itself calls).",
|
|
544
|
+
promptSnippet: "Resolve a position to a call-hierarchy root, or find its callers/callees",
|
|
486
545
|
promptGuidelines: [
|
|
487
|
-
"
|
|
546
|
+
"direction=prepare is optional -- incoming/outgoing already resolve the position internally, so calling prepare first is never required, only useful to confirm what a position resolves to.",
|
|
547
|
+
"direction=incoming finds real callers, distinct from find_references which also finds non-call usages like type positions or re-exports.",
|
|
488
548
|
],
|
|
489
|
-
parameters: Type.Object(
|
|
490
|
-
|
|
549
|
+
parameters: Type.Object({
|
|
550
|
+
direction: Type.String({ description: "prepare | incoming | outgoing" }),
|
|
551
|
+
...positionParameters,
|
|
552
|
+
}),
|
|
553
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<CallHierarchyToolDetails>> {
|
|
491
554
|
const path = resolve(cwd, params.path);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
501
|
-
text.setText(formatPrepareCallHierarchyCall(args as { path?: unknown; line?: unknown; character?: unknown }, theme));
|
|
502
|
-
return text;
|
|
503
|
-
},
|
|
504
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
505
|
-
if (isPartial) return new Text(theme.fg("warning", "Resolving position..."), 0, 0);
|
|
506
|
-
if (context.isError) {
|
|
507
|
-
const errorText = result.content
|
|
508
|
-
.filter((block) => block.type === "text")
|
|
509
|
-
.map((block) => block.text)
|
|
510
|
-
.join("\n");
|
|
511
|
-
return new Text(theme.fg("error", errorText || "prepare_call_hierarchy failed"), 0, 0);
|
|
555
|
+
if (params.direction === "prepare") {
|
|
556
|
+
const { items, provenance } = await codeIntelligenceOperations.prepareCallHierarchy(path, params.line, params.character);
|
|
557
|
+
const text =
|
|
558
|
+
items.length === 0
|
|
559
|
+
? "No call-hierarchy root at this position."
|
|
560
|
+
: items.map((i) => `${i.kind} ${i.name} -- ${i.location.path}:${i.location.line}:${i.location.character}`).join("\n");
|
|
561
|
+
const details: CallHierarchyToolDetails = { direction: "prepare", items, provenance };
|
|
562
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(provenance)}\n${text}` }], details };
|
|
512
563
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
name: "incoming_calls",
|
|
522
|
-
label: "Incoming Calls",
|
|
523
|
-
description: "Find every real caller of the function/method at an exact file position, project-wide.",
|
|
524
|
-
promptSnippet: "Find every caller of a function from an exact position",
|
|
525
|
-
promptGuidelines: [
|
|
526
|
-
"Use incoming_calls to see who actually calls a function, as distinct from find_references, which also finds non-call usages like type positions or re-exports.",
|
|
527
|
-
],
|
|
528
|
-
parameters: Type.Object(positionParameters),
|
|
529
|
-
async execute(_toolCallId, params) {
|
|
530
|
-
const path = resolve(cwd, params.path);
|
|
531
|
-
const details = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
|
|
532
|
-
const text =
|
|
533
|
-
details.calls.length === 0
|
|
534
|
-
? "No incoming calls found."
|
|
535
|
-
: details.calls
|
|
536
|
-
.map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`)
|
|
537
|
-
.join("\n");
|
|
538
|
-
return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
|
|
539
|
-
},
|
|
540
|
-
renderCall(args, theme, context) {
|
|
541
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
542
|
-
text.setText(formatIncomingCallsCall(args as { path?: unknown; line?: unknown; character?: unknown }, theme));
|
|
543
|
-
return text;
|
|
544
|
-
},
|
|
545
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
546
|
-
if (isPartial) return new Text(theme.fg("warning", "Searching for callers..."), 0, 0);
|
|
547
|
-
if (context.isError) {
|
|
548
|
-
const errorText = result.content
|
|
549
|
-
.filter((block) => block.type === "text")
|
|
550
|
-
.map((block) => block.text)
|
|
551
|
-
.join("\n");
|
|
552
|
-
return new Text(theme.fg("error", errorText || "incoming_calls failed"), 0, 0);
|
|
564
|
+
if (params.direction === "incoming") {
|
|
565
|
+
const { calls, provenance } = await codeIntelligenceOperations.incomingCalls(path, params.line, params.character);
|
|
566
|
+
const text =
|
|
567
|
+
calls.length === 0
|
|
568
|
+
? "No incoming calls found."
|
|
569
|
+
: calls.map((c) => `${c.from.kind} ${c.from.name} -- ${c.from.location.path}:${c.from.location.line}:${c.from.location.character}`).join("\n");
|
|
570
|
+
const details: CallHierarchyToolDetails = { direction: "incoming", calls, provenance };
|
|
571
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(provenance)}\n${text}` }], details };
|
|
553
572
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
description: "Find every function/method the function at an exact file position itself calls.",
|
|
565
|
-
promptSnippet: "Find every function a function itself calls",
|
|
566
|
-
promptGuidelines: ["Use outgoing_calls to see what a function calls internally, e.g. to trace a code path forward without opening every file by hand."],
|
|
567
|
-
parameters: Type.Object(positionParameters),
|
|
568
|
-
async execute(_toolCallId, params) {
|
|
569
|
-
const path = resolve(cwd, params.path);
|
|
570
|
-
const details = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
|
|
571
|
-
const text =
|
|
572
|
-
details.calls.length === 0
|
|
573
|
-
? "No outgoing calls found."
|
|
574
|
-
: details.calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
|
|
575
|
-
return { content: [{ type: "text", text: `${describeIntelligenceSource(details.provenance)}\n${text}` }], details };
|
|
573
|
+
if (params.direction === "outgoing") {
|
|
574
|
+
const { calls, provenance } = await codeIntelligenceOperations.outgoingCalls(path, params.line, params.character);
|
|
575
|
+
const text =
|
|
576
|
+
calls.length === 0
|
|
577
|
+
? "No outgoing calls found."
|
|
578
|
+
: calls.map((c) => `${c.to.kind} ${c.to.name} -- ${c.to.location.path}:${c.to.location.line}:${c.to.location.character}`).join("\n");
|
|
579
|
+
const details: CallHierarchyToolDetails = { direction: "outgoing", calls, provenance };
|
|
580
|
+
return { content: [{ type: "text", text: `${describeIntelligenceSource(provenance)}\n${text}` }], details };
|
|
581
|
+
}
|
|
582
|
+
throw new Error(`unknown call_hierarchy direction: ${String(params.direction)}`);
|
|
576
583
|
},
|
|
577
584
|
renderCall(args, theme, context) {
|
|
578
585
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
579
|
-
text.setText(
|
|
586
|
+
text.setText(formatCallHierarchyCall(args, theme));
|
|
580
587
|
return text;
|
|
581
588
|
},
|
|
582
589
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
583
|
-
if (isPartial) return new Text(theme.fg("warning", "
|
|
590
|
+
if (isPartial) return new Text(theme.fg("warning", "Resolving call hierarchy..."), 0, 0);
|
|
584
591
|
if (context.isError) {
|
|
585
592
|
const errorText = result.content
|
|
586
593
|
.filter((block) => block.type === "text")
|
|
587
594
|
.map((block) => block.text)
|
|
588
595
|
.join("\n");
|
|
589
|
-
return new Text(theme.fg("error", errorText || "
|
|
596
|
+
return new Text(theme.fg("error", errorText || "call_hierarchy failed"), 0, 0);
|
|
590
597
|
}
|
|
591
|
-
const details = result.details as
|
|
598
|
+
const details = result.details as CallHierarchyToolDetails | undefined;
|
|
592
599
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
593
|
-
text.setText(renderIntelligenceSource(
|
|
600
|
+
text.setText(renderIntelligenceSource(formatCallHierarchyResult(details, expanded, theme), details?.provenance, theme));
|
|
594
601
|
return text;
|
|
595
602
|
},
|
|
596
603
|
});
|
|
@@ -599,7 +606,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
599
606
|
name: "populate_symbol_graph",
|
|
600
607
|
label: "Populate Symbol Graph",
|
|
601
608
|
description:
|
|
602
|
-
"Walk a workspace's real call relationships into a persisted graph, so reachable_from can answer multi-hop questions (transitive callers, reachability) without chaining many find_references/
|
|
609
|
+
"Walk a workspace's real call relationships into a persisted graph, so reachable_from can answer multi-hop questions (transitive callers, reachability) without chaining many find_references/call_hierarchy calls by hand. Run this once before reachable_from.",
|
|
603
610
|
promptSnippet: "Populate a workspace's symbol graph for multi-hop queries",
|
|
604
611
|
promptGuidelines: [
|
|
605
612
|
"Run populate_symbol_graph once for a workspace before using reachable_from against it; an unpopulated workspace's graph is empty, not an error.",
|
|
@@ -622,7 +629,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
622
629
|
},
|
|
623
630
|
renderCall(args, theme, context) {
|
|
624
631
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
625
|
-
text.setText(formatPopulateSymbolGraphCall(args
|
|
632
|
+
text.setText(formatPopulateSymbolGraphCall(args, theme));
|
|
626
633
|
return text;
|
|
627
634
|
},
|
|
628
635
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -676,6 +683,125 @@ export default function (pi: ExtensionAPI) {
|
|
|
676
683
|
},
|
|
677
684
|
});
|
|
678
685
|
|
|
686
|
+
pi.registerTool({
|
|
687
|
+
name: "reference_based_rename",
|
|
688
|
+
label: "Reference-Based Rename",
|
|
689
|
+
description:
|
|
690
|
+
"Move/rename a file and rewrite every static import/export specifier the workspace's own populated symbol graph knows references it -- atomically, rolled back entirely on any failure. Non-LSP: uses find_references + a real parse of import/export declarations, not a language server's own rename. Refuses outright (touches nothing) unless the workspace's symbol graph is fully populated and current for the given bounds -- a partial rename that silently misses a reference is worse than refusing (Sourcegraph's CodeScaleBench finding). Does not follow dynamic import(expr)/require(expr) or any plain string reference to the file -- always check the returned caveats.",
|
|
691
|
+
promptSnippet: "Move a file and update every import that references it",
|
|
692
|
+
promptGuidelines: [
|
|
693
|
+
"Run populate_symbol_graph for this workspace first if cache_status/has_warm_index doesn't already show a fully cached (never partial) graph -- reference_based_rename refuses outright otherwise.",
|
|
694
|
+
"Always read the returned caveats: this never rewrites a dynamic import(expr)/require(expr) or a plain string reference to the old path, even if one exists.",
|
|
695
|
+
],
|
|
696
|
+
parameters: Type.Object({
|
|
697
|
+
fromPath: Type.String({ description: "Absolute or cwd-relative path to the file to move" }),
|
|
698
|
+
toPath: Type.String({ description: "Absolute or cwd-relative path for its new location" }),
|
|
699
|
+
maxFiles: Type.Number({ description: "Maximum number of source files the workspace's symbol graph must have scanned" }),
|
|
700
|
+
maxSymbolsPerFile: Type.Number({ description: "Maximum number of declarations per file the workspace's symbol graph must have processed" }),
|
|
701
|
+
}),
|
|
702
|
+
async execute(_toolCallId, params) {
|
|
703
|
+
const fromPath = resolve(cwd, params.fromPath);
|
|
704
|
+
const toPath = resolve(cwd, params.toPath);
|
|
705
|
+
const outcome = await referenceBasedRenameOperations.rename(fromPath, toPath, params.maxFiles, params.maxSymbolsPerFile);
|
|
706
|
+
const lines = [
|
|
707
|
+
`moved to ${outcome.movedTo}`,
|
|
708
|
+
outcome.filesUpdated.length === 0
|
|
709
|
+
? "no other files referenced it"
|
|
710
|
+
: `updated imports in ${outcome.filesUpdated.length} file(s): ${outcome.filesUpdated.join(", ")}`,
|
|
711
|
+
...outcome.caveats.map((caveat) => `caveat: ${caveat}`),
|
|
712
|
+
];
|
|
713
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { outcome } };
|
|
714
|
+
},
|
|
715
|
+
renderCall(args, theme, context) {
|
|
716
|
+
const fromPath = typeof args.fromPath === "string" ? args.fromPath : "";
|
|
717
|
+
const toPath = typeof args.toPath === "string" ? args.toPath : "";
|
|
718
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
719
|
+
text.setText(
|
|
720
|
+
`${theme.fg("toolTitle", theme.bold("reference_based_rename"))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
|
|
721
|
+
);
|
|
722
|
+
return text;
|
|
723
|
+
},
|
|
724
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
725
|
+
if (isPartial) return new Text(theme.fg("warning", "Renaming..."), 0, 0);
|
|
726
|
+
if (context.isError) {
|
|
727
|
+
const errorText = result.content
|
|
728
|
+
.filter((block) => block.type === "text")
|
|
729
|
+
.map((block) => block.text)
|
|
730
|
+
.join("\n");
|
|
731
|
+
return new Text(theme.fg("error", errorText || "reference_based_rename failed"), 0, 0);
|
|
732
|
+
}
|
|
733
|
+
const details = result.details as { outcome?: { movedTo: string; filesUpdated: readonly string[] } } | undefined;
|
|
734
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
735
|
+
text.setText(
|
|
736
|
+
details?.outcome
|
|
737
|
+
? `${theme.fg("success", "moved")} ${theme.fg("accent", details.outcome.movedTo)} ${theme.fg("dim", `(${details.outcome.filesUpdated.length} import(s) updated)`)}`
|
|
738
|
+
: theme.fg("success", "rename complete"),
|
|
739
|
+
);
|
|
740
|
+
return text;
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
interface RenameToolDetails {
|
|
745
|
+
prepared?: OperationOutputs["workspace.prepareRename"];
|
|
746
|
+
applied?: OperationOutputs["workspace.rename"];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
pi.registerTool({
|
|
750
|
+
name: "rename",
|
|
751
|
+
label: "Rename",
|
|
752
|
+
description:
|
|
753
|
+
"LSP-driven rename via the negotiated language server's own textDocument/prepareRename and textDocument/rename -- the semantic sibling of reference_based_rename, cross-file and identity-aware (resolves through re-exports/aliasing, not just static import specifiers), but only where the workspace's language server actually implements rename. prepare checks whether the symbol at a position can be renamed at all before committing; apply requests the rename and applies the server's own WorkspaceEdit atomically across every file it touches, rolled back entirely on any failure. Actions: prepare, apply.",
|
|
754
|
+
promptSnippet: "Rename a symbol everywhere it's used, via the language server",
|
|
755
|
+
promptGuidelines: [
|
|
756
|
+
"Call prepare first when unsure a position is renameable -- a null range means nothing to rename there, not an error.",
|
|
757
|
+
"apply fails outright if the negotiated server never advertised rename support -- reference_based_rename is the non-LSP fallback for that case.",
|
|
758
|
+
],
|
|
759
|
+
parameters: Type.Object({
|
|
760
|
+
action: Type.String({ description: "prepare | apply" }),
|
|
761
|
+
...positionParameters,
|
|
762
|
+
newName: Type.Optional(Type.String({ description: "Required for apply" })),
|
|
763
|
+
}),
|
|
764
|
+
async execute(_toolCallId, params): Promise<{ content: [{ type: "text"; text: string }]; details: RenameToolDetails }> {
|
|
765
|
+
const path = resolve(cwd, params.path);
|
|
766
|
+
if (params.action === "prepare") {
|
|
767
|
+
const prepared = await renameOperations.prepareRename(path, params.line, params.character);
|
|
768
|
+
const text = prepared.range
|
|
769
|
+
? `renameable${prepared.range.placeholder ? `: "${prepared.range.placeholder}"` : ""}`
|
|
770
|
+
: "nothing renameable at this position";
|
|
771
|
+
return { content: [{ type: "text", text }], details: { prepared } };
|
|
772
|
+
}
|
|
773
|
+
if (params.action === "apply") {
|
|
774
|
+
if (!params.newName) throw new Error("rename apply requires newName");
|
|
775
|
+
const applied = await renameOperations.rename(path, params.line, params.character, params.newName);
|
|
776
|
+
const text = `renamed to "${params.newName}" -- updated ${applied.touchedPaths.length} file(s): ${applied.touchedPaths.join(", ")}`;
|
|
777
|
+
return { content: [{ type: "text", text }], details: { applied } };
|
|
778
|
+
}
|
|
779
|
+
throw new Error(`unknown rename action "${params.action}" -- expected prepare or apply`);
|
|
780
|
+
},
|
|
781
|
+
renderCall(args, theme, context) {
|
|
782
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
783
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
784
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
785
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("rename"))} ${theme.fg("dim", action)} ${theme.fg("accent", path)}`);
|
|
786
|
+
return text;
|
|
787
|
+
},
|
|
788
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
789
|
+
if (isPartial) return new Text(theme.fg("warning", "Renaming..."), 0, 0);
|
|
790
|
+
if (context.isError) {
|
|
791
|
+
const errorText = result.content
|
|
792
|
+
.filter((block) => block.type === "text")
|
|
793
|
+
.map((block) => block.text)
|
|
794
|
+
.join("\n");
|
|
795
|
+
return new Text(theme.fg("error", errorText || "rename failed"), 0, 0);
|
|
796
|
+
}
|
|
797
|
+
const text = result.content
|
|
798
|
+
.filter((block) => block.type === "text")
|
|
799
|
+
.map((block) => block.text)
|
|
800
|
+
.join("\n");
|
|
801
|
+
return new Text(theme.fg("success", text), 0, 0);
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
|
|
679
805
|
const symbolAnnotationOperations = createLectorSymbolAnnotationOperations();
|
|
680
806
|
function resolveAnchorInputs(anchors: readonly { path: string; line: number; character: number }[]): AnnotationAnchorInput[] {
|
|
681
807
|
return anchors.map((anchor) => ({ path: resolve(cwd, anchor.path), line: anchor.line, character: anchor.character }));
|
|
@@ -685,20 +811,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
685
811
|
annotations?: readonly SymbolAnnotation[];
|
|
686
812
|
scrubbed?: boolean;
|
|
687
813
|
restored?: boolean;
|
|
814
|
+
contained?: boolean;
|
|
815
|
+
uncontained?: boolean;
|
|
688
816
|
}
|
|
689
817
|
|
|
690
818
|
pi.registerTool({
|
|
691
819
|
name: "symbol_annotations",
|
|
692
820
|
label: "Symbol Annotations",
|
|
693
821
|
description:
|
|
694
|
-
'Agent-authored narrative content anchored to one or more symbols in the workspace\'s persisted graph -- e.g. a "user story dataflow" note spanning every symbol touched end-to-end. Every anchor must resolve to a real, currently-known symbol (run populate_symbol_graph first). get/list live-check staleness against the current graph/workspace on every call and persist a correction before returning, so a returned status never disagrees with reality -- a stale annotation must be refreshed (re-authored and re-anchored) or scrubbed (soft-deleted, restorable) by an explicit decision; Lector never rewrites the narrative itself. Actions: create, get, list, refresh, scrub, restore.',
|
|
822
|
+
'Agent-authored narrative content anchored to one or more symbols in the workspace\'s persisted graph -- e.g. a "user story dataflow" note spanning every symbol touched end-to-end. Every anchor must resolve to a real, currently-known symbol (run populate_symbol_graph first). get/list/tree live-check staleness against the current graph/workspace on every call and persist a correction before returning, so a returned status never disagrees with reality -- a stale annotation must be refreshed (re-authored and re-anchored) or scrubbed (soft-deleted, restorable) by an explicit decision; Lector never rewrites the narrative itself. contain/uncontain build a reusable, nestable structure on top of plain annotations: a container (e.g. a "data flow") can contain other annotations -- including per-symbol notes shared by more than one container (DRY reuse) or another container one level deeper (nested data flows) -- without duplicating their content. tree reads a whole bounded subtree in one call. Actions: create, get, list, refresh, scrub, restore, contain, uncontain, tree.',
|
|
695
823
|
promptSnippet: "Attach, read, or invalidate narrative annotations on the symbol graph",
|
|
696
824
|
promptGuidelines: [
|
|
697
825
|
"Resolve real anchor positions first (find_symbols/document_symbols/go_to_definition) -- an anchor position must match populate_symbol_graph's own recorded position for that symbol, not just any occurrence of its name.",
|
|
698
826
|
"A stale annotation's body may no longer describe the code accurately -- read it, decide whether to refresh (re-author) or scrub (remove), never trust it as-is.",
|
|
827
|
+
"Prefer reusing an existing per-symbol annotation as a shared child of several containers over re-authoring the same explanation in each -- that reuse is the reason contain/uncontain exist.",
|
|
828
|
+
"contain/uncontain are idempotent (containing an already-contained child, or uncontaining an already-absent relationship, is a no-op, not an error) and reject a cycle up front rather than accepting one.",
|
|
699
829
|
],
|
|
700
830
|
parameters: Type.Object({
|
|
701
|
-
action: Type.String({ description: "create | get | list | refresh | scrub | restore" }),
|
|
831
|
+
action: Type.String({ description: "create | get | list | refresh | scrub | restore | contain | uncontain | tree" }),
|
|
702
832
|
path: Type.String({ description: "Absolute or cwd-relative path used to resolve which workspace this annotation belongs to" }),
|
|
703
833
|
id: Type.Optional(Type.String({ description: "Annotation id -- required for get/refresh/scrub/restore" })),
|
|
704
834
|
subtype: Type.Optional(Type.String({ description: 'Free-form kind, e.g. "user-story-dataflow" or "comment" -- required for create/refresh' })),
|
|
@@ -716,7 +846,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
716
846
|
),
|
|
717
847
|
listStatus: Type.Optional(Type.String({ description: "fresh | stale | scrubbed -- for list; defaults to excluding scrubbed" })),
|
|
718
848
|
listSubtype: Type.Optional(Type.String({ description: "For list: filter by subtype" })),
|
|
849
|
+
listQuery: Type.Optional(
|
|
850
|
+
Type.String({ description: "For list: case-insensitive substring match against title or body -- the near-term free-text search over annotations" }),
|
|
851
|
+
),
|
|
719
852
|
maxResults: Type.Optional(Type.Number({ description: "For list: bounds the number of results" })),
|
|
853
|
+
parentId: Type.Optional(Type.String({ description: "The containing annotation's id -- required for contain/uncontain" })),
|
|
854
|
+
childId: Type.Optional(Type.String({ description: "The contained annotation's id -- required for contain/uncontain" })),
|
|
855
|
+
rootId: Type.Optional(Type.String({ description: "The subtree's root annotation id -- required for tree" })),
|
|
856
|
+
maxDepth: Type.Optional(Type.Number({ description: "Maximum containment hops from rootId to include -- required for tree" })),
|
|
720
857
|
}),
|
|
721
858
|
async execute(_toolCallId, params) {
|
|
722
859
|
const path = resolve(cwd, params.path);
|
|
@@ -736,7 +873,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
736
873
|
text = annotation ? formatAnnotationDetail(annotation) : `no annotation "${params.id}"`;
|
|
737
874
|
} else if (params.action === "list") {
|
|
738
875
|
const status = params.listStatus === "fresh" || params.listStatus === "stale" || params.listStatus === "scrubbed" ? params.listStatus : undefined;
|
|
739
|
-
const { annotations } = await symbolAnnotationOperations.list(path, {
|
|
876
|
+
const { annotations } = await symbolAnnotationOperations.list(path, {
|
|
877
|
+
subtype: params.listSubtype,
|
|
878
|
+
status,
|
|
879
|
+
maxResults: params.maxResults,
|
|
880
|
+
query: params.listQuery,
|
|
881
|
+
});
|
|
740
882
|
details.annotations = annotations;
|
|
741
883
|
text = annotations.length === 0 ? "no annotations" : annotations.map(formatAnnotationDetail).join("\n\n");
|
|
742
884
|
} else if (params.action === "refresh") {
|
|
@@ -763,6 +905,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
763
905
|
const { restored } = await symbolAnnotationOperations.restore(path, params.id);
|
|
764
906
|
details.restored = restored;
|
|
765
907
|
text = restored ? `restored ${params.id}` : `"${params.id}" was not scrubbed or does not exist`;
|
|
908
|
+
} else if (params.action === "contain") {
|
|
909
|
+
if (!params.parentId || !params.childId) throw new Error("symbol_annotations contain requires parentId and childId");
|
|
910
|
+
const { contained } = await symbolAnnotationOperations.contain(path, params.parentId, params.childId);
|
|
911
|
+
details.contained = contained;
|
|
912
|
+
text = `"${params.parentId}" now contains "${params.childId}"`;
|
|
913
|
+
} else if (params.action === "uncontain") {
|
|
914
|
+
if (!params.parentId || !params.childId) throw new Error("symbol_annotations uncontain requires parentId and childId");
|
|
915
|
+
const { uncontained } = await symbolAnnotationOperations.uncontain(path, params.parentId, params.childId);
|
|
916
|
+
details.uncontained = uncontained;
|
|
917
|
+
text = uncontained ? `"${params.parentId}" no longer contains "${params.childId}"` : `"${params.parentId}" did not contain "${params.childId}"`;
|
|
918
|
+
} else if (params.action === "tree") {
|
|
919
|
+
if (!params.rootId || params.maxDepth === undefined) throw new Error("symbol_annotations tree requires rootId and maxDepth");
|
|
920
|
+
const { annotations } = await symbolAnnotationOperations.tree(path, params.rootId, params.maxDepth);
|
|
921
|
+
details.annotations = annotations;
|
|
922
|
+
text = annotations.length === 0 ? `no annotation "${params.rootId}"` : annotations.map(formatAnnotationDetail).join("\n\n");
|
|
766
923
|
} else {
|
|
767
924
|
throw new Error(`unknown symbol_annotations action: ${String(params.action)}`);
|
|
768
925
|
}
|
|
@@ -770,7 +927,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
770
927
|
},
|
|
771
928
|
renderCall(args, theme, context) {
|
|
772
929
|
const action = typeof args.action === "string" ? args.action : "";
|
|
773
|
-
const id =
|
|
930
|
+
const id =
|
|
931
|
+
typeof args.id === "string"
|
|
932
|
+
? ` ${args.id}`
|
|
933
|
+
: typeof args.parentId === "string" && typeof args.childId === "string"
|
|
934
|
+
? ` ${args.parentId} -> ${args.childId}`
|
|
935
|
+
: typeof args.rootId === "string"
|
|
936
|
+
? ` ${args.rootId}`
|
|
937
|
+
: "";
|
|
774
938
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
775
939
|
text.setText(`${theme.fg("toolTitle", theme.bold("symbol_annotations"))} ${theme.fg("accent", action)}${theme.fg("dim", id)}`);
|
|
776
940
|
return text;
|
|
@@ -803,6 +967,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
803
967
|
text.setText(details.restored ? theme.fg("success", "restored") : theme.fg("muted", "not scrubbed or not found"));
|
|
804
968
|
return text;
|
|
805
969
|
}
|
|
970
|
+
if (details?.contained !== undefined) {
|
|
971
|
+
text.setText(theme.fg("success", "contained"));
|
|
972
|
+
return text;
|
|
973
|
+
}
|
|
974
|
+
if (details?.uncontained !== undefined) {
|
|
975
|
+
text.setText(details.uncontained ? theme.fg("success", "uncontained") : theme.fg("muted", "was not contained"));
|
|
976
|
+
return text;
|
|
977
|
+
}
|
|
806
978
|
text.setText(theme.fg("muted", "done"));
|
|
807
979
|
return text;
|
|
808
980
|
},
|
|
@@ -812,10 +984,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
812
984
|
name: "reachable_from",
|
|
813
985
|
label: "Reachable From",
|
|
814
986
|
description:
|
|
815
|
-
"Every symbol reachable from an exact file position by following the workspace's persisted call graph up to maxDepth hops -- transitive callers/reachability that would otherwise require chaining many find_references/
|
|
987
|
+
"Every symbol reachable from an exact file position by following the workspace's persisted call graph up to maxDepth hops -- transitive callers/reachability that would otherwise require chaining many find_references/call_hierarchy calls by hand. Requires populate_symbol_graph to have been run for this workspace first.",
|
|
816
988
|
promptSnippet: "Find symbols reachable from a position, up to N hops, via the persisted graph",
|
|
817
989
|
promptGuidelines: [
|
|
818
|
-
"Use reachable_from for multi-hop questions (does A eventually call C through B); use
|
|
990
|
+
"Use reachable_from for multi-hop questions (does A eventually call C through B); use call_hierarchy (direction=incoming/outgoing) for a single direct hop live against the language server.",
|
|
819
991
|
],
|
|
820
992
|
parameters: Type.Object({
|
|
821
993
|
...positionParameters,
|
|
@@ -837,7 +1009,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
837
1009
|
},
|
|
838
1010
|
renderCall(args, theme, context) {
|
|
839
1011
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
840
|
-
text.setText(formatReachableFromCall(args
|
|
1012
|
+
text.setText(formatReachableFromCall(args, theme));
|
|
841
1013
|
return text;
|
|
842
1014
|
},
|
|
843
1015
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -888,7 +1060,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
888
1060
|
},
|
|
889
1061
|
renderCall(args, theme, context) {
|
|
890
1062
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
891
|
-
text.setText(formatWorkspaceMapCall(args
|
|
1063
|
+
text.setText(formatWorkspaceMapCall(args, theme));
|
|
892
1064
|
return text;
|
|
893
1065
|
},
|
|
894
1066
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -909,111 +1081,61 @@ export default function (pi: ExtensionAPI) {
|
|
|
909
1081
|
|
|
910
1082
|
const gitOperations = createLectorGitOperations();
|
|
911
1083
|
pi.registerTool({
|
|
912
|
-
name: "
|
|
913
|
-
label: "Git
|
|
1084
|
+
name: "git",
|
|
1085
|
+
label: "Git",
|
|
914
1086
|
description:
|
|
915
|
-
"Working tree status for a real git repository
|
|
916
|
-
promptSnippet: "Show a repository's
|
|
1087
|
+
"Working tree status, recent commit log, and unified diff 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).",
|
|
1088
|
+
promptSnippet: "Show a repository's status, log, or diff",
|
|
1089
|
+
promptGuidelines: [
|
|
1090
|
+
"maxCount is required for action=log; maxBytes is required for action=diff -- every bounded query needs its bound stated explicitly, never defaulted silently.",
|
|
1091
|
+
],
|
|
917
1092
|
parameters: Type.Object({
|
|
1093
|
+
action: Type.String({ description: "status | log | diff" }),
|
|
918
1094
|
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
1095
|
+
maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return, most recent first -- required for action=log" })),
|
|
1096
|
+
ref: Type.Optional(Type.String({ description: "Ref to diff against; defaults to HEAD -- only used for action=diff" })),
|
|
1097
|
+
maxBytes: Type.Optional(Type.Number({ description: "Maximum diff size in bytes before truncating -- required for action=diff" })),
|
|
919
1098
|
}),
|
|
920
1099
|
async execute(_toolCallId, params) {
|
|
921
1100
|
const directory = resolve(cwd, params.directory);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
927
|
-
text.setText(formatGitStatusCall(args as { directory?: unknown }, theme));
|
|
928
|
-
return text;
|
|
929
|
-
},
|
|
930
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
931
|
-
if (isPartial) return new Text(theme.fg("warning", "Checking status..."), 0, 0);
|
|
932
|
-
if (context.isError) {
|
|
933
|
-
const errorText = result.content
|
|
934
|
-
.filter((block) => block.type === "text")
|
|
935
|
-
.map((block) => block.text)
|
|
936
|
-
.join("\n");
|
|
937
|
-
return new Text(theme.fg("error", errorText || "git_status failed"), 0, 0);
|
|
1101
|
+
if (params.action === "status") {
|
|
1102
|
+
const summary = await gitOperations.status(directory);
|
|
1103
|
+
const details: GitToolDetails = { action: "status", summary };
|
|
1104
|
+
return { content: [{ type: "text", text: JSON.stringify(summary) }], details };
|
|
938
1105
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
pi.registerTool({
|
|
947
|
-
name: "git_log",
|
|
948
|
-
label: "Git Log",
|
|
949
|
-
description:
|
|
950
|
-
"Recent commits for a real git repository, most recent first, bounded to maxCount. Fails clearly if `directory` is not inside a git repository.",
|
|
951
|
-
promptSnippet: "List a repository's recent commits",
|
|
952
|
-
parameters: Type.Object({
|
|
953
|
-
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
954
|
-
maxCount: Type.Number({ description: "Maximum number of commits to return, most recent first" }),
|
|
955
|
-
}),
|
|
956
|
-
async execute(_toolCallId, params) {
|
|
957
|
-
const directory = resolve(cwd, params.directory);
|
|
958
|
-
const entries = await gitOperations.log(directory, params.maxCount);
|
|
959
|
-
const text =
|
|
960
|
-
entries.length === 0 ? "No commits found." : entries.map((e) => `${e.sha.slice(0, 8)} ${e.authoredAt} ${e.authorName} -- ${e.message}`).join("\n");
|
|
961
|
-
return { content: [{ type: "text", text }], details: { entries } };
|
|
962
|
-
},
|
|
963
|
-
renderCall(args, theme, context) {
|
|
964
|
-
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
965
|
-
text.setText(formatGitLogCall(args as { directory?: unknown; maxCount?: unknown }, theme));
|
|
966
|
-
return text;
|
|
967
|
-
},
|
|
968
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
969
|
-
if (isPartial) return new Text(theme.fg("warning", "Reading log..."), 0, 0);
|
|
970
|
-
if (context.isError) {
|
|
971
|
-
const errorText = result.content
|
|
972
|
-
.filter((block) => block.type === "text")
|
|
973
|
-
.map((block) => block.text)
|
|
974
|
-
.join("\n");
|
|
975
|
-
return new Text(theme.fg("error", errorText || "git_log failed"), 0, 0);
|
|
1106
|
+
if (params.action === "log") {
|
|
1107
|
+
if (params.maxCount === undefined) throw new Error("git action=log requires maxCount");
|
|
1108
|
+
const entries = await gitOperations.log(directory, params.maxCount);
|
|
1109
|
+
const text =
|
|
1110
|
+
entries.length === 0 ? "No commits found." : entries.map((e) => `${e.sha.slice(0, 8)} ${e.authoredAt} ${e.authorName} -- ${e.message}`).join("\n");
|
|
1111
|
+
const details: GitToolDetails = { action: "log", entries };
|
|
1112
|
+
return { content: [{ type: "text", text }], details };
|
|
976
1113
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
pi.registerTool({
|
|
985
|
-
name: "git_diff",
|
|
986
|
-
label: "Git Diff",
|
|
987
|
-
description:
|
|
988
|
-
"Unified diff of the working tree against `ref` (defaults to HEAD) for a real git repository, bounded to maxBytes. Fails clearly if `directory` is not inside a git repository.",
|
|
989
|
-
promptSnippet: "Show a repository's working tree diff",
|
|
990
|
-
parameters: Type.Object({
|
|
991
|
-
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
992
|
-
ref: Type.Optional(Type.String({ description: "Ref to diff against; defaults to HEAD" })),
|
|
993
|
-
maxBytes: Type.Number({ description: "Maximum diff size in bytes before truncating" }),
|
|
994
|
-
}),
|
|
995
|
-
async execute(_toolCallId, params) {
|
|
996
|
-
const directory = resolve(cwd, params.directory);
|
|
997
|
-
const result = await gitOperations.diff(directory, params.ref, params.maxBytes);
|
|
998
|
-
return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details: { result } };
|
|
1114
|
+
if (params.action === "diff") {
|
|
1115
|
+
if (params.maxBytes === undefined) throw new Error("git action=diff requires maxBytes");
|
|
1116
|
+
const result = await gitOperations.diff(directory, params.ref, params.maxBytes);
|
|
1117
|
+
const details: GitToolDetails = { action: "diff", result };
|
|
1118
|
+
return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details };
|
|
1119
|
+
}
|
|
1120
|
+
throw new Error(`unknown git action: ${String(params.action)}`);
|
|
999
1121
|
},
|
|
1000
1122
|
renderCall(args, theme, context) {
|
|
1001
1123
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1002
|
-
text.setText(
|
|
1124
|
+
text.setText(formatGitCall(args, theme));
|
|
1003
1125
|
return text;
|
|
1004
1126
|
},
|
|
1005
1127
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1006
|
-
if (isPartial) return new Text(theme.fg("warning", "
|
|
1128
|
+
if (isPartial) return new Text(theme.fg("warning", "Running git..."), 0, 0);
|
|
1007
1129
|
if (context.isError) {
|
|
1008
1130
|
const errorText = result.content
|
|
1009
1131
|
.filter((block) => block.type === "text")
|
|
1010
1132
|
.map((block) => block.text)
|
|
1011
1133
|
.join("\n");
|
|
1012
|
-
return new Text(theme.fg("error", errorText || "
|
|
1134
|
+
return new Text(theme.fg("error", errorText || "git failed"), 0, 0);
|
|
1013
1135
|
}
|
|
1014
|
-
const details = result.details as
|
|
1136
|
+
const details = result.details as GitToolDetails | undefined;
|
|
1015
1137
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1016
|
-
text.setText(
|
|
1138
|
+
text.setText(formatGitResult(details, expanded, theme));
|
|
1017
1139
|
return text;
|
|
1018
1140
|
},
|
|
1019
1141
|
});
|
|
@@ -1040,7 +1162,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1040
1162
|
},
|
|
1041
1163
|
renderCall(args, theme, context) {
|
|
1042
1164
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1043
|
-
text.setText(formatSearchCall(args
|
|
1165
|
+
text.setText(formatSearchCall(args, theme));
|
|
1044
1166
|
return text;
|
|
1045
1167
|
},
|
|
1046
1168
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1086,7 +1208,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1086
1208
|
},
|
|
1087
1209
|
renderCall(args, theme, context) {
|
|
1088
1210
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1089
|
-
text.setText(formatFindFilesCall(args
|
|
1211
|
+
text.setText(formatFindFilesCall(args, theme));
|
|
1090
1212
|
return text;
|
|
1091
1213
|
},
|
|
1092
1214
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1151,12 +1273,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1151
1273
|
// The Pi tool schema can only express plain strings for hash fields (TypeBox has no
|
|
1152
1274
|
// concept of Lector's branded LineHash) -- the daemon's own domain validation is the
|
|
1153
1275
|
// real runtime check regardless of what TypeScript sees at this call site.
|
|
1276
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1154
1277
|
const result = await lineEditOperations.lineEdit(absolutePath, params.edits as unknown as LineEdit[]);
|
|
1155
1278
|
return { content: [{ type: "text", text: `${result.path}: ${result.previousHash} -> ${result.newHash}` }], details: { result } };
|
|
1156
1279
|
},
|
|
1157
1280
|
renderCall(args, theme, context) {
|
|
1158
1281
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1159
|
-
text.setText(formatLineEditCall(args
|
|
1282
|
+
text.setText(formatLineEditCall(args, theme));
|
|
1160
1283
|
return text;
|
|
1161
1284
|
},
|
|
1162
1285
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1197,12 +1320,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1197
1320
|
const absolutePath = resolve(cwd, params.path);
|
|
1198
1321
|
// TypeBox has no concept of Lector's branded ContentHash -- the daemon's own domain
|
|
1199
1322
|
// validation is the real runtime check regardless of what TypeScript sees here.
|
|
1323
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
1200
1324
|
const result = await applyPatchOperations.applyPatch(absolutePath, params.expectedHash as ContentHash, params.patchText);
|
|
1201
1325
|
return { content: [{ type: "text", text: `${result.path}: ${result.previousHash ?? "(new)"} -> ${result.newHash}` }], details: { result } };
|
|
1202
1326
|
},
|
|
1203
1327
|
renderCall(args, theme, context) {
|
|
1204
1328
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1205
|
-
text.setText(formatApplyPatchCall(args
|
|
1329
|
+
text.setText(formatApplyPatchCall(args, theme));
|
|
1206
1330
|
return text;
|
|
1207
1331
|
},
|
|
1208
1332
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1221,6 +1345,67 @@ export default function (pi: ExtensionAPI) {
|
|
|
1221
1345
|
},
|
|
1222
1346
|
});
|
|
1223
1347
|
|
|
1348
|
+
type MutationHistoryToolDetails =
|
|
1349
|
+
| { readonly action: "list"; readonly entries: readonly MutationHistoryEntry[] }
|
|
1350
|
+
| { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } };
|
|
1351
|
+
|
|
1352
|
+
const mutationHistoryOperations = createMutationHistoryOperations();
|
|
1353
|
+
pi.registerTool({
|
|
1354
|
+
name: "mutation_history",
|
|
1355
|
+
label: "Mutation History",
|
|
1356
|
+
description:
|
|
1357
|
+
"List or revert a file's recorded edit history. Every successful edit/line_edit/apply_patch is recorded (newest first, bounded, not durable across a daemon restart). Reverting restores the file to its exact content immediately before that entry's own mutation, guarded the same way every Lector write is -- refuses if the file changed since, rather than silently clobbering a newer change. A revert is itself a real, further-revertible mutation -- reverting a revert works.",
|
|
1358
|
+
promptSnippet: "List or revert a file's recorded edit history",
|
|
1359
|
+
promptGuidelines: [
|
|
1360
|
+
"list first to find the entry id you want, then revert -- an id from a different file's history, or one already evicted by the bounded history, fails closed rather than guessing.",
|
|
1361
|
+
],
|
|
1362
|
+
parameters: Type.Object({
|
|
1363
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("revert")]),
|
|
1364
|
+
path: Type.String({ description: "Absolute or workspace-relative path to the file" }),
|
|
1365
|
+
maxResults: Type.Optional(Type.Number({ description: "Required for action=list -- maximum entries to return, newest first" })),
|
|
1366
|
+
entryId: Type.Optional(Type.String({ description: "Required for action=revert -- an id returned by a prior action=list" })),
|
|
1367
|
+
}),
|
|
1368
|
+
async execute(_toolCallId, params): Promise<AgentToolResult<MutationHistoryToolDetails>> {
|
|
1369
|
+
const absolutePath = resolve(cwd, params.path);
|
|
1370
|
+
if (params.action === "list") {
|
|
1371
|
+
if (params.maxResults === undefined) throw new Error("mutation_history action=list requires maxResults");
|
|
1372
|
+
const entries = await mutationHistoryOperations.list(absolutePath, params.maxResults);
|
|
1373
|
+
const text =
|
|
1374
|
+
entries.length === 0
|
|
1375
|
+
? "no recorded mutation history for this path"
|
|
1376
|
+
: entries.map((entry) => `${entry.id} ${new Date(entry.timestamp).toISOString()} ${entry.operation}`).join("\n");
|
|
1377
|
+
return { content: [{ type: "text", text }], details: { action: "list", entries } };
|
|
1378
|
+
}
|
|
1379
|
+
if (params.entryId === undefined) throw new Error("mutation_history action=revert requires entryId");
|
|
1380
|
+
const reverted = await mutationHistoryOperations.revert(absolutePath, params.entryId);
|
|
1381
|
+
return {
|
|
1382
|
+
content: [{ type: "text", text: `${reverted.path} reverted -> ${reverted.newHash ?? "(deleted)"}` }],
|
|
1383
|
+
details: { action: "revert", reverted },
|
|
1384
|
+
};
|
|
1385
|
+
},
|
|
1386
|
+
renderCall(args, theme, context) {
|
|
1387
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
1388
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
1389
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1390
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("mutation_history"))} ${theme.fg("accent", action)} ${theme.fg("dim", path)}`);
|
|
1391
|
+
return text;
|
|
1392
|
+
},
|
|
1393
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
1394
|
+
if (isPartial) return new Text(theme.fg("warning", "Working on mutation history..."), 0, 0);
|
|
1395
|
+
if (context.isError) {
|
|
1396
|
+
const errorText = result.content
|
|
1397
|
+
.filter((block) => block.type === "text")
|
|
1398
|
+
.map((block) => block.text)
|
|
1399
|
+
.join("\n");
|
|
1400
|
+
return new Text(theme.fg("error", errorText || "mutation_history failed"), 0, 0);
|
|
1401
|
+
}
|
|
1402
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1403
|
+
const textBlock = result.content.find((block) => block.type === "text");
|
|
1404
|
+
text.setText(theme.fg("success", textBlock && "text" in textBlock ? textBlock.text : "done"));
|
|
1405
|
+
return text;
|
|
1406
|
+
},
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1224
1409
|
const packageSourceOperations = createLectorPackageSourceOperations();
|
|
1225
1410
|
pi.registerTool({
|
|
1226
1411
|
name: "package_source",
|
|
@@ -1241,7 +1426,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1241
1426
|
},
|
|
1242
1427
|
renderCall(args, theme, context) {
|
|
1243
1428
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1244
|
-
text.setText(formatPackageSourceCall(args
|
|
1429
|
+
text.setText(formatPackageSourceCall(args, theme));
|
|
1245
1430
|
return text;
|
|
1246
1431
|
},
|
|
1247
1432
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1261,6 +1446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1261
1446
|
});
|
|
1262
1447
|
|
|
1263
1448
|
const repoFetchOperations = createLectorRepoFetchOperations();
|
|
1449
|
+
const repoCacheListOperations = createRepoCacheListOperations();
|
|
1264
1450
|
pi.registerTool({
|
|
1265
1451
|
name: "repo_fetch",
|
|
1266
1452
|
label: "Repo Fetch",
|
|
@@ -1279,7 +1465,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1279
1465
|
},
|
|
1280
1466
|
renderCall(args, theme, context) {
|
|
1281
1467
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1282
|
-
text.setText(formatRepoFetchCall(args
|
|
1468
|
+
text.setText(formatRepoFetchCall(args, theme));
|
|
1283
1469
|
return text;
|
|
1284
1470
|
},
|
|
1285
1471
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1298,6 +1484,53 @@ export default function (pi: ExtensionAPI) {
|
|
|
1298
1484
|
},
|
|
1299
1485
|
});
|
|
1300
1486
|
|
|
1487
|
+
pi.registerTool({
|
|
1488
|
+
name: "repo_cache_list",
|
|
1489
|
+
label: "Repo Cache List",
|
|
1490
|
+
description:
|
|
1491
|
+
"Lists and queries repo_fetch's own on-disk cache -- no network call, no cache mutation. Filter by any combination of host/owner/repo/ref (exact match) and text (case-insensitive substring across host/owner/repo/refs). Each entry reports whether it's currently a registered workspace (usable directly by other tools) or just present on disk. Bounded and paginated via cursor.",
|
|
1492
|
+
promptSnippet: "List or search previously-fetched external repositories",
|
|
1493
|
+
parameters: Type.Object({
|
|
1494
|
+
text: Type.Optional(Type.String({ description: "Case-insensitive substring match across host/owner/repo/refs" })),
|
|
1495
|
+
host: Type.Optional(Type.String({ description: "Exact host match, e.g. github.com" })),
|
|
1496
|
+
owner: Type.Optional(Type.String()),
|
|
1497
|
+
repo: Type.Optional(Type.String()),
|
|
1498
|
+
ref: Type.Optional(Type.String({ description: "Matches either the requested or the resolved ref" })),
|
|
1499
|
+
maxResults: Type.Number({ description: "Maximum entries to return in this page" }),
|
|
1500
|
+
cursor: Type.Optional(Type.String({ description: "Opaque cursor from a prior call's nextCursor, to fetch the next page" })),
|
|
1501
|
+
}),
|
|
1502
|
+
async execute(_toolCallId, params) {
|
|
1503
|
+
const page = await repoCacheListOperations.list(
|
|
1504
|
+
{ text: params.text, host: params.host, owner: params.owner, repo: params.repo, ref: params.ref },
|
|
1505
|
+
params.maxResults,
|
|
1506
|
+
params.cursor,
|
|
1507
|
+
);
|
|
1508
|
+
return { content: [{ type: "text", text: JSON.stringify(page) }], details: { page } };
|
|
1509
|
+
},
|
|
1510
|
+
renderCall(_args, theme, context) {
|
|
1511
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1512
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("repo_cache_list"))}`);
|
|
1513
|
+
return text;
|
|
1514
|
+
},
|
|
1515
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
1516
|
+
if (isPartial) return new Text(theme.fg("warning", "Listing cached repositories..."), 0, 0);
|
|
1517
|
+
if (context.isError) {
|
|
1518
|
+
const errorText = result.content
|
|
1519
|
+
.filter((block) => block.type === "text")
|
|
1520
|
+
.map((block) => block.text)
|
|
1521
|
+
.join("\n");
|
|
1522
|
+
return new Text(theme.fg("error", errorText || "repo_cache_list failed"), 0, 0);
|
|
1523
|
+
}
|
|
1524
|
+
const details = result.details as
|
|
1525
|
+
| { page?: { entries: readonly { host: string; owner: string; repo: string }[]; nextCursor: string | null } }
|
|
1526
|
+
| undefined;
|
|
1527
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1528
|
+
const count = details?.page?.entries.length ?? 0;
|
|
1529
|
+
text.setText(count === 0 ? theme.fg("dim", "no cached repositories") : theme.fg("success", `${count} cached repositor${count === 1 ? "y" : "ies"}`));
|
|
1530
|
+
return text;
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1301
1534
|
const crossWorkspaceSearchOperations = createLectorCrossWorkspaceSearchOperations();
|
|
1302
1535
|
pi.registerTool({
|
|
1303
1536
|
name: "find_symbols_across_projects",
|
|
@@ -1317,7 +1550,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1317
1550
|
},
|
|
1318
1551
|
renderCall(args, theme, context) {
|
|
1319
1552
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1320
|
-
text.setText(formatCrossWorkspaceCall(args
|
|
1553
|
+
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
1321
1554
|
return text;
|
|
1322
1555
|
},
|
|
1323
1556
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1356,7 +1589,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1356
1589
|
},
|
|
1357
1590
|
renderCall(args, theme, context) {
|
|
1358
1591
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1359
|
-
text.setText(formatCrossWorkspaceCall(args
|
|
1592
|
+
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
1360
1593
|
return text;
|
|
1361
1594
|
},
|
|
1362
1595
|
renderResult(result, { expanded, isPartial }, theme, context) {
|