toyon 0.4.0 → 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 +9 -12
- package/dist/bridge.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/daemon.js +1745 -787
- package/dist/shell/assets/{Editor-CgKsVXHS.js → Editor-DR-GTFmf.js} +4 -4
- package/dist/shell/assets/{XTerm-CMRoVDAh.js → XTerm-B3kT_naP.js} +12 -12
- package/dist/shell/assets/{cssMode-Dg15kh0f.js → cssMode-CB4CczRa.js} +1 -1
- package/dist/shell/assets/{freemarker2-CU21n-BA.js → freemarker2-Wjq3dVyB.js} +1 -1
- package/dist/shell/assets/{grammar-DGBFIuP2.js → grammar-yCr7BdNP.js} +1 -1
- package/dist/shell/assets/{handlebars-DVAbWe34.js → handlebars-Bjvi6pTu.js} +1 -1
- package/dist/shell/assets/{html-BkxU7kNX.js → html-iB2mKeN2.js} +1 -1
- package/dist/shell/assets/{htmlMode-Brl4kLPo.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-DoIo3Pg8.js → javascript-BpuIkItm.js} +1 -1
- package/dist/shell/assets/{jsonMode-B9qq6lCy.js → jsonMode-QCLe5ypz.js} +1 -1
- package/dist/shell/assets/{liquid-Cn1aqWby.js → liquid-CQI6mzhA.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-C5ez8E_9.js → lspLanguageFeatures-DqXzGsHQ.js} +1 -1
- package/dist/shell/assets/{mdx-1MuVz0xd.js → mdx-D2Unh-By.js} +1 -1
- package/dist/shell/assets/{python-DFhVdcGk.js → python-LLSqiJ4D.js} +1 -1
- package/dist/shell/assets/{razor-BitqUXpg.js → razor-CJ2qsTB8.js} +1 -1
- package/dist/shell/assets/{tsMode-loK_YDOL.js → tsMode-CwjjHUH5.js} +1 -1
- package/dist/shell/assets/{xml-Cc4ljERX.js → xml-BzqCyEOW.js} +1 -1
- package/dist/shell/assets/{yaml-0hpVC8PU.js → yaml-N1Bfi3ds.js} +1 -1
- package/dist/shell/index.html +3 -3
- package/package.json +1 -1
- package/dist/shell/assets/index-DUmxVabc.css +0 -1
- package/dist/shell/assets/index-WHwsRrlj.js +0 -232
package/dist/daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../daemon/src/index.ts
|
|
3
|
-
import { rmSync as rmSync6, writeFileSync as
|
|
4
|
-
import { dirname as
|
|
3
|
+
import { rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
|
|
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 });
|
|
@@ -12694,6 +12740,29 @@ async function excludeFromGit(cwd, pattern) {
|
|
|
12694
12740
|
` : ""}${pattern}
|
|
12695
12741
|
`);
|
|
12696
12742
|
}
|
|
12743
|
+
async function excludeBlock(cwd, name, patterns) {
|
|
12744
|
+
const file = await excludeFile(cwd);
|
|
12745
|
+
if (!file)
|
|
12746
|
+
return;
|
|
12747
|
+
const open = `# toyon ${name}`;
|
|
12748
|
+
const close = `# toyon end ${name}`;
|
|
12749
|
+
const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
12750
|
+
const lines = current.split(`
|
|
12751
|
+
`);
|
|
12752
|
+
const from = lines.indexOf(open);
|
|
12753
|
+
const to = lines.indexOf(close);
|
|
12754
|
+
const kept = from >= 0 && to > from ? [...lines.slice(0, from), ...lines.slice(to + 1)] : lines;
|
|
12755
|
+
while (kept.length > 0 && kept[kept.length - 1] === "")
|
|
12756
|
+
kept.pop();
|
|
12757
|
+
const next = [...kept, ...patterns.length > 0 ? [open, ...patterns, close] : []];
|
|
12758
|
+
const text = next.length > 0 ? `${next.join(`
|
|
12759
|
+
`)}
|
|
12760
|
+
` : "";
|
|
12761
|
+
if (text === current)
|
|
12762
|
+
return;
|
|
12763
|
+
mkdirSync2(dirname(file), { recursive: true });
|
|
12764
|
+
writeFileSync(file, text);
|
|
12765
|
+
}
|
|
12697
12766
|
async function unexcludeFromGit(cwd, pattern) {
|
|
12698
12767
|
const file = await excludeFile(cwd);
|
|
12699
12768
|
if (!file || !existsSync3(file))
|
|
@@ -13197,6 +13266,7 @@ var SYSTEM_APPEND = [
|
|
|
13197
13266
|
"Never run `git push`, delete branches, or create pull requests; shipping is handled by the Toyon UI.",
|
|
13198
13267
|
"Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
|
|
13199
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.",
|
|
13200
13270
|
"`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
|
|
13201
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.`,
|
|
13202
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.",
|
|
@@ -13228,7 +13298,7 @@ function previewContext(p) {
|
|
|
13228
13298
|
line = `The preview is running${at}, and the user sees it live beside this chat.`;
|
|
13229
13299
|
break;
|
|
13230
13300
|
case "asleep":
|
|
13231
|
-
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.`;
|
|
13232
13302
|
break;
|
|
13233
13303
|
default:
|
|
13234
13304
|
line = `The preview is ${p.status}${p.detail ? `: ${p.detail}` : ""}; nothing answers${at} until it is back.`;
|
|
@@ -13619,11 +13689,14 @@ function asRecord(v) {
|
|
|
13619
13689
|
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
13620
13690
|
}
|
|
13621
13691
|
var NETWORK_ASK = "SandboxNetworkAccess";
|
|
13692
|
+
function isNetworkAsk(update) {
|
|
13693
|
+
return update.name === NETWORK_ASK || asRecord(asRecord(update._meta).claudeCode).toolName === NETWORK_ASK;
|
|
13694
|
+
}
|
|
13622
13695
|
function heading(update) {
|
|
13623
|
-
const name = update.name
|
|
13696
|
+
const name = typeof update.name === "string" ? update.name : "";
|
|
13624
13697
|
const host = asRecord(update.rawInput).host;
|
|
13625
|
-
if (
|
|
13626
|
-
return { name: "
|
|
13698
|
+
if (isNetworkAsk(update) && typeof host === "string")
|
|
13699
|
+
return { name: "", title: host, kind: "fetch" };
|
|
13627
13700
|
return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
|
|
13628
13701
|
}
|
|
13629
13702
|
function endOfAsk(req, res, memos) {
|
|
@@ -13641,40 +13714,59 @@ function endOfAsk(req, res, memos) {
|
|
|
13641
13714
|
isError: !allowed
|
|
13642
13715
|
};
|
|
13643
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
|
+
}
|
|
13644
13727
|
function mapUpdate(update, memos, tag) {
|
|
13645
13728
|
switch (update.sessionUpdate) {
|
|
13646
13729
|
case "agent_message_chunk": {
|
|
13730
|
+
const { parentToolId } = spawnOf(update._meta);
|
|
13731
|
+
const out = abandoned(memos, parentToolId);
|
|
13647
13732
|
const text = textOf(update.content, tag, "message");
|
|
13648
13733
|
if (text === null)
|
|
13649
|
-
return
|
|
13650
|
-
|
|
13651
|
-
return
|
|
13734
|
+
return out;
|
|
13735
|
+
out.push(parentToolId ? { type: "tool-delta", toolId: parentToolId, text } : { type: "text-delta", text });
|
|
13736
|
+
return out;
|
|
13652
13737
|
}
|
|
13653
13738
|
case "agent_thought_chunk": {
|
|
13654
|
-
|
|
13655
|
-
|
|
13739
|
+
const { parentToolId } = spawnOf(update._meta);
|
|
13740
|
+
const out = abandoned(memos, parentToolId);
|
|
13741
|
+
if (parentToolId)
|
|
13742
|
+
return out;
|
|
13656
13743
|
const text = textOf(update.content, tag, "thought");
|
|
13657
|
-
|
|
13744
|
+
if (text !== null)
|
|
13745
|
+
out.push({ type: "thinking-delta", text });
|
|
13746
|
+
return out;
|
|
13658
13747
|
}
|
|
13659
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 ?? [] };
|
|
13660
13752
|
const memo = {
|
|
13661
13753
|
...heading(update),
|
|
13662
13754
|
content: update.content ?? [],
|
|
13663
13755
|
rawOutput: update.rawOutput,
|
|
13664
|
-
ended: false
|
|
13756
|
+
ended: false,
|
|
13757
|
+
writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
|
|
13758
|
+
...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
|
|
13665
13759
|
};
|
|
13666
13760
|
memos.set(update.toolCallId, memo);
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
}
|
|
13677
|
-
];
|
|
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
|
+
});
|
|
13678
13770
|
if (update.status === "completed" || update.status === "failed")
|
|
13679
13771
|
out.push(endOf(update.toolCallId, memo, update.status));
|
|
13680
13772
|
return out;
|
|
@@ -13683,37 +13775,45 @@ function mapUpdate(update, memos, tag) {
|
|
|
13683
13775
|
let memo = memos.get(update.toolCallId);
|
|
13684
13776
|
const out = [];
|
|
13685
13777
|
if (!memo) {
|
|
13778
|
+
const spawn = spawnOf(update._meta);
|
|
13779
|
+
const input = update.rawInput ?? { locations: update.locations ?? [] };
|
|
13686
13780
|
memo = {
|
|
13687
|
-
name: update.name ??
|
|
13781
|
+
name: update.name ?? "",
|
|
13688
13782
|
title: update.title ?? update.name ?? "tool",
|
|
13689
13783
|
...update.kind ? { kind: update.kind } : {},
|
|
13690
13784
|
content: [],
|
|
13691
|
-
ended: false
|
|
13785
|
+
ended: false,
|
|
13786
|
+
writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
|
|
13787
|
+
...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
|
|
13692
13788
|
};
|
|
13693
13789
|
memos.set(update.toolCallId, memo);
|
|
13694
13790
|
out.push({
|
|
13695
13791
|
type: "tool-start",
|
|
13696
13792
|
toolId: update.toolCallId,
|
|
13697
13793
|
name: memo.name,
|
|
13698
|
-
input
|
|
13794
|
+
input,
|
|
13699
13795
|
...memo.kind ? { kind: memo.kind } : {},
|
|
13700
13796
|
title: memo.title,
|
|
13701
|
-
...
|
|
13797
|
+
...spawn
|
|
13702
13798
|
});
|
|
13703
13799
|
}
|
|
13800
|
+
if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
|
|
13801
|
+
return out;
|
|
13704
13802
|
const refined = { type: "tool-update", toolId: update.toolCallId };
|
|
13705
13803
|
if (update.title && update.title !== memo.title) {
|
|
13706
13804
|
memo.title = update.title;
|
|
13707
13805
|
refined.title = update.title;
|
|
13708
|
-
if (!update.name && memo.name !== update.title)
|
|
13709
|
-
refined.name = memo.name = update.title;
|
|
13710
13806
|
}
|
|
13711
|
-
if (update.name && update.name !== memo.name)
|
|
13807
|
+
if (update.name && update.name !== memo.name) {
|
|
13712
13808
|
refined.name = memo.name = update.name;
|
|
13809
|
+
}
|
|
13713
13810
|
if (update.kind && update.kind !== memo.kind)
|
|
13714
13811
|
refined.kind = memo.kind = update.kind;
|
|
13715
|
-
if (update.rawInput !== undefined)
|
|
13812
|
+
if (update.rawInput !== undefined) {
|
|
13716
13813
|
refined.input = update.rawInput;
|
|
13814
|
+
if (!emptyInput(update.rawInput))
|
|
13815
|
+
memo.writing = false;
|
|
13816
|
+
}
|
|
13717
13817
|
if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
|
|
13718
13818
|
out.push(refined);
|
|
13719
13819
|
if (update.content)
|
|
@@ -13827,6 +13927,7 @@ function steerOutcome(value) {
|
|
|
13827
13927
|
|
|
13828
13928
|
// ../daemon/src/agent/acp/session.ts
|
|
13829
13929
|
var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
|
|
13930
|
+
var OWN_SETTLE_MS = 1e4;
|
|
13830
13931
|
function recorded(reply) {
|
|
13831
13932
|
if (reply?.kind === "choice")
|
|
13832
13933
|
return { choiceId: reply.choiceId };
|
|
@@ -13845,8 +13946,8 @@ class AcpSession {
|
|
|
13845
13946
|
conn = null;
|
|
13846
13947
|
connecting = null;
|
|
13847
13948
|
live = null;
|
|
13848
|
-
loading = false;
|
|
13849
13949
|
reaper = null;
|
|
13950
|
+
own = null;
|
|
13850
13951
|
asking = 0;
|
|
13851
13952
|
asks = new Map;
|
|
13852
13953
|
log;
|
|
@@ -13874,13 +13975,38 @@ class AcpSession {
|
|
|
13874
13975
|
async warmCommands() {
|
|
13875
13976
|
if (this.commandList.length > 0 || this.stopped)
|
|
13876
13977
|
return;
|
|
13978
|
+
await this.warm();
|
|
13979
|
+
}
|
|
13980
|
+
async warm() {
|
|
13981
|
+
if (this.stopped)
|
|
13982
|
+
return;
|
|
13877
13983
|
try {
|
|
13878
|
-
await this.ensureLive();
|
|
13984
|
+
await this.ensureLive("warm-up");
|
|
13879
13985
|
this.maybeArmReaper();
|
|
13880
13986
|
} catch (e) {
|
|
13881
|
-
log.debug(this.d.worktreeId, `
|
|
13987
|
+
log.debug(this.d.worktreeId, `agent warm-up skipped: ${this.describe(e)}`);
|
|
13882
13988
|
}
|
|
13883
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
|
+
}
|
|
13884
14010
|
setCommands(next) {
|
|
13885
14011
|
if (sameCommands(this.commandList, next))
|
|
13886
14012
|
return;
|
|
@@ -13930,7 +14056,7 @@ class AcpSession {
|
|
|
13930
14056
|
return log.warn(this.d.worktreeId, "send after close dropped");
|
|
13931
14057
|
const { context, attachments } = opts;
|
|
13932
14058
|
const item = { text, context, ...attachments?.length ? { attachments } : {} };
|
|
13933
|
-
if (this.running && !this.interrupted && this.live?.conn.steering) {
|
|
14059
|
+
if ((this.running || this.own) && !this.interrupted && this.live?.conn.steering) {
|
|
13934
14060
|
return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
|
|
13935
14061
|
}
|
|
13936
14062
|
this.enqueue(item);
|
|
@@ -13944,7 +14070,7 @@ class AcpSession {
|
|
|
13944
14070
|
async steer(item) {
|
|
13945
14071
|
item.recorded ??= await this.record(item);
|
|
13946
14072
|
const live = this.live;
|
|
13947
|
-
if (!live || !this.running || this.interrupted)
|
|
14073
|
+
if (!live || !(this.running || this.own) || this.interrupted)
|
|
13948
14074
|
return this.enqueue(item);
|
|
13949
14075
|
this.steered.push(item);
|
|
13950
14076
|
let outcome;
|
|
@@ -13982,8 +14108,14 @@ class AcpSession {
|
|
|
13982
14108
|
}
|
|
13983
14109
|
stop() {
|
|
13984
14110
|
this.cancelAsks();
|
|
13985
|
-
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
|
+
}
|
|
13986
14117
|
return;
|
|
14118
|
+
}
|
|
13987
14119
|
this.interrupted = true;
|
|
13988
14120
|
if (this.steered.length > 0) {
|
|
13989
14121
|
this.queue.unshift(...this.steered);
|
|
@@ -14002,10 +14134,11 @@ class AcpSession {
|
|
|
14002
14134
|
this.steered = [];
|
|
14003
14135
|
this.queueChanged();
|
|
14004
14136
|
this.stop();
|
|
14005
|
-
await this.dropConn();
|
|
14137
|
+
await this.dropConn("closing");
|
|
14006
14138
|
await this.log.flush();
|
|
14007
14139
|
}
|
|
14008
14140
|
async drain() {
|
|
14141
|
+
this.endOwn("end_turn");
|
|
14009
14142
|
this.running = true;
|
|
14010
14143
|
this.clearReaper();
|
|
14011
14144
|
this.setStatus("working");
|
|
@@ -14043,7 +14176,7 @@ class AcpSession {
|
|
|
14043
14176
|
} else {
|
|
14044
14177
|
this.emit({ type: "agent-error", message: this.describe(e), ts: Date.now() });
|
|
14045
14178
|
this.setStatus("error");
|
|
14046
|
-
fireAndForget(this.d.worktreeId, this.dropConn(), "drop agent after error");
|
|
14179
|
+
fireAndForget(this.d.worktreeId, this.dropConn("error"), "drop agent after error");
|
|
14047
14180
|
}
|
|
14048
14181
|
} finally {
|
|
14049
14182
|
this.interrupted = false;
|
|
@@ -14058,7 +14191,7 @@ class AcpSession {
|
|
|
14058
14191
|
this.asking++;
|
|
14059
14192
|
this.clearReaper();
|
|
14060
14193
|
try {
|
|
14061
|
-
const conn = await this.ensureConn();
|
|
14194
|
+
const conn = await this.ensureConn("an ask");
|
|
14062
14195
|
return await askOnce(conn.ctx, {
|
|
14063
14196
|
cwd: this.d.cwd,
|
|
14064
14197
|
system,
|
|
@@ -14084,7 +14217,7 @@ class AcpSession {
|
|
|
14084
14217
|
}
|
|
14085
14218
|
}
|
|
14086
14219
|
async authenticate(methodId, apiKey) {
|
|
14087
|
-
const conn = await this.ensureConn();
|
|
14220
|
+
const conn = await this.ensureConn("login");
|
|
14088
14221
|
const method = conn.authMethods.find((m) => m.id === methodId);
|
|
14089
14222
|
if (!method)
|
|
14090
14223
|
throw new UserError(`${conn.spec.name} offers no login method "${methodId}"`);
|
|
@@ -14160,7 +14293,7 @@ class AcpSession {
|
|
|
14160
14293
|
async runTurn(item) {
|
|
14161
14294
|
item.recorded ??= await this.record(item);
|
|
14162
14295
|
this.emit({ type: "turn-start", ts: Date.now() });
|
|
14163
|
-
const live = await this.ensureLive();
|
|
14296
|
+
const live = await this.ensureLive("a turn");
|
|
14164
14297
|
await this.applyMode(live);
|
|
14165
14298
|
await this.applyOption(live, "model");
|
|
14166
14299
|
await this.applyOption(live, "thought_level");
|
|
@@ -14180,17 +14313,17 @@ class AcpSession {
|
|
|
14180
14313
|
contextFor(item) {
|
|
14181
14314
|
return ambientBlock([...item.context ?? [], this.d.preview?.()]);
|
|
14182
14315
|
}
|
|
14183
|
-
ensureConn() {
|
|
14316
|
+
ensureConn(why) {
|
|
14184
14317
|
if (this.conn)
|
|
14185
14318
|
return Promise.resolve(this.conn);
|
|
14186
14319
|
if (this.connecting)
|
|
14187
14320
|
return this.connecting;
|
|
14188
|
-
this.connecting = this.openConn().finally(() => {
|
|
14321
|
+
this.connecting = this.openConn(why).finally(() => {
|
|
14189
14322
|
this.connecting = null;
|
|
14190
14323
|
});
|
|
14191
14324
|
return this.connecting;
|
|
14192
14325
|
}
|
|
14193
|
-
async openConn() {
|
|
14326
|
+
async openConn(why) {
|
|
14194
14327
|
const spec = this.d.spec();
|
|
14195
14328
|
const prepared = await (this.d.prepare ?? prepareLaunch)(this.d.cwd, spec);
|
|
14196
14329
|
const { bounds } = prepared;
|
|
@@ -14202,13 +14335,17 @@ class AcpSession {
|
|
|
14202
14335
|
this.d.onAuth?.(spec.id, { status: c.params });
|
|
14203
14336
|
});
|
|
14204
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"})`);
|
|
14205
14340
|
const ctx = link.conn.agent;
|
|
14206
14341
|
link.exited.then(() => {
|
|
14342
|
+
this.noteProcess(link, false);
|
|
14207
14343
|
if (this.conn?.link === link) {
|
|
14208
14344
|
log.warn(this.d.worktreeId, "agent process exited while idle");
|
|
14209
14345
|
this.conn = null;
|
|
14210
14346
|
this.live = null;
|
|
14211
14347
|
this.cancelAsks();
|
|
14348
|
+
this.endOwn("interrupted");
|
|
14212
14349
|
}
|
|
14213
14350
|
});
|
|
14214
14351
|
try {
|
|
@@ -14227,7 +14364,7 @@ class AcpSession {
|
|
|
14227
14364
|
spec,
|
|
14228
14365
|
bounds,
|
|
14229
14366
|
authMethods: init.authMethods ?? [],
|
|
14230
|
-
|
|
14367
|
+
resumeSession: !!init.agentCapabilities?.sessionCapabilities?.resume,
|
|
14231
14368
|
closeSupported: !!init.agentCapabilities?.sessionCapabilities?.close,
|
|
14232
14369
|
deleteSupported: !!init.agentCapabilities?.sessionCapabilities?.delete,
|
|
14233
14370
|
acceptsImages: init.agentCapabilities?.promptCapabilities?.image === true,
|
|
@@ -14239,11 +14376,12 @@ class AcpSession {
|
|
|
14239
14376
|
return this.conn;
|
|
14240
14377
|
} catch (e) {
|
|
14241
14378
|
fireAndForget(this.d.worktreeId, link.kill(), "kill agent after failed start");
|
|
14379
|
+
this.noteProcess(link, false);
|
|
14242
14380
|
throw e;
|
|
14243
14381
|
}
|
|
14244
14382
|
}
|
|
14245
|
-
async ensureLive() {
|
|
14246
|
-
const conn = await this.ensureConn();
|
|
14383
|
+
async ensureLive(why) {
|
|
14384
|
+
const conn = await this.ensureConn(why);
|
|
14247
14385
|
if (this.live?.conn === conn)
|
|
14248
14386
|
return this.live;
|
|
14249
14387
|
const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
|
|
@@ -14251,10 +14389,9 @@ class AcpSession {
|
|
|
14251
14389
|
let modes;
|
|
14252
14390
|
let configOptions;
|
|
14253
14391
|
let resumed = false;
|
|
14254
|
-
if (sessionId && conn.
|
|
14255
|
-
this.loading = true;
|
|
14392
|
+
if (sessionId && conn.resumeSession) {
|
|
14256
14393
|
try {
|
|
14257
|
-
const r = await conn.ctx.request(methods.agent.session.
|
|
14394
|
+
const r = await conn.ctx.request(methods.agent.session.resume, {
|
|
14258
14395
|
sessionId,
|
|
14259
14396
|
cwd: this.d.cwd,
|
|
14260
14397
|
mcpServers: [],
|
|
@@ -14267,8 +14404,6 @@ class AcpSession {
|
|
|
14267
14404
|
if (isAuthRequired(e))
|
|
14268
14405
|
throw e;
|
|
14269
14406
|
log.warn(this.d.worktreeId, `could not resume agent session ${sessionId}; starting a new one`, e);
|
|
14270
|
-
} finally {
|
|
14271
|
-
this.loading = false;
|
|
14272
14407
|
}
|
|
14273
14408
|
}
|
|
14274
14409
|
if (!resumed) {
|
|
@@ -14365,8 +14500,6 @@ class AcpSession {
|
|
|
14365
14500
|
this.setCommands(mapped);
|
|
14366
14501
|
return;
|
|
14367
14502
|
}
|
|
14368
|
-
if (this.loading)
|
|
14369
|
-
return;
|
|
14370
14503
|
const listener = side.get(params.sessionId);
|
|
14371
14504
|
if (listener) {
|
|
14372
14505
|
const u = params.update;
|
|
@@ -14384,7 +14517,10 @@ class AcpSession {
|
|
|
14384
14517
|
}
|
|
14385
14518
|
if (params.update.sessionUpdate === "config_option_update")
|
|
14386
14519
|
this.absorb(live, params.update.configOptions);
|
|
14387
|
-
|
|
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) {
|
|
14388
14524
|
if (ev.type === "text-delta" || ev.type === "tool-start")
|
|
14389
14525
|
this.steered = [];
|
|
14390
14526
|
this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
|
|
@@ -14417,17 +14553,18 @@ class AcpSession {
|
|
|
14417
14553
|
}
|
|
14418
14554
|
return pickOption(params.options, verdict);
|
|
14419
14555
|
}
|
|
14420
|
-
askPermission(params) {
|
|
14556
|
+
async askPermission(params) {
|
|
14421
14557
|
const plan = params.toolCall.kind === "switch_mode";
|
|
14422
|
-
|
|
14423
|
-
|
|
14558
|
+
const detail = permissionDetail(params);
|
|
14559
|
+
const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
|
|
14424
14560
|
const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
|
|
14425
14561
|
const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
|
|
14426
14562
|
return this.openAsk((id) => ({
|
|
14427
14563
|
type: "agent-permission",
|
|
14428
14564
|
id,
|
|
14429
14565
|
title: params.toolCall.title ?? params.toolCall.name ?? "the agent needs a decision",
|
|
14430
|
-
...
|
|
14566
|
+
...detail ? { detail } : {},
|
|
14567
|
+
...planPath ? { plan: planPath } : {},
|
|
14431
14568
|
choices,
|
|
14432
14569
|
...params.toolCall.toolCallId ? { toolId: params.toolCall.toolCallId } : {},
|
|
14433
14570
|
ts: Date.now()
|
|
@@ -14438,10 +14575,17 @@ class AcpSession {
|
|
|
14438
14575
|
return { outcome: { outcome: "cancelled" } };
|
|
14439
14576
|
if (plan && (choice.kind === "allow_once" || choice.kind === "allow_always")) {
|
|
14440
14577
|
this.d.setMode?.(modeAfterPlan(choice.name));
|
|
14578
|
+
if (planPath)
|
|
14579
|
+
fireAndForget(this.d.worktreeId, this.sayPlanEdited(detail, planPath), "edited plan");
|
|
14441
14580
|
}
|
|
14442
14581
|
return { outcome: { outcome: "selected", optionId: choice.id } };
|
|
14443
14582
|
});
|
|
14444
14583
|
}
|
|
14584
|
+
async sayPlanEdited(proposed, path) {
|
|
14585
|
+
if (!await this.d.planEdited?.(path, proposed))
|
|
14586
|
+
return;
|
|
14587
|
+
this.send(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
|
|
14588
|
+
}
|
|
14445
14589
|
onElicit(params, signal) {
|
|
14446
14590
|
const live = this.live;
|
|
14447
14591
|
if (!live || !("sessionId" in params) || params.sessionId !== live.sessionId) {
|
|
@@ -14499,21 +14643,56 @@ class AcpSession {
|
|
|
14499
14643
|
syncStatus() {
|
|
14500
14644
|
if (this.status === "error")
|
|
14501
14645
|
return;
|
|
14502
|
-
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();
|
|
14503
14682
|
}
|
|
14504
14683
|
maybeArmReaper() {
|
|
14505
|
-
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)
|
|
14506
14685
|
this.armReaper();
|
|
14507
14686
|
}
|
|
14508
14687
|
armReaper() {
|
|
14509
14688
|
this.clearReaper();
|
|
14689
|
+
const idleMs = this.d.idleMs ?? DEFAULT_IDLE_MS;
|
|
14510
14690
|
const t = setTimeout(() => {
|
|
14511
14691
|
this.reaper = null;
|
|
14512
|
-
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)
|
|
14513
14693
|
return;
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
}, this.d.idleMs ?? DEFAULT_IDLE_MS);
|
|
14694
|
+
fireAndForget(this.d.worktreeId, this.dropConn(`idle for ${Math.round(idleMs / 60000)} min`), "reap agent");
|
|
14695
|
+
}, idleMs);
|
|
14517
14696
|
t.unref?.();
|
|
14518
14697
|
this.reaper = t;
|
|
14519
14698
|
}
|
|
@@ -14522,15 +14701,17 @@ class AcpSession {
|
|
|
14522
14701
|
clearTimeout(this.reaper);
|
|
14523
14702
|
this.reaper = null;
|
|
14524
14703
|
}
|
|
14525
|
-
async dropConn() {
|
|
14704
|
+
async dropConn(why) {
|
|
14526
14705
|
this.cancelAsks();
|
|
14527
14706
|
const conn = this.conn;
|
|
14528
14707
|
this.conn = null;
|
|
14529
14708
|
this.live = null;
|
|
14530
14709
|
if (!conn)
|
|
14531
14710
|
return;
|
|
14711
|
+
log.info(this.d.worktreeId, `agent ${conn.spec.id} stopped: ${why} (pid ${conn.link.pid ?? "unknown"})`);
|
|
14532
14712
|
conn.link.conn.close();
|
|
14533
14713
|
await conn.link.kill();
|
|
14714
|
+
this.noteProcess(conn.link, false);
|
|
14534
14715
|
}
|
|
14535
14716
|
}
|
|
14536
14717
|
function isAuthRequired(e) {
|
|
@@ -14593,6 +14774,217 @@ function sameCommands(a, b) {
|
|
|
14593
14774
|
return a.length === b.length && a.every((c, i) => c.name === b[i]?.name && c.description === b[i]?.description && c.hint === b[i]?.hint);
|
|
14594
14775
|
}
|
|
14595
14776
|
|
|
14777
|
+
// ../daemon/src/agent/planDoc.ts
|
|
14778
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
14779
|
+
import { join as join7 } from "path";
|
|
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
|
+
}
|
|
14796
|
+
async function writePlanDoc(cwd, markdown) {
|
|
14797
|
+
const dir = join7(cwd, PLANS_DIR);
|
|
14798
|
+
const rel = join7(PLANS_DIR, `${nextNumber(dir)}.md`);
|
|
14799
|
+
try {
|
|
14800
|
+
mkdirSync5(dir, { recursive: true });
|
|
14801
|
+
writeFileSync4(join7(cwd, rel), markdown.endsWith(`
|
|
14802
|
+
`) ? markdown : `${markdown}
|
|
14803
|
+
`);
|
|
14804
|
+
} catch (e) {
|
|
14805
|
+
log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
|
|
14806
|
+
return null;
|
|
14807
|
+
}
|
|
14808
|
+
await excludeFromGit(cwd, `${PLANS_DIR}/`);
|
|
14809
|
+
return rel;
|
|
14810
|
+
}
|
|
14811
|
+
function planEdited(cwd, rel, proposed) {
|
|
14812
|
+
const file = join7(cwd, rel);
|
|
14813
|
+
if (!existsSync8(file))
|
|
14814
|
+
return false;
|
|
14815
|
+
try {
|
|
14816
|
+
return readFileSync7(file, "utf8").trim() !== proposed.trim();
|
|
14817
|
+
} catch (e) {
|
|
14818
|
+
log.warn(cwd, `could not read ${rel}`, e);
|
|
14819
|
+
return false;
|
|
14820
|
+
}
|
|
14821
|
+
}
|
|
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
|
+
|
|
14596
14988
|
// ../daemon/src/runtime/ports.ts
|
|
14597
14989
|
var allocated = new Set;
|
|
14598
14990
|
var range = cloud.proxyPorts;
|
|
@@ -14705,7 +15097,7 @@ function expandEnv(env, vars) {
|
|
|
14705
15097
|
|
|
14706
15098
|
// ../daemon/src/core/remote.ts
|
|
14707
15099
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
14708
|
-
import { existsSync as
|
|
15100
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
14709
15101
|
function loadRemote(file, env = process.env) {
|
|
14710
15102
|
if (env.TOYON_CLOUD === "1") {
|
|
14711
15103
|
const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
|
|
@@ -14718,9 +15110,9 @@ function loadRemote(file, env = process.env) {
|
|
|
14718
15110
|
throw new Error(`TOYON_PREVIEWS: ${why}`);
|
|
14719
15111
|
return { host, previews, front: "edge" };
|
|
14720
15112
|
}
|
|
14721
|
-
if (!
|
|
15113
|
+
if (!existsSync9(file))
|
|
14722
15114
|
return null;
|
|
14723
|
-
const view = parseRemote(
|
|
15115
|
+
const view = parseRemote(readFileSync8(file, "utf8"));
|
|
14724
15116
|
if (!view) {
|
|
14725
15117
|
log.warn("remote", `${file} names no valid host or previews; remote access is off`);
|
|
14726
15118
|
return null;
|
|
@@ -15381,7 +15773,7 @@ class WorktreeProcs {
|
|
|
15381
15773
|
async killProc(mp) {
|
|
15382
15774
|
await mp.pty?.kill();
|
|
15383
15775
|
}
|
|
15384
|
-
async sleep() {
|
|
15776
|
+
async sleep(why) {
|
|
15385
15777
|
if (this.asleep || this.stopped)
|
|
15386
15778
|
return;
|
|
15387
15779
|
this.asleep = true;
|
|
@@ -15389,6 +15781,7 @@ class WorktreeProcs {
|
|
|
15389
15781
|
for (const mp of this.procs.values()) {
|
|
15390
15782
|
mp.state.status = "asleep";
|
|
15391
15783
|
mp.state.pid = undefined;
|
|
15784
|
+
mp.state.detail = why;
|
|
15392
15785
|
this.onProc({ ...mp.state });
|
|
15393
15786
|
exits.push(this.killProc(mp));
|
|
15394
15787
|
}
|
|
@@ -15403,13 +15796,16 @@ class WorktreeProcs {
|
|
|
15403
15796
|
this.spawnProc(mp);
|
|
15404
15797
|
}
|
|
15405
15798
|
}
|
|
15406
|
-
|
|
15799
|
+
groups() {
|
|
15407
15800
|
const out = [];
|
|
15408
|
-
for (const mp of this.procs
|
|
15801
|
+
for (const [name, mp] of this.procs)
|
|
15409
15802
|
if (mp.pty?.alive)
|
|
15410
|
-
out.push(mp.pty.pid);
|
|
15803
|
+
out.push({ name, pgid: mp.pty.pid });
|
|
15411
15804
|
return out;
|
|
15412
15805
|
}
|
|
15806
|
+
pgids() {
|
|
15807
|
+
return this.groups().map((g) => g.pgid);
|
|
15808
|
+
}
|
|
15413
15809
|
async stopAll() {
|
|
15414
15810
|
this.stopped = true;
|
|
15415
15811
|
const exits = [];
|
|
@@ -15478,11 +15874,12 @@ function terminalEnv(base, own, urls) {
|
|
|
15478
15874
|
var DEFAULT_AGENT_ID = "claude";
|
|
15479
15875
|
var PREVIEW_WAKE_MS = 8000;
|
|
15480
15876
|
var PREVIEW_POLL_MS = 100;
|
|
15481
|
-
function defaultAgent(wt, d, preview) {
|
|
15877
|
+
function defaultAgent(wt, d, preview, onProcess) {
|
|
15482
15878
|
const agent = new AcpSession({
|
|
15483
15879
|
worktreeId: wt.id,
|
|
15484
15880
|
cwd: wt.path,
|
|
15485
15881
|
preview: () => previewContext(preview()),
|
|
15882
|
+
onProcess,
|
|
15486
15883
|
spec: () => {
|
|
15487
15884
|
const w = d.state.requireWorktree(wt.id);
|
|
15488
15885
|
if (!w.agent) {
|
|
@@ -15512,13 +15909,22 @@ function defaultAgent(wt, d, preview) {
|
|
|
15512
15909
|
d.state.save();
|
|
15513
15910
|
d.hub.emit("worktreesChanged");
|
|
15514
15911
|
},
|
|
15515
|
-
onPlan: () => {
|
|
15912
|
+
onPlan: async (markdown) => {
|
|
15516
15913
|
const w = d.state.worktree(wt.id);
|
|
15517
|
-
if (
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15914
|
+
if (w && !w.planned) {
|
|
15915
|
+
w.planned = true;
|
|
15916
|
+
d.state.save();
|
|
15917
|
+
}
|
|
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;
|
|
15521
15926
|
},
|
|
15927
|
+
planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
|
|
15522
15928
|
option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
|
|
15523
15929
|
onOptionsLearned: (category, choices) => {
|
|
15524
15930
|
const agentId = d.state.requireWorktree(wt.id).agent ?? "";
|
|
@@ -15568,8 +15974,10 @@ class RuntimeRegistry {
|
|
|
15568
15974
|
holds = new Map;
|
|
15569
15975
|
settingUp = new Set;
|
|
15570
15976
|
starting = new Set;
|
|
15977
|
+
stopping = new Map;
|
|
15571
15978
|
constructor(deps) {
|
|
15572
15979
|
this.deps = deps;
|
|
15980
|
+
deps.hub.on("proc", (id) => this.recordGroups(id));
|
|
15573
15981
|
}
|
|
15574
15982
|
hold(id, tag) {
|
|
15575
15983
|
let tags = this.holds.get(id);
|
|
@@ -15638,16 +16046,18 @@ class RuntimeRegistry {
|
|
|
15638
16046
|
if (repo)
|
|
15639
16047
|
await this.start(wt, repo);
|
|
15640
16048
|
}
|
|
15641
|
-
async sleep(id) {
|
|
16049
|
+
async sleep(id, why) {
|
|
15642
16050
|
const rt = this.runtimes.get(id);
|
|
15643
16051
|
if (!rt?.procs || rt.procs.asleep)
|
|
15644
16052
|
return;
|
|
16053
|
+
log.info(id, `asleep: ${why}`);
|
|
15645
16054
|
if (this.ports.ranged()) {
|
|
15646
16055
|
rt.proxy?.stop();
|
|
15647
16056
|
rt.proxy = null;
|
|
15648
16057
|
this.returnLease(id);
|
|
15649
16058
|
}
|
|
15650
|
-
await rt.procs.sleep();
|
|
16059
|
+
await rt.procs.sleep(why);
|
|
16060
|
+
this.recordGroups(id);
|
|
15651
16061
|
this.deps.hub.emit("worktreesChanged");
|
|
15652
16062
|
}
|
|
15653
16063
|
get ports() {
|
|
@@ -15704,11 +16114,15 @@ class RuntimeRegistry {
|
|
|
15704
16114
|
args: ["-l"]
|
|
15705
16115
|
};
|
|
15706
16116
|
try {
|
|
15707
|
-
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
|
+
});
|
|
15708
16121
|
} catch (e) {
|
|
15709
16122
|
throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
|
|
15710
16123
|
}
|
|
15711
16124
|
this.looseShells.set(id, term);
|
|
16125
|
+
this.recordGroups(id);
|
|
15712
16126
|
} else if (term.cols !== cols || term.rows !== rows) {
|
|
15713
16127
|
term.resize(cols, rows);
|
|
15714
16128
|
}
|
|
@@ -15723,6 +16137,7 @@ class RuntimeRegistry {
|
|
|
15723
16137
|
continue;
|
|
15724
16138
|
this.looseShells.delete(id);
|
|
15725
16139
|
fireAndForget(id, Promise.resolve(term.kill()), "loose shell cleanup");
|
|
16140
|
+
this.recordGroups(id);
|
|
15726
16141
|
}
|
|
15727
16142
|
}
|
|
15728
16143
|
get(id) {
|
|
@@ -15744,7 +16159,7 @@ class RuntimeRegistry {
|
|
|
15744
16159
|
return existing;
|
|
15745
16160
|
const rt = {
|
|
15746
16161
|
info: wt,
|
|
15747
|
-
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)),
|
|
15748
16163
|
procs: null,
|
|
15749
16164
|
proxy: null,
|
|
15750
16165
|
previewName: undefined,
|
|
@@ -15760,6 +16175,8 @@ class RuntimeRegistry {
|
|
|
15760
16175
|
const rt = this.ensureAgent(wt);
|
|
15761
16176
|
if (rt.procs)
|
|
15762
16177
|
return;
|
|
16178
|
+
if (wt.kind === "main" && this.deps.mainLeads && !this.deps.mainLeads(wt.repoId))
|
|
16179
|
+
return;
|
|
15763
16180
|
const live = this.deps.state.requireWorktree(wt.id);
|
|
15764
16181
|
const port = this.leasePort(live);
|
|
15765
16182
|
if (port === null)
|
|
@@ -15810,12 +16227,11 @@ class RuntimeRegistry {
|
|
|
15810
16227
|
leased = new Map;
|
|
15811
16228
|
leasePort(wt) {
|
|
15812
16229
|
let port = this.ports.lease(wt.proxyPort);
|
|
15813
|
-
if (port === null && wt.kind !== "spare") {
|
|
16230
|
+
if (port === null && (wt.kind !== "spare" || this.deps.shown?.(wt.id))) {
|
|
15814
16231
|
const holders = [...this.leased.keys()].filter((id) => id !== wt.id && !this.starting.has(id) && !this.busy(id) && !this.deps.viewed?.(id));
|
|
15815
16232
|
const victim = oldestViewed(holders, this.deps.state);
|
|
15816
16233
|
if (victim) {
|
|
15817
|
-
|
|
15818
|
-
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");
|
|
15819
16235
|
port = this.ports.lease(wt.proxyPort);
|
|
15820
16236
|
}
|
|
15821
16237
|
if (port === null) {
|
|
@@ -15843,6 +16259,7 @@ class RuntimeRegistry {
|
|
|
15843
16259
|
proxy?.stop();
|
|
15844
16260
|
this.returnLease(id);
|
|
15845
16261
|
await procs?.stopAll();
|
|
16262
|
+
this.recordGroups(id);
|
|
15846
16263
|
}
|
|
15847
16264
|
async stop(id) {
|
|
15848
16265
|
this.holds.delete(id);
|
|
@@ -15851,11 +16268,18 @@ class RuntimeRegistry {
|
|
|
15851
16268
|
if (!rt)
|
|
15852
16269
|
return;
|
|
15853
16270
|
this.runtimes.delete(id);
|
|
16271
|
+
this.stopping.set(id, rt);
|
|
15854
16272
|
rt.proxy?.stop();
|
|
15855
16273
|
this.returnLease(id);
|
|
15856
16274
|
const login = rt.login;
|
|
15857
16275
|
rt.login = null;
|
|
15858
|
-
|
|
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
|
+
}
|
|
15859
16283
|
}
|
|
15860
16284
|
openTerminal(id, stream, cols, rows) {
|
|
15861
16285
|
if (stream === LOGIN_STREAM)
|
|
@@ -15863,12 +16287,10 @@ class RuntimeRegistry {
|
|
|
15863
16287
|
if (stream !== SHELL_STREAM)
|
|
15864
16288
|
return this.openProcStream(id, stream, cols, rows);
|
|
15865
16289
|
const wt = this.deps.state.requireWorktree(id);
|
|
15866
|
-
if (wt.kind === "spare")
|
|
15867
|
-
throw new UserError("no terminal for a spare worktree");
|
|
15868
16290
|
const rt = this.ensureAgent(wt);
|
|
15869
16291
|
let term = rt.shell;
|
|
15870
16292
|
if (!term?.alive) {
|
|
15871
|
-
const cwd = wt.
|
|
16293
|
+
const cwd = wt.path;
|
|
15872
16294
|
const opts = {
|
|
15873
16295
|
cwd,
|
|
15874
16296
|
env: { ...this.shellEnv(wt), PWD: cwd },
|
|
@@ -15878,11 +16300,15 @@ class RuntimeRegistry {
|
|
|
15878
16300
|
args: ["-l"]
|
|
15879
16301
|
};
|
|
15880
16302
|
try {
|
|
15881
|
-
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
|
+
});
|
|
15882
16307
|
} catch (e) {
|
|
15883
16308
|
throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
|
|
15884
16309
|
}
|
|
15885
16310
|
rt.shell = term;
|
|
16311
|
+
this.recordGroups(wt.id);
|
|
15886
16312
|
} else if (term.cols !== cols || term.rows !== rows) {
|
|
15887
16313
|
term.resize(cols, rows);
|
|
15888
16314
|
}
|
|
@@ -15895,7 +16321,7 @@ class RuntimeRegistry {
|
|
|
15895
16321
|
rt.login = null;
|
|
15896
16322
|
if (prev)
|
|
15897
16323
|
fireAndForget(id, Promise.resolve(prev.pty.kill()), "replace the agent login");
|
|
15898
|
-
const cwd = wt.
|
|
16324
|
+
const cwd = wt.path;
|
|
15899
16325
|
let pty;
|
|
15900
16326
|
try {
|
|
15901
16327
|
pty = (this.deps.makeTerminal ?? defaultTerminal)(wt, {
|
|
@@ -15907,6 +16333,7 @@ class RuntimeRegistry {
|
|
|
15907
16333
|
args: run.args
|
|
15908
16334
|
}, (data) => this.deps.hub.emit("termData", id, LOGIN_STREAM, data), (code) => {
|
|
15909
16335
|
this.deps.hub.emit("termExit", id, LOGIN_STREAM, code);
|
|
16336
|
+
this.recordGroups(id);
|
|
15910
16337
|
if (rt.login?.pty !== pty)
|
|
15911
16338
|
return;
|
|
15912
16339
|
if (code === 0) {
|
|
@@ -15919,6 +16346,7 @@ class RuntimeRegistry {
|
|
|
15919
16346
|
throw new UserError(`could not start the login: ${e instanceof Error ? e.message : String(e)}`);
|
|
15920
16347
|
}
|
|
15921
16348
|
rt.login = { pty, run };
|
|
16349
|
+
this.recordGroups(id);
|
|
15922
16350
|
this.deps.hub.emit("worktreesChanged");
|
|
15923
16351
|
}
|
|
15924
16352
|
openLoginStream(id, cols, rows) {
|
|
@@ -15994,12 +16422,93 @@ class RuntimeRegistry {
|
|
|
15994
16422
|
const url = `http://${host.includes(":") ? `[${host}]` : host}:${st.boundPort ?? st.port}`;
|
|
15995
16423
|
return { status: st.status, url, ...st.detail ? { detail: st.detail } : {} };
|
|
15996
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
|
+
}
|
|
15997
16468
|
async shutdown() {
|
|
15998
16469
|
this.pruneLooseShells(new Set);
|
|
15999
16470
|
await Promise.all([...this.runtimes.keys()].map((id) => this.stop(id)));
|
|
16000
16471
|
}
|
|
16001
16472
|
}
|
|
16002
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
|
+
|
|
16003
16512
|
// ../daemon/src/agent/recap.ts
|
|
16004
16513
|
var REPLY_KEEP = 2000;
|
|
16005
16514
|
var CLIP = 160;
|
|
@@ -16015,6 +16524,7 @@ function turnsSince(entries, seenAt) {
|
|
|
16015
16524
|
let pending = [];
|
|
16016
16525
|
let open = null;
|
|
16017
16526
|
const writes = new Set;
|
|
16527
|
+
const refusals = new Map;
|
|
16018
16528
|
const start = (ts) => {
|
|
16019
16529
|
const t = slice(pending, ts);
|
|
16020
16530
|
pending = [];
|
|
@@ -16035,16 +16545,35 @@ function turnsSince(entries, seenAt) {
|
|
|
16035
16545
|
open = start(e.ts);
|
|
16036
16546
|
break;
|
|
16037
16547
|
case "text-delta":
|
|
16038
|
-
if (open)
|
|
16548
|
+
if (open) {
|
|
16039
16549
|
open.reply = (open.reply + e.text).slice(-REPLY_KEEP);
|
|
16550
|
+
open.refused = undefined;
|
|
16551
|
+
}
|
|
16040
16552
|
break;
|
|
16041
16553
|
case "tool-start":
|
|
16042
16554
|
if (!open)
|
|
16043
16555
|
break;
|
|
16044
16556
|
if (!e.parentToolId)
|
|
16045
16557
|
open.reply = "";
|
|
16558
|
+
open.refused = undefined;
|
|
16046
16559
|
countWrite(open, writes, e.toolId, e);
|
|
16047
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
|
+
}
|
|
16048
16577
|
case "tool-update":
|
|
16049
16578
|
if (open && e.kind)
|
|
16050
16579
|
countWrite(open, writes, e.toolId, { name: e.name ?? "", kind: e.kind });
|
|
@@ -16101,7 +16630,8 @@ function factsOf(turns, end, ask) {
|
|
|
16101
16630
|
...end === "failed" && last?.error ? { error: last.error } : {},
|
|
16102
16631
|
...end === "failed" && last?.auth ? { auth: true } : {},
|
|
16103
16632
|
...end === "done" && cut ? { cut } : {},
|
|
16104
|
-
...end === "asking" && ask ? { ask } : {}
|
|
16633
|
+
...end === "asking" && ask ? { ask } : {},
|
|
16634
|
+
...end === "stopped" && last?.refused ? { planBack: true } : {}
|
|
16105
16635
|
};
|
|
16106
16636
|
}
|
|
16107
16637
|
function firstAskOf(entries) {
|
|
@@ -16110,59 +16640,6 @@ function firstAskOf(entries) {
|
|
|
16110
16640
|
return e.text;
|
|
16111
16641
|
return;
|
|
16112
16642
|
}
|
|
16113
|
-
var RECAP_SYSTEM = "You write one-line status recaps for a developer coming back to a coding task. Reply with only the recap.";
|
|
16114
|
-
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.";
|
|
16115
|
-
var RECAP_TURNS = 6;
|
|
16116
|
-
var RECAP_CHARS = 4000;
|
|
16117
|
-
function recapPrompt(i) {
|
|
16118
|
-
const head = [RECAP_ASK, "", `Task: ${clip(i.title, 200)}`];
|
|
16119
|
-
if (i.firstAsk)
|
|
16120
|
-
head.push(`First request: ${clip(i.firstAsk, 300)}`);
|
|
16121
|
-
const now = `Now: ${standing(i.end, i.facts)}`;
|
|
16122
|
-
let blocks = i.turns.slice(-RECAP_TURNS).map(turnBlock);
|
|
16123
|
-
const size = () => [...head, "", ...blocks, "", now].join(`
|
|
16124
|
-
|
|
16125
|
-
`).length;
|
|
16126
|
-
while (blocks.length > 1 && size() > RECAP_CHARS)
|
|
16127
|
-
blocks = blocks.slice(1);
|
|
16128
|
-
return [head.join(`
|
|
16129
|
-
`), blocks.join(`
|
|
16130
|
-
|
|
16131
|
-
`), now].join(`
|
|
16132
|
-
|
|
16133
|
-
`);
|
|
16134
|
-
}
|
|
16135
|
-
function turnBlock(t) {
|
|
16136
|
-
const facts = [t.edits ? plural(t.edits, "edit") : "", t.toolErrors ? plural(t.toolErrors, "failed tool") : ""].filter(Boolean).join(", ");
|
|
16137
|
-
const lines = [];
|
|
16138
|
-
if (t.asks.length)
|
|
16139
|
-
lines.push(`You asked: ${clip(t.asks.join(" / "), 400)}`);
|
|
16140
|
-
if (t.reply.trim())
|
|
16141
|
-
lines.push(`Agent ended with: ${tail(t.reply, 800)}`);
|
|
16142
|
-
if (facts)
|
|
16143
|
-
lines.push(`Facts: ${facts}`);
|
|
16144
|
-
return lines.join(`
|
|
16145
|
-
`);
|
|
16146
|
-
}
|
|
16147
|
-
function standing(end, f) {
|
|
16148
|
-
switch (end) {
|
|
16149
|
-
case "asking":
|
|
16150
|
-
return f.ask ? `waiting for the user's answer to "${f.ask}"` : "waiting for the user";
|
|
16151
|
-
case "failed":
|
|
16152
|
-
return f.error ? `failed: ${f.error}` : "failed";
|
|
16153
|
-
case "stopped":
|
|
16154
|
-
return "stopped before it finished";
|
|
16155
|
-
case "done":
|
|
16156
|
-
return f.cut ? `finished early (${f.cut})` : "finished";
|
|
16157
|
-
}
|
|
16158
|
-
}
|
|
16159
|
-
function plural(n, word) {
|
|
16160
|
-
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
16161
|
-
}
|
|
16162
|
-
function tail(text, max) {
|
|
16163
|
-
const line = text.replace(/\s+/g, " ").trim();
|
|
16164
|
-
return line.length > max ? `\u2026${line.slice(-(max - 1))}` : line;
|
|
16165
|
-
}
|
|
16166
16643
|
var RECAP_MAX = 160;
|
|
16167
16644
|
function parseRecap(text) {
|
|
16168
16645
|
if (!text)
|
|
@@ -16179,15 +16656,17 @@ function parseRecap(text) {
|
|
|
16179
16656
|
}
|
|
16180
16657
|
|
|
16181
16658
|
// ../daemon/src/agent/landing.ts
|
|
16182
|
-
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.";
|
|
16183
16660
|
var LAND_ASK = [
|
|
16184
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.",
|
|
16185
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.",
|
|
16186
16663
|
"Reply with exactly this shape and nothing else:",
|
|
16187
|
-
"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>",
|
|
16188
16665
|
"Line 2: blank",
|
|
16189
|
-
"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.",
|
|
16190
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",
|
|
16191
16670
|
"Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
|
|
16192
16671
|
].join(`
|
|
16193
16672
|
`);
|
|
@@ -16203,7 +16682,7 @@ ${i.recentSubjects.map((s) => ` ${s}`).join(`
|
|
|
16203
16682
|
`)}`);
|
|
16204
16683
|
const diff = `Diff summary:
|
|
16205
16684
|
${i.diffStat.trim() || "(none)"}`;
|
|
16206
|
-
let blocks = i.turns.slice(-LAND_TURNS).map(
|
|
16685
|
+
let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
|
|
16207
16686
|
const size = () => [...head, "", ...blocks, "", diff].join(`
|
|
16208
16687
|
|
|
16209
16688
|
`).length;
|
|
@@ -16216,13 +16695,36 @@ ${i.diffStat.trim() || "(none)"}`;
|
|
|
16216
16695
|
|
|
16217
16696
|
`);
|
|
16218
16697
|
}
|
|
16219
|
-
function
|
|
16698
|
+
function turnBlock(t) {
|
|
16220
16699
|
const lines = [];
|
|
16221
16700
|
if (t.asks.length)
|
|
16222
16701
|
lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
|
|
16223
16702
|
if (t.reply.trim())
|
|
16224
16703
|
lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
|
|
16225
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
|
+
|
|
16226
16728
|
`);
|
|
16227
16729
|
}
|
|
16228
16730
|
var SUBJECT_MAX = 72;
|
|
@@ -16239,13 +16741,17 @@ function parseLanding(text) {
|
|
|
16239
16741
|
const rest = lines.slice(lines.indexOf(lines.find((l) => l.trim() === first) ?? "") + 1);
|
|
16240
16742
|
const paragraphs = rest.join(`
|
|
16241
16743
|
`).split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
16242
|
-
const
|
|
16243
|
-
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(`
|
|
16244
16749
|
|
|
16245
16750
|
`).replace(/^(body)\s*:\s*/i, "").trim();
|
|
16246
16751
|
return {
|
|
16247
16752
|
ready: verdict === "ready",
|
|
16248
16753
|
...why ? { why: clip(why, 120) } : {},
|
|
16754
|
+
...recap ? { recap } : {},
|
|
16249
16755
|
...subject && subject.split(" ").length >= 2 ? { subject } : {},
|
|
16250
16756
|
...body ? { body: body.slice(0, 1500) } : {}
|
|
16251
16757
|
};
|
|
@@ -16299,7 +16805,7 @@ async function askFreshAgent(agents, agentId, cwd, system, prompt, opts = {}) {
|
|
|
16299
16805
|
|
|
16300
16806
|
// ../daemon/src/agent/tasks.ts
|
|
16301
16807
|
var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
|
|
16302
|
-
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.
|
|
16303
16809
|
|
|
16304
16810
|
Task: ${task.slice(0, 500)}`;
|
|
16305
16811
|
function taskText(text, attachments = []) {
|
|
@@ -16321,13 +16827,13 @@ function parseName(text) {
|
|
|
16321
16827
|
return null;
|
|
16322
16828
|
const line = text.trim().split(`
|
|
16323
16829
|
`).at(-1) ?? "";
|
|
16324
|
-
const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[
|
|
16325
|
-
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);
|
|
16326
16832
|
if (words.length < 1 || words.length > 3 || name.length < 3 || name.length > 20)
|
|
16327
16833
|
return null;
|
|
16328
16834
|
if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
|
|
16329
16835
|
return null;
|
|
16330
|
-
return name;
|
|
16836
|
+
return sentenceCase(name);
|
|
16331
16837
|
}
|
|
16332
16838
|
var PLAN_SYSTEM = "You are a task-planning assistant. Reply with only the requested JSON.";
|
|
16333
16839
|
var planPrompt = (request) => [
|
|
@@ -16357,15 +16863,6 @@ function parsePlan(text) {
|
|
|
16357
16863
|
function makeNamer(runtime) {
|
|
16358
16864
|
return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
|
|
16359
16865
|
}
|
|
16360
|
-
function makeRecapper(runtime, agents, state) {
|
|
16361
|
-
return async (wt, prompt) => {
|
|
16362
|
-
const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
|
|
16363
|
-
if (!spec || lacksQuickModel(spec, state))
|
|
16364
|
-
return null;
|
|
16365
|
-
const agent = runtime.agentFor(wt.id);
|
|
16366
|
-
return agent ? parseRecap(await agent.ask(RECAP_SYSTEM, prompt, { quick: "require" })) : null;
|
|
16367
|
-
};
|
|
16368
|
-
}
|
|
16369
16866
|
function lacksQuickModel(spec, state) {
|
|
16370
16867
|
const quick = spec.quickModel;
|
|
16371
16868
|
if (!quick)
|
|
@@ -16375,43 +16872,51 @@ function lacksQuickModel(spec, state) {
|
|
|
16375
16872
|
const offered = state.cachedOptions(spec.id, "model");
|
|
16376
16873
|
return offered.length > 0 && !offered.some((m) => m.id === quick);
|
|
16377
16874
|
}
|
|
16378
|
-
function
|
|
16379
|
-
return async (wt, prompt) => {
|
|
16875
|
+
function makeQuickAsker(runtime, agents, state) {
|
|
16876
|
+
return async (wt, system, prompt) => {
|
|
16380
16877
|
const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
|
|
16381
16878
|
if (!spec || lacksQuickModel(spec, state))
|
|
16382
16879
|
return null;
|
|
16383
16880
|
const agent = runtime.agentFor(wt.id);
|
|
16384
|
-
return agent ?
|
|
16881
|
+
return agent ? agent.ask(system, prompt, { quick: "require" }) : null;
|
|
16385
16882
|
};
|
|
16386
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
|
+
}
|
|
16387
16892
|
function makePlanner(agents) {
|
|
16388
16893
|
return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
|
|
16389
16894
|
}
|
|
16390
16895
|
|
|
16391
16896
|
// ../daemon/src/core/assets.ts
|
|
16392
|
-
import { existsSync as
|
|
16393
|
-
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";
|
|
16394
16899
|
function locateAssets(here, env = process.env) {
|
|
16395
|
-
const packaged =
|
|
16900
|
+
const packaged = existsSync11(join9(here, "shell", "index.html"));
|
|
16396
16901
|
return {
|
|
16397
|
-
shellDist: env.TOYON_SHELL_DIST ?? (packaged ?
|
|
16398
|
-
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")),
|
|
16399
16904
|
sourceRoot: packaged ? null : resolve5(here, "../../.."),
|
|
16400
|
-
packageJson: packaged ?
|
|
16905
|
+
packageJson: packaged ? join9(here, "..", "package.json") : null
|
|
16401
16906
|
};
|
|
16402
16907
|
}
|
|
16403
16908
|
var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
|
|
16404
16909
|
function pruneAssets(shellDist) {
|
|
16405
|
-
const dir =
|
|
16910
|
+
const dir = join9(shellDist, "assets");
|
|
16406
16911
|
let names;
|
|
16407
16912
|
try {
|
|
16408
|
-
names =
|
|
16913
|
+
names = readdirSync3(dir);
|
|
16409
16914
|
} catch {
|
|
16410
16915
|
return 0;
|
|
16411
16916
|
}
|
|
16412
16917
|
const found = [];
|
|
16413
16918
|
for (const name of names) {
|
|
16414
|
-
const path =
|
|
16919
|
+
const path = join9(dir, name);
|
|
16415
16920
|
try {
|
|
16416
16921
|
found.push({ path, mtime: statSync2(path).mtimeMs });
|
|
16417
16922
|
} catch {}
|
|
@@ -16432,7 +16937,7 @@ function pruneAssets(shellDist) {
|
|
|
16432
16937
|
}
|
|
16433
16938
|
|
|
16434
16939
|
// ../daemon/src/core/dialog.ts
|
|
16435
|
-
import { existsSync as
|
|
16940
|
+
import { existsSync as existsSync12 } from "fs";
|
|
16436
16941
|
import { homedir as homedir4 } from "os";
|
|
16437
16942
|
var PROMPTS = {
|
|
16438
16943
|
location: "Choose where the new project goes",
|
|
@@ -16450,7 +16955,7 @@ function folderDialog() {
|
|
|
16450
16955
|
let cancelled = false;
|
|
16451
16956
|
const ask = async (start, purpose) => {
|
|
16452
16957
|
const typed = start === "~" || start.startsWith("~/") ? `${homedir4()}${start.slice(1)}` : start;
|
|
16453
|
-
const from = typed.startsWith("/") &&
|
|
16958
|
+
const from = typed.startsWith("/") && existsSync12(typed) ? typed : homedir4();
|
|
16454
16959
|
cancelled = false;
|
|
16455
16960
|
const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
|
|
16456
16961
|
stdout: "pipe",
|
|
@@ -16554,12 +17059,12 @@ class Restarter {
|
|
|
16554
17059
|
d.hub.on("agentStatus", settle);
|
|
16555
17060
|
d.hub.on("worktreesChanged", settle);
|
|
16556
17061
|
}
|
|
16557
|
-
request() {
|
|
17062
|
+
request(opts = {}) {
|
|
16558
17063
|
const refused = this.d.refusal();
|
|
16559
17064
|
if (refused)
|
|
16560
17065
|
return refused;
|
|
16561
17066
|
this.asked = true;
|
|
16562
|
-
this.attempt();
|
|
17067
|
+
this.attempt(opts.now);
|
|
16563
17068
|
return null;
|
|
16564
17069
|
}
|
|
16565
17070
|
waitingOn() {
|
|
@@ -16568,13 +17073,18 @@ class Restarter {
|
|
|
16568
17073
|
return this.gone ? [] : this.working();
|
|
16569
17074
|
}
|
|
16570
17075
|
working() {
|
|
16571
|
-
return this.
|
|
17076
|
+
return this.titled("working");
|
|
17077
|
+
}
|
|
17078
|
+
asking() {
|
|
17079
|
+
return this.titled("waiting");
|
|
16572
17080
|
}
|
|
16573
|
-
|
|
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) {
|
|
16574
17085
|
if (this.gone)
|
|
16575
17086
|
return;
|
|
16576
|
-
|
|
16577
|
-
if (names.length === 0)
|
|
17087
|
+
if (now || this.working().length === 0)
|
|
16578
17088
|
this.gone = true;
|
|
16579
17089
|
const key = JSON.stringify(this.waitingOn());
|
|
16580
17090
|
if (key !== this.announced) {
|
|
@@ -16666,18 +17176,19 @@ class SelfWatch {
|
|
|
16666
17176
|
}
|
|
16667
17177
|
|
|
16668
17178
|
// ../daemon/src/core/state.ts
|
|
16669
|
-
import { randomBytes } from "crypto";
|
|
16670
|
-
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;
|
|
16671
17182
|
var SEEN_WORKTREES = 50;
|
|
16672
17183
|
var empty = { repos: [], worktrees: [], sessions: {} };
|
|
16673
17184
|
function loadState(paths) {
|
|
16674
17185
|
ensureDirs(paths);
|
|
16675
17186
|
const STATE_FILE = paths.stateFile;
|
|
16676
|
-
if (!
|
|
17187
|
+
if (!existsSync13(STATE_FILE))
|
|
16677
17188
|
return structuredClone(empty);
|
|
16678
17189
|
let raw;
|
|
16679
17190
|
try {
|
|
16680
|
-
raw =
|
|
17191
|
+
raw = readFileSync9(STATE_FILE, "utf8");
|
|
16681
17192
|
} catch (e) {
|
|
16682
17193
|
log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
|
|
16683
17194
|
return structuredClone(empty);
|
|
@@ -16688,7 +17199,7 @@ function loadState(paths) {
|
|
|
16688
17199
|
} catch (e) {
|
|
16689
17200
|
const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
|
|
16690
17201
|
try {
|
|
16691
|
-
|
|
17202
|
+
writeFileSync5(backup, raw);
|
|
16692
17203
|
} catch (be) {
|
|
16693
17204
|
log.warn("state", "could not write the corrupt-state backup", be);
|
|
16694
17205
|
}
|
|
@@ -16703,6 +17214,10 @@ function loadState(paths) {
|
|
|
16703
17214
|
if (!ids.has(id) && !id.startsWith("disc-"))
|
|
16704
17215
|
delete state.seen?.[id];
|
|
16705
17216
|
}
|
|
17217
|
+
for (const id of Object.keys(state.groups ?? {})) {
|
|
17218
|
+
if (!ids.has(id) && !id.startsWith("disc-"))
|
|
17219
|
+
delete state.groups?.[id];
|
|
17220
|
+
}
|
|
16706
17221
|
if (state.modelCache) {
|
|
16707
17222
|
state.optionCache ??= {};
|
|
16708
17223
|
for (const [agentId, models] of Object.entries(state.modelCache)) {
|
|
@@ -16715,22 +17230,23 @@ function loadState(paths) {
|
|
|
16715
17230
|
}
|
|
16716
17231
|
function saveState(paths, state) {
|
|
16717
17232
|
const tmp = `${paths.stateFile}.tmp`;
|
|
16718
|
-
|
|
17233
|
+
writeFileSync5(tmp, JSON.stringify(state, null, 2));
|
|
16719
17234
|
renameSync2(tmp, paths.stateFile);
|
|
16720
17235
|
}
|
|
16721
17236
|
function loadOrCreateToken(paths) {
|
|
16722
17237
|
ensureDirs(paths);
|
|
16723
17238
|
const TOKEN_FILE = paths.tokenFile;
|
|
16724
|
-
if (
|
|
16725
|
-
return
|
|
16726
|
-
const token =
|
|
16727
|
-
|
|
17239
|
+
if (existsSync13(TOKEN_FILE))
|
|
17240
|
+
return readFileSync9(TOKEN_FILE, "utf8").trim();
|
|
17241
|
+
const token = randomBytes2(32).toString("hex");
|
|
17242
|
+
writeFileSync5(TOKEN_FILE, token, { mode: 384 });
|
|
16728
17243
|
return token;
|
|
16729
17244
|
}
|
|
16730
17245
|
|
|
16731
17246
|
class StateStore {
|
|
16732
17247
|
paths;
|
|
16733
17248
|
state;
|
|
17249
|
+
groupsTimer = null;
|
|
16734
17250
|
constructor(paths, state) {
|
|
16735
17251
|
this.paths = paths;
|
|
16736
17252
|
this.state = state ?? loadState(paths);
|
|
@@ -16813,6 +17329,55 @@ class StateStore {
|
|
|
16813
17329
|
delete this.state.sessions[id];
|
|
16814
17330
|
if (this.state.seen)
|
|
16815
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;
|
|
16816
17381
|
this.save();
|
|
16817
17382
|
}
|
|
16818
17383
|
session(worktreeId) {
|
|
@@ -16873,12 +17438,15 @@ class StateStore {
|
|
|
16873
17438
|
this.save();
|
|
16874
17439
|
}
|
|
16875
17440
|
save() {
|
|
17441
|
+
if (this.groupsTimer)
|
|
17442
|
+
clearTimeout(this.groupsTimer);
|
|
17443
|
+
this.groupsTimer = null;
|
|
16876
17444
|
saveState(this.paths, this.state);
|
|
16877
17445
|
}
|
|
16878
17446
|
}
|
|
16879
17447
|
|
|
16880
17448
|
// ../daemon/src/design/service.ts
|
|
16881
|
-
import { join as
|
|
17449
|
+
import { join as join10 } from "path";
|
|
16882
17450
|
|
|
16883
17451
|
// ../daemon/src/design/scan.ts
|
|
16884
17452
|
var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
|
|
@@ -17328,7 +17896,7 @@ class DesignService {
|
|
|
17328
17896
|
async function readAll(root, paths) {
|
|
17329
17897
|
const out = [];
|
|
17330
17898
|
for (const path of paths) {
|
|
17331
|
-
const file = Bun.file(
|
|
17899
|
+
const file = Bun.file(join10(root, path));
|
|
17332
17900
|
if (file.size === 0 || file.size > MAX_FILE_BYTES)
|
|
17333
17901
|
continue;
|
|
17334
17902
|
try {
|
|
@@ -17443,14 +18011,14 @@ function typicalUse(classes) {
|
|
|
17443
18011
|
var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
|
|
17444
18012
|
async function loadProjectTypescript(root) {
|
|
17445
18013
|
try {
|
|
17446
|
-
return await import(
|
|
18014
|
+
return await import(join10(root, "node_modules", "typescript", "lib", "typescript.js"));
|
|
17447
18015
|
} catch {
|
|
17448
18016
|
return null;
|
|
17449
18017
|
}
|
|
17450
18018
|
}
|
|
17451
18019
|
|
|
17452
18020
|
// ../daemon/src/drafts/store.ts
|
|
17453
|
-
import { existsSync as
|
|
18021
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
17454
18022
|
var SAVE_DELAY_MS = 1000;
|
|
17455
18023
|
|
|
17456
18024
|
class DraftStore {
|
|
@@ -17459,10 +18027,10 @@ class DraftStore {
|
|
|
17459
18027
|
saveTimer = null;
|
|
17460
18028
|
constructor(d) {
|
|
17461
18029
|
this.d = d;
|
|
17462
|
-
if (!
|
|
18030
|
+
if (!existsSync14(d.file))
|
|
17463
18031
|
return;
|
|
17464
18032
|
try {
|
|
17465
|
-
const raw = JSON.parse(
|
|
18033
|
+
const raw = JSON.parse(readFileSync10(d.file, "utf8"));
|
|
17466
18034
|
if (!raw || typeof raw !== "object")
|
|
17467
18035
|
return;
|
|
17468
18036
|
for (const [id, text] of Object.entries(raw)) {
|
|
@@ -17479,6 +18047,9 @@ class DraftStore {
|
|
|
17479
18047
|
has(boxId) {
|
|
17480
18048
|
return !!this.drafts[boxId]?.trim();
|
|
17481
18049
|
}
|
|
18050
|
+
text(boxId) {
|
|
18051
|
+
return this.drafts[boxId] ?? "";
|
|
18052
|
+
}
|
|
17482
18053
|
set(boxId, text, clientId) {
|
|
17483
18054
|
if ((this.drafts[boxId] ?? "") === text)
|
|
17484
18055
|
return;
|
|
@@ -17522,7 +18093,7 @@ class DraftStore {
|
|
|
17522
18093
|
write() {
|
|
17523
18094
|
const tmp = `${this.d.file}.tmp`;
|
|
17524
18095
|
try {
|
|
17525
|
-
|
|
18096
|
+
writeFileSync6(tmp, JSON.stringify(this.drafts));
|
|
17526
18097
|
renameSync3(tmp, this.d.file);
|
|
17527
18098
|
} catch (e) {
|
|
17528
18099
|
log.error("drafts", "could not write the drafts", e);
|
|
@@ -17546,16 +18117,20 @@ class ExecService {
|
|
|
17546
18117
|
run(worktreeId, command) {
|
|
17547
18118
|
fireAndForget(worktreeId, this.exec(worktreeId, command), `exec ${command}`);
|
|
17548
18119
|
}
|
|
17549
|
-
exec(worktreeId, command, name = SHELL_TOOL) {
|
|
18120
|
+
exec(worktreeId, command, name = SHELL_TOOL, opts = {}) {
|
|
17550
18121
|
const wt = this.deps.state.requireWorktree(worktreeId);
|
|
17551
18122
|
if (wt.kind === "spare")
|
|
17552
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");
|
|
17553
18126
|
const agent = this.deps.runtime.agentFor(worktreeId);
|
|
17554
18127
|
if (!agent)
|
|
17555
18128
|
throw new UserError("worktree still starting; try again in a moment");
|
|
17556
18129
|
const toolId = `${name}-${Date.now().toString(36)}-${++this.n}`;
|
|
17557
|
-
|
|
17558
|
-
|
|
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;
|
|
17559
18134
|
const env = { ...this.deps.runtime.shellEnv(wt), PWD: cwd, TERM: "dumb", NO_COLOR: "1" };
|
|
17560
18135
|
let proc;
|
|
17561
18136
|
try {
|
|
@@ -17568,13 +18143,28 @@ class ExecService {
|
|
|
17568
18143
|
});
|
|
17569
18144
|
} catch (e) {
|
|
17570
18145
|
const reason = e instanceof Error ? e.message : String(e);
|
|
18146
|
+
if (opts.quiet)
|
|
18147
|
+
agent.note(start);
|
|
17571
18148
|
agent.note({ type: "tool-end", toolId, output: `could not run: ${reason}`, isError: true });
|
|
17572
18149
|
return Promise.resolve({ exit: reason, text: "" });
|
|
17573
18150
|
}
|
|
17574
18151
|
const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
|
|
17575
18152
|
this.track(worktreeId, toolId, { proc, timer });
|
|
17576
18153
|
this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
|
|
17577
|
-
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 });
|
|
17578
18168
|
}
|
|
17579
18169
|
stop(worktreeId) {
|
|
17580
18170
|
for (const toolId of this.running.get(worktreeId)?.keys() ?? [])
|
|
@@ -17606,7 +18196,7 @@ class ExecService {
|
|
|
17606
18196
|
r.proc.kill("SIGKILL");
|
|
17607
18197
|
}, KILL_GRACE_MS2);
|
|
17608
18198
|
}
|
|
17609
|
-
async collect(worktreeId, toolId, proc, agent) {
|
|
18199
|
+
async collect(worktreeId, toolId, proc, agent, heldStart) {
|
|
17610
18200
|
let text = "";
|
|
17611
18201
|
let truncated = false;
|
|
17612
18202
|
const drain = async (stream) => {
|
|
@@ -17641,6 +18231,10 @@ class ExecService {
|
|
|
17641
18231
|
clearTimeout(r.timer);
|
|
17642
18232
|
this.deps.runtime.release(worktreeId, `exec:${toolId}`);
|
|
17643
18233
|
}
|
|
18234
|
+
if (heldStart && exit === 0)
|
|
18235
|
+
return { exit, text };
|
|
18236
|
+
if (heldStart)
|
|
18237
|
+
agent.note(heldStart);
|
|
17644
18238
|
agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
|
|
17645
18239
|
return { exit, text };
|
|
17646
18240
|
}
|
|
@@ -17687,7 +18281,7 @@ function withRepoLock(repoPath, fn) {
|
|
|
17687
18281
|
var fileLockKey = (absPath) => `file:${absPath}`;
|
|
17688
18282
|
|
|
17689
18283
|
// ../daemon/src/git/status.ts
|
|
17690
|
-
import { join as
|
|
18284
|
+
import { join as join11 } from "path";
|
|
17691
18285
|
async function treeFingerprint(worktreePath) {
|
|
17692
18286
|
const [head, diff, untracked] = await Promise.all([
|
|
17693
18287
|
git(worktreePath, "rev-parse", "HEAD"),
|
|
@@ -17697,7 +18291,7 @@ async function treeFingerprint(worktreePath) {
|
|
|
17697
18291
|
const others = await Promise.all(untracked.out.split(`
|
|
17698
18292
|
`).filter(Boolean).map(async (p) => {
|
|
17699
18293
|
try {
|
|
17700
|
-
const f = Bun.file(
|
|
18294
|
+
const f = Bun.file(join11(worktreePath, p));
|
|
17701
18295
|
const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
|
|
17702
18296
|
return `${p}:${body}`;
|
|
17703
18297
|
} catch {
|
|
@@ -17748,7 +18342,7 @@ async function numstat(worktreePath, ...range) {
|
|
|
17748
18342
|
}
|
|
17749
18343
|
async function untrackedLines(worktreePath, file) {
|
|
17750
18344
|
try {
|
|
17751
|
-
const f = Bun.file(
|
|
18345
|
+
const f = Bun.file(join11(worktreePath, file));
|
|
17752
18346
|
if (f.size > 2000000)
|
|
17753
18347
|
return {};
|
|
17754
18348
|
const buf = new Uint8Array(await f.arrayBuffer());
|
|
@@ -17767,14 +18361,16 @@ async function untrackedLines(worktreePath, file) {
|
|
|
17767
18361
|
return {};
|
|
17768
18362
|
}
|
|
17769
18363
|
}
|
|
18364
|
+
var MAX_UNTRACKED_COUNTED = 500;
|
|
17770
18365
|
async function statusFilesWithCounts(worktreePath) {
|
|
17771
18366
|
const files = await statusFiles(worktreePath);
|
|
17772
18367
|
if (files.length === 0)
|
|
17773
18368
|
return files;
|
|
17774
18369
|
const counts = files.some((f) => f.xy !== "??") ? await numstat(worktreePath, "HEAD") : new Map;
|
|
18370
|
+
const countUntracked = files.filter((f) => f.xy === "??").length <= MAX_UNTRACKED_COUNTED;
|
|
17775
18371
|
return Promise.all(files.map(async (f) => ({
|
|
17776
18372
|
...f,
|
|
17777
|
-
...f.xy === "??" ? await untrackedLines(worktreePath, f.path) : counts.get(f.path) ?? {}
|
|
18373
|
+
...f.xy === "??" ? countUntracked ? await untrackedLines(worktreePath, f.path) : {} : counts.get(f.path) ?? {}
|
|
17778
18374
|
})));
|
|
17779
18375
|
}
|
|
17780
18376
|
async function committedFiles(worktreePath, defaultBr) {
|
|
@@ -17824,6 +18420,10 @@ async function behindUpstream(worktreePath) {
|
|
|
17824
18420
|
const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
|
|
17825
18421
|
return r.ok ? Number(r.out) || 0 : null;
|
|
17826
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
|
+
}
|
|
17827
18427
|
async function aheadBehind(worktreePath, defaultBr) {
|
|
17828
18428
|
const [a, b] = await Promise.all([
|
|
17829
18429
|
git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`),
|
|
@@ -18042,7 +18642,7 @@ function rankElementSources(el, rows) {
|
|
|
18042
18642
|
// ../daemon/src/files/listing.ts
|
|
18043
18643
|
var SUBMODULE_MODE = "160000";
|
|
18044
18644
|
var entries = (out) => out.split("\x00").filter(Boolean);
|
|
18045
|
-
function parseListing(staged, untracked, deleted) {
|
|
18645
|
+
function parseListing(staged, untracked, deleted, ignored = "") {
|
|
18046
18646
|
const gone = new Set(entries(deleted));
|
|
18047
18647
|
const paths = new Set;
|
|
18048
18648
|
const submodules = new Set;
|
|
@@ -18060,7 +18660,18 @@ function parseListing(staged, untracked, deleted) {
|
|
|
18060
18660
|
}
|
|
18061
18661
|
for (const path of entries(untracked))
|
|
18062
18662
|
paths.add(path);
|
|
18063
|
-
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;
|
|
18064
18675
|
}
|
|
18065
18676
|
|
|
18066
18677
|
// ../daemon/src/files/vite-offset.ts
|
|
@@ -18236,12 +18847,13 @@ class FileService {
|
|
|
18236
18847
|
}
|
|
18237
18848
|
async list(worktreeId) {
|
|
18238
18849
|
const cwd = this.require(worktreeId).path;
|
|
18239
|
-
const [staged, untracked, deleted] = await Promise.all([
|
|
18850
|
+
const [staged, untracked, deleted, ignored] = await Promise.all([
|
|
18240
18851
|
git(cwd, "ls-files", "-z", "-s"),
|
|
18241
18852
|
git(cwd, "ls-files", "-z", "-o", "--exclude-standard"),
|
|
18242
|
-
git(cwd, "ls-files", "-z", "-d")
|
|
18853
|
+
git(cwd, "ls-files", "-z", "-d"),
|
|
18854
|
+
git(cwd, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory")
|
|
18243
18855
|
]);
|
|
18244
|
-
return parseListing(staged.out, untracked.out, deleted.out);
|
|
18856
|
+
return parseListing(staged.out, untracked.out, deleted.out, ignored.out);
|
|
18245
18857
|
}
|
|
18246
18858
|
async search(worktreeId, query) {
|
|
18247
18859
|
const { path: cwd } = this.require(worktreeId);
|
|
@@ -18464,6 +19076,7 @@ class AfterLand {
|
|
|
18464
19076
|
fireAndForget(repo.id, done, "afterLand");
|
|
18465
19077
|
}
|
|
18466
19078
|
async exec(repo, commands) {
|
|
19079
|
+
await this.d.settled?.();
|
|
18467
19080
|
if (this.d.self.building(true))
|
|
18468
19081
|
this.d.hub.emit("selfChanged");
|
|
18469
19082
|
let failure;
|
|
@@ -18504,41 +19117,18 @@ class AfterLand {
|
|
|
18504
19117
|
}
|
|
18505
19118
|
|
|
18506
19119
|
// ../daemon/src/repos/registry.ts
|
|
18507
|
-
import { existsSync as
|
|
19120
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
18508
19121
|
import { homedir as homedir7 } from "os";
|
|
18509
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
18510
|
-
|
|
18511
|
-
// ../daemon/src/worktrees/naming.ts
|
|
18512
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
18513
|
-
function shortId() {
|
|
18514
|
-
return randomBytes2(5).toString("hex");
|
|
18515
|
-
}
|
|
18516
|
-
function slugify2(prompt, withRandom = true) {
|
|
18517
|
-
const words = prompt.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter(Boolean).slice(0, 3);
|
|
18518
|
-
const base = words.join("-").slice(0, 20).replace(/-+$/g, "") || "task";
|
|
18519
|
-
return withRandom ? `${base}-${randomBytes2(2).toString("hex")}` : base;
|
|
18520
|
-
}
|
|
18521
|
-
function cleanTitle2(title) {
|
|
18522
|
-
return title.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
18523
|
-
}
|
|
18524
|
-
var VARIANT_LENSES = [
|
|
18525
|
-
"Take the most direct approach: the change most people would expect, in the smallest diff.",
|
|
18526
|
-
"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.",
|
|
18527
|
-
"Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
|
|
18528
|
-
];
|
|
18529
|
-
function variantLens(index) {
|
|
18530
|
-
const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
|
|
18531
|
-
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})`;
|
|
18532
|
-
}
|
|
19122
|
+
import { basename as basename3, dirname as dirname6, join as join16 } from "path";
|
|
18533
19123
|
|
|
18534
19124
|
// ../daemon/src/repos/browse.ts
|
|
18535
|
-
import { existsSync as
|
|
19125
|
+
import { existsSync as existsSync15, statSync as statSync3 } from "fs";
|
|
18536
19126
|
import { readdir } from "fs/promises";
|
|
18537
19127
|
import { homedir as homedir5 } from "os";
|
|
18538
|
-
import { dirname as dirname4, join as
|
|
19128
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
18539
19129
|
var MAX_ENTRIES = 40;
|
|
18540
19130
|
function expandTilde(raw) {
|
|
18541
|
-
return raw === "~" || raw.startsWith("~/") ?
|
|
19131
|
+
return raw === "~" || raw.startsWith("~/") ? join12(homedir5(), raw.slice(1)) : raw;
|
|
18542
19132
|
}
|
|
18543
19133
|
function collapseTilde(abs) {
|
|
18544
19134
|
const home = homedir5();
|
|
@@ -18549,7 +19139,7 @@ function holdsNothing(names) {
|
|
|
18549
19139
|
}
|
|
18550
19140
|
async function describeFolder(abs) {
|
|
18551
19141
|
const path = collapseTilde(abs);
|
|
18552
|
-
if (
|
|
19142
|
+
if (existsSync15(join12(abs, ".git")))
|
|
18553
19143
|
return { path, kind: "project" };
|
|
18554
19144
|
try {
|
|
18555
19145
|
return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
|
|
@@ -18564,10 +19154,10 @@ function describe2(typed, parent) {
|
|
|
18564
19154
|
isDir = statSync3(typed).isDirectory();
|
|
18565
19155
|
} catch {}
|
|
18566
19156
|
return {
|
|
18567
|
-
exists:
|
|
19157
|
+
exists: existsSync15(typed),
|
|
18568
19158
|
isDir,
|
|
18569
|
-
isRepo: isDir &&
|
|
18570
|
-
parentExists:
|
|
19159
|
+
isRepo: isDir && existsSync15(join12(typed, ".git")),
|
|
19160
|
+
parentExists: existsSync15(parent)
|
|
18571
19161
|
};
|
|
18572
19162
|
}
|
|
18573
19163
|
async function browsePath(raw) {
|
|
@@ -18591,16 +19181,16 @@ async function browsePath(raw) {
|
|
|
18591
19181
|
continue;
|
|
18592
19182
|
if (!name.toLowerCase().startsWith(prefix))
|
|
18593
19183
|
continue;
|
|
18594
|
-
const abs =
|
|
18595
|
-
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")) });
|
|
18596
19186
|
}
|
|
18597
19187
|
entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
|
|
18598
19188
|
return { entries: entries.slice(0, MAX_ENTRIES), target };
|
|
18599
19189
|
}
|
|
18600
19190
|
|
|
18601
19191
|
// ../daemon/src/repos/config.ts
|
|
18602
|
-
import { closeSync, existsSync as
|
|
18603
|
-
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";
|
|
18604
19194
|
|
|
18605
19195
|
// ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
18606
19196
|
function createScanner(text, ignoreTrivia = false) {
|
|
@@ -19964,7 +20554,7 @@ function applyEdits(text, edits) {
|
|
|
19964
20554
|
|
|
19965
20555
|
// ../daemon/src/repos/config.ts
|
|
19966
20556
|
var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
|
|
19967
|
-
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)));
|
|
19968
20558
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
19969
20559
|
function parseSettings(text) {
|
|
19970
20560
|
const errors = [];
|
|
@@ -19989,7 +20579,7 @@ function readConfigFile(repoPath) {
|
|
|
19989
20579
|
for (const rel of files) {
|
|
19990
20580
|
let raw;
|
|
19991
20581
|
try {
|
|
19992
|
-
raw = parseSettings(
|
|
20582
|
+
raw = parseSettings(readFileSync11(join13(repoPath, rel), "utf8"));
|
|
19993
20583
|
} catch (e) {
|
|
19994
20584
|
return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
19995
20585
|
}
|
|
@@ -20046,12 +20636,12 @@ function configTarget(repoPath, made) {
|
|
|
20046
20636
|
}
|
|
20047
20637
|
function configBody(repoPath, rel, config) {
|
|
20048
20638
|
const place = PLACES.find((p) => p.local === rel);
|
|
20049
|
-
const abs = place &&
|
|
20050
|
-
if (!abs || !
|
|
20639
|
+
const abs = place && join13(repoPath, place.shared);
|
|
20640
|
+
if (!abs || !existsSync16(abs))
|
|
20051
20641
|
return config;
|
|
20052
20642
|
let shared;
|
|
20053
20643
|
try {
|
|
20054
|
-
shared = parseSettings(
|
|
20644
|
+
shared = parseSettings(readFileSync11(abs, "utf8"));
|
|
20055
20645
|
} catch {
|
|
20056
20646
|
return config;
|
|
20057
20647
|
}
|
|
@@ -20064,7 +20654,7 @@ function configText(repoPath, rel, config) {
|
|
|
20064
20654
|
let text;
|
|
20065
20655
|
let was;
|
|
20066
20656
|
try {
|
|
20067
|
-
text =
|
|
20657
|
+
text = readFileSync11(join13(repoPath, rel), "utf8");
|
|
20068
20658
|
was = parseSettings(text);
|
|
20069
20659
|
} catch {
|
|
20070
20660
|
return fresh;
|
|
@@ -20094,9 +20684,9 @@ function detectConfig(repoPath) {
|
|
|
20094
20684
|
return { config: explicit.config, needsSetup: false };
|
|
20095
20685
|
if (explicit)
|
|
20096
20686
|
log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
|
|
20097
|
-
const pkgPath =
|
|
20098
|
-
if (
|
|
20099
|
-
const pkg = JSON.parse(
|
|
20687
|
+
const pkgPath = join13(repoPath, "package.json");
|
|
20688
|
+
if (existsSync16(pkgPath)) {
|
|
20689
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
20100
20690
|
const scripts = pkg.scripts ?? {};
|
|
20101
20691
|
const runner = detectRunner(repoPath, pkg.packageManager);
|
|
20102
20692
|
const pages = detectPages(repoPath, scripts, runner);
|
|
@@ -20117,7 +20707,7 @@ function detectConfig(repoPath) {
|
|
|
20117
20707
|
};
|
|
20118
20708
|
}
|
|
20119
20709
|
}
|
|
20120
|
-
if (
|
|
20710
|
+
if (existsSync16(join13(repoPath, "start.sh"))) {
|
|
20121
20711
|
return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
|
|
20122
20712
|
}
|
|
20123
20713
|
const assumed = assumeNothingToRun(repoPath);
|
|
@@ -20160,24 +20750,24 @@ function assumeNothingToRun(repoPath) {
|
|
|
20160
20750
|
return marker && !servesPage(repoPath) ? marker : undefined;
|
|
20161
20751
|
}
|
|
20162
20752
|
function markerIn(repoPath) {
|
|
20163
|
-
const named = NO_PAGE_MARKERS.find((f) =>
|
|
20753
|
+
const named = NO_PAGE_MARKERS.find((f) => existsSync16(join13(repoPath, f)));
|
|
20164
20754
|
if (named)
|
|
20165
20755
|
return named;
|
|
20166
20756
|
let entries;
|
|
20167
20757
|
try {
|
|
20168
|
-
entries =
|
|
20758
|
+
entries = readdirSync4(repoPath);
|
|
20169
20759
|
} catch {
|
|
20170
20760
|
return;
|
|
20171
20761
|
}
|
|
20172
20762
|
return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
20173
20763
|
}
|
|
20174
20764
|
function servesPage(repoPath) {
|
|
20175
|
-
if (WEB_FILES.some((f) =>
|
|
20765
|
+
if (WEB_FILES.some((f) => existsSync16(join13(repoPath, f))))
|
|
20176
20766
|
return true;
|
|
20177
|
-
if (/^web:/m.test(head(
|
|
20767
|
+
if (/^web:/m.test(head(join13(repoPath, "Procfile"))))
|
|
20178
20768
|
return true;
|
|
20179
20769
|
for (const [files, names] of WEB_DEPENDENCIES) {
|
|
20180
|
-
const text = files.map((f) => head(
|
|
20770
|
+
const text = files.map((f) => head(join13(repoPath, f))).join(`
|
|
20181
20771
|
`);
|
|
20182
20772
|
if (text.trim() && names.some((n) => mentions(text, n)))
|
|
20183
20773
|
return true;
|
|
@@ -20187,14 +20777,14 @@ function servesPage(repoPath) {
|
|
|
20187
20777
|
function frontEndBeside(repoPath) {
|
|
20188
20778
|
let dirs;
|
|
20189
20779
|
try {
|
|
20190
|
-
dirs =
|
|
20780
|
+
dirs = readdirSync4(repoPath, { withFileTypes: true });
|
|
20191
20781
|
} catch {
|
|
20192
20782
|
return false;
|
|
20193
20783
|
}
|
|
20194
20784
|
return dirs.some((d) => {
|
|
20195
20785
|
if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
|
|
20196
20786
|
return false;
|
|
20197
|
-
const pkg = head(
|
|
20787
|
+
const pkg = head(join13(repoPath, d.name, "package.json"));
|
|
20198
20788
|
if (!pkg)
|
|
20199
20789
|
return false;
|
|
20200
20790
|
try {
|
|
@@ -20209,7 +20799,7 @@ function mentions(text, name) {
|
|
|
20209
20799
|
return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
|
|
20210
20800
|
}
|
|
20211
20801
|
function head(path) {
|
|
20212
|
-
if (!
|
|
20802
|
+
if (!existsSync16(path))
|
|
20213
20803
|
return "";
|
|
20214
20804
|
try {
|
|
20215
20805
|
const fd = openSync2(path, "r");
|
|
@@ -20237,31 +20827,31 @@ function procCommand(runner, script, body) {
|
|
|
20237
20827
|
return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
|
|
20238
20828
|
}
|
|
20239
20829
|
function detectPages(repoPath, scripts, runner) {
|
|
20240
|
-
if (!scripts.build || !
|
|
20830
|
+
if (!scripts.build || !existsSync16(join13(repoPath, "functions")))
|
|
20241
20831
|
return null;
|
|
20242
|
-
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));
|
|
20243
20833
|
if (!cfg)
|
|
20244
20834
|
return null;
|
|
20245
|
-
const out =
|
|
20835
|
+
const out = readFileSync11(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
|
|
20246
20836
|
if (!out)
|
|
20247
20837
|
return null;
|
|
20248
20838
|
return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
|
|
20249
20839
|
}
|
|
20250
20840
|
function detectRunner(repoPath, packageManager) {
|
|
20251
|
-
if (
|
|
20841
|
+
if (existsSync16(join13(repoPath, "bun.lock")) || existsSync16(join13(repoPath, "bun.lockb")))
|
|
20252
20842
|
return "bun";
|
|
20253
|
-
if (
|
|
20843
|
+
if (existsSync16(join13(repoPath, "pnpm-lock.yaml")))
|
|
20254
20844
|
return "pnpm";
|
|
20255
|
-
if (
|
|
20845
|
+
if (existsSync16(join13(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
|
|
20256
20846
|
return "yarn";
|
|
20257
20847
|
return "npm";
|
|
20258
20848
|
}
|
|
20259
20849
|
|
|
20260
20850
|
// ../daemon/src/repos/create.ts
|
|
20261
|
-
import { existsSync as
|
|
20851
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
20262
20852
|
import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
|
|
20263
20853
|
import { homedir as homedir6 } from "os";
|
|
20264
|
-
import { dirname as dirname5, join as
|
|
20854
|
+
import { dirname as dirname5, join as join14, resolve as resolve7 } from "path";
|
|
20265
20855
|
function isInside(dir, parent) {
|
|
20266
20856
|
const p = parent.replace(/\/+$/, "");
|
|
20267
20857
|
return dir === p || dir.startsWith(`${p}/`);
|
|
@@ -20272,29 +20862,29 @@ function planProject(opts, home = homedir6()) {
|
|
|
20272
20862
|
throw new UserError(nameError);
|
|
20273
20863
|
const first = firstProjectsFolder(opts.parent, home);
|
|
20274
20864
|
const parent = first ?? resolveParent(opts.parent);
|
|
20275
|
-
const dir =
|
|
20276
|
-
if (
|
|
20865
|
+
const dir = join14(parent, opts.name.trim());
|
|
20866
|
+
if (existsSync17(dir))
|
|
20277
20867
|
throw new UserError(`${dir} already exists`);
|
|
20278
20868
|
return { parent, dir, makeParent: first !== null };
|
|
20279
20869
|
}
|
|
20280
20870
|
function firstProjectsFolder(raw, home) {
|
|
20281
20871
|
const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
|
|
20282
|
-
if (typed !==
|
|
20872
|
+
if (typed !== join14(home, PROJECTS_FOLDER) || existsSync17(typed) || !existsSync17(home))
|
|
20283
20873
|
return null;
|
|
20284
|
-
return
|
|
20874
|
+
return join14(realpathSync2(home), PROJECTS_FOLDER);
|
|
20285
20875
|
}
|
|
20286
20876
|
function planInPlace(opts) {
|
|
20287
20877
|
const name = opts.name.trim();
|
|
20288
20878
|
if (!name || name === "." || name === ".." || name.includes("/")) {
|
|
20289
20879
|
throw new UserError(`${opts.name} is not a folder name`);
|
|
20290
20880
|
}
|
|
20291
|
-
const typed =
|
|
20292
|
-
if (!
|
|
20881
|
+
const typed = join14(resolveParent(opts.parent), name);
|
|
20882
|
+
if (!existsSync17(typed) || !statSync4(typed).isDirectory())
|
|
20293
20883
|
throw new UserError(`${typed} is not a folder`);
|
|
20294
20884
|
const dir = realpathSync2(typed);
|
|
20295
|
-
if (
|
|
20885
|
+
if (existsSync17(join14(dir, ".git")))
|
|
20296
20886
|
throw new UserError(`${dir} is already a project; open it instead`);
|
|
20297
|
-
if (!holdsNothing(
|
|
20887
|
+
if (!holdsNothing(readdirSync5(dir))) {
|
|
20298
20888
|
throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
|
|
20299
20889
|
}
|
|
20300
20890
|
return { parent: dirname5(dir), dir, makeParent: false };
|
|
@@ -20303,7 +20893,7 @@ function resolveParent(raw) {
|
|
|
20303
20893
|
const typed = expandTilde(raw.trim());
|
|
20304
20894
|
if (!typed.startsWith("/"))
|
|
20305
20895
|
throw new UserError(`${raw} is not a folder path`);
|
|
20306
|
-
if (!
|
|
20896
|
+
if (!existsSync17(typed))
|
|
20307
20897
|
throw new UserError(`${raw} does not exist; make it first`);
|
|
20308
20898
|
if (!statSync4(typed).isDirectory())
|
|
20309
20899
|
throw new UserError(`${raw} is not a folder`);
|
|
@@ -20383,13 +20973,13 @@ async function undoCreate(dir) {
|
|
|
20383
20973
|
}
|
|
20384
20974
|
async function undoInit(dir) {
|
|
20385
20975
|
try {
|
|
20386
|
-
await rm(
|
|
20976
|
+
await rm(join14(dir, ".git"), { recursive: true, force: true });
|
|
20387
20977
|
} catch (e) {
|
|
20388
20978
|
log.warn(dir, "could not take back a half-made .git", e);
|
|
20389
20979
|
}
|
|
20390
20980
|
}
|
|
20391
20981
|
async function unmakeProject(dir, made) {
|
|
20392
|
-
const target = made === "folder" ? dir :
|
|
20982
|
+
const target = made === "folder" ? dir : join14(dir, ".git");
|
|
20393
20983
|
try {
|
|
20394
20984
|
await rm(target, { recursive: true, force: true });
|
|
20395
20985
|
} catch (e) {
|
|
@@ -20419,8 +21009,8 @@ async function ok(p, what) {
|
|
|
20419
21009
|
}
|
|
20420
21010
|
|
|
20421
21011
|
// ../daemon/src/repos/watcher.ts
|
|
20422
|
-
import { existsSync as
|
|
20423
|
-
import { basename as basename2, join as
|
|
21012
|
+
import { existsSync as existsSync18, watch } from "fs";
|
|
21013
|
+
import { basename as basename2, join as join15 } from "path";
|
|
20424
21014
|
function watchDefaultBranch(repoPath, branch, onMove) {
|
|
20425
21015
|
let last = null;
|
|
20426
21016
|
fireAndForget(repoPath, git(repoPath, "rev-parse", branch).then((r) => {
|
|
@@ -20450,8 +21040,8 @@ function watchDefaultBranch(repoPath, branch, onMove) {
|
|
|
20450
21040
|
log.debug(repoPath, `not watching ${p}`, e);
|
|
20451
21041
|
}
|
|
20452
21042
|
};
|
|
20453
|
-
tryWatch(
|
|
20454
|
-
tryWatch(
|
|
21043
|
+
tryWatch(join15(repoPath, ".git"));
|
|
21044
|
+
tryWatch(join15(repoPath, ".git", "refs", "heads"));
|
|
20455
21045
|
return () => {
|
|
20456
21046
|
for (const w of watchers)
|
|
20457
21047
|
w.close();
|
|
@@ -20473,7 +21063,7 @@ function watchWorktreeDir(repoPath, onChange) {
|
|
|
20473
21063
|
}, 300);
|
|
20474
21064
|
};
|
|
20475
21065
|
const arm = (commonDir) => {
|
|
20476
|
-
const dir =
|
|
21066
|
+
const dir = join15(commonDir, "worktrees");
|
|
20477
21067
|
const armInner = () => {
|
|
20478
21068
|
inner?.close();
|
|
20479
21069
|
inner = null;
|
|
@@ -20516,7 +21106,7 @@ function watchWorktreeDir(repoPath, onChange) {
|
|
|
20516
21106
|
function watchConfigFile(repoPath, onChange) {
|
|
20517
21107
|
const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
|
|
20518
21108
|
const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename2(rel)));
|
|
20519
|
-
const dir =
|
|
21109
|
+
const dir = join15(repoPath, CONFIG_DIR);
|
|
20520
21110
|
let timer = null;
|
|
20521
21111
|
let outer = null;
|
|
20522
21112
|
let inner = null;
|
|
@@ -20531,7 +21121,7 @@ function watchConfigFile(repoPath, onChange) {
|
|
|
20531
21121
|
const armInner = () => {
|
|
20532
21122
|
inner?.close();
|
|
20533
21123
|
inner = null;
|
|
20534
|
-
if (!
|
|
21124
|
+
if (!existsSync18(dir))
|
|
20535
21125
|
return;
|
|
20536
21126
|
try {
|
|
20537
21127
|
inner = watch(dir, (_event, filename) => {
|
|
@@ -20585,7 +21175,7 @@ class RepoRegistry {
|
|
|
20585
21175
|
}
|
|
20586
21176
|
async boot() {
|
|
20587
21177
|
const { state } = this.d;
|
|
20588
|
-
const gone = state.worktrees.filter((wt) => !
|
|
21178
|
+
const gone = state.worktrees.filter((wt) => !existsSync19(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
|
|
20589
21179
|
for (const wt of gone)
|
|
20590
21180
|
await this.d.worktrees.forgetGone(wt);
|
|
20591
21181
|
for (const repo of state.repos) {
|
|
@@ -20652,7 +21242,7 @@ class RepoRegistry {
|
|
|
20652
21242
|
return `${repo.name} is set up now`;
|
|
20653
21243
|
if (main.promptedAt || main.lastTurn)
|
|
20654
21244
|
return `${repo.name} has a chat now`;
|
|
20655
|
-
if (
|
|
21245
|
+
if (existsSync19(join16(repo.path, ".claude")))
|
|
20656
21246
|
return `an agent has already run in ${repo.name}`;
|
|
20657
21247
|
if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
|
|
20658
21248
|
return `${repo.name} has files in it now`;
|
|
@@ -20729,7 +21319,7 @@ class RepoRegistry {
|
|
|
20729
21319
|
}
|
|
20730
21320
|
async register(rawPath, made) {
|
|
20731
21321
|
const path = expandTilde(rawPath);
|
|
20732
|
-
if (!
|
|
21322
|
+
if (!existsSync19(path))
|
|
20733
21323
|
throw new UserError(`${path} does not exist`);
|
|
20734
21324
|
if (!await isGitRepo(path))
|
|
20735
21325
|
throw new UserError(`${path} is not a git repository`);
|
|
@@ -20767,7 +21357,6 @@ class RepoRegistry {
|
|
|
20767
21357
|
};
|
|
20768
21358
|
this.d.state.addWorktree(main);
|
|
20769
21359
|
this.warmed.add(repo.id);
|
|
20770
|
-
await this.d.runtime.start(main, repo);
|
|
20771
21360
|
this.startWatcher(repo);
|
|
20772
21361
|
fireAndForget(repo.id, this.d.worktrees.spare.ensure(repo.id), "spare warm-up");
|
|
20773
21362
|
this.d.hub.emit("reposChanged");
|
|
@@ -20789,8 +21378,8 @@ class RepoRegistry {
|
|
|
20789
21378
|
this.d.state.removeWorktree(wt.id);
|
|
20790
21379
|
this.d.worktrees.deleteChat(wt.id);
|
|
20791
21380
|
releasePort(wt.proxyPort);
|
|
21381
|
+
this.d.drafts?.drop(wt.id);
|
|
20792
21382
|
}
|
|
20793
|
-
this.d.drafts?.drop(draftKey(repoId));
|
|
20794
21383
|
this.d.state.removeRepo(repoId);
|
|
20795
21384
|
this.d.hub.emit("reposChanged");
|
|
20796
21385
|
this.d.hub.emit("worktreesChanged");
|
|
@@ -20802,10 +21391,10 @@ class RepoRegistry {
|
|
|
20802
21391
|
throw new UserError(current.reason);
|
|
20803
21392
|
const was = configTarget(repo.path, !!repo.made);
|
|
20804
21393
|
const rel = configSibling(was, kind);
|
|
20805
|
-
const moved = rel !== was &&
|
|
21394
|
+
const moved = rel !== was && existsSync19(join16(repo.path, was));
|
|
20806
21395
|
if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
|
|
20807
21396
|
try {
|
|
20808
|
-
rmSync2(
|
|
21397
|
+
rmSync2(join16(repo.path, was));
|
|
20809
21398
|
} catch (e) {
|
|
20810
21399
|
log.warn(repoId, `could not remove ${was}`, e);
|
|
20811
21400
|
}
|
|
@@ -20814,10 +21403,10 @@ class RepoRegistry {
|
|
|
20814
21403
|
}
|
|
20815
21404
|
if (isLocalConfigFile(rel))
|
|
20816
21405
|
await excludeFromGit(repo.path, rel);
|
|
20817
|
-
const file =
|
|
21406
|
+
const file = join16(repo.path, rel);
|
|
20818
21407
|
try {
|
|
20819
|
-
|
|
20820
|
-
|
|
21408
|
+
mkdirSync6(dirname6(file), { recursive: true });
|
|
21409
|
+
writeFileSync7(file, configText(repo.path, rel, config));
|
|
20821
21410
|
} catch (e) {
|
|
20822
21411
|
log.warn(repoId, `could not write ${rel}`, e);
|
|
20823
21412
|
}
|
|
@@ -20829,7 +21418,7 @@ class RepoRegistry {
|
|
|
20829
21418
|
repo.assumed = undefined;
|
|
20830
21419
|
this.d.state.save();
|
|
20831
21420
|
this.warmed.add(repoId);
|
|
20832
|
-
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))) {
|
|
20833
21422
|
fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.start(wt, repo)), "runtime restart");
|
|
20834
21423
|
}
|
|
20835
21424
|
fireAndForget(repoId, this.d.worktrees.spare.ensure(repoId), "spare warm-up");
|
|
@@ -20891,7 +21480,7 @@ class RepoRegistry {
|
|
|
20891
21480
|
}
|
|
20892
21481
|
placeConfig(repo) {
|
|
20893
21482
|
const rel = configTarget(repo.path, !!repo.made);
|
|
20894
|
-
if (isLocalConfigFile(rel) &&
|
|
21483
|
+
if (isLocalConfigFile(rel) && existsSync19(join16(repo.path, rel))) {
|
|
20895
21484
|
fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
|
|
20896
21485
|
}
|
|
20897
21486
|
if (repo.configFile === rel)
|
|
@@ -20966,7 +21555,7 @@ class RepoRegistry {
|
|
|
20966
21555
|
}
|
|
20967
21556
|
|
|
20968
21557
|
// ../daemon/src/routes/service.ts
|
|
20969
|
-
import { join as
|
|
21558
|
+
import { join as join17 } from "path";
|
|
20970
21559
|
|
|
20971
21560
|
// ../daemon/src/routes/fileRouters.ts
|
|
20972
21561
|
var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
|
|
@@ -22166,7 +22755,7 @@ class RouteService {
|
|
|
22166
22755
|
return mergeRoutes(fileRoutes(paths, roots), code);
|
|
22167
22756
|
}
|
|
22168
22757
|
async cachedRead(root, path, had, kept, decode) {
|
|
22169
|
-
const abs =
|
|
22758
|
+
const abs = join17(root, path);
|
|
22170
22759
|
const stat = await this.fs.stat(abs);
|
|
22171
22760
|
if (!stat)
|
|
22172
22761
|
return null;
|
|
@@ -22191,7 +22780,7 @@ class RouteService {
|
|
|
22191
22780
|
}
|
|
22192
22781
|
}
|
|
22193
22782
|
async hash(root, file) {
|
|
22194
|
-
const abs =
|
|
22783
|
+
const abs = join17(root, file);
|
|
22195
22784
|
const stat = await this.fs.stat(abs);
|
|
22196
22785
|
if (!stat)
|
|
22197
22786
|
return null;
|
|
@@ -22235,7 +22824,7 @@ class RouteService {
|
|
|
22235
22824
|
}
|
|
22236
22825
|
|
|
22237
22826
|
// ../daemon/src/runtime/bridge-script.ts
|
|
22238
|
-
import { existsSync as
|
|
22827
|
+
import { existsSync as existsSync20, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
|
|
22239
22828
|
var LEARNED_MAX = 8;
|
|
22240
22829
|
|
|
22241
22830
|
class BridgeScript {
|
|
@@ -22265,7 +22854,7 @@ class BridgeScript {
|
|
|
22265
22854
|
this.cache = null;
|
|
22266
22855
|
}
|
|
22267
22856
|
get() {
|
|
22268
|
-
if (!
|
|
22857
|
+
if (!existsSync20(this.path))
|
|
22269
22858
|
return "// toyon bridge not built";
|
|
22270
22859
|
const s = statSync5(this.path);
|
|
22271
22860
|
const stamp = `${s.mtimeMs}:${s.size}`;
|
|
@@ -22274,7 +22863,7 @@ class BridgeScript {
|
|
|
22274
22863
|
this.stamp = stamp;
|
|
22275
22864
|
const origins = [...this.origins, ...this.learned];
|
|
22276
22865
|
this.cache = `window.__toyonShellOrigins=${JSON.stringify(origins)};
|
|
22277
|
-
${
|
|
22866
|
+
${readFileSync12(this.path, "utf8")}`;
|
|
22278
22867
|
return this.cache;
|
|
22279
22868
|
}
|
|
22280
22869
|
}
|
|
@@ -22291,70 +22880,6 @@ function servedLocally(origin) {
|
|
|
22291
22880
|
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
|
|
22292
22881
|
}
|
|
22293
22882
|
|
|
22294
|
-
// ../daemon/src/runtime/memory.ts
|
|
22295
|
-
var MAC_PRESSURE_CRITICAL = 4;
|
|
22296
|
-
var AVAILABLE_FLOOR = 0.15;
|
|
22297
|
-
var unsupportedLogged = false;
|
|
22298
|
-
async function memoryTight() {
|
|
22299
|
-
const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
|
|
22300
|
-
if (signal === null && !unsupportedLogged) {
|
|
22301
|
-
unsupportedLogged = true;
|
|
22302
|
-
log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
|
|
22303
|
-
}
|
|
22304
|
-
return signal;
|
|
22305
|
-
}
|
|
22306
|
-
async function darwin() {
|
|
22307
|
-
const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
|
|
22308
|
-
if (!r.ok)
|
|
22309
|
-
return null;
|
|
22310
|
-
const [level, available] = r.out.split(/\s+/).map(Number);
|
|
22311
|
-
if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
|
|
22312
|
-
return null;
|
|
22313
|
-
return darwinSignal(level, available);
|
|
22314
|
-
}
|
|
22315
|
-
function darwinSignal(level, available) {
|
|
22316
|
-
const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
|
|
22317
|
-
return {
|
|
22318
|
-
tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
|
|
22319
|
-
why: `memory pressure ${pressure} with ${available}% available`
|
|
22320
|
-
};
|
|
22321
|
-
}
|
|
22322
|
-
async function linux() {
|
|
22323
|
-
let text;
|
|
22324
|
-
try {
|
|
22325
|
-
text = await Bun.file("/proc/meminfo").text();
|
|
22326
|
-
} catch {
|
|
22327
|
-
return null;
|
|
22328
|
-
}
|
|
22329
|
-
const field = (name) => {
|
|
22330
|
-
const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
|
|
22331
|
-
return m ? Number(m[1]) : null;
|
|
22332
|
-
};
|
|
22333
|
-
const total = field("MemTotal");
|
|
22334
|
-
const available = field("MemAvailable");
|
|
22335
|
-
if (!total || available === null)
|
|
22336
|
-
return null;
|
|
22337
|
-
const percent = Math.round(available / total * 100);
|
|
22338
|
-
return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
|
|
22339
|
-
}
|
|
22340
|
-
async function sampleCosts(pgids) {
|
|
22341
|
-
const costs = new Map;
|
|
22342
|
-
if (pgids.length === 0)
|
|
22343
|
-
return costs;
|
|
22344
|
-
const wanted = new Set(pgids);
|
|
22345
|
-
const r = await run("ps", ["-axo", "pgid=,rss="], "/");
|
|
22346
|
-
if (!r.ok)
|
|
22347
|
-
return costs;
|
|
22348
|
-
for (const line of r.out.split(`
|
|
22349
|
-
`)) {
|
|
22350
|
-
const [pgid, rss] = line.trim().split(/\s+/).map(Number);
|
|
22351
|
-
if (pgid === undefined || rss === undefined || !wanted.has(pgid))
|
|
22352
|
-
continue;
|
|
22353
|
-
costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
|
|
22354
|
-
}
|
|
22355
|
-
return costs;
|
|
22356
|
-
}
|
|
22357
|
-
|
|
22358
22883
|
// ../daemon/src/runtime/idle.ts
|
|
22359
22884
|
var LOCAL_SLEEP_MS = 2 * 60 * 60000;
|
|
22360
22885
|
var CLOUD_SLEEP_MS = 5 * 60000;
|
|
@@ -22377,6 +22902,7 @@ class IdlePolicy {
|
|
|
22377
22902
|
viewing = new Map;
|
|
22378
22903
|
queue = [];
|
|
22379
22904
|
draining = false;
|
|
22905
|
+
drained = [];
|
|
22380
22906
|
sleepMs;
|
|
22381
22907
|
now;
|
|
22382
22908
|
constructor(d) {
|
|
@@ -22470,6 +22996,9 @@ class IdlePolicy {
|
|
|
22470
22996
|
const wt = this.d.state.worktree(id);
|
|
22471
22997
|
return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
|
|
22472
22998
|
}
|
|
22999
|
+
isShown(id) {
|
|
23000
|
+
return (this.entries.get(id)?.viewers.size ?? 0) > 0;
|
|
23001
|
+
}
|
|
22473
23002
|
isViewed(id) {
|
|
22474
23003
|
if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
|
|
22475
23004
|
return true;
|
|
@@ -22492,7 +23021,8 @@ class IdlePolicy {
|
|
|
22492
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];
|
|
22493
23022
|
if (!pick)
|
|
22494
23023
|
return;
|
|
22495
|
-
|
|
23024
|
+
log.info("idle", `${signal.why}; ${pick.id} is the least recently used`);
|
|
23025
|
+
this.sleep(pick.id, "short of memory");
|
|
22496
23026
|
}
|
|
22497
23027
|
async sampleCosts() {
|
|
22498
23028
|
this.recordViewed();
|
|
@@ -22590,13 +23120,12 @@ class IdlePolicy {
|
|
|
22590
23120
|
this.arm(id, this.sleepMs - idle);
|
|
22591
23121
|
return;
|
|
22592
23122
|
}
|
|
22593
|
-
this.sleep(id, `
|
|
23123
|
+
this.sleep(id, `nobody looked for ${humanMs(this.sleepMs)}`);
|
|
22594
23124
|
}
|
|
22595
23125
|
sleep(id, why) {
|
|
22596
23126
|
this.disarm(id);
|
|
22597
23127
|
this.entry(id).costKb = undefined;
|
|
22598
|
-
|
|
22599
|
-
fireAndForget(id, this.d.runtime.sleep(id), "sleep");
|
|
23128
|
+
fireAndForget(id, this.d.runtime.sleep(id, why), "sleep");
|
|
22600
23129
|
}
|
|
22601
23130
|
enqueue(key, run) {
|
|
22602
23131
|
if (this.queue.some((q) => q.key === key))
|
|
@@ -22612,15 +23141,22 @@ class IdlePolicy {
|
|
|
22612
23141
|
await next.run();
|
|
22613
23142
|
} finally {
|
|
22614
23143
|
this.draining = false;
|
|
23144
|
+
for (const r of this.drained.splice(0))
|
|
23145
|
+
r();
|
|
22615
23146
|
}
|
|
22616
23147
|
}
|
|
23148
|
+
settled() {
|
|
23149
|
+
if (!this.draining)
|
|
23150
|
+
return Promise.resolve();
|
|
23151
|
+
return new Promise((r) => this.drained.push(r));
|
|
23152
|
+
}
|
|
22617
23153
|
}
|
|
22618
23154
|
function humanMs(ms) {
|
|
22619
23155
|
if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
|
|
22620
|
-
return `${ms / (60 * 60000)}h`;
|
|
23156
|
+
return `${ms / (60 * 60000)} h`;
|
|
22621
23157
|
if (ms >= 60000)
|
|
22622
23158
|
return `${Math.round(ms / 60000)} min`;
|
|
22623
|
-
return `${Math.round(ms / 1000)}s`;
|
|
23159
|
+
return `${Math.round(ms / 1000)} s`;
|
|
22624
23160
|
}
|
|
22625
23161
|
|
|
22626
23162
|
// ../daemon/src/server/ws.ts
|
|
@@ -22647,50 +23183,50 @@ class PairCodes {
|
|
|
22647
23183
|
}
|
|
22648
23184
|
|
|
22649
23185
|
// ../daemon/src/agent/config.ts
|
|
22650
|
-
import { existsSync as
|
|
23186
|
+
import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
|
|
22651
23187
|
import { homedir as homedir8 } from "os";
|
|
22652
|
-
import { join as
|
|
23188
|
+
import { join as join18 } from "path";
|
|
22653
23189
|
function agentConfigFiles(spec, repoPath, home = homedir8()) {
|
|
22654
23190
|
const file = (id, label, path) => ({
|
|
22655
23191
|
id,
|
|
22656
23192
|
label,
|
|
22657
23193
|
path,
|
|
22658
|
-
exists:
|
|
23194
|
+
exists: existsSync21(path)
|
|
22659
23195
|
});
|
|
22660
23196
|
if (spec.id === "claude") {
|
|
22661
23197
|
return [
|
|
22662
|
-
file("user-config", "user config (MCP servers, projects)",
|
|
22663
|
-
file("user-settings", "user settings (permissions, hooks)",
|
|
22664
|
-
...repoPath ? [file("project-mcp", "project MCP servers",
|
|
22665
|
-
...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"))] : []
|
|
22666
23202
|
];
|
|
22667
23203
|
}
|
|
22668
23204
|
if (spec.id === "codex")
|
|
22669
|
-
return [file("user-config", "config",
|
|
23205
|
+
return [file("user-config", "config", join18(home, ".codex", "config.toml"))];
|
|
22670
23206
|
return [];
|
|
22671
23207
|
}
|
|
22672
23208
|
function describeAgentConfig(spec, repoPath, home = homedir8()) {
|
|
22673
23209
|
const files = agentConfigFiles(spec, repoPath, home);
|
|
22674
23210
|
const servers = [];
|
|
22675
23211
|
if (spec.id === "claude") {
|
|
22676
|
-
const user = readJson(
|
|
23212
|
+
const user = readJson(join18(home, ".claude.json"));
|
|
22677
23213
|
servers.push(...serversOf(user?.mcpServers, "user"));
|
|
22678
23214
|
if (repoPath) {
|
|
22679
23215
|
const projects = user?.projects;
|
|
22680
23216
|
const mine = projects?.[repoPath];
|
|
22681
23217
|
servers.push(...serversOf(mine?.mcpServers, "local"));
|
|
22682
|
-
servers.push(...serversOf(readJson(
|
|
23218
|
+
servers.push(...serversOf(readJson(join18(repoPath, ".mcp.json"))?.mcpServers, "project"));
|
|
22683
23219
|
}
|
|
22684
23220
|
} else if (spec.id === "codex") {
|
|
22685
|
-
servers.push(...codexServers(
|
|
23221
|
+
servers.push(...codexServers(join18(home, ".codex", "config.toml")));
|
|
22686
23222
|
}
|
|
22687
23223
|
return { agent: spec.id, files, servers };
|
|
22688
23224
|
}
|
|
22689
23225
|
function readJson(path) {
|
|
22690
|
-
if (!
|
|
23226
|
+
if (!existsSync21(path))
|
|
22691
23227
|
return null;
|
|
22692
23228
|
try {
|
|
22693
|
-
const v = JSON.parse(
|
|
23229
|
+
const v = JSON.parse(readFileSync13(path, "utf8"));
|
|
22694
23230
|
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
22695
23231
|
} catch (e) {
|
|
22696
23232
|
log.warn("agent-config", `${path} is not valid JSON`, e);
|
|
@@ -22708,11 +23244,11 @@ function serversOf(raw, scope) {
|
|
|
22708
23244
|
});
|
|
22709
23245
|
}
|
|
22710
23246
|
function codexServers(path) {
|
|
22711
|
-
if (!
|
|
23247
|
+
if (!existsSync21(path))
|
|
22712
23248
|
return [];
|
|
22713
23249
|
const out = [];
|
|
22714
23250
|
let current = null;
|
|
22715
|
-
for (const raw of
|
|
23251
|
+
for (const raw of readFileSync13(path, "utf8").split(`
|
|
22716
23252
|
`)) {
|
|
22717
23253
|
const line = raw.trim();
|
|
22718
23254
|
const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
|
|
@@ -23304,8 +23840,11 @@ var handlers = {
|
|
|
23304
23840
|
unsubscribe(msg, ctx) {
|
|
23305
23841
|
ctx.unsubscribe(msg.worktreeId);
|
|
23306
23842
|
},
|
|
23307
|
-
view(msg, ctx) {
|
|
23843
|
+
view(msg, ctx, s) {
|
|
23308
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");
|
|
23309
23848
|
},
|
|
23310
23849
|
seen(msg, _ctx, s) {
|
|
23311
23850
|
s.turns.markSeen(msg.worktreeId);
|
|
@@ -23315,6 +23854,7 @@ var handlers = {
|
|
|
23315
23854
|
},
|
|
23316
23855
|
"set-draft"(msg, _ctx, s) {
|
|
23317
23856
|
s.drafts.set(msg.boxId, msg.text, msg.clientId);
|
|
23857
|
+
s.worktrees.spare.typed(msg.boxId, msg.text);
|
|
23318
23858
|
},
|
|
23319
23859
|
"refresh-git"(msg, _ctx, s) {
|
|
23320
23860
|
s.worktrees.recount(msg.repoId);
|
|
@@ -23339,6 +23879,7 @@ var handlers = {
|
|
|
23339
23879
|
async "create-worktree"(msg, _ctx, s) {
|
|
23340
23880
|
await s.worktrees.create(msg.repoId, msg.prompt, {
|
|
23341
23881
|
createdBy: msg.clientId,
|
|
23882
|
+
worktreeId: msg.worktreeId,
|
|
23342
23883
|
variant: msg.variant,
|
|
23343
23884
|
context: msg.context,
|
|
23344
23885
|
attachments: msg.attachments,
|
|
@@ -23462,6 +24003,9 @@ var handlers = {
|
|
|
23462
24003
|
"update-now"(_msg, _ctx, s) {
|
|
23463
24004
|
return s.update.updateNow();
|
|
23464
24005
|
},
|
|
24006
|
+
"check-update"(_msg, _ctx, s) {
|
|
24007
|
+
return s.update.checkNow();
|
|
24008
|
+
},
|
|
23465
24009
|
async commit(msg, ctx, s) {
|
|
23466
24010
|
const result = await s.worktrees.commit(msg.worktreeId, msg.message);
|
|
23467
24011
|
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
@@ -23516,6 +24060,10 @@ var handlers = {
|
|
|
23516
24060
|
async "discard-file"(msg, _ctx, s) {
|
|
23517
24061
|
await s.files.discard(msg.worktreeId, msg.path);
|
|
23518
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);
|
|
23519
24067
|
},
|
|
23520
24068
|
async "write-file"(msg, ctx, s) {
|
|
23521
24069
|
const head = { t: "file-written", worktreeId: msg.worktreeId, path: msg.path, seq: msg.seq };
|
|
@@ -23698,8 +24246,8 @@ async function dispatch(msg, ctx, s) {
|
|
|
23698
24246
|
}
|
|
23699
24247
|
|
|
23700
24248
|
// ../daemon/src/server/http.ts
|
|
23701
|
-
import { existsSync as
|
|
23702
|
-
import { join as
|
|
24249
|
+
import { existsSync as existsSync22 } from "fs";
|
|
24250
|
+
import { join as join19 } from "path";
|
|
23703
24251
|
var IMMUTABLE = "private, max-age=31536000, immutable";
|
|
23704
24252
|
var NO_STORE = "no-store";
|
|
23705
24253
|
function createFetch(opts) {
|
|
@@ -23769,10 +24317,12 @@ function createFetch(opts) {
|
|
|
23769
24317
|
headers["set-cookie"] = grantCookie(grant, remote.host);
|
|
23770
24318
|
return Response.json(await opts.bootstrap(), { headers });
|
|
23771
24319
|
}
|
|
23772
|
-
if (url.pathname === "/restart" && req.method === "POST") {
|
|
24320
|
+
if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
|
|
23773
24321
|
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
23774
24322
|
return new Response("unauthorized", { status: 401 });
|
|
23775
|
-
|
|
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));
|
|
23776
24326
|
return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
|
|
23777
24327
|
}
|
|
23778
24328
|
if (url.pathname === "/pair" && req.method === "POST") {
|
|
@@ -23821,7 +24371,7 @@ function createFetch(opts) {
|
|
|
23821
24371
|
return new Response("unauthorized", { status: 401 });
|
|
23822
24372
|
const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
|
|
23823
24373
|
const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
|
|
23824
|
-
const path = places.find((p) => !!p &&
|
|
24374
|
+
const path = places.find((p) => !!p && existsSync22(p));
|
|
23825
24375
|
if (!path)
|
|
23826
24376
|
return new Response("not found", { status: 404 });
|
|
23827
24377
|
return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
|
|
@@ -23842,15 +24392,15 @@ function createFetch(opts) {
|
|
|
23842
24392
|
});
|
|
23843
24393
|
}
|
|
23844
24394
|
const rel = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
23845
|
-
const file =
|
|
24395
|
+
const file = join19(opts.shellDist, rel.replaceAll("..", ""));
|
|
23846
24396
|
const hashed = rel.startsWith("/assets/");
|
|
23847
|
-
if (
|
|
24397
|
+
if (existsSync22(file) && Bun.file(file).size > 0) {
|
|
23848
24398
|
return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
|
|
23849
24399
|
}
|
|
23850
24400
|
if (hashed)
|
|
23851
24401
|
return new Response("not found", { status: 404 });
|
|
23852
|
-
const index =
|
|
23853
|
-
if (
|
|
24402
|
+
const index = join19(opts.shellDist, "index.html");
|
|
24403
|
+
if (existsSync22(index))
|
|
23854
24404
|
return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
|
|
23855
24405
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
23856
24406
|
};
|
|
@@ -23962,7 +24512,7 @@ function startServer(opts) {
|
|
|
23962
24512
|
fireAndForget("ws", (async () => {
|
|
23963
24513
|
do {
|
|
23964
24514
|
statusesDirty = false;
|
|
23965
|
-
broadcast({ t: "worktrees", rows: await s.worktrees.rows(),
|
|
24515
|
+
broadcast({ t: "worktrees", rows: await s.worktrees.rows(), trunks: await s.worktrees.trunks() });
|
|
23966
24516
|
} while (statusesDirty);
|
|
23967
24517
|
})().finally(() => {
|
|
23968
24518
|
statusesInFlight = false;
|
|
@@ -24068,10 +24618,11 @@ function startServer(opts) {
|
|
|
24068
24618
|
return {
|
|
24069
24619
|
t: "hello",
|
|
24070
24620
|
version,
|
|
24621
|
+
install: s.update.install(),
|
|
24071
24622
|
protocol: PROTOCOL_VERSION,
|
|
24072
24623
|
repos: s.state.repos,
|
|
24073
24624
|
rows: await s.worktrees.rows({ quick: true }),
|
|
24074
|
-
|
|
24625
|
+
trunks: await s.worktrees.trunks({ quick: true }),
|
|
24075
24626
|
themes: s.themes.themes,
|
|
24076
24627
|
themePrefs: s.themes.prefs,
|
|
24077
24628
|
agents: agentInfos(),
|
|
@@ -24106,7 +24657,8 @@ function startServer(opts) {
|
|
|
24106
24657
|
remote: opts.remote,
|
|
24107
24658
|
preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
|
|
24108
24659
|
bootstrap: helloFrame,
|
|
24109
|
-
restart: () => s.restarter.request(),
|
|
24660
|
+
restart: (now) => s.restarter.request({ now }),
|
|
24661
|
+
restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
|
|
24110
24662
|
pair: new PairCodes,
|
|
24111
24663
|
onPaired: () => {
|
|
24112
24664
|
s.state.notePaired();
|
|
@@ -24218,16 +24770,16 @@ function startServer(opts) {
|
|
|
24218
24770
|
}
|
|
24219
24771
|
|
|
24220
24772
|
// ../daemon/src/themes/store.ts
|
|
24221
|
-
import { existsSync as
|
|
24773
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
24222
24774
|
import { homedir as homedir10 } from "os";
|
|
24223
|
-
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";
|
|
24224
24776
|
var home = homedir10();
|
|
24225
24777
|
var defaultExtensionDirs = [
|
|
24226
|
-
|
|
24227
|
-
|
|
24228
|
-
|
|
24229
|
-
|
|
24230
|
-
|
|
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"),
|
|
24231
24783
|
"/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
|
|
24232
24784
|
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
|
|
24233
24785
|
"/Applications/Cursor.app/Contents/Resources/app/extensions",
|
|
@@ -24311,14 +24863,14 @@ class ThemeStore {
|
|
|
24311
24863
|
name: json?.name ?? base,
|
|
24312
24864
|
source: "file"
|
|
24313
24865
|
});
|
|
24314
|
-
|
|
24866
|
+
writeFileSync8(join20(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
|
|
24315
24867
|
`);
|
|
24316
24868
|
this.load();
|
|
24317
24869
|
return theme;
|
|
24318
24870
|
}
|
|
24319
24871
|
}
|
|
24320
24872
|
function readJsonc(file) {
|
|
24321
|
-
return parse6(
|
|
24873
|
+
return parse6(readFileSync14(file, "utf8"), [], { allowTrailingComma: true });
|
|
24322
24874
|
}
|
|
24323
24875
|
function readVscodeTheme(file, depth = 0) {
|
|
24324
24876
|
const json = readJsonc(file);
|
|
@@ -24340,13 +24892,13 @@ function readVscodeTheme(file, depth = 0) {
|
|
|
24340
24892
|
return t;
|
|
24341
24893
|
}
|
|
24342
24894
|
function loadDir(dir) {
|
|
24343
|
-
if (!
|
|
24895
|
+
if (!existsSync23(dir))
|
|
24344
24896
|
return [];
|
|
24345
24897
|
const out = [];
|
|
24346
|
-
for (const f of
|
|
24898
|
+
for (const f of readdirSync6(dir)) {
|
|
24347
24899
|
if (!/\.jsonc?$/i.test(f))
|
|
24348
24900
|
continue;
|
|
24349
|
-
const file =
|
|
24901
|
+
const file = join20(dir, f);
|
|
24350
24902
|
const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
|
|
24351
24903
|
try {
|
|
24352
24904
|
const json = readJsonc(file);
|
|
@@ -24362,31 +24914,31 @@ function loadDir(dir) {
|
|
|
24362
24914
|
return out;
|
|
24363
24915
|
}
|
|
24364
24916
|
function discover(dir) {
|
|
24365
|
-
if (!
|
|
24917
|
+
if (!existsSync23(dir))
|
|
24366
24918
|
return [];
|
|
24367
24919
|
const out = [];
|
|
24368
24920
|
let entries;
|
|
24369
24921
|
try {
|
|
24370
|
-
entries =
|
|
24922
|
+
entries = readdirSync6(dir);
|
|
24371
24923
|
} catch {
|
|
24372
24924
|
return out;
|
|
24373
24925
|
}
|
|
24374
24926
|
for (const name of entries) {
|
|
24375
|
-
const extDir =
|
|
24376
|
-
const pkgFile =
|
|
24927
|
+
const extDir = join20(dir, name);
|
|
24928
|
+
const pkgFile = join20(extDir, "package.json");
|
|
24377
24929
|
try {
|
|
24378
|
-
if (!statSync6(extDir).isDirectory() || !
|
|
24930
|
+
if (!statSync6(extDir).isDirectory() || !existsSync23(pkgFile))
|
|
24379
24931
|
continue;
|
|
24380
|
-
const pkg = JSON.parse(
|
|
24932
|
+
const pkg = JSON.parse(readFileSync14(pkgFile, "utf8"));
|
|
24381
24933
|
const themes = pkg.contributes?.themes;
|
|
24382
24934
|
if (!Array.isArray(themes))
|
|
24383
24935
|
continue;
|
|
24384
24936
|
const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
|
|
24385
24937
|
let nls = {};
|
|
24386
|
-
const nlsFile =
|
|
24387
|
-
if (
|
|
24938
|
+
const nlsFile = join20(extDir, "package.nls.json");
|
|
24939
|
+
if (existsSync23(nlsFile)) {
|
|
24388
24940
|
try {
|
|
24389
|
-
nls = JSON.parse(
|
|
24941
|
+
nls = JSON.parse(readFileSync14(nlsFile, "utf8"));
|
|
24390
24942
|
} catch {}
|
|
24391
24943
|
}
|
|
24392
24944
|
for (const c of themes) {
|
|
@@ -24525,6 +25077,21 @@ class UpdateService {
|
|
|
24525
25077
|
this.wanted = true;
|
|
24526
25078
|
await this.proceed();
|
|
24527
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
|
+
}
|
|
24528
25095
|
get() {
|
|
24529
25096
|
const restarting = this.d.restarter.waitingOn() ?? (this.wanted && !this.installing ? this.d.restarter.working() : null);
|
|
24530
25097
|
if (this.installed === null && this.latest === null && !this.installing && this.failed === null && restarting === null) {
|
|
@@ -24745,14 +25312,22 @@ var DIFF_CHARS = 2000;
|
|
|
24745
25312
|
class LandingService {
|
|
24746
25313
|
d;
|
|
24747
25314
|
judging = new Map;
|
|
25315
|
+
busy = new Set;
|
|
24748
25316
|
constructor(d) {
|
|
24749
25317
|
this.d = d;
|
|
24750
25318
|
d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
|
|
24751
25319
|
d.hub.on("agentStatus", (id, status) => {
|
|
25320
|
+
this.note(id, status);
|
|
24752
25321
|
if (status === "working")
|
|
24753
25322
|
this.clear(id);
|
|
24754
25323
|
});
|
|
24755
25324
|
}
|
|
25325
|
+
note(worktreeId, status) {
|
|
25326
|
+
if (status === "working" || status === "waiting")
|
|
25327
|
+
this.busy.add(worktreeId);
|
|
25328
|
+
else
|
|
25329
|
+
this.busy.delete(worktreeId);
|
|
25330
|
+
}
|
|
24756
25331
|
clear(worktreeId) {
|
|
24757
25332
|
this.judging.delete(worktreeId);
|
|
24758
25333
|
this.d.worktrees.setLanding(worktreeId, undefined);
|
|
@@ -24763,28 +25338,91 @@ class LandingService {
|
|
|
24763
25338
|
return;
|
|
24764
25339
|
if (turn.end !== "done")
|
|
24765
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) {
|
|
24766
25391
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
24767
25392
|
const files = await statusFiles(wt.path);
|
|
25393
|
+
if (files.length > 0)
|
|
25394
|
+
return true;
|
|
24768
25395
|
const { ahead } = await aheadBehind(wt.path, repo.defaultBranch);
|
|
24769
|
-
|
|
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))
|
|
24770
25402
|
return this.clear(worktreeId);
|
|
24771
|
-
|
|
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);
|
|
24772
25410
|
const started = Date.now();
|
|
24773
|
-
this.d.worktrees.setLanding(worktreeId, { at:
|
|
24774
|
-
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);
|
|
24775
25413
|
let check = "none";
|
|
24776
25414
|
let checkTail;
|
|
24777
25415
|
const command = repo.config.check?.trim();
|
|
24778
25416
|
if (command) {
|
|
24779
|
-
const r = await this.d.check(worktreeId, command);
|
|
25417
|
+
const r = await this.d.check(worktreeId, command, { quiet: opts.quiet });
|
|
24780
25418
|
if (!live())
|
|
24781
25419
|
return;
|
|
24782
25420
|
check = r.exit === 0 ? "pass" : "fail";
|
|
24783
25421
|
if (check === "fail")
|
|
24784
|
-
checkTail =
|
|
25422
|
+
checkTail = tail(r.text) || `exit ${r.exit}`;
|
|
24785
25423
|
}
|
|
24786
25424
|
let verdict = null;
|
|
24787
|
-
if (check !== "fail" && this.d.judge) {
|
|
25425
|
+
if (opts.ask && check !== "fail" && this.d.judge) {
|
|
24788
25426
|
const entries = this.d.transcript(worktreeId);
|
|
24789
25427
|
const prompt = landPrompt({
|
|
24790
25428
|
title: wt.title,
|
|
@@ -24802,10 +25440,11 @@ class LandingService {
|
|
|
24802
25440
|
return;
|
|
24803
25441
|
}
|
|
24804
25442
|
const landing = {
|
|
24805
|
-
at:
|
|
25443
|
+
at: opts.at,
|
|
24806
25444
|
check,
|
|
24807
25445
|
...checkTail ? { checkTail } : {},
|
|
24808
25446
|
ready: check !== "fail",
|
|
25447
|
+
...kept,
|
|
24809
25448
|
...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
|
|
24810
25449
|
...verdict?.subject ? { subject: verdict.subject } : {},
|
|
24811
25450
|
...verdict?.body ? { body: verdict.body } : {},
|
|
@@ -24814,11 +25453,14 @@ class LandingService {
|
|
|
24814
25453
|
if (!live())
|
|
24815
25454
|
return;
|
|
24816
25455
|
this.judging.delete(worktreeId);
|
|
24817
|
-
|
|
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`);
|
|
24818
25460
|
this.d.worktrees.setLanding(worktreeId, landing);
|
|
24819
25461
|
}
|
|
24820
25462
|
}
|
|
24821
|
-
function
|
|
25463
|
+
function tail(text) {
|
|
24822
25464
|
const t = text.replace(/\s+$/, "");
|
|
24823
25465
|
return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
|
|
24824
25466
|
}
|
|
@@ -25028,11 +25670,8 @@ function discoveredId(path) {
|
|
|
25028
25670
|
}
|
|
25029
25671
|
function knownPaths(known) {
|
|
25030
25672
|
const paths = new Set;
|
|
25031
|
-
for (const wt of known)
|
|
25673
|
+
for (const wt of known)
|
|
25032
25674
|
paths.add(canonical(wt.path));
|
|
25033
|
-
if (wt.linkPath)
|
|
25034
|
-
paths.add(canonical(wt.linkPath));
|
|
25035
|
-
}
|
|
25036
25675
|
return paths;
|
|
25037
25676
|
}
|
|
25038
25677
|
function subtractKnown(repoId, listed, known) {
|
|
@@ -25100,7 +25739,7 @@ class RefSearch {
|
|
|
25100
25739
|
rowIdForPath = (path) => {
|
|
25101
25740
|
const want = canonical(path);
|
|
25102
25741
|
for (const wt of this.d.state.worktrees) {
|
|
25103
|
-
if (canonical(wt.path) === want
|
|
25742
|
+
if (canonical(wt.path) === want)
|
|
25104
25743
|
return wt.id;
|
|
25105
25744
|
}
|
|
25106
25745
|
return discoveredId(path);
|
|
@@ -25210,8 +25849,8 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
25210
25849
|
}
|
|
25211
25850
|
|
|
25212
25851
|
// ../daemon/src/worktrees/service.ts
|
|
25213
|
-
import { existsSync as
|
|
25214
|
-
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";
|
|
25215
25854
|
|
|
25216
25855
|
// ../daemon/src/git/archive.ts
|
|
25217
25856
|
import { rmSync as rmSync3 } from "fs";
|
|
@@ -25220,12 +25859,16 @@ async function keepState(repoPath, wtPath, ref, indexFile) {
|
|
|
25220
25859
|
const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
|
|
25221
25860
|
if (!head.ok)
|
|
25222
25861
|
return null;
|
|
25223
|
-
const status = await
|
|
25224
|
-
const
|
|
25862
|
+
const status = await gitRaw(wtPath, "status", "--porcelain", "-uall");
|
|
25863
|
+
const files = status.ok ? parsePorcelain(status.out).length : null;
|
|
25864
|
+
const dirty = files === null || files > 0;
|
|
25225
25865
|
const snapshot = dirty ? await snapshotCommit(wtPath, head.out, indexFile) : null;
|
|
25226
25866
|
if (!(await git(repoPath, "update-ref", ref, snapshot ?? head.out)).ok)
|
|
25227
25867
|
return null;
|
|
25228
|
-
return {
|
|
25868
|
+
return {
|
|
25869
|
+
head: head.out,
|
|
25870
|
+
...snapshot ? { snapshot, ...files ? { dirty: files } : {} } : dirty ? { lost: true } : {}
|
|
25871
|
+
};
|
|
25229
25872
|
}
|
|
25230
25873
|
async function snapshotCommit(wtPath, head, indexFile) {
|
|
25231
25874
|
const env = { GIT_INDEX_FILE: indexFile };
|
|
@@ -25277,6 +25920,32 @@ async function landingMark(worktreePath, defaultBr) {
|
|
|
25277
25920
|
return null;
|
|
25278
25921
|
return { base: base.out, tip: tip.out };
|
|
25279
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
|
+
}
|
|
25280
25949
|
async function dropLandRefs(repoPath, worktreeId) {
|
|
25281
25950
|
const r = await git(repoPath, "for-each-ref", "--format=%(refname)", `refs/toyon/lands/${worktreeId}/`);
|
|
25282
25951
|
if (!r.ok || !r.out)
|
|
@@ -25292,8 +25961,11 @@ async function commitWorktree(worktreePath, message) {
|
|
|
25292
25961
|
return { ok: false, message: "nothing to commit" };
|
|
25293
25962
|
await git(worktreePath, "add", "-A");
|
|
25294
25963
|
const c = await git(worktreePath, "commit", "-m", message);
|
|
25295
|
-
if (!c.ok)
|
|
25296
|
-
|
|
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
|
+
}
|
|
25297
25969
|
return { ok: true, message: `committed: ${message}` };
|
|
25298
25970
|
}
|
|
25299
25971
|
async function requireClean(worktreePath) {
|
|
@@ -25399,21 +26071,37 @@ async function fastForwardMain(repoPath, defaultBr) {
|
|
|
25399
26071
|
const f = await run(GIT, ["fetch", "--quiet"], repoPath, NO_PROMPT);
|
|
25400
26072
|
if (!f.ok)
|
|
25401
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
|
+
}
|
|
25402
26081
|
const behind = await behindUpstream(repoPath);
|
|
25403
|
-
if (behind === null)
|
|
25404
|
-
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
|
+
}
|
|
25405
26085
|
if (behind === 0)
|
|
25406
|
-
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
|
+
}
|
|
25407
26094
|
const m = await git(repoPath, "merge", "--ff-only", "@{upstream}");
|
|
25408
26095
|
if (!m.ok) {
|
|
25409
26096
|
const clobber = /overwritten by merge|would be overwritten/.test(`${m.out}
|
|
25410
26097
|
${m.err}`);
|
|
25411
|
-
return {
|
|
26098
|
+
return clobber ? {
|
|
25412
26099
|
ok: false,
|
|
25413
|
-
|
|
25414
|
-
|
|
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` };
|
|
25415
26103
|
}
|
|
25416
|
-
return { ok: true, message: `pulled ${behind} commit(s) from origin` };
|
|
26104
|
+
return { ok: true, moved: true, message: `pulled ${behind} commit(s) from origin` };
|
|
25417
26105
|
}
|
|
25418
26106
|
async function pushMain(repoPath, defaultBr) {
|
|
25419
26107
|
const p = await run(GIT, ["push", "origin", defaultBr], repoPath, NO_PROMPT);
|
|
@@ -25435,6 +26123,12 @@ async function mergeFailure(cwd, m, conflictMessage) {
|
|
|
25435
26123
|
}
|
|
25436
26124
|
return { ok: false, message: `merge failed: ${said.slice(0, 300)}` };
|
|
25437
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
|
+
}
|
|
25438
26132
|
async function openPr(o) {
|
|
25439
26133
|
const cErr = await requireClean(o.worktreePath);
|
|
25440
26134
|
if (cErr)
|
|
@@ -25445,9 +26139,9 @@ async function openPr(o) {
|
|
|
25445
26139
|
const remote = await git(o.worktreePath, "remote", "get-url", "origin");
|
|
25446
26140
|
if (!remote.ok)
|
|
25447
26141
|
return { ok: false, message: "no 'origin' remote: add one, or land by merging here" };
|
|
25448
|
-
const push = await
|
|
26142
|
+
const push = await pushBranch(o.worktreePath, o.branch);
|
|
25449
26143
|
if (!push.ok)
|
|
25450
|
-
return
|
|
26144
|
+
return push;
|
|
25451
26145
|
const titled = o.subject ? ["--title", o.subject, "--body", o.body ?? ""] : ["--fill"];
|
|
25452
26146
|
const created = await gh(["pr", "create", "--head", o.branch, "--base", o.defaultBr, ...titled], o.worktreePath);
|
|
25453
26147
|
let url = null;
|
|
@@ -25497,20 +26191,20 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
25497
26191
|
}
|
|
25498
26192
|
|
|
25499
26193
|
// ../daemon/src/worktrees/archive.ts
|
|
25500
|
-
import { existsSync as
|
|
25501
|
-
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";
|
|
25502
26196
|
class WorktreeArchive {
|
|
25503
26197
|
dir;
|
|
25504
26198
|
records = new Map;
|
|
25505
26199
|
constructor(dir) {
|
|
25506
26200
|
this.dir = dir;
|
|
25507
|
-
|
|
25508
|
-
for (const id of
|
|
26201
|
+
mkdirSync7(dir, { recursive: true });
|
|
26202
|
+
for (const id of readdirSync7(dir)) {
|
|
25509
26203
|
const file = this.filesOf(id).record;
|
|
25510
|
-
if (!
|
|
26204
|
+
if (!existsSync24(file))
|
|
25511
26205
|
continue;
|
|
25512
26206
|
try {
|
|
25513
|
-
this.records.set(id, JSON.parse(
|
|
26207
|
+
this.records.set(id, JSON.parse(readFileSync15(file, "utf8")));
|
|
25514
26208
|
} catch (e) {
|
|
25515
26209
|
log.warn("archive", `skipped an unreadable record: ${file}`, e);
|
|
25516
26210
|
}
|
|
@@ -25521,11 +26215,13 @@ class WorktreeArchive {
|
|
|
25521
26215
|
}
|
|
25522
26216
|
put(rec, from) {
|
|
25523
26217
|
const at = this.filesOf(rec.worktree.id);
|
|
25524
|
-
|
|
26218
|
+
mkdirSync7(at.dir, { recursive: true });
|
|
25525
26219
|
move(from.transcript, at.transcript);
|
|
25526
26220
|
move(from.attachments, at.attachments);
|
|
25527
|
-
|
|
26221
|
+
move(from.plans, at.plans);
|
|
26222
|
+
writeFileSync9(at.record, JSON.stringify(rec, null, 2));
|
|
25528
26223
|
this.records.set(rec.worktree.id, rec);
|
|
26224
|
+
return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
|
|
25529
26225
|
}
|
|
25530
26226
|
take(id, to) {
|
|
25531
26227
|
if (!this.records.has(id))
|
|
@@ -25533,6 +26229,7 @@ class WorktreeArchive {
|
|
|
25533
26229
|
const at = this.filesOf(id);
|
|
25534
26230
|
move(at.transcript, to.transcript);
|
|
25535
26231
|
move(at.attachments, to.attachments);
|
|
26232
|
+
move(at.plans, to.plans);
|
|
25536
26233
|
this.delete(id);
|
|
25537
26234
|
}
|
|
25538
26235
|
delete(id) {
|
|
@@ -25542,25 +26239,39 @@ class WorktreeArchive {
|
|
|
25542
26239
|
this.records.delete(id);
|
|
25543
26240
|
}
|
|
25544
26241
|
list(repo) {
|
|
25545
|
-
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));
|
|
25546
26243
|
}
|
|
25547
26244
|
chatFiles(id) {
|
|
25548
26245
|
if (!this.records.has(id))
|
|
25549
26246
|
return;
|
|
25550
26247
|
const at = this.filesOf(id);
|
|
25551
|
-
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
|
+
}
|
|
25552
26262
|
}
|
|
25553
26263
|
filesOf(id) {
|
|
25554
|
-
const dir =
|
|
26264
|
+
const dir = join21(this.dir, id);
|
|
25555
26265
|
return {
|
|
25556
26266
|
dir,
|
|
25557
|
-
record:
|
|
25558
|
-
transcript:
|
|
25559
|
-
attachments:
|
|
26267
|
+
record: join21(dir, "record.json"),
|
|
26268
|
+
transcript: join21(dir, "transcript.jsonl"),
|
|
26269
|
+
attachments: join21(dir, "attachments"),
|
|
26270
|
+
plans: join21(dir, "plans")
|
|
25560
26271
|
};
|
|
25561
26272
|
}
|
|
25562
26273
|
}
|
|
25563
|
-
function summarize(r, repoId) {
|
|
26274
|
+
function summarize(r, repoId, transcript) {
|
|
25564
26275
|
return {
|
|
25565
26276
|
id: r.worktree.id,
|
|
25566
26277
|
repoId,
|
|
@@ -25571,16 +26282,18 @@ function summarize(r, repoId) {
|
|
|
25571
26282
|
archivedAt: r.archivedAt,
|
|
25572
26283
|
...r.prompt ? { prompt: r.prompt } : {},
|
|
25573
26284
|
restorable: !!r.kept,
|
|
25574
|
-
|
|
26285
|
+
transcript,
|
|
26286
|
+
...r.sessionId ? { sessionId: r.sessionId } : {},
|
|
26287
|
+
...r.kept?.snapshot ? { uncommitted: true, ...r.kept.dirty ? { dirty: r.kept.dirty } : {} } : {},
|
|
25575
26288
|
...r.worktree.landed ? { landed: true } : {},
|
|
25576
26289
|
...r.cost !== undefined ? { cost: r.cost } : {},
|
|
25577
26290
|
...r.auto ? { auto: r.auto } : {}
|
|
25578
26291
|
};
|
|
25579
26292
|
}
|
|
25580
26293
|
function lastUsage(transcript) {
|
|
25581
|
-
if (!
|
|
26294
|
+
if (!existsSync24(transcript))
|
|
25582
26295
|
return null;
|
|
25583
|
-
const lines =
|
|
26296
|
+
const lines = readFileSync15(transcript, "utf8").split(`
|
|
25584
26297
|
`);
|
|
25585
26298
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
25586
26299
|
if (!lines[i]?.includes('"usage"'))
|
|
@@ -25594,9 +26307,9 @@ function lastUsage(transcript) {
|
|
|
25594
26307
|
return null;
|
|
25595
26308
|
}
|
|
25596
26309
|
function firstPrompt(transcript) {
|
|
25597
|
-
if (!
|
|
26310
|
+
if (!existsSync24(transcript))
|
|
25598
26311
|
return;
|
|
25599
|
-
for (const line of
|
|
26312
|
+
for (const line of readFileSync15(transcript, "utf8").split(`
|
|
25600
26313
|
`)) {
|
|
25601
26314
|
if (!line.includes('"user-message"'))
|
|
25602
26315
|
continue;
|
|
@@ -25609,8 +26322,9 @@ function firstPrompt(transcript) {
|
|
|
25609
26322
|
return;
|
|
25610
26323
|
}
|
|
25611
26324
|
function move(from, to) {
|
|
25612
|
-
if (!
|
|
26325
|
+
if (!existsSync24(from))
|
|
25613
26326
|
return;
|
|
26327
|
+
mkdirSync7(dirname8(to), { recursive: true });
|
|
25614
26328
|
rmSync4(to, { recursive: true, force: true });
|
|
25615
26329
|
renameSync4(from, to);
|
|
25616
26330
|
}
|
|
@@ -25673,16 +26387,31 @@ class ArchivedGit {
|
|
|
25673
26387
|
}
|
|
25674
26388
|
|
|
25675
26389
|
// ../daemon/src/worktrees/spare.ts
|
|
25676
|
-
import {
|
|
26390
|
+
import { existsSync as existsSync25 } from "fs";
|
|
26391
|
+
import { basename as basename7, join as join22 } from "path";
|
|
25677
26392
|
class SparePool {
|
|
25678
26393
|
d;
|
|
25679
|
-
|
|
26394
|
+
inflight = new Map;
|
|
26395
|
+
refreshing = new Map;
|
|
25680
26396
|
constructor(d) {
|
|
25681
26397
|
this.d = d;
|
|
25682
26398
|
}
|
|
26399
|
+
spareOf(repoId) {
|
|
26400
|
+
return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
|
|
26401
|
+
}
|
|
25683
26402
|
current(repoId) {
|
|
25684
|
-
const
|
|
25685
|
-
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");
|
|
25686
26415
|
}
|
|
25687
26416
|
adopt(repoId) {
|
|
25688
26417
|
const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
|
|
@@ -25691,76 +26420,125 @@ class SparePool {
|
|
|
25691
26420
|
const spare = persisted[0];
|
|
25692
26421
|
if (!spare)
|
|
25693
26422
|
return;
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
lockHash: lockfileHash(spare.path),
|
|
25697
|
-
refreshing: null,
|
|
25698
|
-
ready: true
|
|
25699
|
-
});
|
|
26423
|
+
spare.phase ??= "ready";
|
|
26424
|
+
spare.lockfile ??= lockfileHash(spare.path);
|
|
25700
26425
|
}
|
|
25701
26426
|
warm(repoId) {
|
|
25702
|
-
|
|
26427
|
+
const wt = this.spareOf(repoId);
|
|
26428
|
+
if (wt?.phase === "ready")
|
|
25703
26429
|
fireAndForget(repoId, this.revive(repoId), "spare refresh");
|
|
25704
26430
|
else
|
|
25705
26431
|
fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
|
|
25706
26432
|
}
|
|
25707
26433
|
async revive(repoId) {
|
|
25708
26434
|
await this.refresh(repoId);
|
|
25709
|
-
const
|
|
25710
|
-
|
|
25711
|
-
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)
|
|
25712
26437
|
return;
|
|
25713
26438
|
await this.d.runtime.start(wt, this.d.state.requireRepo(repoId));
|
|
25714
26439
|
}
|
|
25715
|
-
|
|
25716
|
-
|
|
25717
|
-
|
|
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();
|
|
25718
26447
|
const repo = this.d.state.requireRepo(repoId);
|
|
26448
|
+
if (have)
|
|
26449
|
+
return this.warmUp(have, repo, {});
|
|
25719
26450
|
if (repo.needsSetup)
|
|
25720
|
-
return;
|
|
25721
|
-
|
|
25722
|
-
|
|
25723
|
-
|
|
25724
|
-
|
|
25725
|
-
|
|
25726
|
-
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
|
|
25735
|
-
|
|
25736
|
-
|
|
25737
|
-
|
|
25738
|
-
|
|
25739
|
-
|
|
25740
|
-
|
|
25741
|
-
|
|
25742
|
-
|
|
25743
|
-
|
|
25744
|
-
|
|
25745
|
-
|
|
25746
|
-
|
|
25747
|
-
|
|
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);
|
|
25748
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;
|
|
25749
26525
|
}
|
|
26526
|
+
this.d.state.removeWorktree(wt.id);
|
|
26527
|
+
releasePort(wt.proxyPort);
|
|
26528
|
+
this.d.state.save();
|
|
26529
|
+
this.d.hub.emit("worktreesChanged");
|
|
25750
26530
|
}
|
|
25751
26531
|
async refresh(repoId) {
|
|
25752
|
-
const
|
|
25753
|
-
if (
|
|
26532
|
+
const wt = this.spareOf(repoId);
|
|
26533
|
+
if (wt?.phase !== "ready" || this.refreshing.has(repoId))
|
|
25754
26534
|
return;
|
|
25755
26535
|
const repo = this.d.state.requireRepo(repoId);
|
|
25756
|
-
const
|
|
25757
|
-
if (wt?.kind !== "spare")
|
|
25758
|
-
return;
|
|
25759
|
-
entry.refreshing = (async () => {
|
|
26536
|
+
const run = (async () => {
|
|
25760
26537
|
await withRepoLock(repo.path, () => git(wt.path, "reset", "--hard", repo.defaultBranch));
|
|
25761
26538
|
const h = lockfileHash(wt.path);
|
|
25762
|
-
if (h !==
|
|
25763
|
-
|
|
26539
|
+
if (h !== wt.lockfile) {
|
|
26540
|
+
wt.lockfile = h;
|
|
26541
|
+
this.d.state.save();
|
|
25764
26542
|
for (const cmd of repo.config.setup ?? []) {
|
|
25765
26543
|
const tail = [];
|
|
25766
26544
|
const code = await runSetup(cmd, wt.path, (line) => {
|
|
@@ -25774,70 +26552,161 @@ class SparePool {
|
|
|
25774
26552
|
}
|
|
25775
26553
|
}
|
|
25776
26554
|
})().finally(() => {
|
|
25777
|
-
|
|
26555
|
+
this.refreshing.delete(repoId);
|
|
25778
26556
|
});
|
|
25779
|
-
|
|
26557
|
+
this.refreshing.set(repoId, run);
|
|
26558
|
+
await run;
|
|
25780
26559
|
}
|
|
25781
|
-
async claim(repoId,
|
|
25782
|
-
const
|
|
25783
|
-
if (!
|
|
26560
|
+
async claim(repoId, title, worktreeId) {
|
|
26561
|
+
const wt = this.spareOf(repoId);
|
|
26562
|
+
if (!wt)
|
|
25784
26563
|
return null;
|
|
25785
|
-
|
|
25786
|
-
if (entry.refreshing)
|
|
25787
|
-
await entry.refreshing;
|
|
25788
|
-
const wt = this.d.state.worktree(entry.worktreeId);
|
|
25789
|
-
if (wt?.kind !== "spare")
|
|
26564
|
+
if (worktreeId && wt.id !== worktreeId)
|
|
25790
26565
|
return null;
|
|
25791
|
-
const
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
this.
|
|
25796
|
-
|
|
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;
|
|
25797
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));
|
|
25798
26580
|
wt.kind = "worktree";
|
|
26581
|
+
delete wt.phase;
|
|
26582
|
+
delete wt.lockfile;
|
|
25799
26583
|
wt.branch = branch;
|
|
25800
|
-
wt.title =
|
|
26584
|
+
wt.title = title;
|
|
26585
|
+
wt.unnamed = true;
|
|
25801
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);
|
|
25802
26590
|
this.d.state.save();
|
|
25803
|
-
|
|
25804
|
-
|
|
26591
|
+
if (next)
|
|
26592
|
+
fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
|
|
25805
26593
|
return wt;
|
|
25806
26594
|
}
|
|
25807
26595
|
}
|
|
25808
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
|
+
|
|
25809
26688
|
// ../daemon/src/worktrees/turns.ts
|
|
25810
26689
|
var TURN_HOLD = "turn";
|
|
25811
|
-
var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
|
|
25812
26690
|
|
|
25813
26691
|
class TurnService {
|
|
25814
26692
|
d;
|
|
25815
26693
|
lastStatus = new Map;
|
|
25816
26694
|
stoppedByHand = new Set;
|
|
25817
|
-
timers = new Map;
|
|
25818
|
-
summarized = new Map;
|
|
25819
26695
|
constructor(d) {
|
|
25820
26696
|
this.d = d;
|
|
25821
26697
|
this.boot();
|
|
25822
26698
|
d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
|
|
25823
26699
|
}
|
|
25824
26700
|
boot() {
|
|
25825
|
-
const now = Date.now();
|
|
25826
26701
|
let changed = false;
|
|
25827
26702
|
for (const wt of this.d.state.worktrees) {
|
|
25828
26703
|
const turn = wt.lastTurn;
|
|
25829
|
-
if (
|
|
26704
|
+
if (turn?.end !== "asking")
|
|
25830
26705
|
continue;
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
|
|
25834
|
-
|
|
25835
|
-
changed = true;
|
|
25836
|
-
}
|
|
25837
|
-
if (!turn.recap && (wt.seenAt ?? 0) < turn.at) {
|
|
25838
|
-
turn.recap = { at: now };
|
|
25839
|
-
changed = true;
|
|
25840
|
-
}
|
|
26706
|
+
const { ask: _expired, ...facts } = turn.facts;
|
|
26707
|
+
turn.end = "stopped";
|
|
26708
|
+
turn.facts = facts;
|
|
26709
|
+
changed = true;
|
|
25841
26710
|
}
|
|
25842
26711
|
if (changed)
|
|
25843
26712
|
this.d.state.save();
|
|
@@ -25845,10 +26714,8 @@ class TurnService {
|
|
|
25845
26714
|
onStatus(worktreeId, status) {
|
|
25846
26715
|
const prev = this.lastStatus.get(worktreeId) ?? "idle";
|
|
25847
26716
|
this.lastStatus.set(worktreeId, status);
|
|
25848
|
-
if (status === "working")
|
|
25849
|
-
this.disarm(worktreeId);
|
|
26717
|
+
if (status === "working")
|
|
25850
26718
|
this.d.hold?.(worktreeId, TURN_HOLD);
|
|
25851
|
-
}
|
|
25852
26719
|
const edge = stopOf(prev, status);
|
|
25853
26720
|
if (!edge)
|
|
25854
26721
|
return;
|
|
@@ -25863,64 +26730,13 @@ class TurnService {
|
|
|
25863
26730
|
wt.lastTurn = { at, end, facts: factsOf(turns, end, end === "asking" ? openAskOf(entries) : undefined) };
|
|
25864
26731
|
if (this.stoppedByHand.delete(worktreeId) && end === "stopped")
|
|
25865
26732
|
wt.seenAt = at;
|
|
25866
|
-
this.disarm(worktreeId);
|
|
25867
|
-
if ((wt.seenAt ?? 0) < at)
|
|
25868
|
-
this.arm(worktreeId, at);
|
|
25869
26733
|
this.d.state.save();
|
|
25870
26734
|
this.d.hub.emit("turnSettled", worktreeId, wt.lastTurn);
|
|
25871
26735
|
}
|
|
25872
|
-
arm(worktreeId, at) {
|
|
25873
|
-
const t = setTimeout(() => this.due(worktreeId, at), this.d.delayMs ?? RECAP_DELAY_MS);
|
|
25874
|
-
t.unref?.();
|
|
25875
|
-
this.timers.set(worktreeId, t);
|
|
25876
|
-
}
|
|
25877
|
-
disarm(worktreeId) {
|
|
25878
|
-
const t = this.timers.get(worktreeId);
|
|
25879
|
-
if (!t)
|
|
25880
|
-
return;
|
|
25881
|
-
clearTimeout(t);
|
|
25882
|
-
this.timers.delete(worktreeId);
|
|
25883
|
-
}
|
|
25884
|
-
due(worktreeId, at) {
|
|
25885
|
-
this.timers.delete(worktreeId);
|
|
25886
|
-
const wt = this.d.state.worktree(worktreeId);
|
|
25887
|
-
const turn = wt?.lastTurn;
|
|
25888
|
-
if (!wt || !turn || turn.at !== at || (wt.seenAt ?? 0) >= at)
|
|
25889
|
-
return;
|
|
25890
|
-
turn.recap = { at: Date.now() };
|
|
25891
|
-
this.d.state.save();
|
|
25892
|
-
this.d.hub.emit("worktreesChanged");
|
|
25893
|
-
const seenAt = wt.seenAt ?? 0;
|
|
25894
|
-
const summarize = this.d.summarize;
|
|
25895
|
-
if (!summarize || turn.facts.auth)
|
|
25896
|
-
return;
|
|
25897
|
-
if (this.summarized.get(worktreeId) === seenAt)
|
|
25898
|
-
return;
|
|
25899
|
-
this.summarized.set(worktreeId, seenAt);
|
|
25900
|
-
const entries = this.d.transcript(worktreeId);
|
|
25901
|
-
const prompt = recapPrompt({
|
|
25902
|
-
title: wt.title,
|
|
25903
|
-
firstAsk: firstAskOf(entries),
|
|
25904
|
-
turns: turnsSince(entries, seenAt),
|
|
25905
|
-
end: turn.end,
|
|
25906
|
-
facts: turn.facts
|
|
25907
|
-
});
|
|
25908
|
-
fireAndForget(worktreeId, this.write(worktreeId, at, summarize(wt, prompt)), "recap summary");
|
|
25909
|
-
}
|
|
25910
|
-
async write(worktreeId, at, sentence) {
|
|
25911
|
-
const text = await sentence;
|
|
25912
|
-
const turn = this.d.state.worktree(worktreeId)?.lastTurn;
|
|
25913
|
-
if (!text || turn?.at !== at || !turn.recap)
|
|
25914
|
-
return;
|
|
25915
|
-
turn.recap.text = text;
|
|
25916
|
-
this.d.state.save();
|
|
25917
|
-
this.d.hub.emit("worktreesChanged");
|
|
25918
|
-
}
|
|
25919
26736
|
stoppedByPerson(worktreeId) {
|
|
25920
26737
|
this.stoppedByHand.add(worktreeId);
|
|
25921
26738
|
}
|
|
25922
26739
|
markSeen(worktreeId) {
|
|
25923
|
-
this.disarm(worktreeId);
|
|
25924
26740
|
const wt = this.d.state.worktree(worktreeId);
|
|
25925
26741
|
if (!wt)
|
|
25926
26742
|
return;
|
|
@@ -25953,6 +26769,7 @@ function stopOf(prev, next) {
|
|
|
25953
26769
|
|
|
25954
26770
|
// ../daemon/src/worktrees/service.ts
|
|
25955
26771
|
var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
|
|
26772
|
+
var BASE_IGNORE = "base .gitignore";
|
|
25956
26773
|
function withoutAttachments(event) {
|
|
25957
26774
|
if (event.type !== "user-message")
|
|
25958
26775
|
return event;
|
|
@@ -25966,20 +26783,27 @@ function withCarry(context, c) {
|
|
|
25966
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.`;
|
|
25967
26784
|
return [...context ?? [], line];
|
|
25968
26785
|
}
|
|
25969
|
-
var FETCH_EVERY_MS = 5 * 60000;
|
|
25970
26786
|
|
|
25971
26787
|
class WorktreeService {
|
|
25972
26788
|
d;
|
|
25973
26789
|
spare;
|
|
26790
|
+
trunk;
|
|
25974
26791
|
archive;
|
|
25975
26792
|
archiving = new Map;
|
|
26793
|
+
going = new Set;
|
|
25976
26794
|
countsCache = new Map;
|
|
26795
|
+
handChecked = new Map;
|
|
25977
26796
|
discoverCache = new Map;
|
|
25978
26797
|
usage = new Map;
|
|
25979
|
-
lastFetch = new Map;
|
|
25980
26798
|
constructor(d) {
|
|
25981
26799
|
this.d = d;
|
|
25982
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
|
+
});
|
|
25983
26807
|
this.spare = new SparePool({
|
|
25984
26808
|
state: d.state,
|
|
25985
26809
|
hub: d.hub,
|
|
@@ -25988,10 +26812,28 @@ class WorktreeService {
|
|
|
25988
26812
|
setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
|
|
25989
26813
|
discard: async (id) => {
|
|
25990
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
|
+
}
|
|
25991
26835
|
}
|
|
25992
26836
|
});
|
|
25993
|
-
for (const wt of d.state.worktrees)
|
|
25994
|
-
this.refreshLink(wt);
|
|
25995
26837
|
d.hub.on("agent", (worktreeId, _seq, event) => {
|
|
25996
26838
|
if (event.type !== "usage")
|
|
25997
26839
|
return;
|
|
@@ -26016,18 +26858,15 @@ class WorktreeService {
|
|
|
26016
26858
|
const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
|
|
26017
26859
|
const profile = this.checkProfile(repo, opts.profile);
|
|
26018
26860
|
const task = taskText(prompt, attachments);
|
|
26019
|
-
|
|
26020
|
-
|
|
26021
|
-
slug = `${slug}-${shortId().slice(0, 3)}`;
|
|
26022
|
-
}
|
|
26023
|
-
const branch = `toyon/${slug}`;
|
|
26861
|
+
const base = titleFrom(task);
|
|
26862
|
+
const title = variant ? `${base} v${variant.index}` : base;
|
|
26024
26863
|
if (opts.carry && variant && variant.of > 1) {
|
|
26025
26864
|
throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
|
|
26026
26865
|
}
|
|
26027
26866
|
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
26028
26867
|
|
|
26029
26868
|
`) : prompt;
|
|
26030
|
-
const claimed = await this.spare.claim(repoId,
|
|
26869
|
+
const claimed = await this.spare.claim(repoId, title, opts.worktreeId);
|
|
26031
26870
|
if (claimed) {
|
|
26032
26871
|
if (variant)
|
|
26033
26872
|
claimed.variant = variant;
|
|
@@ -26041,7 +26880,6 @@ class WorktreeService {
|
|
|
26041
26880
|
claimed.model = opts.model;
|
|
26042
26881
|
if (opts.effort)
|
|
26043
26882
|
claimed.effort = opts.effort;
|
|
26044
|
-
this.refreshLink(claimed);
|
|
26045
26883
|
if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
|
|
26046
26884
|
claimed.profile = profile;
|
|
26047
26885
|
this.restartProcs(claimed, repo);
|
|
@@ -26049,22 +26887,31 @@ class WorktreeService {
|
|
|
26049
26887
|
const carried = await this.carryMain(repo, claimed, !!opts.carry);
|
|
26050
26888
|
this.d.state.save();
|
|
26051
26889
|
this.d.hub.emit("worktreesChanged");
|
|
26052
|
-
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 });
|
|
26053
26894
|
this.scheduleNaming(claimed, task, variant);
|
|
26054
26895
|
if (carried.unmoved)
|
|
26055
26896
|
throw new UserError(carried.unmoved);
|
|
26056
26897
|
return claimed;
|
|
26057
26898
|
}
|
|
26058
|
-
|
|
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}`;
|
|
26059
26905
|
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
|
|
26060
26906
|
const wt = {
|
|
26061
|
-
id
|
|
26907
|
+
id,
|
|
26062
26908
|
repoId,
|
|
26063
26909
|
path: wtPath,
|
|
26064
26910
|
branch,
|
|
26065
26911
|
kind: "worktree",
|
|
26066
26912
|
proxyPort: await allocateProxyPort(),
|
|
26067
|
-
title
|
|
26913
|
+
title,
|
|
26914
|
+
unnamed: true,
|
|
26068
26915
|
createdAt: Date.now(),
|
|
26069
26916
|
promptedAt: Date.now(),
|
|
26070
26917
|
agent,
|
|
@@ -26084,9 +26931,20 @@ class WorktreeService {
|
|
|
26084
26931
|
return wt;
|
|
26085
26932
|
}
|
|
26086
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) {
|
|
26087
26943
|
const main = repo.defaultBranch;
|
|
26088
|
-
if (!move)
|
|
26089
|
-
|
|
26944
|
+
if (!move) {
|
|
26945
|
+
const count = this.countsCache.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
|
|
26946
|
+
return { branch: main, moved: false, count };
|
|
26947
|
+
}
|
|
26090
26948
|
let count = 0;
|
|
26091
26949
|
const unmoved = await withRepoLock(repo.path, async () => {
|
|
26092
26950
|
count = (await statusFiles(repo.path)).length;
|
|
@@ -26106,6 +26964,9 @@ class WorktreeService {
|
|
|
26106
26964
|
`).indexOf(sha);
|
|
26107
26965
|
return i < 0 ? null : `stash@{${i}}`;
|
|
26108
26966
|
};
|
|
26967
|
+
const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
|
|
26968
|
+
const restored = (carried.ok ? carried.out.split(`
|
|
26969
|
+
`).filter(Boolean) : []).filter((p) => !existsSync26(join23(wt.path, p)));
|
|
26109
26970
|
const applied = await git(wt.path, "stash", "apply", sha);
|
|
26110
26971
|
if (applied.ok) {
|
|
26111
26972
|
const ref = await entry();
|
|
@@ -26115,12 +26976,14 @@ class WorktreeService {
|
|
|
26115
26976
|
}
|
|
26116
26977
|
log.warn(wt.id, `moving ${main}'s changes failed`, applied.err);
|
|
26117
26978
|
await git(wt.path, "reset", "--hard");
|
|
26118
|
-
|
|
26979
|
+
for (let i = 0;i < restored.length; i += 100) {
|
|
26980
|
+
await git(wt.path, "--literal-pathspecs", "clean", "-fd", "--", ...restored.slice(i, i + 100));
|
|
26981
|
+
}
|
|
26119
26982
|
const ref = await entry();
|
|
26120
26983
|
const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
|
|
26121
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}"`;
|
|
26122
26985
|
});
|
|
26123
|
-
const mainWt = this.
|
|
26986
|
+
const mainWt = this.mainOf(repo.id);
|
|
26124
26987
|
if (mainWt)
|
|
26125
26988
|
this.countsCache.delete(mainWt.id);
|
|
26126
26989
|
return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
|
|
@@ -26138,10 +27001,10 @@ class WorktreeService {
|
|
|
26138
27001
|
throw new UserError("that is not a PR number");
|
|
26139
27002
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
26140
27003
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
26141
|
-
let slug = title;
|
|
26142
|
-
if (
|
|
27004
|
+
let slug = branchSlug(title) || "branch";
|
|
27005
|
+
if (existsSync26(join23(this.d.paths.worktreesDir, repo.name, slug)))
|
|
26143
27006
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
26144
|
-
const wtPath =
|
|
27007
|
+
const wtPath = join23(this.d.paths.worktreesDir, repo.name, slug);
|
|
26145
27008
|
await withRepoLock(repo.path, async () => {
|
|
26146
27009
|
const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
|
|
26147
27010
|
if (out)
|
|
@@ -26289,7 +27152,7 @@ class WorktreeService {
|
|
|
26289
27152
|
if (!name)
|
|
26290
27153
|
return;
|
|
26291
27154
|
for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
|
|
26292
|
-
await this.rename(sibling.id, `${name}
|
|
27155
|
+
await this.rename(sibling.id, `${name} v${sibling.variant.index}`).catch((e) => {
|
|
26293
27156
|
log.warn(sibling.id, "variant rename failed", e);
|
|
26294
27157
|
});
|
|
26295
27158
|
}
|
|
@@ -26317,6 +27180,14 @@ class WorktreeService {
|
|
|
26317
27180
|
this.d.drafts?.drop(worktreeId);
|
|
26318
27181
|
}
|
|
26319
27182
|
async takeDown(wt, archive, reason) {
|
|
27183
|
+
this.going.add(wt.id);
|
|
27184
|
+
try {
|
|
27185
|
+
return await this.removeAndRecord(wt, archive, reason);
|
|
27186
|
+
} finally {
|
|
27187
|
+
this.going.delete(wt.id);
|
|
27188
|
+
}
|
|
27189
|
+
}
|
|
27190
|
+
async removeAndRecord(wt, archive, reason) {
|
|
26320
27191
|
const worktreeId = wt.id;
|
|
26321
27192
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
26322
27193
|
await this.d.runtime.stop(worktreeId);
|
|
@@ -26324,7 +27195,14 @@ class WorktreeService {
|
|
|
26324
27195
|
const k = archive ? await this.keep(repo, wt) : null;
|
|
26325
27196
|
if (archive && !k)
|
|
26326
27197
|
throw new UserError(`could not keep ${wt.title}'s work, so it was left in place`);
|
|
26327
|
-
|
|
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
|
+
}
|
|
26328
27206
|
if (hasOwnBranch(wt)) {
|
|
26329
27207
|
const r = await git(repo.path, "branch", "-D", wt.branch);
|
|
26330
27208
|
if (!r.ok)
|
|
@@ -26332,7 +27210,6 @@ class WorktreeService {
|
|
|
26332
27210
|
}
|
|
26333
27211
|
return k;
|
|
26334
27212
|
});
|
|
26335
|
-
this.dropLink(wt);
|
|
26336
27213
|
const sessionId = this.d.state.session(worktreeId);
|
|
26337
27214
|
this.d.state.removeWorktree(worktreeId);
|
|
26338
27215
|
releasePort(wt.proxyPort);
|
|
@@ -26348,7 +27225,7 @@ class WorktreeService {
|
|
|
26348
27225
|
return archived;
|
|
26349
27226
|
}
|
|
26350
27227
|
async keep(repo, wt) {
|
|
26351
|
-
const index =
|
|
27228
|
+
const index = join23(this.d.paths.archiveDir, `${wt.id}.index`);
|
|
26352
27229
|
const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index);
|
|
26353
27230
|
if (!kept)
|
|
26354
27231
|
log.warn(wt.id, "could not keep its git state");
|
|
@@ -26356,8 +27233,21 @@ class WorktreeService {
|
|
|
26356
27233
|
log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
|
|
26357
27234
|
return kept;
|
|
26358
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
|
+
}
|
|
26359
27249
|
archiveChat(wt, repo, kept, sessionId, reason) {
|
|
26360
|
-
const files = this.chatFiles(wt.id);
|
|
27250
|
+
const files = { ...this.chatFiles(wt.id), plans: this.plansScratch(wt.id) };
|
|
26361
27251
|
const prompt = firstPrompt(files.transcript);
|
|
26362
27252
|
const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
|
|
26363
27253
|
const rec = {
|
|
@@ -26370,14 +27260,15 @@ class WorktreeService {
|
|
|
26370
27260
|
...kept ? { kept } : {},
|
|
26371
27261
|
...reason ? { auto: reason } : {}
|
|
26372
27262
|
};
|
|
27263
|
+
let at;
|
|
26373
27264
|
try {
|
|
26374
|
-
this.archive.put(rec, files);
|
|
27265
|
+
at = this.archive.put(rec, files);
|
|
26375
27266
|
} catch (e) {
|
|
26376
27267
|
log.warn(wt.id, "could not archive its chat; the files stay where they were", e);
|
|
26377
27268
|
return null;
|
|
26378
27269
|
}
|
|
26379
27270
|
this.d.hub.emit("archiveChanged", repo.id);
|
|
26380
|
-
return summarize(rec, repo.id);
|
|
27271
|
+
return summarize(rec, repo.id, at.transcript);
|
|
26381
27272
|
}
|
|
26382
27273
|
chatFiles(worktreeId) {
|
|
26383
27274
|
return {
|
|
@@ -26397,7 +27288,6 @@ class WorktreeService {
|
|
|
26397
27288
|
async forgetGone(wt) {
|
|
26398
27289
|
const repo = this.d.state.repo(wt.repoId);
|
|
26399
27290
|
const sessionId = this.d.state.session(wt.id);
|
|
26400
|
-
this.dropLink(wt);
|
|
26401
27291
|
this.d.state.removeWorktree(wt.id);
|
|
26402
27292
|
if (!repo || wt.kind !== "worktree") {
|
|
26403
27293
|
this.deleteChat(wt.id);
|
|
@@ -26436,7 +27326,7 @@ class WorktreeService {
|
|
|
26436
27326
|
}
|
|
26437
27327
|
archivedAttachment(archiveId, file) {
|
|
26438
27328
|
const files = this.archive.chatFiles(archiveId);
|
|
26439
|
-
return files && isAttachmentFile(file) ?
|
|
27329
|
+
return files && isAttachmentFile(file) ? join23(files.attachments, file) : null;
|
|
26440
27330
|
}
|
|
26441
27331
|
archivedGit(archiveId) {
|
|
26442
27332
|
const rec = this.archive.get(archiveId);
|
|
@@ -26457,6 +27347,9 @@ class WorktreeService {
|
|
|
26457
27347
|
}
|
|
26458
27348
|
}
|
|
26459
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 };
|
|
26460
27353
|
const kept = this.archivedGit(archiveId);
|
|
26461
27354
|
return kept ? kept.file(path, ref) : null;
|
|
26462
27355
|
}
|
|
@@ -26476,7 +27369,7 @@ class WorktreeService {
|
|
|
26476
27369
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
26477
27370
|
if (rec.auto)
|
|
26478
27371
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
26479
|
-
const path =
|
|
27372
|
+
const path = existsSync26(old.path) ? join23(dirname9(old.path), freeSlot(dirname9(old.path)).dir) : old.path;
|
|
26480
27373
|
const branch = await withRepoLock(repo.path, async () => {
|
|
26481
27374
|
if (!await commitOf(repo.path, archiveRef(old.id))) {
|
|
26482
27375
|
throw new UserError(`${old.title}'s kept commits are gone from git`);
|
|
@@ -26502,7 +27395,7 @@ class WorktreeService {
|
|
|
26502
27395
|
}
|
|
26503
27396
|
return name;
|
|
26504
27397
|
});
|
|
26505
|
-
const { variant: _variant,
|
|
27398
|
+
const { variant: _variant, ...rest } = old;
|
|
26506
27399
|
const wt = {
|
|
26507
27400
|
...rest,
|
|
26508
27401
|
repoId: repo.id,
|
|
@@ -26513,7 +27406,7 @@ class WorktreeService {
|
|
|
26513
27406
|
...createdBy ? { createdBy } : {}
|
|
26514
27407
|
};
|
|
26515
27408
|
try {
|
|
26516
|
-
this.archive.take(old.id, this.chatFiles(old.id));
|
|
27409
|
+
this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join23(path, PLANS_DIR) });
|
|
26517
27410
|
} catch (e) {
|
|
26518
27411
|
log.warn(old.id, "could not move its chat back out of the archive", e);
|
|
26519
27412
|
}
|
|
@@ -26523,7 +27416,6 @@ class WorktreeService {
|
|
|
26523
27416
|
if (!dropped.ok)
|
|
26524
27417
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
26525
27418
|
this.countsCache.delete(wt.id);
|
|
26526
|
-
this.refreshLink(wt);
|
|
26527
27419
|
this.launch(wt, repo, repo.path);
|
|
26528
27420
|
if (message)
|
|
26529
27421
|
this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
|
|
@@ -26536,12 +27428,12 @@ class WorktreeService {
|
|
|
26536
27428
|
return;
|
|
26537
27429
|
const repo = this.repoOf(rec);
|
|
26538
27430
|
const repoPath = repo?.path ?? rec.repoPath;
|
|
26539
|
-
if (rec.kept &&
|
|
27431
|
+
if (rec.kept && existsSync26(repoPath)) {
|
|
26540
27432
|
const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
|
|
26541
27433
|
if (!r.ok)
|
|
26542
27434
|
log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
|
|
26543
27435
|
}
|
|
26544
|
-
if (rec.worktree.lands?.length &&
|
|
27436
|
+
if (rec.worktree.lands?.length && existsSync26(repoPath))
|
|
26545
27437
|
await dropLandRefs(repoPath, archiveId);
|
|
26546
27438
|
this.archive.delete(archiveId);
|
|
26547
27439
|
this.d.drafts?.drop(archiveId);
|
|
@@ -26558,55 +27450,24 @@ class WorktreeService {
|
|
|
26558
27450
|
const clean = cleanTitle2(title);
|
|
26559
27451
|
if (!clean)
|
|
26560
27452
|
return;
|
|
27453
|
+
const slug = branchSlug(clean);
|
|
26561
27454
|
await withRepoLock(repo.path, async () => {
|
|
26562
|
-
let branch = `toyon/${
|
|
26563
|
-
if (branch !== wt.branch) {
|
|
27455
|
+
let branch = `toyon/${slug}`;
|
|
27456
|
+
if (slug && branch !== wt.branch) {
|
|
26564
27457
|
let n = 2;
|
|
26565
27458
|
while (!(await git(wt.path, "branch", "-m", wt.branch, branch)).ok) {
|
|
26566
27459
|
if (n > 5)
|
|
26567
27460
|
return;
|
|
26568
|
-
branch = `toyon/${
|
|
27461
|
+
branch = `toyon/${slug}-${n++}`;
|
|
26569
27462
|
}
|
|
26570
27463
|
wt.branch = branch;
|
|
26571
27464
|
}
|
|
26572
27465
|
wt.title = clean;
|
|
27466
|
+
delete wt.unnamed;
|
|
26573
27467
|
});
|
|
26574
|
-
this.refreshLink(wt);
|
|
26575
27468
|
this.d.state.save();
|
|
26576
27469
|
this.d.hub.emit("worktreesChanged");
|
|
26577
27470
|
}
|
|
26578
|
-
refreshLink(wt) {
|
|
26579
|
-
if (!hasOwnBranch(wt))
|
|
26580
|
-
return;
|
|
26581
|
-
const name = wt.branch.replace(/^toyon\//, "");
|
|
26582
|
-
const desired = wt.kind === "worktree" ? join21(dirname8(wt.path), name) : wt.path;
|
|
26583
|
-
if (wt.linkPath && wt.linkPath !== desired)
|
|
26584
|
-
this.dropLink(wt);
|
|
26585
|
-
if (desired === wt.path)
|
|
26586
|
-
return;
|
|
26587
|
-
try {
|
|
26588
|
-
const st = lstatSync(desired, { throwIfNoEntry: false });
|
|
26589
|
-
if (st && !(st.isSymbolicLink() && readlinkSync(desired) === wt.path))
|
|
26590
|
-
return;
|
|
26591
|
-
if (!st)
|
|
26592
|
-
symlinkSync(wt.path, desired);
|
|
26593
|
-
wt.linkPath = desired;
|
|
26594
|
-
} catch (e) {
|
|
26595
|
-
log.warn(wt.id, "could not link the title to the directory", e);
|
|
26596
|
-
}
|
|
26597
|
-
}
|
|
26598
|
-
dropLink(wt) {
|
|
26599
|
-
const p = wt.linkPath;
|
|
26600
|
-
wt.linkPath = undefined;
|
|
26601
|
-
if (!p)
|
|
26602
|
-
return;
|
|
26603
|
-
try {
|
|
26604
|
-
if (lstatSync(p, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
26605
|
-
unlinkSync(p);
|
|
26606
|
-
} catch (e) {
|
|
26607
|
-
log.warn(wt.id, "could not remove the title link", e);
|
|
26608
|
-
}
|
|
26609
|
-
}
|
|
26610
27471
|
async pickVariant(worktreeId) {
|
|
26611
27472
|
const wt = this.d.state.worktree(worktreeId);
|
|
26612
27473
|
if (!wt?.variant)
|
|
@@ -26671,10 +27532,19 @@ class WorktreeService {
|
|
|
26671
27532
|
this.d.runtime.markSetup(wt.id, false);
|
|
26672
27533
|
}
|
|
26673
27534
|
}
|
|
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;
|
|
27539
|
+
const lines = mirror ? readFileSync16(src, "utf8").split(`
|
|
27540
|
+
`).filter((l) => l.trim() !== "") : [];
|
|
27541
|
+
await excludeBlock(repo.path, BASE_IGNORE, lines);
|
|
27542
|
+
}
|
|
26674
27543
|
async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
|
|
26675
|
-
|
|
26676
|
-
const
|
|
26677
|
-
|
|
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)) {
|
|
26678
27548
|
const started = Date.now();
|
|
26679
27549
|
const attempts = [
|
|
26680
27550
|
["clonefile", ["-Rc", srcNm, dstNm]],
|
|
@@ -26690,10 +27560,10 @@ class WorktreeService {
|
|
|
26690
27560
|
}
|
|
26691
27561
|
}
|
|
26692
27562
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
26693
|
-
const src =
|
|
26694
|
-
if (!
|
|
27563
|
+
const src = join23(depsSource, f);
|
|
27564
|
+
if (!existsSync26(src) || existsSync26(join23(wt.path, f)))
|
|
26695
27565
|
continue;
|
|
26696
|
-
const r = await run("cp", [src,
|
|
27566
|
+
const r = await run("cp", [src, join23(wt.path, f)], wt.path);
|
|
26697
27567
|
if (r.ok)
|
|
26698
27568
|
this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
|
|
26699
27569
|
else
|
|
@@ -26758,10 +27628,42 @@ ${wt.landing.body}` : s;
|
|
|
26758
27628
|
async commitIfDirty(wt, typed) {
|
|
26759
27629
|
if ((await statusFiles(wt.path)).length === 0)
|
|
26760
27630
|
return null;
|
|
26761
|
-
|
|
26762
|
-
|
|
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 });
|
|
26763
27637
|
this.headMoved(wt.id);
|
|
26764
|
-
|
|
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);
|
|
26765
27667
|
}
|
|
26766
27668
|
async land(worktreeId, message) {
|
|
26767
27669
|
const { wt, repo } = this.landable(worktreeId, "land");
|
|
@@ -26772,8 +27674,24 @@ ${wt.landing.body}` : s;
|
|
|
26772
27674
|
${wt.landing.body}` : wt.landing.subject : undefined;
|
|
26773
27675
|
if (policy.land === "pr") {
|
|
26774
27676
|
if (wt.pr?.state === "open") {
|
|
26775
|
-
const
|
|
26776
|
-
|
|
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` } };
|
|
26777
27695
|
}
|
|
26778
27696
|
const committed = await this.commitIfDirty(wt, message);
|
|
26779
27697
|
if (committed && !committed.ok)
|
|
@@ -26799,10 +27717,12 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26799
27717
|
return { result };
|
|
26800
27718
|
}
|
|
26801
27719
|
let mergedHere = false;
|
|
27720
|
+
let committedHere = false;
|
|
26802
27721
|
const result = await withRepoLock(repo.path, async () => {
|
|
26803
27722
|
const committed = await this.commitIfDirty(wt, message);
|
|
26804
27723
|
if (committed && !committed.ok)
|
|
26805
27724
|
return committed;
|
|
27725
|
+
committedHere = committed !== null;
|
|
26806
27726
|
if (policy.land === "push") {
|
|
26807
27727
|
const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
|
|
26808
27728
|
if (!pulled.ok && !/no upstream/.test(pulled.message))
|
|
@@ -26835,8 +27755,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26835
27755
|
if (!result.ok)
|
|
26836
27756
|
return { result };
|
|
26837
27757
|
const archiveIds = siblingsOf(wt, this.d.state.worktrees).map((w) => w.id);
|
|
26838
|
-
const
|
|
26839
|
-
|
|
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 };
|
|
26840
27761
|
}
|
|
26841
27762
|
async noteLand(repo, wt, mark) {
|
|
26842
27763
|
if (!mark)
|
|
@@ -26882,6 +27803,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26882
27803
|
throw new UserError("that worktree is gone");
|
|
26883
27804
|
if (r.wt && isMain(r.wt))
|
|
26884
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");
|
|
26885
27808
|
if (!r.branch)
|
|
26886
27809
|
throw new UserError(`${r.name} is detached: check out a branch in it first`);
|
|
26887
27810
|
if (r.locked)
|
|
@@ -26889,8 +27812,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26889
27812
|
const repo = this.d.state.requireRepo(r.repoId);
|
|
26890
27813
|
const own = r.wt ? hasOwnBranch(r.wt) : false;
|
|
26891
27814
|
const result = await withRepoLock(repo.path, () => takeMainIn(r.path, repo.defaultBranch, own));
|
|
26892
|
-
if (result.ok)
|
|
27815
|
+
if (result.ok) {
|
|
26893
27816
|
this.headMoved(worktreeId);
|
|
27817
|
+
if (r.wt)
|
|
27818
|
+
await this.verdictSurvives(r.wt, { dropMessage: false });
|
|
27819
|
+
}
|
|
26894
27820
|
return { result, defaultBranch: repo.defaultBranch };
|
|
26895
27821
|
}
|
|
26896
27822
|
async pull(worktreeId) {
|
|
@@ -26910,10 +27836,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26910
27836
|
const m = message.trim();
|
|
26911
27837
|
if (!m)
|
|
26912
27838
|
throw new UserError("commit message required");
|
|
26913
|
-
|
|
26914
|
-
if (result.ok)
|
|
26915
|
-
this.headMoved(wt.id);
|
|
26916
|
-
return result;
|
|
27839
|
+
return this.commitRecorded(wt, m);
|
|
26917
27840
|
}
|
|
26918
27841
|
setEmpty(wt, empty) {
|
|
26919
27842
|
if (wt.empty === empty)
|
|
@@ -26922,23 +27845,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
26922
27845
|
this.d.state.save();
|
|
26923
27846
|
this.d.hub.emit("worktreesChanged");
|
|
26924
27847
|
}
|
|
26925
|
-
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
if (Date.now() - last > FETCH_EVERY_MS) {
|
|
26931
|
-
this.lastFetch.set(path, Date.now());
|
|
26932
|
-
fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then((r) => {
|
|
26933
|
-
if (!r.ok) {
|
|
26934
|
-
log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
|
|
26935
|
-
return;
|
|
26936
|
-
}
|
|
26937
|
-
this.invalidateCounts();
|
|
26938
|
-
this.d.hub.emit("worktreesChanged");
|
|
26939
|
-
}));
|
|
26940
|
-
}
|
|
26941
|
-
return { behind };
|
|
27848
|
+
syncTrunk(repoId) {
|
|
27849
|
+
return this.trunk.sync(repoId);
|
|
27850
|
+
}
|
|
27851
|
+
trunks(opts = {}) {
|
|
27852
|
+
return this.trunk.all(opts);
|
|
26942
27853
|
}
|
|
26943
27854
|
usageFor(worktreeId) {
|
|
26944
27855
|
const known = this.usage.get(worktreeId);
|
|
@@ -27004,15 +27915,26 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27004
27915
|
const cached = this.countsCache.get(id);
|
|
27005
27916
|
if (cached && Date.now() - cached.at < 1e4)
|
|
27006
27917
|
return cached;
|
|
27918
|
+
if (this.going.has(id))
|
|
27919
|
+
return cached ?? {};
|
|
27007
27920
|
try {
|
|
27008
|
-
const ab = countable ? await aheadBehind(path, defaultBranch) : await this.
|
|
27009
|
-
const
|
|
27921
|
+
const ab = countable ? await aheadBehind(path, defaultBranch) : await this.trunk.behind(id, path);
|
|
27922
|
+
const dirty = (await statusFiles(path)).length;
|
|
27923
|
+
const unpushed = await this.unpushed(id, path);
|
|
27924
|
+
if (this.going.has(id))
|
|
27925
|
+
return cached ?? {};
|
|
27926
|
+
const fresh = { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty, at: Date.now() };
|
|
27010
27927
|
this.countsCache.set(id, fresh);
|
|
27011
27928
|
return fresh;
|
|
27012
27929
|
} catch {
|
|
27013
27930
|
return cached ?? {};
|
|
27014
27931
|
}
|
|
27015
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
|
+
}
|
|
27016
27938
|
async freshCounts(worktreeId) {
|
|
27017
27939
|
const wt = this.d.state.worktree(worktreeId);
|
|
27018
27940
|
const repo = wt && this.d.state.repo(wt.repoId);
|
|
@@ -27024,8 +27946,6 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27024
27946
|
readable(id) {
|
|
27025
27947
|
const wt = this.d.state.worktree(id);
|
|
27026
27948
|
if (wt) {
|
|
27027
|
-
if (wt.kind === "spare")
|
|
27028
|
-
return null;
|
|
27029
27949
|
const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
|
|
27030
27950
|
return { id, repoId: wt.repoId, path: wt.path, name: wt.title, branch: wt.branch, defaultBranch, wt };
|
|
27031
27951
|
}
|
|
@@ -27044,16 +27964,20 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27044
27964
|
};
|
|
27045
27965
|
}
|
|
27046
27966
|
async gitStatus(worktreeId) {
|
|
27967
|
+
if (this.going.has(worktreeId))
|
|
27968
|
+
return null;
|
|
27047
27969
|
const r = this.readable(worktreeId);
|
|
27048
27970
|
if (!r)
|
|
27049
27971
|
return this.archivedStatus(worktreeId);
|
|
27050
27972
|
try {
|
|
27051
27973
|
const isMain = r.wt?.kind === "main";
|
|
27052
|
-
const [files,
|
|
27974
|
+
const [files, ab, unpushed, head] = await Promise.all([
|
|
27053
27975
|
statusFilesWithCounts(r.path),
|
|
27054
27976
|
isMain ? Promise.resolve({}) : aheadBehind(r.path, r.defaultBranch),
|
|
27977
|
+
this.unpushed(worktreeId, r.path),
|
|
27055
27978
|
git(r.path, "rev-parse", "HEAD")
|
|
27056
27979
|
]);
|
|
27980
|
+
const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
|
|
27057
27981
|
const ahead = counts.ahead ?? 0;
|
|
27058
27982
|
const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.defaultBranch) : undefined;
|
|
27059
27983
|
if (r.wt?.landed && (files.length > 0 || ahead > 0)) {
|
|
@@ -27061,10 +27985,25 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27061
27985
|
if (r.wt.pr)
|
|
27062
27986
|
this.setPr(r.wt.id, undefined);
|
|
27063
27987
|
}
|
|
27064
|
-
if (r.wt
|
|
27065
|
-
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
|
+
}
|
|
27066
28003
|
if (r.wt && isMain)
|
|
27067
28004
|
this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
|
|
28005
|
+
if (this.going.has(worktreeId))
|
|
28006
|
+
return null;
|
|
27068
28007
|
this.noteCounts(worktreeId, isMain, files.length, counts);
|
|
27069
28008
|
return { files, committed, head: head.ok ? head.out : undefined, ...counts };
|
|
27070
28009
|
} catch (e) {
|
|
@@ -27078,8 +28017,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27078
28017
|
return;
|
|
27079
28018
|
const ahead = main ? prev?.ahead : ab.ahead;
|
|
27080
28019
|
const behind = main ? prev?.behind : ab.behind;
|
|
27081
|
-
|
|
27082
|
-
|
|
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)
|
|
27083
28023
|
this.d.hub.emit("worktreesChanged");
|
|
27084
28024
|
}
|
|
27085
28025
|
async gitLog(worktreeId) {
|
|
@@ -27106,7 +28046,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27106
28046
|
await this.restore(worktreeId, msg.clientId, { text: msg.text, attachments: msg.attachments });
|
|
27107
28047
|
return;
|
|
27108
28048
|
}
|
|
27109
|
-
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");
|
|
27110
28052
|
const agent = this.d.runtime.agentFor(worktreeId);
|
|
27111
28053
|
if (!agent)
|
|
27112
28054
|
throw new UserError("worktree still starting; try again in a moment");
|
|
@@ -27128,14 +28070,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27128
28070
|
setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
|
|
27129
28071
|
return [...owned, ...found];
|
|
27130
28072
|
}
|
|
27131
|
-
|
|
27132
|
-
return this.d.state
|
|
27133
|
-
|
|
27134
|
-
|
|
27135
|
-
|
|
27136
|
-
proxyPort: wt.proxyPort,
|
|
27137
|
-
ready: this.d.runtime.get(wt.id)?.proxy != null && this.spare.current(wt.repoId)?.worktreeId === wt.id
|
|
27138
|
-
}));
|
|
28073
|
+
mainOf(repoId) {
|
|
28074
|
+
return mainOf(this.d.state, repoId);
|
|
28075
|
+
}
|
|
28076
|
+
leadSpareOf(repoId) {
|
|
28077
|
+
return this.spare.current(repoId)?.worktreeId ?? null;
|
|
27139
28078
|
}
|
|
27140
28079
|
countsQuick(id, quick) {
|
|
27141
28080
|
const c = this.countsCache.get(id);
|
|
@@ -27144,11 +28083,17 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27144
28083
|
quick.missed = true;
|
|
27145
28084
|
return {};
|
|
27146
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
|
+
}
|
|
27147
28092
|
async ownedRows(quick) {
|
|
27148
|
-
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) => {
|
|
27149
28094
|
const rt = this.d.runtime.get(wt.id);
|
|
27150
28095
|
const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
|
|
27151
|
-
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));
|
|
27152
28097
|
return {
|
|
27153
28098
|
id: wt.id,
|
|
27154
28099
|
repoId: wt.repoId,
|
|
@@ -27161,10 +28106,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
27161
28106
|
login: !!rt?.login,
|
|
27162
28107
|
ahead,
|
|
27163
28108
|
behind,
|
|
28109
|
+
unpushed,
|
|
27164
28110
|
dirty,
|
|
27165
28111
|
queued: rt?.agent.queueLength || undefined,
|
|
27166
28112
|
unseen: isUnseen(wt) || undefined,
|
|
27167
|
-
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)
|
|
27168
28116
|
};
|
|
27169
28117
|
}));
|
|
27170
28118
|
}
|
|
@@ -27269,7 +28217,7 @@ class ArchiveSweep {
|
|
|
27269
28217
|
// ../daemon/src/index.ts
|
|
27270
28218
|
process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
|
|
27271
28219
|
process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
|
|
27272
|
-
var here =
|
|
28220
|
+
var here = dirname10(fileURLToPath(import.meta.url));
|
|
27273
28221
|
var {
|
|
27274
28222
|
shellDist: SHELL_DIST,
|
|
27275
28223
|
bridgeJs: BRIDGE_JS,
|
|
@@ -27323,29 +28271,39 @@ var runtime = new RuntimeRegistry({
|
|
|
27323
28271
|
bridgeScript: () => bridge.get(),
|
|
27324
28272
|
remote,
|
|
27325
28273
|
grant: previewGrant(token),
|
|
27326
|
-
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
|
|
27327
28277
|
});
|
|
27328
28278
|
var drafts = new DraftStore({ file: paths.draftsFile, hub });
|
|
27329
|
-
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
|
+
});
|
|
27330
28289
|
var turns = new TurnService({
|
|
27331
28290
|
state,
|
|
27332
28291
|
hub,
|
|
27333
28292
|
transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
|
|
27334
|
-
summarize: makeRecapper(runtime, agents, state),
|
|
27335
28293
|
hold: (id, tag) => runtime.hold(id, tag),
|
|
27336
28294
|
release: (id, tag) => runtime.release(id, tag)
|
|
27337
28295
|
});
|
|
27338
28296
|
var files = new FileService(state, runtime, (id) => worktrees.readable(id));
|
|
27339
28297
|
var design = new DesignService((id) => worktrees.readable(id));
|
|
27340
28298
|
var routes = new RouteService({ state, hub, readable: (id) => worktrees.readable(id) });
|
|
27341
|
-
var
|
|
27342
|
-
new LandingService({
|
|
28299
|
+
var landing = new LandingService({
|
|
27343
28300
|
state,
|
|
27344
28301
|
hub,
|
|
27345
28302
|
worktrees,
|
|
27346
28303
|
transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
|
|
27347
|
-
check: (id, command) => exec.exec(id, command, CHECK_TOOL),
|
|
27348
|
-
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)
|
|
27349
28307
|
});
|
|
27350
28308
|
var refs = new RefSearch({ state });
|
|
27351
28309
|
var chats = new ChatSearch({
|
|
@@ -27357,7 +28315,7 @@ var chats = new ChatSearch({
|
|
|
27357
28315
|
var prs = new PrService({ state, hub, worktrees, view: viewPr });
|
|
27358
28316
|
var self = new SelfWatch(SOURCE_ROOT);
|
|
27359
28317
|
await self.start();
|
|
27360
|
-
var afterLand = new AfterLand({ state, hub, self });
|
|
28318
|
+
var afterLand = new AfterLand({ state, hub, self, settled: () => idle.settled() });
|
|
27361
28319
|
var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
|
|
27362
28320
|
var idle = new IdlePolicy({
|
|
27363
28321
|
runtime,
|
|
@@ -27426,6 +28384,7 @@ var { branded, stop: stopServer } = startServer({
|
|
|
27426
28384
|
chats,
|
|
27427
28385
|
drafts,
|
|
27428
28386
|
prs,
|
|
28387
|
+
landing,
|
|
27429
28388
|
themes,
|
|
27430
28389
|
agents,
|
|
27431
28390
|
accounts,
|
|
@@ -27449,14 +28408,12 @@ bridge.setShellOrigins([
|
|
|
27449
28408
|
],
|
|
27450
28409
|
...remote ? [`https://${remote.host}`] : []
|
|
27451
28410
|
]);
|
|
27452
|
-
|
|
28411
|
+
writeFileSync10(paths.pidFile, `${process.pid}
|
|
27453
28412
|
`);
|
|
27454
28413
|
var stopLagSampler = startLagSampler();
|
|
28414
|
+
await runtime.reclaimOrphans();
|
|
27455
28415
|
await repos.boot();
|
|
27456
|
-
drafts.prune((id) =>
|
|
27457
|
-
const repoId = draftRepoOf(id);
|
|
27458
|
-
return repoId ? !!state.repo(repoId) : !!state.worktree(id) || worktrees.hasArchived(id);
|
|
27459
|
-
});
|
|
28416
|
+
drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
|
|
27460
28417
|
idle.boot();
|
|
27461
28418
|
fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
|
|
27462
28419
|
var repoArg = process.argv[2];
|
|
@@ -27497,6 +28454,7 @@ async function shutdown(signal, opts = {}) {
|
|
|
27497
28454
|
drafts.flush();
|
|
27498
28455
|
const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
27499
28456
|
await Promise.race([runtime.shutdown(), deadline]);
|
|
28457
|
+
state.flushGroups();
|
|
27500
28458
|
rmSync6(paths.pidFile, { force: true });
|
|
27501
28459
|
if (opts.respawn) {
|
|
27502
28460
|
await Bun.sleep(250);
|