@shanepadgett/tau-agent 0.33.1 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -0,0 +1,547 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { keyText, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
5
+ import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
6
+ import { Marker } from "@shanepadgett/tau-tui";
7
+ import { BoundedTextResultBuilder, truncateBoundedHead } from "../../shared/bounded-text-result.ts";
8
+ import { emitTauEvent, onTauEvent } from "../../shared/events.ts";
9
+ import { createCompleteFileMeta, type ReadCacheMetaV1 } from "../../shared/full-file-knowledge.ts";
10
+ import type { LineRange } from "../../shared/ranges.ts";
11
+ import type { TemporaryOutputStore } from "../../shared/temporary-output-store.ts";
12
+ import type { ToolRowStateStore } from "../../shared/tool-row-state.ts";
13
+ import { formatOutlineEmpty, formatOutlineFile } from "../ast/format/outline.ts";
14
+ import { outlinePath } from "../ast/queries/outline.ts";
15
+ import { showTargets, type ShowView } from "../ast/queries/show.ts";
16
+ import { formatLargeReadOutline, shouldOutlineFullRead } from "../ast/read-policy.ts";
17
+ import { astEngineFor } from "../ast/session.ts";
18
+ import { formatPathForDisplay, resolveExplorePath, stripLeadingAt } from "../ast/traverse.ts";
19
+
20
+ export const FILE_INJECTION_TYPE = "tau.file";
21
+
22
+ const OUTLINE_OPTIONS = { includePrivate: false, includeDocs: false, names: [] as readonly string[] };
23
+ const FILE_RENDER_CHARACTERS = 20_000;
24
+ const FILE_RENDER_LINES = 200;
25
+
26
+ /**
27
+ * `full` always injects complete contents, `outline` always injects structure,
28
+ * `show` injects one resolved declaration slice, and `auto` injects contents
29
+ * unless the source is larger than Explore's structural read threshold.
30
+ */
31
+ export type FileInjectionMode = "full" | "outline" | "auto" | "show";
32
+
33
+ export interface FileInjectionDetails {
34
+ v: 1;
35
+ rowId: string;
36
+ path: string;
37
+ cwd: string;
38
+ source: string;
39
+ batchId: string;
40
+ /** One-based, inclusive ranges used for a partial file read or show slice. */
41
+ ranges?: ReadonlyArray<LineRange>;
42
+ /** Declaration name when kind is show. */
43
+ showName?: string;
44
+ /** Extract view when kind is show. */
45
+ showView?: ShowView;
46
+ /** What was actually injected. `auto` resolves to `full` or `outline`. */
47
+ kind: "full" | "outline" | "show";
48
+ status: "injected" | "failed";
49
+ error?: string;
50
+ readCache?: ReadCacheMetaV1;
51
+ }
52
+
53
+ export interface PreparedFileInjection {
54
+ customType: typeof FILE_INJECTION_TYPE;
55
+ content: string;
56
+ display: boolean;
57
+ details: FileInjectionDetails;
58
+ }
59
+
60
+ export interface FileInjectionFile {
61
+ path: string;
62
+ mode: FileInjectionMode;
63
+ /** One-based, inclusive line ranges. Ranges take precedence over `auto` outlining. */
64
+ ranges?: ReadonlyArray<LineRange>;
65
+ /** Declaration name for mode `show`; may be dotted (`Type.method`). */
66
+ name?: string;
67
+ /** Show extract view; defaults to `declaration`. */
68
+ view?: ShowView;
69
+ }
70
+
71
+ export interface FileInjectionRequest {
72
+ cwd: string;
73
+ /** Caller identifier recorded on each message. */
74
+ source: string;
75
+ /** Groups the visible rows produced by one call. */
76
+ batchId: string;
77
+ files: ReadonlyArray<FileInjectionFile>;
78
+ signal?: AbortSignal;
79
+ }
80
+
81
+ interface FileInjectionHost {
82
+ temporaryOutput: TemporaryOutputStore;
83
+ autoOutline(): { enabled: boolean; thresholdLines: number };
84
+ }
85
+
86
+ function requireHost(host: FileInjectionHost | undefined): FileInjectionHost {
87
+ if (host === undefined) throw new Error("File injection is unavailable: Explore is not loaded");
88
+ return host;
89
+ }
90
+
91
+ /** Explore owns the parse engine settings and temporary output store this module needs. */
92
+ export function registerFileInjection(pi: ExtensionAPI, rowState: ToolRowStateStore, next: FileInjectionHost): void {
93
+ onTauEvent(pi, "explore.file-injection", "tau:file-injection.prepare", ({ request, accept }) => {
94
+ accept(prepareFileInjectionWithHost(request, next));
95
+ });
96
+ pi.registerMessageRenderer<FileInjectionDetails>(FILE_INJECTION_TYPE, (message, options, theme) => {
97
+ const details = parseDetails(message.details);
98
+ if (!details) return undefined;
99
+ return new FileInjectionComponent(
100
+ rowState,
101
+ details,
102
+ typeof message.content === "string" ? message.content : "",
103
+ options.expanded,
104
+ theme,
105
+ );
106
+ });
107
+ }
108
+
109
+ /** Builds injection messages without sending them. */
110
+ export async function prepareFileInjection(
111
+ pi: Pick<ExtensionAPI, "events">,
112
+ request: FileInjectionRequest,
113
+ ): Promise<PreparedFileInjection[]> {
114
+ let accepted: Promise<PreparedFileInjection[]> | undefined;
115
+ emitTauEvent(pi, "tau:file-injection.prepare", {
116
+ request,
117
+ accept(preparation) {
118
+ accepted ??= preparation;
119
+ },
120
+ });
121
+ return accepted ?? prepareFileInjectionWithHost(request, undefined);
122
+ }
123
+
124
+ type InjectionBase = Omit<FileInjectionDetails, "kind" | "status" | "error" | "readCache">;
125
+ type InjectionKind = FileInjectionDetails["kind"];
126
+
127
+ function injectionKind(mode: FileInjectionMode): InjectionKind {
128
+ if (mode === "outline") return "outline";
129
+ if (mode === "show") return "show";
130
+ return "full";
131
+ }
132
+
133
+ function buildInjectionBase(request: FileInjectionRequest, index: number, file: FileInjectionFile): InjectionBase {
134
+ const ranges = file.ranges === undefined ? undefined : normalizeLineRanges(file.ranges);
135
+ return {
136
+ v: 1,
137
+ rowId: `${request.batchId}:${index}`,
138
+ path: stripLeadingAt(file.path),
139
+ cwd: request.cwd,
140
+ source: request.source,
141
+ batchId: request.batchId,
142
+ ...(ranges === undefined ? {} : { ranges }),
143
+ ...(file.mode === "show" && file.name !== undefined ? { showName: file.name } : {}),
144
+ ...(file.mode === "show" ? { showView: file.view ?? "declaration" } : {}),
145
+ };
146
+ }
147
+
148
+ function failedPreparedInjection(base: InjectionBase, kind: InjectionKind, message: string): PreparedFileInjection {
149
+ return {
150
+ customType: FILE_INJECTION_TYPE,
151
+ content: `${base.path}\nInjection failed: ${message}`,
152
+ display: true,
153
+ details: {
154
+ ...base,
155
+ kind,
156
+ status: "failed",
157
+ error: message,
158
+ },
159
+ };
160
+ }
161
+
162
+ async function prepareFileInjectionWithHost(
163
+ request: FileInjectionRequest,
164
+ host: FileInjectionHost | undefined,
165
+ ): Promise<PreparedFileInjection[]> {
166
+ const prepared: PreparedFileInjection[] = [];
167
+ for (const [index, file] of request.files.entries()) {
168
+ request.signal?.throwIfAborted();
169
+ let base: InjectionBase = {
170
+ v: 1,
171
+ rowId: `${request.batchId}:${index}`,
172
+ path: stripLeadingAt(file.path),
173
+ cwd: request.cwd,
174
+ source: request.source,
175
+ batchId: request.batchId,
176
+ };
177
+ try {
178
+ base = buildInjectionBase(request, index, file);
179
+ prepared.push(await prepareOne(base, file, request.signal, host));
180
+ } catch (error) {
181
+ if (request.signal?.aborted) throw error;
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ prepared.push(failedPreparedInjection(base, injectionKind(file.mode), message));
184
+ }
185
+ }
186
+ return prepared;
187
+ }
188
+
189
+ async function prepareOne(
190
+ base: InjectionBase,
191
+ file: FileInjectionFile,
192
+ signal: AbortSignal | undefined,
193
+ host: FileInjectionHost | undefined,
194
+ ): Promise<PreparedFileInjection> {
195
+ if (file.mode === "show") {
196
+ if (base.ranges !== undefined) throw new Error("Line ranges are not supported for show injection");
197
+ const name = file.name?.trim();
198
+ if (!name) throw new Error("Show injection requires a declaration name");
199
+ return prepareShow(base, name, file.view ?? "declaration", signal);
200
+ }
201
+ if (base.ranges !== undefined) {
202
+ if (file.mode === "outline") throw new Error("Line ranges are supported only for file reads");
203
+ return prepareRangedRead(base, signal);
204
+ }
205
+ if (file.mode === "outline") return prepareOutline(base, signal, requireHost(host));
206
+ if (file.mode === "full") return prepareFull(base, signal);
207
+ const settings = requireHost(host).autoOutline();
208
+ if (!settings.enabled) return prepareFull(base, signal);
209
+ const engine = astEngineFor(base.cwd);
210
+ const absolutePath = resolveExplorePath(base.cwd, base.path);
211
+ if (engine.registry.adapterForPath(absolutePath) === undefined) return prepareFull(base, signal);
212
+ let lineCount: number;
213
+ try {
214
+ lineCount = (await engine.sourceForFile(absolutePath)).ir.lineCount;
215
+ } catch {
216
+ return prepareFull(base, signal);
217
+ }
218
+ if (!shouldOutlineFullRead(lineCount, settings.thresholdLines)) return prepareFull(base, signal);
219
+ return prepareLargeSourceOutline(base, lineCount, signal);
220
+ }
221
+
222
+ async function prepareShow(
223
+ base: InjectionBase,
224
+ name: string,
225
+ view: ShowView,
226
+ signal: AbortSignal | undefined,
227
+ ): Promise<PreparedFileInjection> {
228
+ const engine = astEngineFor(base.cwd);
229
+ const abort = signal ?? new AbortController().signal;
230
+ const batch = await showTargets(engine, [{ path: base.path, name }], view, undefined, abort);
231
+ const block = batch.blocks[0];
232
+ if (block === undefined) throw new Error(`No declaration matched ${name} @ ${base.path}`);
233
+ const rangeLabel =
234
+ block.startLine === block.endLine ? `L${block.startLine}` : `L${block.startLine}-${block.endLine}`;
235
+ const messageContent = [
236
+ base.path,
237
+ `${rangeLabel}: ${block.name}`,
238
+ ...block.warnings.map((warning) => `warning: ${warning}`),
239
+ block.text,
240
+ ].join("\n");
241
+ if (truncateBoundedHead(messageContent).truncated)
242
+ throw new Error("Show target exceeds the complete injection limit; request a narrower symbol");
243
+ return {
244
+ customType: FILE_INJECTION_TYPE,
245
+ content: messageContent,
246
+ display: true,
247
+ details: {
248
+ ...base,
249
+ kind: "show",
250
+ status: "injected",
251
+ showName: block.name,
252
+ showView: view,
253
+ ranges: [{ startLine: block.startLine, endLine: block.endLine }],
254
+ },
255
+ };
256
+ }
257
+
258
+ async function prepareRangedRead(base: InjectionBase, signal: AbortSignal | undefined): Promise<PreparedFileInjection> {
259
+ const ranges = base.ranges;
260
+ if (ranges === undefined) throw new Error("Ranged injection requires line ranges");
261
+ const pathKey = resolve(base.cwd, base.path);
262
+ const bytes = signal ? await readFile(pathKey, { signal }) : await readFile(pathKey);
263
+ const sourceText = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
264
+ const lines = sourceText.split("\n");
265
+ const selectedLines = ranges.flatMap((range) => {
266
+ if (range.startLine > lines.length) return [];
267
+ return lines.slice(range.startLine - 1, Math.min(range.endLine, lines.length));
268
+ });
269
+ if (selectedLines.length === 0) throw new Error("Line range starts beyond end of file");
270
+ const selectedText = selectedLines.join("\n");
271
+ const messageContent = `${base.path}\n${selectedText}`;
272
+ if (truncateBoundedHead(messageContent).truncated)
273
+ throw new Error("File range exceeds the complete injection limit; request a smaller range");
274
+ return {
275
+ customType: FILE_INJECTION_TYPE,
276
+ content: messageContent,
277
+ display: true,
278
+ details: { ...base, kind: "full", status: "injected" },
279
+ };
280
+ }
281
+
282
+ async function prepareFull(base: InjectionBase, signal: AbortSignal | undefined): Promise<PreparedFileInjection> {
283
+ const pathKey = resolve(base.cwd, base.path);
284
+ const bytes = signal ? await readFile(pathKey, { signal }) : await readFile(pathKey);
285
+ const content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
286
+ const messageContent = `${base.path}\n${content}`;
287
+ if (truncateBoundedHead(messageContent).truncated)
288
+ throw new Error("File exceeds the complete injection limit; outline or read a range instead");
289
+ const totalLines = content.split("\n").length;
290
+ return {
291
+ customType: FILE_INJECTION_TYPE,
292
+ content: messageContent,
293
+ display: true,
294
+ details: {
295
+ ...base,
296
+ kind: "full",
297
+ status: "injected",
298
+ readCache: createCompleteFileMeta({
299
+ pathKey,
300
+ presentation: "plain",
301
+ servedHash: createHash("sha256").update(content, "utf8").digest("hex"),
302
+ mode: "baseline",
303
+ sourceText: content,
304
+ returnedText: messageContent,
305
+ totalLines,
306
+ summary: `${totalLines} lines`,
307
+ }),
308
+ },
309
+ };
310
+ }
311
+
312
+ async function outlineFile(base: InjectionBase, signal: AbortSignal | undefined) {
313
+ const engine = astEngineFor(base.cwd);
314
+ const result = await outlinePath(engine, base.path, OUTLINE_OPTIONS, signal ?? new AbortController().signal);
315
+ return { engine, file: result.mode === "file" ? result.file : undefined };
316
+ }
317
+
318
+ async function prepareOutline(
319
+ base: InjectionBase,
320
+ signal: AbortSignal | undefined,
321
+ host: FileInjectionHost,
322
+ ): Promise<PreparedFileInjection> {
323
+ const { engine, file } = await outlineFile(base, signal);
324
+ if (!file) throw new Error("Outline injection requires a file path");
325
+ const body =
326
+ file.rows.length === 0 ? formatOutlineEmpty(OUTLINE_OPTIONS.names) : formatOutlineFile(file, engine.cwd, false);
327
+ const builder = new BoundedTextResultBuilder(host.temporaryOutput, "completeBlocks");
328
+ let content: string;
329
+ try {
330
+ await builder.appendBlock(file.path, formatPathForDisplay(file.path, engine.cwd), `${base.path}\n${body}`);
331
+ content = (await builder.finish()).content;
332
+ } catch (error) {
333
+ await builder.abort();
334
+ throw error;
335
+ }
336
+ return {
337
+ customType: FILE_INJECTION_TYPE,
338
+ content,
339
+ display: true,
340
+ details: { ...base, kind: "outline", status: "injected" },
341
+ };
342
+ }
343
+
344
+ async function prepareLargeSourceOutline(
345
+ base: InjectionBase,
346
+ lineCount: number,
347
+ signal: AbortSignal | undefined,
348
+ ): Promise<PreparedFileInjection> {
349
+ const { engine, file } = await outlineFile(base, signal);
350
+ if (!file) return prepareFull(base, signal);
351
+ const outlineText = formatLargeReadOutline(
352
+ file.rows.length === 0 ? "No declarations" : formatOutlineFile(file, engine.cwd, false),
353
+ );
354
+ const messageContent = `${base.path}\n${outlineText}`;
355
+ return {
356
+ customType: FILE_INJECTION_TYPE,
357
+ content: messageContent,
358
+ display: true,
359
+ details: {
360
+ ...base,
361
+ kind: "outline",
362
+ status: "injected",
363
+ readCache: createCompleteFileMeta({
364
+ pathKey: resolve(base.cwd, base.path),
365
+ presentation: "plain",
366
+ servedHash: createHash("sha256").update(outlineText, "utf8").digest("hex"),
367
+ mode: "baseline",
368
+ sourceText: outlineText,
369
+ returnedText: messageContent,
370
+ totalLines: lineCount,
371
+ summary: `outline (${lineCount} lines)`,
372
+ }),
373
+ },
374
+ };
375
+ }
376
+
377
+ function isInjectionKind(value: unknown): value is InjectionKind {
378
+ return value === "full" || value === "outline" || value === "show";
379
+ }
380
+
381
+ function isInjectionStatus(value: unknown): value is FileInjectionDetails["status"] {
382
+ return value === "injected" || value === "failed";
383
+ }
384
+
385
+ function isShowView(value: unknown): value is ShowView {
386
+ return (
387
+ value === "signature" ||
388
+ value === "signatureWithDocs" ||
389
+ value === "declaration" ||
390
+ value === "declarationWithImports"
391
+ );
392
+ }
393
+
394
+ function parseRequiredBase(details: Record<string, unknown>): InjectionBase | undefined {
395
+ if (
396
+ details.v !== 1 ||
397
+ typeof details.rowId !== "string" ||
398
+ typeof details.path !== "string" ||
399
+ typeof details.cwd !== "string" ||
400
+ typeof details.source !== "string" ||
401
+ typeof details.batchId !== "string"
402
+ ) {
403
+ return undefined;
404
+ }
405
+ const ranges = details.ranges === undefined ? undefined : parseRanges(details.ranges);
406
+ if (details.ranges !== undefined && ranges === undefined) return undefined;
407
+ return {
408
+ v: 1,
409
+ rowId: details.rowId,
410
+ path: details.path,
411
+ cwd: details.cwd,
412
+ source: details.source,
413
+ batchId: details.batchId,
414
+ ...(ranges === undefined ? {} : { ranges }),
415
+ };
416
+ }
417
+
418
+ function parseDetails(value: unknown): FileInjectionDetails | undefined {
419
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
420
+ const details = value as Record<string, unknown>;
421
+ const base = parseRequiredBase(details);
422
+ if (!base || !isInjectionKind(details.kind) || !isInjectionStatus(details.status)) return undefined;
423
+ if (details.kind === "show") {
424
+ if (typeof details.showName !== "string" || details.showName.length === 0) return undefined;
425
+ if (!isShowView(details.showView)) return undefined;
426
+ return {
427
+ ...base,
428
+ showName: details.showName,
429
+ showView: details.showView,
430
+ kind: "show",
431
+ status: details.status,
432
+ ...(typeof details.error === "string" ? { error: details.error } : {}),
433
+ };
434
+ }
435
+ return {
436
+ ...base,
437
+ kind: details.kind,
438
+ status: details.status,
439
+ ...(typeof details.error === "string" ? { error: details.error } : {}),
440
+ };
441
+ }
442
+
443
+ function normalizeLineRanges(ranges: ReadonlyArray<LineRange>): LineRange[] {
444
+ if (ranges.length === 0) throw new Error("At least one line range is required");
445
+ const sorted = ranges
446
+ .map((range) => {
447
+ if (
448
+ !Number.isSafeInteger(range.startLine) ||
449
+ !Number.isSafeInteger(range.endLine) ||
450
+ range.startLine < 1 ||
451
+ range.endLine < range.startLine
452
+ )
453
+ throw new Error("Line ranges must use positive, ordered line numbers");
454
+ return { startLine: range.startLine, endLine: range.endLine };
455
+ })
456
+ .sort((left, right) => left.startLine - right.startLine || left.endLine - right.endLine);
457
+ const normalized: LineRange[] = [];
458
+ for (const range of sorted) {
459
+ const previous = normalized[normalized.length - 1];
460
+ if (previous && range.startLine <= previous.endLine + 1) {
461
+ previous.endLine = Math.max(previous.endLine, range.endLine);
462
+ } else {
463
+ normalized.push(range);
464
+ }
465
+ }
466
+ return normalized;
467
+ }
468
+
469
+ function parseRanges(value: unknown): LineRange[] | undefined {
470
+ if (!Array.isArray(value)) return undefined;
471
+ const ranges: LineRange[] = [];
472
+ for (const item of value) {
473
+ if (!item || typeof item !== "object" || Array.isArray(item)) return undefined;
474
+ const range = item as Record<string, unknown>;
475
+ const startLine = range.startLine;
476
+ const endLine = range.endLine;
477
+ if (
478
+ typeof startLine !== "number" ||
479
+ typeof endLine !== "number" ||
480
+ !Number.isSafeInteger(startLine) ||
481
+ !Number.isSafeInteger(endLine) ||
482
+ startLine < 1 ||
483
+ endLine < startLine
484
+ )
485
+ return undefined;
486
+ ranges.push({ startLine, endLine });
487
+ }
488
+ return ranges;
489
+ }
490
+
491
+ class FileInjectionComponent {
492
+ private readonly rowState: ToolRowStateStore;
493
+ private readonly details: FileInjectionDetails;
494
+ private readonly content: string;
495
+ private readonly expanded: boolean;
496
+ private readonly theme: Theme;
497
+
498
+ constructor(
499
+ rowState: ToolRowStateStore,
500
+ details: FileInjectionDetails,
501
+ content: string,
502
+ expanded: boolean,
503
+ theme: Theme,
504
+ ) {
505
+ this.rowState = rowState;
506
+ this.details = details;
507
+ this.content = content;
508
+ this.expanded = expanded;
509
+ this.theme = theme;
510
+ this.rowState.watch(details.rowId, () => this.invalidate());
511
+ }
512
+
513
+ render(width: number): string[] {
514
+ const state =
515
+ this.details.status === "failed"
516
+ ? "error"
517
+ : this.rowState.get(this.details.rowId) === "pruned"
518
+ ? "warning"
519
+ : "complete";
520
+ const marker = new Marker({
521
+ theme: this.theme,
522
+ state,
523
+ label: this.details.kind === "outline" ? "outline" : this.details.kind === "show" ? "show" : "read",
524
+ parts:
525
+ this.details.kind === "show" && this.details.showName
526
+ ? [`${this.details.showName} · ${this.details.path}`]
527
+ : [this.details.path],
528
+ }).render(width);
529
+ if (!this.expanded || this.details.status === "failed") {
530
+ if (this.details.status === "failed") return marker;
531
+ const hint = this.theme.fg("muted", `(${keyText("app.tools.expand")} to expand)`);
532
+ const last = marker.length - 1;
533
+ return marker.map((line, index) => (index === last ? truncateToWidth(`${line} ${hint}`, width, "…") : line));
534
+ }
535
+
536
+ const bounded =
537
+ this.content.length > FILE_RENDER_CHARACTERS
538
+ ? `${this.content.slice(0, FILE_RENDER_CHARACTERS)}\n…`
539
+ : this.content;
540
+ const content = wrapTextWithAnsi(this.theme.fg("dim", bounded), Math.max(1, width - 1))
541
+ .slice(0, FILE_RENDER_LINES)
542
+ .map((line) => truncateToWidth(` ${line}`, width, "…"));
543
+ return [...marker, ...content];
544
+ }
545
+
546
+ invalidate(): void {}
547
+ }
@@ -12,7 +12,7 @@ const REQUEST_TIMEOUT_MS = 60_000;
12
12
  const MAX_ATTEMPTS = 3;
13
13
  const RETRY_BASE_DELAY_MS = 500;
14
14
 
15
- export interface CodexAuth {
15
+ interface CodexAuth {
16
16
  token: string;
17
17
  accountId: string;
18
18
  }
@@ -38,7 +38,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
38
38
  return typeof value === "object" && value !== null && !Array.isArray(value);
39
39
  }
40
40
 
41
- export function resolveCodexAuth(token: string): CodexAuth {
41
+ function resolveCodexAuth(token: string): CodexAuth {
42
42
  const invalidCredential = () =>
43
43
  new Error("The OpenAI Codex credential does not contain a usable ChatGPT account ID. Run /login again.");
44
44
  const segments = token.split(".");
@@ -204,6 +204,51 @@ async function delay(milliseconds: number, signal?: AbortSignal): Promise<void>
204
204
  });
205
205
  }
206
206
 
207
+ async function postXaiImage(
208
+ route: string,
209
+ body: Record<string, unknown>,
210
+ token: string,
211
+ signal?: AbortSignal,
212
+ ): Promise<HttpResponse> {
213
+ const requestSignal = signal
214
+ ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
215
+ : AbortSignal.timeout(REQUEST_TIMEOUT_MS);
216
+ return (await fetch(`${XAI_API_BASE_URL}/images/${route}`, {
217
+ method: "POST",
218
+ headers: {
219
+ Accept: "application/json",
220
+ Authorization: `Bearer ${token}`,
221
+ "Content-Type": "application/json",
222
+ },
223
+ body: JSON.stringify(body),
224
+ signal: requestSignal,
225
+ })) as HttpResponse;
226
+ }
227
+
228
+ async function decodeOkXaiImage(response: HttpResponse): Promise<GeneratedImage> {
229
+ let value: unknown;
230
+ try {
231
+ value = await response.json();
232
+ } catch {
233
+ throw new Error("xAI returned a non-JSON image response");
234
+ }
235
+ return decodeImageResponse(value, "xAI", false);
236
+ }
237
+
238
+ async function throwOrRetryXaiFailure(
239
+ operation: "generation" | "edit",
240
+ response: HttpResponse,
241
+ token: string,
242
+ attempt: number,
243
+ signal?: AbortSignal,
244
+ ): Promise<void> {
245
+ const message = serverErrorMessage(await boundedError(response), [token]);
246
+ if (!retryable(response.status) || attempt === MAX_ATTEMPTS) {
247
+ throw new Error(`xAI image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`);
248
+ }
249
+ await delay(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), signal);
250
+ }
251
+
207
252
  async function requestXaiImage(
208
253
  operation: "generation" | "edit",
209
254
  body: Record<string, unknown>,
@@ -212,43 +257,17 @@ async function requestXaiImage(
212
257
  ): Promise<GeneratedImage> {
213
258
  const route = operation === "generation" ? "generations" : "edits";
214
259
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
215
- const requestSignal = signal
216
- ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
217
- : AbortSignal.timeout(REQUEST_TIMEOUT_MS);
218
260
  let response: HttpResponse;
219
261
  try {
220
- response = (await fetch(`${XAI_API_BASE_URL}/images/${route}`, {
221
- method: "POST",
222
- headers: {
223
- Accept: "application/json",
224
- Authorization: `Bearer ${token}`,
225
- "Content-Type": "application/json",
226
- },
227
- body: JSON.stringify(body),
228
- signal: requestSignal,
229
- })) as HttpResponse;
262
+ response = await postXaiImage(route, body, token, signal);
230
263
  } catch (error) {
231
264
  if (signal?.aborted) throw signal.reason;
232
265
  if (attempt === MAX_ATTEMPTS) throw error;
233
266
  await delay(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), signal);
234
267
  continue;
235
268
  }
236
- if (response.ok) {
237
- let value: unknown;
238
- try {
239
- value = await response.json();
240
- } catch {
241
- throw new Error("xAI returned a non-JSON image response");
242
- }
243
- return decodeImageResponse(value, "xAI", false);
244
- }
245
- const message = serverErrorMessage(await boundedError(response), [token]);
246
- if (!retryable(response.status) || attempt === MAX_ATTEMPTS) {
247
- throw new Error(
248
- `xAI image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`,
249
- );
250
- }
251
- await delay(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), signal);
269
+ if (response.ok) return decodeOkXaiImage(response);
270
+ await throwOrRetryXaiFailure(operation, response, token, attempt, signal);
252
271
  }
253
272
  throw new Error(`xAI image ${operation} failed`);
254
273
  }