@shanepadgett/tau-agent 0.25.1 → 0.27.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/docs/subagents.md +1 -1
- package/extensions/appshot/README.md +1 -1
- package/extensions/appshot/index.ts +5 -11
- package/extensions/context-pruning/README.md +2 -2
- package/extensions/context-pruning/index.ts +58 -57
- package/extensions/context-pruning/render.ts +51 -39
- package/extensions/context-pruning/settings.ts +4 -5
- package/extensions/explore/README.md +18 -10
- package/extensions/explore/ast-guidance.ts +67 -0
- package/extensions/explore/ast-languages.ts +61 -0
- package/extensions/explore/ast-tools.ts +1597 -94
- package/extensions/explore/ast-worker.ts +768 -60
- package/extensions/explore/index.ts +62 -6
- package/extensions/explore/orientation-state.ts +280 -0
- package/extensions/explore/read-stats-panel.ts +23 -1
- package/extensions/explore/read-stats.ts +21 -3
- package/extensions/explore/read.ts +58 -21
- package/extensions/explore/settings.ts +46 -0
- package/extensions/explore/traverse.ts +15 -4
- package/extensions/patch/executor.ts +95 -1
- package/extensions/patch/index.ts +1 -0
- package/extensions/soul/prompt.ts +1 -3
- package/extensions/subagent/agents/review.md +76 -25
- package/extensions/subagent/agents/scout.md +118 -22
- package/extensions/subagent/session-resource.ts +1 -1
- package/extensions/tau-help/help.md +3 -1
- package/native-bin/darwin-arm64/tau-ast +0 -0
- package/package.json +2 -3
- package/schemas/tau.schema.json +44 -4
- package/shared/bounded-text-result.ts +304 -0
- package/shared/events.ts +2 -1
- package/shared/temporary-output-store.ts +275 -0
|
@@ -1,40 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
DEFAULT_MAX_LINES,
|
|
4
|
-
defineTool,
|
|
5
|
-
formatSize,
|
|
6
|
-
keyHint,
|
|
7
|
-
truncateHead,
|
|
8
|
-
type Theme,
|
|
9
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { defineTool, formatSize, keyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
3
|
import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { realpath, stat } from "node:fs/promises";
|
|
6
|
+
import { basename, resolve } from "node:path";
|
|
13
7
|
import { Type } from "typebox";
|
|
8
|
+
import {
|
|
9
|
+
BoundedTextResultBuilder,
|
|
10
|
+
truncateBoundedHead,
|
|
11
|
+
type BoundedTextOverflowDetails,
|
|
12
|
+
} from "../../shared/bounded-text-result.ts";
|
|
13
|
+
import type { TemporaryOutputStore } from "../../shared/temporary-output-store.ts";
|
|
14
14
|
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
15
|
+
import { applyExactSourceMutations, type ApplyPatchSummary } from "../patch/executor.ts";
|
|
16
|
+
import {
|
|
17
|
+
AST_LANGUAGE_REGISTRY,
|
|
18
|
+
astLanguageForPath,
|
|
19
|
+
formatAstLanguageLabels,
|
|
20
|
+
requireAstLanguageForPath,
|
|
21
|
+
type AstLanguage,
|
|
22
|
+
} from "./ast-languages.ts";
|
|
23
|
+
import type { OrientationState } from "./orientation-state.ts";
|
|
15
24
|
import { formatPathForDisplay, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
import {
|
|
26
|
+
AstWorkerError,
|
|
27
|
+
type ApiCandidate,
|
|
28
|
+
type ApiDeclarationKind,
|
|
29
|
+
type ApiDiscoveryResult,
|
|
30
|
+
type ApiQuery,
|
|
31
|
+
type ApiSurfaceFilter,
|
|
32
|
+
type AstSearchResult,
|
|
33
|
+
type AstClient,
|
|
34
|
+
type EditOperation,
|
|
35
|
+
type EditPlanResult,
|
|
36
|
+
type OutlineEntry,
|
|
37
|
+
type OutlineFileResult,
|
|
38
|
+
type SourceRange,
|
|
39
|
+
type OutlineTarget,
|
|
40
|
+
type OutlineTargetResult,
|
|
41
|
+
type RecursiveOutlineSummary,
|
|
42
|
+
type RelationshipOperation,
|
|
43
|
+
type RelationshipResult,
|
|
44
|
+
type SymbolBatchResult,
|
|
45
|
+
type SymbolView,
|
|
24
46
|
} from "./ast-worker.ts";
|
|
25
47
|
|
|
48
|
+
const supportedLanguageLabels = formatAstLanguageLabels(AST_LANGUAGE_REGISTRY, "or");
|
|
49
|
+
|
|
26
50
|
const outlineParams = Type.Object(
|
|
27
51
|
{
|
|
28
52
|
path: Type.String({
|
|
29
|
-
description:
|
|
53
|
+
description: `${supportedLanguageLabels} source file or directory`,
|
|
30
54
|
}),
|
|
31
55
|
includePrivate: Type.Optional(Type.Boolean({ description: "Include private declarations and members" })),
|
|
56
|
+
includeDocs: Type.Optional(
|
|
57
|
+
Type.Boolean({ description: "Include attached documentation comments in declaration signatures" }),
|
|
58
|
+
),
|
|
32
59
|
names: Type.Optional(
|
|
33
60
|
Type.Array(Type.String(), {
|
|
34
61
|
minItems: 1,
|
|
35
62
|
description: "Exact top-level or member declaration names",
|
|
36
63
|
}),
|
|
37
64
|
),
|
|
65
|
+
recursive: Type.Optional(
|
|
66
|
+
Type.Boolean({ description: "Recursively outline every supported source file below a directory" }),
|
|
67
|
+
),
|
|
38
68
|
},
|
|
39
69
|
{ additionalProperties: false },
|
|
40
70
|
);
|
|
@@ -44,21 +74,178 @@ const symbolParams = Type.Object(
|
|
|
44
74
|
minItems: 1,
|
|
45
75
|
description: "Numeric locators shown in parentheses by outline",
|
|
46
76
|
}),
|
|
77
|
+
view: StringEnum(["signature", "signatureWithDocs", "declaration", "declarationWithImports"] as const, {
|
|
78
|
+
description: `Source view to retrieve: signature omits docs and bodies; signatureWithDocs adds attached docs; declaration returns exact source; declarationWithImports adds required imports. ${supportedLanguageLabels} support selective views`,
|
|
79
|
+
}),
|
|
47
80
|
contextLines: Type.Optional(
|
|
48
81
|
Type.Integer({ minimum: 0, description: "Lines of source context before and after each declaration" }),
|
|
49
82
|
),
|
|
50
83
|
},
|
|
51
84
|
{ additionalProperties: false },
|
|
52
85
|
);
|
|
86
|
+
const apiDeclarationKinds = [
|
|
87
|
+
"module",
|
|
88
|
+
"namespace",
|
|
89
|
+
"package",
|
|
90
|
+
"class",
|
|
91
|
+
"method",
|
|
92
|
+
"property",
|
|
93
|
+
"field",
|
|
94
|
+
"constructor",
|
|
95
|
+
"enum",
|
|
96
|
+
"interface",
|
|
97
|
+
"function",
|
|
98
|
+
"variable",
|
|
99
|
+
"constant",
|
|
100
|
+
"object",
|
|
101
|
+
"enumMember",
|
|
102
|
+
"struct",
|
|
103
|
+
"event",
|
|
104
|
+
"operator",
|
|
105
|
+
"typeParameter",
|
|
106
|
+
"heading",
|
|
107
|
+
] as const satisfies readonly ApiDeclarationKind[];
|
|
108
|
+
const apiDiscoverParams = Type.Object(
|
|
109
|
+
{
|
|
110
|
+
path: Type.String({ description: "Repository, package, or subtree directory" }),
|
|
111
|
+
query: Type.Union([
|
|
112
|
+
Type.Object(
|
|
113
|
+
{ kind: Type.Literal("exactName"), name: Type.String({ minLength: 1 }) },
|
|
114
|
+
{ additionalProperties: false },
|
|
115
|
+
),
|
|
116
|
+
Type.Object(
|
|
117
|
+
{ kind: Type.Literal("prefixName"), name: Type.String({ minLength: 1 }) },
|
|
118
|
+
{ additionalProperties: false },
|
|
119
|
+
),
|
|
120
|
+
Type.Object(
|
|
121
|
+
{ kind: Type.Literal("substringName"), name: Type.String({ minLength: 1 }) },
|
|
122
|
+
{ additionalProperties: false },
|
|
123
|
+
),
|
|
124
|
+
Type.Object(
|
|
125
|
+
{
|
|
126
|
+
kind: Type.Literal("fuzzyName"),
|
|
127
|
+
name: Type.String({ minLength: 1 }),
|
|
128
|
+
maxCandidates: Type.Integer({ minimum: 1, maximum: 10_000 }),
|
|
129
|
+
maxWork: Type.Integer({ minimum: 1, maximum: 1_000_000 }),
|
|
130
|
+
},
|
|
131
|
+
{ additionalProperties: false },
|
|
132
|
+
),
|
|
133
|
+
Type.Object(
|
|
134
|
+
{
|
|
135
|
+
kind: Type.Literal("declarationKind"),
|
|
136
|
+
declarationKind: StringEnum(apiDeclarationKinds),
|
|
137
|
+
},
|
|
138
|
+
{ additionalProperties: false },
|
|
139
|
+
),
|
|
140
|
+
Type.Object(
|
|
141
|
+
{
|
|
142
|
+
kind: Type.Literal("documentation"),
|
|
143
|
+
terms: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
144
|
+
maxCandidates: Type.Integer({ minimum: 1, maximum: 10_000 }),
|
|
145
|
+
maxWork: Type.Integer({ minimum: 1, maximum: 1_000_000 }),
|
|
146
|
+
},
|
|
147
|
+
{ additionalProperties: false },
|
|
148
|
+
),
|
|
149
|
+
]),
|
|
150
|
+
surface: StringEnum(["all", "public", "private", "sourceExport", "packageSurface"] as const, {
|
|
151
|
+
description: "Declaration or export surface to search",
|
|
152
|
+
}),
|
|
153
|
+
resultLimit: Type.Integer({ minimum: 1, maximum: 100 }),
|
|
154
|
+
},
|
|
155
|
+
{ additionalProperties: false },
|
|
156
|
+
);
|
|
157
|
+
const astSearchParams = Type.Object(
|
|
158
|
+
{
|
|
159
|
+
path: Type.String({ description: "Supported source file, repository, package, or subtree" }),
|
|
160
|
+
pattern: Type.String({ minLength: 1, maxLength: 16 * 1024, description: "ast-grep code pattern" }),
|
|
161
|
+
language: Type.Optional(
|
|
162
|
+
StringEnum(
|
|
163
|
+
["typeScript", "tsx", "odin", "go", "rust", "cSharp", "java", "kotlin", "swift", "markdown"] as const,
|
|
164
|
+
{ description: "Explicit pattern and target language; required for directory targets" },
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
resultLimit: Type.Integer({ minimum: 1, maximum: 100 }),
|
|
168
|
+
},
|
|
169
|
+
{ additionalProperties: false },
|
|
170
|
+
);
|
|
171
|
+
const relationshipParams = Type.Object(
|
|
172
|
+
{
|
|
173
|
+
path: Type.String({ description: "Repository, package, or subtree directory" }),
|
|
174
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
175
|
+
resultLimit: Type.Integer({ minimum: 1, maximum: 100 }),
|
|
176
|
+
},
|
|
177
|
+
{ additionalProperties: false },
|
|
178
|
+
);
|
|
179
|
+
const replaceDeclarationParams = Type.Object(
|
|
180
|
+
{
|
|
181
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
182
|
+
source: Type.String({
|
|
183
|
+
minLength: 1,
|
|
184
|
+
description: "One complete replacement declaration or Markdown heading section",
|
|
185
|
+
}),
|
|
186
|
+
},
|
|
187
|
+
{ additionalProperties: false },
|
|
188
|
+
);
|
|
189
|
+
const replaceBodyParams = Type.Object(
|
|
190
|
+
{
|
|
191
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
192
|
+
body: Type.String({
|
|
193
|
+
minLength: 1,
|
|
194
|
+
description: "Complete replacement for the adapter-provided code body or Markdown section content",
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
{ additionalProperties: false },
|
|
198
|
+
);
|
|
199
|
+
const insertDeclarationParams = Type.Object(
|
|
200
|
+
{
|
|
201
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
202
|
+
position: StringEnum(["before", "after"] as const),
|
|
203
|
+
source: Type.String({ minLength: 1, description: "One complete declaration to insert" }),
|
|
204
|
+
},
|
|
205
|
+
{ additionalProperties: false },
|
|
206
|
+
);
|
|
207
|
+
const renameDeclarationParams = Type.Object(
|
|
208
|
+
{
|
|
209
|
+
locator: Type.Integer({ minimum: 1, description: "Numeric declaration locator" }),
|
|
210
|
+
newName: Type.String({ minLength: 1, description: "One identifier in the target language" }),
|
|
211
|
+
scope: Type.Union([
|
|
212
|
+
Type.Object({ kind: Type.Literal("file") }, { additionalProperties: false }),
|
|
213
|
+
Type.Object(
|
|
214
|
+
{ kind: Type.Literal("repository"), path: Type.String({ minLength: 1 }) },
|
|
215
|
+
{ additionalProperties: false },
|
|
216
|
+
),
|
|
217
|
+
]),
|
|
218
|
+
includeInferred: Type.Boolean({
|
|
219
|
+
description: "Update inferred references as well as exact references",
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
{ additionalProperties: false },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
interface LocatorEditResult {
|
|
226
|
+
plan: EditPlanResult;
|
|
227
|
+
mutation: ApplyPatchSummary;
|
|
228
|
+
invalidatedLocatorIds: number[];
|
|
229
|
+
freshLocatorIds: number[];
|
|
230
|
+
verificationDiagnostics: string[];
|
|
231
|
+
}
|
|
53
232
|
|
|
54
233
|
interface AstToolDetails {
|
|
55
|
-
kind: "outline" | "symbol";
|
|
56
|
-
result:
|
|
234
|
+
kind: "outline" | "symbol" | "apiDiscover" | "astSearch" | "relationship" | "locatorEdit";
|
|
235
|
+
result:
|
|
236
|
+
| OutlineTargetResult
|
|
237
|
+
| SymbolBatchResult
|
|
238
|
+
| ApiDiscoveryResult
|
|
239
|
+
| AstSearchResult
|
|
240
|
+
| RelationshipResult
|
|
241
|
+
| LocatorEditResult
|
|
242
|
+
| { path: string; summary: RecursiveOutlineSummary; visibleFiles: string[] };
|
|
57
243
|
declarationCount: number;
|
|
58
244
|
sourceBytes: number;
|
|
59
245
|
returnedBytes: number;
|
|
60
246
|
avoidedBytes: number;
|
|
61
247
|
truncated: boolean;
|
|
248
|
+
overflow: BoundedTextOverflowDetails | undefined;
|
|
62
249
|
}
|
|
63
250
|
|
|
64
251
|
interface LocatorRecord {
|
|
@@ -67,11 +254,25 @@ interface LocatorRecord {
|
|
|
67
254
|
path: string;
|
|
68
255
|
name: string;
|
|
69
256
|
stale: boolean;
|
|
257
|
+
declarationRetrieved: boolean;
|
|
258
|
+
generation: number;
|
|
70
259
|
}
|
|
71
260
|
|
|
72
|
-
type OutlineArgs = {
|
|
261
|
+
type OutlineArgs = {
|
|
262
|
+
path: string;
|
|
263
|
+
includePrivate?: boolean;
|
|
264
|
+
includeDocs?: boolean;
|
|
265
|
+
names?: string[];
|
|
266
|
+
recursive?: boolean;
|
|
267
|
+
};
|
|
73
268
|
|
|
74
|
-
export function createAstTools(
|
|
269
|
+
export function createAstTools(
|
|
270
|
+
client: AstClient,
|
|
271
|
+
rowState: ToolRowStateStore,
|
|
272
|
+
temporaryOutput: TemporaryOutputStore,
|
|
273
|
+
orientation: OrientationState,
|
|
274
|
+
emitMutation: (toolCallId: string, cwd: string, summary: ApplyPatchSummary) => void,
|
|
275
|
+
) {
|
|
75
276
|
const locators = new Map<number, LocatorRecord>();
|
|
76
277
|
let nextLocator = 1;
|
|
77
278
|
|
|
@@ -81,14 +282,15 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
81
282
|
declarationCount: number,
|
|
82
283
|
kind: AstToolDetails["kind"],
|
|
83
284
|
result: OutlineTargetResult | SymbolBatchResult,
|
|
84
|
-
): { text: string; details: AstToolDetails } {
|
|
85
|
-
const truncation =
|
|
285
|
+
): { text: string; visibleText: string; details: AstToolDetails } {
|
|
286
|
+
const truncation = truncateBoundedHead(text);
|
|
86
287
|
const returned = truncation.truncated
|
|
87
288
|
? `${truncation.content}\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`
|
|
88
289
|
: truncation.content;
|
|
89
290
|
const returnedBytes = Buffer.byteLength(returned);
|
|
90
291
|
return {
|
|
91
292
|
text: returned,
|
|
293
|
+
visibleText: truncation.content,
|
|
92
294
|
details: {
|
|
93
295
|
kind,
|
|
94
296
|
result,
|
|
@@ -97,46 +299,376 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
97
299
|
returnedBytes,
|
|
98
300
|
avoidedBytes: Math.max(0, sourceBytes - returnedBytes),
|
|
99
301
|
truncated: truncation.truncated,
|
|
302
|
+
overflow: undefined,
|
|
100
303
|
},
|
|
101
304
|
};
|
|
102
305
|
}
|
|
103
306
|
|
|
104
307
|
function locator(entry: OutlineEntry, path: string): number {
|
|
308
|
+
if (!entry.locator) throw new Error(`Structural outline row ${entry.name} has no symbol locator`);
|
|
309
|
+
return registerLocator(entry.locator, path, entry.name);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function registerLocator(token: string, path: string, name: string): number {
|
|
105
313
|
const id = nextLocator++;
|
|
106
|
-
const record = {
|
|
314
|
+
const record = {
|
|
315
|
+
id,
|
|
316
|
+
token,
|
|
317
|
+
path: resolve(path),
|
|
318
|
+
name,
|
|
319
|
+
stale: false,
|
|
320
|
+
declarationRetrieved: false,
|
|
321
|
+
generation: client.getGeneration(),
|
|
322
|
+
};
|
|
107
323
|
locators.set(id, record);
|
|
108
324
|
return id;
|
|
109
325
|
}
|
|
110
326
|
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
327
|
+
function editableLocator(id: number): LocatorRecord {
|
|
328
|
+
const record = locators.get(id);
|
|
329
|
+
if (!record) throw new Error(`Unknown declaration locator: ${id}. Run outline or api_discover again.`);
|
|
330
|
+
if (record.stale || record.generation !== client.getGeneration()) {
|
|
331
|
+
throw new Error(`Declaration locator ${id} is stale. Run outline or api_discover again.`);
|
|
332
|
+
}
|
|
333
|
+
return record;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function renderApiCandidate(candidate: ApiCandidate, id: number, cwd: string): string {
|
|
337
|
+
const lines = [
|
|
338
|
+
`${formatPathForDisplay(candidate.definingFile, cwd)}:${displayLineRange(candidate.range)}(${id}): ${candidate.name} — ${candidate.symbolType}`,
|
|
339
|
+
...candidate.signature
|
|
340
|
+
.trim()
|
|
341
|
+
.split("\n")
|
|
342
|
+
.map((line) => ` ${line}`),
|
|
343
|
+
];
|
|
344
|
+
const surface: string[] = [];
|
|
345
|
+
if (candidate.visibility !== "public") surface.push(`visibility=${candidate.visibility}`);
|
|
346
|
+
if (candidate.sourceExport !== "yes") surface.push(`sourceExport=${candidate.sourceExport}`);
|
|
347
|
+
if (candidate.packageSurface !== "yes") surface.push(`packageSurface=${candidate.packageSurface}`);
|
|
348
|
+
if (candidate.internalOnly !== "no") surface.push(`internalOnly=${candidate.internalOnly}`);
|
|
349
|
+
if (surface.length > 0) lines.push(` surface: ${surface.join(", ")}`);
|
|
350
|
+
if (candidate.callerAccess) {
|
|
351
|
+
lines.push(
|
|
352
|
+
` caller: ${candidate.callerAccess.importStatement} use ${candidate.callerAccess.accessExpression}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (candidate.reExportChain.length > 1) lines.push(` re-exports: ${candidate.reExportChain.join(" -> ")}`);
|
|
356
|
+
if (candidate.provenance !== "exact" || candidate.certainty !== "certain" || candidate.certaintyReason) {
|
|
357
|
+
const resolution = [`provenance=${candidate.provenance}`, `parse=${candidate.certainty}`];
|
|
358
|
+
if (candidate.certaintyReason) resolution.push(candidate.certaintyReason);
|
|
359
|
+
lines.push(` resolution: ${resolution.join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
if (candidate.uncertainty) lines.push(` uncertainty: ${candidate.uncertainty}`);
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function renderEntry(
|
|
366
|
+
entry: OutlineEntry,
|
|
367
|
+
path: string,
|
|
368
|
+
language: AstLanguage,
|
|
369
|
+
indent: string,
|
|
370
|
+
signatureOverride?: string,
|
|
371
|
+
): string {
|
|
372
|
+
const lines = displayLineRange(entry.range);
|
|
373
|
+
let signature = (signatureOverride ?? entry.signature).trim();
|
|
374
|
+
if (
|
|
375
|
+
language === "typeScript" ||
|
|
376
|
+
language === "tsx" ||
|
|
377
|
+
language === "odin" ||
|
|
378
|
+
language === "go" ||
|
|
379
|
+
language === "rust" ||
|
|
380
|
+
language === "cSharp" ||
|
|
381
|
+
language === "java" ||
|
|
382
|
+
language === "kotlin" ||
|
|
383
|
+
language === "swift" ||
|
|
384
|
+
language === "markdown"
|
|
385
|
+
) {
|
|
386
|
+
const signatureLines = signature.split("\n");
|
|
387
|
+
const relativeNameLine = entry.nameRange.start.line - entry.range.start.line;
|
|
388
|
+
const keywordNameLine =
|
|
389
|
+
language === "go"
|
|
390
|
+
? signatureLines.findIndex(
|
|
391
|
+
(line) => /\b(?:func|type|const|var)\b/.test(line) && line.includes(entry.name),
|
|
392
|
+
)
|
|
393
|
+
: -1;
|
|
394
|
+
let inBlockComment = false;
|
|
395
|
+
let annotationDepth = 0;
|
|
396
|
+
let attributeDepth = 0;
|
|
397
|
+
const metadataLines = signatureLines.map((line) => {
|
|
398
|
+
const trimmed = line.trimStart();
|
|
399
|
+
if (inBlockComment) {
|
|
400
|
+
if (trimmed.includes("*/")) inBlockComment = false;
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
if (trimmed.startsWith("/*")) {
|
|
404
|
+
inBlockComment = !trimmed.includes("*/");
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
if (annotationDepth > 0) {
|
|
408
|
+
annotationDepth += (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (attributeDepth > 0) {
|
|
412
|
+
attributeDepth += (line.match(/\[/g) ?? []).length - (line.match(/\]/g) ?? []).length;
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
if (trimmed.startsWith("@") && !trimmed.startsWith(`@interface ${entry.name}`)) {
|
|
416
|
+
annotationDepth = (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
if (trimmed.startsWith("#[")) {
|
|
420
|
+
attributeDepth = (line.match(/\[/g) ?? []).length - (line.match(/\]/g) ?? []).length;
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
return trimmed.startsWith("//");
|
|
424
|
+
});
|
|
425
|
+
const declarationNameLine = signatureLines.findIndex(
|
|
426
|
+
(line, index) => !metadataLines[index] && line.includes(entry.name),
|
|
427
|
+
);
|
|
428
|
+
const nameLine =
|
|
429
|
+
relativeNameLine >= 0 && relativeNameLine === declarationNameLine
|
|
430
|
+
? relativeNameLine
|
|
431
|
+
: keywordNameLine >= 0
|
|
432
|
+
? keywordNameLine
|
|
433
|
+
: declarationNameLine;
|
|
434
|
+
const declarationLine = language === "markdown" ? Math.max(0, relativeNameLine) : nameLine >= 0 ? nameLine : 0;
|
|
435
|
+
const sourceLine = signatureLines[declarationLine] ?? entry.name;
|
|
436
|
+
const first =
|
|
437
|
+
language === "markdown" || sourceLine.includes(entry.name) ? sourceLine : `${entry.name} ${sourceLine}`;
|
|
438
|
+
const warning =
|
|
439
|
+
entry.certainty === "certain"
|
|
440
|
+
? ""
|
|
441
|
+
: ` [${entry.certainty}${entry.certaintyReason ? `: ${entry.certaintyReason}` : ""}]`;
|
|
442
|
+
return [
|
|
443
|
+
...signatureLines.slice(0, declarationLine).map((line) => `${indent}${line}`),
|
|
444
|
+
`${indent}${lines}(${locator(entry, path)}): ${first}${warning}`,
|
|
445
|
+
...signatureLines.slice(declarationLine + 1).map((line) => `${indent}${line}`),
|
|
446
|
+
].join("\n");
|
|
123
447
|
}
|
|
448
|
+
signature = signature.replace(/\s+/g, " ");
|
|
124
449
|
const label = signature && signature.includes(entry.name) ? signature : `${entry.name} ${signature}`.trim();
|
|
125
450
|
return `${indent}${lines}(${locator(entry, path)}): ${label}`;
|
|
126
451
|
}
|
|
127
452
|
|
|
453
|
+
function renderStructuralEntry(entry: OutlineEntry, indent: string): string {
|
|
454
|
+
const lines = displayLineRange(entry.range);
|
|
455
|
+
return `${indent}${lines}: ${entry.signature.trim().replace(/\s+/g, " ")}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function containerFrame(
|
|
459
|
+
item: OutlineFileResult["items"][number],
|
|
460
|
+
language: AstLanguage,
|
|
461
|
+
): { header: string; footer: string } | undefined {
|
|
462
|
+
const firstMember = item.members[0];
|
|
463
|
+
if (!firstMember) return undefined;
|
|
464
|
+
if (
|
|
465
|
+
(language === "odin" ||
|
|
466
|
+
language === "cSharp" ||
|
|
467
|
+
language === "java" ||
|
|
468
|
+
language === "kotlin" ||
|
|
469
|
+
language === "swift") &&
|
|
470
|
+
item.bodyRange
|
|
471
|
+
) {
|
|
472
|
+
const bodyOffset = item.bodyRange.startByte - item.range.startByte;
|
|
473
|
+
const signature = Buffer.from(item.signature);
|
|
474
|
+
if (bodyOffset >= 0 && bodyOffset < signature.byteLength) {
|
|
475
|
+
const header = signature
|
|
476
|
+
.subarray(0, bodyOffset + 1)
|
|
477
|
+
.toString("utf8")
|
|
478
|
+
.trimEnd();
|
|
479
|
+
if (header.endsWith("{")) return { header, footer: "}" };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (
|
|
483
|
+
item.symbolType === "class" ||
|
|
484
|
+
item.symbolType === "struct" ||
|
|
485
|
+
item.symbolType === "interface" ||
|
|
486
|
+
item.symbolType === "enum" ||
|
|
487
|
+
item.symbolType === "object" ||
|
|
488
|
+
item.symbolType === "namespace"
|
|
489
|
+
) {
|
|
490
|
+
const members = (
|
|
491
|
+
language === "typeScript" ||
|
|
492
|
+
language === "tsx" ||
|
|
493
|
+
language === "cSharp" ||
|
|
494
|
+
language === "java" ||
|
|
495
|
+
language === "kotlin" ||
|
|
496
|
+
language === "swift"
|
|
497
|
+
? item.members.filter(
|
|
498
|
+
(member) => member.qualifiedName.split(".").slice(0, -1).join(".") === item.qualifiedName,
|
|
499
|
+
)
|
|
500
|
+
: item.members
|
|
501
|
+
)
|
|
502
|
+
.map((member) =>
|
|
503
|
+
(language === "java" &&
|
|
504
|
+
(member.symbolType === "class" ||
|
|
505
|
+
member.symbolType === "struct" ||
|
|
506
|
+
member.symbolType === "interface" ||
|
|
507
|
+
member.symbolType === "enum") &&
|
|
508
|
+
member.signature.endsWith("{")
|
|
509
|
+
? `${member.signature} … }`
|
|
510
|
+
: member.signature
|
|
511
|
+
)
|
|
512
|
+
.split("\n")
|
|
513
|
+
.map((line) => ` ${line}`)
|
|
514
|
+
.join("\n"),
|
|
515
|
+
)
|
|
516
|
+
.join("\n");
|
|
517
|
+
const braceSuffix = `\n${members}\n}`;
|
|
518
|
+
if (item.signature.endsWith(braceSuffix))
|
|
519
|
+
return { header: item.signature.slice(0, -braceSuffix.length), footer: "}" };
|
|
520
|
+
const tupleMembers = item.members
|
|
521
|
+
.map((member) =>
|
|
522
|
+
`${member.signature},`
|
|
523
|
+
.split("\n")
|
|
524
|
+
.map((line) => ` ${line}`)
|
|
525
|
+
.join("\n"),
|
|
526
|
+
)
|
|
527
|
+
.join("\n");
|
|
528
|
+
const tupleSuffix = `\n${tupleMembers}\n);`;
|
|
529
|
+
if (item.signature.endsWith(tupleSuffix))
|
|
530
|
+
return { header: item.signature.slice(0, -tupleSuffix.length), footer: ");" };
|
|
531
|
+
const tupleMarker = `\n${tupleMembers}\n)`;
|
|
532
|
+
const tupleMarkerIndex = item.signature.lastIndexOf(tupleMarker);
|
|
533
|
+
if (tupleMarkerIndex >= 0)
|
|
534
|
+
return {
|
|
535
|
+
header: item.signature.slice(0, tupleMarkerIndex),
|
|
536
|
+
footer: item.signature.slice(tupleMarkerIndex + tupleMarker.length - 1),
|
|
537
|
+
};
|
|
538
|
+
if (item.symbolType === "class") return undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const memberOffset = firstMember.range.startByte - item.range.startByte;
|
|
542
|
+
const signature = Buffer.from(item.signature);
|
|
543
|
+
if (memberOffset <= 0 || memberOffset > signature.byteLength) return undefined;
|
|
544
|
+
const header = signature.subarray(0, memberOffset).toString("utf8").trimEnd();
|
|
545
|
+
return header.endsWith("{") ? { header, footer: "}" } : undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
128
548
|
function renderOutlineFile(file: OutlineFileResult, cwd: string, includeHeader: boolean): string[] {
|
|
129
|
-
const lines = includeHeader
|
|
130
|
-
? [
|
|
131
|
-
`${formatPathForDisplay(file.path, cwd)} (${file.language}, ${file.lineCount} lines, ${formatSize(file.byteLength)})`,
|
|
132
|
-
]
|
|
133
|
-
: [];
|
|
549
|
+
const lines = includeHeader ? [formatPathForDisplay(file.path, cwd)] : [];
|
|
134
550
|
if (file.diagnostics.errorNodes > 0 || file.diagnostics.missingNodes > 0) {
|
|
135
551
|
lines.push(
|
|
136
552
|
`warning: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
|
|
137
553
|
);
|
|
138
554
|
}
|
|
139
|
-
const declarations = file.items.filter((item) =>
|
|
555
|
+
const declarations = file.items.filter((item) => item.rowKind === "declaration");
|
|
556
|
+
if (
|
|
557
|
+
file.language === "typeScript" ||
|
|
558
|
+
file.language === "tsx" ||
|
|
559
|
+
file.language === "odin" ||
|
|
560
|
+
file.language === "go" ||
|
|
561
|
+
file.language === "rust" ||
|
|
562
|
+
file.language === "cSharp" ||
|
|
563
|
+
file.language === "java" ||
|
|
564
|
+
file.language === "kotlin" ||
|
|
565
|
+
file.language === "swift" ||
|
|
566
|
+
file.language === "markdown"
|
|
567
|
+
) {
|
|
568
|
+
let previousSection = "";
|
|
569
|
+
for (const item of file.items) {
|
|
570
|
+
const section =
|
|
571
|
+
item.rowKind === "package"
|
|
572
|
+
? "package"
|
|
573
|
+
: item.rowKind === "import"
|
|
574
|
+
? "imports"
|
|
575
|
+
: item.rowKind === "export"
|
|
576
|
+
? "exports"
|
|
577
|
+
: item.rowKind === "sideEffect"
|
|
578
|
+
? "side effects"
|
|
579
|
+
: "declarations";
|
|
580
|
+
if (section !== previousSection) {
|
|
581
|
+
if (lines.length > 0) lines.push("");
|
|
582
|
+
lines.push(section);
|
|
583
|
+
previousSection = section;
|
|
584
|
+
}
|
|
585
|
+
const frame = item.rowKind === "declaration" ? containerFrame(item, file.language) : undefined;
|
|
586
|
+
lines.push(
|
|
587
|
+
item.rowKind !== "declaration"
|
|
588
|
+
? renderStructuralEntry(item, "")
|
|
589
|
+
: renderEntry(item, file.path, file.language, "", frame?.header),
|
|
590
|
+
);
|
|
591
|
+
const nestedContainers = new Map(
|
|
592
|
+
file.language === "typeScript" ||
|
|
593
|
+
file.language === "tsx" ||
|
|
594
|
+
file.language === "odin" ||
|
|
595
|
+
file.language === "cSharp" ||
|
|
596
|
+
file.language === "java" ||
|
|
597
|
+
file.language === "kotlin" ||
|
|
598
|
+
file.language === "swift"
|
|
599
|
+
? [item, ...item.members]
|
|
600
|
+
.filter(
|
|
601
|
+
(entry) =>
|
|
602
|
+
entry.symbolType === "class" ||
|
|
603
|
+
entry.symbolType === "struct" ||
|
|
604
|
+
entry.symbolType === "interface" ||
|
|
605
|
+
entry.symbolType === "enum",
|
|
606
|
+
)
|
|
607
|
+
.map((entry) => [entry.qualifiedName, entry] as const)
|
|
608
|
+
: [],
|
|
609
|
+
);
|
|
610
|
+
const emittedEnumSeparators = new Set<string>();
|
|
611
|
+
const openNestedDepths: number[] = [];
|
|
612
|
+
for (const [memberIndex, member] of item.members.entries()) {
|
|
613
|
+
const depth =
|
|
614
|
+
file.language === "typeScript" ||
|
|
615
|
+
file.language === "tsx" ||
|
|
616
|
+
file.language === "odin" ||
|
|
617
|
+
file.language === "cSharp" ||
|
|
618
|
+
file.language === "java" ||
|
|
619
|
+
file.language === "kotlin" ||
|
|
620
|
+
file.language === "swift"
|
|
621
|
+
? Math.max(1, member.qualifiedName.split(".").length - item.qualifiedName.split(".").length)
|
|
622
|
+
: 1;
|
|
623
|
+
const parent = member.qualifiedName.split(".").slice(0, -1).join(".");
|
|
624
|
+
const siblings =
|
|
625
|
+
file.language === "typeScript" ||
|
|
626
|
+
file.language === "tsx" ||
|
|
627
|
+
file.language === "odin" ||
|
|
628
|
+
file.language === "cSharp" ||
|
|
629
|
+
file.language === "java" ||
|
|
630
|
+
file.language === "kotlin" ||
|
|
631
|
+
file.language === "swift"
|
|
632
|
+
? item.members.filter(
|
|
633
|
+
(candidate) => candidate.qualifiedName.split(".").slice(0, -1).join(".") === parent,
|
|
634
|
+
)
|
|
635
|
+
: [];
|
|
636
|
+
const parentIsEnum = file.language === "java" && nestedContainers.get(parent)?.symbolType === "enum";
|
|
637
|
+
while (openNestedDepths.length > 0 && (openNestedDepths.at(-1) ?? 0) >= depth) {
|
|
638
|
+
const closingDepth = openNestedDepths.pop();
|
|
639
|
+
if (closingDepth !== undefined) lines.push(`${" ".repeat(closingDepth)}}`);
|
|
640
|
+
}
|
|
641
|
+
if (
|
|
642
|
+
parentIsEnum &&
|
|
643
|
+
!siblings.some((candidate) => candidate.symbolType === "enumMember") &&
|
|
644
|
+
!emittedEnumSeparators.has(parent)
|
|
645
|
+
) {
|
|
646
|
+
lines.push(`${" ".repeat(depth)};`);
|
|
647
|
+
emittedEnumSeparators.add(parent);
|
|
648
|
+
}
|
|
649
|
+
let memberSignature = member.signature;
|
|
650
|
+
const nextMember = item.members[memberIndex + 1];
|
|
651
|
+
const hasDescendants = nextMember?.qualifiedName.startsWith(`${member.qualifiedName}.`) ?? false;
|
|
652
|
+
if (hasDescendants && memberSignature.endsWith(" { … }")) {
|
|
653
|
+
memberSignature = `${memberSignature.slice(0, -" { … }".length)} {`;
|
|
654
|
+
openNestedDepths.push(depth);
|
|
655
|
+
}
|
|
656
|
+
if (parentIsEnum && member.symbolType === "enumMember") {
|
|
657
|
+
const constants = siblings.filter((candidate) => candidate.symbolType === "enumMember");
|
|
658
|
+
const constantIndex = constants.indexOf(member);
|
|
659
|
+
if (constantIndex + 1 < constants.length) memberSignature += ",";
|
|
660
|
+
else if (siblings.some((candidate) => candidate.symbolType !== "enumMember")) memberSignature += ";";
|
|
661
|
+
}
|
|
662
|
+
lines.push(renderEntry(member, file.path, file.language, " ".repeat(depth), memberSignature));
|
|
663
|
+
}
|
|
664
|
+
while (openNestedDepths.length > 0) {
|
|
665
|
+
const closingDepth = openNestedDepths.pop();
|
|
666
|
+
if (closingDepth !== undefined) lines.push(`${" ".repeat(closingDepth)}}`);
|
|
667
|
+
}
|
|
668
|
+
if (frame) lines.push(frame.footer);
|
|
669
|
+
}
|
|
670
|
+
return lines;
|
|
671
|
+
}
|
|
140
672
|
const groups = new Map<string, typeof declarations>();
|
|
141
673
|
for (const item of declarations) {
|
|
142
674
|
const visibility = item.isExported ? "public" : "private";
|
|
@@ -162,35 +694,225 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
162
694
|
name: "outline",
|
|
163
695
|
label: "outline",
|
|
164
696
|
description:
|
|
165
|
-
"Inspect
|
|
697
|
+
"Inspect declarations in one supported source file, one non-recursive package directory, or a recursive mixed-language subtree without returning implementation bodies. Parenthesized numbers are locators for symbol.",
|
|
166
698
|
promptSnippet: "Inspect public declarations and get symbol locators without reading implementation bodies",
|
|
167
699
|
promptGuidelines: [
|
|
168
|
-
"
|
|
169
|
-
"
|
|
700
|
+
"Set recursive=true to orient an unfamiliar repository or subtree across supported languages.",
|
|
701
|
+
"Leave includePrivate off for surface discovery; enable it only for targeted internal implementation work.",
|
|
702
|
+
"Leave includeDocs off when names and signatures answer the question; enable it only when documentation comments matter.",
|
|
170
703
|
"Treat each parenthesized number after a line range as that declaration's symbol locator.",
|
|
704
|
+
"Use symbol(signatureWithDocs) only when one selected contract needs its attached documentation.",
|
|
171
705
|
"Use symbol with several locators when complete declaration source is needed.",
|
|
172
706
|
],
|
|
173
707
|
parameters: outlineParams,
|
|
174
|
-
async execute(
|
|
175
|
-
const path = resolveExplorePath(ctx.cwd, params.path);
|
|
708
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
709
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
176
710
|
const metadata = await stat(path);
|
|
711
|
+
if (params.recursive) {
|
|
712
|
+
if (!metadata.isDirectory()) throw new Error("recursive outline requires a directory target");
|
|
713
|
+
const names = params.names ?? [];
|
|
714
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
715
|
+
const filesByPath = new Map<
|
|
716
|
+
string,
|
|
717
|
+
{ file: OutlineFileResult; locatorIds: number[]; renderedBytes: number }
|
|
718
|
+
>();
|
|
719
|
+
const allLocatorIds = new Set<number>();
|
|
720
|
+
const fatalFallbacks: Array<{
|
|
721
|
+
path: string;
|
|
722
|
+
fingerprint: string;
|
|
723
|
+
message: string;
|
|
724
|
+
code: "outlineFailed" | "resultFrameTooLarge";
|
|
725
|
+
}> = [];
|
|
726
|
+
let declarationCount = 0;
|
|
727
|
+
try {
|
|
728
|
+
const summary = await client.outlineRecursive(
|
|
729
|
+
path,
|
|
730
|
+
params.includePrivate ?? false,
|
|
731
|
+
params.includeDocs ?? false,
|
|
732
|
+
names,
|
|
733
|
+
{
|
|
734
|
+
async onFile(_relativePath, file) {
|
|
735
|
+
const firstLocator = nextLocator;
|
|
736
|
+
const block = renderOutlineFile(file, ctx.cwd, true).join("\n");
|
|
737
|
+
const fileLocatorIds = Array.from(
|
|
738
|
+
{ length: nextLocator - firstLocator },
|
|
739
|
+
(_, index) => firstLocator + index,
|
|
740
|
+
);
|
|
741
|
+
filesByPath.set(file.path, {
|
|
742
|
+
file,
|
|
743
|
+
locatorIds: fileLocatorIds,
|
|
744
|
+
renderedBytes: Buffer.byteLength(block),
|
|
745
|
+
});
|
|
746
|
+
for (const id of fileLocatorIds) allLocatorIds.add(id);
|
|
747
|
+
declarationCount += file.items.reduce(
|
|
748
|
+
(count, item) => count + (item.rowKind === "declaration" ? 1 + item.members.length : 0),
|
|
749
|
+
0,
|
|
750
|
+
);
|
|
751
|
+
await builder.appendBlock(file.path, formatPathForDisplay(file.path, ctx.cwd), block);
|
|
752
|
+
},
|
|
753
|
+
async onDiagnostic(diagnostic) {
|
|
754
|
+
if (
|
|
755
|
+
(diagnostic.code === "outlineFailed" || diagnostic.code === "resultFrameTooLarge") &&
|
|
756
|
+
diagnostic.sourceFingerprint
|
|
757
|
+
) {
|
|
758
|
+
fatalFallbacks.push({
|
|
759
|
+
path: resolve(path, diagnostic.relativePath),
|
|
760
|
+
fingerprint: diagnostic.sourceFingerprint,
|
|
761
|
+
message: diagnostic.message,
|
|
762
|
+
code: diagnostic.code,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const language = diagnostic.language ? ` (${diagnostic.language})` : "";
|
|
766
|
+
await builder.appendBlock(
|
|
767
|
+
undefined,
|
|
768
|
+
diagnostic.relativePath,
|
|
769
|
+
`diagnostic: ${diagnostic.relativePath}${language} [${diagnostic.code}]: ${diagnostic.message}`,
|
|
770
|
+
);
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
signal,
|
|
774
|
+
);
|
|
775
|
+
const exceptionLines = exceptionSummaryLines([
|
|
776
|
+
...(summary.failedFiles > 0 ||
|
|
777
|
+
summary.unreadableFiles > 0 ||
|
|
778
|
+
summary.oversizedFiles > 0 ||
|
|
779
|
+
summary.parserDegradedFiles > 0
|
|
780
|
+
? [
|
|
781
|
+
`exceptions: ${summary.failedFiles} failed, ${summary.unreadableFiles} unreadable, ${summary.oversizedFiles} oversized, parser degraded ${summary.parserDegradedFiles}`,
|
|
782
|
+
]
|
|
783
|
+
: []),
|
|
784
|
+
...limitReachedLine([
|
|
785
|
+
...(summary.fileLimitReached ? ["file count"] : []),
|
|
786
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
787
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
788
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
789
|
+
]),
|
|
790
|
+
]);
|
|
791
|
+
if (exceptionLines) await builder.appendRequiredBlock("recursive outline exceptions", exceptionLines);
|
|
792
|
+
const bounded = await builder.finish();
|
|
793
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
794
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
795
|
+
for (const [file, { locatorIds: ids }] of filesByPath) {
|
|
796
|
+
if (visible.has(file)) continue;
|
|
797
|
+
for (const id of ids) locators.delete(id);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
801
|
+
orientation.recordAttempts(
|
|
802
|
+
[...filesByPath.values()].map((record) => ({
|
|
803
|
+
path: record.file.path,
|
|
804
|
+
toolCallId,
|
|
805
|
+
fingerprint: record.file.sourceFingerprint,
|
|
806
|
+
kind: "directOutline",
|
|
807
|
+
sourceBytesDeflected: Math.max(0, record.file.byteLength - record.renderedBytes),
|
|
808
|
+
})),
|
|
809
|
+
);
|
|
810
|
+
for (const fallback of fatalFallbacks) {
|
|
811
|
+
orientation.recordFatal({
|
|
812
|
+
...fallback,
|
|
813
|
+
includePrivate: params.includePrivate ?? false,
|
|
814
|
+
names,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (bounded.overflow.temporaryPath) {
|
|
818
|
+
orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
819
|
+
}
|
|
820
|
+
orientation.recordOutlineTelemetry(toolCallId, {
|
|
821
|
+
workerInputBytes: summary.totalByteLength,
|
|
822
|
+
completeRenderedBytes: bounded.overflow.totalBytes,
|
|
823
|
+
modelVisibleAstBytes: returnedBytes,
|
|
824
|
+
temporaryOutputBytes: bounded.overflow.temporaryPath ? bounded.overflow.totalBytes : 0,
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
content: [{ type: "text", text: bounded.content }],
|
|
828
|
+
details: {
|
|
829
|
+
kind: "outline",
|
|
830
|
+
result: { path, summary, visibleFiles: bounded.visibleUnitIds },
|
|
831
|
+
declarationCount,
|
|
832
|
+
sourceBytes: summary.totalByteLength,
|
|
833
|
+
returnedBytes,
|
|
834
|
+
avoidedBytes: Math.max(0, summary.totalByteLength - returnedBytes),
|
|
835
|
+
truncated: bounded.overflow.truncated,
|
|
836
|
+
overflow: bounded.overflow,
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
} catch (error) {
|
|
840
|
+
await builder.abort();
|
|
841
|
+
for (const id of allLocatorIds) locators.delete(id);
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
177
845
|
const target: OutlineTarget = metadata.isDirectory()
|
|
178
846
|
? { kind: "directory", path }
|
|
179
|
-
: { kind: "file", path, language:
|
|
847
|
+
: { kind: "file", path, language: requireAstLanguageForPath(path) };
|
|
180
848
|
const names = params.names ?? [];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
849
|
+
let result: OutlineTargetResult;
|
|
850
|
+
try {
|
|
851
|
+
result = await client.outline(
|
|
852
|
+
target,
|
|
853
|
+
params.includePrivate ?? false,
|
|
854
|
+
params.includeDocs ?? false,
|
|
855
|
+
names,
|
|
856
|
+
signal,
|
|
857
|
+
);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
if (
|
|
860
|
+
target.kind === "file" &&
|
|
861
|
+
error instanceof AstWorkerError &&
|
|
862
|
+
(error.code === "outline_failed" || error.code === "response_too_large") &&
|
|
863
|
+
error.sourceFingerprint
|
|
864
|
+
) {
|
|
865
|
+
orientation.recordFatal({
|
|
866
|
+
path,
|
|
867
|
+
fingerprint: error.sourceFingerprint,
|
|
868
|
+
includePrivate: params.includePrivate ?? false,
|
|
869
|
+
names,
|
|
870
|
+
code: error.code,
|
|
871
|
+
message: error.message,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
const renderedFiles = result.files.map((file) => {
|
|
877
|
+
const firstLocator = nextLocator;
|
|
878
|
+
const text = renderOutlineFile(file, ctx.cwd, result.files.length > 1).join("\n");
|
|
879
|
+
return {
|
|
880
|
+
file,
|
|
881
|
+
text,
|
|
882
|
+
locatorIds: Array.from({ length: nextLocator - firstLocator }, (_, index) => firstLocator + index),
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
const lines = renderedFiles.flatMap((file, index) => [...(index === 0 ? [] : [""]), file.text]);
|
|
186
886
|
const declarationCount = result.files.reduce(
|
|
187
887
|
(count, file) =>
|
|
188
888
|
count +
|
|
189
|
-
file.items.reduce(
|
|
889
|
+
file.items.reduce(
|
|
890
|
+
(fileCount, item) => fileCount + (item.rowKind === "declaration" ? 1 + item.members.length : 0),
|
|
891
|
+
0,
|
|
892
|
+
),
|
|
190
893
|
0,
|
|
191
894
|
);
|
|
192
895
|
if (declarationCount === 0) lines.push(names.length > 0 ? "No matching declarations" : "No declarations");
|
|
193
|
-
const
|
|
896
|
+
const completeText = lines.join("\n");
|
|
897
|
+
const output = compact(completeText, result.totalByteLength, declarationCount, "outline", result);
|
|
898
|
+
orientation.recordAttempts(
|
|
899
|
+
renderedFiles.map((rendered) => {
|
|
900
|
+
const renderedBytes = Buffer.byteLength(rendered.text);
|
|
901
|
+
return {
|
|
902
|
+
path: rendered.file.path,
|
|
903
|
+
toolCallId,
|
|
904
|
+
fingerprint: rendered.file.sourceFingerprint,
|
|
905
|
+
kind: "directOutline" as const,
|
|
906
|
+
sourceBytesDeflected: Math.max(0, rendered.file.byteLength - renderedBytes),
|
|
907
|
+
};
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
910
|
+
orientation.recordOutlineTelemetry(toolCallId, {
|
|
911
|
+
workerInputBytes: result.totalByteLength,
|
|
912
|
+
completeRenderedBytes: Buffer.byteLength(completeText),
|
|
913
|
+
modelVisibleAstBytes: output.details.returnedBytes,
|
|
914
|
+
temporaryOutputBytes: 0,
|
|
915
|
+
});
|
|
194
916
|
return { content: [{ type: "text", text: output.text }], details: output.details };
|
|
195
917
|
},
|
|
196
918
|
renderCall(args, theme, context) {
|
|
@@ -218,21 +940,37 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
218
940
|
name: "symbol",
|
|
219
941
|
label: "symbol",
|
|
220
942
|
description:
|
|
221
|
-
"Return
|
|
222
|
-
promptSnippet: "Retrieve exact
|
|
943
|
+
"Return signatures, documented signatures, exact declarations, or declarations with required imports for numeric outline locators; stale locators fail atomically.",
|
|
944
|
+
promptSnippet: "Retrieve signatures or exact declarations for several outline locators",
|
|
223
945
|
parameters: symbolParams,
|
|
224
|
-
async execute(
|
|
946
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
947
|
+
if (params.contextLines !== undefined && params.view !== "declaration") {
|
|
948
|
+
throw new Error("contextLines is supported only with view=declaration");
|
|
949
|
+
}
|
|
225
950
|
const records = [...new Set(params.locators)].map((id) => {
|
|
226
951
|
const record = locators.get(id);
|
|
227
952
|
if (!record) throw new Error(`Unknown symbol locator: ${id}. Run outline again.`);
|
|
228
953
|
if (record.stale) throw new Error(`Symbol locator ${id} is stale. Run outline again.`);
|
|
954
|
+
if (record.generation !== client.getGeneration()) {
|
|
955
|
+
throw new Error(`Symbol locator ${id} is stale because the AST worker restarted. Run outline again.`);
|
|
956
|
+
}
|
|
229
957
|
return record;
|
|
230
958
|
});
|
|
231
959
|
const result = await client.symbol(
|
|
232
960
|
[...new Set(records.map((record) => record.token))],
|
|
961
|
+
params.view,
|
|
233
962
|
params.contextLines ?? 0,
|
|
234
963
|
signal,
|
|
235
964
|
);
|
|
965
|
+
orientation.recordAttempts(
|
|
966
|
+
result.declarations.map((declaration) => ({
|
|
967
|
+
path: declaration.path,
|
|
968
|
+
fingerprint: declaration.sourceFingerprint,
|
|
969
|
+
kind: "symbol" as const,
|
|
970
|
+
toolCallId,
|
|
971
|
+
sourceBytesDeflected: 0,
|
|
972
|
+
})),
|
|
973
|
+
);
|
|
236
974
|
const requestedByToken = new Map<string, LocatorRecord[]>();
|
|
237
975
|
for (const record of records) {
|
|
238
976
|
const requested = requestedByToken.get(record.token);
|
|
@@ -248,18 +986,25 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
248
986
|
return declaration ? (requestedByToken.get(declaration.locator) ?? []) : [];
|
|
249
987
|
});
|
|
250
988
|
const range = block.returnedRange;
|
|
251
|
-
const lineRange =
|
|
252
|
-
range.start.line === range.end.line
|
|
253
|
-
? `${range.start.line + 1}`
|
|
254
|
-
: `${range.start.line + 1}-${range.end.line + 1}`;
|
|
989
|
+
const lineRange = displayLineRange(range);
|
|
255
990
|
lines.push(
|
|
256
991
|
...(includePaths ? [formatPathForDisplay(block.path, ctx.cwd)] : []),
|
|
257
992
|
`${lineRange}(${represented.map((record) => record.id).join(",")}): ${represented.map((record) => record.name).join(", ")}`,
|
|
993
|
+
...block.declarationIndexes.flatMap((index) => {
|
|
994
|
+
const declaration = result.declarations[index];
|
|
995
|
+
if (!declaration) return [];
|
|
996
|
+
const ids = (requestedByToken.get(declaration.locator) ?? []).map((record) => record.id).join(",");
|
|
997
|
+
return declaration.diagnostics.map((diagnostic) => `warning (${ids}): ${diagnostic}`);
|
|
998
|
+
}),
|
|
258
999
|
block.source,
|
|
259
1000
|
);
|
|
260
1001
|
}
|
|
261
1002
|
const sourceBytes = result.blocks.reduce((count, block) => count + Buffer.byteLength(block.source), 0);
|
|
262
1003
|
const output = compact(lines.join("\n"), sourceBytes, result.declarations.length, "symbol", result);
|
|
1004
|
+
if (output.details.truncated) {
|
|
1005
|
+
throw new Error("Symbol result exceeded the output limit. Request fewer locators.");
|
|
1006
|
+
}
|
|
1007
|
+
if (params.view === "declaration") for (const record of records) record.declarationRetrieved = true;
|
|
263
1008
|
return { content: [{ type: "text", text: output.text }], details: output.details };
|
|
264
1009
|
},
|
|
265
1010
|
renderCall(args, theme, context) {
|
|
@@ -272,10 +1017,335 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
272
1017
|
context.toolCallId,
|
|
273
1018
|
"symbol",
|
|
274
1019
|
targets,
|
|
275
|
-
[args.
|
|
1020
|
+
[symbolOptions(args.view, args.contextLines)],
|
|
1021
|
+
theme,
|
|
1022
|
+
);
|
|
1023
|
+
component.set(targets, [symbolOptions(args.view, args.contextLines)], theme);
|
|
1024
|
+
return component;
|
|
1025
|
+
},
|
|
1026
|
+
renderResult(result, options, theme, context) {
|
|
1027
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1028
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
async function executeLocatorEdit(
|
|
1033
|
+
toolCallId: string,
|
|
1034
|
+
locatorId: number,
|
|
1035
|
+
edit: EditOperation,
|
|
1036
|
+
signal: AbortSignal | undefined,
|
|
1037
|
+
cwd: string,
|
|
1038
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details: AstToolDetails }> {
|
|
1039
|
+
const record = editableLocator(locatorId);
|
|
1040
|
+
const plan = await client.planEdit(record.token, edit, signal);
|
|
1041
|
+
const mutation = await applyExactSourceMutations(cwd, plan.files, signal);
|
|
1042
|
+
if (mutation.changes.length === 0) {
|
|
1043
|
+
const message = mutation.failures.map((failure) => failure.message).join("; ") || "No files changed.";
|
|
1044
|
+
throw new Error(`Locator edit failed: ${message}`);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const changedPaths = new Set(mutation.changes.map((change) => resolve(cwd, change.path)));
|
|
1048
|
+
const invalidatedLocatorIds: number[] = [];
|
|
1049
|
+
for (const candidate of locators.values()) {
|
|
1050
|
+
if (changedPaths.has(candidate.path) && !candidate.stale) {
|
|
1051
|
+
candidate.stale = true;
|
|
1052
|
+
invalidatedLocatorIds.push(candidate.id);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
orientation.invalidate([...changedPaths]);
|
|
1056
|
+
emitMutation(toolCallId, cwd, mutation);
|
|
1057
|
+
|
|
1058
|
+
const verificationDiagnostics: string[] = [];
|
|
1059
|
+
const verifiedTokens = new Set<string>();
|
|
1060
|
+
if (mutation.status === "completed") {
|
|
1061
|
+
for (const filePlan of plan.files) {
|
|
1062
|
+
try {
|
|
1063
|
+
const reparsed = await client.outline(
|
|
1064
|
+
{ kind: "file", path: filePlan.path, language: requireAstLanguageForPath(filePlan.path) },
|
|
1065
|
+
true,
|
|
1066
|
+
false,
|
|
1067
|
+
[],
|
|
1068
|
+
signal,
|
|
1069
|
+
);
|
|
1070
|
+
const file = reparsed.files[0];
|
|
1071
|
+
if (!file || file.sourceFingerprint !== sourceFingerprintForPlan(filePlan.source)) {
|
|
1072
|
+
verificationDiagnostics.push(`${filePlan.path}: post-mutation fingerprint did not match the plan`);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
if (file.diagnostics.errorNodes > 0 || file.diagnostics.missingNodes > 0) {
|
|
1076
|
+
verificationDiagnostics.push(
|
|
1077
|
+
`${filePlan.path}: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
|
|
1078
|
+
);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
for (const item of file.items) {
|
|
1082
|
+
if (item.certainty === "certain" && item.locator) verifiedTokens.add(item.locator);
|
|
1083
|
+
for (const member of item.members) {
|
|
1084
|
+
if (member.certainty === "certain" && member.locator) verifiedTokens.add(member.locator);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
verificationDiagnostics.push(
|
|
1089
|
+
`${filePlan.path}: post-mutation reparse failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const freshLocatorIds = plan.freshLocators
|
|
1095
|
+
.filter((fresh) => verifiedTokens.has(fresh.locator))
|
|
1096
|
+
.map((fresh) => registerLocator(fresh.locator, fresh.path, fresh.name));
|
|
1097
|
+
const freshTargetId = freshLocatorIds[0];
|
|
1098
|
+
|
|
1099
|
+
const lines = [
|
|
1100
|
+
`changed: ${mutation.changes.map((change) => change.path).join(", ")}`,
|
|
1101
|
+
`invalidated locators: ${invalidatedLocatorIds.join(", ") || "none"}; fresh locators: ${freshLocatorIds.map((id) => `(${id})`).join(", ") || "none"}; skipped impacts: ${plan.skippedImpacts.length}; status: ${mutation.status}`,
|
|
1102
|
+
];
|
|
1103
|
+
for (const skipped of plan.skippedImpacts) {
|
|
1104
|
+
const candidateIds = skipped.candidateLocators.flatMap((token, candidateIndex) => {
|
|
1105
|
+
if (token === record.token && freshTargetId !== undefined) return [freshTargetId];
|
|
1106
|
+
const existing = [...locators.values()].find((candidate) => candidate.token === token && !candidate.stale);
|
|
1107
|
+
if (existing) return [existing.id];
|
|
1108
|
+
const candidatePath = skipped.candidatePaths[candidateIndex];
|
|
1109
|
+
return candidatePath && !changedPaths.has(resolve(candidatePath))
|
|
1110
|
+
? [registerLocator(token, candidatePath, "rename candidate")]
|
|
1111
|
+
: [];
|
|
1112
|
+
});
|
|
1113
|
+
lines.push(
|
|
1114
|
+
`skipped: ${formatPathForDisplay(skipped.path, cwd)}:${displayLineRange(skipped.range)} [${skipped.reason}]${candidateIds.length > 0 ? ` candidates ${candidateIds.map((id) => `(${id})`).join(", ")}` : ""}`,
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
for (const diagnostic of verificationDiagnostics) lines.push(`warning: ${diagnostic}`);
|
|
1118
|
+
const text = lines.join("\n");
|
|
1119
|
+
const sourceBytes = plan.files.reduce((total, file) => total + Buffer.byteLength(file.source), 0);
|
|
1120
|
+
const returnedBytes = Buffer.byteLength(text);
|
|
1121
|
+
return {
|
|
1122
|
+
content: [{ type: "text", text }],
|
|
1123
|
+
details: {
|
|
1124
|
+
kind: "locatorEdit",
|
|
1125
|
+
result: { plan, mutation, invalidatedLocatorIds, freshLocatorIds, verificationDiagnostics },
|
|
1126
|
+
declarationCount: freshLocatorIds.length,
|
|
1127
|
+
sourceBytes,
|
|
1128
|
+
returnedBytes,
|
|
1129
|
+
avoidedBytes: Math.max(0, sourceBytes - returnedBytes),
|
|
1130
|
+
truncated: false,
|
|
1131
|
+
overflow: undefined,
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const replaceDeclaration = defineTool<typeof replaceDeclarationParams, AstToolDetails>({
|
|
1137
|
+
name: "replace_declaration",
|
|
1138
|
+
label: "replace_declaration",
|
|
1139
|
+
description:
|
|
1140
|
+
"Replace one complete declaration through its stale-safe numeric locator. Code must parse as one declaration in the current parent container. A Markdown heading locator replaces the heading and its entire section, including deeper subsections, and requires one root heading at the same depth.",
|
|
1141
|
+
promptSnippet: "Replace one complete declaration by structural locator",
|
|
1142
|
+
parameters: replaceDeclarationParams,
|
|
1143
|
+
executionMode: "sequential",
|
|
1144
|
+
execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1145
|
+
return executeLocatorEdit(
|
|
1146
|
+
toolCallId,
|
|
1147
|
+
params.locator,
|
|
1148
|
+
{ kind: "replaceDeclaration", source: params.source },
|
|
1149
|
+
signal,
|
|
1150
|
+
ctx.cwd,
|
|
1151
|
+
);
|
|
1152
|
+
},
|
|
1153
|
+
renderCall(args, theme, context) {
|
|
1154
|
+
return locatorEditCall("replace_declaration", args.locator, locators, rowState, theme, context);
|
|
1155
|
+
},
|
|
1156
|
+
renderResult(result, options, theme, context) {
|
|
1157
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1158
|
+
},
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
const replaceBody = defineTool<typeof replaceBodyParams, AstToolDetails>({
|
|
1162
|
+
name: "replace_body",
|
|
1163
|
+
label: "replace_body",
|
|
1164
|
+
description:
|
|
1165
|
+
"Replace exactly the reliable adapter-provided body range while preserving its signature and attached metadata. For a Markdown heading locator, preserve the heading and replace its section content; nested headings must be deeper than the selected heading.",
|
|
1166
|
+
promptSnippet: "Replace one executable body by structural locator",
|
|
1167
|
+
parameters: replaceBodyParams,
|
|
1168
|
+
executionMode: "sequential",
|
|
1169
|
+
execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1170
|
+
return executeLocatorEdit(
|
|
1171
|
+
toolCallId,
|
|
1172
|
+
params.locator,
|
|
1173
|
+
{ kind: "replaceBody", body: params.body },
|
|
1174
|
+
signal,
|
|
1175
|
+
ctx.cwd,
|
|
1176
|
+
);
|
|
1177
|
+
},
|
|
1178
|
+
renderCall(args, theme, context) {
|
|
1179
|
+
return locatorEditCall("replace_body", args.locator, locators, rowState, theme, context);
|
|
1180
|
+
},
|
|
1181
|
+
renderResult(result, options, theme, context) {
|
|
1182
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1183
|
+
},
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const insertDeclaration = defineTool<typeof insertDeclarationParams, AstToolDetails>({
|
|
1187
|
+
name: "insert_declaration",
|
|
1188
|
+
label: "insert_declaration",
|
|
1189
|
+
description:
|
|
1190
|
+
"Insert exactly one declaration immediately before or after a declaration locator in its current parent container.",
|
|
1191
|
+
promptSnippet: "Insert one declaration adjacent to a structural locator",
|
|
1192
|
+
parameters: insertDeclarationParams,
|
|
1193
|
+
executionMode: "sequential",
|
|
1194
|
+
execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1195
|
+
return executeLocatorEdit(
|
|
1196
|
+
toolCallId,
|
|
1197
|
+
params.locator,
|
|
1198
|
+
{ kind: "insertDeclaration", position: params.position, source: params.source },
|
|
1199
|
+
signal,
|
|
1200
|
+
ctx.cwd,
|
|
1201
|
+
);
|
|
1202
|
+
},
|
|
1203
|
+
renderCall(args, theme, context) {
|
|
1204
|
+
return locatorEditCall("insert_declaration", args.locator, locators, rowState, theme, context, args.position);
|
|
1205
|
+
},
|
|
1206
|
+
renderResult(result, options, theme, context) {
|
|
1207
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
const renameDeclaration = defineTool<typeof renameDeclarationParams, AstToolDetails>({
|
|
1212
|
+
name: "rename_declaration",
|
|
1213
|
+
label: "rename_declaration",
|
|
1214
|
+
description:
|
|
1215
|
+
"Rename one declaration and exact references within an explicit file or repository scope. Inferred references require includeInferred=true; ambiguous references remain unchanged.",
|
|
1216
|
+
promptSnippet: "Rename one declaration through stale-safe structural identity",
|
|
1217
|
+
parameters: renameDeclarationParams,
|
|
1218
|
+
executionMode: "sequential",
|
|
1219
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1220
|
+
const scope =
|
|
1221
|
+
params.scope.kind === "repository"
|
|
1222
|
+
? { kind: "repository" as const, path: await realpath(resolveExplorePath(ctx.cwd, params.scope.path)) }
|
|
1223
|
+
: { kind: "file" as const };
|
|
1224
|
+
return executeLocatorEdit(
|
|
1225
|
+
toolCallId,
|
|
1226
|
+
params.locator,
|
|
1227
|
+
{ kind: "renameDeclaration", newName: params.newName, scope, includeInferred: params.includeInferred },
|
|
1228
|
+
signal,
|
|
1229
|
+
ctx.cwd,
|
|
1230
|
+
);
|
|
1231
|
+
},
|
|
1232
|
+
renderCall(args, theme, context) {
|
|
1233
|
+
return locatorEditCall(
|
|
1234
|
+
"rename_declaration",
|
|
1235
|
+
args.locator,
|
|
1236
|
+
locators,
|
|
1237
|
+
rowState,
|
|
1238
|
+
theme,
|
|
1239
|
+
context,
|
|
1240
|
+
`${args.newName} ${args.scope.kind}${args.includeInferred ? " inferred" : ""}`,
|
|
1241
|
+
);
|
|
1242
|
+
},
|
|
1243
|
+
renderResult(result, options, theme, context) {
|
|
1244
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const apiDiscover = defineTool<typeof apiDiscoverParams, AstToolDetails>({
|
|
1249
|
+
name: "api_discover",
|
|
1250
|
+
label: "api_discover",
|
|
1251
|
+
description:
|
|
1252
|
+
"Discover declarations and supported caller import paths across a canonical repository, package, or subtree without returning implementation bodies. Results include numeric symbol locators.",
|
|
1253
|
+
promptSnippet: "Find reusable declarations and caller import paths across a repository or subtree",
|
|
1254
|
+
promptGuidelines: [
|
|
1255
|
+
"Use api_discover when reuse intent is known but the declaration path or exact name is not.",
|
|
1256
|
+
"Choose exactly one query kind and keep fuzzy or documentation work limits narrow.",
|
|
1257
|
+
"Use packageSurface when the caller needs a supported public import path.",
|
|
1258
|
+
"Treat a clear candidate signature as sufficient; retrieve a selected locator with symbol only when its contract or implementation needs closer inspection.",
|
|
1259
|
+
],
|
|
1260
|
+
parameters: apiDiscoverParams,
|
|
1261
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1262
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
1263
|
+
if (!(await stat(path)).isDirectory()) throw new Error("api_discover requires a directory scope");
|
|
1264
|
+
const result = await client.discoverApi(
|
|
1265
|
+
path,
|
|
1266
|
+
params.query as ApiQuery,
|
|
1267
|
+
params.surface as ApiSurfaceFilter,
|
|
1268
|
+
params.resultLimit,
|
|
1269
|
+
signal,
|
|
1270
|
+
);
|
|
1271
|
+
orientation.recordAttempts(
|
|
1272
|
+
result.candidates.map((candidate) => ({
|
|
1273
|
+
path: candidate.definingFile,
|
|
1274
|
+
fingerprint: candidate.sourceFingerprint,
|
|
1275
|
+
kind: "apiCandidate" as const,
|
|
1276
|
+
toolCallId,
|
|
1277
|
+
sourceBytesDeflected: 0,
|
|
1278
|
+
})),
|
|
1279
|
+
);
|
|
1280
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
1281
|
+
const locatorByUnit = new Map<string, number>();
|
|
1282
|
+
try {
|
|
1283
|
+
for (const [index, candidate] of result.candidates.entries()) {
|
|
1284
|
+
const id = registerLocator(candidate.locator, candidate.definingFile, candidate.name);
|
|
1285
|
+
const unit = `${candidate.definingFile}:${candidate.range.startByte}:${index}`;
|
|
1286
|
+
locatorByUnit.set(unit, id);
|
|
1287
|
+
await builder.appendBlock(unit, candidate.name, renderApiCandidate(candidate, id, ctx.cwd));
|
|
1288
|
+
}
|
|
1289
|
+
if (result.candidates.length === 0)
|
|
1290
|
+
await builder.appendRequiredBlock("no matches", "No matching declarations");
|
|
1291
|
+
const summary = result.summary;
|
|
1292
|
+
const exceptionLines = exceptionSummaryLines([
|
|
1293
|
+
...(summary.omittedCandidates > 0
|
|
1294
|
+
? [`${summary.omittedCandidates} candidates omitted (result limit ${summary.resultLimit})`]
|
|
1295
|
+
: []),
|
|
1296
|
+
...(summary.resolutionDiagnostics > 0
|
|
1297
|
+
? [`resolution diagnostics: ${summary.resolutionDiagnostics}`]
|
|
1298
|
+
: []),
|
|
1299
|
+
...limitReachedLine([
|
|
1300
|
+
...(summary.candidateLimitReached ? ["query candidates"] : []),
|
|
1301
|
+
...(summary.workLimitReached ? ["query work"] : []),
|
|
1302
|
+
...(summary.fileLimitReached ? ["files"] : []),
|
|
1303
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
1304
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
1305
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
1306
|
+
]),
|
|
1307
|
+
]);
|
|
1308
|
+
if (exceptionLines) await builder.appendRequiredBlock("API discovery exceptions", exceptionLines);
|
|
1309
|
+
const bounded = await builder.finish();
|
|
1310
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
1311
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
1312
|
+
for (const [unit, id] of locatorByUnit) if (!visible.has(unit)) locators.delete(id);
|
|
1313
|
+
}
|
|
1314
|
+
if (bounded.overflow.temporaryPath) orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
1315
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{ type: "text", text: bounded.content }],
|
|
1318
|
+
details: {
|
|
1319
|
+
kind: "apiDiscover",
|
|
1320
|
+
result,
|
|
1321
|
+
declarationCount: result.candidates.length,
|
|
1322
|
+
sourceBytes: summary.totalSourceBytes,
|
|
1323
|
+
returnedBytes,
|
|
1324
|
+
avoidedBytes: Math.max(0, summary.totalSourceBytes - returnedBytes),
|
|
1325
|
+
truncated: bounded.overflow.truncated,
|
|
1326
|
+
overflow: bounded.overflow,
|
|
1327
|
+
},
|
|
1328
|
+
};
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
await builder.abort();
|
|
1331
|
+
for (const id of locatorByUnit.values()) locators.delete(id);
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
renderCall(args, theme, context) {
|
|
1336
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1337
|
+
const query = formatApiQuery(args.query as ApiQuery);
|
|
1338
|
+
const component =
|
|
1339
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1340
|
+
new AstCallComponent(
|
|
1341
|
+
rowState,
|
|
1342
|
+
context.toolCallId,
|
|
1343
|
+
"api_discover",
|
|
1344
|
+
[stripLeadingAt(args.path)],
|
|
1345
|
+
[`[${query} ${args.surface} limit=${args.resultLimit}]`],
|
|
276
1346
|
theme,
|
|
277
1347
|
);
|
|
278
|
-
component.set(
|
|
1348
|
+
component.set([stripLeadingAt(args.path)], [`[${query} ${args.surface} limit=${args.resultLimit}]`], theme);
|
|
279
1349
|
return component;
|
|
280
1350
|
},
|
|
281
1351
|
renderResult(result, options, theme, context) {
|
|
@@ -284,22 +1354,434 @@ export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
|
|
|
284
1354
|
},
|
|
285
1355
|
});
|
|
286
1356
|
|
|
1357
|
+
const astSearch = defineTool<typeof astSearchParams, AstToolDetails>({
|
|
1358
|
+
name: "ast_search",
|
|
1359
|
+
label: "ast_search",
|
|
1360
|
+
description:
|
|
1361
|
+
"Search one canonical source file, repository, package, or subtree with an ast-grep code pattern. Returns bounded deterministic matches, metavariable bindings, parser certainty, and numeric locators for exact source or enclosing scopes.",
|
|
1362
|
+
promptSnippet: "Search repository code shapes with ast-grep patterns and retrievable locators",
|
|
1363
|
+
promptGuidelines: [
|
|
1364
|
+
"Use ast_search for code shapes; use grep for literal text.",
|
|
1365
|
+
"Pass language for repository, package, and subtree targets. Supported files can infer it.",
|
|
1366
|
+
"Use $NAME and $$$NAME metavariables. Keep resultLimit narrow, then retrieve selected numeric locators with symbol(declaration).",
|
|
1367
|
+
],
|
|
1368
|
+
parameters: astSearchParams,
|
|
1369
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1370
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
1371
|
+
const metadata = await stat(path);
|
|
1372
|
+
const inferred = metadata.isFile() ? astLanguageForPath(path) : undefined;
|
|
1373
|
+
if (metadata.isFile() && !inferred) throw new Error(`Unsupported ast_search file type: ${path}`);
|
|
1374
|
+
if (metadata.isDirectory() && !params.language) {
|
|
1375
|
+
throw new Error("ast_search requires language for repository, package, and subtree targets");
|
|
1376
|
+
}
|
|
1377
|
+
if (inferred && params.language && inferred !== params.language) {
|
|
1378
|
+
throw new Error(`ast_search language ${params.language} does not match ${inferred} target ${path}`);
|
|
1379
|
+
}
|
|
1380
|
+
const language = params.language ?? inferred;
|
|
1381
|
+
if (!language) throw new Error("ast_search could not infer a language; pass language explicitly");
|
|
1382
|
+
const result = await client.search(path, language, params.pattern, params.resultLimit, signal);
|
|
1383
|
+
orientation.recordAttempts([
|
|
1384
|
+
...(metadata.isFile() && result.targetSourceFingerprint
|
|
1385
|
+
? [
|
|
1386
|
+
{
|
|
1387
|
+
path,
|
|
1388
|
+
fingerprint: result.targetSourceFingerprint,
|
|
1389
|
+
kind: "structuralMatch" as const,
|
|
1390
|
+
toolCallId,
|
|
1391
|
+
sourceBytesDeflected: 0,
|
|
1392
|
+
},
|
|
1393
|
+
]
|
|
1394
|
+
: []),
|
|
1395
|
+
...result.matches.map((match) => ({
|
|
1396
|
+
path: metadata.isDirectory() ? resolve(path, match.relativePath) : path,
|
|
1397
|
+
fingerprint: match.sourceFingerprint,
|
|
1398
|
+
kind: "structuralMatch" as const,
|
|
1399
|
+
toolCallId,
|
|
1400
|
+
sourceBytesDeflected: 0,
|
|
1401
|
+
})),
|
|
1402
|
+
]);
|
|
1403
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
1404
|
+
const locatorByUnit = new Map<string, number[]>();
|
|
1405
|
+
try {
|
|
1406
|
+
for (const [index, match] of result.matches.entries()) {
|
|
1407
|
+
const sourcePath = metadata.isDirectory() ? resolve(path, match.relativePath) : path;
|
|
1408
|
+
const matchId = registerLocator(match.locator, sourcePath, `match ${match.relativePath}`);
|
|
1409
|
+
const ids = [matchId];
|
|
1410
|
+
const unit = `${match.relativePath}:${match.range.startByte}:${index}`;
|
|
1411
|
+
const lines = [
|
|
1412
|
+
`${match.relativePath}:${displayLineRange(match.range)}(${matchId}) [${match.language}, ${match.certainty}]`,
|
|
1413
|
+
...match.preview.split("\n").map((line) => ` ${line}`),
|
|
1414
|
+
];
|
|
1415
|
+
if (match.previewTruncated) lines.push(" [preview truncated]");
|
|
1416
|
+
for (const binding of match.bindings) {
|
|
1417
|
+
const values = binding.values.map((value) => JSON.stringify(value.preview)).join(", ");
|
|
1418
|
+
lines.push(` $${binding.name} = ${values}${binding.valuesTruncated ? ", …" : ""}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (match.bindingsTruncated) lines.push(" [bindings truncated]");
|
|
1421
|
+
if (match.certaintyReason) lines.push(` uncertainty: ${match.certaintyReason}`);
|
|
1422
|
+
if (match.enclosingScope) {
|
|
1423
|
+
const scopeId = registerLocator(
|
|
1424
|
+
match.enclosingScope.locator,
|
|
1425
|
+
sourcePath,
|
|
1426
|
+
match.enclosingScope.astKind,
|
|
1427
|
+
);
|
|
1428
|
+
ids.push(scopeId);
|
|
1429
|
+
lines.push(
|
|
1430
|
+
` enclosing ${displayLineRange(match.enclosingScope.range)}(${scopeId}): ${match.enclosingScope.astKind}`,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
locatorByUnit.set(unit, ids);
|
|
1434
|
+
await builder.appendBlock(unit, match.relativePath, lines.join("\n"));
|
|
1435
|
+
}
|
|
1436
|
+
if (result.matches.length === 0) await builder.appendRequiredBlock("no matches", "No structural matches");
|
|
1437
|
+
for (const [index, diagnostic] of result.diagnostics.entries()) {
|
|
1438
|
+
await builder.appendBlock(
|
|
1439
|
+
undefined,
|
|
1440
|
+
`diagnostic-${index}`,
|
|
1441
|
+
`diagnostic: ${diagnostic.relativePath} [${diagnostic.code}]: ${diagnostic.message}`,
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
const summary = result.summary;
|
|
1445
|
+
const exceptionLines = exceptionSummaryLines([
|
|
1446
|
+
...(summary.matchesFound > summary.matchesReturned
|
|
1447
|
+
? [
|
|
1448
|
+
`${summary.matchesFound - summary.matchesReturned} matches omitted (result limit ${summary.resultLimit})`,
|
|
1449
|
+
]
|
|
1450
|
+
: []),
|
|
1451
|
+
...(summary.unreadableFiles > 0 ||
|
|
1452
|
+
summary.oversizedFiles > 0 ||
|
|
1453
|
+
summary.failedFiles > 0 ||
|
|
1454
|
+
summary.parserDegradedFiles > 0 ||
|
|
1455
|
+
summary.diagnosticsOmitted > 0
|
|
1456
|
+
? [
|
|
1457
|
+
`exceptions: ${summary.unreadableFiles} unreadable, ${summary.oversizedFiles} oversized, ${summary.failedFiles} failed, parser degraded ${summary.parserDegradedFiles}, diagnostics omitted ${summary.diagnosticsOmitted}`,
|
|
1458
|
+
]
|
|
1459
|
+
: []),
|
|
1460
|
+
...limitReachedLine([
|
|
1461
|
+
...(summary.resultLimitReached ? ["results"] : []),
|
|
1462
|
+
...(summary.fileLimitReached ? ["files"] : []),
|
|
1463
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
1464
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
1465
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
1466
|
+
]),
|
|
1467
|
+
]);
|
|
1468
|
+
if (exceptionLines) await builder.appendRequiredBlock("structural search exceptions", exceptionLines);
|
|
1469
|
+
const bounded = await builder.finish();
|
|
1470
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
1471
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
1472
|
+
for (const [unit, ids] of locatorByUnit) {
|
|
1473
|
+
if (!visible.has(unit)) for (const id of ids) locators.delete(id);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (bounded.overflow.temporaryPath) orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
1477
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{ type: "text", text: bounded.content }],
|
|
1480
|
+
details: {
|
|
1481
|
+
kind: "astSearch",
|
|
1482
|
+
result,
|
|
1483
|
+
declarationCount: summary.matchesReturned,
|
|
1484
|
+
sourceBytes: summary.sourceBytes,
|
|
1485
|
+
returnedBytes,
|
|
1486
|
+
avoidedBytes: Math.max(0, summary.sourceBytes - returnedBytes),
|
|
1487
|
+
truncated: bounded.overflow.truncated,
|
|
1488
|
+
overflow: bounded.overflow,
|
|
1489
|
+
},
|
|
1490
|
+
};
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
await builder.abort();
|
|
1493
|
+
for (const ids of locatorByUnit.values()) for (const id of ids) locators.delete(id);
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
renderCall(args, theme, context) {
|
|
1498
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1499
|
+
const options = `[${args.language ?? "infer"} limit=${args.resultLimit}]`;
|
|
1500
|
+
const component =
|
|
1501
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1502
|
+
new AstCallComponent(
|
|
1503
|
+
rowState,
|
|
1504
|
+
context.toolCallId,
|
|
1505
|
+
"ast_search",
|
|
1506
|
+
[stripLeadingAt(args.path)],
|
|
1507
|
+
[options],
|
|
1508
|
+
theme,
|
|
1509
|
+
);
|
|
1510
|
+
component.set([stripLeadingAt(args.path)], [options], theme);
|
|
1511
|
+
return component;
|
|
1512
|
+
},
|
|
1513
|
+
renderResult(result, options, theme, context) {
|
|
1514
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1515
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
function relationshipTool(name: RelationshipOperation, description: string, guideline: string) {
|
|
1520
|
+
return defineTool<typeof relationshipParams, AstToolDetails>({
|
|
1521
|
+
name,
|
|
1522
|
+
label: name,
|
|
1523
|
+
description,
|
|
1524
|
+
promptSnippet: description,
|
|
1525
|
+
promptGuidelines: [guideline, "Inspect ambiguous candidate locators before selecting an edit target."],
|
|
1526
|
+
parameters: relationshipParams,
|
|
1527
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
1528
|
+
const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
|
|
1529
|
+
if (!(await stat(path)).isDirectory()) throw new Error(`${name} requires a directory scope`);
|
|
1530
|
+
const record = locators.get(params.locator);
|
|
1531
|
+
if (!record)
|
|
1532
|
+
throw new Error(`Unknown declaration locator: ${params.locator}. Run outline or api_discover again.`);
|
|
1533
|
+
if (record.stale || record.generation !== client.getGeneration()) {
|
|
1534
|
+
throw new Error(`Declaration locator ${params.locator} is stale. Run outline or api_discover again.`);
|
|
1535
|
+
}
|
|
1536
|
+
const result = await client.relationships(path, record.token, name, params.resultLimit, signal);
|
|
1537
|
+
orientation.recordAttempts(
|
|
1538
|
+
result.relationships.flatMap((relationship) => [
|
|
1539
|
+
{
|
|
1540
|
+
path: resolve(path, relationship.relativePath),
|
|
1541
|
+
fingerprint: relationship.sourceFingerprint,
|
|
1542
|
+
kind: "relationshipLocation" as const,
|
|
1543
|
+
toolCallId,
|
|
1544
|
+
sourceBytesDeflected: 0,
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
path: relationship.targetPath,
|
|
1548
|
+
fingerprint: relationship.targetSourceFingerprint,
|
|
1549
|
+
kind: "relationshipLocation" as const,
|
|
1550
|
+
toolCallId,
|
|
1551
|
+
sourceBytesDeflected: 0,
|
|
1552
|
+
},
|
|
1553
|
+
...relationship.candidatePaths.flatMap((candidatePath, index) => {
|
|
1554
|
+
const fingerprint = relationship.candidateSourceFingerprints[index];
|
|
1555
|
+
return fingerprint
|
|
1556
|
+
? [
|
|
1557
|
+
{
|
|
1558
|
+
path: candidatePath,
|
|
1559
|
+
fingerprint,
|
|
1560
|
+
kind: "relationshipLocation" as const,
|
|
1561
|
+
toolCallId,
|
|
1562
|
+
sourceBytesDeflected: 0,
|
|
1563
|
+
},
|
|
1564
|
+
]
|
|
1565
|
+
: [];
|
|
1566
|
+
}),
|
|
1567
|
+
{
|
|
1568
|
+
path: resolve(path, relationship.relativePath),
|
|
1569
|
+
fingerprint: relationship.enclosingScope.sourceFingerprint,
|
|
1570
|
+
kind: "relationshipScope" as const,
|
|
1571
|
+
toolCallId,
|
|
1572
|
+
sourceBytesDeflected: 0,
|
|
1573
|
+
},
|
|
1574
|
+
]),
|
|
1575
|
+
);
|
|
1576
|
+
const builder = new BoundedTextResultBuilder(temporaryOutput, "completeBlocks");
|
|
1577
|
+
const locatorByUnit = new Map<string, number[]>();
|
|
1578
|
+
try {
|
|
1579
|
+
for (const [index, relationship] of result.relationships.entries()) {
|
|
1580
|
+
const sourcePath = resolve(path, relationship.relativePath);
|
|
1581
|
+
const targetId =
|
|
1582
|
+
relationship.targetLocator === record.token
|
|
1583
|
+
? record.id
|
|
1584
|
+
: registerLocator(
|
|
1585
|
+
relationship.targetLocator,
|
|
1586
|
+
relationship.targetPath,
|
|
1587
|
+
`${relationship.relationshipKind} target`,
|
|
1588
|
+
);
|
|
1589
|
+
const candidateIds = relationship.candidateLocators.map((token, candidateIndex) =>
|
|
1590
|
+
token === record.token
|
|
1591
|
+
? record.id
|
|
1592
|
+
: registerLocator(
|
|
1593
|
+
token,
|
|
1594
|
+
relationship.candidatePaths[candidateIndex] ?? relationship.targetPath,
|
|
1595
|
+
"relationship candidate",
|
|
1596
|
+
),
|
|
1597
|
+
);
|
|
1598
|
+
const scopeId = registerLocator(
|
|
1599
|
+
relationship.enclosingScope.locator,
|
|
1600
|
+
sourcePath,
|
|
1601
|
+
relationship.enclosingScope.qualifiedIdentity,
|
|
1602
|
+
);
|
|
1603
|
+
const ids = [targetId, ...candidateIds, scopeId];
|
|
1604
|
+
const unit = `${relationship.relativePath}:${relationship.range.startByte}:${index}`;
|
|
1605
|
+
locatorByUnit.set(unit, ids);
|
|
1606
|
+
const kind =
|
|
1607
|
+
relationship.classification === "production"
|
|
1608
|
+
? relationship.relationshipKind
|
|
1609
|
+
: `${relationship.relationshipKind}, ${relationship.classification}`;
|
|
1610
|
+
const certainty = relationship.certainty === "exact" ? "" : `, ${relationship.certainty}`;
|
|
1611
|
+
const lines = [
|
|
1612
|
+
`${relationship.relativePath}:${displayLineRange(relationship.range)}(${scopeId}) [${kind}${certainty}]`,
|
|
1613
|
+
...relationship.sitePreview.split("\n").map((line) => ` ${line}`),
|
|
1614
|
+
];
|
|
1615
|
+
if (relationship.sitePreviewTruncated) lines.push(" [preview truncated]");
|
|
1616
|
+
if (relationship.certainty === "ambiguous") {
|
|
1617
|
+
lines.push(
|
|
1618
|
+
` candidates: ${candidateIds.map((id) => `(${id})`).join(", ")}${relationship.competingCandidatesOmitted ? `; ${relationship.competingCandidatesOmitted} omitted` : ""}`,
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (relationship.certaintyReason) lines.push(` uncertainty: ${relationship.certaintyReason}`);
|
|
1622
|
+
if (targetId !== record.id) lines.push(` target (${targetId})`);
|
|
1623
|
+
await builder.appendBlock(unit, relationship.relativePath, lines.join("\n"));
|
|
1624
|
+
}
|
|
1625
|
+
if (result.relationships.length === 0)
|
|
1626
|
+
await builder.appendRequiredBlock("no relationships", `No direct ${name} found`);
|
|
1627
|
+
const summary = result.summary;
|
|
1628
|
+
const exceptionLines = exceptionSummaryLines([
|
|
1629
|
+
...(summary.relationshipsFound > summary.relationshipsReturned
|
|
1630
|
+
? [
|
|
1631
|
+
`${summary.relationshipsFound - summary.relationshipsReturned} relationships omitted (result limit ${summary.resultLimit})`,
|
|
1632
|
+
]
|
|
1633
|
+
: []),
|
|
1634
|
+
...(summary.ambiguousRelationships > 0 ? [`${summary.ambiguousRelationships} ambiguous`] : []),
|
|
1635
|
+
...(summary.parserDegradedFiles > 0 || summary.diagnostics > 0
|
|
1636
|
+
? [
|
|
1637
|
+
`exceptions: parser degraded ${summary.parserDegradedFiles}, diagnostics ${summary.diagnostics}`,
|
|
1638
|
+
]
|
|
1639
|
+
: []),
|
|
1640
|
+
...limitReachedLine([
|
|
1641
|
+
...(summary.resultLimitReached ? ["results"] : []),
|
|
1642
|
+
...(summary.fileLimitReached ? ["files"] : []),
|
|
1643
|
+
...(summary.sourceByteLimitReached ? ["source bytes"] : []),
|
|
1644
|
+
...(summary.depthLimitReached ? ["depth"] : []),
|
|
1645
|
+
...(summary.elapsedLimitReached ? ["elapsed time"] : []),
|
|
1646
|
+
]),
|
|
1647
|
+
]);
|
|
1648
|
+
if (exceptionLines) await builder.appendRequiredBlock("relationship exceptions", exceptionLines);
|
|
1649
|
+
const bounded = await builder.finish();
|
|
1650
|
+
if (!bounded.overflow.fullOutputComplete) {
|
|
1651
|
+
const visible = new Set(bounded.visibleUnitIds);
|
|
1652
|
+
for (const [unit, ids] of locatorByUnit)
|
|
1653
|
+
if (!visible.has(unit)) for (const id of ids) if (id !== record.id) locators.delete(id);
|
|
1654
|
+
}
|
|
1655
|
+
if (bounded.overflow.temporaryPath) orientation.recordTemporaryOutput(bounded.overflow.temporaryPath);
|
|
1656
|
+
const returnedBytes = Buffer.byteLength(bounded.content);
|
|
1657
|
+
return {
|
|
1658
|
+
content: [{ type: "text", text: bounded.content }],
|
|
1659
|
+
details: {
|
|
1660
|
+
kind: "relationship",
|
|
1661
|
+
result,
|
|
1662
|
+
declarationCount: summary.relationshipsReturned,
|
|
1663
|
+
sourceBytes: summary.sourceBytes,
|
|
1664
|
+
returnedBytes,
|
|
1665
|
+
avoidedBytes: Math.max(0, summary.sourceBytes - returnedBytes),
|
|
1666
|
+
truncated: bounded.overflow.truncated,
|
|
1667
|
+
overflow: bounded.overflow,
|
|
1668
|
+
},
|
|
1669
|
+
};
|
|
1670
|
+
} catch (error) {
|
|
1671
|
+
await builder.abort();
|
|
1672
|
+
for (const ids of locatorByUnit.values())
|
|
1673
|
+
for (const id of ids) if (id !== record.id) locators.delete(id);
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
renderCall(args, theme, context) {
|
|
1678
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1679
|
+
const component =
|
|
1680
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1681
|
+
new AstCallComponent(
|
|
1682
|
+
rowState,
|
|
1683
|
+
context.toolCallId,
|
|
1684
|
+
name,
|
|
1685
|
+
[stripLeadingAt(args.path)],
|
|
1686
|
+
[`[${args.locator} limit=${args.resultLimit}]`],
|
|
1687
|
+
theme,
|
|
1688
|
+
);
|
|
1689
|
+
component.set([stripLeadingAt(args.path)], [`[${args.locator} limit=${args.resultLimit}]`], theme);
|
|
1690
|
+
return component;
|
|
1691
|
+
},
|
|
1692
|
+
renderResult(result, options, theme, context) {
|
|
1693
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1694
|
+
return renderAstResult(result, options.expanded, theme, context);
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const references = relationshipTool(
|
|
1700
|
+
"references",
|
|
1701
|
+
"Find direct references and type usages for a declaration locator.",
|
|
1702
|
+
"Use references to inspect direct repository usage and re-exports.",
|
|
1703
|
+
);
|
|
1704
|
+
const callers = relationshipTool(
|
|
1705
|
+
"callers",
|
|
1706
|
+
"Find direct callers for a declaration locator.",
|
|
1707
|
+
"Use callers for syntactic call sites; inferred dispatch is labelled.",
|
|
1708
|
+
);
|
|
1709
|
+
const callees = relationshipTool(
|
|
1710
|
+
"callees",
|
|
1711
|
+
"Find direct callees inside a declaration locator.",
|
|
1712
|
+
"Use callees to inspect direct dependencies of one executable scope.",
|
|
1713
|
+
);
|
|
1714
|
+
const implementations = relationshipTool(
|
|
1715
|
+
"implementations",
|
|
1716
|
+
"Find implementations and overrides for a declaration locator.",
|
|
1717
|
+
"Use implementations for syntactic inheritance and conservative same-name overrides.",
|
|
1718
|
+
);
|
|
1719
|
+
const tests = relationshipTool(
|
|
1720
|
+
"tests",
|
|
1721
|
+
"Find directly affected tests for a declaration locator.",
|
|
1722
|
+
"Use tests after selecting a change target, or earlier when the task starts from test behavior or a failure.",
|
|
1723
|
+
);
|
|
1724
|
+
|
|
287
1725
|
return {
|
|
288
1726
|
outline,
|
|
289
1727
|
symbol,
|
|
1728
|
+
replace_declaration: replaceDeclaration,
|
|
1729
|
+
replace_body: replaceBody,
|
|
1730
|
+
insert_declaration: insertDeclaration,
|
|
1731
|
+
rename_declaration: renameDeclaration,
|
|
1732
|
+
api_discover: apiDiscover,
|
|
1733
|
+
ast_search: astSearch,
|
|
1734
|
+
references,
|
|
1735
|
+
callers,
|
|
1736
|
+
callees,
|
|
1737
|
+
implementations,
|
|
1738
|
+
tests,
|
|
290
1739
|
clear() {
|
|
291
1740
|
locators.clear();
|
|
292
1741
|
nextLocator = 1;
|
|
1742
|
+
orientation.clear();
|
|
1743
|
+
},
|
|
1744
|
+
resetForTree() {
|
|
1745
|
+
locators.clear();
|
|
1746
|
+
nextLocator = 1;
|
|
1747
|
+
orientation.resetGate();
|
|
293
1748
|
},
|
|
294
1749
|
invalidate(paths: readonly string[]) {
|
|
295
1750
|
const changed = new Set(paths.map((path) => resolve(path)));
|
|
296
1751
|
for (const record of locators.values()) {
|
|
297
1752
|
if (changed.has(record.path)) record.stale = true;
|
|
298
1753
|
}
|
|
1754
|
+
orientation.invalidate(paths);
|
|
299
1755
|
},
|
|
300
1756
|
};
|
|
301
1757
|
}
|
|
302
1758
|
|
|
1759
|
+
function formatApiQuery(query: ApiQuery): string {
|
|
1760
|
+
switch (query.kind) {
|
|
1761
|
+
case "exactName":
|
|
1762
|
+
case "prefixName":
|
|
1763
|
+
case "substringName":
|
|
1764
|
+
case "fuzzyName":
|
|
1765
|
+
return `${query.kind}=${query.name}`;
|
|
1766
|
+
case "declarationKind":
|
|
1767
|
+
return `kind=${query.declarationKind}`;
|
|
1768
|
+
case "documentation":
|
|
1769
|
+
return `docs=${query.terms.join(",")}`;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function displayLineRange(range: SourceRange): string {
|
|
1774
|
+
const endLine =
|
|
1775
|
+
range.endByte > range.startByte && range.end.column === 0
|
|
1776
|
+
? Math.max(range.start.line, range.end.line - 1)
|
|
1777
|
+
: range.end.line;
|
|
1778
|
+
return range.start.line === endLine ? `${range.start.line + 1}` : `${range.start.line + 1}-${endLine + 1}`;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function symbolOptions(view: SymbolView, contextLines: number | undefined): string {
|
|
1782
|
+
return `[${view}${contextLines === undefined ? "" : ` context=${contextLines}`}]`;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
303
1785
|
function renderAstResult(
|
|
304
1786
|
result: { content: Array<{ type: string; text?: string }>; details?: AstToolDetails },
|
|
305
1787
|
expanded: boolean,
|
|
@@ -310,7 +1792,14 @@ function renderAstResult(
|
|
|
310
1792
|
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
311
1793
|
if (!expanded && !context.isError) {
|
|
312
1794
|
const declarationCount = details?.declarationCount ?? 0;
|
|
313
|
-
const noun =
|
|
1795
|
+
const noun =
|
|
1796
|
+
details?.kind === "locatorEdit"
|
|
1797
|
+
? declarationCount === 1
|
|
1798
|
+
? "fresh locator"
|
|
1799
|
+
: "fresh locators"
|
|
1800
|
+
: declarationCount === 1
|
|
1801
|
+
? "declaration"
|
|
1802
|
+
: "declarations";
|
|
314
1803
|
const byteSummary = details
|
|
315
1804
|
? `, ${formatSize(details.returnedBytes)} returned, ${formatSize(details.avoidedBytes)} avoided`
|
|
316
1805
|
: "";
|
|
@@ -407,7 +1896,11 @@ function truncateLeft(text: string, width: number): string {
|
|
|
407
1896
|
}
|
|
408
1897
|
|
|
409
1898
|
function outlineOptionVariants(args: OutlineArgs): string[] {
|
|
410
|
-
const fixed =
|
|
1899
|
+
const fixed = [
|
|
1900
|
+
...(args.recursive ? ["recursive"] : []),
|
|
1901
|
+
...(args.includePrivate ? ["private"] : []),
|
|
1902
|
+
...(args.includeDocs ? ["docs"] : []),
|
|
1903
|
+
];
|
|
411
1904
|
const names = args.names ?? [];
|
|
412
1905
|
if (names.length === 0) return [fixed.length > 0 ? `[${fixed.join(" ")}]` : ""];
|
|
413
1906
|
|
|
@@ -463,29 +1956,39 @@ function symbolTargetVariants(ids: readonly number[], locators: ReadonlyMap<numb
|
|
|
463
1956
|
return variants;
|
|
464
1957
|
}
|
|
465
1958
|
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1959
|
+
function sourceFingerprintForPlan(source: string): string {
|
|
1960
|
+
return `sha256:${createHash("sha256").update(source, "utf8").digest("hex")}`;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
function locatorEditCall(
|
|
1964
|
+
tool: string,
|
|
1965
|
+
id: number,
|
|
1966
|
+
locators: ReadonlyMap<number, LocatorRecord>,
|
|
1967
|
+
rowState: ToolRowStateStore,
|
|
1968
|
+
theme: Theme,
|
|
1969
|
+
context: {
|
|
1970
|
+
toolCallId: string;
|
|
1971
|
+
invalidate: () => void;
|
|
1972
|
+
lastComponent?: Component;
|
|
1973
|
+
},
|
|
1974
|
+
option = "",
|
|
1975
|
+
): Component {
|
|
1976
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
1977
|
+
const record = locators.get(id);
|
|
1978
|
+
const targets = record ? [`${record.name}@${basename(record.path)}`, String(id)] : [String(id)];
|
|
1979
|
+
const options = [option ? `[${option}]` : ""];
|
|
1980
|
+
const component =
|
|
1981
|
+
(context.lastComponent as AstCallComponent | undefined) ??
|
|
1982
|
+
new AstCallComponent(rowState, context.toolCallId, tool, targets, options, theme);
|
|
1983
|
+
component.set(targets, options, theme);
|
|
1984
|
+
return component;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function exceptionSummaryLines(parts: string[]): string | undefined {
|
|
1988
|
+
const lines = parts.filter((part) => part.length > 0);
|
|
1989
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function limitReachedLine(limits: string[]): string[] {
|
|
1993
|
+
return limits.length > 0 ? [`limits reached: ${limits.join(", ")}`] : [];
|
|
491
1994
|
}
|