@shanepadgett/tau-agent 0.1.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 (138) hide show
  1. package/README.md +30 -0
  2. package/docs/extending-tau-agent.md +119 -0
  3. package/docs/subagents.md +57 -0
  4. package/docs/tui.md +107 -0
  5. package/extensions/appshot/README.md +11 -0
  6. package/extensions/appshot/capture.swift +191 -0
  7. package/extensions/appshot/index.ts +245 -0
  8. package/extensions/appshot/native-helper.ts +102 -0
  9. package/extensions/attention/README.md +23 -0
  10. package/extensions/attention/index.ts +71 -0
  11. package/extensions/auto-name/README.md +26 -0
  12. package/extensions/auto-name/index.ts +163 -0
  13. package/extensions/branch/README.md +22 -0
  14. package/extensions/branch/index.ts +145 -0
  15. package/extensions/branch/panel.ts +148 -0
  16. package/extensions/clear-screen/README.md +29 -0
  17. package/extensions/clear-screen/index.ts +75 -0
  18. package/extensions/commit/README.md +15 -0
  19. package/extensions/commit/commit-plan.ts +328 -0
  20. package/extensions/commit/git-change-set.ts +268 -0
  21. package/extensions/commit/index.ts +138 -0
  22. package/extensions/commit/review-ui.ts +745 -0
  23. package/extensions/explore/README.md +7 -0
  24. package/extensions/explore/autoread.ts +140 -0
  25. package/extensions/explore/find.ts +147 -0
  26. package/extensions/explore/grep.ts +749 -0
  27. package/extensions/explore/index.ts +17 -0
  28. package/extensions/explore/limits.ts +22 -0
  29. package/extensions/explore/ls.ts +113 -0
  30. package/extensions/explore/path-display.ts +41 -0
  31. package/extensions/explore/path-tree.ts +150 -0
  32. package/extensions/explore/read.ts +160 -0
  33. package/extensions/explore/result.ts +29 -0
  34. package/extensions/explore/traverse.ts +226 -0
  35. package/extensions/footer/README.md +23 -0
  36. package/extensions/footer/index.ts +534 -0
  37. package/extensions/footer/settings.ts +13 -0
  38. package/extensions/ideas/README.md +10 -0
  39. package/extensions/ideas/browser.ts +66 -0
  40. package/extensions/ideas/index.ts +31 -0
  41. package/extensions/ideas/store.ts +32 -0
  42. package/extensions/image-gen/README.md +11 -0
  43. package/extensions/image-gen/client.ts +186 -0
  44. package/extensions/image-gen/index.ts +152 -0
  45. package/extensions/manage-sessions/README.md +12 -0
  46. package/extensions/manage-sessions/index.ts +58 -0
  47. package/extensions/manage-sessions/manager-ui.ts +323 -0
  48. package/extensions/manage-sessions/sessions.ts +126 -0
  49. package/extensions/patch/README.md +48 -0
  50. package/extensions/patch/executor.ts +355 -0
  51. package/extensions/patch/index.ts +170 -0
  52. package/extensions/patch/matcher.ts +236 -0
  53. package/extensions/patch/parser.ts +347 -0
  54. package/extensions/patch/render.ts +247 -0
  55. package/extensions/patch/summary.ts +35 -0
  56. package/extensions/publish/README.md +11 -0
  57. package/extensions/publish/index.ts +225 -0
  58. package/extensions/qna/README.md +45 -0
  59. package/extensions/qna/additional-context-body.ts +51 -0
  60. package/extensions/qna/body-render.ts +6 -0
  61. package/extensions/qna/choice-question-body.ts +322 -0
  62. package/extensions/qna/index.ts +245 -0
  63. package/extensions/qna/inline-editor-row.ts +56 -0
  64. package/extensions/qna/input-question-body.ts +89 -0
  65. package/extensions/qna/model.ts +317 -0
  66. package/extensions/qna/panel.ts +240 -0
  67. package/extensions/qna/ui.ts +16 -0
  68. package/extensions/reference/README.md +47 -0
  69. package/extensions/reference/index.ts +76 -0
  70. package/extensions/reference/panel.ts +874 -0
  71. package/extensions/reference/settings.ts +29 -0
  72. package/extensions/run-summary/README.md +5 -0
  73. package/extensions/run-summary/index.ts +98 -0
  74. package/extensions/silent-command-runner/README.md +25 -0
  75. package/extensions/silent-command-runner/index.ts +468 -0
  76. package/extensions/silent-command-runner/settings.ts +65 -0
  77. package/extensions/soul/README.md +19 -0
  78. package/extensions/soul/index.ts +20 -0
  79. package/extensions/soul/prompt.ts +254 -0
  80. package/extensions/soul/settings.ts +13 -0
  81. package/extensions/stash/README.md +48 -0
  82. package/extensions/stash/browser.ts +51 -0
  83. package/extensions/stash/index.ts +55 -0
  84. package/extensions/stash/store.ts +36 -0
  85. package/extensions/subagent/README.md +34 -0
  86. package/extensions/subagent/agents/scout.md +85 -0
  87. package/extensions/subagent/agents/web-research.md +95 -0
  88. package/extensions/subagent/agents.ts +196 -0
  89. package/extensions/subagent/index.ts +214 -0
  90. package/extensions/subagent/render.ts +89 -0
  91. package/extensions/subagent/run.ts +325 -0
  92. package/extensions/tau/README.md +17 -0
  93. package/extensions/tau/index.ts +122 -0
  94. package/extensions/tau-help/README.md +3 -0
  95. package/extensions/tau-help/help.md +125 -0
  96. package/extensions/tau-help/index.ts +68 -0
  97. package/extensions/turn-budget/README.md +14 -0
  98. package/extensions/turn-budget/index.ts +150 -0
  99. package/extensions/turn-budget/settings.ts +35 -0
  100. package/extensions/web/README.md +13 -0
  101. package/extensions/web/codesearch.ts +79 -0
  102. package/extensions/web/exa.ts +101 -0
  103. package/extensions/web/html.ts +67 -0
  104. package/extensions/web/index.ts +13 -0
  105. package/extensions/web/limits.ts +11 -0
  106. package/extensions/web/tool-output.ts +54 -0
  107. package/extensions/web/webfetch.ts +177 -0
  108. package/extensions/web/websearch.ts +100 -0
  109. package/package.json +57 -0
  110. package/prompts/.gitkeep +1 -0
  111. package/prompts/cavemanify.md +25 -0
  112. package/prompts/implement.md +25 -0
  113. package/prompts/interview.md +48 -0
  114. package/prompts/plan-feature.md +30 -0
  115. package/prompts/plan-implementation.md +50 -0
  116. package/schemas/tau.schema.json +205 -0
  117. package/shared/agent-blocked.ts +12 -0
  118. package/shared/description.ts +28 -0
  119. package/shared/events.ts +135 -0
  120. package/shared/git.ts +41 -0
  121. package/shared/injected-context.ts +69 -0
  122. package/shared/jsonl-store.ts +108 -0
  123. package/shared/model-fallback/index.ts +278 -0
  124. package/shared/model-fallback/settings.ts +18 -0
  125. package/shared/model-fallback/types.ts +8 -0
  126. package/shared/ranges.ts +21 -0
  127. package/shared/settings/define.ts +15 -0
  128. package/shared/settings/files.ts +20 -0
  129. package/shared/settings/json.ts +41 -0
  130. package/shared/settings/load.ts +59 -0
  131. package/shared/settings/merge.ts +21 -0
  132. package/shared/settings/paths.ts +41 -0
  133. package/shared/settings/schema.ts +25 -0
  134. package/shared/settings/specs.ts +23 -0
  135. package/shared/text.ts +11 -0
  136. package/shared/tool-row-state.ts +43 -0
  137. package/skills/writing-preferences/SKILL.md +136 -0
  138. package/themes/.gitkeep +1 -0
@@ -0,0 +1,355 @@
1
+ import { access, mkdir, readFile, rmdir, stat, unlink, writeFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
4
+ import { applyChunksWithRanges, countLogicalLines, UpdateChunkApplyError } from "./matcher.ts";
5
+ import { type PatchFailure, type PatchOperation, parsePatch } from "./parser.ts";
6
+
7
+ export interface ApplyPatchChange {
8
+ sectionIndex: number;
9
+ kind: PatchOperation["type"];
10
+ path: string;
11
+ move?: { from: string; to: string };
12
+ linesAdded: number;
13
+ linesRemoved: number;
14
+ snapshotRanges?: Array<{ startLine: number; endLine: number }>;
15
+ }
16
+
17
+ export interface ApplyPatchSummary {
18
+ status: "completed" | "partial" | "failed";
19
+ changes: ApplyPatchChange[];
20
+ failures: PatchFailure[];
21
+ totalSections: number;
22
+ }
23
+
24
+ export interface ApplyPatchStats {
25
+ added: string[];
26
+ replaced: string[];
27
+ updated: string[];
28
+ deleted: string[];
29
+ moved: Array<{ from: string; to: string }>;
30
+ linesAdded: number;
31
+ linesRemoved: number;
32
+ completedOperations: number;
33
+ }
34
+
35
+ export function deriveStats(summary: ApplyPatchSummary): ApplyPatchStats {
36
+ const added: string[] = [];
37
+ const replaced: string[] = [];
38
+ const updated: string[] = [];
39
+ const deleted: string[] = [];
40
+ const moved: Array<{ from: string; to: string }> = [];
41
+ let linesAdded = 0;
42
+ let linesRemoved = 0;
43
+ for (const c of summary.changes) {
44
+ if (c.kind === "add") added.push(c.path);
45
+ else if (c.kind === "replace") replaced.push(c.path);
46
+ else if (c.kind === "delete") deleted.push(c.path);
47
+ else {
48
+ updated.push(c.move?.to ?? c.path);
49
+ if (c.move) moved.push(c.move);
50
+ }
51
+ linesAdded += c.linesAdded;
52
+ linesRemoved += c.linesRemoved;
53
+ }
54
+ return {
55
+ added,
56
+ replaced,
57
+ updated,
58
+ deleted,
59
+ moved,
60
+ linesAdded,
61
+ linesRemoved,
62
+ completedOperations: summary.changes.length,
63
+ };
64
+ }
65
+
66
+ interface StagedMutation {
67
+ change: ApplyPatchChange;
68
+ commit(): Promise<void>;
69
+ }
70
+
71
+ // lean: resolve(cwd,path) without realpath collapse; OK for typical working trees without symlinked dirs;
72
+ // upgrade to realpath ancestor walk when symlink-based dedup collisions appear
73
+ function resolvePath(cwd: string, rawPath: string): string {
74
+ const input = rawPath.trim();
75
+ if (!input) throw new Error("Path must not be empty.");
76
+ return isAbsolute(input) ? resolve(input) : resolve(cwd, input);
77
+ }
78
+
79
+ function throwIfAborted(signal: AbortSignal | undefined): void {
80
+ if (signal?.aborted) throw new Error("Operation aborted");
81
+ }
82
+
83
+ async function pathExists(path: string): Promise<boolean> {
84
+ try {
85
+ await access(path);
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ async function readUtf8(path: string, displayPath: string): Promise<string> {
93
+ try {
94
+ return await readFile(path, "utf8");
95
+ } catch (error) {
96
+ const message = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
97
+ throw new Error(`Could not read file: ${displayPath}. ${message}.`);
98
+ }
99
+ }
100
+
101
+ async function assertExistingFile(path: string, displayPath: string): Promise<void> {
102
+ if (!(await pathExists(path))) throw new Error(`Path does not exist: ${displayPath}`);
103
+ const info = await stat(path);
104
+ if (!info.isFile()) throw new Error(`Expected a file: ${displayPath}`);
105
+ }
106
+
107
+ async function removeEmptyParentDirs(cwd: string, startDir: string): Promise<void> {
108
+ let current = resolve(startDir);
109
+ const root = resolve(cwd);
110
+ while (current !== root) {
111
+ const rel = relative(root, current);
112
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return;
113
+ try {
114
+ await rmdir(current);
115
+ } catch {
116
+ return;
117
+ }
118
+ current = dirname(current);
119
+ }
120
+ }
121
+
122
+ async function stageWholeFile(
123
+ cwd: string,
124
+ op: Extract<PatchOperation, { type: "add" | "replace" }>,
125
+ ): Promise<StagedMutation> {
126
+ const target = resolvePath(cwd, op.path);
127
+ let linesRemoved = 0;
128
+ if (await pathExists(target)) {
129
+ linesRemoved = countLogicalLines(await readUtf8(target, op.path));
130
+ }
131
+ const lineCount = countLogicalLines(op.content);
132
+ return {
133
+ change: {
134
+ sectionIndex: op.sectionIndex,
135
+ kind: op.type,
136
+ path: op.path,
137
+ linesAdded: op.linesAdded,
138
+ linesRemoved,
139
+ snapshotRanges: lineCount > 0 ? [{ startLine: 1, endLine: Math.min(lineCount, 120) }] : undefined,
140
+ },
141
+ async commit() {
142
+ await mkdir(dirname(target), { recursive: true });
143
+ await writeFile(target, op.content, "utf8");
144
+ },
145
+ };
146
+ }
147
+
148
+ async function stageDelete(cwd: string, op: Extract<PatchOperation, { type: "delete" }>): Promise<StagedMutation> {
149
+ const target = resolvePath(cwd, op.path);
150
+ await assertExistingFile(target, op.path);
151
+ const current = await readUtf8(target, op.path);
152
+ return {
153
+ change: {
154
+ sectionIndex: op.sectionIndex,
155
+ kind: "delete",
156
+ path: op.path,
157
+ linesAdded: 0,
158
+ linesRemoved: countLogicalLines(current),
159
+ },
160
+ async commit() {
161
+ await unlink(target);
162
+ await removeEmptyParentDirs(cwd, dirname(target));
163
+ },
164
+ };
165
+ }
166
+
167
+ async function stageUpdate(cwd: string, op: Extract<PatchOperation, { type: "update" }>): Promise<StagedMutation> {
168
+ const source = resolvePath(cwd, op.path);
169
+ await assertExistingFile(source, op.path);
170
+ const current = await readUtf8(source, op.path);
171
+ const result = op.chunks.length > 0 ? applyChunksWithRanges(current, op.chunks) : undefined;
172
+ const next = result?.content ?? current;
173
+ const moveTarget = op.movePath ? resolvePath(cwd, op.movePath) : undefined;
174
+
175
+ if (!moveTarget || moveTarget === source) {
176
+ return {
177
+ change: {
178
+ sectionIndex: op.sectionIndex,
179
+ kind: "update",
180
+ path: op.path,
181
+ linesAdded: op.linesAdded,
182
+ linesRemoved: op.linesRemoved,
183
+ snapshotRanges: result?.snapshotRanges,
184
+ },
185
+ async commit() {
186
+ await writeFile(source, next, "utf8");
187
+ },
188
+ };
189
+ }
190
+
191
+ const movePath = op.movePath;
192
+ if (!movePath) throw new Error("Move target path is missing.");
193
+ if (await pathExists(moveTarget)) {
194
+ throw new Error(
195
+ `move target already exists: ${movePath}. Move targets must be unused to avoid overwriting existing files.`,
196
+ );
197
+ }
198
+ return {
199
+ change: {
200
+ sectionIndex: op.sectionIndex,
201
+ kind: "update",
202
+ path: movePath,
203
+ move: { from: op.path, to: movePath },
204
+ linesAdded: op.linesAdded,
205
+ linesRemoved: op.linesRemoved,
206
+ snapshotRanges: result?.snapshotRanges,
207
+ },
208
+ async commit() {
209
+ await mkdir(dirname(moveTarget), { recursive: true });
210
+ await writeFile(moveTarget, next, "utf8");
211
+ await unlink(source);
212
+ await removeEmptyParentDirs(cwd, dirname(source));
213
+ },
214
+ };
215
+ }
216
+
217
+ function collectDupPathFailures(cwd: string, operations: PatchOperation[]): PatchFailure[] {
218
+ const pathSections = new Map<string, number[]>();
219
+ const displayPaths = new Map<string, string>();
220
+
221
+ for (const op of operations) {
222
+ const paths = [resolvePath(cwd, op.path)];
223
+ if (op.type === "update" && op.movePath) paths.push(resolvePath(cwd, op.movePath));
224
+
225
+ for (const p of paths) {
226
+ const existing = pathSections.get(p) ?? [];
227
+ existing.push(op.sectionIndex);
228
+ pathSections.set(p, existing);
229
+ if (!displayPaths.has(p)) displayPaths.set(p, op.path);
230
+ }
231
+ }
232
+
233
+ const failures: PatchFailure[] = [];
234
+ for (const [canonical, sections] of pathSections) {
235
+ if (sections.length <= 1) continue;
236
+ const display = displayPaths.get(canonical) ?? canonical;
237
+ for (const sectionIndex of sections) {
238
+ failures.push({
239
+ phase: "apply",
240
+ sectionIndex,
241
+ path: display,
242
+ message: `conflicting operations for path: ${display} (sections ${sections.join(", ")}). Use Add File or Replace File for whole-file writes, or split dependent changes into separate patches.`,
243
+ });
244
+ }
245
+ }
246
+ return failures;
247
+ }
248
+
249
+ function withMutationQueuePaths<T>(paths: string[], fn: () => Promise<T>): Promise<T> {
250
+ const unique = Array.from(new Set(paths)).sort();
251
+ let current = fn;
252
+ for (const path of unique.reverse()) {
253
+ const next = current;
254
+ current = () => withFileMutationQueue(path, next);
255
+ }
256
+ return current();
257
+ }
258
+
259
+ export async function applyPatch(
260
+ cwd: string,
261
+ input: string,
262
+ signal?: AbortSignal,
263
+ onProgress?: (summary: ApplyPatchSummary) => void | Promise<void>,
264
+ ): Promise<ApplyPatchSummary> {
265
+ throwIfAborted(signal);
266
+ const { operations, parseFailures, totalSections } = parsePatch(input);
267
+
268
+ if (operations.length === 0) {
269
+ const summary: ApplyPatchSummary = {
270
+ status: "failed",
271
+ changes: [],
272
+ failures: parseFailures,
273
+ totalSections: totalSections || 1,
274
+ };
275
+ await onProgress?.(summary);
276
+ return summary;
277
+ }
278
+
279
+ const dupFailures = collectDupPathFailures(cwd, operations);
280
+ const initialFailures = [...parseFailures, ...dupFailures];
281
+ const blockedSections = new Set(initialFailures.map((failure) => failure.sectionIndex));
282
+ const runnableOperations = operations.filter((op) => !blockedSections.has(op.sectionIndex));
283
+
284
+ const queuePaths = runnableOperations.flatMap((op) => {
285
+ const paths = [resolvePath(cwd, op.path)];
286
+ if (op.type === "update" && op.movePath) paths.push(resolvePath(cwd, op.movePath));
287
+ return paths;
288
+ });
289
+
290
+ return withMutationQueuePaths(queuePaths, async () => {
291
+ const staged: StagedMutation[] = [];
292
+ const failures: PatchFailure[] = [...initialFailures];
293
+
294
+ for (const op of runnableOperations) {
295
+ try {
296
+ if (op.type === "add" || op.type === "replace") staged.push(await stageWholeFile(cwd, op));
297
+ else if (op.type === "delete") staged.push(await stageDelete(cwd, op));
298
+ else staged.push(await stageUpdate(cwd, op));
299
+ } catch (error) {
300
+ if (error instanceof Error && error.message === "Operation aborted") throw error;
301
+ const failure: PatchFailure = {
302
+ phase: "apply",
303
+ sectionIndex: op.sectionIndex,
304
+ path: op.path,
305
+ kind: op.type,
306
+ message: error instanceof Error ? error.message : String(error),
307
+ };
308
+ if (error instanceof UpdateChunkApplyError) {
309
+ failure.chunkIndex = error.chunkIndex;
310
+ failure.totalChunks = error.totalChunks;
311
+ failure.contextHint = error.contextHint;
312
+ }
313
+ failures.push(failure);
314
+ }
315
+ }
316
+
317
+ if (staged.length === 0) {
318
+ const summary: ApplyPatchSummary = { status: "failed", changes: [], failures, totalSections };
319
+ await onProgress?.(summary);
320
+ return summary;
321
+ }
322
+
323
+ throwIfAborted(signal);
324
+
325
+ const changes: ApplyPatchChange[] = [];
326
+ for (const mutation of staged) {
327
+ try {
328
+ await mutation.commit();
329
+ changes.push(mutation.change);
330
+ await onProgress?.({ status: "partial", changes: [...changes], failures: [...failures], totalSections });
331
+ } catch (error) {
332
+ const message = error instanceof Error ? error.message : String(error);
333
+ failures.push({
334
+ phase: "apply",
335
+ sectionIndex: mutation.change.sectionIndex,
336
+ path: mutation.change.path,
337
+ kind: mutation.change.kind,
338
+ message,
339
+ });
340
+ const status = changes.length === 0 ? "failed" : "partial";
341
+ const summary: ApplyPatchSummary = {
342
+ status,
343
+ changes: [...changes],
344
+ failures: [...failures],
345
+ totalSections,
346
+ };
347
+ await onProgress?.(summary);
348
+ return summary;
349
+ }
350
+ }
351
+
352
+ const status: ApplyPatchSummary["status"] = failures.length > 0 ? "partial" : "completed";
353
+ return { status, changes, failures, totalSections };
354
+ });
355
+ }
@@ -0,0 +1,170 @@
1
+ import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { emitTauEvent } from "../../shared/events.js";
4
+ import { createToolRowStateStore } from "../../shared/tool-row-state.js";
5
+ import { type ApplyPatchSummary, applyPatch } from "./executor.ts";
6
+ import { renderPatchCall, renderPatchResult } from "./render.ts";
7
+ import { formatPatchSummary } from "./summary.ts";
8
+
9
+ const SUPPRESSED_TOOLS = new Set(["edit", "write"]);
10
+
11
+ const patchParams = Type.Object({
12
+ input: Type.String({
13
+ description: [
14
+ 'Pass the full patch as this input string. Tool call arguments must be exactly { "input": "*** Begin Patch\\n...\\n*** End Patch" }; never put the patch text in a JSON object key.',
15
+ "",
16
+ "Apply one structured multi-file patch. Put the full patch text in this field.",
17
+ "",
18
+ "ENVELOPE",
19
+ "*** Begin Patch",
20
+ "[one or more file sections]",
21
+ "*** End Patch",
22
+ "",
23
+ "Patch marker lines may have leading/trailing whitespace. File content starts after the patch prefix and keeps the remaining text exactly.",
24
+ "Malformed envelopes fail the whole patch. Section parse, match, duplicate-path, missing-file, and move-target errors fail those sections while independent valid sections still apply.",
25
+ "",
26
+ "SECTIONS",
27
+ "*** Add File: <path> whole-file write; only + lines become content; may overwrite",
28
+ "*** Replace File: <path> whole-file write reported as replacement; only + lines become content",
29
+ "*** Update File: <path> contextual edit of an existing file",
30
+ "*** Move to: <new-path> optional, first line after Update File header, before any hunks; target must not exist",
31
+ "*** Delete File: <path> removes an existing file; must be the only line in its section",
32
+ "",
33
+ "UPDATE HUNKS",
34
+ "@@ or @@ <context line> optional chunk marker; context marker moves the search cursor after that existing line",
35
+ " context line space prefix; old and new text",
36
+ "-removed line removed old text",
37
+ "+added line added new text",
38
+ "*** End of File optional EOF-sensitive match / append marker",
39
+ "",
40
+ "RULES",
41
+ "- One section per touched path per patch.",
42
+ "- Read files before writing Update File patches so context and removed lines match current content.",
43
+ "- Use context body lines for positional inserts. A pure + update chunk appends to EOF.",
44
+ "- Unanchored repeated matches use the first forward match.",
45
+ "- The @@ context marker is not changed by the hunk. To change that line, include it as a -/+ body line.",
46
+ "- Fuzzy matching tolerates minor trailing whitespace, smart quote/dash, and Unicode-space differences; context body lines are kept from the original file.",
47
+ "- If a match fails, read the current file content and retry with corrected lines.",
48
+ "",
49
+ "EXAMPLE",
50
+ "*** Begin Patch",
51
+ "*** Add File: src/new-module.ts",
52
+ '+export const version = "1.0.0";',
53
+ "*** Replace File: src/generated.ts",
54
+ "+export const generated = true;",
55
+ "*** Update File: src/config.ts",
56
+ " export const PORT = 3000;",
57
+ "-export const DEBUG = false;",
58
+ "+export const DEBUG = true;",
59
+ "*** Update File: src/old-name.ts",
60
+ "*** Move to: src/new-name.ts",
61
+ " const handler = () => {}",
62
+ "-module.exports = handler;",
63
+ "+export default handler;",
64
+ "*** Delete File: src/deprecated.ts",
65
+ "*** End Patch",
66
+ ].join("\n"),
67
+ }),
68
+ });
69
+
70
+ function createPatchTool(rowState: ReturnType<typeof createToolRowStateStore>) {
71
+ return defineTool<typeof patchParams, ApplyPatchSummary>({
72
+ name: "patch",
73
+ label: "Patch",
74
+ description:
75
+ "Apply a multi-file patch to create, edit, move, and delete files. Use it as the default for hand-authored file mutations across one or many files, including large writes; use shell commands or scripts when they express mechanical bulk changes with fewer tokens and less risk. Invalid sections fail independently when possible.",
76
+ promptSnippet: "Apply multi-file patches to create, edit, move, and delete files",
77
+ promptGuidelines: [
78
+ 'Call patch with arguments shaped exactly as { "input": "*** Begin Patch\\n...\\n*** End Patch" }. Do not put the patch body in a JSON key.',
79
+ "Use patch as the default for hand-authored file mutations: creates, replacements, updates, moves, and deletes across one or many files, including large writes.",
80
+ "Use shell commands or scripts when they are the lower-token, clearer, safer representation of mechanical bulk work: copying template trees, moving many files, running generators, or applying repeated transforms. Inspect generated, copied, or scripted results before hand-editing them.",
81
+ "Use one envelope: *** Begin Patch, one or more sections, then *** End Patch.",
82
+ "Use one section per touched path. Include adds, replaces, updates, deletes, and moves together when possible; retry failed sections separately when needed.",
83
+ "Before writing Update File patches, read every file you need to touch so context and removed lines match current content.",
84
+ "In Update File hunks, one leading prefix is syntax: space for context, - for removed text, + for added text. The rest of the line is file text.",
85
+ "Use context body lines for positional inserts. A pure + update chunk appends to EOF.",
86
+ "Unanchored repeated matches use the first forward match. Anchor hunks on distinctive nearby lines when matching could be ambiguous; avoid lone braces or generic repeated lines.",
87
+ "The @@ context marker moves the search cursor after that existing line; the marker line itself is not changed by the hunk.",
88
+ "Move targets must not already exist.",
89
+ "If a match fails, read the current file content and retry with corrected lines.",
90
+ ],
91
+ parameters: patchParams,
92
+ executionMode: "sequential",
93
+
94
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
95
+ const input = params.input.replace(/\r\n/g, "\n").trim();
96
+ const summary = await applyPatch(ctx.cwd, input, signal, async (progress) => {
97
+ await onUpdate?.({
98
+ content: [{ type: "text", text: formatPatchSummary(progress) }],
99
+ details: progress,
100
+ });
101
+ });
102
+
103
+ return {
104
+ content: [{ type: "text", text: formatPatchSummary(summary) }],
105
+ details: summary,
106
+ };
107
+ },
108
+
109
+ renderCall(args, theme, context) {
110
+ return renderPatchCall(args as { input?: string }, theme, {
111
+ expanded: context.expanded,
112
+ executionStarted: context.executionStarted,
113
+ isPartial: context.isPartial,
114
+ lastComponent: context.lastComponent,
115
+ rowState,
116
+ rowId: context.toolCallId,
117
+ invalidate: context.invalidate,
118
+ });
119
+ },
120
+
121
+ renderResult(result, options, theme, context) {
122
+ return renderPatchResult(result, { expanded: options.expanded }, theme, {
123
+ expanded: options.expanded,
124
+ args: context.args as { input?: string },
125
+ lastComponent: context.lastComponent,
126
+ rowState,
127
+ rowId: context.toolCallId,
128
+ invalidate: context.invalidate,
129
+ });
130
+ },
131
+ });
132
+ }
133
+
134
+ export default function patchExtension(pi: ExtensionAPI): void {
135
+ const rowState = createToolRowStateStore(pi, "patch.tool-row-state");
136
+ pi.registerTool(createPatchTool(rowState));
137
+
138
+ // AgentToolResult has no isError field, so execute returns are always treated as success.
139
+ // Override via tool_result to flag partial/failed patches as errors for the model and UI.
140
+ pi.on("tool_result", async (event, ctx) => {
141
+ if (event.toolName !== "patch") return;
142
+ const summary = event.details as ApplyPatchSummary | undefined;
143
+ if (summary) {
144
+ emitTauEvent(pi, "tau:file-mutation.applied", {
145
+ source: "patch",
146
+ toolCallId: event.toolCallId,
147
+ cwd: ctx.cwd,
148
+ status: summary.status,
149
+ changes: summary.changes.map((change) => ({
150
+ path: change.path,
151
+ kind: change.kind,
152
+ move: change.move,
153
+ linesAdded: change.linesAdded,
154
+ linesRemoved: change.linesRemoved,
155
+ snapshotRanges: change.snapshotRanges,
156
+ })),
157
+ });
158
+ }
159
+ if (!summary || summary.status === "completed") return;
160
+ return { isError: true };
161
+ });
162
+
163
+ pi.on("session_start", () => {
164
+ rowState.clear();
165
+ const active = new Set(pi.getActiveTools());
166
+ active.add("patch");
167
+ for (const tool of SUPPRESSED_TOOLS) active.delete(tool);
168
+ pi.setActiveTools([...active]);
169
+ });
170
+ }