@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,323 @@
1
+ import { homedir } from "node:os";
2
+ import { relative } from "node:path";
3
+ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
4
+ import { type Component, getKeybindings, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
+ import { errorText, formatAge, preview } from "../../shared/text.ts";
6
+ import { bindingHint, rawHint, type ToolKeyHint } from "@shanepadgett/tau-tui";
7
+ import { type SelectableListActionResult, SelectableList } from "@shanepadgett/tau-tui";
8
+ import { Tabs } from "@shanepadgett/tau-tui";
9
+ import { ToolPanel, type ToolPanelConfig } from "@shanepadgett/tau-tui";
10
+ import {
11
+ archiveSession,
12
+ deleteSessionFile,
13
+ getArchiveRoot,
14
+ listManagedSessions,
15
+ type ManagedSession,
16
+ type SessionScope,
17
+ unarchiveSession,
18
+ } from "./sessions.ts";
19
+
20
+ type TabId = "active" | "archive";
21
+ type PendingKind = "archive" | "delete" | "unarchive";
22
+
23
+ interface PendingAction {
24
+ kind: PendingKind;
25
+ items: readonly ManagedSession[];
26
+ tab: TabId;
27
+ }
28
+
29
+ export async function showSessionManager(ctx: ExtensionCommandContext): Promise<void> {
30
+ if (ctx.mode !== "tui" || !ctx.hasUI) {
31
+ ctx.ui.notify("Session manager requires TUI mode.", "error");
32
+ return;
33
+ }
34
+
35
+ const currentSessionFile = ctx.sessionManager.getSessionFile() ?? undefined;
36
+ const initial = await listManagedSessions(ctx.cwd, "current", currentSessionFile);
37
+
38
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
39
+ const manager = new SessionManagerPanel(
40
+ ctx,
41
+ theme,
42
+ currentSessionFile,
43
+ initial,
44
+ () => done(undefined),
45
+ () => tui.requestRender(),
46
+ );
47
+ return manager.component;
48
+ });
49
+ }
50
+
51
+ class SessionManagerPanel {
52
+ readonly component: Component;
53
+ private scope: SessionScope = "current";
54
+ private active: readonly ManagedSession[];
55
+ private archive: readonly ManagedSession[];
56
+ private pending: PendingAction | undefined;
57
+ private executing = false;
58
+ private readonly ctx: ExtensionCommandContext;
59
+ private readonly theme: Theme;
60
+ private readonly currentSessionFile: string | undefined;
61
+ private readonly done: () => void;
62
+ private readonly requestRender: () => void;
63
+ private readonly activeList: SelectableList<ManagedSession>;
64
+ private readonly archiveList: SelectableList<ManagedSession>;
65
+ private readonly tabs: Tabs;
66
+ private readonly panelConfig: ToolPanelConfig;
67
+
68
+ constructor(
69
+ ctx: ExtensionCommandContext,
70
+ theme: Theme,
71
+ currentSessionFile: string | undefined,
72
+ initial: { active: readonly ManagedSession[]; archive: readonly ManagedSession[] },
73
+ done: () => void,
74
+ requestRender: () => void,
75
+ ) {
76
+ this.ctx = ctx;
77
+ this.theme = theme;
78
+ this.currentSessionFile = currentSessionFile;
79
+ this.done = done;
80
+ this.requestRender = requestRender;
81
+ this.active = initial.active;
82
+ this.archive = initial.archive;
83
+ this.activeList = this.createList("active", this.active);
84
+ this.archiveList = this.createList("archive", this.archive);
85
+ this.tabs = new Tabs(theme, this.tabItems(), "active");
86
+ this.panelConfig = {
87
+ title: "Manage sessions",
88
+ secondary: this.secondaryText(),
89
+ body: this.tabs,
90
+ footer: { kind: "hints", hints: this.footerHints() },
91
+ };
92
+ const panel = new ToolPanel(theme, this.panelConfig);
93
+ this.component = {
94
+ render: (width) => panel.render(width),
95
+ invalidate: () => panel.invalidate(),
96
+ handleInput: (data) => this.handleInput(data),
97
+ };
98
+ }
99
+
100
+ private createList(tab: TabId, items: readonly ManagedSession[]): SelectableList<ManagedSession> {
101
+ return new SelectableList(this.theme, {
102
+ items,
103
+ emptyMessage: tab === "active" ? "No saved sessions." : "No archived sessions.",
104
+ selection: { kind: "multi" },
105
+ maxVisible: 14,
106
+ actions:
107
+ tab === "active"
108
+ ? [
109
+ { id: "archive", key: "a", hint: rawHint("a", "archive"), target: "currentOrSelection" },
110
+ {
111
+ id: "archiveOlder",
112
+ key: Key.shift("a"),
113
+ hint: rawHint("A", "archive older"),
114
+ target: "olderThanCursor",
115
+ },
116
+ { id: "delete", key: "d", hint: rawHint("d", "delete"), target: "currentOrSelection" },
117
+ {
118
+ id: "deleteOlder",
119
+ key: Key.shift("d"),
120
+ hint: rawHint("D", "delete older"),
121
+ target: "olderThanCursor",
122
+ },
123
+ ]
124
+ : [
125
+ { id: "unarchive", key: "u", hint: rawHint("u", "unarchive"), target: "currentOrSelection" },
126
+ {
127
+ id: "unarchiveOlder",
128
+ key: Key.shift("u"),
129
+ hint: rawHint("U", "unarchive older"),
130
+ target: "olderThanCursor",
131
+ },
132
+ { id: "delete", key: "d", hint: rawHint("d", "delete"), target: "currentOrSelection" },
133
+ {
134
+ id: "deleteOlder",
135
+ key: Key.shift("d"),
136
+ hint: rawHint("D", "delete older"),
137
+ target: "olderThanCursor",
138
+ },
139
+ ],
140
+ renderItem: (item, state, width) => this.renderRow(item, state.active, width),
141
+ onResult: (result) => {
142
+ if (result.kind === "action") this.prepareAction(tab, result);
143
+ },
144
+ onSelectionChange: () => {},
145
+ });
146
+ }
147
+
148
+ private handleInput(data: string): void {
149
+ if (this.executing) return;
150
+ const keybindings = getKeybindings();
151
+
152
+ if (this.pending) {
153
+ if (keybindings.matches(data, "tui.select.confirm")) {
154
+ void this.confirmPending();
155
+ return;
156
+ }
157
+ if (keybindings.matches(data, "tui.select.cancel")) {
158
+ this.pending = undefined;
159
+ this.syncPanel();
160
+ return;
161
+ }
162
+ return;
163
+ }
164
+
165
+ if (matchesKey(data, Key.enter)) return;
166
+ if (matchesKey(data, Key.escape)) {
167
+ this.done();
168
+ return;
169
+ }
170
+ if (matchesKey(data, "s")) {
171
+ void this.toggleScope();
172
+ return;
173
+ }
174
+
175
+ this.tabs.handleInput(data);
176
+ this.syncPanel();
177
+ }
178
+
179
+ private prepareAction(tab: TabId, result: SelectableListActionResult<ManagedSession>): void {
180
+ if (result.items.length === 0) {
181
+ this.ctx.ui.notify(
182
+ result.target === "olderThanCursor" ? "No older sessions." : "No sessions selected.",
183
+ "info",
184
+ );
185
+ return;
186
+ }
187
+
188
+ const kind = result.actionId.startsWith("archive")
189
+ ? "archive"
190
+ : result.actionId.startsWith("unarchive")
191
+ ? "unarchive"
192
+ : "delete";
193
+ this.pending = { kind, items: result.items, tab };
194
+ this.syncPanel();
195
+ }
196
+
197
+ private async confirmPending(): Promise<void> {
198
+ const pending = this.pending;
199
+ if (!pending) return;
200
+ this.pending = undefined;
201
+ this.executing = true;
202
+
203
+ let failures = 0;
204
+ for (const session of pending.items) {
205
+ try {
206
+ if (pending.kind === "archive") await archiveSession(session.path);
207
+ else if (pending.kind === "unarchive") await unarchiveSession(session.path);
208
+ else await deleteSessionFile(session.path);
209
+ } catch (error) {
210
+ failures++;
211
+ this.ctx.ui.notify(`${preview(session.name, 40)}: ${errorText(error)}`, "error");
212
+ }
213
+ }
214
+
215
+ await this.reload();
216
+ this.listFor(pending.tab).clearSelection();
217
+ this.executing = false;
218
+ this.ctx.ui.notify(
219
+ this.resultMessage(pending.kind, pending.items.length - failures, failures),
220
+ failures ? "error" : "info",
221
+ );
222
+ this.syncPanel();
223
+ }
224
+
225
+ private async toggleScope(): Promise<void> {
226
+ this.scope = this.scope === "current" ? "all" : "current";
227
+ this.activeList.clearSelection();
228
+ this.archiveList.clearSelection();
229
+ await this.reload();
230
+ this.syncPanel();
231
+ }
232
+
233
+ private async reload(): Promise<void> {
234
+ try {
235
+ const sessions = await listManagedSessions(this.ctx.cwd, this.scope, this.currentSessionFile);
236
+ this.active = sessions.active;
237
+ this.archive = sessions.archive;
238
+ this.activeList.setItems(this.active);
239
+ this.archiveList.setItems(this.archive);
240
+ this.tabs.setTabs(this.tabItems());
241
+ } catch (error) {
242
+ this.ctx.ui.notify(`Could not load sessions: ${errorText(error)}`, "error");
243
+ }
244
+ }
245
+
246
+ private syncPanel(): void {
247
+ this.panelConfig.secondary = this.secondaryText();
248
+ this.panelConfig.footer = this.pending
249
+ ? {
250
+ kind: "destructiveAck",
251
+ message: this.pendingMessage(this.pending),
252
+ hints: [bindingHint("tui.select.confirm", "confirm"), bindingHint("tui.select.cancel", "cancel")],
253
+ }
254
+ : { kind: "hints", hints: this.footerHints() };
255
+ this.requestRender();
256
+ }
257
+
258
+ private tabItems() {
259
+ return [
260
+ {
261
+ id: "active",
262
+ label: "Sessions",
263
+ count: this.active.length,
264
+ body: this.activeList,
265
+ getKeyHints: () => this.activeList.getKeyHints(),
266
+ },
267
+ {
268
+ id: "archive",
269
+ label: "Archive",
270
+ count: this.archive.length,
271
+ body: this.archiveList,
272
+ getKeyHints: () => this.archiveList.getKeyHints(),
273
+ },
274
+ ];
275
+ }
276
+
277
+ private footerHints(): readonly ToolKeyHint[] {
278
+ return [
279
+ ...this.tabs.getKeyHints(),
280
+ rawHint("s", this.scope === "current" ? "all projects" : "current folder"),
281
+ bindingHint("tui.select.cancel", "close"),
282
+ ];
283
+ }
284
+
285
+ private renderRow(item: ManagedSession, active: boolean, width: number): string[] {
286
+ const path = this.scope === "all" && item.cwd ? shortenPath(item.cwd) : undefined;
287
+ const age = formatAge(item.modified.getTime());
288
+ const count = item.messageCount > 0 ? `${item.messageCount} msgs` : undefined;
289
+ const requiredSuffix = [path, age].filter((part) => part !== undefined).join(" ");
290
+ const fullSuffix = [path, count, age].filter((part) => part !== undefined).join(" ");
291
+ const fullSuffixFits = visibleWidth(fullSuffix) + 9 <= width;
292
+ const suffix = this.theme.fg("dim", count && fullSuffixFits ? fullSuffix : requiredSuffix);
293
+ const nameWidth = Math.max(8, width - visibleWidth(suffix) - 1);
294
+ const name = this.theme.fg(active ? "accent" : "text", preview(item.name, nameWidth));
295
+ return [truncateToWidth(`${name} ${suffix}`, width, "")];
296
+ }
297
+
298
+ private listFor(tab: TabId): SelectableList<ManagedSession> {
299
+ return tab === "active" ? this.activeList : this.archiveList;
300
+ }
301
+
302
+ private secondaryText(): string {
303
+ return `scope: ${this.scope === "current" ? "current folder" : "all projects"} · archive: ${getArchiveRoot()}`;
304
+ }
305
+
306
+ private pendingMessage(action: PendingAction): string {
307
+ const verb = action.kind === "archive" ? "Archive" : action.kind === "unarchive" ? "Unarchive" : "Delete";
308
+ return `${verb} ${action.items.length} session${action.items.length === 1 ? "" : "s"}?`;
309
+ }
310
+
311
+ private resultMessage(kind: PendingKind, count: number, failures: number): string {
312
+ const verb = kind === "archive" ? "Archived" : kind === "unarchive" ? "Unarchived" : "Deleted";
313
+ const base = `${verb} ${count} session${count === 1 ? "" : "s"}.`;
314
+ return failures > 0 ? `${base} ${failures} failed.` : base;
315
+ }
316
+ }
317
+
318
+ function shortenPath(cwd: string): string {
319
+ const home = homedir();
320
+ if (cwd === home) return "~";
321
+ if (cwd.startsWith(`${home}/`)) return `~/${relative(home, cwd)}`;
322
+ return cwd;
323
+ }
@@ -0,0 +1,126 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir, rename, unlink } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
+ import { getAgentDir, type SessionInfo, SessionManager } from "@earendil-works/pi-coding-agent";
6
+
7
+ export type SessionScope = "current" | "all";
8
+
9
+ export interface ManagedSession {
10
+ id: string;
11
+ path: string;
12
+ name: string;
13
+ cwd: string;
14
+ modified: Date;
15
+ messageCount: number;
16
+ }
17
+
18
+ function getSessionsRoot(): string {
19
+ return join(getAgentDir(), "sessions");
20
+ }
21
+
22
+ export function getArchiveRoot(): string {
23
+ return join(getAgentDir(), "session-archive");
24
+ }
25
+
26
+ export async function listManagedSessions(
27
+ cwd: string,
28
+ scope: SessionScope,
29
+ currentSessionFile?: string,
30
+ ): Promise<{ active: ManagedSession[]; archive: ManagedSession[] }> {
31
+ const currentSessionPath = currentSessionFile ? resolve(currentSessionFile) : undefined;
32
+ const activeSessions = scope === "current" ? await SessionManager.list(cwd) : await SessionManager.listAll();
33
+ const active = activeSessions
34
+ .filter((session) => resolve(session.path) !== currentSessionPath)
35
+ .map(toManagedSession);
36
+
37
+ if (scope === "current") {
38
+ // Mirrors Pi's default session dir encoding so archive paths match active paths.
39
+ const safeCwd = `--${resolve(cwd)
40
+ .replace(/^[/\\]/, "")
41
+ .replace(/[/\\:]/g, "-")}--`;
42
+ const archiveProjectDir = join(getArchiveRoot(), safeCwd);
43
+ return {
44
+ active,
45
+ archive: (await SessionManager.list(cwd, archiveProjectDir)).map(toManagedSession),
46
+ };
47
+ }
48
+
49
+ let archive: ManagedSession[] = [];
50
+ try {
51
+ const projectDirs = (await readdir(getArchiveRoot(), { withFileTypes: true }))
52
+ .filter((entry) => entry.isDirectory())
53
+ .map((entry) => join(getArchiveRoot(), entry.name));
54
+
55
+ for (const projectDir of projectDirs) {
56
+ archive = archive.concat((await SessionManager.listAll(projectDir)).map(toManagedSession));
57
+ }
58
+ } catch {
59
+ archive = [];
60
+ }
61
+
62
+ archive.sort((a, b) => b.modified.getTime() - a.modified.getTime());
63
+ return { active, archive };
64
+ }
65
+
66
+ export async function archiveSession(sessionPath: string): Promise<void> {
67
+ const relativePath = relative(getSessionsRoot(), sessionPath);
68
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath)) {
69
+ throw new Error("Cannot archive session outside sessions root.");
70
+ }
71
+
72
+ const destination = join(getArchiveRoot(), relativePath);
73
+ if (existsSync(destination)) {
74
+ throw new Error("Archived session already exists.");
75
+ }
76
+
77
+ await mkdir(dirname(destination), { recursive: true });
78
+ await rename(sessionPath, destination);
79
+ }
80
+
81
+ export async function unarchiveSession(sessionPath: string): Promise<void> {
82
+ const relativePath = relative(getArchiveRoot(), sessionPath);
83
+ if (relativePath === "" || relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath)) {
84
+ throw new Error("Cannot unarchive session outside archive root.");
85
+ }
86
+
87
+ const destination = join(getSessionsRoot(), relativePath);
88
+ if (existsSync(destination)) {
89
+ throw new Error("Active session already exists.");
90
+ }
91
+
92
+ await mkdir(dirname(destination), { recursive: true });
93
+ await rename(sessionPath, destination);
94
+ }
95
+
96
+ export async function deleteSessionFile(sessionPath: string): Promise<{ method: "trash" | "unlink" }> {
97
+ const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
98
+ const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
99
+
100
+ if (trashResult.status === 0 || !existsSync(sessionPath)) {
101
+ return { method: "trash" };
102
+ }
103
+
104
+ try {
105
+ await unlink(sessionPath);
106
+ return { method: "unlink" };
107
+ } catch (error) {
108
+ const unlinkError = error instanceof Error ? error.message : String(error);
109
+ const trashDetails = [trashResult.error?.message, trashResult.stderr?.trim().split("\n")[0]].filter(
110
+ (detail) => detail && detail.length > 0,
111
+ );
112
+ const trashError = trashDetails.length > 0 ? ` (trash: ${trashDetails.join(" · ").slice(0, 200)})` : "";
113
+ throw new Error(`Delete failed: ${unlinkError}${trashError}`);
114
+ }
115
+ }
116
+
117
+ function toManagedSession(session: SessionInfo): ManagedSession {
118
+ return {
119
+ id: session.path,
120
+ path: session.path,
121
+ name: session.name ?? session.firstMessage,
122
+ cwd: session.cwd,
123
+ modified: session.modified,
124
+ messageCount: session.messageCount,
125
+ };
126
+ }
@@ -0,0 +1,48 @@
1
+ # patch
2
+
3
+ Replaces the built-in `edit` and `write` tools with one multi-file patch tool. The agent applies structured patches to create, edit, move, and delete files while this extension is active.
4
+
5
+ ## What it does
6
+
7
+ Registers a `patch` tool that accepts a Codex-style patch body: one envelope, one or more file sections. It supports new files, full rewrites, contextual edits, renames, and deletions. Tau also supports `*** Replace File:` for explicit full-file replacement.
8
+
9
+ Malformed patch envelopes fail without changing files. Section-level failures — parse errors, duplicate paths, missing files, match failures, and existing move targets — fail those sections while independent valid sections still apply.
10
+
11
+ ## How to use
12
+
13
+ Patch marker lines may have leading/trailing whitespace.
14
+
15
+ Tool arguments must put the patch in the `input` string. Do not use the patch body as a JSON object key.
16
+
17
+ ```json
18
+ {
19
+ "input": "*** Begin Patch\n*** Add File: path/to/new.ts\n+file content here\n*** End Patch"
20
+ }
21
+ ```
22
+
23
+ ```text
24
+ *** Begin Patch
25
+ *** Add File: path/to/new.ts
26
+ +file content here
27
+ *** Replace File: path/to/existing-full-rewrite.ts
28
+ +full replacement content here
29
+ *** Update File: path/to/existing.ts
30
+ context line
31
+ -old line
32
+ +new line
33
+ *** Update File: path/to/old-location.ts
34
+ *** Move to: path/to/new-location.ts
35
+ const handler = () => {}
36
+ -module.exports = handler;
37
+ +export default handler;
38
+ *** Delete File: path/to/remove.ts
39
+ *** End Patch
40
+ ```
41
+
42
+ - **Add File** — writes whole-file content. Only `+` lines become file content. May overwrite existing files.
43
+ - **Replace File** — writes whole-file content and reports as a replacement. Only `+` lines become file content.
44
+ - **Update File** — edits existing files with context, removal, and addition lines. Unanchored repeated matches use the first forward match. A pure `+` hunk appends to EOF; include context body lines for positional inserts.
45
+ - **Move to** — appears immediately after an Update File header. The target must not already exist.
46
+ - **Delete File** — removes an existing file.
47
+
48
+ If an update match fails, reread the current file and retry with corrected context.