toyon 0.4.1 → 0.5.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 +1 -1
- package/dist/bridge.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/daemon.js +1619 -761
- package/dist/shell/assets/{Editor-BV5mlMUd.js → Editor-DR-GTFmf.js} +3 -3
- package/dist/shell/assets/{XTerm-vYHVHYnx.js → XTerm-B3kT_naP.js} +12 -12
- package/dist/shell/assets/{cssMode-DmPPq0RM.js → cssMode-CB4CczRa.js} +1 -1
- package/dist/shell/assets/{freemarker2-D0jw6qr7.js → freemarker2-Wjq3dVyB.js} +1 -1
- package/dist/shell/assets/{grammar-SjzA6dB3.js → grammar-yCr7BdNP.js} +1 -1
- package/dist/shell/assets/{handlebars-DP51I8rr.js → handlebars-Bjvi6pTu.js} +1 -1
- package/dist/shell/assets/{html-lErft7TP.js → html-iB2mKeN2.js} +1 -1
- package/dist/shell/assets/{htmlMode-B8WYzHvO.js → htmlMode-DtZVhckD.js} +1 -1
- package/dist/shell/assets/index-DVg10dTN.js +237 -0
- package/dist/shell/assets/index-wt4cLIYn.css +1 -0
- package/dist/shell/assets/{javascript-2o8dwNxB.js → javascript-BpuIkItm.js} +1 -1
- package/dist/shell/assets/{jsonMode-BzyCh8f1.js → jsonMode-QCLe5ypz.js} +1 -1
- package/dist/shell/assets/{liquid-DwwOicEv.js → liquid-CQI6mzhA.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-BSPiRlDO.js → lspLanguageFeatures-DqXzGsHQ.js} +1 -1
- package/dist/shell/assets/{mdx-Dx7gR42C.js → mdx-D2Unh-By.js} +1 -1
- package/dist/shell/assets/{python-B09pxq08.js → python-LLSqiJ4D.js} +1 -1
- package/dist/shell/assets/{razor-DO8UQZDP.js → razor-CJ2qsTB8.js} +1 -1
- package/dist/shell/assets/{tsMode-1xhdljsc.js → tsMode-CwjjHUH5.js} +1 -1
- package/dist/shell/assets/{xml-Be6HmYJA.js → xml-BzqCyEOW.js} +1 -1
- package/dist/shell/assets/{yaml-D4xpEWFo.js → yaml-N1Bfi3ds.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/package.json +1 -1
- package/dist/shell/assets/index-2d0IGh4f.js +0 -232
- package/dist/shell/assets/index-DZ08PVmu.css +0 -1
package/dist/daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../daemon/src/index.ts
|
|
3
3
|
import { rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
|
|
4
|
-
import { dirname as
|
|
4
|
+
import { dirname as dirname10 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
7
7
|
// ../shared/src/agent-tools.ts
|
|
@@ -12,6 +12,16 @@ var WRITE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
|
12
12
|
function isWriteTool(ev) {
|
|
13
13
|
return ev.kind ? WRITE_KINDS.has(ev.kind) : WRITE_TOOLS.has(ev.name);
|
|
14
14
|
}
|
|
15
|
+
var WRITTEN_KINDS = ["read", "edit", "delete", "move", "search", "execute", "fetch"];
|
|
16
|
+
function isWrittenKind(kind) {
|
|
17
|
+
return WRITTEN_KINDS.includes(kind ?? "");
|
|
18
|
+
}
|
|
19
|
+
function emptyInput(input) {
|
|
20
|
+
if (!input || typeof input !== "object")
|
|
21
|
+
return true;
|
|
22
|
+
const rec = input;
|
|
23
|
+
return Object.keys(rec).every((k) => k === "locations" && Array.isArray(rec[k]) && rec[k].length === 0);
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
// ../shared/src/model.ts
|
|
17
27
|
var DEFAULT_PERMISSION_MODE = "auto";
|
|
@@ -37,6 +47,12 @@ function streamKey(worktreeId, stream) {
|
|
|
37
47
|
function isMain(wt) {
|
|
38
48
|
return wt.kind === "main";
|
|
39
49
|
}
|
|
50
|
+
function isProvisional(wt) {
|
|
51
|
+
return wt.kind === "spare";
|
|
52
|
+
}
|
|
53
|
+
function isLead(wt) {
|
|
54
|
+
return isProvisional(wt) || isMain(wt);
|
|
55
|
+
}
|
|
40
56
|
function canArchive(wt) {
|
|
41
57
|
return wt.kind === "worktree";
|
|
42
58
|
}
|
|
@@ -52,8 +68,8 @@ function canGraft(wt) {
|
|
|
52
68
|
|
|
53
69
|
// ../shared/src/railOrder.ts
|
|
54
70
|
var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
|
|
55
|
-
var tierOf = (w) =>
|
|
56
|
-
var railUnitOf = (w) => w.variant ?
|
|
71
|
+
var tierOf = (w) => isLead(w) ? 0 : 1;
|
|
72
|
+
var railUnitOf = (w) => w.variant ? `group:${w.variant.group}` : `row:${w.id}`;
|
|
57
73
|
function railOrder(rows) {
|
|
58
74
|
const units = [];
|
|
59
75
|
const groups = new Map;
|
|
@@ -275,11 +291,6 @@ function middle(a, b) {
|
|
|
275
291
|
return ops;
|
|
276
292
|
}
|
|
277
293
|
|
|
278
|
-
// ../shared/src/drafts.ts
|
|
279
|
-
var DRAFT_PREFIX = "draft:";
|
|
280
|
-
var draftKey = (repoId) => DRAFT_PREFIX + repoId;
|
|
281
|
-
var draftRepoOf = (boxId) => boxId.startsWith(DRAFT_PREFIX) ? boxId.slice(DRAFT_PREFIX.length) : null;
|
|
282
|
-
|
|
283
294
|
// ../shared/src/land.ts
|
|
284
295
|
var DEFAULT_LAND_ROUTE = "merge";
|
|
285
296
|
var DEFAULT_MERGE_METHOD = "merge";
|
|
@@ -297,7 +308,7 @@ var PAIR_TTL_MS = 120000;
|
|
|
297
308
|
var pairLink = (host, code) => `https://${host}/#pair=${code}`;
|
|
298
309
|
|
|
299
310
|
// ../shared/src/protocol/limits.ts
|
|
300
|
-
var PROTOCOL_VERSION =
|
|
311
|
+
var PROTOCOL_VERSION = 65;
|
|
301
312
|
var FILE_MAX_CHARS = 5000000;
|
|
302
313
|
var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
303
314
|
var IMAGE_MAX_EDGE = 2576;
|
|
@@ -6384,6 +6395,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6384
6395
|
t: literal("create-worktree"),
|
|
6385
6396
|
clientId: string2().max(64).optional(),
|
|
6386
6397
|
repoId: id,
|
|
6398
|
+
worktreeId: id.optional(),
|
|
6387
6399
|
prompt,
|
|
6388
6400
|
variant: variantSchema.optional(),
|
|
6389
6401
|
context: ambient.optional(),
|
|
@@ -6432,6 +6444,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6432
6444
|
object({ t: literal("run-after-land"), repoId: id }),
|
|
6433
6445
|
object({ t: literal("restart-daemon") }),
|
|
6434
6446
|
object({ t: literal("update-now") }),
|
|
6447
|
+
object({ t: literal("check-update") }),
|
|
6435
6448
|
object({ t: literal("pull-main"), worktreeId: id }),
|
|
6436
6449
|
object({
|
|
6437
6450
|
t: literal("write-file"),
|
|
@@ -6447,6 +6460,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6447
6460
|
object({ t: literal("find-element"), worktreeId: id, seq, element: elementTraitsSchema }),
|
|
6448
6461
|
object({ t: literal("design-scan"), worktreeId: id }),
|
|
6449
6462
|
object({ t: literal("discard-file"), worktreeId: id, path: relPath }),
|
|
6463
|
+
object({ t: literal("judge"), worktreeId: id }),
|
|
6450
6464
|
object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
|
|
6451
6465
|
object({ t: literal("stop-agent"), worktreeId: id }),
|
|
6452
6466
|
object({ t: literal("seen"), worktreeId: id }),
|
|
@@ -6548,6 +6562,9 @@ function parseClientMsg(raw) {
|
|
|
6548
6562
|
return { ok: false, reason: issueReason(r.error, "invalid message") };
|
|
6549
6563
|
}
|
|
6550
6564
|
|
|
6565
|
+
// ../shared/src/restart.ts
|
|
6566
|
+
var RESTART_NOW = "now";
|
|
6567
|
+
|
|
6551
6568
|
// ../shared/src/routes.ts
|
|
6552
6569
|
var BASE = "http://preview.invalid";
|
|
6553
6570
|
var TITLE_MAX = 200;
|
|
@@ -7722,7 +7739,7 @@ function slug(s) {
|
|
|
7722
7739
|
// ../daemon/package.json
|
|
7723
7740
|
var package_default = {
|
|
7724
7741
|
name: "@toyon/daemon",
|
|
7725
|
-
version: "0.
|
|
7742
|
+
version: "0.5.0",
|
|
7726
7743
|
private: true,
|
|
7727
7744
|
type: "module",
|
|
7728
7745
|
main: "src/index.ts",
|
|
@@ -11840,11 +11857,18 @@ function fmtExtra(extra) {
|
|
|
11840
11857
|
return ` ${String(extra)}`;
|
|
11841
11858
|
}
|
|
11842
11859
|
}
|
|
11860
|
+
var lastDay = "";
|
|
11843
11861
|
function write(level, tag, message, extra) {
|
|
11844
11862
|
if (LEVELS[level] < threshold)
|
|
11845
11863
|
return;
|
|
11846
|
-
const
|
|
11847
|
-
|
|
11864
|
+
const iso = new Date().toISOString();
|
|
11865
|
+
const day = iso.slice(0, 10);
|
|
11866
|
+
if (day !== lastDay) {
|
|
11867
|
+
lastDay = day;
|
|
11868
|
+
process.stderr.write(`-------- ${day} --------
|
|
11869
|
+
`);
|
|
11870
|
+
}
|
|
11871
|
+
process.stderr.write(`${iso.slice(11, 19)} ${level.padEnd(5)} [${tag}] ${message}${fmtExtra(extra)}
|
|
11848
11872
|
`);
|
|
11849
11873
|
}
|
|
11850
11874
|
var log = {
|
|
@@ -12001,6 +12025,27 @@ async function within(p, ms) {
|
|
|
12001
12025
|
clearTimeout(timer);
|
|
12002
12026
|
return done;
|
|
12003
12027
|
}
|
|
12028
|
+
var GONE_POLL_MS = 100;
|
|
12029
|
+
function groupGone(pgid) {
|
|
12030
|
+
const deadline = Date.now() + KILL_GRACE_MS + EXIT_GRACE_MS + 500;
|
|
12031
|
+
return new Promise((resolve) => {
|
|
12032
|
+
const poll = () => {
|
|
12033
|
+
try {
|
|
12034
|
+
process.kill(-pgid, 0);
|
|
12035
|
+
} catch (e) {
|
|
12036
|
+
if (e.code === "ESRCH")
|
|
12037
|
+
return resolve();
|
|
12038
|
+
}
|
|
12039
|
+
if (Date.now() >= deadline)
|
|
12040
|
+
return resolve();
|
|
12041
|
+
setTimeout(poll, GONE_POLL_MS).unref?.();
|
|
12042
|
+
};
|
|
12043
|
+
poll();
|
|
12044
|
+
});
|
|
12045
|
+
}
|
|
12046
|
+
function reclaimGroup(pgid) {
|
|
12047
|
+
return killGroup(pgid, groupGone(pgid));
|
|
12048
|
+
}
|
|
12004
12049
|
|
|
12005
12050
|
// ../daemon/src/agent/acp/transport.ts
|
|
12006
12051
|
function spawnAcp(app, launch, cwd, tag) {
|
|
@@ -12035,7 +12080,7 @@ function spawnAcp(app, launch, cwd, tag) {
|
|
|
12035
12080
|
exitInfo = `agent process exited (${why})${tail ? `: ${tail}` : ""}`;
|
|
12036
12081
|
conn.close(new Error(exitInfo));
|
|
12037
12082
|
});
|
|
12038
|
-
return { conn, kill: () => killProcessGroup(child), exited, exitInfo: () => exitInfo };
|
|
12083
|
+
return { conn, pid: child.pid ?? null, kill: () => killProcessGroup(child), exited, exitInfo: () => exitInfo };
|
|
12039
12084
|
}
|
|
12040
12085
|
|
|
12041
12086
|
// ../daemon/src/agent/attachments.ts
|
|
@@ -12366,6 +12411,7 @@ var NO_PROMPT = {
|
|
|
12366
12411
|
SSH_ASKPASS: "",
|
|
12367
12412
|
GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
|
|
12368
12413
|
};
|
|
12414
|
+
var SPAWN_ENV = { GIT_OPTIONAL_LOCKS: "0" };
|
|
12369
12415
|
async function run(cmd, args, cwd, env) {
|
|
12370
12416
|
const started = Date.now();
|
|
12371
12417
|
try {
|
|
@@ -12374,7 +12420,7 @@ async function run(cmd, args, cwd, env) {
|
|
|
12374
12420
|
stdout: "pipe",
|
|
12375
12421
|
stderr: "pipe",
|
|
12376
12422
|
stdin: "ignore",
|
|
12377
|
-
env: { ...process.env, ...env }
|
|
12423
|
+
env: { ...process.env, ...SPAWN_ENV, ...env }
|
|
12378
12424
|
});
|
|
12379
12425
|
const [rawOut, err, status] = await Promise.all([
|
|
12380
12426
|
new Response(p.stdout).text(),
|
|
@@ -12402,7 +12448,7 @@ async function runLive(cmd, args, cwd, opts = {}) {
|
|
|
12402
12448
|
stdout: "pipe",
|
|
12403
12449
|
stderr: "pipe",
|
|
12404
12450
|
stdin: "ignore",
|
|
12405
|
-
env: { ...process.env, ...opts.env }
|
|
12451
|
+
env: { ...process.env, ...SPAWN_ENV, ...opts.env }
|
|
12406
12452
|
});
|
|
12407
12453
|
const stop = () => p.kill();
|
|
12408
12454
|
opts.signal?.addEventListener("abort", stop, { once: true });
|
|
@@ -13220,6 +13266,7 @@ var SYSTEM_APPEND = [
|
|
|
13220
13266
|
"Never run `git push`, delete branches, or create pull requests; shipping is handled by the Toyon UI.",
|
|
13221
13267
|
"Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
|
|
13222
13268
|
"Keep the scope tight: do the asked task well, then stop. Suggest follow-ups in chat instead of expanding scope.",
|
|
13269
|
+
"When referencing a file in this worktree in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor. A file outside the worktree has no page in Toyon: name its path in backticks instead of linking it.",
|
|
13223
13270
|
"`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
|
|
13224
13271
|
`Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed. Both files may carry comments and trailing commas; leave any you find.`,
|
|
13225
13272
|
"Toyon runs those commands itself in every worktree, and the user watches the result live in a preview beside this chat, so never start a dev server or open a browser of your own to check your work. Each message says where this worktree's preview answers; fetch a page from there when you want to see it.",
|
|
@@ -13251,7 +13298,7 @@ function previewContext(p) {
|
|
|
13251
13298
|
line = `The preview is running${at}, and the user sees it live beside this chat.`;
|
|
13252
13299
|
break;
|
|
13253
13300
|
case "asleep":
|
|
13254
|
-
line = `The preview is asleep; it answers${at} once the user opens this worktree again.`;
|
|
13301
|
+
line = `The preview is asleep${p.detail ? ` (${p.detail})` : ""}; it answers${at} once the user opens this worktree again.`;
|
|
13255
13302
|
break;
|
|
13256
13303
|
default:
|
|
13257
13304
|
line = `The preview is ${p.status}${p.detail ? `: ${p.detail}` : ""}; nothing answers${at} until it is back.`;
|
|
@@ -13642,11 +13689,14 @@ function asRecord(v) {
|
|
|
13642
13689
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
13643
13690
|
}
|
|
13644
13691
|
var NETWORK_ASK = "SandboxNetworkAccess";
|
|
13692
|
+
function isNetworkAsk(update) {
|
|
13693
|
+
return update.name === NETWORK_ASK || asRecord(asRecord(update._meta).claudeCode).toolName === NETWORK_ASK;
|
|
13694
|
+
}
|
|
13645
13695
|
function heading(update) {
|
|
13646
|
-
const name = update.name
|
|
13696
|
+
const name = typeof update.name === "string" ? update.name : "";
|
|
13647
13697
|
const host = asRecord(update.rawInput).host;
|
|
13648
|
-
if (
|
|
13649
|
-
return { name: "
|
|
13698
|
+
if (isNetworkAsk(update) && typeof host === "string")
|
|
13699
|
+
return { name: "", title: host, kind: "fetch" };
|
|
13650
13700
|
return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
|
|
13651
13701
|
}
|
|
13652
13702
|
function endOfAsk(req, res, memos) {
|
|
@@ -13664,40 +13714,59 @@ function endOfAsk(req, res, memos) {
|
|
|
13664
13714
|
isError: !allowed
|
|
13665
13715
|
};
|
|
13666
13716
|
}
|
|
13717
|
+
function abandoned(memos, parent) {
|
|
13718
|
+
const out = [];
|
|
13719
|
+
for (const [toolId, memo] of memos) {
|
|
13720
|
+
if (memo.ended || !memo.writing || memo.parent !== parent)
|
|
13721
|
+
continue;
|
|
13722
|
+
memo.ended = true;
|
|
13723
|
+
out.push({ type: "tool-end", toolId });
|
|
13724
|
+
}
|
|
13725
|
+
return out;
|
|
13726
|
+
}
|
|
13667
13727
|
function mapUpdate(update, memos, tag) {
|
|
13668
13728
|
switch (update.sessionUpdate) {
|
|
13669
13729
|
case "agent_message_chunk": {
|
|
13730
|
+
const { parentToolId } = spawnOf(update._meta);
|
|
13731
|
+
const out = abandoned(memos, parentToolId);
|
|
13670
13732
|
const text = textOf(update.content, tag, "message");
|
|
13671
13733
|
if (text === null)
|
|
13672
|
-
return
|
|
13673
|
-
|
|
13674
|
-
return
|
|
13734
|
+
return out;
|
|
13735
|
+
out.push(parentToolId ? { type: "tool-delta", toolId: parentToolId, text } : { type: "text-delta", text });
|
|
13736
|
+
return out;
|
|
13675
13737
|
}
|
|
13676
13738
|
case "agent_thought_chunk": {
|
|
13677
|
-
|
|
13678
|
-
|
|
13739
|
+
const { parentToolId } = spawnOf(update._meta);
|
|
13740
|
+
const out = abandoned(memos, parentToolId);
|
|
13741
|
+
if (parentToolId)
|
|
13742
|
+
return out;
|
|
13679
13743
|
const text = textOf(update.content, tag, "thought");
|
|
13680
|
-
|
|
13744
|
+
if (text !== null)
|
|
13745
|
+
out.push({ type: "thinking-delta", text });
|
|
13746
|
+
return out;
|
|
13681
13747
|
}
|
|
13682
13748
|
case "tool_call": {
|
|
13749
|
+
const spawn = spawnOf(update._meta);
|
|
13750
|
+
const out = abandoned(memos, spawn.parentToolId);
|
|
13751
|
+
const input = update.rawInput ?? { locations: update.locations ?? [] };
|
|
13683
13752
|
const memo = {
|
|
13684
13753
|
...heading(update),
|
|
13685
13754
|
content: update.content ?? [],
|
|
13686
13755
|
rawOutput: update.rawOutput,
|
|
13687
|
-
ended: false
|
|
13756
|
+
ended: false,
|
|
13757
|
+
writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
|
|
13758
|
+
...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
|
|
13688
13759
|
};
|
|
13689
13760
|
memos.set(update.toolCallId, memo);
|
|
13690
|
-
|
|
13691
|
-
|
|
13692
|
-
|
|
13693
|
-
|
|
13694
|
-
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
}
|
|
13700
|
-
];
|
|
13761
|
+
out.push({
|
|
13762
|
+
type: "tool-start",
|
|
13763
|
+
toolId: update.toolCallId,
|
|
13764
|
+
name: memo.name,
|
|
13765
|
+
input,
|
|
13766
|
+
...memo.kind ? { kind: memo.kind } : {},
|
|
13767
|
+
title: memo.title,
|
|
13768
|
+
...spawn
|
|
13769
|
+
});
|
|
13701
13770
|
if (update.status === "completed" || update.status === "failed")
|
|
13702
13771
|
out.push(endOf(update.toolCallId, memo, update.status));
|
|
13703
13772
|
return out;
|
|
@@ -13706,37 +13775,45 @@ function mapUpdate(update, memos, tag) {
|
|
|
13706
13775
|
let memo = memos.get(update.toolCallId);
|
|
13707
13776
|
const out = [];
|
|
13708
13777
|
if (!memo) {
|
|
13778
|
+
const spawn = spawnOf(update._meta);
|
|
13779
|
+
const input = update.rawInput ?? { locations: update.locations ?? [] };
|
|
13709
13780
|
memo = {
|
|
13710
|
-
name: update.name ??
|
|
13781
|
+
name: update.name ?? "",
|
|
13711
13782
|
title: update.title ?? update.name ?? "tool",
|
|
13712
13783
|
...update.kind ? { kind: update.kind } : {},
|
|
13713
13784
|
content: [],
|
|
13714
|
-
ended: false
|
|
13785
|
+
ended: false,
|
|
13786
|
+
writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
|
|
13787
|
+
...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
|
|
13715
13788
|
};
|
|
13716
13789
|
memos.set(update.toolCallId, memo);
|
|
13717
13790
|
out.push({
|
|
13718
13791
|
type: "tool-start",
|
|
13719
13792
|
toolId: update.toolCallId,
|
|
13720
13793
|
name: memo.name,
|
|
13721
|
-
input
|
|
13794
|
+
input,
|
|
13722
13795
|
...memo.kind ? { kind: memo.kind } : {},
|
|
13723
13796
|
title: memo.title,
|
|
13724
|
-
...
|
|
13797
|
+
...spawn
|
|
13725
13798
|
});
|
|
13726
13799
|
}
|
|
13800
|
+
if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
|
|
13801
|
+
return out;
|
|
13727
13802
|
const refined = { type: "tool-update", toolId: update.toolCallId };
|
|
13728
13803
|
if (update.title && update.title !== memo.title) {
|
|
13729
13804
|
memo.title = update.title;
|
|
13730
13805
|
refined.title = update.title;
|
|
13731
|
-
if (!update.name && memo.name !== update.title)
|
|
13732
|
-
refined.name = memo.name = update.title;
|
|
13733
13806
|
}
|
|
13734
|
-
if (update.name && update.name !== memo.name)
|
|
13807
|
+
if (update.name && update.name !== memo.name) {
|
|
13735
13808
|
refined.name = memo.name = update.name;
|
|
13809
|
+
}
|
|
13736
13810
|
if (update.kind && update.kind !== memo.kind)
|
|
13737
13811
|
refined.kind = memo.kind = update.kind;
|
|
13738
|
-
if (update.rawInput !== undefined)
|
|
13812
|
+
if (update.rawInput !== undefined) {
|
|
13739
13813
|
refined.input = update.rawInput;
|
|
13814
|
+
if (!emptyInput(update.rawInput))
|
|
13815
|
+
memo.writing = false;
|
|
13816
|
+
}
|
|
13740
13817
|
if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
|
|
13741
13818
|
out.push(refined);
|
|
13742
13819
|
if (update.content)
|
|
@@ -13850,6 +13927,7 @@ function steerOutcome(value) {
|
|
|
13850
13927
|
|
|
13851
13928
|
// ../daemon/src/agent/acp/session.ts
|
|
13852
13929
|
var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
|
|
13930
|
+
var OWN_SETTLE_MS = 1e4;
|
|
13853
13931
|
function recorded(reply) {
|
|
13854
13932
|
if (reply?.kind === "choice")
|
|
13855
13933
|
return { choiceId: reply.choiceId };
|
|
@@ -13868,8 +13946,8 @@ class AcpSession {
|
|
|
13868
13946
|
conn = null;
|
|
13869
13947
|
connecting = null;
|
|
13870
13948
|
live = null;
|
|
13871
|
-
loading = false;
|
|
13872
13949
|
reaper = null;
|
|
13950
|
+
own = null;
|
|
13873
13951
|
asking = 0;
|
|
13874
13952
|
asks = new Map;
|
|
13875
13953
|
log;
|
|
@@ -13897,13 +13975,38 @@ class AcpSession {
|
|
|
13897
13975
|
async warmCommands() {
|
|
13898
13976
|
if (this.commandList.length > 0 || this.stopped)
|
|
13899
13977
|
return;
|
|
13978
|
+
await this.warm();
|
|
13979
|
+
}
|
|
13980
|
+
async warm() {
|
|
13981
|
+
if (this.stopped)
|
|
13982
|
+
return;
|
|
13900
13983
|
try {
|
|
13901
|
-
await this.ensureLive();
|
|
13984
|
+
await this.ensureLive("warm-up");
|
|
13902
13985
|
this.maybeArmReaper();
|
|
13903
13986
|
} catch (e) {
|
|
13904
|
-
log.debug(this.d.worktreeId, `
|
|
13987
|
+
log.debug(this.d.worktreeId, `agent warm-up skipped: ${this.describe(e)}`);
|
|
13905
13988
|
}
|
|
13906
13989
|
}
|
|
13990
|
+
get runningAgent() {
|
|
13991
|
+
return this.conn?.spec.id ?? null;
|
|
13992
|
+
}
|
|
13993
|
+
pgid_ = null;
|
|
13994
|
+
get pgid() {
|
|
13995
|
+
return this.pgid_;
|
|
13996
|
+
}
|
|
13997
|
+
noteProcess(link, up) {
|
|
13998
|
+
if (link.pid === null)
|
|
13999
|
+
return;
|
|
14000
|
+
if (up)
|
|
14001
|
+
this.pgid_ = link.pid;
|
|
14002
|
+
else if (this.pgid_ === link.pid)
|
|
14003
|
+
this.pgid_ = null;
|
|
14004
|
+
this.d.onProcess?.(link.pid, up);
|
|
14005
|
+
}
|
|
14006
|
+
async restart() {
|
|
14007
|
+
this.clearReaper();
|
|
14008
|
+
await this.dropConn("restart");
|
|
14009
|
+
}
|
|
13907
14010
|
setCommands(next) {
|
|
13908
14011
|
if (sameCommands(this.commandList, next))
|
|
13909
14012
|
return;
|
|
@@ -13953,7 +14056,7 @@ class AcpSession {
|
|
|
13953
14056
|
return log.warn(this.d.worktreeId, "send after close dropped");
|
|
13954
14057
|
const { context, attachments } = opts;
|
|
13955
14058
|
const item = { text, context, ...attachments?.length ? { attachments } : {} };
|
|
13956
|
-
if (this.running && !this.interrupted && this.live?.conn.steering) {
|
|
14059
|
+
if ((this.running || this.own) && !this.interrupted && this.live?.conn.steering) {
|
|
13957
14060
|
return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
|
|
13958
14061
|
}
|
|
13959
14062
|
this.enqueue(item);
|
|
@@ -13967,7 +14070,7 @@ class AcpSession {
|
|
|
13967
14070
|
async steer(item) {
|
|
13968
14071
|
item.recorded ??= await this.record(item);
|
|
13969
14072
|
const live = this.live;
|
|
13970
|
-
if (!live || !this.running || this.interrupted)
|
|
14073
|
+
if (!live || !(this.running || this.own) || this.interrupted)
|
|
13971
14074
|
return this.enqueue(item);
|
|
13972
14075
|
this.steered.push(item);
|
|
13973
14076
|
let outcome;
|
|
@@ -14005,8 +14108,14 @@ class AcpSession {
|
|
|
14005
14108
|
}
|
|
14006
14109
|
stop() {
|
|
14007
14110
|
this.cancelAsks();
|
|
14008
|
-
if (!this.running)
|
|
14111
|
+
if (!this.running) {
|
|
14112
|
+
if (this.own && this.live) {
|
|
14113
|
+
const { conn, sessionId } = this.live;
|
|
14114
|
+
fireAndForget(this.d.worktreeId, conn.ctx.notify(methods.agent.session.cancel, { sessionId }), "cancel");
|
|
14115
|
+
this.endOwn("interrupted");
|
|
14116
|
+
}
|
|
14009
14117
|
return;
|
|
14118
|
+
}
|
|
14010
14119
|
this.interrupted = true;
|
|
14011
14120
|
if (this.steered.length > 0) {
|
|
14012
14121
|
this.queue.unshift(...this.steered);
|
|
@@ -14025,10 +14134,11 @@ class AcpSession {
|
|
|
14025
14134
|
this.steered = [];
|
|
14026
14135
|
this.queueChanged();
|
|
14027
14136
|
this.stop();
|
|
14028
|
-
await this.dropConn();
|
|
14137
|
+
await this.dropConn("closing");
|
|
14029
14138
|
await this.log.flush();
|
|
14030
14139
|
}
|
|
14031
14140
|
async drain() {
|
|
14141
|
+
this.endOwn("end_turn");
|
|
14032
14142
|
this.running = true;
|
|
14033
14143
|
this.clearReaper();
|
|
14034
14144
|
this.setStatus("working");
|
|
@@ -14066,7 +14176,7 @@ class AcpSession {
|
|
|
14066
14176
|
} else {
|
|
14067
14177
|
this.emit({ type: "agent-error", message: this.describe(e), ts: Date.now() });
|
|
14068
14178
|
this.setStatus("error");
|
|
14069
|
-
fireAndForget(this.d.worktreeId, this.dropConn(), "drop agent after error");
|
|
14179
|
+
fireAndForget(this.d.worktreeId, this.dropConn("error"), "drop agent after error");
|
|
14070
14180
|
}
|
|
14071
14181
|
} finally {
|
|
14072
14182
|
this.interrupted = false;
|
|
@@ -14081,7 +14191,7 @@ class AcpSession {
|
|
|
14081
14191
|
this.asking++;
|
|
14082
14192
|
this.clearReaper();
|
|
14083
14193
|
try {
|
|
14084
|
-
const conn = await this.ensureConn();
|
|
14194
|
+
const conn = await this.ensureConn("an ask");
|
|
14085
14195
|
return await askOnce(conn.ctx, {
|
|
14086
14196
|
cwd: this.d.cwd,
|
|
14087
14197
|
system,
|
|
@@ -14107,7 +14217,7 @@ class AcpSession {
|
|
|
14107
14217
|
}
|
|
14108
14218
|
}
|
|
14109
14219
|
async authenticate(methodId, apiKey) {
|
|
14110
|
-
const conn = await this.ensureConn();
|
|
14220
|
+
const conn = await this.ensureConn("login");
|
|
14111
14221
|
const method = conn.authMethods.find((m) => m.id === methodId);
|
|
14112
14222
|
if (!method)
|
|
14113
14223
|
throw new UserError(`${conn.spec.name} offers no login method "${methodId}"`);
|
|
@@ -14183,7 +14293,7 @@ class AcpSession {
|
|
|
14183
14293
|
async runTurn(item) {
|
|
14184
14294
|
item.recorded ??= await this.record(item);
|
|
14185
14295
|
this.emit({ type: "turn-start", ts: Date.now() });
|
|
14186
|
-
const live = await this.ensureLive();
|
|
14296
|
+
const live = await this.ensureLive("a turn");
|
|
14187
14297
|
await this.applyMode(live);
|
|
14188
14298
|
await this.applyOption(live, "model");
|
|
14189
14299
|
await this.applyOption(live, "thought_level");
|
|
@@ -14203,17 +14313,17 @@ class AcpSession {
|
|
|
14203
14313
|
contextFor(item) {
|
|
14204
14314
|
return ambientBlock([...item.context ?? [], this.d.preview?.()]);
|
|
14205
14315
|
}
|
|
14206
|
-
ensureConn() {
|
|
14316
|
+
ensureConn(why) {
|
|
14207
14317
|
if (this.conn)
|
|
14208
14318
|
return Promise.resolve(this.conn);
|
|
14209
14319
|
if (this.connecting)
|
|
14210
14320
|
return this.connecting;
|
|
14211
|
-
this.connecting = this.openConn().finally(() => {
|
|
14321
|
+
this.connecting = this.openConn(why).finally(() => {
|
|
14212
14322
|
this.connecting = null;
|
|
14213
14323
|
});
|
|
14214
14324
|
return this.connecting;
|
|
14215
14325
|
}
|
|
14216
|
-
async openConn() {
|
|
14326
|
+
async openConn(why) {
|
|
14217
14327
|
const spec = this.d.spec();
|
|
14218
14328
|
const prepared = await (this.d.prepare ?? prepareLaunch)(this.d.cwd, spec);
|
|
14219
14329
|
const { bounds } = prepared;
|
|
@@ -14225,13 +14335,17 @@ class AcpSession {
|
|
|
14225
14335
|
this.d.onAuth?.(spec.id, { status: c.params });
|
|
14226
14336
|
});
|
|
14227
14337
|
const link = this.d.connect(app, spec, prepared);
|
|
14338
|
+
this.noteProcess(link, true);
|
|
14339
|
+
log.info(this.d.worktreeId, `agent ${spec.id} started for ${why} (pid ${link.pid ?? "unknown"})`);
|
|
14228
14340
|
const ctx = link.conn.agent;
|
|
14229
14341
|
link.exited.then(() => {
|
|
14342
|
+
this.noteProcess(link, false);
|
|
14230
14343
|
if (this.conn?.link === link) {
|
|
14231
14344
|
log.warn(this.d.worktreeId, "agent process exited while idle");
|
|
14232
14345
|
this.conn = null;
|
|
14233
14346
|
this.live = null;
|
|
14234
14347
|
this.cancelAsks();
|
|
14348
|
+
this.endOwn("interrupted");
|
|
14235
14349
|
}
|
|
14236
14350
|
});
|
|
14237
14351
|
try {
|
|
@@ -14250,7 +14364,7 @@ class AcpSession {
|
|
|
14250
14364
|
spec,
|
|
14251
14365
|
bounds,
|
|
14252
14366
|
authMethods: init.authMethods ?? [],
|
|
14253
|
-
|
|
14367
|
+
resumeSession: !!init.agentCapabilities?.sessionCapabilities?.resume,
|
|
14254
14368
|
closeSupported: !!init.agentCapabilities?.sessionCapabilities?.close,
|
|
14255
14369
|
deleteSupported: !!init.agentCapabilities?.sessionCapabilities?.delete,
|
|
14256
14370
|
acceptsImages: init.agentCapabilities?.promptCapabilities?.image === true,
|
|
@@ -14262,11 +14376,12 @@ class AcpSession {
|
|
|
14262
14376
|
return this.conn;
|
|
14263
14377
|
} catch (e) {
|
|
14264
14378
|
fireAndForget(this.d.worktreeId, link.kill(), "kill agent after failed start");
|
|
14379
|
+
this.noteProcess(link, false);
|
|
14265
14380
|
throw e;
|
|
14266
14381
|
}
|
|
14267
14382
|
}
|
|
14268
|
-
async ensureLive() {
|
|
14269
|
-
const conn = await this.ensureConn();
|
|
14383
|
+
async ensureLive(why) {
|
|
14384
|
+
const conn = await this.ensureConn(why);
|
|
14270
14385
|
if (this.live?.conn === conn)
|
|
14271
14386
|
return this.live;
|
|
14272
14387
|
const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
|
|
@@ -14274,10 +14389,9 @@ class AcpSession {
|
|
|
14274
14389
|
let modes;
|
|
14275
14390
|
let configOptions;
|
|
14276
14391
|
let resumed = false;
|
|
14277
|
-
if (sessionId && conn.
|
|
14278
|
-
this.loading = true;
|
|
14392
|
+
if (sessionId && conn.resumeSession) {
|
|
14279
14393
|
try {
|
|
14280
|
-
const r = await conn.ctx.request(methods.agent.session.
|
|
14394
|
+
const r = await conn.ctx.request(methods.agent.session.resume, {
|
|
14281
14395
|
sessionId,
|
|
14282
14396
|
cwd: this.d.cwd,
|
|
14283
14397
|
mcpServers: [],
|
|
@@ -14290,8 +14404,6 @@ class AcpSession {
|
|
|
14290
14404
|
if (isAuthRequired(e))
|
|
14291
14405
|
throw e;
|
|
14292
14406
|
log.warn(this.d.worktreeId, `could not resume agent session ${sessionId}; starting a new one`, e);
|
|
14293
|
-
} finally {
|
|
14294
|
-
this.loading = false;
|
|
14295
14407
|
}
|
|
14296
14408
|
}
|
|
14297
14409
|
if (!resumed) {
|
|
@@ -14388,8 +14500,6 @@ class AcpSession {
|
|
|
14388
14500
|
this.setCommands(mapped);
|
|
14389
14501
|
return;
|
|
14390
14502
|
}
|
|
14391
|
-
if (this.loading)
|
|
14392
|
-
return;
|
|
14393
14503
|
const listener = side.get(params.sessionId);
|
|
14394
14504
|
if (listener) {
|
|
14395
14505
|
const u = params.update;
|
|
@@ -14407,7 +14517,10 @@ class AcpSession {
|
|
|
14407
14517
|
}
|
|
14408
14518
|
if (params.update.sessionUpdate === "config_option_update")
|
|
14409
14519
|
this.absorb(live, params.update.configOptions);
|
|
14410
|
-
|
|
14520
|
+
const events = mapUpdate(params.update, live.tools, this.d.worktreeId);
|
|
14521
|
+
if (!this.running)
|
|
14522
|
+
this.trackOwn(events);
|
|
14523
|
+
for (const ev of events) {
|
|
14411
14524
|
if (ev.type === "text-delta" || ev.type === "tool-start")
|
|
14412
14525
|
this.steered = [];
|
|
14413
14526
|
this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
|
|
@@ -14469,7 +14582,7 @@ class AcpSession {
|
|
|
14469
14582
|
});
|
|
14470
14583
|
}
|
|
14471
14584
|
async sayPlanEdited(proposed, path) {
|
|
14472
|
-
if (!await this.d.planEdited?.(proposed))
|
|
14585
|
+
if (!await this.d.planEdited?.(path, proposed))
|
|
14473
14586
|
return;
|
|
14474
14587
|
this.send(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
|
|
14475
14588
|
}
|
|
@@ -14530,21 +14643,56 @@ class AcpSession {
|
|
|
14530
14643
|
syncStatus() {
|
|
14531
14644
|
if (this.status === "error")
|
|
14532
14645
|
return;
|
|
14533
|
-
this.setStatus(this.asks.size > 0 ? "waiting" : this.running ? "working" : "idle");
|
|
14646
|
+
this.setStatus(this.asks.size > 0 ? "waiting" : this.running || this.own ? "working" : "idle");
|
|
14647
|
+
}
|
|
14648
|
+
trackOwn(events) {
|
|
14649
|
+
if (!this.own) {
|
|
14650
|
+
const doing = events.some((e) => e.type === "text-delta" || e.type === "thinking-delta" || e.type === "tool-start");
|
|
14651
|
+
if (!doing)
|
|
14652
|
+
return;
|
|
14653
|
+
this.own = { open: new Set, settle: null };
|
|
14654
|
+
this.clearReaper();
|
|
14655
|
+
this.emit({ type: "turn-start", ts: Date.now() });
|
|
14656
|
+
this.syncStatus();
|
|
14657
|
+
}
|
|
14658
|
+
for (const e of events) {
|
|
14659
|
+
if (e.type === "tool-start")
|
|
14660
|
+
this.own.open.add(e.toolId);
|
|
14661
|
+
else if (e.type === "tool-end")
|
|
14662
|
+
this.own.open.delete(e.toolId);
|
|
14663
|
+
}
|
|
14664
|
+
if (this.own.settle)
|
|
14665
|
+
clearTimeout(this.own.settle);
|
|
14666
|
+
this.own.settle = null;
|
|
14667
|
+
if (this.own.open.size > 0)
|
|
14668
|
+
return;
|
|
14669
|
+
const t = setTimeout(() => this.endOwn("end_turn"), this.d.ownSettleMs ?? OWN_SETTLE_MS);
|
|
14670
|
+
t.unref?.();
|
|
14671
|
+
this.own.settle = t;
|
|
14672
|
+
}
|
|
14673
|
+
endOwn(stopReason) {
|
|
14674
|
+
if (!this.own)
|
|
14675
|
+
return;
|
|
14676
|
+
if (this.own.settle)
|
|
14677
|
+
clearTimeout(this.own.settle);
|
|
14678
|
+
this.own = null;
|
|
14679
|
+
this.emit({ type: "turn-end", stopReason, ts: Date.now() });
|
|
14680
|
+
this.syncStatus();
|
|
14681
|
+
this.maybeArmReaper();
|
|
14534
14682
|
}
|
|
14535
14683
|
maybeArmReaper() {
|
|
14536
|
-
if (!this.running && !this.asking && this.asks.size === 0 && this.conn && !this.stopped)
|
|
14684
|
+
if (!this.running && !this.own && !this.asking && this.asks.size === 0 && this.conn && !this.stopped)
|
|
14537
14685
|
this.armReaper();
|
|
14538
14686
|
}
|
|
14539
14687
|
armReaper() {
|
|
14540
14688
|
this.clearReaper();
|
|
14689
|
+
const idleMs = this.d.idleMs ?? DEFAULT_IDLE_MS;
|
|
14541
14690
|
const t = setTimeout(() => {
|
|
14542
14691
|
this.reaper = null;
|
|
14543
|
-
if (this.running || this.asking || this.asks.size > 0 || !this.conn)
|
|
14692
|
+
if (this.running || this.own || this.asking || this.asks.size > 0 || !this.conn)
|
|
14544
14693
|
return;
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
}, this.d.idleMs ?? DEFAULT_IDLE_MS);
|
|
14694
|
+
fireAndForget(this.d.worktreeId, this.dropConn(`idle for ${Math.round(idleMs / 60000)} min`), "reap agent");
|
|
14695
|
+
}, idleMs);
|
|
14548
14696
|
t.unref?.();
|
|
14549
14697
|
this.reaper = t;
|
|
14550
14698
|
}
|
|
@@ -14553,15 +14701,17 @@ class AcpSession {
|
|
|
14553
14701
|
clearTimeout(this.reaper);
|
|
14554
14702
|
this.reaper = null;
|
|
14555
14703
|
}
|
|
14556
|
-
async dropConn() {
|
|
14704
|
+
async dropConn(why) {
|
|
14557
14705
|
this.cancelAsks();
|
|
14558
14706
|
const conn = this.conn;
|
|
14559
14707
|
this.conn = null;
|
|
14560
14708
|
this.live = null;
|
|
14561
14709
|
if (!conn)
|
|
14562
14710
|
return;
|
|
14711
|
+
log.info(this.d.worktreeId, `agent ${conn.spec.id} stopped: ${why} (pid ${conn.link.pid ?? "unknown"})`);
|
|
14563
14712
|
conn.link.conn.close();
|
|
14564
14713
|
await conn.link.kill();
|
|
14714
|
+
this.noteProcess(conn.link, false);
|
|
14565
14715
|
}
|
|
14566
14716
|
}
|
|
14567
14717
|
function isAuthRequired(e) {
|
|
@@ -14625,34 +14775,216 @@ function sameCommands(a, b) {
|
|
|
14625
14775
|
}
|
|
14626
14776
|
|
|
14627
14777
|
// ../daemon/src/agent/planDoc.ts
|
|
14628
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
14778
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
14629
14779
|
import { join as join7 } from "path";
|
|
14630
|
-
var
|
|
14780
|
+
var PLANS_DIR = join7(".toyon", "plans");
|
|
14781
|
+
var PLAN_NAME = /^(\d+)\.md$/;
|
|
14782
|
+
function isPlanPath(rel) {
|
|
14783
|
+
return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
|
|
14784
|
+
}
|
|
14785
|
+
function nextNumber(dir) {
|
|
14786
|
+
if (!existsSync8(dir))
|
|
14787
|
+
return 1;
|
|
14788
|
+
let max = 0;
|
|
14789
|
+
for (const name of readdirSync2(dir)) {
|
|
14790
|
+
const n = Number(PLAN_NAME.exec(name)?.[1]);
|
|
14791
|
+
if (n > max)
|
|
14792
|
+
max = n;
|
|
14793
|
+
}
|
|
14794
|
+
return max + 1;
|
|
14795
|
+
}
|
|
14631
14796
|
async function writePlanDoc(cwd, markdown) {
|
|
14797
|
+
const dir = join7(cwd, PLANS_DIR);
|
|
14798
|
+
const rel = join7(PLANS_DIR, `${nextNumber(dir)}.md`);
|
|
14632
14799
|
try {
|
|
14633
|
-
mkdirSync5(
|
|
14634
|
-
writeFileSync4(join7(cwd,
|
|
14800
|
+
mkdirSync5(dir, { recursive: true });
|
|
14801
|
+
writeFileSync4(join7(cwd, rel), markdown.endsWith(`
|
|
14635
14802
|
`) ? markdown : `${markdown}
|
|
14636
14803
|
`);
|
|
14637
14804
|
} catch (e) {
|
|
14638
|
-
log.warn(cwd, `could not write ${
|
|
14805
|
+
log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
|
|
14639
14806
|
return null;
|
|
14640
14807
|
}
|
|
14641
|
-
await excludeFromGit(cwd,
|
|
14642
|
-
return
|
|
14808
|
+
await excludeFromGit(cwd, `${PLANS_DIR}/`);
|
|
14809
|
+
return rel;
|
|
14643
14810
|
}
|
|
14644
|
-
function planEdited(cwd, proposed) {
|
|
14645
|
-
const file = join7(cwd,
|
|
14811
|
+
function planEdited(cwd, rel, proposed) {
|
|
14812
|
+
const file = join7(cwd, rel);
|
|
14646
14813
|
if (!existsSync8(file))
|
|
14647
14814
|
return false;
|
|
14648
14815
|
try {
|
|
14649
14816
|
return readFileSync7(file, "utf8").trim() !== proposed.trim();
|
|
14650
14817
|
} catch (e) {
|
|
14651
|
-
log.warn(cwd, `could not read ${
|
|
14818
|
+
log.warn(cwd, `could not read ${rel}`, e);
|
|
14652
14819
|
return false;
|
|
14653
14820
|
}
|
|
14654
14821
|
}
|
|
14655
14822
|
|
|
14823
|
+
// ../daemon/src/runtime/memory.ts
|
|
14824
|
+
var MAC_PRESSURE_CRITICAL = 4;
|
|
14825
|
+
var AVAILABLE_FLOOR = 0.15;
|
|
14826
|
+
var PAGING_FLOOR_BPS = 16 * 2 ** 20;
|
|
14827
|
+
var paging = [];
|
|
14828
|
+
var unsupportedLogged = false;
|
|
14829
|
+
async function memoryTight() {
|
|
14830
|
+
const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
|
|
14831
|
+
if (signal === null && !unsupportedLogged) {
|
|
14832
|
+
unsupportedLogged = true;
|
|
14833
|
+
log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
|
|
14834
|
+
}
|
|
14835
|
+
return signal;
|
|
14836
|
+
}
|
|
14837
|
+
async function darwin() {
|
|
14838
|
+
const [r, v] = await Promise.all([
|
|
14839
|
+
run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/"),
|
|
14840
|
+
run("vm_stat", [], "/")
|
|
14841
|
+
]);
|
|
14842
|
+
if (!r.ok)
|
|
14843
|
+
return null;
|
|
14844
|
+
const [level, available] = r.out.split(/\s+/).map(Number);
|
|
14845
|
+
if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
|
|
14846
|
+
return null;
|
|
14847
|
+
const sample = v.ok ? parseVmStat(v.out, Date.now()) : null;
|
|
14848
|
+
if (sample)
|
|
14849
|
+
paging = [...paging, sample].slice(-3);
|
|
14850
|
+
return darwinSignal(level, available, sustainedPaging(paging));
|
|
14851
|
+
}
|
|
14852
|
+
function parseVmStat(text, at) {
|
|
14853
|
+
const pageSize = text.match(/page size of (\d+) bytes/);
|
|
14854
|
+
const swapouts = text.match(/^Swapouts:\s+(\d+)/m);
|
|
14855
|
+
if (!pageSize || !swapouts)
|
|
14856
|
+
return null;
|
|
14857
|
+
return { at, bytes: Number(swapouts[1]) * Number(pageSize[1]) };
|
|
14858
|
+
}
|
|
14859
|
+
function sustainedPaging(samples) {
|
|
14860
|
+
if (samples.length < 3)
|
|
14861
|
+
return null;
|
|
14862
|
+
const rates = [];
|
|
14863
|
+
for (let i = samples.length - 2;i < samples.length; i++) {
|
|
14864
|
+
const prev = samples[i - 1];
|
|
14865
|
+
const next = samples[i];
|
|
14866
|
+
const seconds = (next.at - prev.at) / 1000;
|
|
14867
|
+
const bytes = next.bytes - prev.bytes;
|
|
14868
|
+
if (seconds <= 0 || bytes < 0)
|
|
14869
|
+
return null;
|
|
14870
|
+
rates.push(bytes / seconds);
|
|
14871
|
+
}
|
|
14872
|
+
return Math.min(...rates);
|
|
14873
|
+
}
|
|
14874
|
+
function darwinSignal(level, available, pagingBps = null) {
|
|
14875
|
+
const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
|
|
14876
|
+
const swapping = pagingBps === null ? "" : `, swapping out ${Math.round(pagingBps / 2 ** 20)} MB/s`;
|
|
14877
|
+
return {
|
|
14878
|
+
tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100 || pagingBps !== null && pagingBps >= PAGING_FLOOR_BPS,
|
|
14879
|
+
why: `memory pressure ${pressure} with ${available}% available${swapping}`
|
|
14880
|
+
};
|
|
14881
|
+
}
|
|
14882
|
+
async function linux() {
|
|
14883
|
+
let text;
|
|
14884
|
+
try {
|
|
14885
|
+
text = await Bun.file("/proc/meminfo").text();
|
|
14886
|
+
} catch {
|
|
14887
|
+
return null;
|
|
14888
|
+
}
|
|
14889
|
+
const field = (name) => {
|
|
14890
|
+
const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
|
|
14891
|
+
return m ? Number(m[1]) : null;
|
|
14892
|
+
};
|
|
14893
|
+
const total = field("MemTotal");
|
|
14894
|
+
const available = field("MemAvailable");
|
|
14895
|
+
if (!total || available === null)
|
|
14896
|
+
return null;
|
|
14897
|
+
const percent = Math.round(available / total * 100);
|
|
14898
|
+
return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
|
|
14899
|
+
}
|
|
14900
|
+
async function sampleCosts(pgids) {
|
|
14901
|
+
const costs = new Map;
|
|
14902
|
+
if (pgids.length === 0)
|
|
14903
|
+
return costs;
|
|
14904
|
+
const wanted = new Set(pgids);
|
|
14905
|
+
const r = await run("ps", ["-axo", "pgid=,rss="], "/");
|
|
14906
|
+
if (!r.ok)
|
|
14907
|
+
return costs;
|
|
14908
|
+
for (const line of r.out.split(`
|
|
14909
|
+
`)) {
|
|
14910
|
+
const [pgid, rss] = line.trim().split(/\s+/).map(Number);
|
|
14911
|
+
if (pgid === undefined || rss === undefined || !wanted.has(pgid))
|
|
14912
|
+
continue;
|
|
14913
|
+
costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
|
|
14914
|
+
}
|
|
14915
|
+
return costs;
|
|
14916
|
+
}
|
|
14917
|
+
async function bootId() {
|
|
14918
|
+
if (process.platform === "darwin") {
|
|
14919
|
+
const r = await run("sysctl", ["-n", "kern.boottime"], "/");
|
|
14920
|
+
const m = r.ok ? r.out.match(/sec = (\d+)/) : null;
|
|
14921
|
+
return m ? m[1] : null;
|
|
14922
|
+
}
|
|
14923
|
+
if (process.platform === "linux") {
|
|
14924
|
+
try {
|
|
14925
|
+
const m = (await Bun.file("/proc/stat").text()).match(/^btime (\d+)/m);
|
|
14926
|
+
return m ? m[1] : null;
|
|
14927
|
+
} catch {
|
|
14928
|
+
return null;
|
|
14929
|
+
}
|
|
14930
|
+
}
|
|
14931
|
+
return null;
|
|
14932
|
+
}
|
|
14933
|
+
async function psGroups(now = Date.now()) {
|
|
14934
|
+
const r = await run("ps", ["-axo", "pid=,pgid=,ppid=,etime=,command="], "/");
|
|
14935
|
+
if (!r.ok)
|
|
14936
|
+
return [];
|
|
14937
|
+
const rows = [];
|
|
14938
|
+
for (const line of r.out.split(`
|
|
14939
|
+
`)) {
|
|
14940
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
|
|
14941
|
+
if (!m)
|
|
14942
|
+
continue;
|
|
14943
|
+
const elapsed = parseEtime(m[4]);
|
|
14944
|
+
if (elapsed === null)
|
|
14945
|
+
continue;
|
|
14946
|
+
rows.push({
|
|
14947
|
+
pid: Number(m[1]),
|
|
14948
|
+
pgid: Number(m[2]),
|
|
14949
|
+
ppid: Number(m[3]),
|
|
14950
|
+
startedAt: now - elapsed * 1000,
|
|
14951
|
+
command: m[5] ?? ""
|
|
14952
|
+
});
|
|
14953
|
+
}
|
|
14954
|
+
return rows;
|
|
14955
|
+
}
|
|
14956
|
+
function parseEtime(text) {
|
|
14957
|
+
const m = text.match(/^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/);
|
|
14958
|
+
if (!m)
|
|
14959
|
+
return null;
|
|
14960
|
+
const [, days, hours, minutes, seconds] = m;
|
|
14961
|
+
return Number(days ?? 0) * 86400 + Number(hours ?? 0) * 3600 + Number(minutes) * 60 + Number(seconds);
|
|
14962
|
+
}
|
|
14963
|
+
|
|
14964
|
+
// ../daemon/src/runtime/orphans.ts
|
|
14965
|
+
var GROUP_TTL_MS = 24 * 60 * 60000;
|
|
14966
|
+
var LEADER_SLACK_MS = 1e4;
|
|
14967
|
+
function orphansIn(ledger, rows, bootId, ledgerBootId, now) {
|
|
14968
|
+
if (bootId === null || ledgerBootId === undefined || bootId !== ledgerBootId)
|
|
14969
|
+
return [];
|
|
14970
|
+
const out = [];
|
|
14971
|
+
for (const entry of ledger) {
|
|
14972
|
+
if (now - entry.startedAt > GROUP_TTL_MS)
|
|
14973
|
+
continue;
|
|
14974
|
+
const members = rows.filter((r) => r.pgid === entry.pgid);
|
|
14975
|
+
if (members.length === 0)
|
|
14976
|
+
continue;
|
|
14977
|
+
const leader = members.find((r) => r.pid === entry.pgid);
|
|
14978
|
+
if (leader && Math.abs(leader.startedAt - entry.startedAt) > LEADER_SLACK_MS)
|
|
14979
|
+
continue;
|
|
14980
|
+
out.push({ worktreeId: entry.worktreeId, pgid: entry.pgid, name: entry.name });
|
|
14981
|
+
}
|
|
14982
|
+
return out;
|
|
14983
|
+
}
|
|
14984
|
+
function strayAdapters(rows, agentsDir) {
|
|
14985
|
+
return rows.filter((r) => r.pid === r.pgid && r.ppid === 1 && r.command.includes(agentsDir));
|
|
14986
|
+
}
|
|
14987
|
+
|
|
14656
14988
|
// ../daemon/src/runtime/ports.ts
|
|
14657
14989
|
var allocated = new Set;
|
|
14658
14990
|
var range = cloud.proxyPorts;
|
|
@@ -15441,7 +15773,7 @@ class WorktreeProcs {
|
|
|
15441
15773
|
async killProc(mp) {
|
|
15442
15774
|
await mp.pty?.kill();
|
|
15443
15775
|
}
|
|
15444
|
-
async sleep() {
|
|
15776
|
+
async sleep(why) {
|
|
15445
15777
|
if (this.asleep || this.stopped)
|
|
15446
15778
|
return;
|
|
15447
15779
|
this.asleep = true;
|
|
@@ -15449,6 +15781,7 @@ class WorktreeProcs {
|
|
|
15449
15781
|
for (const mp of this.procs.values()) {
|
|
15450
15782
|
mp.state.status = "asleep";
|
|
15451
15783
|
mp.state.pid = undefined;
|
|
15784
|
+
mp.state.detail = why;
|
|
15452
15785
|
this.onProc({ ...mp.state });
|
|
15453
15786
|
exits.push(this.killProc(mp));
|
|
15454
15787
|
}
|
|
@@ -15463,13 +15796,16 @@ class WorktreeProcs {
|
|
|
15463
15796
|
this.spawnProc(mp);
|
|
15464
15797
|
}
|
|
15465
15798
|
}
|
|
15466
|
-
|
|
15799
|
+
groups() {
|
|
15467
15800
|
const out = [];
|
|
15468
|
-
for (const mp of this.procs
|
|
15801
|
+
for (const [name, mp] of this.procs)
|
|
15469
15802
|
if (mp.pty?.alive)
|
|
15470
|
-
out.push(mp.pty.pid);
|
|
15803
|
+
out.push({ name, pgid: mp.pty.pid });
|
|
15471
15804
|
return out;
|
|
15472
15805
|
}
|
|
15806
|
+
pgids() {
|
|
15807
|
+
return this.groups().map((g) => g.pgid);
|
|
15808
|
+
}
|
|
15473
15809
|
async stopAll() {
|
|
15474
15810
|
this.stopped = true;
|
|
15475
15811
|
const exits = [];
|
|
@@ -15538,11 +15874,12 @@ function terminalEnv(base, own, urls) {
|
|
|
15538
15874
|
var DEFAULT_AGENT_ID = "claude";
|
|
15539
15875
|
var PREVIEW_WAKE_MS = 8000;
|
|
15540
15876
|
var PREVIEW_POLL_MS = 100;
|
|
15541
|
-
function defaultAgent(wt, d, preview) {
|
|
15877
|
+
function defaultAgent(wt, d, preview, onProcess) {
|
|
15542
15878
|
const agent = new AcpSession({
|
|
15543
15879
|
worktreeId: wt.id,
|
|
15544
15880
|
cwd: wt.path,
|
|
15545
15881
|
preview: () => previewContext(preview()),
|
|
15882
|
+
onProcess,
|
|
15546
15883
|
spec: () => {
|
|
15547
15884
|
const w = d.state.requireWorktree(wt.id);
|
|
15548
15885
|
if (!w.agent) {
|
|
@@ -15578,9 +15915,16 @@ function defaultAgent(wt, d, preview) {
|
|
|
15578
15915
|
w.planned = true;
|
|
15579
15916
|
d.state.save();
|
|
15580
15917
|
}
|
|
15581
|
-
|
|
15918
|
+
const path = markdown.trim() ? await writePlanDoc(wt.path, markdown) : null;
|
|
15919
|
+
const fresh = path ? d.state.worktree(wt.id) : undefined;
|
|
15920
|
+
if (fresh && path && fresh.plan !== path) {
|
|
15921
|
+
fresh.plan = path;
|
|
15922
|
+
d.state.save();
|
|
15923
|
+
d.hub.emit("worktreesChanged");
|
|
15924
|
+
}
|
|
15925
|
+
return path;
|
|
15582
15926
|
},
|
|
15583
|
-
planEdited: async (proposed) => planEdited(wt.path, proposed),
|
|
15927
|
+
planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
|
|
15584
15928
|
option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
|
|
15585
15929
|
onOptionsLearned: (category, choices) => {
|
|
15586
15930
|
const agentId = d.state.requireWorktree(wt.id).agent ?? "";
|
|
@@ -15630,8 +15974,10 @@ class RuntimeRegistry {
|
|
|
15630
15974
|
holds = new Map;
|
|
15631
15975
|
settingUp = new Set;
|
|
15632
15976
|
starting = new Set;
|
|
15977
|
+
stopping = new Map;
|
|
15633
15978
|
constructor(deps) {
|
|
15634
15979
|
this.deps = deps;
|
|
15980
|
+
deps.hub.on("proc", (id) => this.recordGroups(id));
|
|
15635
15981
|
}
|
|
15636
15982
|
hold(id, tag) {
|
|
15637
15983
|
let tags = this.holds.get(id);
|
|
@@ -15700,16 +16046,18 @@ class RuntimeRegistry {
|
|
|
15700
16046
|
if (repo)
|
|
15701
16047
|
await this.start(wt, repo);
|
|
15702
16048
|
}
|
|
15703
|
-
async sleep(id) {
|
|
16049
|
+
async sleep(id, why) {
|
|
15704
16050
|
const rt = this.runtimes.get(id);
|
|
15705
16051
|
if (!rt?.procs || rt.procs.asleep)
|
|
15706
16052
|
return;
|
|
16053
|
+
log.info(id, `asleep: ${why}`);
|
|
15707
16054
|
if (this.ports.ranged()) {
|
|
15708
16055
|
rt.proxy?.stop();
|
|
15709
16056
|
rt.proxy = null;
|
|
15710
16057
|
this.returnLease(id);
|
|
15711
16058
|
}
|
|
15712
|
-
await rt.procs.sleep();
|
|
16059
|
+
await rt.procs.sleep(why);
|
|
16060
|
+
this.recordGroups(id);
|
|
15713
16061
|
this.deps.hub.emit("worktreesChanged");
|
|
15714
16062
|
}
|
|
15715
16063
|
get ports() {
|
|
@@ -15766,11 +16114,15 @@ class RuntimeRegistry {
|
|
|
15766
16114
|
args: ["-l"]
|
|
15767
16115
|
};
|
|
15768
16116
|
try {
|
|
15769
|
-
term = (this.deps.makeTerminal ?? defaultTerminal)({ id }, opts, (data) => this.deps.hub.emit("termData", id, SHELL_STREAM, data), (code) =>
|
|
16117
|
+
term = (this.deps.makeTerminal ?? defaultTerminal)({ id }, opts, (data) => this.deps.hub.emit("termData", id, SHELL_STREAM, data), (code) => {
|
|
16118
|
+
this.deps.hub.emit("termExit", id, SHELL_STREAM, code);
|
|
16119
|
+
this.recordGroups(id);
|
|
16120
|
+
});
|
|
15770
16121
|
} catch (e) {
|
|
15771
16122
|
throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
|
|
15772
16123
|
}
|
|
15773
16124
|
this.looseShells.set(id, term);
|
|
16125
|
+
this.recordGroups(id);
|
|
15774
16126
|
} else if (term.cols !== cols || term.rows !== rows) {
|
|
15775
16127
|
term.resize(cols, rows);
|
|
15776
16128
|
}
|
|
@@ -15785,6 +16137,7 @@ class RuntimeRegistry {
|
|
|
15785
16137
|
continue;
|
|
15786
16138
|
this.looseShells.delete(id);
|
|
15787
16139
|
fireAndForget(id, Promise.resolve(term.kill()), "loose shell cleanup");
|
|
16140
|
+
this.recordGroups(id);
|
|
15788
16141
|
}
|
|
15789
16142
|
}
|
|
15790
16143
|
get(id) {
|
|
@@ -15806,7 +16159,7 @@ class RuntimeRegistry {
|
|
|
15806
16159
|
return existing;
|
|
15807
16160
|
const rt = {
|
|
15808
16161
|
info: wt,
|
|
15809
|
-
agent: (this.deps.makeAgent ?? defaultAgent)(wt, this.deps, () => this.previewStanding(wt.id)),
|
|
16162
|
+
agent: (this.deps.makeAgent ?? defaultAgent)(wt, this.deps, () => this.previewStanding(wt.id), () => this.recordGroups(wt.id)),
|
|
15810
16163
|
procs: null,
|
|
15811
16164
|
proxy: null,
|
|
15812
16165
|
previewName: undefined,
|
|
@@ -15822,6 +16175,8 @@ class RuntimeRegistry {
|
|
|
15822
16175
|
const rt = this.ensureAgent(wt);
|
|
15823
16176
|
if (rt.procs)
|
|
15824
16177
|
return;
|
|
16178
|
+
if (wt.kind === "main" && this.deps.mainLeads && !this.deps.mainLeads(wt.repoId))
|
|
16179
|
+
return;
|
|
15825
16180
|
const live = this.deps.state.requireWorktree(wt.id);
|
|
15826
16181
|
const port = this.leasePort(live);
|
|
15827
16182
|
if (port === null)
|
|
@@ -15872,12 +16227,11 @@ class RuntimeRegistry {
|
|
|
15872
16227
|
leased = new Map;
|
|
15873
16228
|
leasePort(wt) {
|
|
15874
16229
|
let port = this.ports.lease(wt.proxyPort);
|
|
15875
|
-
if (port === null && wt.kind !== "spare") {
|
|
16230
|
+
if (port === null && (wt.kind !== "spare" || this.deps.shown?.(wt.id))) {
|
|
15876
16231
|
const holders = [...this.leased.keys()].filter((id) => id !== wt.id && !this.starting.has(id) && !this.busy(id) && !this.deps.viewed?.(id));
|
|
15877
16232
|
const victim = oldestViewed(holders, this.deps.state);
|
|
15878
16233
|
if (victim) {
|
|
15879
|
-
|
|
15880
|
-
fireAndForget(victim.id, this.sleep(victim.id), "sleep for a preview port");
|
|
16234
|
+
fireAndForget(victim.id, this.sleep(victim.id, `its preview port went to ${wt.title || wt.branch}`), "sleep for a preview port");
|
|
15881
16235
|
port = this.ports.lease(wt.proxyPort);
|
|
15882
16236
|
}
|
|
15883
16237
|
if (port === null) {
|
|
@@ -15905,6 +16259,7 @@ class RuntimeRegistry {
|
|
|
15905
16259
|
proxy?.stop();
|
|
15906
16260
|
this.returnLease(id);
|
|
15907
16261
|
await procs?.stopAll();
|
|
16262
|
+
this.recordGroups(id);
|
|
15908
16263
|
}
|
|
15909
16264
|
async stop(id) {
|
|
15910
16265
|
this.holds.delete(id);
|
|
@@ -15913,11 +16268,18 @@ class RuntimeRegistry {
|
|
|
15913
16268
|
if (!rt)
|
|
15914
16269
|
return;
|
|
15915
16270
|
this.runtimes.delete(id);
|
|
16271
|
+
this.stopping.set(id, rt);
|
|
15916
16272
|
rt.proxy?.stop();
|
|
15917
16273
|
this.returnLease(id);
|
|
15918
16274
|
const login = rt.login;
|
|
15919
16275
|
rt.login = null;
|
|
15920
|
-
|
|
16276
|
+
try {
|
|
16277
|
+
await Promise.all([rt.agent.close(), rt.procs?.stopAll(), rt.shell?.kill(), login?.pty.kill()]);
|
|
16278
|
+
} finally {
|
|
16279
|
+
if (this.stopping.get(id) === rt)
|
|
16280
|
+
this.stopping.delete(id);
|
|
16281
|
+
this.recordGroups(id);
|
|
16282
|
+
}
|
|
15921
16283
|
}
|
|
15922
16284
|
openTerminal(id, stream, cols, rows) {
|
|
15923
16285
|
if (stream === LOGIN_STREAM)
|
|
@@ -15925,12 +16287,10 @@ class RuntimeRegistry {
|
|
|
15925
16287
|
if (stream !== SHELL_STREAM)
|
|
15926
16288
|
return this.openProcStream(id, stream, cols, rows);
|
|
15927
16289
|
const wt = this.deps.state.requireWorktree(id);
|
|
15928
|
-
if (wt.kind === "spare")
|
|
15929
|
-
throw new UserError("no terminal for a spare worktree");
|
|
15930
16290
|
const rt = this.ensureAgent(wt);
|
|
15931
16291
|
let term = rt.shell;
|
|
15932
16292
|
if (!term?.alive) {
|
|
15933
|
-
const cwd = wt.
|
|
16293
|
+
const cwd = wt.path;
|
|
15934
16294
|
const opts = {
|
|
15935
16295
|
cwd,
|
|
15936
16296
|
env: { ...this.shellEnv(wt), PWD: cwd },
|
|
@@ -15940,11 +16300,15 @@ class RuntimeRegistry {
|
|
|
15940
16300
|
args: ["-l"]
|
|
15941
16301
|
};
|
|
15942
16302
|
try {
|
|
15943
|
-
term = (this.deps.makeTerminal ?? defaultTerminal)(wt, opts, (data) => this.deps.hub.emit("termData", wt.id, SHELL_STREAM, data), (code) =>
|
|
16303
|
+
term = (this.deps.makeTerminal ?? defaultTerminal)(wt, opts, (data) => this.deps.hub.emit("termData", wt.id, SHELL_STREAM, data), (code) => {
|
|
16304
|
+
this.deps.hub.emit("termExit", wt.id, SHELL_STREAM, code);
|
|
16305
|
+
this.recordGroups(wt.id);
|
|
16306
|
+
});
|
|
15944
16307
|
} catch (e) {
|
|
15945
16308
|
throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
|
|
15946
16309
|
}
|
|
15947
16310
|
rt.shell = term;
|
|
16311
|
+
this.recordGroups(wt.id);
|
|
15948
16312
|
} else if (term.cols !== cols || term.rows !== rows) {
|
|
15949
16313
|
term.resize(cols, rows);
|
|
15950
16314
|
}
|
|
@@ -15957,7 +16321,7 @@ class RuntimeRegistry {
|
|
|
15957
16321
|
rt.login = null;
|
|
15958
16322
|
if (prev)
|
|
15959
16323
|
fireAndForget(id, Promise.resolve(prev.pty.kill()), "replace the agent login");
|
|
15960
|
-
const cwd = wt.
|
|
16324
|
+
const cwd = wt.path;
|
|
15961
16325
|
let pty;
|
|
15962
16326
|
try {
|
|
15963
16327
|
pty = (this.deps.makeTerminal ?? defaultTerminal)(wt, {
|
|
@@ -15969,6 +16333,7 @@ class RuntimeRegistry {
|
|
|
15969
16333
|
args: run.args
|
|
15970
16334
|
}, (data) => this.deps.hub.emit("termData", id, LOGIN_STREAM, data), (code) => {
|
|
15971
16335
|
this.deps.hub.emit("termExit", id, LOGIN_STREAM, code);
|
|
16336
|
+
this.recordGroups(id);
|
|
15972
16337
|
if (rt.login?.pty !== pty)
|
|
15973
16338
|
return;
|
|
15974
16339
|
if (code === 0) {
|
|
@@ -15981,6 +16346,7 @@ class RuntimeRegistry {
|
|
|
15981
16346
|
throw new UserError(`could not start the login: ${e instanceof Error ? e.message : String(e)}`);
|
|
15982
16347
|
}
|
|
15983
16348
|
rt.login = { pty, run };
|
|
16349
|
+
this.recordGroups(id);
|
|
15984
16350
|
this.deps.hub.emit("worktreesChanged");
|
|
15985
16351
|
}
|
|
15986
16352
|
openLoginStream(id, cols, rows) {
|
|
@@ -16056,12 +16422,93 @@ class RuntimeRegistry {
|
|
|
16056
16422
|
const url = `http://${host.includes(":") ? `[${host}]` : host}:${st.boundPort ?? st.port}`;
|
|
16057
16423
|
return { status: st.status, url, ...st.detail ? { detail: st.detail } : {} };
|
|
16058
16424
|
}
|
|
16425
|
+
recordGroups(id) {
|
|
16426
|
+
const rt = this.runtimes.get(id) ?? this.stopping.get(id);
|
|
16427
|
+
const live = [];
|
|
16428
|
+
if (rt) {
|
|
16429
|
+
for (const g of rt.procs?.groups() ?? [])
|
|
16430
|
+
live.push(g);
|
|
16431
|
+
if (rt.agent.pgid !== null) {
|
|
16432
|
+
const agentId = this.deps.state.worktree(id)?.agent ?? this.deps.state.defaultAgent ?? DEFAULT_AGENT_ID;
|
|
16433
|
+
live.push({ name: `agent:${agentId}`, pgid: rt.agent.pgid });
|
|
16434
|
+
}
|
|
16435
|
+
if (rt.shell?.alive)
|
|
16436
|
+
live.push({ name: "shell", pgid: rt.shell.pid });
|
|
16437
|
+
if (rt.login?.pty.alive)
|
|
16438
|
+
live.push({ name: "login", pgid: rt.login.pty.pid });
|
|
16439
|
+
}
|
|
16440
|
+
const loose = this.looseShells.get(id);
|
|
16441
|
+
if (loose?.alive)
|
|
16442
|
+
live.push({ name: "shell", pgid: loose.pid });
|
|
16443
|
+
const before = new Map(this.deps.state.groups(id).map((g) => [g.pgid, g]));
|
|
16444
|
+
const now = Date.now();
|
|
16445
|
+
const next = live.map((g) => ({ ...g, startedAt: before.get(g.pgid)?.startedAt ?? now }));
|
|
16446
|
+
this.deps.state.setGroups(id, next);
|
|
16447
|
+
}
|
|
16448
|
+
async reclaimOrphans() {
|
|
16449
|
+
const machine = this.deps.machine ?? { bootId, psGroups };
|
|
16450
|
+
const [boot, rows] = await Promise.all([machine.bootId(), machine.psGroups()]);
|
|
16451
|
+
const state = this.deps.state;
|
|
16452
|
+
const found = orphansIn(state.allGroups(), rows, boot, state.bootAt, Date.now());
|
|
16453
|
+
const seen = new Set(found.map((o) => o.pgid));
|
|
16454
|
+
for (const row of strayAdapters(rows, this.deps.paths.agentsDir)) {
|
|
16455
|
+
if (seen.has(row.pgid))
|
|
16456
|
+
continue;
|
|
16457
|
+
seen.add(row.pgid);
|
|
16458
|
+
found.push({ worktreeId: "daemon", pgid: row.pgid, name: "adapter" });
|
|
16459
|
+
}
|
|
16460
|
+
const reclaim = this.deps.reclaim ?? reclaimGroup;
|
|
16461
|
+
await Promise.all(found.map(async (o) => {
|
|
16462
|
+
await reclaim(o.pgid);
|
|
16463
|
+
log.info(o.worktreeId, `reclaimed ${o.name} group ${o.pgid} left by the last daemon`);
|
|
16464
|
+
}));
|
|
16465
|
+
state.clearGroups();
|
|
16466
|
+
state.setBootAt(boot);
|
|
16467
|
+
}
|
|
16059
16468
|
async shutdown() {
|
|
16060
16469
|
this.pruneLooseShells(new Set);
|
|
16061
16470
|
await Promise.all([...this.runtimes.keys()].map((id) => this.stop(id)));
|
|
16062
16471
|
}
|
|
16063
16472
|
}
|
|
16064
16473
|
|
|
16474
|
+
// ../daemon/src/worktrees/naming.ts
|
|
16475
|
+
import { randomBytes } from "crypto";
|
|
16476
|
+
import { existsSync as existsSync10 } from "fs";
|
|
16477
|
+
import { join as join8 } from "path";
|
|
16478
|
+
function shortId() {
|
|
16479
|
+
return randomBytes(5).toString("hex");
|
|
16480
|
+
}
|
|
16481
|
+
function freeSlot(parent) {
|
|
16482
|
+
for (;; ) {
|
|
16483
|
+
const id = shortId();
|
|
16484
|
+
const dir = `wt-${id.slice(0, 4)}`;
|
|
16485
|
+
if (!existsSync10(join8(parent, dir)))
|
|
16486
|
+
return { id, dir };
|
|
16487
|
+
}
|
|
16488
|
+
}
|
|
16489
|
+
function titleFrom(prompt) {
|
|
16490
|
+
const words = prompt.split(/\s+/).filter(Boolean).slice(0, 3).join(" ");
|
|
16491
|
+
return sentenceCase(cleanTitle2(words) || "task");
|
|
16492
|
+
}
|
|
16493
|
+
function cleanTitle2(title) {
|
|
16494
|
+
return title.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").slice(0, 40).replace(/^[\s:;,.!?]+|[\s:;,.!?]+$/g, "");
|
|
16495
|
+
}
|
|
16496
|
+
function branchSlug(title) {
|
|
16497
|
+
return title.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
16498
|
+
}
|
|
16499
|
+
function sentenceCase(title) {
|
|
16500
|
+
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
16501
|
+
}
|
|
16502
|
+
var VARIANT_LENSES = [
|
|
16503
|
+
"Take the most direct approach: the change most people would expect, in the smallest diff.",
|
|
16504
|
+
"Lean toward how it looks and reads: polish what the task touches using only the design tokens, components and voice the project already has. Add no new colours, fonts or effects, and restyle nothing outside the task.",
|
|
16505
|
+
"Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
|
|
16506
|
+
];
|
|
16507
|
+
function variantLens(index) {
|
|
16508
|
+
const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
|
|
16509
|
+
return `(You are attempt ${index} of several parallel attempts at this task. Stay within the task and follow the project's existing styles, components, tokens and writing conventions; the attempts differ in emphasis, not in how much they change. ${lens})`;
|
|
16510
|
+
}
|
|
16511
|
+
|
|
16065
16512
|
// ../daemon/src/agent/recap.ts
|
|
16066
16513
|
var REPLY_KEEP = 2000;
|
|
16067
16514
|
var CLIP = 160;
|
|
@@ -16077,6 +16524,7 @@ function turnsSince(entries, seenAt) {
|
|
|
16077
16524
|
let pending = [];
|
|
16078
16525
|
let open = null;
|
|
16079
16526
|
const writes = new Set;
|
|
16527
|
+
const refusals = new Map;
|
|
16080
16528
|
const start = (ts) => {
|
|
16081
16529
|
const t = slice(pending, ts);
|
|
16082
16530
|
pending = [];
|
|
@@ -16097,16 +16545,35 @@ function turnsSince(entries, seenAt) {
|
|
|
16097
16545
|
open = start(e.ts);
|
|
16098
16546
|
break;
|
|
16099
16547
|
case "text-delta":
|
|
16100
|
-
if (open)
|
|
16548
|
+
if (open) {
|
|
16101
16549
|
open.reply = (open.reply + e.text).slice(-REPLY_KEEP);
|
|
16550
|
+
open.refused = undefined;
|
|
16551
|
+
}
|
|
16102
16552
|
break;
|
|
16103
16553
|
case "tool-start":
|
|
16104
16554
|
if (!open)
|
|
16105
16555
|
break;
|
|
16106
16556
|
if (!e.parentToolId)
|
|
16107
16557
|
open.reply = "";
|
|
16558
|
+
open.refused = undefined;
|
|
16108
16559
|
countWrite(open, writes, e.toolId, e);
|
|
16109
16560
|
break;
|
|
16561
|
+
case "agent-permission":
|
|
16562
|
+
refusals.set(e.id, new Set(e.choices.filter((c) => c.kind.startsWith("reject")).map((c) => c.id)));
|
|
16563
|
+
if (open)
|
|
16564
|
+
open.refused = undefined;
|
|
16565
|
+
break;
|
|
16566
|
+
case "agent-question":
|
|
16567
|
+
if (open)
|
|
16568
|
+
open.refused = undefined;
|
|
16569
|
+
break;
|
|
16570
|
+
case "agent-ask-end": {
|
|
16571
|
+
const refusing = refusals.get(e.id);
|
|
16572
|
+
refusals.delete(e.id);
|
|
16573
|
+
if (open && e.outcome === "answered" && e.choiceId && refusing?.has(e.choiceId))
|
|
16574
|
+
open.refused = true;
|
|
16575
|
+
break;
|
|
16576
|
+
}
|
|
16110
16577
|
case "tool-update":
|
|
16111
16578
|
if (open && e.kind)
|
|
16112
16579
|
countWrite(open, writes, e.toolId, { name: e.name ?? "", kind: e.kind });
|
|
@@ -16163,7 +16630,8 @@ function factsOf(turns, end, ask) {
|
|
|
16163
16630
|
...end === "failed" && last?.error ? { error: last.error } : {},
|
|
16164
16631
|
...end === "failed" && last?.auth ? { auth: true } : {},
|
|
16165
16632
|
...end === "done" && cut ? { cut } : {},
|
|
16166
|
-
...end === "asking" && ask ? { ask } : {}
|
|
16633
|
+
...end === "asking" && ask ? { ask } : {},
|
|
16634
|
+
...end === "stopped" && last?.refused ? { planBack: true } : {}
|
|
16167
16635
|
};
|
|
16168
16636
|
}
|
|
16169
16637
|
function firstAskOf(entries) {
|
|
@@ -16172,59 +16640,6 @@ function firstAskOf(entries) {
|
|
|
16172
16640
|
return e.text;
|
|
16173
16641
|
return;
|
|
16174
16642
|
}
|
|
16175
|
-
var RECAP_SYSTEM = "You write one-line status recaps for a developer coming back to a coding task. Reply with only the recap.";
|
|
16176
|
-
var RECAP_ASK = "The user stepped away and is coming back. Recap in one sentence, under 20 words, no markdown. Say what the task is, then what just happened or what to do next. Skip root-cause narrative, fix internals and secondary to-dos.";
|
|
16177
|
-
var RECAP_TURNS = 6;
|
|
16178
|
-
var RECAP_CHARS = 4000;
|
|
16179
|
-
function recapPrompt(i) {
|
|
16180
|
-
const head = [RECAP_ASK, "", `Task: ${clip(i.title, 200)}`];
|
|
16181
|
-
if (i.firstAsk)
|
|
16182
|
-
head.push(`First request: ${clip(i.firstAsk, 300)}`);
|
|
16183
|
-
const now = `Now: ${standing(i.end, i.facts)}`;
|
|
16184
|
-
let blocks = i.turns.slice(-RECAP_TURNS).map(turnBlock);
|
|
16185
|
-
const size = () => [...head, "", ...blocks, "", now].join(`
|
|
16186
|
-
|
|
16187
|
-
`).length;
|
|
16188
|
-
while (blocks.length > 1 && size() > RECAP_CHARS)
|
|
16189
|
-
blocks = blocks.slice(1);
|
|
16190
|
-
return [head.join(`
|
|
16191
|
-
`), blocks.join(`
|
|
16192
|
-
|
|
16193
|
-
`), now].join(`
|
|
16194
|
-
|
|
16195
|
-
`);
|
|
16196
|
-
}
|
|
16197
|
-
function turnBlock(t) {
|
|
16198
|
-
const facts = [t.edits ? plural(t.edits, "edit") : "", t.toolErrors ? plural(t.toolErrors, "failed tool") : ""].filter(Boolean).join(", ");
|
|
16199
|
-
const lines = [];
|
|
16200
|
-
if (t.asks.length)
|
|
16201
|
-
lines.push(`You asked: ${clip(t.asks.join(" / "), 400)}`);
|
|
16202
|
-
if (t.reply.trim())
|
|
16203
|
-
lines.push(`Agent ended with: ${tail(t.reply, 800)}`);
|
|
16204
|
-
if (facts)
|
|
16205
|
-
lines.push(`Facts: ${facts}`);
|
|
16206
|
-
return lines.join(`
|
|
16207
|
-
`);
|
|
16208
|
-
}
|
|
16209
|
-
function standing(end, f) {
|
|
16210
|
-
switch (end) {
|
|
16211
|
-
case "asking":
|
|
16212
|
-
return f.ask ? `waiting for the user's answer to "${f.ask}"` : "waiting for the user";
|
|
16213
|
-
case "failed":
|
|
16214
|
-
return f.error ? `failed: ${f.error}` : "failed";
|
|
16215
|
-
case "stopped":
|
|
16216
|
-
return "stopped before it finished";
|
|
16217
|
-
case "done":
|
|
16218
|
-
return f.cut ? `finished early (${f.cut})` : "finished";
|
|
16219
|
-
}
|
|
16220
|
-
}
|
|
16221
|
-
function plural(n, word) {
|
|
16222
|
-
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
16223
|
-
}
|
|
16224
|
-
function tail(text, max) {
|
|
16225
|
-
const line = text.replace(/\s+/g, " ").trim();
|
|
16226
|
-
return line.length > max ? `\u2026${line.slice(-(max - 1))}` : line;
|
|
16227
|
-
}
|
|
16228
16643
|
var RECAP_MAX = 160;
|
|
16229
16644
|
function parseRecap(text) {
|
|
16230
16645
|
if (!text)
|
|
@@ -16241,15 +16656,17 @@ function parseRecap(text) {
|
|
|
16241
16656
|
}
|
|
16242
16657
|
|
|
16243
16658
|
// ../daemon/src/agent/landing.ts
|
|
16244
|
-
var LAND_SYSTEM = "You judge whether a coding task is finished and write its commit message. Reply in exactly the format asked, nothing else.";
|
|
16659
|
+
var LAND_SYSTEM = "You judge whether a coding task is finished, recap where it stands, and write its commit message. Reply in exactly the format asked, nothing else.";
|
|
16245
16660
|
var LAND_ASK = [
|
|
16246
16661
|
"A coding agent just stopped in a git worktree. Decide whether the work is finished and ready to merge, or still in progress: a question the agent is waiting on, a step it said it would do next, or a part of the request it did not get to.",
|
|
16247
16662
|
"The agent never commits: every change is left uncommitted for the user, and the agent saying so is normal, not a sign the work is unfinished.",
|
|
16248
16663
|
"Reply with exactly this shape and nothing else:",
|
|
16249
|
-
"Line 1: READY, or NOT READY: <
|
|
16664
|
+
"Line 1: READY, or NOT READY: <what is still left to do, under 8 words, about the change itself and not the conversation>",
|
|
16250
16665
|
"Line 2: blank",
|
|
16251
|
-
"Line 3:
|
|
16666
|
+
"Line 3: Recap: one sentence under 20 words for the user coming back to this task, saying what the task is and then what just happened or what to do next. Skip root-cause narrative, fix internals and secondary to-dos.",
|
|
16252
16667
|
"Line 4: blank",
|
|
16668
|
+
"Line 5: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
|
|
16669
|
+
"Line 6: blank",
|
|
16253
16670
|
"Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
|
|
16254
16671
|
].join(`
|
|
16255
16672
|
`);
|
|
@@ -16265,7 +16682,7 @@ ${i.recentSubjects.map((s) => ` ${s}`).join(`
|
|
|
16265
16682
|
`)}`);
|
|
16266
16683
|
const diff = `Diff summary:
|
|
16267
16684
|
${i.diffStat.trim() || "(none)"}`;
|
|
16268
|
-
let blocks = i.turns.slice(-LAND_TURNS).map(
|
|
16685
|
+
let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
|
|
16269
16686
|
const size = () => [...head, "", ...blocks, "", diff].join(`
|
|
16270
16687
|
|
|
16271
16688
|
`).length;
|
|
@@ -16278,13 +16695,36 @@ ${i.diffStat.trim() || "(none)"}`;
|
|
|
16278
16695
|
|
|
16279
16696
|
`);
|
|
16280
16697
|
}
|
|
16281
|
-
function
|
|
16698
|
+
function turnBlock(t) {
|
|
16282
16699
|
const lines = [];
|
|
16283
16700
|
if (t.asks.length)
|
|
16284
16701
|
lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
|
|
16285
16702
|
if (t.reply.trim())
|
|
16286
16703
|
lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
|
|
16287
16704
|
return lines.join(`
|
|
16705
|
+
`);
|
|
16706
|
+
}
|
|
16707
|
+
var ANSWER_SYSTEM = "You recap where a coding task stands in one sentence. Reply with the sentence only.";
|
|
16708
|
+
var ANSWER_ASK = [
|
|
16709
|
+
"A coding agent just replied in a git worktree without changing any files: it answered a question, reviewed something, or explained what it found.",
|
|
16710
|
+
"Write one sentence under 20 words for the user coming back to this task, saying what was asked and what the agent answered or recommended. Skip narrative and secondary points. No markdown, no dashes as punctuation."
|
|
16711
|
+
].join(`
|
|
16712
|
+
`);
|
|
16713
|
+
function answerPrompt(i) {
|
|
16714
|
+
const head = [ANSWER_ASK, "", `Task: ${clip(i.title, 200)}`];
|
|
16715
|
+
if (i.firstAsk)
|
|
16716
|
+
head.push(`First request: ${clip(i.firstAsk, 300)}`);
|
|
16717
|
+
let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
|
|
16718
|
+
const size = () => [...head, "", ...blocks].join(`
|
|
16719
|
+
|
|
16720
|
+
`).length;
|
|
16721
|
+
while (blocks.length > 1 && size() > LAND_CHARS)
|
|
16722
|
+
blocks = blocks.slice(1);
|
|
16723
|
+
return [head.join(`
|
|
16724
|
+
`), blocks.join(`
|
|
16725
|
+
|
|
16726
|
+
`)].join(`
|
|
16727
|
+
|
|
16288
16728
|
`);
|
|
16289
16729
|
}
|
|
16290
16730
|
var SUBJECT_MAX = 72;
|
|
@@ -16301,13 +16741,17 @@ function parseLanding(text) {
|
|
|
16301
16741
|
const rest = lines.slice(lines.indexOf(lines.find((l) => l.trim() === first) ?? "") + 1);
|
|
16302
16742
|
const paragraphs = rest.join(`
|
|
16303
16743
|
`).split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
16304
|
-
const
|
|
16305
|
-
const
|
|
16744
|
+
const hasRecap = /^recap\s*:/i.test(paragraphs[0] ?? "") || paragraphs.length >= 3;
|
|
16745
|
+
const recap = hasRecap ? parseRecap(paragraphs[0] ?? null) : null;
|
|
16746
|
+
const message = hasRecap ? paragraphs.slice(1) : paragraphs;
|
|
16747
|
+
const subject = message[0]?.replace(/\s+/g, " ").replace(/^(subject|title)\s*:\s*/i, "").replace(/[.]+$/, "").slice(0, SUBJECT_MAX).trim();
|
|
16748
|
+
const body = message.slice(1).join(`
|
|
16306
16749
|
|
|
16307
16750
|
`).replace(/^(body)\s*:\s*/i, "").trim();
|
|
16308
16751
|
return {
|
|
16309
16752
|
ready: verdict === "ready",
|
|
16310
16753
|
...why ? { why: clip(why, 120) } : {},
|
|
16754
|
+
...recap ? { recap } : {},
|
|
16311
16755
|
...subject && subject.split(" ").length >= 2 ? { subject } : {},
|
|
16312
16756
|
...body ? { body: body.slice(0, 1500) } : {}
|
|
16313
16757
|
};
|
|
@@ -16361,7 +16805,7 @@ async function askFreshAgent(agents, agentId, cwd, system, prompt, opts = {}) {
|
|
|
16361
16805
|
|
|
16362
16806
|
// ../daemon/src/agent/tasks.ts
|
|
16363
16807
|
var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
|
|
16364
|
-
var namePrompt = (task) => `Name this coding task the way a teammate would say it out loud: 1 or 2 lowercase
|
|
16808
|
+
var namePrompt = (task) => `Name this coding task the way a teammate would say it out loud: 1 or 2 plain lowercase words, 16 characters at most (like "sticky header", "dark mode", "price badge"). Leave out words every task in the project would share, like the product or the app. Reply with ONLY the name, nothing else.
|
|
16365
16809
|
|
|
16366
16810
|
Task: ${task.slice(0, 500)}`;
|
|
16367
16811
|
function taskText(text, attachments = []) {
|
|
@@ -16383,13 +16827,13 @@ function parseName(text) {
|
|
|
16383
16827
|
return null;
|
|
16384
16828
|
const line = text.trim().split(`
|
|
16385
16829
|
`).at(-1) ?? "";
|
|
16386
|
-
const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[
|
|
16387
|
-
const words = name.split("
|
|
16830
|
+
const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
16831
|
+
const words = name.split(" ").filter(Boolean);
|
|
16388
16832
|
if (words.length < 1 || words.length > 3 || name.length < 3 || name.length > 20)
|
|
16389
16833
|
return null;
|
|
16390
16834
|
if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
|
|
16391
16835
|
return null;
|
|
16392
|
-
return name;
|
|
16836
|
+
return sentenceCase(name);
|
|
16393
16837
|
}
|
|
16394
16838
|
var PLAN_SYSTEM = "You are a task-planning assistant. Reply with only the requested JSON.";
|
|
16395
16839
|
var planPrompt = (request) => [
|
|
@@ -16419,15 +16863,6 @@ function parsePlan(text) {
|
|
|
16419
16863
|
function makeNamer(runtime) {
|
|
16420
16864
|
return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
|
|
16421
16865
|
}
|
|
16422
|
-
function makeRecapper(runtime, agents, state) {
|
|
16423
|
-
return async (wt, prompt) => {
|
|
16424
|
-
const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
|
|
16425
|
-
if (!spec || lacksQuickModel(spec, state))
|
|
16426
|
-
return null;
|
|
16427
|
-
const agent = runtime.agentFor(wt.id);
|
|
16428
|
-
return agent ? parseRecap(await agent.ask(RECAP_SYSTEM, prompt, { quick: "require" })) : null;
|
|
16429
|
-
};
|
|
16430
|
-
}
|
|
16431
16866
|
function lacksQuickModel(spec, state) {
|
|
16432
16867
|
const quick = spec.quickModel;
|
|
16433
16868
|
if (!quick)
|
|
@@ -16437,43 +16872,51 @@ function lacksQuickModel(spec, state) {
|
|
|
16437
16872
|
const offered = state.cachedOptions(spec.id, "model");
|
|
16438
16873
|
return offered.length > 0 && !offered.some((m) => m.id === quick);
|
|
16439
16874
|
}
|
|
16440
|
-
function
|
|
16441
|
-
return async (wt, prompt) => {
|
|
16875
|
+
function makeQuickAsker(runtime, agents, state) {
|
|
16876
|
+
return async (wt, system, prompt) => {
|
|
16442
16877
|
const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
|
|
16443
16878
|
if (!spec || lacksQuickModel(spec, state))
|
|
16444
16879
|
return null;
|
|
16445
16880
|
const agent = runtime.agentFor(wt.id);
|
|
16446
|
-
return agent ?
|
|
16881
|
+
return agent ? agent.ask(system, prompt, { quick: "require" }) : null;
|
|
16447
16882
|
};
|
|
16448
16883
|
}
|
|
16884
|
+
function makeLander(...deps) {
|
|
16885
|
+
const ask = makeQuickAsker(...deps);
|
|
16886
|
+
return async (wt, prompt) => parseLanding(await ask(wt, LAND_SYSTEM, prompt));
|
|
16887
|
+
}
|
|
16888
|
+
function makeAnswerRecapper(...deps) {
|
|
16889
|
+
const ask = makeQuickAsker(...deps);
|
|
16890
|
+
return async (wt, prompt) => parseRecap(await ask(wt, ANSWER_SYSTEM, prompt));
|
|
16891
|
+
}
|
|
16449
16892
|
function makePlanner(agents) {
|
|
16450
16893
|
return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
|
|
16451
16894
|
}
|
|
16452
16895
|
|
|
16453
16896
|
// ../daemon/src/core/assets.ts
|
|
16454
|
-
import { existsSync as
|
|
16455
|
-
import { join as
|
|
16897
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, rmSync, statSync as statSync2 } from "fs";
|
|
16898
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
16456
16899
|
function locateAssets(here, env = process.env) {
|
|
16457
|
-
const packaged =
|
|
16900
|
+
const packaged = existsSync11(join9(here, "shell", "index.html"));
|
|
16458
16901
|
return {
|
|
16459
|
-
shellDist: env.TOYON_SHELL_DIST ?? (packaged ?
|
|
16460
|
-
bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ?
|
|
16902
|
+
shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join9(here, "shell") : join9(here, "../../shell/dist")),
|
|
16903
|
+
bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join9(here, "bridge.js") : join9(here, "../../bridge/dist/bridge.js")),
|
|
16461
16904
|
sourceRoot: packaged ? null : resolve5(here, "../../.."),
|
|
16462
|
-
packageJson: packaged ?
|
|
16905
|
+
packageJson: packaged ? join9(here, "..", "package.json") : null
|
|
16463
16906
|
};
|
|
16464
16907
|
}
|
|
16465
16908
|
var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
|
|
16466
16909
|
function pruneAssets(shellDist) {
|
|
16467
|
-
const dir =
|
|
16910
|
+
const dir = join9(shellDist, "assets");
|
|
16468
16911
|
let names;
|
|
16469
16912
|
try {
|
|
16470
|
-
names =
|
|
16913
|
+
names = readdirSync3(dir);
|
|
16471
16914
|
} catch {
|
|
16472
16915
|
return 0;
|
|
16473
16916
|
}
|
|
16474
16917
|
const found = [];
|
|
16475
16918
|
for (const name of names) {
|
|
16476
|
-
const path =
|
|
16919
|
+
const path = join9(dir, name);
|
|
16477
16920
|
try {
|
|
16478
16921
|
found.push({ path, mtime: statSync2(path).mtimeMs });
|
|
16479
16922
|
} catch {}
|
|
@@ -16494,7 +16937,7 @@ function pruneAssets(shellDist) {
|
|
|
16494
16937
|
}
|
|
16495
16938
|
|
|
16496
16939
|
// ../daemon/src/core/dialog.ts
|
|
16497
|
-
import { existsSync as
|
|
16940
|
+
import { existsSync as existsSync12 } from "fs";
|
|
16498
16941
|
import { homedir as homedir4 } from "os";
|
|
16499
16942
|
var PROMPTS = {
|
|
16500
16943
|
location: "Choose where the new project goes",
|
|
@@ -16512,7 +16955,7 @@ function folderDialog() {
|
|
|
16512
16955
|
let cancelled = false;
|
|
16513
16956
|
const ask = async (start, purpose) => {
|
|
16514
16957
|
const typed = start === "~" || start.startsWith("~/") ? `${homedir4()}${start.slice(1)}` : start;
|
|
16515
|
-
const from = typed.startsWith("/") &&
|
|
16958
|
+
const from = typed.startsWith("/") && existsSync12(typed) ? typed : homedir4();
|
|
16516
16959
|
cancelled = false;
|
|
16517
16960
|
const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
|
|
16518
16961
|
stdout: "pipe",
|
|
@@ -16616,12 +17059,12 @@ class Restarter {
|
|
|
16616
17059
|
d.hub.on("agentStatus", settle);
|
|
16617
17060
|
d.hub.on("worktreesChanged", settle);
|
|
16618
17061
|
}
|
|
16619
|
-
request() {
|
|
17062
|
+
request(opts = {}) {
|
|
16620
17063
|
const refused = this.d.refusal();
|
|
16621
17064
|
if (refused)
|
|
16622
17065
|
return refused;
|
|
16623
17066
|
this.asked = true;
|
|
16624
|
-
this.attempt();
|
|
17067
|
+
this.attempt(opts.now);
|
|
16625
17068
|
return null;
|
|
16626
17069
|
}
|
|
16627
17070
|
waitingOn() {
|
|
@@ -16630,13 +17073,18 @@ class Restarter {
|
|
|
16630
17073
|
return this.gone ? [] : this.working();
|
|
16631
17074
|
}
|
|
16632
17075
|
working() {
|
|
16633
|
-
return this.
|
|
17076
|
+
return this.titled("working");
|
|
17077
|
+
}
|
|
17078
|
+
asking() {
|
|
17079
|
+
return this.titled("waiting");
|
|
16634
17080
|
}
|
|
16635
|
-
|
|
17081
|
+
titled(status) {
|
|
17082
|
+
return this.d.state.worktrees.filter((w) => this.d.runtime.agentFor(w.id)?.status === status).map((w) => w.title);
|
|
17083
|
+
}
|
|
17084
|
+
attempt(now = false) {
|
|
16636
17085
|
if (this.gone)
|
|
16637
17086
|
return;
|
|
16638
|
-
|
|
16639
|
-
if (names.length === 0)
|
|
17087
|
+
if (now || this.working().length === 0)
|
|
16640
17088
|
this.gone = true;
|
|
16641
17089
|
const key = JSON.stringify(this.waitingOn());
|
|
16642
17090
|
if (key !== this.announced) {
|
|
@@ -16728,14 +17176,15 @@ class SelfWatch {
|
|
|
16728
17176
|
}
|
|
16729
17177
|
|
|
16730
17178
|
// ../daemon/src/core/state.ts
|
|
16731
|
-
import { randomBytes } from "crypto";
|
|
16732
|
-
import { existsSync as
|
|
17179
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
17180
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
17181
|
+
var GROUPS_SAVE_MS = 250;
|
|
16733
17182
|
var SEEN_WORKTREES = 50;
|
|
16734
17183
|
var empty = { repos: [], worktrees: [], sessions: {} };
|
|
16735
17184
|
function loadState(paths) {
|
|
16736
17185
|
ensureDirs(paths);
|
|
16737
17186
|
const STATE_FILE = paths.stateFile;
|
|
16738
|
-
if (!
|
|
17187
|
+
if (!existsSync13(STATE_FILE))
|
|
16739
17188
|
return structuredClone(empty);
|
|
16740
17189
|
let raw;
|
|
16741
17190
|
try {
|
|
@@ -16765,6 +17214,10 @@ function loadState(paths) {
|
|
|
16765
17214
|
if (!ids.has(id) && !id.startsWith("disc-"))
|
|
16766
17215
|
delete state.seen?.[id];
|
|
16767
17216
|
}
|
|
17217
|
+
for (const id of Object.keys(state.groups ?? {})) {
|
|
17218
|
+
if (!ids.has(id) && !id.startsWith("disc-"))
|
|
17219
|
+
delete state.groups?.[id];
|
|
17220
|
+
}
|
|
16768
17221
|
if (state.modelCache) {
|
|
16769
17222
|
state.optionCache ??= {};
|
|
16770
17223
|
for (const [agentId, models] of Object.entries(state.modelCache)) {
|
|
@@ -16783,9 +17236,9 @@ function saveState(paths, state) {
|
|
|
16783
17236
|
function loadOrCreateToken(paths) {
|
|
16784
17237
|
ensureDirs(paths);
|
|
16785
17238
|
const TOKEN_FILE = paths.tokenFile;
|
|
16786
|
-
if (
|
|
17239
|
+
if (existsSync13(TOKEN_FILE))
|
|
16787
17240
|
return readFileSync9(TOKEN_FILE, "utf8").trim();
|
|
16788
|
-
const token =
|
|
17241
|
+
const token = randomBytes2(32).toString("hex");
|
|
16789
17242
|
writeFileSync5(TOKEN_FILE, token, { mode: 384 });
|
|
16790
17243
|
return token;
|
|
16791
17244
|
}
|
|
@@ -16793,6 +17246,7 @@ function loadOrCreateToken(paths) {
|
|
|
16793
17246
|
class StateStore {
|
|
16794
17247
|
paths;
|
|
16795
17248
|
state;
|
|
17249
|
+
groupsTimer = null;
|
|
16796
17250
|
constructor(paths, state) {
|
|
16797
17251
|
this.paths = paths;
|
|
16798
17252
|
this.state = state ?? loadState(paths);
|
|
@@ -16875,6 +17329,55 @@ class StateStore {
|
|
|
16875
17329
|
delete this.state.sessions[id];
|
|
16876
17330
|
if (this.state.seen)
|
|
16877
17331
|
delete this.state.seen[id];
|
|
17332
|
+
if (this.state.groups)
|
|
17333
|
+
delete this.state.groups[id];
|
|
17334
|
+
this.save();
|
|
17335
|
+
}
|
|
17336
|
+
groups(worktreeId) {
|
|
17337
|
+
return this.state.groups?.[worktreeId] ?? [];
|
|
17338
|
+
}
|
|
17339
|
+
setGroups(worktreeId, list) {
|
|
17340
|
+
this.state.groups ??= {};
|
|
17341
|
+
if (list.length === 0)
|
|
17342
|
+
delete this.state.groups[worktreeId];
|
|
17343
|
+
else
|
|
17344
|
+
this.state.groups[worktreeId] = list;
|
|
17345
|
+
if (this.groupsTimer)
|
|
17346
|
+
return;
|
|
17347
|
+
const t = setTimeout(() => {
|
|
17348
|
+
try {
|
|
17349
|
+
this.save();
|
|
17350
|
+
} catch (e) {
|
|
17351
|
+
log.warn("state", "could not write the process group ledger", e);
|
|
17352
|
+
}
|
|
17353
|
+
}, GROUPS_SAVE_MS);
|
|
17354
|
+
t.unref?.();
|
|
17355
|
+
this.groupsTimer = t;
|
|
17356
|
+
}
|
|
17357
|
+
flushGroups() {
|
|
17358
|
+
if (this.groupsTimer)
|
|
17359
|
+
this.save();
|
|
17360
|
+
}
|
|
17361
|
+
allGroups() {
|
|
17362
|
+
const out = [];
|
|
17363
|
+
for (const [worktreeId, list] of Object.entries(this.state.groups ?? {})) {
|
|
17364
|
+
for (const g of list)
|
|
17365
|
+
out.push({ ...g, worktreeId });
|
|
17366
|
+
}
|
|
17367
|
+
return out;
|
|
17368
|
+
}
|
|
17369
|
+
clearGroups() {
|
|
17370
|
+
delete this.state.groups;
|
|
17371
|
+
this.save();
|
|
17372
|
+
}
|
|
17373
|
+
get bootAt() {
|
|
17374
|
+
return this.state.bootAt;
|
|
17375
|
+
}
|
|
17376
|
+
setBootAt(id) {
|
|
17377
|
+
if (id === null)
|
|
17378
|
+
delete this.state.bootAt;
|
|
17379
|
+
else
|
|
17380
|
+
this.state.bootAt = id;
|
|
16878
17381
|
this.save();
|
|
16879
17382
|
}
|
|
16880
17383
|
session(worktreeId) {
|
|
@@ -16935,12 +17438,15 @@ class StateStore {
|
|
|
16935
17438
|
this.save();
|
|
16936
17439
|
}
|
|
16937
17440
|
save() {
|
|
17441
|
+
if (this.groupsTimer)
|
|
17442
|
+
clearTimeout(this.groupsTimer);
|
|
17443
|
+
this.groupsTimer = null;
|
|
16938
17444
|
saveState(this.paths, this.state);
|
|
16939
17445
|
}
|
|
16940
17446
|
}
|
|
16941
17447
|
|
|
16942
17448
|
// ../daemon/src/design/service.ts
|
|
16943
|
-
import { join as
|
|
17449
|
+
import { join as join10 } from "path";
|
|
16944
17450
|
|
|
16945
17451
|
// ../daemon/src/design/scan.ts
|
|
16946
17452
|
var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
|
|
@@ -17390,7 +17896,7 @@ class DesignService {
|
|
|
17390
17896
|
async function readAll(root, paths) {
|
|
17391
17897
|
const out = [];
|
|
17392
17898
|
for (const path of paths) {
|
|
17393
|
-
const file = Bun.file(
|
|
17899
|
+
const file = Bun.file(join10(root, path));
|
|
17394
17900
|
if (file.size === 0 || file.size > MAX_FILE_BYTES)
|
|
17395
17901
|
continue;
|
|
17396
17902
|
try {
|
|
@@ -17505,14 +18011,14 @@ function typicalUse(classes) {
|
|
|
17505
18011
|
var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
|
|
17506
18012
|
async function loadProjectTypescript(root) {
|
|
17507
18013
|
try {
|
|
17508
|
-
return await import(
|
|
18014
|
+
return await import(join10(root, "node_modules", "typescript", "lib", "typescript.js"));
|
|
17509
18015
|
} catch {
|
|
17510
18016
|
return null;
|
|
17511
18017
|
}
|
|
17512
18018
|
}
|
|
17513
18019
|
|
|
17514
18020
|
// ../daemon/src/drafts/store.ts
|
|
17515
|
-
import { existsSync as
|
|
18021
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
17516
18022
|
var SAVE_DELAY_MS = 1000;
|
|
17517
18023
|
|
|
17518
18024
|
class DraftStore {
|
|
@@ -17521,7 +18027,7 @@ class DraftStore {
|
|
|
17521
18027
|
saveTimer = null;
|
|
17522
18028
|
constructor(d) {
|
|
17523
18029
|
this.d = d;
|
|
17524
|
-
if (!
|
|
18030
|
+
if (!existsSync14(d.file))
|
|
17525
18031
|
return;
|
|
17526
18032
|
try {
|
|
17527
18033
|
const raw = JSON.parse(readFileSync10(d.file, "utf8"));
|
|
@@ -17541,6 +18047,9 @@ class DraftStore {
|
|
|
17541
18047
|
has(boxId) {
|
|
17542
18048
|
return !!this.drafts[boxId]?.trim();
|
|
17543
18049
|
}
|
|
18050
|
+
text(boxId) {
|
|
18051
|
+
return this.drafts[boxId] ?? "";
|
|
18052
|
+
}
|
|
17544
18053
|
set(boxId, text, clientId) {
|
|
17545
18054
|
if ((this.drafts[boxId] ?? "") === text)
|
|
17546
18055
|
return;
|
|
@@ -17608,16 +18117,20 @@ class ExecService {
|
|
|
17608
18117
|
run(worktreeId, command) {
|
|
17609
18118
|
fireAndForget(worktreeId, this.exec(worktreeId, command), `exec ${command}`);
|
|
17610
18119
|
}
|
|
17611
|
-
exec(worktreeId, command, name = SHELL_TOOL) {
|
|
18120
|
+
exec(worktreeId, command, name = SHELL_TOOL, opts = {}) {
|
|
17612
18121
|
const wt = this.deps.state.requireWorktree(worktreeId);
|
|
17613
18122
|
if (wt.kind === "spare")
|
|
17614
18123
|
throw new UserError("no shell for a spare worktree");
|
|
18124
|
+
if (wt.kind === "main")
|
|
18125
|
+
throw new UserError("nothing runs on main: the plus starts a worktree for it");
|
|
17615
18126
|
const agent = this.deps.runtime.agentFor(worktreeId);
|
|
17616
18127
|
if (!agent)
|
|
17617
18128
|
throw new UserError("worktree still starting; try again in a moment");
|
|
17618
18129
|
const toolId = `${name}-${Date.now().toString(36)}-${++this.n}`;
|
|
17619
|
-
|
|
17620
|
-
|
|
18130
|
+
const start = { type: "tool-start", toolId, name, input: { command }, kind: "execute" };
|
|
18131
|
+
if (!opts.quiet)
|
|
18132
|
+
agent.note(start);
|
|
18133
|
+
const cwd = wt.path;
|
|
17621
18134
|
const env = { ...this.deps.runtime.shellEnv(wt), PWD: cwd, TERM: "dumb", NO_COLOR: "1" };
|
|
17622
18135
|
let proc;
|
|
17623
18136
|
try {
|
|
@@ -17630,13 +18143,28 @@ class ExecService {
|
|
|
17630
18143
|
});
|
|
17631
18144
|
} catch (e) {
|
|
17632
18145
|
const reason = e instanceof Error ? e.message : String(e);
|
|
18146
|
+
if (opts.quiet)
|
|
18147
|
+
agent.note(start);
|
|
17633
18148
|
agent.note({ type: "tool-end", toolId, output: `could not run: ${reason}`, isError: true });
|
|
17634
18149
|
return Promise.resolve({ exit: reason, text: "" });
|
|
17635
18150
|
}
|
|
17636
18151
|
const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
|
|
17637
18152
|
this.track(worktreeId, toolId, { proc, timer });
|
|
17638
18153
|
this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
|
|
17639
|
-
return this.collect(worktreeId, toolId, proc, agent);
|
|
18154
|
+
return this.collect(worktreeId, toolId, proc, agent, opts.quiet ? start : undefined);
|
|
18155
|
+
}
|
|
18156
|
+
record(worktreeId, command, text, exit) {
|
|
18157
|
+
const wt = this.deps.state.requireWorktree(worktreeId);
|
|
18158
|
+
if (wt.kind === "spare")
|
|
18159
|
+
throw new UserError("no shell for a spare worktree");
|
|
18160
|
+
const agent = this.deps.runtime.agentFor(worktreeId);
|
|
18161
|
+
if (!agent)
|
|
18162
|
+
throw new UserError("worktree still starting; try again in a moment");
|
|
18163
|
+
const toolId = `${SHELL_TOOL}-${Date.now().toString(36)}-${++this.n}`;
|
|
18164
|
+
agent.note({ type: "tool-start", toolId, name: SHELL_TOOL, input: { command }, kind: "execute" });
|
|
18165
|
+
const truncated = text.length > OUTPUT_CAP;
|
|
18166
|
+
const shown = truncated ? text.slice(0, OUTPUT_CAP) : text;
|
|
18167
|
+
agent.note({ type: "tool-end", toolId, output: formatOutput(shown, exit, truncated), isError: exit !== 0 });
|
|
17640
18168
|
}
|
|
17641
18169
|
stop(worktreeId) {
|
|
17642
18170
|
for (const toolId of this.running.get(worktreeId)?.keys() ?? [])
|
|
@@ -17668,7 +18196,7 @@ class ExecService {
|
|
|
17668
18196
|
r.proc.kill("SIGKILL");
|
|
17669
18197
|
}, KILL_GRACE_MS2);
|
|
17670
18198
|
}
|
|
17671
|
-
async collect(worktreeId, toolId, proc, agent) {
|
|
18199
|
+
async collect(worktreeId, toolId, proc, agent, heldStart) {
|
|
17672
18200
|
let text = "";
|
|
17673
18201
|
let truncated = false;
|
|
17674
18202
|
const drain = async (stream) => {
|
|
@@ -17703,6 +18231,10 @@ class ExecService {
|
|
|
17703
18231
|
clearTimeout(r.timer);
|
|
17704
18232
|
this.deps.runtime.release(worktreeId, `exec:${toolId}`);
|
|
17705
18233
|
}
|
|
18234
|
+
if (heldStart && exit === 0)
|
|
18235
|
+
return { exit, text };
|
|
18236
|
+
if (heldStart)
|
|
18237
|
+
agent.note(heldStart);
|
|
17706
18238
|
agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
|
|
17707
18239
|
return { exit, text };
|
|
17708
18240
|
}
|
|
@@ -17749,7 +18281,7 @@ function withRepoLock(repoPath, fn) {
|
|
|
17749
18281
|
var fileLockKey = (absPath) => `file:${absPath}`;
|
|
17750
18282
|
|
|
17751
18283
|
// ../daemon/src/git/status.ts
|
|
17752
|
-
import { join as
|
|
18284
|
+
import { join as join11 } from "path";
|
|
17753
18285
|
async function treeFingerprint(worktreePath) {
|
|
17754
18286
|
const [head, diff, untracked] = await Promise.all([
|
|
17755
18287
|
git(worktreePath, "rev-parse", "HEAD"),
|
|
@@ -17759,7 +18291,7 @@ async function treeFingerprint(worktreePath) {
|
|
|
17759
18291
|
const others = await Promise.all(untracked.out.split(`
|
|
17760
18292
|
`).filter(Boolean).map(async (p) => {
|
|
17761
18293
|
try {
|
|
17762
|
-
const f = Bun.file(
|
|
18294
|
+
const f = Bun.file(join11(worktreePath, p));
|
|
17763
18295
|
const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
|
|
17764
18296
|
return `${p}:${body}`;
|
|
17765
18297
|
} catch {
|
|
@@ -17810,7 +18342,7 @@ async function numstat(worktreePath, ...range) {
|
|
|
17810
18342
|
}
|
|
17811
18343
|
async function untrackedLines(worktreePath, file) {
|
|
17812
18344
|
try {
|
|
17813
|
-
const f = Bun.file(
|
|
18345
|
+
const f = Bun.file(join11(worktreePath, file));
|
|
17814
18346
|
if (f.size > 2000000)
|
|
17815
18347
|
return {};
|
|
17816
18348
|
const buf = new Uint8Array(await f.arrayBuffer());
|
|
@@ -17888,6 +18420,10 @@ async function behindUpstream(worktreePath) {
|
|
|
17888
18420
|
const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
|
|
17889
18421
|
return r.ok ? Number(r.out) || 0 : null;
|
|
17890
18422
|
}
|
|
18423
|
+
async function aheadUpstream(worktreePath) {
|
|
18424
|
+
const r = await git(worktreePath, "rev-list", "--count", "@{upstream}..HEAD");
|
|
18425
|
+
return r.ok ? Number(r.out) || 0 : null;
|
|
18426
|
+
}
|
|
17891
18427
|
async function aheadBehind(worktreePath, defaultBr) {
|
|
17892
18428
|
const [a, b] = await Promise.all([
|
|
17893
18429
|
git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`),
|
|
@@ -18106,7 +18642,7 @@ function rankElementSources(el, rows) {
|
|
|
18106
18642
|
// ../daemon/src/files/listing.ts
|
|
18107
18643
|
var SUBMODULE_MODE = "160000";
|
|
18108
18644
|
var entries = (out) => out.split("\x00").filter(Boolean);
|
|
18109
|
-
function parseListing(staged, untracked, deleted) {
|
|
18645
|
+
function parseListing(staged, untracked, deleted, ignored = "") {
|
|
18110
18646
|
const gone = new Set(entries(deleted));
|
|
18111
18647
|
const paths = new Set;
|
|
18112
18648
|
const submodules = new Set;
|
|
@@ -18124,7 +18660,18 @@ function parseListing(staged, untracked, deleted) {
|
|
|
18124
18660
|
}
|
|
18125
18661
|
for (const path of entries(untracked))
|
|
18126
18662
|
paths.add(path);
|
|
18127
|
-
return { paths: [...paths], submodules: [...submodules] };
|
|
18663
|
+
return { paths: [...paths], submodules: [...submodules], ignored: collapse(entries(ignored)) };
|
|
18664
|
+
}
|
|
18665
|
+
function collapse(list) {
|
|
18666
|
+
const out = [];
|
|
18667
|
+
let under = null;
|
|
18668
|
+
for (const p of [...list].sort()) {
|
|
18669
|
+
if (under !== null && p.startsWith(under))
|
|
18670
|
+
continue;
|
|
18671
|
+
under = p.endsWith("/") ? p : null;
|
|
18672
|
+
out.push(p);
|
|
18673
|
+
}
|
|
18674
|
+
return out;
|
|
18128
18675
|
}
|
|
18129
18676
|
|
|
18130
18677
|
// ../daemon/src/files/vite-offset.ts
|
|
@@ -18300,12 +18847,13 @@ class FileService {
|
|
|
18300
18847
|
}
|
|
18301
18848
|
async list(worktreeId) {
|
|
18302
18849
|
const cwd = this.require(worktreeId).path;
|
|
18303
|
-
const [staged, untracked, deleted] = await Promise.all([
|
|
18850
|
+
const [staged, untracked, deleted, ignored] = await Promise.all([
|
|
18304
18851
|
git(cwd, "ls-files", "-z", "-s"),
|
|
18305
18852
|
git(cwd, "ls-files", "-z", "-o", "--exclude-standard"),
|
|
18306
|
-
git(cwd, "ls-files", "-z", "-d")
|
|
18853
|
+
git(cwd, "ls-files", "-z", "-d"),
|
|
18854
|
+
git(cwd, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory")
|
|
18307
18855
|
]);
|
|
18308
|
-
return parseListing(staged.out, untracked.out, deleted.out);
|
|
18856
|
+
return parseListing(staged.out, untracked.out, deleted.out, ignored.out);
|
|
18309
18857
|
}
|
|
18310
18858
|
async search(worktreeId, query) {
|
|
18311
18859
|
const { path: cwd } = this.require(worktreeId);
|
|
@@ -18528,6 +19076,7 @@ class AfterLand {
|
|
|
18528
19076
|
fireAndForget(repo.id, done, "afterLand");
|
|
18529
19077
|
}
|
|
18530
19078
|
async exec(repo, commands) {
|
|
19079
|
+
await this.d.settled?.();
|
|
18531
19080
|
if (this.d.self.building(true))
|
|
18532
19081
|
this.d.hub.emit("selfChanged");
|
|
18533
19082
|
let failure;
|
|
@@ -18568,41 +19117,18 @@ class AfterLand {
|
|
|
18568
19117
|
}
|
|
18569
19118
|
|
|
18570
19119
|
// ../daemon/src/repos/registry.ts
|
|
18571
|
-
import { existsSync as
|
|
19120
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
18572
19121
|
import { homedir as homedir7 } from "os";
|
|
18573
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
18574
|
-
|
|
18575
|
-
// ../daemon/src/worktrees/naming.ts
|
|
18576
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
18577
|
-
function shortId() {
|
|
18578
|
-
return randomBytes2(5).toString("hex");
|
|
18579
|
-
}
|
|
18580
|
-
function slugify2(prompt, withRandom = true) {
|
|
18581
|
-
const words = prompt.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter(Boolean).slice(0, 3);
|
|
18582
|
-
const base = words.join("-").slice(0, 20).replace(/-+$/g, "") || "task";
|
|
18583
|
-
return withRandom ? `${base}-${randomBytes2(2).toString("hex")}` : base;
|
|
18584
|
-
}
|
|
18585
|
-
function cleanTitle2(title) {
|
|
18586
|
-
return title.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
18587
|
-
}
|
|
18588
|
-
var VARIANT_LENSES = [
|
|
18589
|
-
"Take the most direct approach: the change most people would expect, in the smallest diff.",
|
|
18590
|
-
"Lean toward how it looks and reads: polish what the task touches using only the design tokens, components and voice the project already has. Add no new colours, fonts or effects, and restyle nothing outside the task.",
|
|
18591
|
-
"Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
|
|
18592
|
-
];
|
|
18593
|
-
function variantLens(index) {
|
|
18594
|
-
const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
|
|
18595
|
-
return `(You are attempt ${index} of several parallel attempts at this task. Stay within the task and follow the project's existing styles, components, tokens and writing conventions; the attempts differ in emphasis, not in how much they change. ${lens})`;
|
|
18596
|
-
}
|
|
19122
|
+
import { basename as basename3, dirname as dirname6, join as join16 } from "path";
|
|
18597
19123
|
|
|
18598
19124
|
// ../daemon/src/repos/browse.ts
|
|
18599
|
-
import { existsSync as
|
|
19125
|
+
import { existsSync as existsSync15, statSync as statSync3 } from "fs";
|
|
18600
19126
|
import { readdir } from "fs/promises";
|
|
18601
19127
|
import { homedir as homedir5 } from "os";
|
|
18602
|
-
import { dirname as dirname4, join as
|
|
19128
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
18603
19129
|
var MAX_ENTRIES = 40;
|
|
18604
19130
|
function expandTilde(raw) {
|
|
18605
|
-
return raw === "~" || raw.startsWith("~/") ?
|
|
19131
|
+
return raw === "~" || raw.startsWith("~/") ? join12(homedir5(), raw.slice(1)) : raw;
|
|
18606
19132
|
}
|
|
18607
19133
|
function collapseTilde(abs) {
|
|
18608
19134
|
const home = homedir5();
|
|
@@ -18613,7 +19139,7 @@ function holdsNothing(names) {
|
|
|
18613
19139
|
}
|
|
18614
19140
|
async function describeFolder(abs) {
|
|
18615
19141
|
const path = collapseTilde(abs);
|
|
18616
|
-
if (
|
|
19142
|
+
if (existsSync15(join12(abs, ".git")))
|
|
18617
19143
|
return { path, kind: "project" };
|
|
18618
19144
|
try {
|
|
18619
19145
|
return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
|
|
@@ -18628,10 +19154,10 @@ function describe2(typed, parent) {
|
|
|
18628
19154
|
isDir = statSync3(typed).isDirectory();
|
|
18629
19155
|
} catch {}
|
|
18630
19156
|
return {
|
|
18631
|
-
exists:
|
|
19157
|
+
exists: existsSync15(typed),
|
|
18632
19158
|
isDir,
|
|
18633
|
-
isRepo: isDir &&
|
|
18634
|
-
parentExists:
|
|
19159
|
+
isRepo: isDir && existsSync15(join12(typed, ".git")),
|
|
19160
|
+
parentExists: existsSync15(parent)
|
|
18635
19161
|
};
|
|
18636
19162
|
}
|
|
18637
19163
|
async function browsePath(raw) {
|
|
@@ -18655,16 +19181,16 @@ async function browsePath(raw) {
|
|
|
18655
19181
|
continue;
|
|
18656
19182
|
if (!name.toLowerCase().startsWith(prefix))
|
|
18657
19183
|
continue;
|
|
18658
|
-
const abs =
|
|
18659
|
-
entries.push({ path: collapseTilde(abs), name, isRepo:
|
|
19184
|
+
const abs = join12(dir, name);
|
|
19185
|
+
entries.push({ path: collapseTilde(abs), name, isRepo: existsSync15(join12(abs, ".git")) });
|
|
18660
19186
|
}
|
|
18661
19187
|
entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
|
|
18662
19188
|
return { entries: entries.slice(0, MAX_ENTRIES), target };
|
|
18663
19189
|
}
|
|
18664
19190
|
|
|
18665
19191
|
// ../daemon/src/repos/config.ts
|
|
18666
|
-
import { closeSync, existsSync as
|
|
18667
|
-
import { join as
|
|
19192
|
+
import { closeSync, existsSync as existsSync16, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync11, readSync } from "fs";
|
|
19193
|
+
import { join as join13 } from "path";
|
|
18668
19194
|
|
|
18669
19195
|
// ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
18670
19196
|
function createScanner(text, ignoreTrivia = false) {
|
|
@@ -20028,7 +20554,7 @@ function applyEdits(text, edits) {
|
|
|
20028
20554
|
|
|
20029
20555
|
// ../daemon/src/repos/config.ts
|
|
20030
20556
|
var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
|
|
20031
|
-
var present = (repoPath, place) => [place.shared, place.local].filter((rel) =>
|
|
20557
|
+
var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync16(join13(repoPath, rel)));
|
|
20032
20558
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
20033
20559
|
function parseSettings(text) {
|
|
20034
20560
|
const errors = [];
|
|
@@ -20053,7 +20579,7 @@ function readConfigFile(repoPath) {
|
|
|
20053
20579
|
for (const rel of files) {
|
|
20054
20580
|
let raw;
|
|
20055
20581
|
try {
|
|
20056
|
-
raw = parseSettings(readFileSync11(
|
|
20582
|
+
raw = parseSettings(readFileSync11(join13(repoPath, rel), "utf8"));
|
|
20057
20583
|
} catch (e) {
|
|
20058
20584
|
return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
20059
20585
|
}
|
|
@@ -20110,8 +20636,8 @@ function configTarget(repoPath, made) {
|
|
|
20110
20636
|
}
|
|
20111
20637
|
function configBody(repoPath, rel, config) {
|
|
20112
20638
|
const place = PLACES.find((p) => p.local === rel);
|
|
20113
|
-
const abs = place &&
|
|
20114
|
-
if (!abs || !
|
|
20639
|
+
const abs = place && join13(repoPath, place.shared);
|
|
20640
|
+
if (!abs || !existsSync16(abs))
|
|
20115
20641
|
return config;
|
|
20116
20642
|
let shared;
|
|
20117
20643
|
try {
|
|
@@ -20128,7 +20654,7 @@ function configText(repoPath, rel, config) {
|
|
|
20128
20654
|
let text;
|
|
20129
20655
|
let was;
|
|
20130
20656
|
try {
|
|
20131
|
-
text = readFileSync11(
|
|
20657
|
+
text = readFileSync11(join13(repoPath, rel), "utf8");
|
|
20132
20658
|
was = parseSettings(text);
|
|
20133
20659
|
} catch {
|
|
20134
20660
|
return fresh;
|
|
@@ -20158,8 +20684,8 @@ function detectConfig(repoPath) {
|
|
|
20158
20684
|
return { config: explicit.config, needsSetup: false };
|
|
20159
20685
|
if (explicit)
|
|
20160
20686
|
log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
|
|
20161
|
-
const pkgPath =
|
|
20162
|
-
if (
|
|
20687
|
+
const pkgPath = join13(repoPath, "package.json");
|
|
20688
|
+
if (existsSync16(pkgPath)) {
|
|
20163
20689
|
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
20164
20690
|
const scripts = pkg.scripts ?? {};
|
|
20165
20691
|
const runner = detectRunner(repoPath, pkg.packageManager);
|
|
@@ -20181,7 +20707,7 @@ function detectConfig(repoPath) {
|
|
|
20181
20707
|
};
|
|
20182
20708
|
}
|
|
20183
20709
|
}
|
|
20184
|
-
if (
|
|
20710
|
+
if (existsSync16(join13(repoPath, "start.sh"))) {
|
|
20185
20711
|
return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
|
|
20186
20712
|
}
|
|
20187
20713
|
const assumed = assumeNothingToRun(repoPath);
|
|
@@ -20224,24 +20750,24 @@ function assumeNothingToRun(repoPath) {
|
|
|
20224
20750
|
return marker && !servesPage(repoPath) ? marker : undefined;
|
|
20225
20751
|
}
|
|
20226
20752
|
function markerIn(repoPath) {
|
|
20227
|
-
const named = NO_PAGE_MARKERS.find((f) =>
|
|
20753
|
+
const named = NO_PAGE_MARKERS.find((f) => existsSync16(join13(repoPath, f)));
|
|
20228
20754
|
if (named)
|
|
20229
20755
|
return named;
|
|
20230
20756
|
let entries;
|
|
20231
20757
|
try {
|
|
20232
|
-
entries =
|
|
20758
|
+
entries = readdirSync4(repoPath);
|
|
20233
20759
|
} catch {
|
|
20234
20760
|
return;
|
|
20235
20761
|
}
|
|
20236
20762
|
return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
20237
20763
|
}
|
|
20238
20764
|
function servesPage(repoPath) {
|
|
20239
|
-
if (WEB_FILES.some((f) =>
|
|
20765
|
+
if (WEB_FILES.some((f) => existsSync16(join13(repoPath, f))))
|
|
20240
20766
|
return true;
|
|
20241
|
-
if (/^web:/m.test(head(
|
|
20767
|
+
if (/^web:/m.test(head(join13(repoPath, "Procfile"))))
|
|
20242
20768
|
return true;
|
|
20243
20769
|
for (const [files, names] of WEB_DEPENDENCIES) {
|
|
20244
|
-
const text = files.map((f) => head(
|
|
20770
|
+
const text = files.map((f) => head(join13(repoPath, f))).join(`
|
|
20245
20771
|
`);
|
|
20246
20772
|
if (text.trim() && names.some((n) => mentions(text, n)))
|
|
20247
20773
|
return true;
|
|
@@ -20251,14 +20777,14 @@ function servesPage(repoPath) {
|
|
|
20251
20777
|
function frontEndBeside(repoPath) {
|
|
20252
20778
|
let dirs;
|
|
20253
20779
|
try {
|
|
20254
|
-
dirs =
|
|
20780
|
+
dirs = readdirSync4(repoPath, { withFileTypes: true });
|
|
20255
20781
|
} catch {
|
|
20256
20782
|
return false;
|
|
20257
20783
|
}
|
|
20258
20784
|
return dirs.some((d) => {
|
|
20259
20785
|
if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
|
|
20260
20786
|
return false;
|
|
20261
|
-
const pkg = head(
|
|
20787
|
+
const pkg = head(join13(repoPath, d.name, "package.json"));
|
|
20262
20788
|
if (!pkg)
|
|
20263
20789
|
return false;
|
|
20264
20790
|
try {
|
|
@@ -20273,7 +20799,7 @@ function mentions(text, name) {
|
|
|
20273
20799
|
return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
|
|
20274
20800
|
}
|
|
20275
20801
|
function head(path) {
|
|
20276
|
-
if (!
|
|
20802
|
+
if (!existsSync16(path))
|
|
20277
20803
|
return "";
|
|
20278
20804
|
try {
|
|
20279
20805
|
const fd = openSync2(path, "r");
|
|
@@ -20301,9 +20827,9 @@ function procCommand(runner, script, body) {
|
|
|
20301
20827
|
return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
|
|
20302
20828
|
}
|
|
20303
20829
|
function detectPages(repoPath, scripts, runner) {
|
|
20304
|
-
if (!scripts.build || !
|
|
20830
|
+
if (!scripts.build || !existsSync16(join13(repoPath, "functions")))
|
|
20305
20831
|
return null;
|
|
20306
|
-
const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) =>
|
|
20832
|
+
const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join13(repoPath, f)).find((f) => existsSync16(f));
|
|
20307
20833
|
if (!cfg)
|
|
20308
20834
|
return null;
|
|
20309
20835
|
const out = readFileSync11(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
|
|
@@ -20312,20 +20838,20 @@ function detectPages(repoPath, scripts, runner) {
|
|
|
20312
20838
|
return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
|
|
20313
20839
|
}
|
|
20314
20840
|
function detectRunner(repoPath, packageManager) {
|
|
20315
|
-
if (
|
|
20841
|
+
if (existsSync16(join13(repoPath, "bun.lock")) || existsSync16(join13(repoPath, "bun.lockb")))
|
|
20316
20842
|
return "bun";
|
|
20317
|
-
if (
|
|
20843
|
+
if (existsSync16(join13(repoPath, "pnpm-lock.yaml")))
|
|
20318
20844
|
return "pnpm";
|
|
20319
|
-
if (
|
|
20845
|
+
if (existsSync16(join13(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
|
|
20320
20846
|
return "yarn";
|
|
20321
20847
|
return "npm";
|
|
20322
20848
|
}
|
|
20323
20849
|
|
|
20324
20850
|
// ../daemon/src/repos/create.ts
|
|
20325
|
-
import { existsSync as
|
|
20851
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
20326
20852
|
import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
|
|
20327
20853
|
import { homedir as homedir6 } from "os";
|
|
20328
|
-
import { dirname as dirname5, join as
|
|
20854
|
+
import { dirname as dirname5, join as join14, resolve as resolve7 } from "path";
|
|
20329
20855
|
function isInside(dir, parent) {
|
|
20330
20856
|
const p = parent.replace(/\/+$/, "");
|
|
20331
20857
|
return dir === p || dir.startsWith(`${p}/`);
|
|
@@ -20336,29 +20862,29 @@ function planProject(opts, home = homedir6()) {
|
|
|
20336
20862
|
throw new UserError(nameError);
|
|
20337
20863
|
const first = firstProjectsFolder(opts.parent, home);
|
|
20338
20864
|
const parent = first ?? resolveParent(opts.parent);
|
|
20339
|
-
const dir =
|
|
20340
|
-
if (
|
|
20865
|
+
const dir = join14(parent, opts.name.trim());
|
|
20866
|
+
if (existsSync17(dir))
|
|
20341
20867
|
throw new UserError(`${dir} already exists`);
|
|
20342
20868
|
return { parent, dir, makeParent: first !== null };
|
|
20343
20869
|
}
|
|
20344
20870
|
function firstProjectsFolder(raw, home) {
|
|
20345
20871
|
const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
|
|
20346
|
-
if (typed !==
|
|
20872
|
+
if (typed !== join14(home, PROJECTS_FOLDER) || existsSync17(typed) || !existsSync17(home))
|
|
20347
20873
|
return null;
|
|
20348
|
-
return
|
|
20874
|
+
return join14(realpathSync2(home), PROJECTS_FOLDER);
|
|
20349
20875
|
}
|
|
20350
20876
|
function planInPlace(opts) {
|
|
20351
20877
|
const name = opts.name.trim();
|
|
20352
20878
|
if (!name || name === "." || name === ".." || name.includes("/")) {
|
|
20353
20879
|
throw new UserError(`${opts.name} is not a folder name`);
|
|
20354
20880
|
}
|
|
20355
|
-
const typed =
|
|
20356
|
-
if (!
|
|
20881
|
+
const typed = join14(resolveParent(opts.parent), name);
|
|
20882
|
+
if (!existsSync17(typed) || !statSync4(typed).isDirectory())
|
|
20357
20883
|
throw new UserError(`${typed} is not a folder`);
|
|
20358
20884
|
const dir = realpathSync2(typed);
|
|
20359
|
-
if (
|
|
20885
|
+
if (existsSync17(join14(dir, ".git")))
|
|
20360
20886
|
throw new UserError(`${dir} is already a project; open it instead`);
|
|
20361
|
-
if (!holdsNothing(
|
|
20887
|
+
if (!holdsNothing(readdirSync5(dir))) {
|
|
20362
20888
|
throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
|
|
20363
20889
|
}
|
|
20364
20890
|
return { parent: dirname5(dir), dir, makeParent: false };
|
|
@@ -20367,7 +20893,7 @@ function resolveParent(raw) {
|
|
|
20367
20893
|
const typed = expandTilde(raw.trim());
|
|
20368
20894
|
if (!typed.startsWith("/"))
|
|
20369
20895
|
throw new UserError(`${raw} is not a folder path`);
|
|
20370
|
-
if (!
|
|
20896
|
+
if (!existsSync17(typed))
|
|
20371
20897
|
throw new UserError(`${raw} does not exist; make it first`);
|
|
20372
20898
|
if (!statSync4(typed).isDirectory())
|
|
20373
20899
|
throw new UserError(`${raw} is not a folder`);
|
|
@@ -20447,13 +20973,13 @@ async function undoCreate(dir) {
|
|
|
20447
20973
|
}
|
|
20448
20974
|
async function undoInit(dir) {
|
|
20449
20975
|
try {
|
|
20450
|
-
await rm(
|
|
20976
|
+
await rm(join14(dir, ".git"), { recursive: true, force: true });
|
|
20451
20977
|
} catch (e) {
|
|
20452
20978
|
log.warn(dir, "could not take back a half-made .git", e);
|
|
20453
20979
|
}
|
|
20454
20980
|
}
|
|
20455
20981
|
async function unmakeProject(dir, made) {
|
|
20456
|
-
const target = made === "folder" ? dir :
|
|
20982
|
+
const target = made === "folder" ? dir : join14(dir, ".git");
|
|
20457
20983
|
try {
|
|
20458
20984
|
await rm(target, { recursive: true, force: true });
|
|
20459
20985
|
} catch (e) {
|
|
@@ -20483,8 +21009,8 @@ async function ok(p, what) {
|
|
|
20483
21009
|
}
|
|
20484
21010
|
|
|
20485
21011
|
// ../daemon/src/repos/watcher.ts
|
|
20486
|
-
import { existsSync as
|
|
20487
|
-
import { basename as basename2, join as
|
|
21012
|
+
import { existsSync as existsSync18, watch } from "fs";
|
|
21013
|
+
import { basename as basename2, join as join15 } from "path";
|
|
20488
21014
|
function watchDefaultBranch(repoPath, branch, onMove) {
|
|
20489
21015
|
let last = null;
|
|
20490
21016
|
fireAndForget(repoPath, git(repoPath, "rev-parse", branch).then((r) => {
|
|
@@ -20514,8 +21040,8 @@ function watchDefaultBranch(repoPath, branch, onMove) {
|
|
|
20514
21040
|
log.debug(repoPath, `not watching ${p}`, e);
|
|
20515
21041
|
}
|
|
20516
21042
|
};
|
|
20517
|
-
tryWatch(
|
|
20518
|
-
tryWatch(
|
|
21043
|
+
tryWatch(join15(repoPath, ".git"));
|
|
21044
|
+
tryWatch(join15(repoPath, ".git", "refs", "heads"));
|
|
20519
21045
|
return () => {
|
|
20520
21046
|
for (const w of watchers)
|
|
20521
21047
|
w.close();
|
|
@@ -20537,7 +21063,7 @@ function watchWorktreeDir(repoPath, onChange) {
|
|
|
20537
21063
|
}, 300);
|
|
20538
21064
|
};
|
|
20539
21065
|
const arm = (commonDir) => {
|
|
20540
|
-
const dir =
|
|
21066
|
+
const dir = join15(commonDir, "worktrees");
|
|
20541
21067
|
const armInner = () => {
|
|
20542
21068
|
inner?.close();
|
|
20543
21069
|
inner = null;
|
|
@@ -20580,7 +21106,7 @@ function watchWorktreeDir(repoPath, onChange) {
|
|
|
20580
21106
|
function watchConfigFile(repoPath, onChange) {
|
|
20581
21107
|
const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
|
|
20582
21108
|
const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename2(rel)));
|
|
20583
|
-
const dir =
|
|
21109
|
+
const dir = join15(repoPath, CONFIG_DIR);
|
|
20584
21110
|
let timer = null;
|
|
20585
21111
|
let outer = null;
|
|
20586
21112
|
let inner = null;
|
|
@@ -20595,7 +21121,7 @@ function watchConfigFile(repoPath, onChange) {
|
|
|
20595
21121
|
const armInner = () => {
|
|
20596
21122
|
inner?.close();
|
|
20597
21123
|
inner = null;
|
|
20598
|
-
if (!
|
|
21124
|
+
if (!existsSync18(dir))
|
|
20599
21125
|
return;
|
|
20600
21126
|
try {
|
|
20601
21127
|
inner = watch(dir, (_event, filename) => {
|
|
@@ -20649,7 +21175,7 @@ class RepoRegistry {
|
|
|
20649
21175
|
}
|
|
20650
21176
|
async boot() {
|
|
20651
21177
|
const { state } = this.d;
|
|
20652
|
-
const gone = state.worktrees.filter((wt) => !
|
|
21178
|
+
const gone = state.worktrees.filter((wt) => !existsSync19(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
|
|
20653
21179
|
for (const wt of gone)
|
|
20654
21180
|
await this.d.worktrees.forgetGone(wt);
|
|
20655
21181
|
for (const repo of state.repos) {
|
|
@@ -20716,7 +21242,7 @@ class RepoRegistry {
|
|
|
20716
21242
|
return `${repo.name} is set up now`;
|
|
20717
21243
|
if (main.promptedAt || main.lastTurn)
|
|
20718
21244
|
return `${repo.name} has a chat now`;
|
|
20719
|
-
if (
|
|
21245
|
+
if (existsSync19(join16(repo.path, ".claude")))
|
|
20720
21246
|
return `an agent has already run in ${repo.name}`;
|
|
20721
21247
|
if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
|
|
20722
21248
|
return `${repo.name} has files in it now`;
|
|
@@ -20793,7 +21319,7 @@ class RepoRegistry {
|
|
|
20793
21319
|
}
|
|
20794
21320
|
async register(rawPath, made) {
|
|
20795
21321
|
const path = expandTilde(rawPath);
|
|
20796
|
-
if (!
|
|
21322
|
+
if (!existsSync19(path))
|
|
20797
21323
|
throw new UserError(`${path} does not exist`);
|
|
20798
21324
|
if (!await isGitRepo(path))
|
|
20799
21325
|
throw new UserError(`${path} is not a git repository`);
|
|
@@ -20831,7 +21357,6 @@ class RepoRegistry {
|
|
|
20831
21357
|
};
|
|
20832
21358
|
this.d.state.addWorktree(main);
|
|
20833
21359
|
this.warmed.add(repo.id);
|
|
20834
|
-
await this.d.runtime.start(main, repo);
|
|
20835
21360
|
this.startWatcher(repo);
|
|
20836
21361
|
fireAndForget(repo.id, this.d.worktrees.spare.ensure(repo.id), "spare warm-up");
|
|
20837
21362
|
this.d.hub.emit("reposChanged");
|
|
@@ -20853,8 +21378,8 @@ class RepoRegistry {
|
|
|
20853
21378
|
this.d.state.removeWorktree(wt.id);
|
|
20854
21379
|
this.d.worktrees.deleteChat(wt.id);
|
|
20855
21380
|
releasePort(wt.proxyPort);
|
|
21381
|
+
this.d.drafts?.drop(wt.id);
|
|
20856
21382
|
}
|
|
20857
|
-
this.d.drafts?.drop(draftKey(repoId));
|
|
20858
21383
|
this.d.state.removeRepo(repoId);
|
|
20859
21384
|
this.d.hub.emit("reposChanged");
|
|
20860
21385
|
this.d.hub.emit("worktreesChanged");
|
|
@@ -20866,10 +21391,10 @@ class RepoRegistry {
|
|
|
20866
21391
|
throw new UserError(current.reason);
|
|
20867
21392
|
const was = configTarget(repo.path, !!repo.made);
|
|
20868
21393
|
const rel = configSibling(was, kind);
|
|
20869
|
-
const moved = rel !== was &&
|
|
21394
|
+
const moved = rel !== was && existsSync19(join16(repo.path, was));
|
|
20870
21395
|
if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
|
|
20871
21396
|
try {
|
|
20872
|
-
rmSync2(
|
|
21397
|
+
rmSync2(join16(repo.path, was));
|
|
20873
21398
|
} catch (e) {
|
|
20874
21399
|
log.warn(repoId, `could not remove ${was}`, e);
|
|
20875
21400
|
}
|
|
@@ -20878,7 +21403,7 @@ class RepoRegistry {
|
|
|
20878
21403
|
}
|
|
20879
21404
|
if (isLocalConfigFile(rel))
|
|
20880
21405
|
await excludeFromGit(repo.path, rel);
|
|
20881
|
-
const file =
|
|
21406
|
+
const file = join16(repo.path, rel);
|
|
20882
21407
|
try {
|
|
20883
21408
|
mkdirSync6(dirname6(file), { recursive: true });
|
|
20884
21409
|
writeFileSync7(file, configText(repo.path, rel, config));
|
|
@@ -20893,7 +21418,7 @@ class RepoRegistry {
|
|
|
20893
21418
|
repo.assumed = undefined;
|
|
20894
21419
|
this.d.state.save();
|
|
20895
21420
|
this.warmed.add(repoId);
|
|
20896
|
-
for (const wt of this.d.state.worktrees.filter((w) => w.repoId === repoId)) {
|
|
21421
|
+
for (const wt of this.d.state.worktrees.filter((w) => w.repoId === repoId && (w.kind !== "main" || this.d.runtime.get(w.id)?.procs))) {
|
|
20897
21422
|
fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.start(wt, repo)), "runtime restart");
|
|
20898
21423
|
}
|
|
20899
21424
|
fireAndForget(repoId, this.d.worktrees.spare.ensure(repoId), "spare warm-up");
|
|
@@ -20955,7 +21480,7 @@ class RepoRegistry {
|
|
|
20955
21480
|
}
|
|
20956
21481
|
placeConfig(repo) {
|
|
20957
21482
|
const rel = configTarget(repo.path, !!repo.made);
|
|
20958
|
-
if (isLocalConfigFile(rel) &&
|
|
21483
|
+
if (isLocalConfigFile(rel) && existsSync19(join16(repo.path, rel))) {
|
|
20959
21484
|
fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
|
|
20960
21485
|
}
|
|
20961
21486
|
if (repo.configFile === rel)
|
|
@@ -21030,7 +21555,7 @@ class RepoRegistry {
|
|
|
21030
21555
|
}
|
|
21031
21556
|
|
|
21032
21557
|
// ../daemon/src/routes/service.ts
|
|
21033
|
-
import { join as
|
|
21558
|
+
import { join as join17 } from "path";
|
|
21034
21559
|
|
|
21035
21560
|
// ../daemon/src/routes/fileRouters.ts
|
|
21036
21561
|
var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
|
|
@@ -22230,7 +22755,7 @@ class RouteService {
|
|
|
22230
22755
|
return mergeRoutes(fileRoutes(paths, roots), code);
|
|
22231
22756
|
}
|
|
22232
22757
|
async cachedRead(root, path, had, kept, decode) {
|
|
22233
|
-
const abs =
|
|
22758
|
+
const abs = join17(root, path);
|
|
22234
22759
|
const stat = await this.fs.stat(abs);
|
|
22235
22760
|
if (!stat)
|
|
22236
22761
|
return null;
|
|
@@ -22255,7 +22780,7 @@ class RouteService {
|
|
|
22255
22780
|
}
|
|
22256
22781
|
}
|
|
22257
22782
|
async hash(root, file) {
|
|
22258
|
-
const abs =
|
|
22783
|
+
const abs = join17(root, file);
|
|
22259
22784
|
const stat = await this.fs.stat(abs);
|
|
22260
22785
|
if (!stat)
|
|
22261
22786
|
return null;
|
|
@@ -22299,7 +22824,7 @@ class RouteService {
|
|
|
22299
22824
|
}
|
|
22300
22825
|
|
|
22301
22826
|
// ../daemon/src/runtime/bridge-script.ts
|
|
22302
|
-
import { existsSync as
|
|
22827
|
+
import { existsSync as existsSync20, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
|
|
22303
22828
|
var LEARNED_MAX = 8;
|
|
22304
22829
|
|
|
22305
22830
|
class BridgeScript {
|
|
@@ -22329,7 +22854,7 @@ class BridgeScript {
|
|
|
22329
22854
|
this.cache = null;
|
|
22330
22855
|
}
|
|
22331
22856
|
get() {
|
|
22332
|
-
if (!
|
|
22857
|
+
if (!existsSync20(this.path))
|
|
22333
22858
|
return "// toyon bridge not built";
|
|
22334
22859
|
const s = statSync5(this.path);
|
|
22335
22860
|
const stamp = `${s.mtimeMs}:${s.size}`;
|
|
@@ -22355,70 +22880,6 @@ function servedLocally(origin) {
|
|
|
22355
22880
|
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
|
|
22356
22881
|
}
|
|
22357
22882
|
|
|
22358
|
-
// ../daemon/src/runtime/memory.ts
|
|
22359
|
-
var MAC_PRESSURE_CRITICAL = 4;
|
|
22360
|
-
var AVAILABLE_FLOOR = 0.15;
|
|
22361
|
-
var unsupportedLogged = false;
|
|
22362
|
-
async function memoryTight() {
|
|
22363
|
-
const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
|
|
22364
|
-
if (signal === null && !unsupportedLogged) {
|
|
22365
|
-
unsupportedLogged = true;
|
|
22366
|
-
log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
|
|
22367
|
-
}
|
|
22368
|
-
return signal;
|
|
22369
|
-
}
|
|
22370
|
-
async function darwin() {
|
|
22371
|
-
const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
|
|
22372
|
-
if (!r.ok)
|
|
22373
|
-
return null;
|
|
22374
|
-
const [level, available] = r.out.split(/\s+/).map(Number);
|
|
22375
|
-
if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
|
|
22376
|
-
return null;
|
|
22377
|
-
return darwinSignal(level, available);
|
|
22378
|
-
}
|
|
22379
|
-
function darwinSignal(level, available) {
|
|
22380
|
-
const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
|
|
22381
|
-
return {
|
|
22382
|
-
tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
|
|
22383
|
-
why: `memory pressure ${pressure} with ${available}% available`
|
|
22384
|
-
};
|
|
22385
|
-
}
|
|
22386
|
-
async function linux() {
|
|
22387
|
-
let text;
|
|
22388
|
-
try {
|
|
22389
|
-
text = await Bun.file("/proc/meminfo").text();
|
|
22390
|
-
} catch {
|
|
22391
|
-
return null;
|
|
22392
|
-
}
|
|
22393
|
-
const field = (name) => {
|
|
22394
|
-
const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
|
|
22395
|
-
return m ? Number(m[1]) : null;
|
|
22396
|
-
};
|
|
22397
|
-
const total = field("MemTotal");
|
|
22398
|
-
const available = field("MemAvailable");
|
|
22399
|
-
if (!total || available === null)
|
|
22400
|
-
return null;
|
|
22401
|
-
const percent = Math.round(available / total * 100);
|
|
22402
|
-
return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
|
|
22403
|
-
}
|
|
22404
|
-
async function sampleCosts(pgids) {
|
|
22405
|
-
const costs = new Map;
|
|
22406
|
-
if (pgids.length === 0)
|
|
22407
|
-
return costs;
|
|
22408
|
-
const wanted = new Set(pgids);
|
|
22409
|
-
const r = await run("ps", ["-axo", "pgid=,rss="], "/");
|
|
22410
|
-
if (!r.ok)
|
|
22411
|
-
return costs;
|
|
22412
|
-
for (const line of r.out.split(`
|
|
22413
|
-
`)) {
|
|
22414
|
-
const [pgid, rss] = line.trim().split(/\s+/).map(Number);
|
|
22415
|
-
if (pgid === undefined || rss === undefined || !wanted.has(pgid))
|
|
22416
|
-
continue;
|
|
22417
|
-
costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
|
|
22418
|
-
}
|
|
22419
|
-
return costs;
|
|
22420
|
-
}
|
|
22421
|
-
|
|
22422
22883
|
// ../daemon/src/runtime/idle.ts
|
|
22423
22884
|
var LOCAL_SLEEP_MS = 2 * 60 * 60000;
|
|
22424
22885
|
var CLOUD_SLEEP_MS = 5 * 60000;
|
|
@@ -22441,6 +22902,7 @@ class IdlePolicy {
|
|
|
22441
22902
|
viewing = new Map;
|
|
22442
22903
|
queue = [];
|
|
22443
22904
|
draining = false;
|
|
22905
|
+
drained = [];
|
|
22444
22906
|
sleepMs;
|
|
22445
22907
|
now;
|
|
22446
22908
|
constructor(d) {
|
|
@@ -22534,6 +22996,9 @@ class IdlePolicy {
|
|
|
22534
22996
|
const wt = this.d.state.worktree(id);
|
|
22535
22997
|
return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
|
|
22536
22998
|
}
|
|
22999
|
+
isShown(id) {
|
|
23000
|
+
return (this.entries.get(id)?.viewers.size ?? 0) > 0;
|
|
23001
|
+
}
|
|
22537
23002
|
isViewed(id) {
|
|
22538
23003
|
if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
|
|
22539
23004
|
return true;
|
|
@@ -22556,7 +23021,8 @@ class IdlePolicy {
|
|
|
22556
23021
|
const pick = this.d.state.worktrees.filter((w) => this.candidate(w.id)).sort((a, b) => this.entry(a.id).activeAt - this.entry(b.id).activeAt)[0];
|
|
22557
23022
|
if (!pick)
|
|
22558
23023
|
return;
|
|
22559
|
-
|
|
23024
|
+
log.info("idle", `${signal.why}; ${pick.id} is the least recently used`);
|
|
23025
|
+
this.sleep(pick.id, "short of memory");
|
|
22560
23026
|
}
|
|
22561
23027
|
async sampleCosts() {
|
|
22562
23028
|
this.recordViewed();
|
|
@@ -22654,13 +23120,12 @@ class IdlePolicy {
|
|
|
22654
23120
|
this.arm(id, this.sleepMs - idle);
|
|
22655
23121
|
return;
|
|
22656
23122
|
}
|
|
22657
|
-
this.sleep(id, `
|
|
23123
|
+
this.sleep(id, `nobody looked for ${humanMs(this.sleepMs)}`);
|
|
22658
23124
|
}
|
|
22659
23125
|
sleep(id, why) {
|
|
22660
23126
|
this.disarm(id);
|
|
22661
23127
|
this.entry(id).costKb = undefined;
|
|
22662
|
-
|
|
22663
|
-
fireAndForget(id, this.d.runtime.sleep(id), "sleep");
|
|
23128
|
+
fireAndForget(id, this.d.runtime.sleep(id, why), "sleep");
|
|
22664
23129
|
}
|
|
22665
23130
|
enqueue(key, run) {
|
|
22666
23131
|
if (this.queue.some((q) => q.key === key))
|
|
@@ -22676,15 +23141,22 @@ class IdlePolicy {
|
|
|
22676
23141
|
await next.run();
|
|
22677
23142
|
} finally {
|
|
22678
23143
|
this.draining = false;
|
|
23144
|
+
for (const r of this.drained.splice(0))
|
|
23145
|
+
r();
|
|
22679
23146
|
}
|
|
22680
23147
|
}
|
|
23148
|
+
settled() {
|
|
23149
|
+
if (!this.draining)
|
|
23150
|
+
return Promise.resolve();
|
|
23151
|
+
return new Promise((r) => this.drained.push(r));
|
|
23152
|
+
}
|
|
22681
23153
|
}
|
|
22682
23154
|
function humanMs(ms) {
|
|
22683
23155
|
if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
|
|
22684
|
-
return `${ms / (60 * 60000)}h`;
|
|
23156
|
+
return `${ms / (60 * 60000)} h`;
|
|
22685
23157
|
if (ms >= 60000)
|
|
22686
23158
|
return `${Math.round(ms / 60000)} min`;
|
|
22687
|
-
return `${Math.round(ms / 1000)}s`;
|
|
23159
|
+
return `${Math.round(ms / 1000)} s`;
|
|
22688
23160
|
}
|
|
22689
23161
|
|
|
22690
23162
|
// ../daemon/src/server/ws.ts
|
|
@@ -22711,47 +23183,47 @@ class PairCodes {
|
|
|
22711
23183
|
}
|
|
22712
23184
|
|
|
22713
23185
|
// ../daemon/src/agent/config.ts
|
|
22714
|
-
import { existsSync as
|
|
23186
|
+
import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
|
|
22715
23187
|
import { homedir as homedir8 } from "os";
|
|
22716
|
-
import { join as
|
|
23188
|
+
import { join as join18 } from "path";
|
|
22717
23189
|
function agentConfigFiles(spec, repoPath, home = homedir8()) {
|
|
22718
23190
|
const file = (id, label, path) => ({
|
|
22719
23191
|
id,
|
|
22720
23192
|
label,
|
|
22721
23193
|
path,
|
|
22722
|
-
exists:
|
|
23194
|
+
exists: existsSync21(path)
|
|
22723
23195
|
});
|
|
22724
23196
|
if (spec.id === "claude") {
|
|
22725
23197
|
return [
|
|
22726
|
-
file("user-config", "user config (MCP servers, projects)",
|
|
22727
|
-
file("user-settings", "user settings (permissions, hooks)",
|
|
22728
|
-
...repoPath ? [file("project-mcp", "project MCP servers",
|
|
22729
|
-
...repoPath ? [file("project-settings", "project settings",
|
|
23198
|
+
file("user-config", "user config (MCP servers, projects)", join18(home, ".claude.json")),
|
|
23199
|
+
file("user-settings", "user settings (permissions, hooks)", join18(home, ".claude", "settings.json")),
|
|
23200
|
+
...repoPath ? [file("project-mcp", "project MCP servers", join18(repoPath, ".mcp.json"))] : [],
|
|
23201
|
+
...repoPath ? [file("project-settings", "project settings", join18(repoPath, ".claude", "settings.json"))] : []
|
|
22730
23202
|
];
|
|
22731
23203
|
}
|
|
22732
23204
|
if (spec.id === "codex")
|
|
22733
|
-
return [file("user-config", "config",
|
|
23205
|
+
return [file("user-config", "config", join18(home, ".codex", "config.toml"))];
|
|
22734
23206
|
return [];
|
|
22735
23207
|
}
|
|
22736
23208
|
function describeAgentConfig(spec, repoPath, home = homedir8()) {
|
|
22737
23209
|
const files = agentConfigFiles(spec, repoPath, home);
|
|
22738
23210
|
const servers = [];
|
|
22739
23211
|
if (spec.id === "claude") {
|
|
22740
|
-
const user = readJson(
|
|
23212
|
+
const user = readJson(join18(home, ".claude.json"));
|
|
22741
23213
|
servers.push(...serversOf(user?.mcpServers, "user"));
|
|
22742
23214
|
if (repoPath) {
|
|
22743
23215
|
const projects = user?.projects;
|
|
22744
23216
|
const mine = projects?.[repoPath];
|
|
22745
23217
|
servers.push(...serversOf(mine?.mcpServers, "local"));
|
|
22746
|
-
servers.push(...serversOf(readJson(
|
|
23218
|
+
servers.push(...serversOf(readJson(join18(repoPath, ".mcp.json"))?.mcpServers, "project"));
|
|
22747
23219
|
}
|
|
22748
23220
|
} else if (spec.id === "codex") {
|
|
22749
|
-
servers.push(...codexServers(
|
|
23221
|
+
servers.push(...codexServers(join18(home, ".codex", "config.toml")));
|
|
22750
23222
|
}
|
|
22751
23223
|
return { agent: spec.id, files, servers };
|
|
22752
23224
|
}
|
|
22753
23225
|
function readJson(path) {
|
|
22754
|
-
if (!
|
|
23226
|
+
if (!existsSync21(path))
|
|
22755
23227
|
return null;
|
|
22756
23228
|
try {
|
|
22757
23229
|
const v = JSON.parse(readFileSync13(path, "utf8"));
|
|
@@ -22772,7 +23244,7 @@ function serversOf(raw, scope) {
|
|
|
22772
23244
|
});
|
|
22773
23245
|
}
|
|
22774
23246
|
function codexServers(path) {
|
|
22775
|
-
if (!
|
|
23247
|
+
if (!existsSync21(path))
|
|
22776
23248
|
return [];
|
|
22777
23249
|
const out = [];
|
|
22778
23250
|
let current = null;
|
|
@@ -23368,8 +23840,11 @@ var handlers = {
|
|
|
23368
23840
|
unsubscribe(msg, ctx) {
|
|
23369
23841
|
ctx.unsubscribe(msg.worktreeId);
|
|
23370
23842
|
},
|
|
23371
|
-
view(msg, ctx) {
|
|
23843
|
+
view(msg, ctx, s) {
|
|
23372
23844
|
ctx.view(msg.worktreeId);
|
|
23845
|
+
const wt = msg.worktreeId ? s.state.worktree(msg.worktreeId) : undefined;
|
|
23846
|
+
if (wt && isLead(wt))
|
|
23847
|
+
fireAndForget(wt.repoId, s.worktrees.syncTrunk(wt.repoId), "trunk sync");
|
|
23373
23848
|
},
|
|
23374
23849
|
seen(msg, _ctx, s) {
|
|
23375
23850
|
s.turns.markSeen(msg.worktreeId);
|
|
@@ -23379,6 +23854,7 @@ var handlers = {
|
|
|
23379
23854
|
},
|
|
23380
23855
|
"set-draft"(msg, _ctx, s) {
|
|
23381
23856
|
s.drafts.set(msg.boxId, msg.text, msg.clientId);
|
|
23857
|
+
s.worktrees.spare.typed(msg.boxId, msg.text);
|
|
23382
23858
|
},
|
|
23383
23859
|
"refresh-git"(msg, _ctx, s) {
|
|
23384
23860
|
s.worktrees.recount(msg.repoId);
|
|
@@ -23403,6 +23879,7 @@ var handlers = {
|
|
|
23403
23879
|
async "create-worktree"(msg, _ctx, s) {
|
|
23404
23880
|
await s.worktrees.create(msg.repoId, msg.prompt, {
|
|
23405
23881
|
createdBy: msg.clientId,
|
|
23882
|
+
worktreeId: msg.worktreeId,
|
|
23406
23883
|
variant: msg.variant,
|
|
23407
23884
|
context: msg.context,
|
|
23408
23885
|
attachments: msg.attachments,
|
|
@@ -23526,6 +24003,9 @@ var handlers = {
|
|
|
23526
24003
|
"update-now"(_msg, _ctx, s) {
|
|
23527
24004
|
return s.update.updateNow();
|
|
23528
24005
|
},
|
|
24006
|
+
"check-update"(_msg, _ctx, s) {
|
|
24007
|
+
return s.update.checkNow();
|
|
24008
|
+
},
|
|
23529
24009
|
async commit(msg, ctx, s) {
|
|
23530
24010
|
const result = await s.worktrees.commit(msg.worktreeId, msg.message);
|
|
23531
24011
|
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
@@ -23580,6 +24060,10 @@ var handlers = {
|
|
|
23580
24060
|
async "discard-file"(msg, _ctx, s) {
|
|
23581
24061
|
await s.files.discard(msg.worktreeId, msg.path);
|
|
23582
24062
|
s.hub.emit("filesChanged", msg.worktreeId);
|
|
24063
|
+
s.landing.recheck(msg.worktreeId);
|
|
24064
|
+
},
|
|
24065
|
+
async judge(msg, _ctx, s) {
|
|
24066
|
+
await s.landing.judge(msg.worktreeId);
|
|
23583
24067
|
},
|
|
23584
24068
|
async "write-file"(msg, ctx, s) {
|
|
23585
24069
|
const head = { t: "file-written", worktreeId: msg.worktreeId, path: msg.path, seq: msg.seq };
|
|
@@ -23762,8 +24246,8 @@ async function dispatch(msg, ctx, s) {
|
|
|
23762
24246
|
}
|
|
23763
24247
|
|
|
23764
24248
|
// ../daemon/src/server/http.ts
|
|
23765
|
-
import { existsSync as
|
|
23766
|
-
import { join as
|
|
24249
|
+
import { existsSync as existsSync22 } from "fs";
|
|
24250
|
+
import { join as join19 } from "path";
|
|
23767
24251
|
var IMMUTABLE = "private, max-age=31536000, immutable";
|
|
23768
24252
|
var NO_STORE = "no-store";
|
|
23769
24253
|
function createFetch(opts) {
|
|
@@ -23833,10 +24317,12 @@ function createFetch(opts) {
|
|
|
23833
24317
|
headers["set-cookie"] = grantCookie(grant, remote.host);
|
|
23834
24318
|
return Response.json(await opts.bootstrap(), { headers });
|
|
23835
24319
|
}
|
|
23836
|
-
if (url.pathname === "/restart" && req.method === "POST") {
|
|
24320
|
+
if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
|
|
23837
24321
|
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
23838
24322
|
return new Response("unauthorized", { status: 401 });
|
|
23839
|
-
|
|
24323
|
+
if (req.method === "GET")
|
|
24324
|
+
return Response.json(opts.restartWait(), { headers: { "cache-control": NO_STORE } });
|
|
24325
|
+
const refused = opts.restart(url.searchParams.has(RESTART_NOW));
|
|
23840
24326
|
return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
|
|
23841
24327
|
}
|
|
23842
24328
|
if (url.pathname === "/pair" && req.method === "POST") {
|
|
@@ -23885,7 +24371,7 @@ function createFetch(opts) {
|
|
|
23885
24371
|
return new Response("unauthorized", { status: 401 });
|
|
23886
24372
|
const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
|
|
23887
24373
|
const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
|
|
23888
|
-
const path = places.find((p) => !!p &&
|
|
24374
|
+
const path = places.find((p) => !!p && existsSync22(p));
|
|
23889
24375
|
if (!path)
|
|
23890
24376
|
return new Response("not found", { status: 404 });
|
|
23891
24377
|
return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
|
|
@@ -23906,15 +24392,15 @@ function createFetch(opts) {
|
|
|
23906
24392
|
});
|
|
23907
24393
|
}
|
|
23908
24394
|
const rel = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
23909
|
-
const file =
|
|
24395
|
+
const file = join19(opts.shellDist, rel.replaceAll("..", ""));
|
|
23910
24396
|
const hashed = rel.startsWith("/assets/");
|
|
23911
|
-
if (
|
|
24397
|
+
if (existsSync22(file) && Bun.file(file).size > 0) {
|
|
23912
24398
|
return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
|
|
23913
24399
|
}
|
|
23914
24400
|
if (hashed)
|
|
23915
24401
|
return new Response("not found", { status: 404 });
|
|
23916
|
-
const index =
|
|
23917
|
-
if (
|
|
24402
|
+
const index = join19(opts.shellDist, "index.html");
|
|
24403
|
+
if (existsSync22(index))
|
|
23918
24404
|
return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
|
|
23919
24405
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
23920
24406
|
};
|
|
@@ -24026,7 +24512,7 @@ function startServer(opts) {
|
|
|
24026
24512
|
fireAndForget("ws", (async () => {
|
|
24027
24513
|
do {
|
|
24028
24514
|
statusesDirty = false;
|
|
24029
|
-
broadcast({ t: "worktrees", rows: await s.worktrees.rows(),
|
|
24515
|
+
broadcast({ t: "worktrees", rows: await s.worktrees.rows(), trunks: await s.worktrees.trunks() });
|
|
24030
24516
|
} while (statusesDirty);
|
|
24031
24517
|
})().finally(() => {
|
|
24032
24518
|
statusesInFlight = false;
|
|
@@ -24132,10 +24618,11 @@ function startServer(opts) {
|
|
|
24132
24618
|
return {
|
|
24133
24619
|
t: "hello",
|
|
24134
24620
|
version,
|
|
24621
|
+
install: s.update.install(),
|
|
24135
24622
|
protocol: PROTOCOL_VERSION,
|
|
24136
24623
|
repos: s.state.repos,
|
|
24137
24624
|
rows: await s.worktrees.rows({ quick: true }),
|
|
24138
|
-
|
|
24625
|
+
trunks: await s.worktrees.trunks({ quick: true }),
|
|
24139
24626
|
themes: s.themes.themes,
|
|
24140
24627
|
themePrefs: s.themes.prefs,
|
|
24141
24628
|
agents: agentInfos(),
|
|
@@ -24170,7 +24657,8 @@ function startServer(opts) {
|
|
|
24170
24657
|
remote: opts.remote,
|
|
24171
24658
|
preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
|
|
24172
24659
|
bootstrap: helloFrame,
|
|
24173
|
-
restart: () => s.restarter.request(),
|
|
24660
|
+
restart: (now) => s.restarter.request({ now }),
|
|
24661
|
+
restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
|
|
24174
24662
|
pair: new PairCodes,
|
|
24175
24663
|
onPaired: () => {
|
|
24176
24664
|
s.state.notePaired();
|
|
@@ -24282,16 +24770,16 @@ function startServer(opts) {
|
|
|
24282
24770
|
}
|
|
24283
24771
|
|
|
24284
24772
|
// ../daemon/src/themes/store.ts
|
|
24285
|
-
import { existsSync as
|
|
24773
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
24286
24774
|
import { homedir as homedir10 } from "os";
|
|
24287
|
-
import { basename as basename4, dirname as dirname7, join as
|
|
24775
|
+
import { basename as basename4, dirname as dirname7, join as join20, resolve as resolve8 } from "path";
|
|
24288
24776
|
var home = homedir10();
|
|
24289
24777
|
var defaultExtensionDirs = [
|
|
24290
|
-
|
|
24291
|
-
|
|
24292
|
-
|
|
24293
|
-
|
|
24294
|
-
|
|
24778
|
+
join20(home, ".vscode", "extensions"),
|
|
24779
|
+
join20(home, ".vscode-insiders", "extensions"),
|
|
24780
|
+
join20(home, ".vscode-oss", "extensions"),
|
|
24781
|
+
join20(home, ".cursor", "extensions"),
|
|
24782
|
+
join20(home, ".windsurf", "extensions"),
|
|
24295
24783
|
"/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
|
|
24296
24784
|
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
|
|
24297
24785
|
"/Applications/Cursor.app/Contents/Resources/app/extensions",
|
|
@@ -24375,7 +24863,7 @@ class ThemeStore {
|
|
|
24375
24863
|
name: json?.name ?? base,
|
|
24376
24864
|
source: "file"
|
|
24377
24865
|
});
|
|
24378
|
-
writeFileSync8(
|
|
24866
|
+
writeFileSync8(join20(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
|
|
24379
24867
|
`);
|
|
24380
24868
|
this.load();
|
|
24381
24869
|
return theme;
|
|
@@ -24404,13 +24892,13 @@ function readVscodeTheme(file, depth = 0) {
|
|
|
24404
24892
|
return t;
|
|
24405
24893
|
}
|
|
24406
24894
|
function loadDir(dir) {
|
|
24407
|
-
if (!
|
|
24895
|
+
if (!existsSync23(dir))
|
|
24408
24896
|
return [];
|
|
24409
24897
|
const out = [];
|
|
24410
|
-
for (const f of
|
|
24898
|
+
for (const f of readdirSync6(dir)) {
|
|
24411
24899
|
if (!/\.jsonc?$/i.test(f))
|
|
24412
24900
|
continue;
|
|
24413
|
-
const file =
|
|
24901
|
+
const file = join20(dir, f);
|
|
24414
24902
|
const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
|
|
24415
24903
|
try {
|
|
24416
24904
|
const json = readJsonc(file);
|
|
@@ -24426,20 +24914,20 @@ function loadDir(dir) {
|
|
|
24426
24914
|
return out;
|
|
24427
24915
|
}
|
|
24428
24916
|
function discover(dir) {
|
|
24429
|
-
if (!
|
|
24917
|
+
if (!existsSync23(dir))
|
|
24430
24918
|
return [];
|
|
24431
24919
|
const out = [];
|
|
24432
24920
|
let entries;
|
|
24433
24921
|
try {
|
|
24434
|
-
entries =
|
|
24922
|
+
entries = readdirSync6(dir);
|
|
24435
24923
|
} catch {
|
|
24436
24924
|
return out;
|
|
24437
24925
|
}
|
|
24438
24926
|
for (const name of entries) {
|
|
24439
|
-
const extDir =
|
|
24440
|
-
const pkgFile =
|
|
24927
|
+
const extDir = join20(dir, name);
|
|
24928
|
+
const pkgFile = join20(extDir, "package.json");
|
|
24441
24929
|
try {
|
|
24442
|
-
if (!statSync6(extDir).isDirectory() || !
|
|
24930
|
+
if (!statSync6(extDir).isDirectory() || !existsSync23(pkgFile))
|
|
24443
24931
|
continue;
|
|
24444
24932
|
const pkg = JSON.parse(readFileSync14(pkgFile, "utf8"));
|
|
24445
24933
|
const themes = pkg.contributes?.themes;
|
|
@@ -24447,8 +24935,8 @@ function discover(dir) {
|
|
|
24447
24935
|
continue;
|
|
24448
24936
|
const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
|
|
24449
24937
|
let nls = {};
|
|
24450
|
-
const nlsFile =
|
|
24451
|
-
if (
|
|
24938
|
+
const nlsFile = join20(extDir, "package.nls.json");
|
|
24939
|
+
if (existsSync23(nlsFile)) {
|
|
24452
24940
|
try {
|
|
24453
24941
|
nls = JSON.parse(readFileSync14(nlsFile, "utf8"));
|
|
24454
24942
|
} catch {}
|
|
@@ -24589,6 +25077,21 @@ class UpdateService {
|
|
|
24589
25077
|
this.wanted = true;
|
|
24590
25078
|
await this.proceed();
|
|
24591
25079
|
}
|
|
25080
|
+
async checkNow() {
|
|
25081
|
+
if (this.d.managed)
|
|
25082
|
+
throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
|
|
25083
|
+
if (this.d.method === "none") {
|
|
25084
|
+
throw new UserError(`Toyon ${this.d.running} runs from a checkout, which does not update itself`);
|
|
25085
|
+
}
|
|
25086
|
+
await this.check();
|
|
25087
|
+
if (this.unreachable)
|
|
25088
|
+
throw new UserError(`Could not reach ${this.unreachable}`);
|
|
25089
|
+
if (this.target() === null)
|
|
25090
|
+
throw new UserError(`Toyon ${this.d.running} is the newest version`);
|
|
25091
|
+
}
|
|
25092
|
+
install() {
|
|
25093
|
+
return this.d.method;
|
|
25094
|
+
}
|
|
24592
25095
|
get() {
|
|
24593
25096
|
const restarting = this.d.restarter.waitingOn() ?? (this.wanted && !this.installing ? this.d.restarter.working() : null);
|
|
24594
25097
|
if (this.installed === null && this.latest === null && !this.installing && this.failed === null && restarting === null) {
|
|
@@ -24809,14 +25312,22 @@ var DIFF_CHARS = 2000;
|
|
|
24809
25312
|
class LandingService {
|
|
24810
25313
|
d;
|
|
24811
25314
|
judging = new Map;
|
|
25315
|
+
busy = new Set;
|
|
24812
25316
|
constructor(d) {
|
|
24813
25317
|
this.d = d;
|
|
24814
25318
|
d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
|
|
24815
25319
|
d.hub.on("agentStatus", (id, status) => {
|
|
25320
|
+
this.note(id, status);
|
|
24816
25321
|
if (status === "working")
|
|
24817
25322
|
this.clear(id);
|
|
24818
25323
|
});
|
|
24819
25324
|
}
|
|
25325
|
+
note(worktreeId, status) {
|
|
25326
|
+
if (status === "working" || status === "waiting")
|
|
25327
|
+
this.busy.add(worktreeId);
|
|
25328
|
+
else
|
|
25329
|
+
this.busy.delete(worktreeId);
|
|
25330
|
+
}
|
|
24820
25331
|
clear(worktreeId) {
|
|
24821
25332
|
this.judging.delete(worktreeId);
|
|
24822
25333
|
this.d.worktrees.setLanding(worktreeId, undefined);
|
|
@@ -24827,28 +25338,91 @@ class LandingService {
|
|
|
24827
25338
|
return;
|
|
24828
25339
|
if (turn.end !== "done")
|
|
24829
25340
|
return this.clear(worktreeId);
|
|
25341
|
+
if (!await this.hasWork(wt)) {
|
|
25342
|
+
this.clear(worktreeId);
|
|
25343
|
+
return this.recapAnswer(wt, turn);
|
|
25344
|
+
}
|
|
25345
|
+
await this.run(wt, { at: turn.at, ask: true, quiet: false });
|
|
25346
|
+
}
|
|
25347
|
+
async recapAnswer(wt, turn) {
|
|
25348
|
+
if (!this.d.recap)
|
|
25349
|
+
return;
|
|
25350
|
+
const worktreeId = wt.id;
|
|
25351
|
+
const entries = this.d.transcript(worktreeId);
|
|
25352
|
+
const turns = turnsSince(entries, 0);
|
|
25353
|
+
if (!turns.at(-1)?.reply.trim())
|
|
25354
|
+
return;
|
|
25355
|
+
this.judging.set(worktreeId, turn.at);
|
|
25356
|
+
const started = Date.now();
|
|
25357
|
+
let text = null;
|
|
25358
|
+
try {
|
|
25359
|
+
text = await this.d.recap(wt, answerPrompt({ title: wt.title, firstAsk: firstAskOf(entries), turns }));
|
|
25360
|
+
} catch (e) {
|
|
25361
|
+
log.warn(worktreeId, "answer recap failed", e);
|
|
25362
|
+
}
|
|
25363
|
+
if (this.judging.get(worktreeId) !== turn.at)
|
|
25364
|
+
return;
|
|
25365
|
+
this.judging.delete(worktreeId);
|
|
25366
|
+
const record = this.d.state.worktree(worktreeId)?.lastTurn;
|
|
25367
|
+
if (!text || !record || record.at !== turn.at)
|
|
25368
|
+
return;
|
|
25369
|
+
record.recap = { at: Date.now(), text };
|
|
25370
|
+
log.info(worktreeId, `answer recap, ${Date.now() - started}ms`);
|
|
25371
|
+
this.d.state.save();
|
|
25372
|
+
this.d.hub.emit("worktreesChanged");
|
|
25373
|
+
}
|
|
25374
|
+
async judge(worktreeId) {
|
|
25375
|
+
const wt = this.d.state.requireWorktree(worktreeId);
|
|
25376
|
+
if (!canLand(wt))
|
|
25377
|
+
throw new UserError("nothing to land from here");
|
|
25378
|
+
if (this.busy.has(worktreeId))
|
|
25379
|
+
throw new UserError("wait for the turn to end");
|
|
25380
|
+
if (!await this.hasWork(wt))
|
|
25381
|
+
throw new UserError("nothing to check: no changes here");
|
|
25382
|
+
fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: true, quiet: false }), "landing verdict by hand");
|
|
25383
|
+
}
|
|
25384
|
+
recheck(worktreeId) {
|
|
25385
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
25386
|
+
if (!wt?.landing || !canLand(wt) || this.busy.has(worktreeId))
|
|
25387
|
+
return;
|
|
25388
|
+
fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
|
|
25389
|
+
}
|
|
25390
|
+
async hasWork(wt) {
|
|
24830
25391
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
24831
25392
|
const files = await statusFiles(wt.path);
|
|
25393
|
+
if (files.length > 0)
|
|
25394
|
+
return true;
|
|
24832
25395
|
const { ahead } = await aheadBehind(wt.path, repo.defaultBranch);
|
|
24833
|
-
|
|
25396
|
+
return ahead > 0;
|
|
25397
|
+
}
|
|
25398
|
+
async run(wt, opts) {
|
|
25399
|
+
const worktreeId = wt.id;
|
|
25400
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
25401
|
+
if (!await this.hasWork(wt))
|
|
24834
25402
|
return this.clear(worktreeId);
|
|
24835
|
-
|
|
25403
|
+
const held = opts.ask ? undefined : wt.landing;
|
|
25404
|
+
const kept = {
|
|
25405
|
+
...held?.why ? { why: held.why } : {},
|
|
25406
|
+
...held?.subject ? { subject: held.subject } : {},
|
|
25407
|
+
...held?.body ? { body: held.body } : {}
|
|
25408
|
+
};
|
|
25409
|
+
this.judging.set(worktreeId, opts.at);
|
|
24836
25410
|
const started = Date.now();
|
|
24837
|
-
this.d.worktrees.setLanding(worktreeId, { at:
|
|
24838
|
-
const live = () => this.judging.get(worktreeId) ===
|
|
25411
|
+
this.d.worktrees.setLanding(worktreeId, { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept });
|
|
25412
|
+
const live = () => this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
|
|
24839
25413
|
let check = "none";
|
|
24840
25414
|
let checkTail;
|
|
24841
25415
|
const command = repo.config.check?.trim();
|
|
24842
25416
|
if (command) {
|
|
24843
|
-
const r = await this.d.check(worktreeId, command);
|
|
25417
|
+
const r = await this.d.check(worktreeId, command, { quiet: opts.quiet });
|
|
24844
25418
|
if (!live())
|
|
24845
25419
|
return;
|
|
24846
25420
|
check = r.exit === 0 ? "pass" : "fail";
|
|
24847
25421
|
if (check === "fail")
|
|
24848
|
-
checkTail =
|
|
25422
|
+
checkTail = tail(r.text) || `exit ${r.exit}`;
|
|
24849
25423
|
}
|
|
24850
25424
|
let verdict = null;
|
|
24851
|
-
if (check !== "fail" && this.d.judge) {
|
|
25425
|
+
if (opts.ask && check !== "fail" && this.d.judge) {
|
|
24852
25426
|
const entries = this.d.transcript(worktreeId);
|
|
24853
25427
|
const prompt = landPrompt({
|
|
24854
25428
|
title: wt.title,
|
|
@@ -24866,10 +25440,11 @@ class LandingService {
|
|
|
24866
25440
|
return;
|
|
24867
25441
|
}
|
|
24868
25442
|
const landing = {
|
|
24869
|
-
at:
|
|
25443
|
+
at: opts.at,
|
|
24870
25444
|
check,
|
|
24871
25445
|
...checkTail ? { checkTail } : {},
|
|
24872
25446
|
ready: check !== "fail",
|
|
25447
|
+
...kept,
|
|
24873
25448
|
...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
|
|
24874
25449
|
...verdict?.subject ? { subject: verdict.subject } : {},
|
|
24875
25450
|
...verdict?.body ? { body: verdict.body } : {},
|
|
@@ -24878,11 +25453,14 @@ class LandingService {
|
|
|
24878
25453
|
if (!live())
|
|
24879
25454
|
return;
|
|
24880
25455
|
this.judging.delete(worktreeId);
|
|
24881
|
-
|
|
25456
|
+
const record = this.d.state.worktree(worktreeId)?.lastTurn;
|
|
25457
|
+
if (record && verdict?.recap)
|
|
25458
|
+
record.recap = { at: Date.now(), text: verdict.recap };
|
|
25459
|
+
log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms`);
|
|
24882
25460
|
this.d.worktrees.setLanding(worktreeId, landing);
|
|
24883
25461
|
}
|
|
24884
25462
|
}
|
|
24885
|
-
function
|
|
25463
|
+
function tail(text) {
|
|
24886
25464
|
const t = text.replace(/\s+$/, "");
|
|
24887
25465
|
return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
|
|
24888
25466
|
}
|
|
@@ -25092,11 +25670,8 @@ function discoveredId(path) {
|
|
|
25092
25670
|
}
|
|
25093
25671
|
function knownPaths(known) {
|
|
25094
25672
|
const paths = new Set;
|
|
25095
|
-
for (const wt of known)
|
|
25673
|
+
for (const wt of known)
|
|
25096
25674
|
paths.add(canonical(wt.path));
|
|
25097
|
-
if (wt.linkPath)
|
|
25098
|
-
paths.add(canonical(wt.linkPath));
|
|
25099
|
-
}
|
|
25100
25675
|
return paths;
|
|
25101
25676
|
}
|
|
25102
25677
|
function subtractKnown(repoId, listed, known) {
|
|
@@ -25164,7 +25739,7 @@ class RefSearch {
|
|
|
25164
25739
|
rowIdForPath = (path) => {
|
|
25165
25740
|
const want = canonical(path);
|
|
25166
25741
|
for (const wt of this.d.state.worktrees) {
|
|
25167
|
-
if (canonical(wt.path) === want
|
|
25742
|
+
if (canonical(wt.path) === want)
|
|
25168
25743
|
return wt.id;
|
|
25169
25744
|
}
|
|
25170
25745
|
return discoveredId(path);
|
|
@@ -25274,8 +25849,8 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
25274
25849
|
}
|
|
25275
25850
|
|
|
25276
25851
|
// ../daemon/src/worktrees/service.ts
|
|
25277
|
-
import { existsSync as
|
|
25278
|
-
import {
|
|
25852
|
+
import { cpSync, existsSync as existsSync26, readFileSync as readFileSync16, rmSync as rmSync5 } from "fs";
|
|
25853
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
25279
25854
|
|
|
25280
25855
|
// ../daemon/src/git/archive.ts
|
|
25281
25856
|
import { rmSync as rmSync3 } from "fs";
|
|
@@ -25345,6 +25920,32 @@ async function landingMark(worktreePath, defaultBr) {
|
|
|
25345
25920
|
return null;
|
|
25346
25921
|
return { base: base.out, tip: tip.out };
|
|
25347
25922
|
}
|
|
25923
|
+
async function handLanding(worktreePath, branch, defaultBr, since) {
|
|
25924
|
+
const log = await git(worktreePath, "reflog", "show", "--format=%H %gs", branch);
|
|
25925
|
+
if (!log.ok || !log.out)
|
|
25926
|
+
return null;
|
|
25927
|
+
let own = 0;
|
|
25928
|
+
for (const line of log.out.split(`
|
|
25929
|
+
`)) {
|
|
25930
|
+
const subject = line.slice(line.indexOf(" ") + 1);
|
|
25931
|
+
if (/^(reset|branch):/.test(subject))
|
|
25932
|
+
break;
|
|
25933
|
+
if (/^(commit|cherry-pick)\b/.test(subject) && !subject.startsWith("commit (amend)"))
|
|
25934
|
+
own++;
|
|
25935
|
+
}
|
|
25936
|
+
if (own === 0)
|
|
25937
|
+
return null;
|
|
25938
|
+
const [tip, walked, onMain, fromSince] = await Promise.all([
|
|
25939
|
+
git(worktreePath, "rev-parse", "HEAD"),
|
|
25940
|
+
git(worktreePath, "rev-parse", `HEAD~${own}`),
|
|
25941
|
+
git(worktreePath, "merge-base", "--is-ancestor", "HEAD", defaultBr),
|
|
25942
|
+
since ? git(worktreePath, "merge-base", "--is-ancestor", since, "HEAD") : null
|
|
25943
|
+
]);
|
|
25944
|
+
if (!tip.ok || !onMain.ok || !walked.ok)
|
|
25945
|
+
return null;
|
|
25946
|
+
const base = fromSince?.ok && since ? since : walked.out;
|
|
25947
|
+
return base === tip.out ? null : { base, tip: tip.out };
|
|
25948
|
+
}
|
|
25348
25949
|
async function dropLandRefs(repoPath, worktreeId) {
|
|
25349
25950
|
const r = await git(repoPath, "for-each-ref", "--format=%(refname)", `refs/toyon/lands/${worktreeId}/`);
|
|
25350
25951
|
if (!r.ok || !r.out)
|
|
@@ -25360,8 +25961,11 @@ async function commitWorktree(worktreePath, message) {
|
|
|
25360
25961
|
return { ok: false, message: "nothing to commit" };
|
|
25361
25962
|
await git(worktreePath, "add", "-A");
|
|
25362
25963
|
const c = await git(worktreePath, "commit", "-m", message);
|
|
25363
|
-
if (!c.ok)
|
|
25364
|
-
|
|
25964
|
+
if (!c.ok) {
|
|
25965
|
+
const output = [c.out, c.err].filter(Boolean).join(`
|
|
25966
|
+
`);
|
|
25967
|
+
return { ok: false, message: `commit failed: ${c.err.slice(0, 200)}`, output, exit: c.exit };
|
|
25968
|
+
}
|
|
25365
25969
|
return { ok: true, message: `committed: ${message}` };
|
|
25366
25970
|
}
|
|
25367
25971
|
async function requireClean(worktreePath) {
|
|
@@ -25467,21 +26071,37 @@ async function fastForwardMain(repoPath, defaultBr) {
|
|
|
25467
26071
|
const f = await run(GIT, ["fetch", "--quiet"], repoPath, NO_PROMPT);
|
|
25468
26072
|
if (!f.ok)
|
|
25469
26073
|
return { ok: false, message: `fetch failed: ${f.err.slice(0, 200)}` };
|
|
26074
|
+
return fastForwardFetched(repoPath, defaultBr);
|
|
26075
|
+
}
|
|
26076
|
+
async function fastForwardFetched(repoPath, defaultBr) {
|
|
26077
|
+
const current = await git(repoPath, "branch", "--show-current");
|
|
26078
|
+
if (current.out !== defaultBr) {
|
|
26079
|
+
return { ok: false, message: `main checkout is on '${current.out}', not ${defaultBr}; switch it first` };
|
|
26080
|
+
}
|
|
25470
26081
|
const behind = await behindUpstream(repoPath);
|
|
25471
|
-
if (behind === null)
|
|
25472
|
-
return { ok: false, message: `${defaultBr} has no upstream to pull from` };
|
|
26082
|
+
if (behind === null) {
|
|
26083
|
+
return { ok: false, stale: "no-upstream", message: `${defaultBr} has no upstream to pull from` };
|
|
26084
|
+
}
|
|
25473
26085
|
if (behind === 0)
|
|
25474
|
-
return { ok: true, message: "already up to date with origin" };
|
|
26086
|
+
return { ok: true, moved: false, message: "already up to date with origin" };
|
|
26087
|
+
if ((await statusFiles(repoPath)).length > 0) {
|
|
26088
|
+
return {
|
|
26089
|
+
ok: false,
|
|
26090
|
+
stale: "dirty",
|
|
26091
|
+
message: `${defaultBr} has uncommitted changes: commit or stash them there first`
|
|
26092
|
+
};
|
|
26093
|
+
}
|
|
25475
26094
|
const m = await git(repoPath, "merge", "--ff-only", "@{upstream}");
|
|
25476
26095
|
if (!m.ok) {
|
|
25477
26096
|
const clobber = /overwritten by merge|would be overwritten/.test(`${m.out}
|
|
25478
26097
|
${m.err}`);
|
|
25479
|
-
return {
|
|
26098
|
+
return clobber ? {
|
|
25480
26099
|
ok: false,
|
|
25481
|
-
|
|
25482
|
-
|
|
26100
|
+
stale: "dirty",
|
|
26101
|
+
message: `${defaultBr} here has edits the pull would overwrite; commit or stash them first`
|
|
26102
|
+
} : { ok: false, stale: "diverged", message: `${defaultBr} has diverged from origin; reconcile it in a terminal` };
|
|
25483
26103
|
}
|
|
25484
|
-
return { ok: true, message: `pulled ${behind} commit(s) from origin` };
|
|
26104
|
+
return { ok: true, moved: true, message: `pulled ${behind} commit(s) from origin` };
|
|
25485
26105
|
}
|
|
25486
26106
|
async function pushMain(repoPath, defaultBr) {
|
|
25487
26107
|
const p = await run(GIT, ["push", "origin", defaultBr], repoPath, NO_PROMPT);
|
|
@@ -25503,6 +26123,12 @@ async function mergeFailure(cwd, m, conflictMessage) {
|
|
|
25503
26123
|
}
|
|
25504
26124
|
return { ok: false, message: `merge failed: ${said.slice(0, 300)}` };
|
|
25505
26125
|
}
|
|
26126
|
+
async function pushBranch(worktreePath, branch) {
|
|
26127
|
+
const push = await run(GIT, ["push", "-u", "--force-with-lease", "origin", branch], worktreePath, NO_PROMPT);
|
|
26128
|
+
if (!push.ok)
|
|
26129
|
+
return { ok: false, message: `push failed: ${push.err.slice(0, 300)}` };
|
|
26130
|
+
return { ok: true, message: `pushed ${branch}` };
|
|
26131
|
+
}
|
|
25506
26132
|
async function openPr(o) {
|
|
25507
26133
|
const cErr = await requireClean(o.worktreePath);
|
|
25508
26134
|
if (cErr)
|
|
@@ -25513,9 +26139,9 @@ async function openPr(o) {
|
|
|
25513
26139
|
const remote = await git(o.worktreePath, "remote", "get-url", "origin");
|
|
25514
26140
|
if (!remote.ok)
|
|
25515
26141
|
return { ok: false, message: "no 'origin' remote: add one, or land by merging here" };
|
|
25516
|
-
const push = await
|
|
26142
|
+
const push = await pushBranch(o.worktreePath, o.branch);
|
|
25517
26143
|
if (!push.ok)
|
|
25518
|
-
return
|
|
26144
|
+
return push;
|
|
25519
26145
|
const titled = o.subject ? ["--title", o.subject, "--body", o.body ?? ""] : ["--fill"];
|
|
25520
26146
|
const created = await gh(["pr", "create", "--head", o.branch, "--base", o.defaultBr, ...titled], o.worktreePath);
|
|
25521
26147
|
let url = null;
|
|
@@ -25565,17 +26191,17 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
25565
26191
|
}
|
|
25566
26192
|
|
|
25567
26193
|
// ../daemon/src/worktrees/archive.ts
|
|
25568
|
-
import { existsSync as
|
|
25569
|
-
import { join as
|
|
26194
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
26195
|
+
import { basename as basename6, dirname as dirname8, join as join21 } from "path";
|
|
25570
26196
|
class WorktreeArchive {
|
|
25571
26197
|
dir;
|
|
25572
26198
|
records = new Map;
|
|
25573
26199
|
constructor(dir) {
|
|
25574
26200
|
this.dir = dir;
|
|
25575
26201
|
mkdirSync7(dir, { recursive: true });
|
|
25576
|
-
for (const id of
|
|
26202
|
+
for (const id of readdirSync7(dir)) {
|
|
25577
26203
|
const file = this.filesOf(id).record;
|
|
25578
|
-
if (!
|
|
26204
|
+
if (!existsSync24(file))
|
|
25579
26205
|
continue;
|
|
25580
26206
|
try {
|
|
25581
26207
|
this.records.set(id, JSON.parse(readFileSync15(file, "utf8")));
|
|
@@ -25592,8 +26218,10 @@ class WorktreeArchive {
|
|
|
25592
26218
|
mkdirSync7(at.dir, { recursive: true });
|
|
25593
26219
|
move(from.transcript, at.transcript);
|
|
25594
26220
|
move(from.attachments, at.attachments);
|
|
26221
|
+
move(from.plans, at.plans);
|
|
25595
26222
|
writeFileSync9(at.record, JSON.stringify(rec, null, 2));
|
|
25596
26223
|
this.records.set(rec.worktree.id, rec);
|
|
26224
|
+
return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
|
|
25597
26225
|
}
|
|
25598
26226
|
take(id, to) {
|
|
25599
26227
|
if (!this.records.has(id))
|
|
@@ -25601,6 +26229,7 @@ class WorktreeArchive {
|
|
|
25601
26229
|
const at = this.filesOf(id);
|
|
25602
26230
|
move(at.transcript, to.transcript);
|
|
25603
26231
|
move(at.attachments, to.attachments);
|
|
26232
|
+
move(at.plans, to.plans);
|
|
25604
26233
|
this.delete(id);
|
|
25605
26234
|
}
|
|
25606
26235
|
delete(id) {
|
|
@@ -25610,25 +26239,39 @@ class WorktreeArchive {
|
|
|
25610
26239
|
this.records.delete(id);
|
|
25611
26240
|
}
|
|
25612
26241
|
list(repo) {
|
|
25613
|
-
return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id));
|
|
26242
|
+
return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id, this.filesOf(r.worktree.id).transcript));
|
|
25614
26243
|
}
|
|
25615
26244
|
chatFiles(id) {
|
|
25616
26245
|
if (!this.records.has(id))
|
|
25617
26246
|
return;
|
|
25618
26247
|
const at = this.filesOf(id);
|
|
25619
|
-
return { transcript: at.transcript, attachments: at.attachments };
|
|
26248
|
+
return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
|
|
26249
|
+
}
|
|
26250
|
+
planFile(id, rel) {
|
|
26251
|
+
if (!this.records.has(id) || !isPlanPath(rel))
|
|
26252
|
+
return null;
|
|
26253
|
+
const file = join21(this.filesOf(id).plans, basename6(rel));
|
|
26254
|
+
if (!existsSync24(file))
|
|
26255
|
+
return null;
|
|
26256
|
+
try {
|
|
26257
|
+
return readFileSync15(file, "utf8");
|
|
26258
|
+
} catch (e) {
|
|
26259
|
+
log.warn(id, `could not read its archived plan ${rel}`, e);
|
|
26260
|
+
return null;
|
|
26261
|
+
}
|
|
25620
26262
|
}
|
|
25621
26263
|
filesOf(id) {
|
|
25622
|
-
const dir =
|
|
26264
|
+
const dir = join21(this.dir, id);
|
|
25623
26265
|
return {
|
|
25624
26266
|
dir,
|
|
25625
|
-
record:
|
|
25626
|
-
transcript:
|
|
25627
|
-
attachments:
|
|
26267
|
+
record: join21(dir, "record.json"),
|
|
26268
|
+
transcript: join21(dir, "transcript.jsonl"),
|
|
26269
|
+
attachments: join21(dir, "attachments"),
|
|
26270
|
+
plans: join21(dir, "plans")
|
|
25628
26271
|
};
|
|
25629
26272
|
}
|
|
25630
26273
|
}
|
|
25631
|
-
function summarize(r, repoId) {
|
|
26274
|
+
function summarize(r, repoId, transcript) {
|
|
25632
26275
|
return {
|
|
25633
26276
|
id: r.worktree.id,
|
|
25634
26277
|
repoId,
|
|
@@ -25639,6 +26282,8 @@ function summarize(r, repoId) {
|
|
|
25639
26282
|
archivedAt: r.archivedAt,
|
|
25640
26283
|
...r.prompt ? { prompt: r.prompt } : {},
|
|
25641
26284
|
restorable: !!r.kept,
|
|
26285
|
+
transcript,
|
|
26286
|
+
...r.sessionId ? { sessionId: r.sessionId } : {},
|
|
25642
26287
|
...r.kept?.snapshot ? { uncommitted: true, ...r.kept.dirty ? { dirty: r.kept.dirty } : {} } : {},
|
|
25643
26288
|
...r.worktree.landed ? { landed: true } : {},
|
|
25644
26289
|
...r.cost !== undefined ? { cost: r.cost } : {},
|
|
@@ -25646,7 +26291,7 @@ function summarize(r, repoId) {
|
|
|
25646
26291
|
};
|
|
25647
26292
|
}
|
|
25648
26293
|
function lastUsage(transcript) {
|
|
25649
|
-
if (!
|
|
26294
|
+
if (!existsSync24(transcript))
|
|
25650
26295
|
return null;
|
|
25651
26296
|
const lines = readFileSync15(transcript, "utf8").split(`
|
|
25652
26297
|
`);
|
|
@@ -25662,7 +26307,7 @@ function lastUsage(transcript) {
|
|
|
25662
26307
|
return null;
|
|
25663
26308
|
}
|
|
25664
26309
|
function firstPrompt(transcript) {
|
|
25665
|
-
if (!
|
|
26310
|
+
if (!existsSync24(transcript))
|
|
25666
26311
|
return;
|
|
25667
26312
|
for (const line of readFileSync15(transcript, "utf8").split(`
|
|
25668
26313
|
`)) {
|
|
@@ -25677,8 +26322,9 @@ function firstPrompt(transcript) {
|
|
|
25677
26322
|
return;
|
|
25678
26323
|
}
|
|
25679
26324
|
function move(from, to) {
|
|
25680
|
-
if (!
|
|
26325
|
+
if (!existsSync24(from))
|
|
25681
26326
|
return;
|
|
26327
|
+
mkdirSync7(dirname8(to), { recursive: true });
|
|
25682
26328
|
rmSync4(to, { recursive: true, force: true });
|
|
25683
26329
|
renameSync4(from, to);
|
|
25684
26330
|
}
|
|
@@ -25741,16 +26387,31 @@ class ArchivedGit {
|
|
|
25741
26387
|
}
|
|
25742
26388
|
|
|
25743
26389
|
// ../daemon/src/worktrees/spare.ts
|
|
25744
|
-
import {
|
|
26390
|
+
import { existsSync as existsSync25 } from "fs";
|
|
26391
|
+
import { basename as basename7, join as join22 } from "path";
|
|
25745
26392
|
class SparePool {
|
|
25746
26393
|
d;
|
|
25747
|
-
|
|
26394
|
+
inflight = new Map;
|
|
26395
|
+
refreshing = new Map;
|
|
25748
26396
|
constructor(d) {
|
|
25749
26397
|
this.d = d;
|
|
25750
26398
|
}
|
|
26399
|
+
spareOf(repoId) {
|
|
26400
|
+
return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
|
|
26401
|
+
}
|
|
25751
26402
|
current(repoId) {
|
|
25752
|
-
const
|
|
25753
|
-
return
|
|
26403
|
+
const wt = this.spareOf(repoId);
|
|
26404
|
+
return wt ? { worktreeId: wt.id, ready: wt.phase === "ready" } : null;
|
|
26405
|
+
}
|
|
26406
|
+
typed(boxId, text) {
|
|
26407
|
+
if (!text.trim())
|
|
26408
|
+
return;
|
|
26409
|
+
const wt = this.d.state.worktree(boxId);
|
|
26410
|
+
if (wt?.kind !== "spare" || !existsSync25(wt.path))
|
|
26411
|
+
return;
|
|
26412
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
26413
|
+
if (agent.runningAgent === null)
|
|
26414
|
+
fireAndForget(wt.id, agent.warm(), "warm on the first keystroke");
|
|
25754
26415
|
}
|
|
25755
26416
|
adopt(repoId) {
|
|
25756
26417
|
const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
|
|
@@ -25759,76 +26420,125 @@ class SparePool {
|
|
|
25759
26420
|
const spare = persisted[0];
|
|
25760
26421
|
if (!spare)
|
|
25761
26422
|
return;
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
lockHash: lockfileHash(spare.path),
|
|
25765
|
-
refreshing: null,
|
|
25766
|
-
ready: true
|
|
25767
|
-
});
|
|
26423
|
+
spare.phase ??= "ready";
|
|
26424
|
+
spare.lockfile ??= lockfileHash(spare.path);
|
|
25768
26425
|
}
|
|
25769
26426
|
warm(repoId) {
|
|
25770
|
-
|
|
26427
|
+
const wt = this.spareOf(repoId);
|
|
26428
|
+
if (wt?.phase === "ready")
|
|
25771
26429
|
fireAndForget(repoId, this.revive(repoId), "spare refresh");
|
|
25772
26430
|
else
|
|
25773
26431
|
fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
|
|
25774
26432
|
}
|
|
25775
26433
|
async revive(repoId) {
|
|
25776
26434
|
await this.refresh(repoId);
|
|
25777
|
-
const
|
|
25778
|
-
|
|
25779
|
-
if (wt?.kind !== "spare" || this.d.runtime.get(wt.id)?.procs)
|
|
26435
|
+
const wt = this.spareOf(repoId);
|
|
26436
|
+
if (wt?.phase !== "ready" || this.d.runtime.get(wt.id)?.procs)
|
|
25780
26437
|
return;
|
|
25781
26438
|
await this.d.runtime.start(wt, this.d.state.requireRepo(repoId));
|
|
25782
26439
|
}
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
26440
|
+
ensure(repoId) {
|
|
26441
|
+
const running = this.inflight.get(repoId);
|
|
26442
|
+
if (running)
|
|
26443
|
+
return running.done;
|
|
26444
|
+
const have = this.spareOf(repoId);
|
|
26445
|
+
if (have?.phase === "ready")
|
|
26446
|
+
return Promise.resolve();
|
|
25786
26447
|
const repo = this.d.state.requireRepo(repoId);
|
|
26448
|
+
if (have)
|
|
26449
|
+
return this.warmUp(have, repo, {});
|
|
25787
26450
|
if (repo.needsSetup)
|
|
25788
|
-
return;
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
25802
|
-
|
|
25803
|
-
|
|
25804
|
-
|
|
25805
|
-
|
|
25806
|
-
|
|
25807
|
-
|
|
25808
|
-
|
|
25809
|
-
|
|
25810
|
-
|
|
25811
|
-
|
|
25812
|
-
|
|
25813
|
-
|
|
25814
|
-
|
|
25815
|
-
|
|
26451
|
+
return Promise.resolve();
|
|
26452
|
+
return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
|
|
26453
|
+
}
|
|
26454
|
+
async record(repoId, repo) {
|
|
26455
|
+
const { id, dir } = freeSlot(join22(this.d.paths.worktreesDir, repo.name));
|
|
26456
|
+
const wt = {
|
|
26457
|
+
id,
|
|
26458
|
+
repoId,
|
|
26459
|
+
path: join22(this.d.paths.worktreesDir, repo.name, dir),
|
|
26460
|
+
branch: repo.defaultBranch,
|
|
26461
|
+
kind: "spare",
|
|
26462
|
+
phase: "reserved",
|
|
26463
|
+
proxyPort: await allocateProxyPort(),
|
|
26464
|
+
title: dir,
|
|
26465
|
+
createdAt: Date.now()
|
|
26466
|
+
};
|
|
26467
|
+
this.d.state.addWorktree(wt);
|
|
26468
|
+
return wt;
|
|
26469
|
+
}
|
|
26470
|
+
warmUp(wt, repo, { after }) {
|
|
26471
|
+
const repoId = repo.id;
|
|
26472
|
+
let cut = () => {};
|
|
26473
|
+
const added = (async () => {
|
|
26474
|
+
if (existsSync25(wt.path))
|
|
26475
|
+
return;
|
|
26476
|
+
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, repo.defaultBranch));
|
|
26477
|
+
})();
|
|
26478
|
+
const done = (async () => {
|
|
26479
|
+
try {
|
|
26480
|
+
await added;
|
|
26481
|
+
if (after) {
|
|
26482
|
+
await Promise.race([
|
|
26483
|
+
this.d.runtime.awaitPreview(after, STAGGER_MS),
|
|
26484
|
+
new Promise((r) => {
|
|
26485
|
+
cut = r;
|
|
26486
|
+
})
|
|
26487
|
+
]);
|
|
26488
|
+
}
|
|
26489
|
+
if (!this.d.state.worktree(wt.id))
|
|
26490
|
+
return;
|
|
26491
|
+
if (wt.kind === "spare") {
|
|
26492
|
+
wt.phase = "warming";
|
|
26493
|
+
this.d.state.save();
|
|
26494
|
+
this.d.hub.emit("worktreesChanged");
|
|
26495
|
+
await withRepoLock(repo.path, () => git(wt.path, "reset", "--hard", repo.defaultBranch));
|
|
26496
|
+
}
|
|
26497
|
+
await this.d.setupAndStart(wt, repo);
|
|
26498
|
+
if (wt.kind !== "spare")
|
|
26499
|
+
return;
|
|
26500
|
+
wt.lockfile = lockfileHash(wt.path);
|
|
26501
|
+
wt.phase = "ready";
|
|
26502
|
+
this.d.state.save();
|
|
26503
|
+
this.d.ready?.(repoId);
|
|
26504
|
+
this.d.hub.emit("worktreesChanged");
|
|
26505
|
+
} catch (e) {
|
|
26506
|
+
await this.rollBack(wt, e);
|
|
26507
|
+
} finally {
|
|
26508
|
+
if (this.inflight.get(repoId)?.added === added)
|
|
26509
|
+
this.inflight.delete(repoId);
|
|
25816
26510
|
}
|
|
26511
|
+
})();
|
|
26512
|
+
this.inflight.set(repoId, { added, done, cut: () => cut() });
|
|
26513
|
+
return done;
|
|
26514
|
+
}
|
|
26515
|
+
async rollBack(wt, e) {
|
|
26516
|
+
if (wt.kind !== "spare") {
|
|
26517
|
+
log.warn(wt.id, "setup failed after the spare was claimed", e);
|
|
26518
|
+
return;
|
|
26519
|
+
}
|
|
26520
|
+
log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
|
|
26521
|
+
this.d.carryDraft?.(wt.id, wt.repoId);
|
|
26522
|
+
if (existsSync25(wt.path)) {
|
|
26523
|
+
await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
|
|
26524
|
+
return;
|
|
25817
26525
|
}
|
|
26526
|
+
this.d.state.removeWorktree(wt.id);
|
|
26527
|
+
releasePort(wt.proxyPort);
|
|
26528
|
+
this.d.state.save();
|
|
26529
|
+
this.d.hub.emit("worktreesChanged");
|
|
25818
26530
|
}
|
|
25819
26531
|
async refresh(repoId) {
|
|
25820
|
-
const
|
|
25821
|
-
if (
|
|
26532
|
+
const wt = this.spareOf(repoId);
|
|
26533
|
+
if (wt?.phase !== "ready" || this.refreshing.has(repoId))
|
|
25822
26534
|
return;
|
|
25823
26535
|
const repo = this.d.state.requireRepo(repoId);
|
|
25824
|
-
const
|
|
25825
|
-
if (wt?.kind !== "spare")
|
|
25826
|
-
return;
|
|
25827
|
-
entry.refreshing = (async () => {
|
|
26536
|
+
const run = (async () => {
|
|
25828
26537
|
await withRepoLock(repo.path, () => git(wt.path, "reset", "--hard", repo.defaultBranch));
|
|
25829
26538
|
const h = lockfileHash(wt.path);
|
|
25830
|
-
if (h !==
|
|
25831
|
-
|
|
26539
|
+
if (h !== wt.lockfile) {
|
|
26540
|
+
wt.lockfile = h;
|
|
26541
|
+
this.d.state.save();
|
|
25832
26542
|
for (const cmd of repo.config.setup ?? []) {
|
|
25833
26543
|
const tail = [];
|
|
25834
26544
|
const code = await runSetup(cmd, wt.path, (line) => {
|
|
@@ -25842,70 +26552,161 @@ class SparePool {
|
|
|
25842
26552
|
}
|
|
25843
26553
|
}
|
|
25844
26554
|
})().finally(() => {
|
|
25845
|
-
|
|
26555
|
+
this.refreshing.delete(repoId);
|
|
25846
26556
|
});
|
|
25847
|
-
|
|
26557
|
+
this.refreshing.set(repoId, run);
|
|
26558
|
+
await run;
|
|
25848
26559
|
}
|
|
25849
|
-
async claim(repoId,
|
|
25850
|
-
const
|
|
25851
|
-
if (!
|
|
26560
|
+
async claim(repoId, title, worktreeId) {
|
|
26561
|
+
const wt = this.spareOf(repoId);
|
|
26562
|
+
if (!wt)
|
|
25852
26563
|
return null;
|
|
25853
|
-
|
|
25854
|
-
if (entry.refreshing)
|
|
25855
|
-
await entry.refreshing;
|
|
25856
|
-
const wt = this.d.state.worktree(entry.worktreeId);
|
|
25857
|
-
if (wt?.kind !== "spare")
|
|
26564
|
+
if (worktreeId && wt.id !== worktreeId)
|
|
25858
26565
|
return null;
|
|
25859
|
-
const
|
|
25860
|
-
|
|
25861
|
-
|
|
25862
|
-
|
|
25863
|
-
this.
|
|
25864
|
-
|
|
26566
|
+
const wasReady = wt.phase === "ready";
|
|
26567
|
+
if (!wasReady) {
|
|
26568
|
+
if (!worktreeId)
|
|
26569
|
+
return null;
|
|
26570
|
+
const running = this.inflight.get(repoId);
|
|
26571
|
+
await running?.added.catch(() => {});
|
|
26572
|
+
running?.cut();
|
|
26573
|
+
if (wt.kind !== "spare" || !existsSync25(wt.path))
|
|
26574
|
+
return null;
|
|
25865
26575
|
}
|
|
26576
|
+
await this.refreshing.get(repoId);
|
|
26577
|
+
const repo = this.d.state.requireRepo(repoId);
|
|
26578
|
+
const branch = `toyon/${basename7(wt.path)}`;
|
|
26579
|
+
await withRepoLock(repo.path, () => gitOrThrow(wt.path, "switch", "-c", branch));
|
|
25866
26580
|
wt.kind = "worktree";
|
|
26581
|
+
delete wt.phase;
|
|
26582
|
+
delete wt.lockfile;
|
|
25867
26583
|
wt.branch = branch;
|
|
25868
|
-
wt.title =
|
|
26584
|
+
wt.title = title;
|
|
26585
|
+
wt.unnamed = true;
|
|
25869
26586
|
wt.createdAt = Date.now();
|
|
26587
|
+
if (wasReady)
|
|
26588
|
+
fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
|
|
26589
|
+
const next = repo.needsSetup ? null : await this.record(repoId, repo);
|
|
25870
26590
|
this.d.state.save();
|
|
25871
|
-
|
|
25872
|
-
|
|
26591
|
+
if (next)
|
|
26592
|
+
fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
|
|
25873
26593
|
return wt;
|
|
25874
26594
|
}
|
|
25875
26595
|
}
|
|
25876
26596
|
|
|
26597
|
+
// ../daemon/src/worktrees/trunk.ts
|
|
26598
|
+
var FETCH_EVERY_MS = 5 * 60000;
|
|
26599
|
+
var OPEN_FETCH_MIN_MS = 60000;
|
|
26600
|
+
function mainOf(state, repoId) {
|
|
26601
|
+
return state.worktrees.find((w) => w.repoId === repoId && w.kind === "main");
|
|
26602
|
+
}
|
|
26603
|
+
|
|
26604
|
+
class Trunk {
|
|
26605
|
+
d;
|
|
26606
|
+
lastFetch = new Map;
|
|
26607
|
+
stale = new Map;
|
|
26608
|
+
constructor(d) {
|
|
26609
|
+
this.d = d;
|
|
26610
|
+
}
|
|
26611
|
+
async behind(mainId, path) {
|
|
26612
|
+
const behind = await behindUpstream(path);
|
|
26613
|
+
if (behind === null)
|
|
26614
|
+
return {};
|
|
26615
|
+
const last = this.lastFetch.get(path) ?? 0;
|
|
26616
|
+
if (Date.now() - last > FETCH_EVERY_MS) {
|
|
26617
|
+
this.lastFetch.set(path, Date.now());
|
|
26618
|
+
fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then(async (r) => {
|
|
26619
|
+
if (!r.ok) {
|
|
26620
|
+
log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
|
|
26621
|
+
return;
|
|
26622
|
+
}
|
|
26623
|
+
this.d.invalidateCounts();
|
|
26624
|
+
this.d.hub.emit("worktreesChanged");
|
|
26625
|
+
if (await behindUpstream(path) ?? 0)
|
|
26626
|
+
await this.follow(mainId);
|
|
26627
|
+
}));
|
|
26628
|
+
}
|
|
26629
|
+
return { behind };
|
|
26630
|
+
}
|
|
26631
|
+
async sync(repoId) {
|
|
26632
|
+
const repo = this.d.state.repo(repoId);
|
|
26633
|
+
const main = mainOf(this.d.state, repoId);
|
|
26634
|
+
if (!repo || !main)
|
|
26635
|
+
return;
|
|
26636
|
+
const last = this.lastFetch.get(repo.path) ?? 0;
|
|
26637
|
+
if (Date.now() - last < OPEN_FETCH_MIN_MS)
|
|
26638
|
+
return;
|
|
26639
|
+
this.lastFetch.set(repo.path, Date.now());
|
|
26640
|
+
const f = await run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT);
|
|
26641
|
+
if (!f.ok) {
|
|
26642
|
+
log.warn("fetch", `could not fetch ${repo.path}: ${f.err.slice(0, 200)}`);
|
|
26643
|
+
return;
|
|
26644
|
+
}
|
|
26645
|
+
this.d.invalidateCounts();
|
|
26646
|
+
await this.follow(main.id);
|
|
26647
|
+
}
|
|
26648
|
+
async follow(mainId) {
|
|
26649
|
+
const main = this.d.state.worktree(mainId);
|
|
26650
|
+
const repo = main && this.d.state.repo(main.repoId);
|
|
26651
|
+
if (!main || !repo)
|
|
26652
|
+
return;
|
|
26653
|
+
const ff = await withRepoLock(repo.path, () => fastForwardFetched(repo.path, repo.defaultBranch));
|
|
26654
|
+
const was = this.stale.get(repo.id);
|
|
26655
|
+
if (ff.stale)
|
|
26656
|
+
this.stale.set(repo.id, ff.stale);
|
|
26657
|
+
else
|
|
26658
|
+
this.stale.delete(repo.id);
|
|
26659
|
+
if (ff.moved) {
|
|
26660
|
+
this.d.invalidateCounts();
|
|
26661
|
+
this.d.hub.emit("worktreesChanged");
|
|
26662
|
+
} else if (was !== ff.stale)
|
|
26663
|
+
this.d.hub.emit("worktreesChanged");
|
|
26664
|
+
}
|
|
26665
|
+
async all(opts = {}) {
|
|
26666
|
+
const quick = opts.quick ? { missed: false } : null;
|
|
26667
|
+
const out = {};
|
|
26668
|
+
await Promise.all(this.d.state.repos.map(async (repo) => {
|
|
26669
|
+
const main = mainOf(this.d.state, repo.id);
|
|
26670
|
+
if (!main)
|
|
26671
|
+
return;
|
|
26672
|
+
const { behind, dirty } = await this.d.counts(main, repo.defaultBranch, quick);
|
|
26673
|
+
const stale = this.stale.get(repo.id);
|
|
26674
|
+
out[repo.id] = {
|
|
26675
|
+
id: main.id,
|
|
26676
|
+
...behind !== undefined ? { behind } : {},
|
|
26677
|
+
dirty: dirty ?? 0,
|
|
26678
|
+
empty: main.empty === true,
|
|
26679
|
+
...stale ? { stale } : {}
|
|
26680
|
+
};
|
|
26681
|
+
}));
|
|
26682
|
+
if (quick?.missed)
|
|
26683
|
+
setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
|
|
26684
|
+
return out;
|
|
26685
|
+
}
|
|
26686
|
+
}
|
|
26687
|
+
|
|
25877
26688
|
// ../daemon/src/worktrees/turns.ts
|
|
25878
26689
|
var TURN_HOLD = "turn";
|
|
25879
|
-
var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
|
|
25880
26690
|
|
|
25881
26691
|
class TurnService {
|
|
25882
26692
|
d;
|
|
25883
26693
|
lastStatus = new Map;
|
|
25884
26694
|
stoppedByHand = new Set;
|
|
25885
|
-
timers = new Map;
|
|
25886
|
-
summarized = new Map;
|
|
25887
26695
|
constructor(d) {
|
|
25888
26696
|
this.d = d;
|
|
25889
26697
|
this.boot();
|
|
25890
26698
|
d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
|
|
25891
26699
|
}
|
|
25892
26700
|
boot() {
|
|
25893
|
-
const now = Date.now();
|
|
25894
26701
|
let changed = false;
|
|
25895
26702
|
for (const wt of this.d.state.worktrees) {
|
|
25896
26703
|
const turn = wt.lastTurn;
|
|
25897
|
-
if (
|
|
26704
|
+
if (turn?.end !== "asking")
|
|
25898
26705
|
continue;
|
|
25899
|
-
|
|
25900
|
-
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
changed = true;
|
|
25904
|
-
}
|
|
25905
|
-
if (!turn.recap && (wt.seenAt ?? 0) < turn.at) {
|
|
25906
|
-
turn.recap = { at: now };
|
|
25907
|
-
changed = true;
|
|
25908
|
-
}
|
|
26706
|
+
const { ask: _expired, ...facts } = turn.facts;
|
|
26707
|
+
turn.end = "stopped";
|
|
26708
|
+
turn.facts = facts;
|
|
26709
|
+
changed = true;
|
|
25909
26710
|
}
|
|
25910
26711
|
if (changed)
|
|
25911
26712
|
this.d.state.save();
|
|
@@ -25913,10 +26714,8 @@ class TurnService {
|
|
|
25913
26714
|
onStatus(worktreeId, status) {
|
|
25914
26715
|
const prev = this.lastStatus.get(worktreeId) ?? "idle";
|
|
25915
26716
|
this.lastStatus.set(worktreeId, status);
|
|
25916
|
-
if (status === "working")
|
|
25917
|
-
this.disarm(worktreeId);
|
|
26717
|
+
if (status === "working")
|
|
25918
26718
|
this.d.hold?.(worktreeId, TURN_HOLD);
|
|
25919
|
-
}
|
|
25920
26719
|
const edge = stopOf(prev, status);
|
|
25921
26720
|
if (!edge)
|
|
25922
26721
|
return;
|
|
@@ -25931,64 +26730,13 @@ class TurnService {
|
|
|
25931
26730
|
wt.lastTurn = { at, end, facts: factsOf(turns, end, end === "asking" ? openAskOf(entries) : undefined) };
|
|
25932
26731
|
if (this.stoppedByHand.delete(worktreeId) && end === "stopped")
|
|
25933
26732
|
wt.seenAt = at;
|
|
25934
|
-
this.disarm(worktreeId);
|
|
25935
|
-
if ((wt.seenAt ?? 0) < at)
|
|
25936
|
-
this.arm(worktreeId, at);
|
|
25937
26733
|
this.d.state.save();
|
|
25938
26734
|
this.d.hub.emit("turnSettled", worktreeId, wt.lastTurn);
|
|
25939
26735
|
}
|
|
25940
|
-
arm(worktreeId, at) {
|
|
25941
|
-
const t = setTimeout(() => this.due(worktreeId, at), this.d.delayMs ?? RECAP_DELAY_MS);
|
|
25942
|
-
t.unref?.();
|
|
25943
|
-
this.timers.set(worktreeId, t);
|
|
25944
|
-
}
|
|
25945
|
-
disarm(worktreeId) {
|
|
25946
|
-
const t = this.timers.get(worktreeId);
|
|
25947
|
-
if (!t)
|
|
25948
|
-
return;
|
|
25949
|
-
clearTimeout(t);
|
|
25950
|
-
this.timers.delete(worktreeId);
|
|
25951
|
-
}
|
|
25952
|
-
due(worktreeId, at) {
|
|
25953
|
-
this.timers.delete(worktreeId);
|
|
25954
|
-
const wt = this.d.state.worktree(worktreeId);
|
|
25955
|
-
const turn = wt?.lastTurn;
|
|
25956
|
-
if (!wt || !turn || turn.at !== at || (wt.seenAt ?? 0) >= at)
|
|
25957
|
-
return;
|
|
25958
|
-
turn.recap = { at: Date.now() };
|
|
25959
|
-
this.d.state.save();
|
|
25960
|
-
this.d.hub.emit("worktreesChanged");
|
|
25961
|
-
const seenAt = wt.seenAt ?? 0;
|
|
25962
|
-
const summarize = this.d.summarize;
|
|
25963
|
-
if (!summarize || turn.facts.auth)
|
|
25964
|
-
return;
|
|
25965
|
-
if (this.summarized.get(worktreeId) === seenAt)
|
|
25966
|
-
return;
|
|
25967
|
-
this.summarized.set(worktreeId, seenAt);
|
|
25968
|
-
const entries = this.d.transcript(worktreeId);
|
|
25969
|
-
const prompt = recapPrompt({
|
|
25970
|
-
title: wt.title,
|
|
25971
|
-
firstAsk: firstAskOf(entries),
|
|
25972
|
-
turns: turnsSince(entries, seenAt),
|
|
25973
|
-
end: turn.end,
|
|
25974
|
-
facts: turn.facts
|
|
25975
|
-
});
|
|
25976
|
-
fireAndForget(worktreeId, this.write(worktreeId, at, summarize(wt, prompt)), "recap summary");
|
|
25977
|
-
}
|
|
25978
|
-
async write(worktreeId, at, sentence) {
|
|
25979
|
-
const text = await sentence;
|
|
25980
|
-
const turn = this.d.state.worktree(worktreeId)?.lastTurn;
|
|
25981
|
-
if (!text || turn?.at !== at || !turn.recap)
|
|
25982
|
-
return;
|
|
25983
|
-
turn.recap.text = text;
|
|
25984
|
-
this.d.state.save();
|
|
25985
|
-
this.d.hub.emit("worktreesChanged");
|
|
25986
|
-
}
|
|
25987
26736
|
stoppedByPerson(worktreeId) {
|
|
25988
26737
|
this.stoppedByHand.add(worktreeId);
|
|
25989
26738
|
}
|
|
25990
26739
|
markSeen(worktreeId) {
|
|
25991
|
-
this.disarm(worktreeId);
|
|
25992
26740
|
const wt = this.d.state.worktree(worktreeId);
|
|
25993
26741
|
if (!wt)
|
|
25994
26742
|
return;
|
|
@@ -26035,21 +26783,27 @@ function withCarry(context, c) {
|
|
|
26035
26783
|
const line = c.moved ? `This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.` : `${c.branch} has ${files} the person chose to leave there. This worktree does not have them.`;
|
|
26036
26784
|
return [...context ?? [], line];
|
|
26037
26785
|
}
|
|
26038
|
-
var FETCH_EVERY_MS = 5 * 60000;
|
|
26039
26786
|
|
|
26040
26787
|
class WorktreeService {
|
|
26041
26788
|
d;
|
|
26042
26789
|
spare;
|
|
26790
|
+
trunk;
|
|
26043
26791
|
archive;
|
|
26044
26792
|
archiving = new Map;
|
|
26045
26793
|
going = new Set;
|
|
26046
26794
|
countsCache = new Map;
|
|
26795
|
+
handChecked = new Map;
|
|
26047
26796
|
discoverCache = new Map;
|
|
26048
26797
|
usage = new Map;
|
|
26049
|
-
lastFetch = new Map;
|
|
26050
26798
|
constructor(d) {
|
|
26051
26799
|
this.d = d;
|
|
26052
26800
|
this.archive = new WorktreeArchive(d.paths.archiveDir);
|
|
26801
|
+
this.trunk = new Trunk({
|
|
26802
|
+
state: d.state,
|
|
26803
|
+
hub: d.hub,
|
|
26804
|
+
counts: (main, defaultBranch, quick) => quick ? Promise.resolve(this.countsQuick(main.id, quick)) : this.counts(main.id, main.path, defaultBranch, false),
|
|
26805
|
+
invalidateCounts: () => this.invalidateCounts()
|
|
26806
|
+
});
|
|
26053
26807
|
this.spare = new SparePool({
|
|
26054
26808
|
state: d.state,
|
|
26055
26809
|
hub: d.hub,
|
|
@@ -26058,10 +26812,28 @@ class WorktreeService {
|
|
|
26058
26812
|
setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
|
|
26059
26813
|
discard: async (id) => {
|
|
26060
26814
|
await this.discardWorktree(id);
|
|
26815
|
+
},
|
|
26816
|
+
carryDraft: (fromId, repoId) => {
|
|
26817
|
+
const text = this.d.drafts?.text(fromId);
|
|
26818
|
+
const main = this.mainOf(repoId);
|
|
26819
|
+
if (text && main)
|
|
26820
|
+
this.d.drafts?.set(main.id, text);
|
|
26821
|
+
},
|
|
26822
|
+
ready: (repoId) => {
|
|
26823
|
+
const main = this.mainOf(repoId);
|
|
26824
|
+
if (!main)
|
|
26825
|
+
return;
|
|
26826
|
+
if (this.d.runtime.get(main.id)?.procs) {
|
|
26827
|
+
fireAndForget(main.id, this.d.runtime.stopProcs(main.id), "main stops for the spare");
|
|
26828
|
+
}
|
|
26829
|
+
const text = this.d.drafts?.text(main.id);
|
|
26830
|
+
const spare = this.leadSpareOf(repoId);
|
|
26831
|
+
if (text && spare && !this.d.drafts?.text(spare)) {
|
|
26832
|
+
this.d.drafts?.set(spare, text);
|
|
26833
|
+
this.d.drafts?.drop(main.id);
|
|
26834
|
+
}
|
|
26061
26835
|
}
|
|
26062
26836
|
});
|
|
26063
|
-
for (const wt of d.state.worktrees)
|
|
26064
|
-
this.refreshLink(wt);
|
|
26065
26837
|
d.hub.on("agent", (worktreeId, _seq, event) => {
|
|
26066
26838
|
if (event.type !== "usage")
|
|
26067
26839
|
return;
|
|
@@ -26086,18 +26858,15 @@ class WorktreeService {
|
|
|
26086
26858
|
const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
|
|
26087
26859
|
const profile = this.checkProfile(repo, opts.profile);
|
|
26088
26860
|
const task = taskText(prompt, attachments);
|
|
26089
|
-
|
|
26090
|
-
|
|
26091
|
-
slug = `${slug}-${shortId().slice(0, 3)}`;
|
|
26092
|
-
}
|
|
26093
|
-
const branch = `toyon/${slug}`;
|
|
26861
|
+
const base = titleFrom(task);
|
|
26862
|
+
const title = variant ? `${base} v${variant.index}` : base;
|
|
26094
26863
|
if (opts.carry && variant && variant.of > 1) {
|
|
26095
26864
|
throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
|
|
26096
26865
|
}
|
|
26097
26866
|
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
26098
26867
|
|
|
26099
26868
|
`) : prompt;
|
|
26100
|
-
const claimed = await this.spare.claim(repoId,
|
|
26869
|
+
const claimed = await this.spare.claim(repoId, title, opts.worktreeId);
|
|
26101
26870
|
if (claimed) {
|
|
26102
26871
|
if (variant)
|
|
26103
26872
|
claimed.variant = variant;
|
|
@@ -26111,7 +26880,6 @@ class WorktreeService {
|
|
|
26111
26880
|
claimed.model = opts.model;
|
|
26112
26881
|
if (opts.effort)
|
|
26113
26882
|
claimed.effort = opts.effort;
|
|
26114
|
-
this.refreshLink(claimed);
|
|
26115
26883
|
if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
|
|
26116
26884
|
claimed.profile = profile;
|
|
26117
26885
|
this.restartProcs(claimed, repo);
|
|
@@ -26119,22 +26887,31 @@ class WorktreeService {
|
|
|
26119
26887
|
const carried = await this.carryMain(repo, claimed, !!opts.carry);
|
|
26120
26888
|
this.d.state.save();
|
|
26121
26889
|
this.d.hub.emit("worktreesChanged");
|
|
26122
|
-
this.d.runtime.ensureAgent(claimed)
|
|
26890
|
+
const rt = this.d.runtime.ensureAgent(claimed);
|
|
26891
|
+
if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
|
|
26892
|
+
await rt.agent.restart();
|
|
26893
|
+
rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
26123
26894
|
this.scheduleNaming(claimed, task, variant);
|
|
26124
26895
|
if (carried.unmoved)
|
|
26125
26896
|
throw new UserError(carried.unmoved);
|
|
26126
26897
|
return claimed;
|
|
26127
26898
|
}
|
|
26128
|
-
|
|
26899
|
+
if (!await isGitRepo(repo.path)) {
|
|
26900
|
+
throw new UserError(`git no longer reads ${repo.name} as a checkout, so there is nothing to start a worktree from. If \`git config core.bare\` says true there, \`git config --unset core.bare\` puts it back.`);
|
|
26901
|
+
}
|
|
26902
|
+
const { id, dir } = freeSlot(join23(this.d.paths.worktreesDir, repo.name));
|
|
26903
|
+
const wtPath = join23(this.d.paths.worktreesDir, repo.name, dir);
|
|
26904
|
+
const branch = `toyon/${dir}`;
|
|
26129
26905
|
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
|
|
26130
26906
|
const wt = {
|
|
26131
|
-
id
|
|
26907
|
+
id,
|
|
26132
26908
|
repoId,
|
|
26133
26909
|
path: wtPath,
|
|
26134
26910
|
branch,
|
|
26135
26911
|
kind: "worktree",
|
|
26136
26912
|
proxyPort: await allocateProxyPort(),
|
|
26137
|
-
title
|
|
26913
|
+
title,
|
|
26914
|
+
unnamed: true,
|
|
26138
26915
|
createdAt: Date.now(),
|
|
26139
26916
|
promptedAt: Date.now(),
|
|
26140
26917
|
agent,
|
|
@@ -26154,9 +26931,20 @@ class WorktreeService {
|
|
|
26154
26931
|
return wt;
|
|
26155
26932
|
}
|
|
26156
26933
|
async carryMain(repo, wt, move) {
|
|
26934
|
+
try {
|
|
26935
|
+
return await this.carryMainOrThrow(repo, wt, move);
|
|
26936
|
+
} catch (e) {
|
|
26937
|
+
log.warn(wt.id, `reading ${repo.defaultBranch}'s changes failed`, e);
|
|
26938
|
+
const unmoved = move ? `the changes on ${repo.defaultBranch} could not be read, so they stayed there` : undefined;
|
|
26939
|
+
return { branch: repo.defaultBranch, moved: false, count: 0, ...unmoved ? { unmoved } : {} };
|
|
26940
|
+
}
|
|
26941
|
+
}
|
|
26942
|
+
async carryMainOrThrow(repo, wt, move) {
|
|
26157
26943
|
const main = repo.defaultBranch;
|
|
26158
|
-
if (!move)
|
|
26159
|
-
|
|
26944
|
+
if (!move) {
|
|
26945
|
+
const count = this.countsCache.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
|
|
26946
|
+
return { branch: main, moved: false, count };
|
|
26947
|
+
}
|
|
26160
26948
|
let count = 0;
|
|
26161
26949
|
const unmoved = await withRepoLock(repo.path, async () => {
|
|
26162
26950
|
count = (await statusFiles(repo.path)).length;
|
|
@@ -26178,7 +26966,7 @@ class WorktreeService {
|
|
|
26178
26966
|
};
|
|
26179
26967
|
const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
|
|
26180
26968
|
const restored = (carried.ok ? carried.out.split(`
|
|
26181
|
-
`).filter(Boolean) : []).filter((p) => !
|
|
26969
|
+
`).filter(Boolean) : []).filter((p) => !existsSync26(join23(wt.path, p)));
|
|
26182
26970
|
const applied = await git(wt.path, "stash", "apply", sha);
|
|
26183
26971
|
if (applied.ok) {
|
|
26184
26972
|
const ref = await entry();
|
|
@@ -26195,7 +26983,7 @@ class WorktreeService {
|
|
|
26195
26983
|
const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
|
|
26196
26984
|
return back?.ok ? `the changes on ${main} did not apply in ${wt.title}, so they stayed on ${main}` : `the changes on ${main} did not apply in ${wt.title}; they are kept in git stash as "toyon: into ${wt.title}"`;
|
|
26197
26985
|
});
|
|
26198
|
-
const mainWt = this.
|
|
26986
|
+
const mainWt = this.mainOf(repo.id);
|
|
26199
26987
|
if (mainWt)
|
|
26200
26988
|
this.countsCache.delete(mainWt.id);
|
|
26201
26989
|
return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
|
|
@@ -26213,10 +27001,10 @@ class WorktreeService {
|
|
|
26213
27001
|
throw new UserError("that is not a PR number");
|
|
26214
27002
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
26215
27003
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
26216
|
-
let slug = title;
|
|
26217
|
-
if (
|
|
27004
|
+
let slug = branchSlug(title) || "branch";
|
|
27005
|
+
if (existsSync26(join23(this.d.paths.worktreesDir, repo.name, slug)))
|
|
26218
27006
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
26219
|
-
const wtPath =
|
|
27007
|
+
const wtPath = join23(this.d.paths.worktreesDir, repo.name, slug);
|
|
26220
27008
|
await withRepoLock(repo.path, async () => {
|
|
26221
27009
|
const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
|
|
26222
27010
|
if (out)
|
|
@@ -26364,7 +27152,7 @@ class WorktreeService {
|
|
|
26364
27152
|
if (!name)
|
|
26365
27153
|
return;
|
|
26366
27154
|
for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
|
|
26367
|
-
await this.rename(sibling.id, `${name}
|
|
27155
|
+
await this.rename(sibling.id, `${name} v${sibling.variant.index}`).catch((e) => {
|
|
26368
27156
|
log.warn(sibling.id, "variant rename failed", e);
|
|
26369
27157
|
});
|
|
26370
27158
|
}
|
|
@@ -26407,7 +27195,14 @@ class WorktreeService {
|
|
|
26407
27195
|
const k = archive ? await this.keep(repo, wt) : null;
|
|
26408
27196
|
if (archive && !k)
|
|
26409
27197
|
throw new UserError(`could not keep ${wt.title}'s work, so it was left in place`);
|
|
26410
|
-
|
|
27198
|
+
if (archive)
|
|
27199
|
+
this.keepPlans(wt);
|
|
27200
|
+
try {
|
|
27201
|
+
await gitOrThrow(repo.path, "worktree", "remove", "--force", wt.path);
|
|
27202
|
+
} catch (e) {
|
|
27203
|
+
rmSync5(this.plansScratch(wt.id), { recursive: true, force: true });
|
|
27204
|
+
throw e;
|
|
27205
|
+
}
|
|
26411
27206
|
if (hasOwnBranch(wt)) {
|
|
26412
27207
|
const r = await git(repo.path, "branch", "-D", wt.branch);
|
|
26413
27208
|
if (!r.ok)
|
|
@@ -26415,7 +27210,6 @@ class WorktreeService {
|
|
|
26415
27210
|
}
|
|
26416
27211
|
return k;
|
|
26417
27212
|
});
|
|
26418
|
-
this.dropLink(wt);
|
|
26419
27213
|
const sessionId = this.d.state.session(worktreeId);
|
|
26420
27214
|
this.d.state.removeWorktree(worktreeId);
|
|
26421
27215
|
releasePort(wt.proxyPort);
|
|
@@ -26431,7 +27225,7 @@ class WorktreeService {
|
|
|
26431
27225
|
return archived;
|
|
26432
27226
|
}
|
|
26433
27227
|
async keep(repo, wt) {
|
|
26434
|
-
const index =
|
|
27228
|
+
const index = join23(this.d.paths.archiveDir, `${wt.id}.index`);
|
|
26435
27229
|
const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index);
|
|
26436
27230
|
if (!kept)
|
|
26437
27231
|
log.warn(wt.id, "could not keep its git state");
|
|
@@ -26439,8 +27233,21 @@ class WorktreeService {
|
|
|
26439
27233
|
log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
|
|
26440
27234
|
return kept;
|
|
26441
27235
|
}
|
|
27236
|
+
plansScratch(worktreeId) {
|
|
27237
|
+
return join23(this.d.paths.archiveDir, `${worktreeId}.plans`);
|
|
27238
|
+
}
|
|
27239
|
+
keepPlans(wt) {
|
|
27240
|
+
const from = join23(wt.path, PLANS_DIR);
|
|
27241
|
+
if (!existsSync26(from))
|
|
27242
|
+
return;
|
|
27243
|
+
try {
|
|
27244
|
+
cpSync(from, this.plansScratch(wt.id), { recursive: true });
|
|
27245
|
+
} catch (e) {
|
|
27246
|
+
log.warn(wt.id, "could not keep its plans", e);
|
|
27247
|
+
}
|
|
27248
|
+
}
|
|
26442
27249
|
archiveChat(wt, repo, kept, sessionId, reason) {
|
|
26443
|
-
const files = this.chatFiles(wt.id);
|
|
27250
|
+
const files = { ...this.chatFiles(wt.id), plans: this.plansScratch(wt.id) };
|
|
26444
27251
|
const prompt = firstPrompt(files.transcript);
|
|
26445
27252
|
const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
|
|
26446
27253
|
const rec = {
|
|
@@ -26453,14 +27260,15 @@ class WorktreeService {
|
|
|
26453
27260
|
...kept ? { kept } : {},
|
|
26454
27261
|
...reason ? { auto: reason } : {}
|
|
26455
27262
|
};
|
|
27263
|
+
let at;
|
|
26456
27264
|
try {
|
|
26457
|
-
this.archive.put(rec, files);
|
|
27265
|
+
at = this.archive.put(rec, files);
|
|
26458
27266
|
} catch (e) {
|
|
26459
27267
|
log.warn(wt.id, "could not archive its chat; the files stay where they were", e);
|
|
26460
27268
|
return null;
|
|
26461
27269
|
}
|
|
26462
27270
|
this.d.hub.emit("archiveChanged", repo.id);
|
|
26463
|
-
return summarize(rec, repo.id);
|
|
27271
|
+
return summarize(rec, repo.id, at.transcript);
|
|
26464
27272
|
}
|
|
26465
27273
|
chatFiles(worktreeId) {
|
|
26466
27274
|
return {
|
|
@@ -26480,7 +27288,6 @@ class WorktreeService {
|
|
|
26480
27288
|
async forgetGone(wt) {
|
|
26481
27289
|
const repo = this.d.state.repo(wt.repoId);
|
|
26482
27290
|
const sessionId = this.d.state.session(wt.id);
|
|
26483
|
-
this.dropLink(wt);
|
|
26484
27291
|
this.d.state.removeWorktree(wt.id);
|
|
26485
27292
|
if (!repo || wt.kind !== "worktree") {
|
|
26486
27293
|
this.deleteChat(wt.id);
|
|
@@ -26519,7 +27326,7 @@ class WorktreeService {
|
|
|
26519
27326
|
}
|
|
26520
27327
|
archivedAttachment(archiveId, file) {
|
|
26521
27328
|
const files = this.archive.chatFiles(archiveId);
|
|
26522
|
-
return files && isAttachmentFile(file) ?
|
|
27329
|
+
return files && isAttachmentFile(file) ? join23(files.attachments, file) : null;
|
|
26523
27330
|
}
|
|
26524
27331
|
archivedGit(archiveId) {
|
|
26525
27332
|
const rec = this.archive.get(archiveId);
|
|
@@ -26540,6 +27347,9 @@ class WorktreeService {
|
|
|
26540
27347
|
}
|
|
26541
27348
|
}
|
|
26542
27349
|
async archivedFile(archiveId, path, ref) {
|
|
27350
|
+
const plan = ref ? null : this.archive.planFile(archiveId, path);
|
|
27351
|
+
if (plan !== null)
|
|
27352
|
+
return { before: plan, after: plan };
|
|
26543
27353
|
const kept = this.archivedGit(archiveId);
|
|
26544
27354
|
return kept ? kept.file(path, ref) : null;
|
|
26545
27355
|
}
|
|
@@ -26559,7 +27369,7 @@ class WorktreeService {
|
|
|
26559
27369
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
26560
27370
|
if (rec.auto)
|
|
26561
27371
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
26562
|
-
const path =
|
|
27372
|
+
const path = existsSync26(old.path) ? join23(dirname9(old.path), freeSlot(dirname9(old.path)).dir) : old.path;
|
|
26563
27373
|
const branch = await withRepoLock(repo.path, async () => {
|
|
26564
27374
|
if (!await commitOf(repo.path, archiveRef(old.id))) {
|
|
26565
27375
|
throw new UserError(`${old.title}'s kept commits are gone from git`);
|
|
@@ -26585,7 +27395,7 @@ class WorktreeService {
|
|
|
26585
27395
|
}
|
|
26586
27396
|
return name;
|
|
26587
27397
|
});
|
|
26588
|
-
const { variant: _variant,
|
|
27398
|
+
const { variant: _variant, ...rest } = old;
|
|
26589
27399
|
const wt = {
|
|
26590
27400
|
...rest,
|
|
26591
27401
|
repoId: repo.id,
|
|
@@ -26596,7 +27406,7 @@ class WorktreeService {
|
|
|
26596
27406
|
...createdBy ? { createdBy } : {}
|
|
26597
27407
|
};
|
|
26598
27408
|
try {
|
|
26599
|
-
this.archive.take(old.id, this.chatFiles(old.id));
|
|
27409
|
+
this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join23(path, PLANS_DIR) });
|
|
26600
27410
|
} catch (e) {
|
|
26601
27411
|
log.warn(old.id, "could not move its chat back out of the archive", e);
|
|
26602
27412
|
}
|
|
@@ -26606,7 +27416,6 @@ class WorktreeService {
|
|
|
26606
27416
|
if (!dropped.ok)
|
|
26607
27417
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
26608
27418
|
this.countsCache.delete(wt.id);
|
|
26609
|
-
this.refreshLink(wt);
|
|
26610
27419
|
this.launch(wt, repo, repo.path);
|
|
26611
27420
|
if (message)
|
|
26612
27421
|
this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
|
|
@@ -26619,12 +27428,12 @@ class WorktreeService {
|
|
|
26619
27428
|
return;
|
|
26620
27429
|
const repo = this.repoOf(rec);
|
|
26621
27430
|
const repoPath = repo?.path ?? rec.repoPath;
|
|
26622
|
-
if (rec.kept &&
|
|
27431
|
+
if (rec.kept && existsSync26(repoPath)) {
|
|
26623
27432
|
const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
|
|
26624
27433
|
if (!r.ok)
|
|
26625
27434
|
log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
|
|
26626
27435
|
}
|
|
26627
|
-
if (rec.worktree.lands?.length &&
|
|
27436
|
+
if (rec.worktree.lands?.length && existsSync26(repoPath))
|
|
26628
27437
|
await dropLandRefs(repoPath, archiveId);
|
|
26629
27438
|
this.archive.delete(archiveId);
|
|
26630
27439
|
this.d.drafts?.drop(archiveId);
|
|
@@ -26641,55 +27450,24 @@ class WorktreeService {
|
|
|
26641
27450
|
const clean = cleanTitle2(title);
|
|
26642
27451
|
if (!clean)
|
|
26643
27452
|
return;
|
|
27453
|
+
const slug = branchSlug(clean);
|
|
26644
27454
|
await withRepoLock(repo.path, async () => {
|
|
26645
|
-
let branch = `toyon/${
|
|
26646
|
-
if (branch !== wt.branch) {
|
|
27455
|
+
let branch = `toyon/${slug}`;
|
|
27456
|
+
if (slug && branch !== wt.branch) {
|
|
26647
27457
|
let n = 2;
|
|
26648
27458
|
while (!(await git(wt.path, "branch", "-m", wt.branch, branch)).ok) {
|
|
26649
27459
|
if (n > 5)
|
|
26650
27460
|
return;
|
|
26651
|
-
branch = `toyon/${
|
|
27461
|
+
branch = `toyon/${slug}-${n++}`;
|
|
26652
27462
|
}
|
|
26653
27463
|
wt.branch = branch;
|
|
26654
27464
|
}
|
|
26655
27465
|
wt.title = clean;
|
|
27466
|
+
delete wt.unnamed;
|
|
26656
27467
|
});
|
|
26657
|
-
this.refreshLink(wt);
|
|
26658
27468
|
this.d.state.save();
|
|
26659
27469
|
this.d.hub.emit("worktreesChanged");
|
|
26660
27470
|
}
|
|
26661
|
-
refreshLink(wt) {
|
|
26662
|
-
if (!hasOwnBranch(wt))
|
|
26663
|
-
return;
|
|
26664
|
-
const name = wt.branch.replace(/^toyon\//, "");
|
|
26665
|
-
const desired = wt.kind === "worktree" ? join22(dirname8(wt.path), name) : wt.path;
|
|
26666
|
-
if (wt.linkPath && wt.linkPath !== desired)
|
|
26667
|
-
this.dropLink(wt);
|
|
26668
|
-
if (desired === wt.path)
|
|
26669
|
-
return;
|
|
26670
|
-
try {
|
|
26671
|
-
const st = lstatSync(desired, { throwIfNoEntry: false });
|
|
26672
|
-
if (st && !(st.isSymbolicLink() && readlinkSync(desired) === wt.path))
|
|
26673
|
-
return;
|
|
26674
|
-
if (!st)
|
|
26675
|
-
symlinkSync(wt.path, desired);
|
|
26676
|
-
wt.linkPath = desired;
|
|
26677
|
-
} catch (e) {
|
|
26678
|
-
log.warn(wt.id, "could not link the title to the directory", e);
|
|
26679
|
-
}
|
|
26680
|
-
}
|
|
26681
|
-
dropLink(wt) {
|
|
26682
|
-
const p = wt.linkPath;
|
|
26683
|
-
wt.linkPath = undefined;
|
|
26684
|
-
if (!p)
|
|
26685
|
-
return;
|
|
26686
|
-
try {
|
|
26687
|
-
if (lstatSync(p, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
26688
|
-
unlinkSync(p);
|
|
26689
|
-
} catch (e) {
|
|
26690
|
-
log.warn(wt.id, "could not remove the title link", e);
|
|
26691
|
-
}
|
|
26692
|
-
}
|
|
26693
27471
|
async pickVariant(worktreeId) {
|
|
26694
27472
|
const wt = this.d.state.worktree(worktreeId);
|
|
26695
27473
|
if (!wt?.variant)
|
|
@@ -26754,18 +27532,19 @@ class WorktreeService {
|
|
|
26754
27532
|
this.d.runtime.markSetup(wt.id, false);
|
|
26755
27533
|
}
|
|
26756
27534
|
}
|
|
26757
|
-
async mirrorBaseIgnore(
|
|
26758
|
-
const src =
|
|
26759
|
-
const
|
|
27535
|
+
async mirrorBaseIgnore(repo) {
|
|
27536
|
+
const src = join23(repo.path, ".gitignore");
|
|
27537
|
+
const tracked = existsSync26(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
|
|
27538
|
+
const mirror = existsSync26(src) && !tracked;
|
|
26760
27539
|
const lines = mirror ? readFileSync16(src, "utf8").split(`
|
|
26761
27540
|
`).filter((l) => l.trim() !== "") : [];
|
|
26762
27541
|
await excludeBlock(repo.path, BASE_IGNORE, lines);
|
|
26763
27542
|
}
|
|
26764
27543
|
async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
|
|
26765
|
-
await this.mirrorBaseIgnore(
|
|
26766
|
-
const srcNm =
|
|
26767
|
-
const dstNm =
|
|
26768
|
-
if (
|
|
27544
|
+
await this.mirrorBaseIgnore(repo);
|
|
27545
|
+
const srcNm = join23(depsSource, "node_modules");
|
|
27546
|
+
const dstNm = join23(wt.path, "node_modules");
|
|
27547
|
+
if (existsSync26(srcNm) && !existsSync26(dstNm)) {
|
|
26769
27548
|
const started = Date.now();
|
|
26770
27549
|
const attempts = [
|
|
26771
27550
|
["clonefile", ["-Rc", srcNm, dstNm]],
|
|
@@ -26781,10 +27560,10 @@ class WorktreeService {
|
|
|
26781
27560
|
}
|
|
26782
27561
|
}
|
|
26783
27562
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
26784
|
-
const src =
|
|
26785
|
-
if (!
|
|
27563
|
+
const src = join23(depsSource, f);
|
|
27564
|
+
if (!existsSync26(src) || existsSync26(join23(wt.path, f)))
|
|
26786
27565
|
continue;
|
|
26787
|
-
const r = await run("cp", [src,
|
|
27566
|
+
const r = await run("cp", [src, join23(wt.path, f)], wt.path);
|
|
26788
27567
|
if (r.ok)
|
|
26789
27568
|
this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
|
|
26790
27569
|
else
|
|
@@ -26849,10 +27628,42 @@ ${wt.landing.body}` : s;
|
|
|
26849
27628
|
async commitIfDirty(wt, typed) {
|
|
26850
27629
|
if ((await statusFiles(wt.path)).length === 0)
|
|
26851
27630
|
return null;
|
|
26852
|
-
|
|
26853
|
-
|
|
27631
|
+
return this.commitRecorded(wt, this.commitMessage(wt, typed));
|
|
27632
|
+
}
|
|
27633
|
+
async commitRecorded(wt, message) {
|
|
27634
|
+
const result = await commitWorktree(wt.path, message);
|
|
27635
|
+
if (result.ok) {
|
|
27636
|
+
await this.verdictSurvives(wt, { dropMessage: true });
|
|
26854
27637
|
this.headMoved(wt.id);
|
|
26855
|
-
|
|
27638
|
+
return result;
|
|
27639
|
+
}
|
|
27640
|
+
if (!result.output?.trim() || !this.d.record)
|
|
27641
|
+
return result;
|
|
27642
|
+
const subject = message.split(`
|
|
27643
|
+
`, 1)[0] ?? "";
|
|
27644
|
+
try {
|
|
27645
|
+
this.d.record(wt.id, `git commit -m ${JSON.stringify(subject)}`, result.output, result.exit ?? null);
|
|
27646
|
+
} catch (e) {
|
|
27647
|
+
log.warn(wt.id, `could not put the refused commit on the transcript: ${e instanceof Error ? e.message : e}`);
|
|
27648
|
+
return result;
|
|
27649
|
+
}
|
|
27650
|
+
return { ...result, message: "commit refused: what git and its hooks printed is on the chat" };
|
|
27651
|
+
}
|
|
27652
|
+
async verdictSurvives(wt, opts) {
|
|
27653
|
+
const l = wt.landing;
|
|
27654
|
+
if (!l || l.check === "pending")
|
|
27655
|
+
return;
|
|
27656
|
+
const { subject, body, ...kept } = l;
|
|
27657
|
+
const message = opts.dropMessage ? {} : { ...subject ? { subject } : {}, ...body ? { body } : {} };
|
|
27658
|
+
this.setLanding(wt.id, { ...kept, ...message, fingerprint: await treeFingerprint(wt.path) });
|
|
27659
|
+
}
|
|
27660
|
+
async landedByHand(wt, mark) {
|
|
27661
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
27662
|
+
log.info(wt.id, `landed on ${repo.defaultBranch} outside toyon: ${mark.base.slice(0, 7)}..${mark.tip.slice(0, 7)}`);
|
|
27663
|
+
await this.noteLand(repo, wt, mark);
|
|
27664
|
+
await this.restartFromMain(wt, repo.defaultBranch);
|
|
27665
|
+
this.setLanding(wt.id, undefined);
|
|
27666
|
+
this.setLanded(wt.id, true);
|
|
26856
27667
|
}
|
|
26857
27668
|
async land(worktreeId, message) {
|
|
26858
27669
|
const { wt, repo } = this.landable(worktreeId, "land");
|
|
@@ -26863,8 +27674,24 @@ ${wt.landing.body}` : s;
|
|
|
26863
27674
|
${wt.landing.body}` : wt.landing.subject : undefined;
|
|
26864
27675
|
if (policy.land === "pr") {
|
|
26865
27676
|
if (wt.pr?.state === "open") {
|
|
26866
|
-
const
|
|
26867
|
-
|
|
27677
|
+
const dirty = (await statusFiles(wt.path)).length > 0;
|
|
27678
|
+
const missing = dirty || (await aheadUpstream(wt.path) ?? 0) > 0;
|
|
27679
|
+
if (!missing)
|
|
27680
|
+
return { result: await mergePr(wt.path, wt.pr.number, policy.merge) };
|
|
27681
|
+
const committed = await this.commitIfDirty(wt, message);
|
|
27682
|
+
if (committed && !committed.ok)
|
|
27683
|
+
return { result: committed };
|
|
27684
|
+
const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
|
|
27685
|
+
if (!taken.ok)
|
|
27686
|
+
return { result: taken };
|
|
27687
|
+
this.headMoved(wt.id);
|
|
27688
|
+
const pushed = await pushBranch(wt.path, wt.branch);
|
|
27689
|
+
if (!pushed.ok)
|
|
27690
|
+
return { result: pushed };
|
|
27691
|
+
this.headMoved(wt.id);
|
|
27692
|
+
this.setLanding(wt.id, undefined);
|
|
27693
|
+
const did = committed ? "committed and pushed" : "pushed";
|
|
27694
|
+
return { result: { ok: true, url: wt.pr.url, message: `${did}; PR #${wt.pr.number} has the new commits` } };
|
|
26868
27695
|
}
|
|
26869
27696
|
const committed = await this.commitIfDirty(wt, message);
|
|
26870
27697
|
if (committed && !committed.ok)
|
|
@@ -26890,10 +27717,12 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26890
27717
|
return { result };
|
|
26891
27718
|
}
|
|
26892
27719
|
let mergedHere = false;
|
|
27720
|
+
let committedHere = false;
|
|
26893
27721
|
const result = await withRepoLock(repo.path, async () => {
|
|
26894
27722
|
const committed = await this.commitIfDirty(wt, message);
|
|
26895
27723
|
if (committed && !committed.ok)
|
|
26896
27724
|
return committed;
|
|
27725
|
+
committedHere = committed !== null;
|
|
26897
27726
|
if (policy.land === "push") {
|
|
26898
27727
|
const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
|
|
26899
27728
|
if (!pulled.ok && !/no upstream/.test(pulled.message))
|
|
@@ -26926,8 +27755,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26926
27755
|
if (!result.ok)
|
|
26927
27756
|
return { result };
|
|
26928
27757
|
const archiveIds = siblingsOf(wt, this.d.state.worktrees).map((w) => w.id);
|
|
26929
|
-
const
|
|
26930
|
-
|
|
27758
|
+
const did = committedHere ? "committed and merged" : "merged";
|
|
27759
|
+
const then = policy.land === "push" ? " and pushed" : "";
|
|
27760
|
+
return { result: { ...result, message: `${did} into ${repo.defaultBranch}${then}` }, archiveIds };
|
|
26931
27761
|
}
|
|
26932
27762
|
async noteLand(repo, wt, mark) {
|
|
26933
27763
|
if (!mark)
|
|
@@ -26973,6 +27803,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26973
27803
|
throw new UserError("that worktree is gone");
|
|
26974
27804
|
if (r.wt && isMain(r.wt))
|
|
26975
27805
|
throw new UserError("sync from a worktree, not main");
|
|
27806
|
+
if (r.wt && isProvisional(r.wt))
|
|
27807
|
+
throw new UserError("the new worktree already sits on main; it follows it on its own");
|
|
26976
27808
|
if (!r.branch)
|
|
26977
27809
|
throw new UserError(`${r.name} is detached: check out a branch in it first`);
|
|
26978
27810
|
if (r.locked)
|
|
@@ -26980,8 +27812,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26980
27812
|
const repo = this.d.state.requireRepo(r.repoId);
|
|
26981
27813
|
const own = r.wt ? hasOwnBranch(r.wt) : false;
|
|
26982
27814
|
const result = await withRepoLock(repo.path, () => takeMainIn(r.path, repo.defaultBranch, own));
|
|
26983
|
-
if (result.ok)
|
|
27815
|
+
if (result.ok) {
|
|
26984
27816
|
this.headMoved(worktreeId);
|
|
27817
|
+
if (r.wt)
|
|
27818
|
+
await this.verdictSurvives(r.wt, { dropMessage: false });
|
|
27819
|
+
}
|
|
26985
27820
|
return { result, defaultBranch: repo.defaultBranch };
|
|
26986
27821
|
}
|
|
26987
27822
|
async pull(worktreeId) {
|
|
@@ -27001,10 +27836,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27001
27836
|
const m = message.trim();
|
|
27002
27837
|
if (!m)
|
|
27003
27838
|
throw new UserError("commit message required");
|
|
27004
|
-
|
|
27005
|
-
if (result.ok)
|
|
27006
|
-
this.headMoved(wt.id);
|
|
27007
|
-
return result;
|
|
27839
|
+
return this.commitRecorded(wt, m);
|
|
27008
27840
|
}
|
|
27009
27841
|
setEmpty(wt, empty) {
|
|
27010
27842
|
if (wt.empty === empty)
|
|
@@ -27013,23 +27845,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27013
27845
|
this.d.state.save();
|
|
27014
27846
|
this.d.hub.emit("worktreesChanged");
|
|
27015
27847
|
}
|
|
27016
|
-
|
|
27017
|
-
|
|
27018
|
-
|
|
27019
|
-
|
|
27020
|
-
|
|
27021
|
-
if (Date.now() - last > FETCH_EVERY_MS) {
|
|
27022
|
-
this.lastFetch.set(path, Date.now());
|
|
27023
|
-
fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then((r) => {
|
|
27024
|
-
if (!r.ok) {
|
|
27025
|
-
log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
|
|
27026
|
-
return;
|
|
27027
|
-
}
|
|
27028
|
-
this.invalidateCounts();
|
|
27029
|
-
this.d.hub.emit("worktreesChanged");
|
|
27030
|
-
}));
|
|
27031
|
-
}
|
|
27032
|
-
return { behind };
|
|
27848
|
+
syncTrunk(repoId) {
|
|
27849
|
+
return this.trunk.sync(repoId);
|
|
27850
|
+
}
|
|
27851
|
+
trunks(opts = {}) {
|
|
27852
|
+
return this.trunk.all(opts);
|
|
27033
27853
|
}
|
|
27034
27854
|
usageFor(worktreeId) {
|
|
27035
27855
|
const known = this.usage.get(worktreeId);
|
|
@@ -27098,17 +27918,23 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27098
27918
|
if (this.going.has(id))
|
|
27099
27919
|
return cached ?? {};
|
|
27100
27920
|
try {
|
|
27101
|
-
const ab = countable ? await aheadBehind(path, defaultBranch) : await this.
|
|
27921
|
+
const ab = countable ? await aheadBehind(path, defaultBranch) : await this.trunk.behind(id, path);
|
|
27102
27922
|
const dirty = (await statusFiles(path)).length;
|
|
27923
|
+
const unpushed = await this.unpushed(id, path);
|
|
27103
27924
|
if (this.going.has(id))
|
|
27104
27925
|
return cached ?? {};
|
|
27105
|
-
const fresh = { ...ab, dirty, at: Date.now() };
|
|
27926
|
+
const fresh = { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty, at: Date.now() };
|
|
27106
27927
|
this.countsCache.set(id, fresh);
|
|
27107
27928
|
return fresh;
|
|
27108
27929
|
} catch {
|
|
27109
27930
|
return cached ?? {};
|
|
27110
27931
|
}
|
|
27111
27932
|
}
|
|
27933
|
+
async unpushed(id, path) {
|
|
27934
|
+
if (this.d.state.worktree(id)?.pr?.state !== "open")
|
|
27935
|
+
return;
|
|
27936
|
+
return await aheadUpstream(path) ?? undefined;
|
|
27937
|
+
}
|
|
27112
27938
|
async freshCounts(worktreeId) {
|
|
27113
27939
|
const wt = this.d.state.worktree(worktreeId);
|
|
27114
27940
|
const repo = wt && this.d.state.repo(wt.repoId);
|
|
@@ -27120,8 +27946,6 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27120
27946
|
readable(id) {
|
|
27121
27947
|
const wt = this.d.state.worktree(id);
|
|
27122
27948
|
if (wt) {
|
|
27123
|
-
if (wt.kind === "spare")
|
|
27124
|
-
return null;
|
|
27125
27949
|
const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
|
|
27126
27950
|
return { id, repoId: wt.repoId, path: wt.path, name: wt.title, branch: wt.branch, defaultBranch, wt };
|
|
27127
27951
|
}
|
|
@@ -27147,11 +27971,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27147
27971
|
return this.archivedStatus(worktreeId);
|
|
27148
27972
|
try {
|
|
27149
27973
|
const isMain = r.wt?.kind === "main";
|
|
27150
|
-
const [files,
|
|
27974
|
+
const [files, ab, unpushed, head] = await Promise.all([
|
|
27151
27975
|
statusFilesWithCounts(r.path),
|
|
27152
27976
|
isMain ? Promise.resolve({}) : aheadBehind(r.path, r.defaultBranch),
|
|
27977
|
+
this.unpushed(worktreeId, r.path),
|
|
27153
27978
|
git(r.path, "rev-parse", "HEAD")
|
|
27154
27979
|
]);
|
|
27980
|
+
const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
|
|
27155
27981
|
const ahead = counts.ahead ?? 0;
|
|
27156
27982
|
const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.defaultBranch) : undefined;
|
|
27157
27983
|
if (r.wt?.landed && (files.length > 0 || ahead > 0)) {
|
|
@@ -27159,8 +27985,21 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27159
27985
|
if (r.wt.pr)
|
|
27160
27986
|
this.setPr(r.wt.id, undefined);
|
|
27161
27987
|
}
|
|
27162
|
-
if (r.wt
|
|
27163
|
-
this.
|
|
27988
|
+
if (r.wt && !isMain && !r.wt.landed && hasOwnBranch(r.wt) && files.length === 0 && ahead === 0 && head.ok && this.handChecked.get(r.wt.id) !== head.out) {
|
|
27989
|
+
this.handChecked.set(r.wt.id, head.out);
|
|
27990
|
+
const mark = await handLanding(r.path, r.wt.branch, r.defaultBranch, r.wt.lands?.at(-1)?.tip);
|
|
27991
|
+
if (mark) {
|
|
27992
|
+
await this.landedByHand(r.wt, mark);
|
|
27993
|
+
return this.gitStatus(worktreeId);
|
|
27994
|
+
}
|
|
27995
|
+
}
|
|
27996
|
+
if (r.wt?.landing && r.wt.landing.check !== "pending") {
|
|
27997
|
+
const moved = r.wt.landing.fingerprint !== await treeFingerprint(r.path);
|
|
27998
|
+
if (moved !== !!r.wt.landing.stale) {
|
|
27999
|
+
const { stale: _was, ...rest } = r.wt.landing;
|
|
28000
|
+
this.setLanding(r.wt.id, moved ? { ...rest, stale: true } : rest);
|
|
28001
|
+
}
|
|
28002
|
+
}
|
|
27164
28003
|
if (r.wt && isMain)
|
|
27165
28004
|
this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
|
|
27166
28005
|
if (this.going.has(worktreeId))
|
|
@@ -27178,8 +28017,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27178
28017
|
return;
|
|
27179
28018
|
const ahead = main ? prev?.ahead : ab.ahead;
|
|
27180
28019
|
const behind = main ? prev?.behind : ab.behind;
|
|
27181
|
-
|
|
27182
|
-
|
|
28020
|
+
const unpushed = main ? prev?.unpushed : ab.unpushed;
|
|
28021
|
+
this.countsCache.set(id, { ahead, behind, unpushed, dirty, at: Date.now() });
|
|
28022
|
+
if (prev?.dirty !== dirty || prev?.ahead !== ahead || prev?.behind !== behind || prev?.unpushed !== unpushed)
|
|
27183
28023
|
this.d.hub.emit("worktreesChanged");
|
|
27184
28024
|
}
|
|
27185
28025
|
async gitLog(worktreeId) {
|
|
@@ -27206,7 +28046,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27206
28046
|
await this.restore(worktreeId, msg.clientId, { text: msg.text, attachments: msg.attachments });
|
|
27207
28047
|
return;
|
|
27208
28048
|
}
|
|
27209
|
-
this.requireRun(worktreeId);
|
|
28049
|
+
const wt = this.requireRun(worktreeId);
|
|
28050
|
+
if (wt.kind === "main")
|
|
28051
|
+
throw new UserError("nothing runs on main: the plus starts a worktree for it");
|
|
27210
28052
|
const agent = this.d.runtime.agentFor(worktreeId);
|
|
27211
28053
|
if (!agent)
|
|
27212
28054
|
throw new UserError("worktree still starting; try again in a moment");
|
|
@@ -27228,14 +28070,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27228
28070
|
setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
|
|
27229
28071
|
return [...owned, ...found];
|
|
27230
28072
|
}
|
|
27231
|
-
|
|
27232
|
-
return this.d.state
|
|
27233
|
-
|
|
27234
|
-
|
|
27235
|
-
|
|
27236
|
-
proxyPort: wt.proxyPort,
|
|
27237
|
-
ready: this.d.runtime.get(wt.id)?.proxy != null && this.spare.current(wt.repoId)?.worktreeId === wt.id
|
|
27238
|
-
}));
|
|
28073
|
+
mainOf(repoId) {
|
|
28074
|
+
return mainOf(this.d.state, repoId);
|
|
28075
|
+
}
|
|
28076
|
+
leadSpareOf(repoId) {
|
|
28077
|
+
return this.spare.current(repoId)?.worktreeId ?? null;
|
|
27239
28078
|
}
|
|
27240
28079
|
countsQuick(id, quick) {
|
|
27241
28080
|
const c = this.countsCache.get(id);
|
|
@@ -27244,11 +28083,17 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27244
28083
|
quick.missed = true;
|
|
27245
28084
|
return {};
|
|
27246
28085
|
}
|
|
28086
|
+
isRow(wt) {
|
|
28087
|
+
if (wt.kind === "worktree")
|
|
28088
|
+
return true;
|
|
28089
|
+
const lead = this.leadSpareOf(wt.repoId);
|
|
28090
|
+
return wt.kind === "spare" ? lead === wt.id : lead === null;
|
|
28091
|
+
}
|
|
27247
28092
|
async ownedRows(quick) {
|
|
27248
|
-
return Promise.all(this.d.state.worktrees.filter((wt) => wt
|
|
28093
|
+
return Promise.all(this.d.state.worktrees.filter((wt) => this.isRow(wt)).map(async (wt) => {
|
|
27249
28094
|
const rt = this.d.runtime.get(wt.id);
|
|
27250
28095
|
const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
|
|
27251
|
-
const { ahead, behind, dirty } = quick ? this.countsQuick(wt.id, quick) : await this.counts(wt.id, wt.path, defaultBranch, !isMain(wt));
|
|
28096
|
+
const { ahead, behind, unpushed, dirty } = quick ? this.countsQuick(wt.id, quick) : await this.counts(wt.id, wt.path, defaultBranch, !isMain(wt));
|
|
27252
28097
|
return {
|
|
27253
28098
|
id: wt.id,
|
|
27254
28099
|
repoId: wt.repoId,
|
|
@@ -27261,10 +28106,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27261
28106
|
login: !!rt?.login,
|
|
27262
28107
|
ahead,
|
|
27263
28108
|
behind,
|
|
28109
|
+
unpushed,
|
|
27264
28110
|
dirty,
|
|
27265
28111
|
queued: rt?.agent.queueLength || undefined,
|
|
27266
28112
|
unseen: isUnseen(wt) || undefined,
|
|
27267
|
-
usage: this.usageFor(wt.id)
|
|
28113
|
+
usage: this.usageFor(wt.id),
|
|
28114
|
+
transcript: transcriptPathFor(this.d.paths.transcriptsDir, wt.id),
|
|
28115
|
+
sessionId: this.d.state.session(wt.id)
|
|
27268
28116
|
};
|
|
27269
28117
|
}));
|
|
27270
28118
|
}
|
|
@@ -27369,7 +28217,7 @@ class ArchiveSweep {
|
|
|
27369
28217
|
// ../daemon/src/index.ts
|
|
27370
28218
|
process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
|
|
27371
28219
|
process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
|
|
27372
|
-
var here =
|
|
28220
|
+
var here = dirname10(fileURLToPath(import.meta.url));
|
|
27373
28221
|
var {
|
|
27374
28222
|
shellDist: SHELL_DIST,
|
|
27375
28223
|
bridgeJs: BRIDGE_JS,
|
|
@@ -27423,29 +28271,39 @@ var runtime = new RuntimeRegistry({
|
|
|
27423
28271
|
bridgeScript: () => bridge.get(),
|
|
27424
28272
|
remote,
|
|
27425
28273
|
grant: previewGrant(token),
|
|
27426
|
-
viewed: (id) => idle.isViewed(id)
|
|
28274
|
+
viewed: (id) => idle.isViewed(id),
|
|
28275
|
+
shown: (id) => idle.isShown(id),
|
|
28276
|
+
mainLeads: (repoId) => worktrees.spare.current(repoId) === null
|
|
27427
28277
|
});
|
|
27428
28278
|
var drafts = new DraftStore({ file: paths.draftsFile, hub });
|
|
27429
|
-
var
|
|
28279
|
+
var exec = new ExecService({ state, runtime });
|
|
28280
|
+
var worktrees = new WorktreeService({
|
|
28281
|
+
state,
|
|
28282
|
+
hub,
|
|
28283
|
+
runtime,
|
|
28284
|
+
paths,
|
|
28285
|
+
agents,
|
|
28286
|
+
drafts,
|
|
28287
|
+
record: (id, command, text, exit) => exec.record(id, command, text, exit)
|
|
28288
|
+
});
|
|
27430
28289
|
var turns = new TurnService({
|
|
27431
28290
|
state,
|
|
27432
28291
|
hub,
|
|
27433
28292
|
transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
|
|
27434
|
-
summarize: makeRecapper(runtime, agents, state),
|
|
27435
28293
|
hold: (id, tag) => runtime.hold(id, tag),
|
|
27436
28294
|
release: (id, tag) => runtime.release(id, tag)
|
|
27437
28295
|
});
|
|
27438
28296
|
var files = new FileService(state, runtime, (id) => worktrees.readable(id));
|
|
27439
28297
|
var design = new DesignService((id) => worktrees.readable(id));
|
|
27440
28298
|
var routes = new RouteService({ state, hub, readable: (id) => worktrees.readable(id) });
|
|
27441
|
-
var
|
|
27442
|
-
new LandingService({
|
|
28299
|
+
var landing = new LandingService({
|
|
27443
28300
|
state,
|
|
27444
28301
|
hub,
|
|
27445
28302
|
worktrees,
|
|
27446
28303
|
transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
|
|
27447
|
-
check: (id, command) => exec.exec(id, command, CHECK_TOOL),
|
|
27448
|
-
judge: makeLander(runtime, agents, state)
|
|
28304
|
+
check: (id, command, opts) => exec.exec(id, command, CHECK_TOOL, opts),
|
|
28305
|
+
judge: makeLander(runtime, agents, state),
|
|
28306
|
+
recap: makeAnswerRecapper(runtime, agents, state)
|
|
27449
28307
|
});
|
|
27450
28308
|
var refs = new RefSearch({ state });
|
|
27451
28309
|
var chats = new ChatSearch({
|
|
@@ -27457,7 +28315,7 @@ var chats = new ChatSearch({
|
|
|
27457
28315
|
var prs = new PrService({ state, hub, worktrees, view: viewPr });
|
|
27458
28316
|
var self = new SelfWatch(SOURCE_ROOT);
|
|
27459
28317
|
await self.start();
|
|
27460
|
-
var afterLand = new AfterLand({ state, hub, self });
|
|
28318
|
+
var afterLand = new AfterLand({ state, hub, self, settled: () => idle.settled() });
|
|
27461
28319
|
var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
|
|
27462
28320
|
var idle = new IdlePolicy({
|
|
27463
28321
|
runtime,
|
|
@@ -27526,6 +28384,7 @@ var { branded, stop: stopServer } = startServer({
|
|
|
27526
28384
|
chats,
|
|
27527
28385
|
drafts,
|
|
27528
28386
|
prs,
|
|
28387
|
+
landing,
|
|
27529
28388
|
themes,
|
|
27530
28389
|
agents,
|
|
27531
28390
|
accounts,
|
|
@@ -27552,11 +28411,9 @@ bridge.setShellOrigins([
|
|
|
27552
28411
|
writeFileSync10(paths.pidFile, `${process.pid}
|
|
27553
28412
|
`);
|
|
27554
28413
|
var stopLagSampler = startLagSampler();
|
|
28414
|
+
await runtime.reclaimOrphans();
|
|
27555
28415
|
await repos.boot();
|
|
27556
|
-
drafts.prune((id) =>
|
|
27557
|
-
const repoId = draftRepoOf(id);
|
|
27558
|
-
return repoId ? !!state.repo(repoId) : !!state.worktree(id) || worktrees.hasArchived(id);
|
|
27559
|
-
});
|
|
28416
|
+
drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
|
|
27560
28417
|
idle.boot();
|
|
27561
28418
|
fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
|
|
27562
28419
|
var repoArg = process.argv[2];
|
|
@@ -27597,6 +28454,7 @@ async function shutdown(signal, opts = {}) {
|
|
|
27597
28454
|
drafts.flush();
|
|
27598
28455
|
const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
27599
28456
|
await Promise.race([runtime.shutdown(), deadline]);
|
|
28457
|
+
state.flushGroups();
|
|
27600
28458
|
rmSync6(paths.pidFile, { force: true });
|
|
27601
28459
|
if (opts.respawn) {
|
|
27602
28460
|
await Bun.sleep(250);
|