tau-coding-agent 0.1.6 → 0.2.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.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -79
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +17 -2
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +109 -191
- package/extensions/loop.ts +149 -145
- package/extensions/memory.ts +138 -86
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +41 -38
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +132 -3338
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/index.ts +249 -356
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +33 -39
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +10 -1
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
package/extensions/worktree.ts
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
|
|
21
21
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import {
|
|
23
|
-
CURRENT_SESSION_VERSION,
|
|
24
23
|
DynamicBorder,
|
|
25
24
|
SessionManager,
|
|
26
25
|
highlightCode,
|
|
@@ -35,7 +34,6 @@ import {
|
|
|
35
34
|
matchesKey,
|
|
36
35
|
} from "@earendil-works/pi-tui";
|
|
37
36
|
import { spawnSync } from "node:child_process";
|
|
38
|
-
import { randomUUID } from "node:crypto";
|
|
39
37
|
import * as fs from "node:fs";
|
|
40
38
|
import * as path from "node:path";
|
|
41
39
|
|
|
@@ -46,22 +44,6 @@ const SCRIPT_RERUN_ENTRY_TYPE = "worktree-script-rerun-command";
|
|
|
46
44
|
const MANUAL_OPEN_INTRO =
|
|
47
45
|
"Worktree ready. Open it in a separate terminal or tmux pane with this command";
|
|
48
46
|
|
|
49
|
-
type PromptStatus = "completed" | "error";
|
|
50
|
-
|
|
51
|
-
async function withPromptSignal<T>(pi: ExtensionAPI, run: () => Promise<T>): Promise<T> {
|
|
52
|
-
pi.events.emit("ui:prompt_start", { source: "worktree" });
|
|
53
|
-
|
|
54
|
-
let status: PromptStatus = "completed";
|
|
55
|
-
try {
|
|
56
|
-
return await run();
|
|
57
|
-
} catch (error) {
|
|
58
|
-
status = "error";
|
|
59
|
-
throw error;
|
|
60
|
-
} finally {
|
|
61
|
-
pi.events.emit("ui:prompt_end", { source: "worktree", status });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
47
|
const FETCH_TIMEOUT_MS = 60_000;
|
|
66
48
|
const STATUS_SPINNER_INTERVAL_MS = 80;
|
|
67
49
|
const STATUS_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -620,7 +602,6 @@ function pathExistsAndIsNotEmptyDir(p: string): boolean {
|
|
|
620
602
|
}
|
|
621
603
|
|
|
622
604
|
async function resolveWorktreePath(
|
|
623
|
-
pi: ExtensionAPI,
|
|
624
605
|
ctx: ExtensionCommandContext,
|
|
625
606
|
repo: RepoInfo,
|
|
626
607
|
worktrees: WorktreeInfo[],
|
|
@@ -639,9 +620,7 @@ async function resolveWorktreePath(
|
|
|
639
620
|
`Branch name "${branch}" normalizes to an empty directory name. Please choose a worktree directory.`,
|
|
640
621
|
"warning",
|
|
641
622
|
);
|
|
642
|
-
const input = await
|
|
643
|
-
ctx.ui.input("Enter worktree directory", suggested),
|
|
644
|
-
);
|
|
623
|
+
const input = await ctx.ui.input("Enter worktree directory", suggested);
|
|
645
624
|
if (!input) return null;
|
|
646
625
|
candidate = path.isAbsolute(input) ? input : path.resolve(repo.currentRoot, input);
|
|
647
626
|
} else {
|
|
@@ -663,9 +642,7 @@ async function resolveWorktreePath(
|
|
|
663
642
|
}
|
|
664
643
|
|
|
665
644
|
ctx.ui.notify(`${reason}: ${candidate}`, "warning");
|
|
666
|
-
const input = await
|
|
667
|
-
ctx.ui.input("Enter a different worktree directory", candidate),
|
|
668
|
-
);
|
|
645
|
+
const input = await ctx.ui.input("Enter a different worktree directory", candidate);
|
|
669
646
|
if (!input) return null;
|
|
670
647
|
|
|
671
648
|
candidate = path.isAbsolute(input) ? input : path.resolve(repo.currentRoot, input);
|
|
@@ -848,13 +825,14 @@ function parseWorktreeIncludePatterns(content: string): Array<{ glob: string; ne
|
|
|
848
825
|
function matchesWorktreeInclude(
|
|
849
826
|
entry: string,
|
|
850
827
|
patterns: Array<{ glob: string; negate: boolean }>,
|
|
828
|
+
included = false,
|
|
851
829
|
): boolean {
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
let matched = false;
|
|
830
|
+
// Both git ls-files --directory and our traversal mark directories with /.
|
|
831
|
+
const isDirectory = entry.endsWith("/");
|
|
832
|
+
const normalizedEntry = isDirectory ? entry.slice(0, -1) : entry;
|
|
833
|
+
let matched = included;
|
|
857
834
|
for (const { glob, negate } of patterns) {
|
|
835
|
+
if (glob.endsWith("/") && !isDirectory) continue;
|
|
858
836
|
const normalizedGlob = glob.replace(/\/$/, "");
|
|
859
837
|
if (path.matchesGlob(normalizedEntry, normalizedGlob)) {
|
|
860
838
|
matched = !negate;
|
|
@@ -873,10 +851,22 @@ function matchesWorktreeInclude(
|
|
|
873
851
|
* clonefile on large directories can saturate disk I/O and block interactive
|
|
874
852
|
* processes). Symlinks are preserved (re-created, not followed).
|
|
875
853
|
*/
|
|
876
|
-
function copyDirRecursive(
|
|
854
|
+
function copyDirRecursive(
|
|
855
|
+
src: string,
|
|
856
|
+
dest: string,
|
|
857
|
+
relativeDir: string,
|
|
858
|
+
patterns: Array<{ glob: string; negate: boolean }>,
|
|
859
|
+
): void {
|
|
877
860
|
fs.mkdirSync(dest, { recursive: true });
|
|
878
861
|
|
|
879
862
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
863
|
+
const relativePath = `${relativeDir}/${entry.name}`;
|
|
864
|
+
// Children inherit inclusion, but their own rules can override it. Like
|
|
865
|
+
// gitignore, excluded directories are pruned; reinclude the directory itself
|
|
866
|
+
// before trying to reinclude anything inside it. No preliminary tree scan.
|
|
867
|
+
if (!matchesWorktreeInclude(relativePath + (entry.isDirectory() ? "/" : ""), patterns, true)) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
880
870
|
const srcPath = path.join(src, entry.name);
|
|
881
871
|
const destPath = path.join(dest, entry.name);
|
|
882
872
|
|
|
@@ -888,7 +878,7 @@ function copyDirRecursive(src: string, dest: string): void {
|
|
|
888
878
|
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
889
879
|
}
|
|
890
880
|
} else if (entry.isDirectory()) {
|
|
891
|
-
copyDirRecursive(srcPath, destPath);
|
|
881
|
+
copyDirRecursive(srcPath, destPath, relativePath, patterns);
|
|
892
882
|
} else {
|
|
893
883
|
try {
|
|
894
884
|
fs.copyFileSync(srcPath, destPath, COPYFILE_COW_EXCL);
|
|
@@ -955,11 +945,9 @@ async function applyWorktreeInclude(
|
|
|
955
945
|
if (entriesToCopy.length === 0) return;
|
|
956
946
|
|
|
957
947
|
const listing = entriesToCopy.map((e) => ` ${e}`).join("\n");
|
|
958
|
-
const ok = await
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
`Found .worktreeinclude. Copy these gitignored entries:\n\n${listing}`,
|
|
962
|
-
),
|
|
948
|
+
const ok = await ctx.ui.confirm(
|
|
949
|
+
"Copy cached files from main worktree?",
|
|
950
|
+
`Found .worktreeinclude. Copy these gitignored entries:\n\n${listing}`,
|
|
963
951
|
);
|
|
964
952
|
if (!ok) return;
|
|
965
953
|
|
|
@@ -977,7 +965,7 @@ async function applyWorktreeInclude(
|
|
|
977
965
|
try {
|
|
978
966
|
const srcStat = fs.lstatSync(src);
|
|
979
967
|
if (srcStat.isDirectory()) {
|
|
980
|
-
copyDirRecursive(src, dest);
|
|
968
|
+
copyDirRecursive(src, dest, entry.replace(/\/$/, ""), patterns);
|
|
981
969
|
} else if (srcStat.isSymbolicLink()) {
|
|
982
970
|
const target = fs.readlinkSync(src);
|
|
983
971
|
const destParent = path.dirname(dest);
|
|
@@ -1023,27 +1011,24 @@ async function runProjectScripts(
|
|
|
1023
1011
|
|
|
1024
1012
|
if (actions.length === 1) {
|
|
1025
1013
|
const action = actions[0];
|
|
1026
|
-
const ok = await
|
|
1027
|
-
|
|
1014
|
+
const ok = await ctx.ui.confirm(
|
|
1015
|
+
`Run worktree ${phase}?`,
|
|
1016
|
+
`${action.label}\n\nCommand:\n${action.command}`,
|
|
1028
1017
|
);
|
|
1029
1018
|
if (!ok) return;
|
|
1030
1019
|
chosen = action;
|
|
1031
1020
|
} else {
|
|
1032
1021
|
const options = ["Skip", ...actions.map((a) => `${a.label} (${a.source})`)];
|
|
1033
|
-
const choice = await
|
|
1034
|
-
ctx.ui.select(`Choose ${phase} script to run`, options),
|
|
1035
|
-
);
|
|
1022
|
+
const choice = await ctx.ui.select(`Choose ${phase} script to run`, options);
|
|
1036
1023
|
if (!choice || choice === "Skip") return;
|
|
1037
1024
|
|
|
1038
1025
|
const idx = options.indexOf(choice) - 1;
|
|
1039
1026
|
const selectedAction = actions[idx];
|
|
1040
1027
|
if (!selectedAction) return;
|
|
1041
1028
|
|
|
1042
|
-
const ok = await
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
`${selectedAction.label}\n\nCommand:\n${selectedAction.command}`,
|
|
1046
|
-
),
|
|
1029
|
+
const ok = await ctx.ui.confirm(
|
|
1030
|
+
`Run worktree ${phase}?`,
|
|
1031
|
+
`${selectedAction.label}\n\nCommand:\n${selectedAction.command}`,
|
|
1047
1032
|
);
|
|
1048
1033
|
if (!ok) return;
|
|
1049
1034
|
|
|
@@ -1092,7 +1077,6 @@ type ParsedWorktreeTargetArgs = {
|
|
|
1092
1077
|
};
|
|
1093
1078
|
|
|
1094
1079
|
async function parseWorktreeTargetArgs(
|
|
1095
|
-
pi: ExtensionAPI,
|
|
1096
1080
|
ctx: ExtensionCommandContext,
|
|
1097
1081
|
args: string,
|
|
1098
1082
|
usage: string,
|
|
@@ -1103,7 +1087,7 @@ async function parseWorktreeTargetArgs(
|
|
|
1103
1087
|
if (!ctx.hasUI) {
|
|
1104
1088
|
throw new Error(usage);
|
|
1105
1089
|
}
|
|
1106
|
-
const input = await
|
|
1090
|
+
const input = await ctx.ui.input("Branch name");
|
|
1107
1091
|
if (!input) return null;
|
|
1108
1092
|
branch = input.trim();
|
|
1109
1093
|
}
|
|
@@ -1166,22 +1150,18 @@ async function maybeSwitchMainToDefaultBranch(
|
|
|
1166
1150
|
);
|
|
1167
1151
|
}
|
|
1168
1152
|
|
|
1169
|
-
const ok = await
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
`Main worktree is on ${current}. Checkout ${defaultBranch} to ${reason}?`,
|
|
1173
|
-
),
|
|
1153
|
+
const ok = await ctx.ui.confirm(
|
|
1154
|
+
"Switch main worktree?",
|
|
1155
|
+
`Main worktree is on ${current}. Checkout ${defaultBranch} to ${reason}?`,
|
|
1174
1156
|
);
|
|
1175
1157
|
if (!ok) return { proceed: false, switched: false };
|
|
1176
1158
|
|
|
1177
1159
|
let stashSpec: string | undefined;
|
|
1178
1160
|
if (await isDirty(pi, repo.mainRoot)) {
|
|
1179
|
-
const choice = await
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
]),
|
|
1184
|
-
);
|
|
1161
|
+
const choice = await ctx.ui.select("Main worktree has uncommitted changes", [
|
|
1162
|
+
"Stash changes (including untracked) and continue",
|
|
1163
|
+
"Cancel",
|
|
1164
|
+
]);
|
|
1185
1165
|
if (!choice || choice.startsWith("Cancel")) {
|
|
1186
1166
|
return { proceed: false, switched: false };
|
|
1187
1167
|
}
|
|
@@ -1217,17 +1197,15 @@ async function createWorktree(
|
|
|
1217
1197
|
branch: string,
|
|
1218
1198
|
fromRef?: string,
|
|
1219
1199
|
): Promise<string | null> {
|
|
1220
|
-
const targetPath = await resolveWorktreePath(
|
|
1200
|
+
const targetPath = await resolveWorktreePath(ctx, repo, worktrees, branch);
|
|
1221
1201
|
if (!targetPath) return null;
|
|
1222
1202
|
|
|
1223
1203
|
const branchExists = await localBranchExists(pi, repo.mainRoot, branch);
|
|
1224
1204
|
if (branchExists && fromRef) {
|
|
1225
1205
|
if (ctx.hasUI) {
|
|
1226
|
-
const ok = await
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
`Branch ${branch} already exists.\n\nContinuing will use the existing branch at its current state; --from (${fromRef}) will have no effect.\n\nContinue?`,
|
|
1230
|
-
),
|
|
1206
|
+
const ok = await ctx.ui.confirm(
|
|
1207
|
+
"Branch exists",
|
|
1208
|
+
`Branch ${branch} already exists.\n\nContinuing will use the existing branch at its current state; --from (${fromRef}) will have no effect.\n\nContinue?`,
|
|
1231
1209
|
);
|
|
1232
1210
|
if (!ok) return null;
|
|
1233
1211
|
} else {
|
|
@@ -1281,22 +1259,12 @@ function hasValidSessionFile(sessionFile: string): boolean {
|
|
|
1281
1259
|
}
|
|
1282
1260
|
}
|
|
1283
1261
|
|
|
1284
|
-
function createFreshSessionFile(targetCwd: string, sessionDir
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
const sessionFile = path.join(sessionDir, `${fileTimestamp}_${sessionId}.jsonl`);
|
|
1291
|
-
const header: SessionHeader = {
|
|
1292
|
-
type: "session",
|
|
1293
|
-
version: CURRENT_SESSION_VERSION,
|
|
1294
|
-
id: sessionId,
|
|
1295
|
-
timestamp,
|
|
1296
|
-
cwd: targetCwd,
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`);
|
|
1262
|
+
function createFreshSessionFile(targetCwd: string, sessionDir?: string): string {
|
|
1263
|
+
const session = SessionManager.create(targetCwd, sessionDir);
|
|
1264
|
+
const sessionFile = session.getSessionFile()!;
|
|
1265
|
+
// Pi defers its first write until an assistant reply. Persist the native header
|
|
1266
|
+
// now so switchSession can open an otherwise empty conversation.
|
|
1267
|
+
fs.writeFileSync(sessionFile, `${JSON.stringify(session.getHeader())}\n`, { flag: "wx" });
|
|
1300
1268
|
return sessionFile;
|
|
1301
1269
|
}
|
|
1302
1270
|
|
|
@@ -1334,14 +1302,24 @@ async function switchToWorktree(
|
|
|
1334
1302
|
return;
|
|
1335
1303
|
}
|
|
1336
1304
|
|
|
1337
|
-
const
|
|
1305
|
+
const currentSessionDir = ctx.sessionManager.getSessionDir();
|
|
1306
|
+
// The read-only context omits usesDefaultSessionDir(). A new, unwritten manager
|
|
1307
|
+
// asks Pi's native storage policy without opening or modifying source history,
|
|
1308
|
+
// including when the source has not been persisted yet.
|
|
1309
|
+
const storage = SessionManager.create(ctx.sessionManager.getCwd(), currentSessionDir);
|
|
1310
|
+
const sessionDir = storage.usesDefaultSessionDir() ? undefined : currentSessionDir;
|
|
1338
1311
|
const hasAssistantReply = ctx.sessionManager
|
|
1339
1312
|
.getEntries()
|
|
1340
1313
|
.some((entry) => entry.type === "message" && entry.message.role === "assistant");
|
|
1341
1314
|
|
|
1342
1315
|
let sessionFile: string;
|
|
1343
1316
|
if (hasValidSessionFile(currentSessionFile)) {
|
|
1317
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
1344
1318
|
const nextSession = SessionManager.forkFrom(currentSessionFile, targetPath, sessionDir);
|
|
1319
|
+
if (leafId) nextSession.branch(leafId);
|
|
1320
|
+
else nextSession.resetLeaf();
|
|
1321
|
+
// Leaf navigation alone is not persisted when the session is reopened.
|
|
1322
|
+
nextSession.appendCustomEntry("worktree-branch", { sourceLeafId: leafId });
|
|
1345
1323
|
const forkedSessionFile = nextSession.getSessionFile();
|
|
1346
1324
|
if (!forkedSessionFile) {
|
|
1347
1325
|
throw new Error(`Failed to create a session for worktree: ${targetPath}`);
|
|
@@ -1382,7 +1360,6 @@ async function handleNew(
|
|
|
1382
1360
|
args: string,
|
|
1383
1361
|
): Promise<void> {
|
|
1384
1362
|
const parsed = await parseWorktreeTargetArgs(
|
|
1385
|
-
pi,
|
|
1386
1363
|
ctx,
|
|
1387
1364
|
args,
|
|
1388
1365
|
"Usage: /worktree new <branch> [--from <ref>]",
|
|
@@ -1474,7 +1451,6 @@ async function handleSwitch(
|
|
|
1474
1451
|
args: string,
|
|
1475
1452
|
): Promise<void> {
|
|
1476
1453
|
const parsed = await parseWorktreeTargetArgs(
|
|
1477
|
-
pi,
|
|
1478
1454
|
ctx,
|
|
1479
1455
|
args,
|
|
1480
1456
|
"Usage: /worktree switch <branch> [--from <ref>]",
|
|
@@ -1562,13 +1538,11 @@ async function archiveWorktree(
|
|
|
1562
1538
|
throw new Error(`Cannot archive dirty worktree without UI: ${wt.path}`);
|
|
1563
1539
|
}
|
|
1564
1540
|
|
|
1565
|
-
const choice = await
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
]),
|
|
1571
|
-
);
|
|
1541
|
+
const choice = await ctx.ui.select(`Worktree has uncommitted changes: ${wt.path}`, [
|
|
1542
|
+
"Stash changes (including untracked) and archive",
|
|
1543
|
+
"Force remove (lose changes)",
|
|
1544
|
+
"Cancel",
|
|
1545
|
+
]);
|
|
1572
1546
|
|
|
1573
1547
|
if (!choice || choice === "Cancel") {
|
|
1574
1548
|
return {
|
|
@@ -1634,11 +1608,9 @@ async function archiveWorktree(
|
|
|
1634
1608
|
|
|
1635
1609
|
// Branch has commits not on upstream (not fully pushed)
|
|
1636
1610
|
if (ctx.hasUI) {
|
|
1637
|
-
const ok = await
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
`Branch ${branch} is ahead of ${upstream} by ${ahead} commit(s). Delete it anyway?`,
|
|
1641
|
-
),
|
|
1611
|
+
const ok = await ctx.ui.confirm(
|
|
1612
|
+
"Delete local branch?",
|
|
1613
|
+
`Branch ${branch} is ahead of ${upstream} by ${ahead} commit(s). Delete it anyway?`,
|
|
1642
1614
|
);
|
|
1643
1615
|
if (ok) {
|
|
1644
1616
|
const del = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
|
|
@@ -1648,11 +1620,9 @@ async function archiveWorktree(
|
|
|
1648
1620
|
} else if (!aheadBehind) {
|
|
1649
1621
|
// Couldn't determine ahead/behind; be conservative.
|
|
1650
1622
|
if (ctx.hasUI) {
|
|
1651
|
-
const ok = await
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
`Branch ${branch} has an upstream (${upstream}), but I couldn't determine if it's fully pushed. Delete it anyway?`,
|
|
1655
|
-
),
|
|
1623
|
+
const ok = await ctx.ui.confirm(
|
|
1624
|
+
"Delete local branch?",
|
|
1625
|
+
`Branch ${branch} has an upstream (${upstream}), but I couldn't determine if it's fully pushed. Delete it anyway?`,
|
|
1656
1626
|
);
|
|
1657
1627
|
if (ok) {
|
|
1658
1628
|
const del = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
|
|
@@ -1667,11 +1637,9 @@ async function archiveWorktree(
|
|
|
1667
1637
|
|
|
1668
1638
|
if (!branchDeleted && ctx.hasUI) {
|
|
1669
1639
|
const details = [del.stdout.trim(), del.stderr.trim()].filter(Boolean).join("\n");
|
|
1670
|
-
const ok = await
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
`git branch -d ${branch} failed.${details ? `\n\n${details}` : ""}\n\nThis usually means the branch isn't merged into the main worktree's current branch.\n\nThe branch appears fully pushed to ${upstream}. Force delete it with -D?`,
|
|
1674
|
-
),
|
|
1640
|
+
const ok = await ctx.ui.confirm(
|
|
1641
|
+
"Force delete local branch?",
|
|
1642
|
+
`git branch -d ${branch} failed.${details ? `\n\n${details}` : ""}\n\nThis usually means the branch isn't merged into the main worktree's current branch.\n\nThe branch appears fully pushed to ${upstream}. Force delete it with -D?`,
|
|
1675
1643
|
);
|
|
1676
1644
|
if (ok) {
|
|
1677
1645
|
const forceDel = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
|
|
@@ -1688,11 +1656,9 @@ async function archiveWorktree(
|
|
|
1688
1656
|
}
|
|
1689
1657
|
} else {
|
|
1690
1658
|
if (ctx.hasUI) {
|
|
1691
|
-
const ok = await
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
`Branch ${branch} has no upstream. Delete it anyway?`,
|
|
1695
|
-
),
|
|
1659
|
+
const ok = await ctx.ui.confirm(
|
|
1660
|
+
"Delete local branch?",
|
|
1661
|
+
`Branch ${branch} has no upstream. Delete it anyway?`,
|
|
1696
1662
|
);
|
|
1697
1663
|
if (ok) {
|
|
1698
1664
|
const del = await git(pi, repo.mainRoot, ["branch", "-D", branch]);
|
|
@@ -1718,7 +1684,7 @@ async function handleArchive(
|
|
|
1718
1684
|
let branch = tokens[0];
|
|
1719
1685
|
if (!branch || branch.startsWith("-")) {
|
|
1720
1686
|
if (!ctx.hasUI) return;
|
|
1721
|
-
const input = await
|
|
1687
|
+
const input = await ctx.ui.input("Branch name");
|
|
1722
1688
|
if (!input) return;
|
|
1723
1689
|
branch = input.trim();
|
|
1724
1690
|
}
|
|
@@ -1848,16 +1814,14 @@ async function handleClean(pi: ExtensionAPI, ctx: ExtensionCommandContext): Prom
|
|
|
1848
1814
|
const dirtyCount = candidates.filter((c) => c.dirty).length;
|
|
1849
1815
|
|
|
1850
1816
|
if (dirtyCount > 0) {
|
|
1851
|
-
const choice = await
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
],
|
|
1860
|
-
),
|
|
1817
|
+
const choice = await ctx.ui.select(
|
|
1818
|
+
`Found ${candidates.length} pushed worktree(s): ${candidates.length - dirtyCount} clean, ${dirtyCount} dirty`,
|
|
1819
|
+
[
|
|
1820
|
+
"Archive clean only (skip dirty)",
|
|
1821
|
+
"Stash dirty (including untracked) and archive all",
|
|
1822
|
+
"Force remove dirty and archive all (lose changes)",
|
|
1823
|
+
"Cancel",
|
|
1824
|
+
],
|
|
1861
1825
|
);
|
|
1862
1826
|
|
|
1863
1827
|
if (!choice || choice === "Cancel") {
|
|
@@ -1871,11 +1835,9 @@ async function handleClean(pi: ExtensionAPI, ctx: ExtensionCommandContext): Prom
|
|
|
1871
1835
|
? "force"
|
|
1872
1836
|
: "skip";
|
|
1873
1837
|
} else {
|
|
1874
|
-
const ok = await
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
`Archive ${candidates.length} pushed worktree(s)?`,
|
|
1878
|
-
),
|
|
1838
|
+
const ok = await ctx.ui.confirm(
|
|
1839
|
+
"Archive pushed worktrees?",
|
|
1840
|
+
`Archive ${candidates.length} pushed worktree(s)?`,
|
|
1879
1841
|
);
|
|
1880
1842
|
if (!ok) {
|
|
1881
1843
|
ctx.ui.notify("Cancelled", "warning");
|
|
@@ -2026,59 +1988,57 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
|
|
|
2026
1988
|
const theme = ctx.ui.theme;
|
|
2027
1989
|
|
|
2028
1990
|
const selectItems: SelectItem[] = items.map((item) => ({
|
|
2029
|
-
value: item.
|
|
1991
|
+
value: item.wt.path,
|
|
2030
1992
|
label: formatWorktreeLabel(item, theme),
|
|
2031
1993
|
}));
|
|
2032
1994
|
|
|
2033
|
-
const itemByValue = new Map(items.map((item) => [item.
|
|
1995
|
+
const itemByValue = new Map(items.map((item) => [item.wt.path, item]));
|
|
2034
1996
|
|
|
2035
1997
|
type ListResult = { action: "switch" | "archive"; item: WorktreeDisplayItem } | null;
|
|
2036
1998
|
|
|
2037
|
-
const result = await
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
|
|
2041
|
-
|
|
2042
|
-
const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
|
|
2043
|
-
selectedPrefix: (t) => theme.fg("accent", t),
|
|
2044
|
-
selectedText: (t) => theme.fg("accent", t),
|
|
2045
|
-
description: (t) => t,
|
|
2046
|
-
scrollInfo: (t) => theme.fg("dim", t),
|
|
2047
|
-
noMatch: (t) => theme.fg("warning", t),
|
|
2048
|
-
});
|
|
2049
|
-
selectList.onSelect = (si) => {
|
|
2050
|
-
const item = itemByValue.get(si.value);
|
|
2051
|
-
if (item) done({ action: "switch", item });
|
|
2052
|
-
else done(null);
|
|
2053
|
-
};
|
|
2054
|
-
selectList.onCancel = () => done(null);
|
|
2055
|
-
container.addChild(selectList);
|
|
1999
|
+
const result = await ctx.ui.custom<ListResult>((tui, theme, _kb, done) => {
|
|
2000
|
+
const container = new Container();
|
|
2001
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
|
|
2056
2002
|
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
)
|
|
2060
|
-
|
|
2003
|
+
const selectList = new SelectList(selectItems, Math.min(selectItems.length, 15), {
|
|
2004
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
2005
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
2006
|
+
description: (t) => t,
|
|
2007
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
2008
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
2009
|
+
});
|
|
2010
|
+
selectList.onSelect = (si) => {
|
|
2011
|
+
const item = itemByValue.get(si.value);
|
|
2012
|
+
if (item) done({ action: "switch", item });
|
|
2013
|
+
else done(null);
|
|
2014
|
+
};
|
|
2015
|
+
selectList.onCancel = () => done(null);
|
|
2016
|
+
container.addChild(selectList);
|
|
2061
2017
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2018
|
+
container.addChild(
|
|
2019
|
+
new Text(theme.fg("dim", " ↑↓ navigate enter switch a archive esc close"), 0, 0),
|
|
2020
|
+
);
|
|
2021
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("borderMuted", s)));
|
|
2022
|
+
|
|
2023
|
+
return {
|
|
2024
|
+
render: (w) => container.render(w),
|
|
2025
|
+
invalidate: () => container.invalidate(),
|
|
2026
|
+
handleInput: (data) => {
|
|
2027
|
+
if (matchesKey(data, "a")) {
|
|
2028
|
+
const si = selectList.getSelectedItem();
|
|
2029
|
+
if (si) {
|
|
2030
|
+
const item = itemByValue.get(si.value);
|
|
2031
|
+
if (item) {
|
|
2032
|
+
done({ action: "archive", item });
|
|
2033
|
+
return;
|
|
2074
2034
|
}
|
|
2075
2035
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
);
|
|
2036
|
+
}
|
|
2037
|
+
selectList.handleInput(data);
|
|
2038
|
+
tui.requestRender();
|
|
2039
|
+
},
|
|
2040
|
+
};
|
|
2041
|
+
});
|
|
2082
2042
|
|
|
2083
2043
|
if (!result) return;
|
|
2084
2044
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tau-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Opinionated Pi package with web search, review workflows, sandboxing, memory, usage reporting, skills, and a theme",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.
|
|
30
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.73",
|
|
31
31
|
"@mozilla/readability": "^0.6.0",
|
|
32
32
|
"jsdom": "^29.0.2",
|
|
33
|
+
"proper-lockfile": "4.1.2",
|
|
33
34
|
"puppeteer-core": "^24.40.0",
|
|
34
35
|
"turndown": "^7.2.4",
|
|
35
|
-
"turndown-plugin-gfm": "^1.0.2"
|
|
36
|
+
"turndown-plugin-gfm": "^1.0.2",
|
|
37
|
+
"undici": "8.9.0"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
|
38
40
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -47,8 +49,9 @@
|
|
|
47
49
|
"pi": {
|
|
48
50
|
"extensions": [
|
|
49
51
|
"extensions/answer.ts",
|
|
50
|
-
"extensions/branch-term.ts",
|
|
52
|
+
"extensions/branch-term/index.ts",
|
|
51
53
|
"extensions/btw.ts",
|
|
54
|
+
"extensions/caffeinate/index.ts",
|
|
52
55
|
"extensions/ghostty.ts",
|
|
53
56
|
"extensions/git-diff-stats.ts",
|
|
54
57
|
"extensions/git-pr-status.ts",
|
|
@@ -57,10 +60,11 @@
|
|
|
57
60
|
"extensions/loop.ts",
|
|
58
61
|
"extensions/memory.ts",
|
|
59
62
|
"extensions/notify.ts",
|
|
60
|
-
"extensions/
|
|
63
|
+
"extensions/fast.ts",
|
|
61
64
|
"extensions/openai-verbosity.ts",
|
|
62
65
|
"extensions/review/index.ts",
|
|
63
66
|
"extensions/sandbox/index.ts",
|
|
67
|
+
"extensions/subagent/index.ts",
|
|
64
68
|
"extensions/tool-display-mode.ts",
|
|
65
69
|
"extensions/usage/index.ts",
|
|
66
70
|
"extensions/websearch/index.ts",
|
|
@@ -70,6 +74,7 @@
|
|
|
70
74
|
"skills/browser-tools",
|
|
71
75
|
"skills/git-clean-history",
|
|
72
76
|
"skills/git-commit",
|
|
77
|
+
"skills/github-pull-request",
|
|
73
78
|
"skills/oracle",
|
|
74
79
|
"skills/sentry",
|
|
75
80
|
"skills/update-changelog",
|