@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
@@ -0,0 +1,203 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import type { ExploreEngine } from "../engine.ts";
3
+ import type { Decl, DeclKind, FileIr } from "../ir.ts";
4
+ import { filterDeclForest, walkDecls } from "../query.ts";
5
+ import { scanSources, type ScanOutcome } from "../scan.ts";
6
+ import { docsText, signatureText } from "../slice.ts";
7
+ import { formatPathForDisplay, pathResolutionError, resolveExplorePath, walkPaths } from "../../traverse.ts";
8
+
9
+ export type OutlineOptions = {
10
+ includePrivate: boolean;
11
+ includeDocs: boolean;
12
+ /** Exact `name` / `qualifiedName` filter. Empty → no name filter. */
13
+ names: readonly string[];
14
+ };
15
+
16
+ export type OutlineRow = {
17
+ depth: number;
18
+ startLine: number;
19
+ endLine: number;
20
+ kind: DeclKind;
21
+ name: string;
22
+ qualifiedName: string;
23
+ /** Signature source slice (no body). */
24
+ signature: string;
25
+ /** Attached docs when `includeDocs` and a span exists. */
26
+ docs: string | undefined;
27
+ };
28
+
29
+ export type OutlineFileView = {
30
+ /** Absolute path. */
31
+ path: string;
32
+ languageId: string;
33
+ parseDegraded: boolean;
34
+ rows: OutlineRow[];
35
+ };
36
+
37
+ export type OutlineResult = { mode: "file"; file: OutlineFileView } | { mode: "directory"; files: OutlineFileView[] };
38
+
39
+ function keepVisibility(includePrivate: boolean): (decl: Decl) => boolean {
40
+ if (includePrivate) return () => true;
41
+ return (decl) => decl.visibility === "public" || decl.visibility === "protected";
42
+ }
43
+
44
+ /** Keep decls that match a name, or ancestors of a match (structure preserved). */
45
+ function filterByNames(decls: readonly Decl[], names: ReadonlySet<string>): Decl[] {
46
+ const out: Decl[] = [];
47
+ for (const decl of decls) {
48
+ const selfMatch = names.has(decl.name) || names.has(decl.qualifiedName);
49
+ if (selfMatch) {
50
+ out.push(decl);
51
+ continue;
52
+ }
53
+ const children = filterByNames(decl.children, names);
54
+ if (children.length > 0) out.push({ ...decl, children });
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function prepareDecls(ir: FileIr, options: OutlineOptions): Decl[] {
60
+ let decls = filterDeclForest(ir.decls, keepVisibility(options.includePrivate));
61
+ if (options.names.length > 0) {
62
+ decls = filterByNames(decls, new Set(options.names));
63
+ }
64
+ return decls;
65
+ }
66
+
67
+ function outlineFromIr(ir: FileIr, source: string, options: OutlineOptions): OutlineFileView {
68
+ const decls = prepareDecls(ir, options);
69
+ const rows: OutlineRow[] = [];
70
+ walkDecls(decls, (decl, depth) => {
71
+ rows.push({
72
+ depth,
73
+ startLine: decl.startLine,
74
+ endLine: decl.endLine,
75
+ kind: decl.kind,
76
+ name: decl.name,
77
+ qualifiedName: decl.qualifiedName,
78
+ signature: signatureText(decl, source),
79
+ docs: options.includeDocs ? docsText(decl, source) : undefined,
80
+ });
81
+ });
82
+ return {
83
+ path: ir.path,
84
+ languageId: ir.languageId,
85
+ parseDegraded: ir.parseDegraded,
86
+ rows,
87
+ };
88
+ }
89
+
90
+ /** Multi-file: keep files with rows, or degraded parses (diagnostic only). */
91
+ function keepMultiFileView(view: OutlineFileView): boolean {
92
+ return view.rows.length > 0 || view.parseDegraded;
93
+ }
94
+
95
+ async function outlineAbsoluteFile(
96
+ engine: ExploreEngine,
97
+ absolutePath: string,
98
+ options: OutlineOptions,
99
+ ): Promise<OutlineFileView> {
100
+ const source = await engine.sourceForFile(absolutePath);
101
+ return outlineFromIr(source.ir, source.source, options);
102
+ }
103
+
104
+ /** Ignore-aware one-level file list. Intentional shallow walk — maxDepth limit is not a failure. */
105
+ async function listSupportedFilesOneLevel(
106
+ engine: ExploreEngine,
107
+ directory: string,
108
+ signal: AbortSignal,
109
+ ): Promise<string[]> {
110
+ const walk = await walkPaths({
111
+ cwd: engine.cwd,
112
+ root: directory,
113
+ filesOnly: true,
114
+ includeRoot: false,
115
+ budgets: { maxDepth: 1 },
116
+ signal,
117
+ matchFile: (hit) => engine.registry.adapterForPath(hit.absolutePath) !== undefined,
118
+ });
119
+ if (walk.limit === "cancelled" || signal.aborted) {
120
+ throw new Error("outline cancelled");
121
+ }
122
+ return walk.entries
123
+ .filter((hit) => hit.depth === 1 && hit.type === "file")
124
+ .map((hit) => hit.absolutePath)
125
+ .sort((a, b) => a.localeCompare(b));
126
+ }
127
+
128
+ /**
129
+ * File or one-level directory outline (non-recursive).
130
+ * Recursive streaming lives in the tool (complete-block emission).
131
+ */
132
+ export async function outlinePath(
133
+ engine: ExploreEngine,
134
+ pathInput: string,
135
+ options: OutlineOptions,
136
+ signal: AbortSignal,
137
+ ): Promise<OutlineResult> {
138
+ signal.throwIfAborted();
139
+ const absolutePath = resolveExplorePath(engine.cwd, pathInput);
140
+ let stats;
141
+ try {
142
+ stats = await lstat(absolutePath);
143
+ } catch (error) {
144
+ throw pathResolutionError(error, pathInput);
145
+ }
146
+
147
+ if (stats.isDirectory()) {
148
+ const paths = await listSupportedFilesOneLevel(engine, absolutePath, signal);
149
+ const files: OutlineFileView[] = [];
150
+ for (const filePath of paths) {
151
+ signal.throwIfAborted();
152
+ const view = await outlineAbsoluteFile(engine, filePath, options);
153
+ if (keepMultiFileView(view)) files.push(view);
154
+ }
155
+ return { mode: "directory", files };
156
+ }
157
+
158
+ if (!stats.isFile()) {
159
+ throw new Error(`Not a file or directory: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
160
+ }
161
+ if (engine.registry.adapterForPath(absolutePath) === undefined) {
162
+ throw new Error(`Unsupported language for path: ${formatPathForDisplay(absolutePath, engine.cwd)}`);
163
+ }
164
+ const file = await outlineAbsoluteFile(engine, absolutePath, options);
165
+ return { mode: "file", file };
166
+ }
167
+
168
+ /**
169
+ * Stream recursive outline file units. Yields IR+bytes together (one source read per file).
170
+ * Return value is the scan outcome (limits / counters).
171
+ */
172
+ export async function* outlineRecursive(
173
+ engine: ExploreEngine,
174
+ pathInput: string,
175
+ options: OutlineOptions,
176
+ signal: AbortSignal,
177
+ ): AsyncGenerator<OutlineFileView, ScanOutcome> {
178
+ signal.throwIfAborted();
179
+ const absolutePath = resolveExplorePath(engine.cwd, pathInput);
180
+ let stats;
181
+ try {
182
+ stats = await lstat(absolutePath);
183
+ } catch (error) {
184
+ throw pathResolutionError(error, pathInput);
185
+ }
186
+ if (!stats.isDirectory()) {
187
+ throw new Error("recursive outline requires a directory target");
188
+ }
189
+
190
+ const scan = scanSources({ engine, cwd: engine.cwd, root: absolutePath, signal });
191
+ let step = await scan.next();
192
+ while (!step.done) {
193
+ signal.throwIfAborted();
194
+ const view = outlineFromIr(step.value.ir, step.value.source, options);
195
+ if (keepMultiFileView(view)) yield view;
196
+ step = await scan.next();
197
+ }
198
+ const outcome = step.value;
199
+ if (outcome.limit === "cancelled" || signal.aborted) {
200
+ throw new Error("outline cancelled");
201
+ }
202
+ return outcome;
203
+ }
@@ -0,0 +1,262 @@
1
+ import type { ExploreEngine } from "../engine.ts";
2
+ import type { Candidate, Target } from "../identity.ts";
3
+ import { resolveTarget } from "../identity.ts";
4
+ import type { Decl, FileIr, ImportRef } from "../ir.ts";
5
+ import { declarationText, signatureText, signatureWithDocsText } from "../slice.ts";
6
+ import { formatPathForDisplay, stripLeadingAt } from "../../traverse.ts";
7
+
8
+ export type ShowView = "signature" | "signatureWithDocs" | "declaration" | "declarationWithImports";
9
+
10
+ export type ShowTargetInput = {
11
+ path: string;
12
+ name: string;
13
+ line?: number;
14
+ };
15
+
16
+ export type ShowBlock = {
17
+ path: string;
18
+ name: string;
19
+ qualifiedName: string;
20
+ startLine: number;
21
+ endLine: number;
22
+ text: string;
23
+ /** Real warnings only (e.g. no docs for signatureWithDocs). */
24
+ warnings: string[];
25
+ };
26
+
27
+ export type ShowBatch = {
28
+ blocks: ShowBlock[];
29
+ };
30
+
31
+ const IDENTIFIER = /[A-Za-z_$][\w$]*/gu;
32
+
33
+ const NO_IMPORT_NOISE: ReadonlySet<string> = new Set();
34
+
35
+ function targetKey(target: ShowTargetInput): string {
36
+ const line = target.line === undefined ? "" : String(target.line);
37
+ return `${stripLeadingAt(target.path)}\0${target.name}\0${line}`;
38
+ }
39
+
40
+ /** Dedupe identical path+name+line targets, preserving first-seen order. */
41
+ function dedupeShowTargets(targets: readonly ShowTargetInput[]): ShowTargetInput[] {
42
+ const seen = new Set<string>();
43
+ const out: ShowTargetInput[] = [];
44
+ for (const target of targets) {
45
+ const key = targetKey(target);
46
+ if (seen.has(key)) continue;
47
+ seen.add(key);
48
+ out.push({
49
+ path: stripLeadingAt(target.path),
50
+ name: target.name,
51
+ ...(target.line === undefined ? {} : { line: target.line }),
52
+ });
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function identifiersIn(text: string, noise: ReadonlySet<string>): Set<string> {
58
+ const out = new Set<string>();
59
+ for (const match of text.matchAll(IDENTIFIER)) {
60
+ const id = match[0];
61
+ if (id !== undefined && !noise.has(id)) out.add(id);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** Exact import statement text from adapter byte span. Noise set is language-owned. */
67
+ function importStatementsFor(
68
+ declText: string,
69
+ imports: readonly ImportRef[],
70
+ source: string,
71
+ noise: ReadonlySet<string>,
72
+ ): string[] {
73
+ const declIds = identifiersIn(declText, noise);
74
+ const seenSpans = new Set<string>();
75
+ const out: string[] = [];
76
+ for (const importRef of imports) {
77
+ const spanKey = `${importRef.startOffset}:${importRef.endOffset}`;
78
+ if (seenSpans.has(spanKey)) continue;
79
+ const statement = source.slice(importRef.startOffset, importRef.endOffset).replace(/\s+$/u, "");
80
+ if (statement.length === 0) continue;
81
+ const hit = [...identifiersIn(statement, noise)].some((id) => declIds.has(id));
82
+ if (!hit) continue;
83
+ seenSpans.add(spanKey);
84
+ out.push(statement);
85
+ }
86
+ return out;
87
+ }
88
+
89
+ function lineRanges(source: string): { start: number; end: number }[] {
90
+ const ranges: { start: number; end: number }[] = [];
91
+ let start = 0;
92
+ for (let i = 0; i < source.length; i += 1) {
93
+ if (source.charCodeAt(i) === 10) {
94
+ ranges.push({ start, end: i });
95
+ start = i + 1;
96
+ }
97
+ }
98
+ if (start < source.length || source.length === 0) {
99
+ ranges.push({ start, end: source.length });
100
+ }
101
+ return ranges;
102
+ }
103
+
104
+ function sliceLines(source: string, startLine: number, endLine: number): string {
105
+ const ranges = lineRanges(source);
106
+ if (ranges.length === 0) return "";
107
+ const lo = Math.max(1, startLine);
108
+ const hi = Math.min(ranges.length, endLine);
109
+ if (hi < lo) return "";
110
+ const start = ranges[lo - 1];
111
+ const end = ranges[hi - 1];
112
+ if (start === undefined || end === undefined) return "";
113
+ return source.slice(start.start, end.end);
114
+ }
115
+
116
+ /** Exact source slice for one resolved decl. Shared by show and context. */
117
+ export function extractShowView(
118
+ decl: Decl,
119
+ path: string,
120
+ ir: FileIr,
121
+ source: string,
122
+ view: ShowView,
123
+ importNoise: ReadonlySet<string> = NO_IMPORT_NOISE,
124
+ contextLines = 0,
125
+ ): ShowBlock {
126
+ return buildBlock(decl, path, ir, source, view, contextLines, importNoise);
127
+ }
128
+
129
+ function buildBlock(
130
+ decl: Decl,
131
+ path: string,
132
+ ir: FileIr,
133
+ source: string,
134
+ view: ShowView,
135
+ contextLines: number,
136
+ importNoise: ReadonlySet<string>,
137
+ ): ShowBlock {
138
+ const warnings: string[] = [];
139
+ let text: string;
140
+ let startLine = decl.startLine;
141
+ let endLine = decl.endLine;
142
+
143
+ switch (view) {
144
+ case "signature": {
145
+ text = signatureText(decl, source);
146
+ break;
147
+ }
148
+ case "signatureWithDocs": {
149
+ if (decl.docStartOffset === undefined || decl.docEndOffset === undefined) {
150
+ warnings.push("no attached documentation");
151
+ text = signatureText(decl, source);
152
+ } else {
153
+ text = signatureWithDocsText(decl, source);
154
+ }
155
+ break;
156
+ }
157
+ case "declaration": {
158
+ if (contextLines > 0) {
159
+ startLine = Math.max(1, decl.startLine - contextLines);
160
+ endLine = Math.min(Math.max(ir.lineCount, decl.endLine), decl.endLine + contextLines);
161
+ text = sliceLines(source, startLine, endLine);
162
+ } else {
163
+ text = declarationText(decl, source);
164
+ }
165
+ break;
166
+ }
167
+ case "declarationWithImports": {
168
+ const declText = declarationText(decl, source);
169
+ const importBlocks = importStatementsFor(declText, ir.imports, source, importNoise);
170
+ text = importBlocks.length > 0 ? `${importBlocks.join("\n")}\n\n${declText}` : declText;
171
+ break;
172
+ }
173
+ }
174
+
175
+ return {
176
+ path,
177
+ name: decl.name,
178
+ qualifiedName: decl.qualifiedName,
179
+ startLine,
180
+ endLine,
181
+ text,
182
+ warnings,
183
+ };
184
+ }
185
+
186
+ function formatCandidateError(label: string, candidates: readonly Candidate[], cwd: string): string {
187
+ const lines = [
188
+ `Ambiguous target ${label}:`,
189
+ ...candidates.map((candidate) => {
190
+ const path = formatPathForDisplay(candidate.path, cwd);
191
+ const range =
192
+ candidate.startLine === candidate.endLine
193
+ ? `L${candidate.startLine}`
194
+ : `L${candidate.startLine}-${candidate.endLine}`;
195
+ const sig = candidate.signature.replace(/\s+/gu, " ").trim();
196
+ return `${path}:${range} ${candidate.kind} ${candidate.qualifiedName} — ${sig}`;
197
+ }),
198
+ ];
199
+ return lines.join("\n");
200
+ }
201
+
202
+ /**
203
+ * Resolve every target before emitting. Any missing/ambiguous target fails the whole batch.
204
+ * `contextLines` must be omitted (undefined) unless view is declaration.
205
+ */
206
+ export async function showTargets(
207
+ engine: ExploreEngine,
208
+ targets: readonly ShowTargetInput[],
209
+ view: ShowView,
210
+ contextLines: number | undefined,
211
+ signal: AbortSignal,
212
+ ): Promise<ShowBatch> {
213
+ if (contextLines !== undefined && view !== "declaration") {
214
+ throw new Error("contextLines is supported only with view=declaration");
215
+ }
216
+ const context = contextLines ?? 0;
217
+
218
+ const unique = dedupeShowTargets(targets);
219
+ const blocks: ShowBlock[] = [];
220
+ let docsWarningEmitted = false;
221
+
222
+ for (const target of unique) {
223
+ signal.throwIfAborted();
224
+ const resolutionTarget: Target = {
225
+ path: target.path,
226
+ name: target.name,
227
+ ...(target.line === undefined ? {} : { line: target.line }),
228
+ };
229
+ const resolution = await resolveTarget(engine, engine.cwd, resolutionTarget, signal);
230
+ const label =
231
+ target.line === undefined
232
+ ? `${target.name} @ ${target.path}`
233
+ : `${target.name} @ ${target.path}:${target.line}`;
234
+
235
+ if (resolution.kind === "notFound") {
236
+ throw new Error(`No declaration matched ${label}`);
237
+ }
238
+ if (resolution.kind === "candidates") {
239
+ throw new Error(formatCandidateError(label, resolution.candidates, engine.cwd));
240
+ }
241
+
242
+ const adapter = engine.registry.adapterForPath(resolution.path);
243
+ const importNoise = adapter?.importNoiseIdentifiers ?? NO_IMPORT_NOISE;
244
+ const block = buildBlock(
245
+ resolution.decl,
246
+ resolution.path,
247
+ resolution.ir,
248
+ resolution.source,
249
+ view,
250
+ context,
251
+ importNoise,
252
+ );
253
+ if (docsWarningEmitted) {
254
+ block.warnings = block.warnings.filter((warning) => warning !== "no attached documentation");
255
+ } else if (block.warnings.includes("no attached documentation")) {
256
+ docsWarningEmitted = true;
257
+ }
258
+ blocks.push(block);
259
+ }
260
+
261
+ return { blocks };
262
+ }
@@ -0,0 +1,48 @@
1
+ import type { Decl } from "./ir.ts";
2
+
3
+ /** Depth-first visit of a decl forest. */
4
+ export function walkDecls(decls: readonly Decl[], visit: (decl: Decl, depth: number) => void): void {
5
+ const go = (nodes: readonly Decl[], depth: number): void => {
6
+ for (const decl of nodes) {
7
+ visit(decl, depth);
8
+ if (decl.children.length > 0) go(decl.children, depth + 1);
9
+ }
10
+ };
11
+ go(decls, 0);
12
+ }
13
+
14
+ function nameMatches(decl: Decl, name: string): boolean {
15
+ return decl.name === name || decl.qualifiedName === name;
16
+ }
17
+
18
+ function coversLine(decl: Decl, line: number): boolean {
19
+ return line >= decl.startLine && line <= decl.endLine;
20
+ }
21
+
22
+ function findByName(decls: readonly Decl[], name: string): Decl[] {
23
+ const out: Decl[] = [];
24
+ walkDecls(decls, (decl) => {
25
+ if (nameMatches(decl, name)) out.push(decl);
26
+ });
27
+ return out;
28
+ }
29
+
30
+ /** Name matches, then optional line filter. */
31
+ export function findTargets(decls: readonly Decl[], name: string, line?: number): Decl[] {
32
+ const named = findByName(decls, name);
33
+ if (line === undefined) return named;
34
+ return named.filter((decl) => coversLine(decl, line));
35
+ }
36
+
37
+ /** Drop private/internal decls from a forest; keep structure. */
38
+ export function filterDeclForest(decls: readonly Decl[], keep: (decl: Decl) => boolean): Decl[] {
39
+ const out: Decl[] = [];
40
+ for (const decl of decls) {
41
+ if (!keep(decl)) continue;
42
+ out.push({
43
+ ...decl,
44
+ children: filterDeclForest(decl.children, keep),
45
+ });
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,125 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadTauExtensionSettings } from "../../../../shared/settings/load.ts";
3
+ import exploreSettings from "../../settings.ts";
4
+ import { resolveExplorePath } from "../../traverse.ts";
5
+ import type { ExploreEngine } from "../engine.ts";
6
+ import { formatOutlineFile } from "../format/outline.ts";
7
+ import { outlinePath } from "../queries/outline.ts";
8
+ import {
9
+ countLines,
10
+ formatLargeReadOutline,
11
+ rangedReadOverLimitMessage,
12
+ readCallKind,
13
+ shouldOutlineFullRead,
14
+ type ExploreReadSettings,
15
+ } from "./policy.ts";
16
+
17
+ type ReadOverlayResult = {
18
+ content: Array<{ type: "text"; text: string }>;
19
+ isError: boolean;
20
+ };
21
+
22
+ const DEFAULT_OUTLINE_OPTIONS = {
23
+ includePrivate: false,
24
+ includeDocs: false,
25
+ names: [] as readonly string[],
26
+ };
27
+
28
+ export function registerReadOutlineHook(pi: ExtensionAPI, engineFor: (cwd: string) => ExploreEngine): void {
29
+ let readSettings: ExploreReadSettings = exploreSettings.defaults.read;
30
+
31
+ pi.on("session_start", async (_event, ctx) => {
32
+ const settings = await loadTauExtensionSettings(ctx, exploreSettings);
33
+ readSettings = settings.read;
34
+ });
35
+
36
+ pi.on("tool_result", async (event, ctx) => {
37
+ if (event.toolName !== "read") return;
38
+ if (event.isError) return;
39
+ if (!readSettings.enabled) return;
40
+ if (event.content.some((part) => part.type === "image")) return;
41
+
42
+ const path = readPathInput(event.input);
43
+ if (path === undefined) return;
44
+
45
+ const engine = engineFor(ctx.cwd);
46
+ const absolutePath = resolveExplorePath(engine.cwd, path);
47
+ if (engine.registry.adapterForPath(absolutePath) === undefined) return;
48
+
49
+ const kind = readCallKind(event.input);
50
+ if (kind === "ranged") {
51
+ return enforceRangedLimit(event.input, event.content, readSettings.maxRangeLines);
52
+ }
53
+
54
+ return substituteLargeFullRead({
55
+ engine,
56
+ path,
57
+ absolutePath,
58
+ structureThresholdLines: readSettings.structureThresholdLines,
59
+ });
60
+ });
61
+ }
62
+
63
+ async function substituteLargeFullRead(options: {
64
+ engine: ExploreEngine;
65
+ path: string;
66
+ absolutePath: string;
67
+ structureThresholdLines: number;
68
+ }): Promise<ReadOverlayResult | undefined> {
69
+ let lineCount: number;
70
+ try {
71
+ const source = await options.engine.sourceForFile(options.absolutePath);
72
+ lineCount = source.ir.lineCount;
73
+ } catch {
74
+ // Unreadable / unsupported mid-flight — leave Pi result alone.
75
+ return undefined;
76
+ }
77
+
78
+ if (!shouldOutlineFullRead(lineCount, options.structureThresholdLines)) return undefined;
79
+
80
+ try {
81
+ const result = await outlinePath(
82
+ options.engine,
83
+ options.path,
84
+ DEFAULT_OUTLINE_OPTIONS,
85
+ new AbortController().signal,
86
+ );
87
+ if (result.mode !== "file") return undefined;
88
+ const outlineText =
89
+ result.file.rows.length === 0 ? "No declarations" : formatOutlineFile(result.file, options.engine.cwd, false);
90
+ return {
91
+ content: [{ type: "text", text: formatLargeReadOutline(outlineText) }],
92
+ isError: false,
93
+ };
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ }
98
+
99
+ function enforceRangedLimit(
100
+ input: Record<string, unknown>,
101
+ content: ReadonlyArray<{ type: string; text?: string }>,
102
+ maxRangeLines: number,
103
+ ): ReadOverlayResult | undefined {
104
+ const limit = typeof input.limit === "number" && Number.isFinite(input.limit) ? input.limit : undefined;
105
+ const returnedLines = countLines(textFromContent(content));
106
+ const message = rangedReadOverLimitMessage({ limit, returnedLines, maxRangeLines });
107
+ if (message === undefined) return undefined;
108
+ return {
109
+ content: [{ type: "text", text: message }],
110
+ isError: true,
111
+ };
112
+ }
113
+
114
+ function readPathInput(input: Record<string, unknown>): string | undefined {
115
+ const path = input.path;
116
+ return typeof path === "string" && path.length > 0 ? path : undefined;
117
+ }
118
+
119
+ function textFromContent(content: ReadonlyArray<{ type: string; text?: string }>): string {
120
+ const parts: string[] = [];
121
+ for (const part of content) {
122
+ if (part.type === "text" && typeof part.text === "string") parts.push(part.text);
123
+ }
124
+ return parts.join("\n");
125
+ }
@@ -0,0 +1,55 @@
1
+ export type ExploreReadSettings = {
2
+ enabled: boolean;
3
+ structureThresholdLines: number;
4
+ maxRangeLines: number;
5
+ };
6
+
7
+ export type ReadCallKind = "full" | "ranged";
8
+
9
+ const LARGE_READ_OUTLINE_INSTRUCTION = "large file: use ranged read or show for bodies";
10
+
11
+ /** Newline-based line count (empty string → 0). */
12
+ export function countLines(text: string): number {
13
+ if (text.length === 0) return 0;
14
+ let count = 1;
15
+ for (let i = 0; i < text.length; i += 1) {
16
+ const code = text.charCodeAt(i);
17
+ if (code === 10) count += 1;
18
+ else if (code === 13) {
19
+ count += 1;
20
+ if (text.charCodeAt(i + 1) === 10) i += 1;
21
+ }
22
+ }
23
+ return count;
24
+ }
25
+
26
+ export function readCallKind(input: { offset?: unknown; limit?: unknown }): ReadCallKind {
27
+ const hasOffset = typeof input.offset === "number" && Number.isFinite(input.offset);
28
+ const hasLimit = typeof input.limit === "number" && Number.isFinite(input.limit);
29
+ return hasOffset || hasLimit ? "ranged" : "full";
30
+ }
31
+
32
+ export function shouldOutlineFullRead(lineCount: number, structureThresholdLines: number): boolean {
33
+ return lineCount > structureThresholdLines;
34
+ }
35
+
36
+ /** Hard-error message when a ranged read exceeds maxRangeLines; undefined when allowed. */
37
+ export function rangedReadOverLimitMessage(options: {
38
+ limit: number | undefined;
39
+ returnedLines: number;
40
+ maxRangeLines: number;
41
+ }): string | undefined {
42
+ const { limit, returnedLines, maxRangeLines } = options;
43
+ if (typeof limit === "number" && limit > maxRangeLines) {
44
+ return `Range limit ${limit} exceeds explore.read.maxRangeLines (${maxRangeLines}). Shrink limit and retry.`;
45
+ }
46
+ if (returnedLines > maxRangeLines) {
47
+ return `Ranged read returned ${returnedLines} lines; max is explore.read.maxRangeLines (${maxRangeLines}). Set a smaller limit and retry.`;
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ export function formatLargeReadOutline(outlineText: string): string {
53
+ if (outlineText.length === 0) return LARGE_READ_OUTLINE_INSTRUCTION;
54
+ return `${outlineText}\n${LARGE_READ_OUTLINE_INSTRUCTION}`;
55
+ }