@shanepadgett/tau-agent 0.27.0 → 0.28.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.
Files changed (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +130 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +716 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +31 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -1,1994 +0,0 @@
1
- import { defineTool, formatSize, keyHint, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { StringEnum } from "@earendil-works/pi-ai";
3
- import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
4
- import { createHash } from "node:crypto";
5
- import { realpath, stat } from "node:fs/promises";
6
- import { basename, resolve } from "node:path";
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
- 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";
24
- import { formatPathForDisplay, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
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,
46
- } from "./ast-worker.ts";
47
-
48
- const supportedLanguageLabels = formatAstLanguageLabels(AST_LANGUAGE_REGISTRY, "or");
49
-
50
- const outlineParams = Type.Object(
51
- {
52
- path: Type.String({
53
- description: `${supportedLanguageLabels} source file or directory`,
54
- }),
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
- ),
59
- names: Type.Optional(
60
- Type.Array(Type.String(), {
61
- minItems: 1,
62
- description: "Exact top-level or member declaration names",
63
- }),
64
- ),
65
- recursive: Type.Optional(
66
- Type.Boolean({ description: "Recursively outline every supported source file below a directory" }),
67
- ),
68
- },
69
- { additionalProperties: false },
70
- );
71
- const symbolParams = Type.Object(
72
- {
73
- locators: Type.Array(Type.Integer({ minimum: 1 }), {
74
- minItems: 1,
75
- description: "Numeric locators shown in parentheses by outline",
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
- }),
80
- contextLines: Type.Optional(
81
- Type.Integer({ minimum: 0, description: "Lines of source context before and after each declaration" }),
82
- ),
83
- },
84
- { additionalProperties: false },
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
- }
232
-
233
- interface AstToolDetails {
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[] };
243
- declarationCount: number;
244
- sourceBytes: number;
245
- returnedBytes: number;
246
- avoidedBytes: number;
247
- truncated: boolean;
248
- overflow: BoundedTextOverflowDetails | undefined;
249
- }
250
-
251
- interface LocatorRecord {
252
- id: number;
253
- token: string;
254
- path: string;
255
- name: string;
256
- stale: boolean;
257
- declarationRetrieved: boolean;
258
- generation: number;
259
- }
260
-
261
- type OutlineArgs = {
262
- path: string;
263
- includePrivate?: boolean;
264
- includeDocs?: boolean;
265
- names?: string[];
266
- recursive?: boolean;
267
- };
268
-
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
- ) {
276
- const locators = new Map<number, LocatorRecord>();
277
- let nextLocator = 1;
278
-
279
- function compact(
280
- text: string,
281
- sourceBytes: number,
282
- declarationCount: number,
283
- kind: AstToolDetails["kind"],
284
- result: OutlineTargetResult | SymbolBatchResult,
285
- ): { text: string; visibleText: string; details: AstToolDetails } {
286
- const truncation = truncateBoundedHead(text);
287
- const returned = truncation.truncated
288
- ? `${truncation.content}\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`
289
- : truncation.content;
290
- const returnedBytes = Buffer.byteLength(returned);
291
- return {
292
- text: returned,
293
- visibleText: truncation.content,
294
- details: {
295
- kind,
296
- result,
297
- declarationCount,
298
- sourceBytes,
299
- returnedBytes,
300
- avoidedBytes: Math.max(0, sourceBytes - returnedBytes),
301
- truncated: truncation.truncated,
302
- overflow: undefined,
303
- },
304
- };
305
- }
306
-
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 {
313
- const id = nextLocator++;
314
- const record = {
315
- id,
316
- token,
317
- path: resolve(path),
318
- name,
319
- stale: false,
320
- declarationRetrieved: false,
321
- generation: client.getGeneration(),
322
- };
323
- locators.set(id, record);
324
- return id;
325
- }
326
-
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");
447
- }
448
- signature = signature.replace(/\s+/g, " ");
449
- const label = signature && signature.includes(entry.name) ? signature : `${entry.name} ${signature}`.trim();
450
- return `${indent}${lines}(${locator(entry, path)}): ${label}`;
451
- }
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
-
548
- function renderOutlineFile(file: OutlineFileResult, cwd: string, includeHeader: boolean): string[] {
549
- const lines = includeHeader ? [formatPathForDisplay(file.path, cwd)] : [];
550
- if (file.diagnostics.errorNodes > 0 || file.diagnostics.missingNodes > 0) {
551
- lines.push(
552
- `warning: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
553
- );
554
- }
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
- }
672
- const groups = new Map<string, typeof declarations>();
673
- for (const item of declarations) {
674
- const visibility = item.isExported ? "public" : "private";
675
- const key = `${visibility} ${item.symbolType}`;
676
- const group = groups.get(key);
677
- if (group) group.push(item);
678
- else groups.set(key, [item]);
679
- }
680
- for (const [label, items] of groups) {
681
- if (lines.length > 0) lines.push("");
682
- lines.push(label);
683
- for (const item of items) {
684
- lines.push(renderEntry(item, file.path, file.language, ""));
685
- for (const member of item.members) {
686
- lines.push(renderEntry(member, file.path, file.language, " "));
687
- }
688
- }
689
- }
690
- return lines;
691
- }
692
-
693
- const outline = defineTool<typeof outlineParams, AstToolDetails>({
694
- name: "outline",
695
- label: "outline",
696
- description:
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.",
698
- promptSnippet: "Inspect public declarations and get symbol locators without reading implementation bodies",
699
- promptGuidelines: [
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.",
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.",
705
- "Use symbol with several locators when complete declaration source is needed.",
706
- ],
707
- parameters: outlineParams,
708
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
709
- const path = await realpath(resolveExplorePath(ctx.cwd, params.path));
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
- }
845
- const target: OutlineTarget = metadata.isDirectory()
846
- ? { kind: "directory", path }
847
- : { kind: "file", path, language: requireAstLanguageForPath(path) };
848
- const names = params.names ?? [];
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]);
886
- const declarationCount = result.files.reduce(
887
- (count, file) =>
888
- count +
889
- file.items.reduce(
890
- (fileCount, item) => fileCount + (item.rowKind === "declaration" ? 1 + item.members.length : 0),
891
- 0,
892
- ),
893
- 0,
894
- );
895
- if (declarationCount === 0) lines.push(names.length > 0 ? "No matching declarations" : "No declarations");
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
- });
916
- return { content: [{ type: "text", text: output.text }], details: output.details };
917
- },
918
- renderCall(args, theme, context) {
919
- rowState.watch(context.toolCallId, context.invalidate);
920
- const component =
921
- (context.lastComponent as AstCallComponent | undefined) ??
922
- new AstCallComponent(
923
- rowState,
924
- context.toolCallId,
925
- "outline",
926
- [stripLeadingAt(args.path)],
927
- outlineOptionVariants(args),
928
- theme,
929
- );
930
- component.set([stripLeadingAt(args.path)], outlineOptionVariants(args), theme);
931
- return component;
932
- },
933
- renderResult(result, options, theme, context) {
934
- rowState.watch(context.toolCallId, context.invalidate);
935
- return renderAstResult(result, options.expanded, theme, context);
936
- },
937
- });
938
-
939
- const symbol = defineTool<typeof symbolParams, AstToolDetails>({
940
- name: "symbol",
941
- label: "symbol",
942
- description:
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",
945
- parameters: symbolParams,
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
- }
950
- const records = [...new Set(params.locators)].map((id) => {
951
- const record = locators.get(id);
952
- if (!record) throw new Error(`Unknown symbol locator: ${id}. Run outline again.`);
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
- }
957
- return record;
958
- });
959
- const result = await client.symbol(
960
- [...new Set(records.map((record) => record.token))],
961
- params.view,
962
- params.contextLines ?? 0,
963
- signal,
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
- );
974
- const requestedByToken = new Map<string, LocatorRecord[]>();
975
- for (const record of records) {
976
- const requested = requestedByToken.get(record.token);
977
- if (requested) requested.push(record);
978
- else requestedByToken.set(record.token, [record]);
979
- }
980
- const lines: string[] = [];
981
- const includePaths = new Set(result.blocks.map((block) => block.path)).size > 1;
982
- for (const [blockIndex, block] of result.blocks.entries()) {
983
- if (blockIndex > 0) lines.push("");
984
- const represented = block.declarationIndexes.flatMap((index) => {
985
- const declaration = result.declarations[index];
986
- return declaration ? (requestedByToken.get(declaration.locator) ?? []) : [];
987
- });
988
- const range = block.returnedRange;
989
- const lineRange = displayLineRange(range);
990
- lines.push(
991
- ...(includePaths ? [formatPathForDisplay(block.path, ctx.cwd)] : []),
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
- }),
999
- block.source,
1000
- );
1001
- }
1002
- const sourceBytes = result.blocks.reduce((count, block) => count + Buffer.byteLength(block.source), 0);
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;
1008
- return { content: [{ type: "text", text: output.text }], details: output.details };
1009
- },
1010
- renderCall(args, theme, context) {
1011
- rowState.watch(context.toolCallId, context.invalidate);
1012
- const targets = symbolTargetVariants(args.locators, locators);
1013
- const component =
1014
- (context.lastComponent as AstCallComponent | undefined) ??
1015
- new AstCallComponent(
1016
- rowState,
1017
- context.toolCallId,
1018
- "symbol",
1019
- targets,
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}]`],
1346
- theme,
1347
- );
1348
- component.set([stripLeadingAt(args.path)], [`[${query} ${args.surface} limit=${args.resultLimit}]`], theme);
1349
- return component;
1350
- },
1351
- renderResult(result, options, theme, context) {
1352
- rowState.watch(context.toolCallId, context.invalidate);
1353
- return renderAstResult(result, options.expanded, theme, context);
1354
- },
1355
- });
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
-
1725
- return {
1726
- outline,
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,
1739
- clear() {
1740
- locators.clear();
1741
- nextLocator = 1;
1742
- orientation.clear();
1743
- },
1744
- resetForTree() {
1745
- locators.clear();
1746
- nextLocator = 1;
1747
- orientation.resetGate();
1748
- },
1749
- invalidate(paths: readonly string[]) {
1750
- const changed = new Set(paths.map((path) => resolve(path)));
1751
- for (const record of locators.values()) {
1752
- if (changed.has(record.path)) record.stale = true;
1753
- }
1754
- orientation.invalidate(paths);
1755
- },
1756
- };
1757
- }
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
-
1785
- function renderAstResult(
1786
- result: { content: Array<{ type: string; text?: string }>; details?: AstToolDetails },
1787
- expanded: boolean,
1788
- theme: Theme,
1789
- context: { lastComponent?: Component; isError: boolean },
1790
- ): Text {
1791
- const details = result.details;
1792
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
1793
- if (!expanded && !context.isError) {
1794
- const declarationCount = details?.declarationCount ?? 0;
1795
- const noun =
1796
- details?.kind === "locatorEdit"
1797
- ? declarationCount === 1
1798
- ? "fresh locator"
1799
- : "fresh locators"
1800
- : declarationCount === 1
1801
- ? "declaration"
1802
- : "declarations";
1803
- const byteSummary = details
1804
- ? `, ${formatSize(details.returnedBytes)} returned, ${formatSize(details.avoidedBytes)} avoided`
1805
- : "";
1806
- text.setText(
1807
- theme.fg("muted", `${declarationCount} ${noun}${byteSummary} (`) +
1808
- keyHint("app.tools.expand", "to expand") +
1809
- theme.fg("muted", ")"),
1810
- );
1811
- return text;
1812
- }
1813
-
1814
- const output = result.content
1815
- .filter((item): item is { type: string; text: string } => item.type === "text" && typeof item.text === "string")
1816
- .map((item) => item.text)
1817
- .join("\n");
1818
- text.setText(
1819
- output
1820
- ? output
1821
- .split("\n")
1822
- .map((line) => theme.fg("toolOutput", line))
1823
- .join("\n")
1824
- : "",
1825
- );
1826
- return text;
1827
- }
1828
-
1829
- class AstCallComponent implements Component {
1830
- private readonly rowState: ToolRowStateStore;
1831
- private readonly rowId: string;
1832
- private readonly tool: string;
1833
- private targetVariants: string[];
1834
- private optionVariants: string[];
1835
- private theme: Theme;
1836
-
1837
- constructor(
1838
- rowState: ToolRowStateStore,
1839
- rowId: string,
1840
- tool: string,
1841
- targetVariants: string[],
1842
- optionVariants: string[],
1843
- theme: Theme,
1844
- ) {
1845
- this.rowState = rowState;
1846
- this.rowId = rowId;
1847
- this.tool = tool;
1848
- this.targetVariants = targetVariants;
1849
- this.optionVariants = optionVariants;
1850
- this.theme = theme;
1851
- }
1852
-
1853
- set(targetVariants: string[], optionVariants: string[], theme: Theme): void {
1854
- this.targetVariants = targetVariants;
1855
- this.optionVariants = optionVariants;
1856
- this.theme = theme;
1857
- }
1858
-
1859
- render(width: number): string[] {
1860
- if (width <= 0) return [];
1861
- const prefixWidth = visibleWidth(`${this.tool} → `);
1862
- const shortestTarget = this.targetVariants.at(-1) ?? "";
1863
- const minimumTargetWidth = Math.min(12, visibleWidth(shortestTarget));
1864
- const options =
1865
- this.optionVariants.find(
1866
- (candidate) => prefixWidth + minimumTargetWidth + (candidate ? visibleWidth(candidate) + 1 : 0) <= width,
1867
- ) ??
1868
- this.optionVariants.at(-1) ??
1869
- "";
1870
- const optionsWidth = options ? visibleWidth(options) + 1 : 0;
1871
- const targetWidth = Math.max(1, width - prefixWidth - optionsWidth);
1872
- const target = this.targetVariants.find((candidate) => visibleWidth(candidate) <= targetWidth) ?? shortestTarget;
1873
- const displayTarget = truncateLeft(target, targetWidth);
1874
- const line =
1875
- formatToolRowTitle(this.rowState, this.rowId, this.tool, this.theme) +
1876
- this.theme.fg("toolOutput", " → ") +
1877
- this.theme.fg("accent", displayTarget) +
1878
- (options ? ` ${this.theme.fg("muted", options)}` : "");
1879
- return [truncateToWidth(line, width, "")];
1880
- }
1881
-
1882
- invalidate(): void {}
1883
- }
1884
-
1885
- function truncateLeft(text: string, width: number): string {
1886
- if (width <= 0) return "";
1887
- if (visibleWidth(text) <= width) return text;
1888
- if (width === 1) return "…";
1889
-
1890
- let suffix = "";
1891
- for (const character of Array.from(text).reverse()) {
1892
- if (visibleWidth(`…${character}${suffix}`) > width) break;
1893
- suffix = character + suffix;
1894
- }
1895
- return `…${suffix}`;
1896
- }
1897
-
1898
- function outlineOptionVariants(args: OutlineArgs): string[] {
1899
- const fixed = [
1900
- ...(args.recursive ? ["recursive"] : []),
1901
- ...(args.includePrivate ? ["private"] : []),
1902
- ...(args.includeDocs ? ["docs"] : []),
1903
- ];
1904
- const names = args.names ?? [];
1905
- if (names.length === 0) return [fixed.length > 0 ? `[${fixed.join(" ")}]` : ""];
1906
-
1907
- const variants = [];
1908
- for (let shown = names.length; shown >= 1; shown -= 1) {
1909
- const omitted = names.length - shown;
1910
- const namesText = omitted > 0 ? `${names.slice(0, shown).join(",")},+${omitted}` : names.join(",");
1911
- variants.push(`[${[...fixed, `names=${namesText}`].join(" ")}]`);
1912
- }
1913
- variants.push(`[${[...fixed, `names=${names.length}`].join(" ")}]`);
1914
- return variants;
1915
- }
1916
-
1917
- function symbolTargetVariants(ids: readonly number[], locators: ReadonlyMap<number, LocatorRecord>): string[] {
1918
- const records: LocatorRecord[] = [];
1919
- for (const id of new Set(ids)) {
1920
- const record = locators.get(id);
1921
- if (!record) return [ids.join(",")];
1922
- records.push(record);
1923
- }
1924
- const first = records[0];
1925
- if (!first) return ["symbols"];
1926
-
1927
- const fileCount = new Set(records.map((record) => record.path)).size;
1928
- const variants: string[] = [];
1929
- if (fileCount === 1) {
1930
- const file = basename(first.path);
1931
- for (let shown = records.length; shown >= 1; shown -= 1) {
1932
- const omitted = records.length - shown;
1933
- const suffix = omitted > 0 ? `,+${omitted}` : "";
1934
- variants.push(
1935
- `${file}: ${records
1936
- .slice(0, shown)
1937
- .map((record) => record.name)
1938
- .join(",")}${suffix}`,
1939
- );
1940
- }
1941
- variants.push(`${file}: ${records.length} symbols`);
1942
- return variants;
1943
- }
1944
-
1945
- for (let shown = records.length; shown >= 1; shown -= 1) {
1946
- const omitted = records.length - shown;
1947
- const suffix = omitted > 0 ? `,+${omitted}` : "";
1948
- variants.push(
1949
- `${records
1950
- .slice(0, shown)
1951
- .map((record) => `${record.name}@${basename(record.path)}`)
1952
- .join(",")}${suffix}`,
1953
- );
1954
- }
1955
- variants.push(`${records.length} symbols in ${fileCount} files`);
1956
- return variants;
1957
- }
1958
-
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(", ")}`] : [];
1994
- }