@shanepadgett/tau-agent 0.26.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 (121) hide show
  1. package/docs/subagents.md +1 -1
  2. package/extensions/appshot/README.md +1 -1
  3. package/extensions/appshot/index.ts +5 -11
  4. package/extensions/context/sync.ts +1 -9
  5. package/extensions/context-pruning/README.md +2 -2
  6. package/extensions/context-pruning/index.ts +58 -57
  7. package/extensions/context-pruning/prune.ts +2 -2
  8. package/extensions/context-pruning/render.ts +51 -39
  9. package/extensions/context-pruning/settings.ts +4 -5
  10. package/extensions/explore/README.md +13 -14
  11. package/extensions/explore/ast/adapter.ts +97 -0
  12. package/extensions/explore/ast/engine.ts +470 -0
  13. package/extensions/explore/ast/format/ast-search.ts +97 -0
  14. package/extensions/explore/ast/format/composite.ts +26 -0
  15. package/extensions/explore/ast/format/context.ts +66 -0
  16. package/extensions/explore/ast/format/deps.ts +86 -0
  17. package/extensions/explore/ast/format/discover.ts +85 -0
  18. package/extensions/explore/ast/format/impact.ts +105 -0
  19. package/extensions/explore/ast/format/outline.ts +66 -0
  20. package/extensions/explore/ast/format/relationships.ts +81 -0
  21. package/extensions/explore/ast/format/show.ts +38 -0
  22. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  23. package/extensions/explore/ast/grammars/manifest.json +41 -0
  24. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  25. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  26. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  27. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  28. package/extensions/explore/ast/graph/relationships.ts +597 -0
  29. package/extensions/explore/ast/identity.ts +119 -0
  30. package/extensions/explore/ast/ir.ts +130 -0
  31. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  32. package/extensions/explore/ast/languages/csharp.ts +493 -0
  33. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  34. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  35. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  36. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  37. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  38. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  39. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  40. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  41. package/extensions/explore/ast/languages/go.ts +545 -0
  42. package/extensions/explore/ast/languages/java.ts +412 -0
  43. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  44. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  45. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  46. package/extensions/explore/ast/languages/odin.ts +407 -0
  47. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  48. package/extensions/explore/ast/languages/rust.ts +506 -0
  49. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  50. package/extensions/explore/ast/languages/swift.ts +466 -0
  51. package/extensions/explore/ast/languages/tree.ts +220 -0
  52. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  53. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  54. package/extensions/explore/ast/languages/typescript.ts +716 -0
  55. package/extensions/explore/ast/markdown.ts +145 -0
  56. package/extensions/explore/ast/package-surface.ts +42 -0
  57. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  58. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  59. package/extensions/explore/ast/queries/context.ts +406 -0
  60. package/extensions/explore/ast/queries/discover.ts +427 -0
  61. package/extensions/explore/ast/queries/impact.ts +250 -0
  62. package/extensions/explore/ast/queries/outline.ts +203 -0
  63. package/extensions/explore/ast/queries/show.ts +262 -0
  64. package/extensions/explore/ast/query.ts +48 -0
  65. package/extensions/explore/ast/read/hook.ts +125 -0
  66. package/extensions/explore/ast/read/policy.ts +55 -0
  67. package/extensions/explore/ast/registry.ts +120 -0
  68. package/extensions/explore/ast/scan.ts +76 -0
  69. package/extensions/explore/ast/slice.ts +31 -0
  70. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  71. package/extensions/explore/ast/tools/context.ts +126 -0
  72. package/extensions/explore/ast/tools/deps.ts +72 -0
  73. package/extensions/explore/ast/tools/discover.ts +208 -0
  74. package/extensions/explore/ast/tools/impact.ts +135 -0
  75. package/extensions/explore/ast/tools/outline.ts +200 -0
  76. package/extensions/explore/ast/tools/relationships.ts +160 -0
  77. package/extensions/explore/ast/tools/render.ts +121 -0
  78. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  79. package/extensions/explore/ast/tools/show.ts +116 -0
  80. package/extensions/explore/guidance.ts +52 -0
  81. package/extensions/explore/index.ts +91 -45
  82. package/extensions/explore/read/autoread.ts +118 -0
  83. package/extensions/explore/settings.ts +64 -0
  84. package/extensions/explore/traverse.ts +260 -86
  85. package/extensions/handoff/index.ts +1 -1
  86. package/extensions/patch/executor.ts +95 -1
  87. package/extensions/patch/index.ts +1 -0
  88. package/extensions/publish/index.ts +18 -85
  89. package/extensions/publish/publish-ui.ts +0 -4
  90. package/extensions/soul/README.md +7 -4
  91. package/extensions/soul/index.ts +11 -5
  92. package/extensions/soul/prompt.ts +30 -14
  93. package/extensions/soul/settings.ts +7 -2
  94. package/extensions/subagent/agents/context-sync.md +6 -10
  95. package/extensions/subagent/agents/review.md +44 -24
  96. package/extensions/subagent/agents/scout.md +53 -34
  97. package/extensions/subagent/run.ts +1 -1
  98. package/extensions/tau-help/help.md +2 -2
  99. package/package.json +6 -5
  100. package/schemas/tau.schema.json +51 -6
  101. package/{extensions/explore → shared}/autoread.ts +21 -7
  102. package/shared/bounded-text-result.ts +304 -0
  103. package/shared/events.ts +1 -0
  104. package/shared/full-file-knowledge.ts +49 -0
  105. package/shared/temporary-output-store.ts +275 -0
  106. package/extensions/explore/ast-tools.ts +0 -828
  107. package/extensions/explore/ast-worker.ts +0 -356
  108. package/extensions/explore/find.ts +0 -147
  109. package/extensions/explore/full-file-knowledge.ts +0 -234
  110. package/extensions/explore/grep.ts +0 -749
  111. package/extensions/explore/limits.ts +0 -22
  112. package/extensions/explore/ls.ts +0 -113
  113. package/extensions/explore/path-display.ts +0 -41
  114. package/extensions/explore/path-tree.ts +0 -150
  115. package/extensions/explore/read-cache.ts +0 -273
  116. package/extensions/explore/read-snapshots.ts +0 -57
  117. package/extensions/explore/read-stats-panel.ts +0 -154
  118. package/extensions/explore/read-stats.ts +0 -120
  119. package/extensions/explore/read.ts +0 -335
  120. package/extensions/explore/result.ts +0 -29
  121. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -1,828 +0,0 @@
1
- import {
2
- DEFAULT_MAX_BYTES,
3
- DEFAULT_MAX_LINES,
4
- defineTool,
5
- formatSize,
6
- keyHint,
7
- truncateHead,
8
- type Theme,
9
- } from "@earendil-works/pi-coding-agent";
10
- import { StringEnum } from "@earendil-works/pi-ai";
11
- import { Text, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
12
- import { stat } from "node:fs/promises";
13
- import { basename, extname, resolve } from "node:path";
14
- import { Type } from "typebox";
15
- import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
16
- import { formatPathForDisplay, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
17
- import type {
18
- AstClient,
19
- AstLanguage,
20
- OutlineEntry,
21
- OutlineFileResult,
22
- SourceRange,
23
- OutlineTarget,
24
- OutlineTargetResult,
25
- SymbolBatchResult,
26
- SymbolView,
27
- } from "./ast-worker.ts";
28
-
29
- const outlineParams = Type.Object(
30
- {
31
- path: Type.String({
32
- description: "TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, Swift, or Markdown source file or directory",
33
- }),
34
- includePrivate: Type.Optional(Type.Boolean({ description: "Include private declarations and members" })),
35
- includeDocs: Type.Optional(
36
- Type.Boolean({ description: "Include attached documentation comments in declaration signatures" }),
37
- ),
38
- names: Type.Optional(
39
- Type.Array(Type.String(), {
40
- minItems: 1,
41
- description: "Exact top-level or member declaration names",
42
- }),
43
- ),
44
- },
45
- { additionalProperties: false },
46
- );
47
- const symbolParams = Type.Object(
48
- {
49
- locators: Type.Array(Type.Integer({ minimum: 1 }), {
50
- minItems: 1,
51
- description: "Numeric locators shown in parentheses by outline",
52
- }),
53
- view: StringEnum(["signature", "declaration", "declarationWithImports"] as const, {
54
- description:
55
- "Source view to retrieve; TypeScript, TSX, Odin, Go, Rust, C#, Java, Kotlin, Swift, and Markdown support selective views",
56
- }),
57
- contextLines: Type.Optional(
58
- Type.Integer({ minimum: 0, description: "Lines of source context before and after each declaration" }),
59
- ),
60
- },
61
- { additionalProperties: false },
62
- );
63
-
64
- interface AstToolDetails {
65
- kind: "outline" | "symbol";
66
- result: OutlineTargetResult | SymbolBatchResult;
67
- declarationCount: number;
68
- sourceBytes: number;
69
- returnedBytes: number;
70
- avoidedBytes: number;
71
- truncated: boolean;
72
- }
73
-
74
- interface LocatorRecord {
75
- id: number;
76
- token: string;
77
- path: string;
78
- name: string;
79
- stale: boolean;
80
- declarationRetrieved: boolean;
81
- generation: number;
82
- }
83
-
84
- type OutlineArgs = { path: string; includePrivate?: boolean; includeDocs?: boolean; names?: string[] };
85
-
86
- export function createAstTools(client: AstClient, rowState: ToolRowStateStore) {
87
- const locators = new Map<number, LocatorRecord>();
88
- let nextLocator = 1;
89
-
90
- function compact(
91
- text: string,
92
- sourceBytes: number,
93
- declarationCount: number,
94
- kind: AstToolDetails["kind"],
95
- result: OutlineTargetResult | SymbolBatchResult,
96
- ): { text: string; details: AstToolDetails } {
97
- const truncation = truncateHead(text, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
98
- const returned = truncation.truncated
99
- ? `${truncation.content}\n\n[Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`
100
- : truncation.content;
101
- const returnedBytes = Buffer.byteLength(returned);
102
- return {
103
- text: returned,
104
- details: {
105
- kind,
106
- result,
107
- declarationCount,
108
- sourceBytes,
109
- returnedBytes,
110
- avoidedBytes: Math.max(0, sourceBytes - returnedBytes),
111
- truncated: truncation.truncated,
112
- },
113
- };
114
- }
115
-
116
- function locator(entry: OutlineEntry, path: string): number {
117
- if (!entry.locator) throw new Error(`Structural outline row ${entry.name} has no symbol locator`);
118
- const id = nextLocator++;
119
- const record = {
120
- id,
121
- token: entry.locator,
122
- path: resolve(path),
123
- name: entry.name,
124
- stale: false,
125
- declarationRetrieved: false,
126
- generation: client.getGeneration(),
127
- };
128
- locators.set(id, record);
129
- return id;
130
- }
131
-
132
- function renderEntry(
133
- entry: OutlineEntry,
134
- path: string,
135
- language: AstLanguage,
136
- indent: string,
137
- signatureOverride?: string,
138
- ): string {
139
- const lines = displayLineRange(entry.range);
140
- let signature = (signatureOverride ?? entry.signature).trim();
141
- if (
142
- language === "typeScript" ||
143
- language === "tsx" ||
144
- language === "odin" ||
145
- language === "go" ||
146
- language === "rust" ||
147
- language === "cSharp" ||
148
- language === "java" ||
149
- language === "kotlin" ||
150
- language === "swift" ||
151
- language === "markdown"
152
- ) {
153
- const signatureLines = signature.split("\n");
154
- const relativeNameLine = entry.nameRange.start.line - entry.range.start.line;
155
- const keywordNameLine =
156
- language === "go"
157
- ? signatureLines.findIndex(
158
- (line) => /\b(?:func|type|const|var)\b/.test(line) && line.includes(entry.name),
159
- )
160
- : -1;
161
- let inBlockComment = false;
162
- let annotationDepth = 0;
163
- let attributeDepth = 0;
164
- const metadataLines = signatureLines.map((line) => {
165
- const trimmed = line.trimStart();
166
- if (inBlockComment) {
167
- if (trimmed.includes("*/")) inBlockComment = false;
168
- return true;
169
- }
170
- if (trimmed.startsWith("/*")) {
171
- inBlockComment = !trimmed.includes("*/");
172
- return true;
173
- }
174
- if (annotationDepth > 0) {
175
- annotationDepth += (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
176
- return true;
177
- }
178
- if (attributeDepth > 0) {
179
- attributeDepth += (line.match(/\[/g) ?? []).length - (line.match(/\]/g) ?? []).length;
180
- return true;
181
- }
182
- if (trimmed.startsWith("@") && !trimmed.startsWith(`@interface ${entry.name}`)) {
183
- annotationDepth = (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
184
- return true;
185
- }
186
- if (trimmed.startsWith("#[")) {
187
- attributeDepth = (line.match(/\[/g) ?? []).length - (line.match(/\]/g) ?? []).length;
188
- return true;
189
- }
190
- return trimmed.startsWith("//");
191
- });
192
- const declarationNameLine = signatureLines.findIndex(
193
- (line, index) => !metadataLines[index] && line.includes(entry.name),
194
- );
195
- const nameLine =
196
- relativeNameLine >= 0 && relativeNameLine === declarationNameLine
197
- ? relativeNameLine
198
- : keywordNameLine >= 0
199
- ? keywordNameLine
200
- : declarationNameLine;
201
- const declarationLine = language === "markdown" ? Math.max(0, relativeNameLine) : nameLine >= 0 ? nameLine : 0;
202
- const sourceLine = signatureLines[declarationLine] ?? entry.name;
203
- const first =
204
- language === "markdown" || sourceLine.includes(entry.name) ? sourceLine : `${entry.name} ${sourceLine}`;
205
- const warning =
206
- entry.certainty === "certain"
207
- ? ""
208
- : ` [${entry.certainty}${entry.certaintyReason ? `: ${entry.certaintyReason}` : ""}]`;
209
- return [
210
- ...signatureLines.slice(0, declarationLine).map((line) => `${indent}${line}`),
211
- `${indent}${lines}(${locator(entry, path)}): ${first}${warning}`,
212
- ...signatureLines.slice(declarationLine + 1).map((line) => `${indent}${line}`),
213
- ].join("\n");
214
- }
215
- signature = signature.replace(/\s+/g, " ");
216
- const label = signature && signature.includes(entry.name) ? signature : `${entry.name} ${signature}`.trim();
217
- return `${indent}${lines}(${locator(entry, path)}): ${label}`;
218
- }
219
-
220
- function renderStructuralEntry(entry: OutlineEntry, indent: string): string {
221
- const lines = displayLineRange(entry.range);
222
- return `${indent}${lines}: ${entry.signature.trim().replace(/\s+/g, " ")}`;
223
- }
224
-
225
- function containerFrame(
226
- item: OutlineFileResult["items"][number],
227
- language: AstLanguage,
228
- ): { header: string; footer: string } | undefined {
229
- const firstMember = item.members[0];
230
- if (!firstMember) return undefined;
231
- if (
232
- (language === "odin" ||
233
- language === "cSharp" ||
234
- language === "java" ||
235
- language === "kotlin" ||
236
- language === "swift") &&
237
- item.bodyRange
238
- ) {
239
- const bodyOffset = item.bodyRange.startByte - item.range.startByte;
240
- const signature = Buffer.from(item.signature);
241
- if (bodyOffset >= 0 && bodyOffset < signature.byteLength) {
242
- const header = signature
243
- .subarray(0, bodyOffset + 1)
244
- .toString("utf8")
245
- .trimEnd();
246
- if (header.endsWith("{")) return { header, footer: "}" };
247
- }
248
- }
249
- if (
250
- item.symbolType === "class" ||
251
- item.symbolType === "struct" ||
252
- item.symbolType === "interface" ||
253
- item.symbolType === "enum" ||
254
- item.symbolType === "object" ||
255
- item.symbolType === "namespace"
256
- ) {
257
- const members = (
258
- language === "typeScript" ||
259
- language === "tsx" ||
260
- language === "cSharp" ||
261
- language === "java" ||
262
- language === "kotlin" ||
263
- language === "swift"
264
- ? item.members.filter(
265
- (member) => member.qualifiedName.split(".").slice(0, -1).join(".") === item.qualifiedName,
266
- )
267
- : item.members
268
- )
269
- .map((member) =>
270
- (language === "java" &&
271
- (member.symbolType === "class" ||
272
- member.symbolType === "struct" ||
273
- member.symbolType === "interface" ||
274
- member.symbolType === "enum") &&
275
- member.signature.endsWith("{")
276
- ? `${member.signature} … }`
277
- : member.signature
278
- )
279
- .split("\n")
280
- .map((line) => ` ${line}`)
281
- .join("\n"),
282
- )
283
- .join("\n");
284
- const braceSuffix = `\n${members}\n}`;
285
- if (item.signature.endsWith(braceSuffix))
286
- return { header: item.signature.slice(0, -braceSuffix.length), footer: "}" };
287
- const tupleMembers = item.members
288
- .map((member) =>
289
- `${member.signature},`
290
- .split("\n")
291
- .map((line) => ` ${line}`)
292
- .join("\n"),
293
- )
294
- .join("\n");
295
- const tupleSuffix = `\n${tupleMembers}\n);`;
296
- if (item.signature.endsWith(tupleSuffix))
297
- return { header: item.signature.slice(0, -tupleSuffix.length), footer: ");" };
298
- const tupleMarker = `\n${tupleMembers}\n)`;
299
- const tupleMarkerIndex = item.signature.lastIndexOf(tupleMarker);
300
- if (tupleMarkerIndex >= 0)
301
- return {
302
- header: item.signature.slice(0, tupleMarkerIndex),
303
- footer: item.signature.slice(tupleMarkerIndex + tupleMarker.length - 1),
304
- };
305
- if (item.symbolType === "class") return undefined;
306
- }
307
-
308
- const memberOffset = firstMember.range.startByte - item.range.startByte;
309
- const signature = Buffer.from(item.signature);
310
- if (memberOffset <= 0 || memberOffset > signature.byteLength) return undefined;
311
- const header = signature.subarray(0, memberOffset).toString("utf8").trimEnd();
312
- return header.endsWith("{") ? { header, footer: "}" } : undefined;
313
- }
314
-
315
- function renderOutlineFile(file: OutlineFileResult, cwd: string, includeHeader: boolean): string[] {
316
- const lines = includeHeader
317
- ? [
318
- `${formatPathForDisplay(file.path, cwd)} (${file.language}, ${file.lineCount} lines, ${formatSize(file.byteLength)})`,
319
- ]
320
- : [];
321
- if (file.diagnostics.errorNodes > 0 || file.diagnostics.missingNodes > 0) {
322
- lines.push(
323
- `warning: parser recovered with ${file.diagnostics.errorNodes} ERROR and ${file.diagnostics.missingNodes} MISSING nodes`,
324
- );
325
- }
326
- const declarations = file.items.filter((item) => item.rowKind === "declaration");
327
- if (
328
- file.language === "typeScript" ||
329
- file.language === "tsx" ||
330
- file.language === "odin" ||
331
- file.language === "go" ||
332
- file.language === "rust" ||
333
- file.language === "cSharp" ||
334
- file.language === "java" ||
335
- file.language === "kotlin" ||
336
- file.language === "swift" ||
337
- file.language === "markdown"
338
- ) {
339
- let previousSection = "";
340
- for (const item of file.items) {
341
- const section =
342
- item.rowKind === "package"
343
- ? "package"
344
- : item.rowKind === "import"
345
- ? "imports"
346
- : item.rowKind === "export"
347
- ? "exports"
348
- : item.rowKind === "sideEffect"
349
- ? "side effects"
350
- : "declarations";
351
- if (section !== previousSection) {
352
- if (lines.length > 0) lines.push("");
353
- lines.push(section);
354
- previousSection = section;
355
- }
356
- const frame = item.rowKind === "declaration" ? containerFrame(item, file.language) : undefined;
357
- lines.push(
358
- item.rowKind !== "declaration"
359
- ? renderStructuralEntry(item, "")
360
- : renderEntry(item, file.path, file.language, "", frame?.header),
361
- );
362
- const nestedContainers = new Map(
363
- file.language === "typeScript" ||
364
- file.language === "tsx" ||
365
- file.language === "odin" ||
366
- file.language === "cSharp" ||
367
- file.language === "java" ||
368
- file.language === "kotlin" ||
369
- file.language === "swift"
370
- ? [item, ...item.members]
371
- .filter(
372
- (entry) =>
373
- entry.symbolType === "class" ||
374
- entry.symbolType === "struct" ||
375
- entry.symbolType === "interface" ||
376
- entry.symbolType === "enum",
377
- )
378
- .map((entry) => [entry.qualifiedName, entry] as const)
379
- : [],
380
- );
381
- const emittedEnumSeparators = new Set<string>();
382
- const openNestedDepths: number[] = [];
383
- for (const [memberIndex, member] of item.members.entries()) {
384
- const depth =
385
- file.language === "typeScript" ||
386
- file.language === "tsx" ||
387
- file.language === "odin" ||
388
- file.language === "cSharp" ||
389
- file.language === "java" ||
390
- file.language === "kotlin" ||
391
- file.language === "swift"
392
- ? Math.max(1, member.qualifiedName.split(".").length - item.qualifiedName.split(".").length)
393
- : 1;
394
- const parent = member.qualifiedName.split(".").slice(0, -1).join(".");
395
- const siblings =
396
- file.language === "typeScript" ||
397
- file.language === "tsx" ||
398
- file.language === "odin" ||
399
- file.language === "cSharp" ||
400
- file.language === "java" ||
401
- file.language === "kotlin" ||
402
- file.language === "swift"
403
- ? item.members.filter(
404
- (candidate) => candidate.qualifiedName.split(".").slice(0, -1).join(".") === parent,
405
- )
406
- : [];
407
- const parentIsEnum = file.language === "java" && nestedContainers.get(parent)?.symbolType === "enum";
408
- while (openNestedDepths.length > 0 && (openNestedDepths.at(-1) ?? 0) >= depth) {
409
- const closingDepth = openNestedDepths.pop();
410
- if (closingDepth !== undefined) lines.push(`${" ".repeat(closingDepth)}}`);
411
- }
412
- if (
413
- parentIsEnum &&
414
- !siblings.some((candidate) => candidate.symbolType === "enumMember") &&
415
- !emittedEnumSeparators.has(parent)
416
- ) {
417
- lines.push(`${" ".repeat(depth)};`);
418
- emittedEnumSeparators.add(parent);
419
- }
420
- let memberSignature = member.signature;
421
- const nextMember = item.members[memberIndex + 1];
422
- const hasDescendants = nextMember?.qualifiedName.startsWith(`${member.qualifiedName}.`) ?? false;
423
- if (hasDescendants && memberSignature.endsWith(" { … }")) {
424
- memberSignature = `${memberSignature.slice(0, -" { … }".length)} {`;
425
- openNestedDepths.push(depth);
426
- }
427
- if (parentIsEnum && member.symbolType === "enumMember") {
428
- const constants = siblings.filter((candidate) => candidate.symbolType === "enumMember");
429
- const constantIndex = constants.indexOf(member);
430
- if (constantIndex + 1 < constants.length) memberSignature += ",";
431
- else if (siblings.some((candidate) => candidate.symbolType !== "enumMember")) memberSignature += ";";
432
- }
433
- lines.push(renderEntry(member, file.path, file.language, " ".repeat(depth), memberSignature));
434
- }
435
- while (openNestedDepths.length > 0) {
436
- const closingDepth = openNestedDepths.pop();
437
- if (closingDepth !== undefined) lines.push(`${" ".repeat(closingDepth)}}`);
438
- }
439
- if (frame) lines.push(frame.footer);
440
- }
441
- return lines;
442
- }
443
- const groups = new Map<string, typeof declarations>();
444
- for (const item of declarations) {
445
- const visibility = item.isExported ? "public" : "private";
446
- const key = `${visibility} ${item.symbolType}`;
447
- const group = groups.get(key);
448
- if (group) group.push(item);
449
- else groups.set(key, [item]);
450
- }
451
- for (const [label, items] of groups) {
452
- if (lines.length > 0) lines.push("");
453
- lines.push(label);
454
- for (const item of items) {
455
- lines.push(renderEntry(item, file.path, file.language, ""));
456
- for (const member of item.members) {
457
- lines.push(renderEntry(member, file.path, file.language, " "));
458
- }
459
- }
460
- }
461
- return lines;
462
- }
463
-
464
- const outline = defineTool<typeof outlineParams, AstToolDetails>({
465
- name: "outline",
466
- label: "outline",
467
- description:
468
- "Inspect public declarations in one supported source file or non-recursive package directory without returning implementation bodies. Parenthesized numbers are locators for symbol.",
469
- promptSnippet: "Inspect public declarations and get symbol locators without reading implementation bodies",
470
- promptGuidelines: [
471
- "Use a public package outline first to discover reusable APIs; add exact names when likely symbols are known.",
472
- "Set includePrivate when internal implementation discovery is needed.",
473
- "Leave includeDocs off for routine exploration; enable it when documentation comments are needed.",
474
- "Treat each parenthesized number after a line range as that declaration's symbol locator.",
475
- "Use symbol with several locators when complete declaration source is needed.",
476
- ],
477
- parameters: outlineParams,
478
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
479
- const path = resolveExplorePath(ctx.cwd, params.path);
480
- const metadata = await stat(path);
481
- const target: OutlineTarget = metadata.isDirectory()
482
- ? { kind: "directory", path }
483
- : { kind: "file", path, language: languageForPath(path) };
484
- const names = params.names ?? [];
485
- const result = await client.outline(
486
- target,
487
- params.includePrivate ?? false,
488
- params.includeDocs ?? false,
489
- names,
490
- signal,
491
- );
492
- const lines = result.files.flatMap((file, index) => [
493
- ...(index === 0 ? [] : [""]),
494
- ...renderOutlineFile(file, ctx.cwd, result.files.length > 1),
495
- ]);
496
- const declarationCount = result.files.reduce(
497
- (count, file) =>
498
- count +
499
- file.items.reduce(
500
- (fileCount, item) => fileCount + (item.rowKind === "declaration" ? 1 + item.members.length : 0),
501
- 0,
502
- ),
503
- 0,
504
- );
505
- if (declarationCount === 0) lines.push(names.length > 0 ? "No matching declarations" : "No declarations");
506
- const output = compact(lines.join("\n"), result.totalByteLength, declarationCount, "outline", result);
507
- return { content: [{ type: "text", text: output.text }], details: output.details };
508
- },
509
- renderCall(args, theme, context) {
510
- rowState.watch(context.toolCallId, context.invalidate);
511
- const component =
512
- (context.lastComponent as AstCallComponent | undefined) ??
513
- new AstCallComponent(
514
- rowState,
515
- context.toolCallId,
516
- "outline",
517
- [stripLeadingAt(args.path)],
518
- outlineOptionVariants(args),
519
- theme,
520
- );
521
- component.set([stripLeadingAt(args.path)], outlineOptionVariants(args), theme);
522
- return component;
523
- },
524
- renderResult(result, options, theme, context) {
525
- rowState.watch(context.toolCallId, context.invalidate);
526
- return renderAstResult(result, options.expanded, theme, context);
527
- },
528
- });
529
-
530
- const symbol = defineTool<typeof symbolParams, AstToolDetails>({
531
- name: "symbol",
532
- label: "symbol",
533
- description:
534
- "Return exact declaration source for one or more numeric outline locators, with optional surrounding lines; stale locators fail atomically.",
535
- promptSnippet: "Retrieve exact declaration source for several outline locators",
536
- parameters: symbolParams,
537
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
538
- if (params.contextLines !== undefined && params.view !== "declaration") {
539
- throw new Error("contextLines is supported only with view=declaration");
540
- }
541
- const records = [...new Set(params.locators)].map((id) => {
542
- const record = locators.get(id);
543
- if (!record) throw new Error(`Unknown symbol locator: ${id}. Run outline again.`);
544
- if (record.stale) throw new Error(`Symbol locator ${id} is stale. Run outline again.`);
545
- if (record.generation !== client.getGeneration()) {
546
- throw new Error(`Symbol locator ${id} is stale because the AST worker restarted. Run outline again.`);
547
- }
548
- return record;
549
- });
550
- const result = await client.symbol(
551
- [...new Set(records.map((record) => record.token))],
552
- params.view,
553
- params.contextLines ?? 0,
554
- signal,
555
- );
556
- const requestedByToken = new Map<string, LocatorRecord[]>();
557
- for (const record of records) {
558
- const requested = requestedByToken.get(record.token);
559
- if (requested) requested.push(record);
560
- else requestedByToken.set(record.token, [record]);
561
- }
562
- const lines: string[] = [];
563
- const includePaths = new Set(result.blocks.map((block) => block.path)).size > 1;
564
- for (const [blockIndex, block] of result.blocks.entries()) {
565
- if (blockIndex > 0) lines.push("");
566
- const represented = block.declarationIndexes.flatMap((index) => {
567
- const declaration = result.declarations[index];
568
- return declaration ? (requestedByToken.get(declaration.locator) ?? []) : [];
569
- });
570
- const range = block.returnedRange;
571
- const lineRange = displayLineRange(range);
572
- lines.push(
573
- ...(includePaths ? [formatPathForDisplay(block.path, ctx.cwd)] : []),
574
- `${lineRange}(${represented.map((record) => record.id).join(",")}): ${represented.map((record) => record.name).join(", ")}`,
575
- block.source,
576
- );
577
- }
578
- const sourceBytes = result.blocks.reduce((count, block) => count + Buffer.byteLength(block.source), 0);
579
- const output = compact(lines.join("\n"), sourceBytes, result.declarations.length, "symbol", result);
580
- if (output.details.truncated) {
581
- throw new Error("Symbol result exceeded the output limit. Request fewer locators.");
582
- }
583
- if (params.view === "declaration") for (const record of records) record.declarationRetrieved = true;
584
- return { content: [{ type: "text", text: output.text }], details: output.details };
585
- },
586
- renderCall(args, theme, context) {
587
- rowState.watch(context.toolCallId, context.invalidate);
588
- const targets = symbolTargetVariants(args.locators, locators);
589
- const component =
590
- (context.lastComponent as AstCallComponent | undefined) ??
591
- new AstCallComponent(
592
- rowState,
593
- context.toolCallId,
594
- "symbol",
595
- targets,
596
- [symbolOptions(args.view, args.contextLines)],
597
- theme,
598
- );
599
- component.set(targets, [symbolOptions(args.view, args.contextLines)], theme);
600
- return component;
601
- },
602
- renderResult(result, options, theme, context) {
603
- rowState.watch(context.toolCallId, context.invalidate);
604
- return renderAstResult(result, options.expanded, theme, context);
605
- },
606
- });
607
-
608
- return {
609
- outline,
610
- symbol,
611
- clear() {
612
- locators.clear();
613
- nextLocator = 1;
614
- },
615
- invalidate(paths: readonly string[]) {
616
- const changed = new Set(paths.map((path) => resolve(path)));
617
- for (const record of locators.values()) {
618
- if (changed.has(record.path)) record.stale = true;
619
- }
620
- },
621
- };
622
- }
623
-
624
- function displayLineRange(range: SourceRange): string {
625
- const endLine =
626
- range.endByte > range.startByte && range.end.column === 0
627
- ? Math.max(range.start.line, range.end.line - 1)
628
- : range.end.line;
629
- return range.start.line === endLine ? `${range.start.line + 1}` : `${range.start.line + 1}-${endLine + 1}`;
630
- }
631
-
632
- function symbolOptions(view: SymbolView, contextLines: number | undefined): string {
633
- return `[${view}${contextLines === undefined ? "" : ` context=${contextLines}`}]`;
634
- }
635
-
636
- function renderAstResult(
637
- result: { content: Array<{ type: string; text?: string }>; details?: AstToolDetails },
638
- expanded: boolean,
639
- theme: Theme,
640
- context: { lastComponent?: Component; isError: boolean },
641
- ): Text {
642
- const details = result.details;
643
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
644
- if (!expanded && !context.isError) {
645
- const declarationCount = details?.declarationCount ?? 0;
646
- const noun = declarationCount === 1 ? "declaration" : "declarations";
647
- const byteSummary = details
648
- ? `, ${formatSize(details.returnedBytes)} returned, ${formatSize(details.avoidedBytes)} avoided`
649
- : "";
650
- text.setText(
651
- theme.fg("muted", `${declarationCount} ${noun}${byteSummary} (`) +
652
- keyHint("app.tools.expand", "to expand") +
653
- theme.fg("muted", ")"),
654
- );
655
- return text;
656
- }
657
-
658
- const output = result.content
659
- .filter((item): item is { type: string; text: string } => item.type === "text" && typeof item.text === "string")
660
- .map((item) => item.text)
661
- .join("\n");
662
- text.setText(
663
- output
664
- ? output
665
- .split("\n")
666
- .map((line) => theme.fg("toolOutput", line))
667
- .join("\n")
668
- : "",
669
- );
670
- return text;
671
- }
672
-
673
- class AstCallComponent implements Component {
674
- private readonly rowState: ToolRowStateStore;
675
- private readonly rowId: string;
676
- private readonly tool: string;
677
- private targetVariants: string[];
678
- private optionVariants: string[];
679
- private theme: Theme;
680
-
681
- constructor(
682
- rowState: ToolRowStateStore,
683
- rowId: string,
684
- tool: string,
685
- targetVariants: string[],
686
- optionVariants: string[],
687
- theme: Theme,
688
- ) {
689
- this.rowState = rowState;
690
- this.rowId = rowId;
691
- this.tool = tool;
692
- this.targetVariants = targetVariants;
693
- this.optionVariants = optionVariants;
694
- this.theme = theme;
695
- }
696
-
697
- set(targetVariants: string[], optionVariants: string[], theme: Theme): void {
698
- this.targetVariants = targetVariants;
699
- this.optionVariants = optionVariants;
700
- this.theme = theme;
701
- }
702
-
703
- render(width: number): string[] {
704
- if (width <= 0) return [];
705
- const prefixWidth = visibleWidth(`${this.tool} → `);
706
- const shortestTarget = this.targetVariants.at(-1) ?? "";
707
- const minimumTargetWidth = Math.min(12, visibleWidth(shortestTarget));
708
- const options =
709
- this.optionVariants.find(
710
- (candidate) => prefixWidth + minimumTargetWidth + (candidate ? visibleWidth(candidate) + 1 : 0) <= width,
711
- ) ??
712
- this.optionVariants.at(-1) ??
713
- "";
714
- const optionsWidth = options ? visibleWidth(options) + 1 : 0;
715
- const targetWidth = Math.max(1, width - prefixWidth - optionsWidth);
716
- const target = this.targetVariants.find((candidate) => visibleWidth(candidate) <= targetWidth) ?? shortestTarget;
717
- const displayTarget = truncateLeft(target, targetWidth);
718
- const line =
719
- formatToolRowTitle(this.rowState, this.rowId, this.tool, this.theme) +
720
- this.theme.fg("toolOutput", " → ") +
721
- this.theme.fg("accent", displayTarget) +
722
- (options ? ` ${this.theme.fg("muted", options)}` : "");
723
- return [truncateToWidth(line, width, "")];
724
- }
725
-
726
- invalidate(): void {}
727
- }
728
-
729
- function truncateLeft(text: string, width: number): string {
730
- if (width <= 0) return "";
731
- if (visibleWidth(text) <= width) return text;
732
- if (width === 1) return "…";
733
-
734
- let suffix = "";
735
- for (const character of Array.from(text).reverse()) {
736
- if (visibleWidth(`…${character}${suffix}`) > width) break;
737
- suffix = character + suffix;
738
- }
739
- return `…${suffix}`;
740
- }
741
-
742
- function outlineOptionVariants(args: OutlineArgs): string[] {
743
- const fixed = [...(args.includePrivate ? ["private"] : []), ...(args.includeDocs ? ["docs"] : [])];
744
- const names = args.names ?? [];
745
- if (names.length === 0) return [fixed.length > 0 ? `[${fixed.join(" ")}]` : ""];
746
-
747
- const variants = [];
748
- for (let shown = names.length; shown >= 1; shown -= 1) {
749
- const omitted = names.length - shown;
750
- const namesText = omitted > 0 ? `${names.slice(0, shown).join(",")},+${omitted}` : names.join(",");
751
- variants.push(`[${[...fixed, `names=${namesText}`].join(" ")}]`);
752
- }
753
- variants.push(`[${[...fixed, `names=${names.length}`].join(" ")}]`);
754
- return variants;
755
- }
756
-
757
- function symbolTargetVariants(ids: readonly number[], locators: ReadonlyMap<number, LocatorRecord>): string[] {
758
- const records: LocatorRecord[] = [];
759
- for (const id of new Set(ids)) {
760
- const record = locators.get(id);
761
- if (!record) return [ids.join(",")];
762
- records.push(record);
763
- }
764
- const first = records[0];
765
- if (!first) return ["symbols"];
766
-
767
- const fileCount = new Set(records.map((record) => record.path)).size;
768
- const variants: string[] = [];
769
- if (fileCount === 1) {
770
- const file = basename(first.path);
771
- for (let shown = records.length; shown >= 1; shown -= 1) {
772
- const omitted = records.length - shown;
773
- const suffix = omitted > 0 ? `,+${omitted}` : "";
774
- variants.push(
775
- `${file}: ${records
776
- .slice(0, shown)
777
- .map((record) => record.name)
778
- .join(",")}${suffix}`,
779
- );
780
- }
781
- variants.push(`${file}: ${records.length} symbols`);
782
- return variants;
783
- }
784
-
785
- for (let shown = records.length; shown >= 1; shown -= 1) {
786
- const omitted = records.length - shown;
787
- const suffix = omitted > 0 ? `,+${omitted}` : "";
788
- variants.push(
789
- `${records
790
- .slice(0, shown)
791
- .map((record) => `${record.name}@${basename(record.path)}`)
792
- .join(",")}${suffix}`,
793
- );
794
- }
795
- variants.push(`${records.length} symbols in ${fileCount} files`);
796
- return variants;
797
- }
798
-
799
- function languageForPath(path: string): AstLanguage {
800
- switch (extname(path).toLowerCase()) {
801
- case ".ts":
802
- return "typeScript";
803
- case ".tsx":
804
- return "tsx";
805
- case ".odin":
806
- return "odin";
807
- case ".go":
808
- return "go";
809
- case ".rs":
810
- return "rust";
811
- case ".cs":
812
- return "cSharp";
813
- case ".java":
814
- return "java";
815
- case ".kt":
816
- case ".ktm":
817
- case ".kts":
818
- return "kotlin";
819
- case ".swift":
820
- return "swift";
821
- case ".md":
822
- case ".markdown":
823
- case ".mdown":
824
- return "markdown";
825
- default:
826
- throw new Error(`Unsupported outline file type: ${extname(path) || "no extension"}`);
827
- }
828
- }