toyon 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +62 -39
  2. package/dist/bridge.js +1 -1
  3. package/dist/cli.js +255 -68
  4. package/dist/daemon.js +2404 -516
  5. package/dist/shell/assets/{Editor-Cn-KsoFr.js → Editor-Lle8R8mY.js} +148 -148
  6. package/dist/shell/assets/{XTerm-DSSGYyFZ.js → XTerm-DcWpV1FL.js} +13 -13
  7. package/dist/shell/assets/{cssMode-BU_otbjh.js → cssMode-Ck0mAapo.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-C-NGmKSC.js → freemarker2-B5OkSZOW.js} +1 -1
  9. package/dist/shell/assets/{handlebars-BWvVdY_e.js → handlebars-C6vrFwYj.js} +1 -1
  10. package/dist/shell/assets/{html-CBO-8nU2.js → html-CE92cnIr.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-Dwz6npQ-.js → htmlMode-CfM52A9Q.js} +1 -1
  12. package/dist/shell/assets/index-DOf1M9v5.js +233 -0
  13. package/dist/shell/assets/index-b4LMTPcs.css +1 -0
  14. package/dist/shell/assets/{javascript-__evPE7m.js → javascript-C7Q1bCJn.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-DpbQTbP6.js → jsonMode-Cq43R9DW.js} +1 -1
  16. package/dist/shell/assets/{liquid-CgOzfjEh.js → liquid-CnxQAGJm.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-DM57zeAM.js → lspLanguageFeatures-BtjVqb14.js} +1 -1
  18. package/dist/shell/assets/{mdx-DxN7Bq5u.js → mdx-ycMLgnto.js} +1 -1
  19. package/dist/shell/assets/{python-C3rGYQJF.js → python-se_0lOPN.js} +1 -1
  20. package/dist/shell/assets/{razor-C5rkTP_6.js → razor-DjByizBq.js} +1 -1
  21. package/dist/shell/assets/{tsMode-DDCDv-RH.js → tsMode-D1mOZbLC.js} +1 -1
  22. package/dist/shell/assets/{typescript-CyHgeKIZ.js → typescript-B-dZdvXG.js} +1 -1
  23. package/dist/shell/assets/{workers-C7XbwGBM.js → workers-DXwGNRtC.js} +1 -1
  24. package/dist/shell/assets/{xml-Dmw92JAf.js → xml-CoqNFco9.js} +1 -1
  25. package/dist/shell/assets/{yaml-Bj_aHuSs.js → yaml-D5Qru8sI.js} +1 -1
  26. package/dist/shell/index.html +2 -2
  27. package/dist/shell/manifest.json +1 -1
  28. package/package.json +2 -2
  29. package/dist/shell/assets/index-B61fMQI2.css +0 -1
  30. package/dist/shell/assets/index-Mb4zTZaD.js +0 -230
package/dist/daemon.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // ../daemon/src/index.ts
3
- import { rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
3
+ import { rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
4
4
  import { dirname as dirname9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
@@ -13,6 +13,111 @@ function isWriteTool(ev) {
13
13
  return ev.kind ? WRITE_KINDS.has(ev.kind) : WRITE_TOOLS.has(ev.name);
14
14
  }
15
15
 
16
+ // ../shared/src/model.ts
17
+ var DEFAULT_PERMISSION_MODE = "auto";
18
+ function hasOwnBranch(wt) {
19
+ return wt.branch.startsWith("toyon/");
20
+ }
21
+ function isUnseen(wt) {
22
+ return wt.unread === true || wt.lastTurn != null && (wt.seenAt == null || wt.seenAt < wt.lastTurn.at);
23
+ }
24
+ var SHELL_STREAM = "shell";
25
+ var LOGIN_STREAM = "login";
26
+ function streamKey(worktreeId, stream) {
27
+ return `${worktreeId}/${stream}`;
28
+ }
29
+
30
+ // ../shared/src/worktree-caps.ts
31
+ function isMain(wt) {
32
+ return wt.kind === "main";
33
+ }
34
+ function canArchive(wt) {
35
+ return wt.kind === "worktree";
36
+ }
37
+ function canRename(wt) {
38
+ return wt.kind === "worktree" && hasOwnBranch(wt);
39
+ }
40
+ function canLand(wt) {
41
+ return wt.kind === "worktree" && wt.from?.kind !== "pr";
42
+ }
43
+ function canGraft(wt) {
44
+ return wt.kind === "worktree";
45
+ }
46
+
47
+ // ../shared/src/railOrder.ts
48
+ var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
49
+ var tierOf = (w) => isMain(w) ? 0 : w.landed ? 2 : 1;
50
+ var railUnitOf = (w) => w.variant ? `${tierOf(w)}:${w.variant.group}` : `row:${w.id}`;
51
+ function railOrder(rows) {
52
+ const units = [];
53
+ const groups = new Map;
54
+ for (const row of rows) {
55
+ const w = row.worktree;
56
+ const key = railUnitOf(w);
57
+ const unit = groups.get(key);
58
+ if (unit) {
59
+ unit.rows.push(row);
60
+ unit.at = Math.max(unit.at, sentAt(w));
61
+ continue;
62
+ }
63
+ const fresh = { tier: tierOf(w), at: sentAt(w), rows: [row] };
64
+ units.push(fresh);
65
+ groups.set(key, fresh);
66
+ }
67
+ units.sort((a, b) => a.tier - b.tier || b.at - a.at);
68
+ return units.flatMap((u) => u.rows.length > 1 ? u.rows.sort((a, b) => (a.worktree.variant?.index ?? 0) - (b.worktree.variant?.index ?? 0)) : u.rows);
69
+ }
70
+
71
+ // ../shared/src/archiveRule.ts
72
+ var ARCHIVE_KEEP_RECENT = 5;
73
+ function archiveReason(wt, f) {
74
+ const group = wt.variant ? f.rows.filter((w) => w.variant?.group === wt.variant?.group) : [wt];
75
+ const decided = group.some((w) => !!w.lands?.length);
76
+ const lost = decided && !wt.lands?.length;
77
+ if (decided) {
78
+ if (!nothingToKeep(wt, f, lost))
79
+ return null;
80
+ } else if (!group.every((w) => nothingToKeep(w, f, false)))
81
+ return null;
82
+ if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
83
+ return null;
84
+ const what = lost ? "a sibling landed" : wt.landed ? "landed" : "no changes";
85
+ return `${what}, not opened in ${duration(f.afterMs)}`;
86
+ }
87
+ function nothingToKeep(w, f, lost) {
88
+ if (w.kind !== "worktree" || !hasOwnBranch(w))
89
+ return false;
90
+ if (w.lastTurn?.end !== "done" || isUnseen(w))
91
+ return false;
92
+ if (w.planned || w.pr?.state === "open")
93
+ return false;
94
+ if (f.viewed(w.id) || f.pending(w.id) || f.drafted(w.id))
95
+ return false;
96
+ if (f.now - (w.viewedAt ?? w.createdAt) < f.afterMs)
97
+ return false;
98
+ const c = f.counts(w.id);
99
+ return c.dirty === 0 && (lost ? c.ahead !== undefined : c.ahead === 0);
100
+ }
101
+ function unitsAhead(wt, rows) {
102
+ const tasks = railOrder(rows.filter((w) => w.kind === "worktree").map((worktree) => ({ worktree })));
103
+ const mine = railUnitOf(wt);
104
+ const seen = new Set;
105
+ for (const { worktree: w } of tasks) {
106
+ const unit = railUnitOf(w);
107
+ if (unit === mine)
108
+ return seen.size;
109
+ seen.add(unit);
110
+ }
111
+ return seen.size;
112
+ }
113
+ function duration(ms) {
114
+ if (ms >= 60 * 60000)
115
+ return `${Math.round(ms / (60 * 60000))}h`;
116
+ if (ms >= 60000)
117
+ return `${Math.round(ms / 60000)} min`;
118
+ return `${Math.round(ms / 1000)}s`;
119
+ }
120
+
16
121
  // ../shared/src/attachment.ts
17
122
  var ATTACHMENT_KINDS = ["image", "paste", "pick"];
18
123
  var ATTACHMENT_LIMITS = { image: 6, paste: 4, pick: 8 };
@@ -40,12 +145,17 @@ var CONFIG_DIR = ".toyon";
40
145
  function isLocalConfigFile(rel) {
41
146
  return rel === CONFIG_FILES.folder.local || rel === CONFIG_FILES.root.local;
42
147
  }
148
+ function configSibling(rel, kind) {
149
+ const place = Object.values(CONFIG_FILES).find((p) => p.shared === rel || p.local === rel) ?? CONFIG_FILES.folder;
150
+ return place[kind];
151
+ }
43
152
 
44
153
  // ../shared/src/daemon.ts
45
154
  var DAEMON_FILES = {
46
155
  token: "token",
47
156
  pid: "daemon.pid",
48
157
  state: "state.json",
158
+ drafts: "drafts.json",
49
159
  log: "daemon.log",
50
160
  remote: "remote.json"
51
161
  };
@@ -159,6 +269,11 @@ function middle(a, b) {
159
269
  return ops;
160
270
  }
161
271
 
272
+ // ../shared/src/drafts.ts
273
+ var DRAFT_PREFIX = "draft:";
274
+ var draftKey = (repoId) => DRAFT_PREFIX + repoId;
275
+ var draftRepoOf = (boxId) => boxId.startsWith(DRAFT_PREFIX) ? boxId.slice(DRAFT_PREFIX.length) : null;
276
+
162
277
  // ../shared/src/land.ts
163
278
  var DEFAULT_LAND_ROUTE = "merge";
164
279
  var DEFAULT_MERGE_METHOD = "merge";
@@ -171,19 +286,8 @@ function landPolicy(config) {
171
286
  };
172
287
  }
173
288
 
174
- // ../shared/src/model.ts
175
- var DEFAULT_PERMISSION_MODE = "auto";
176
- function hasOwnBranch(wt) {
177
- return wt.branch.startsWith("toyon/");
178
- }
179
- var SHELL_STREAM = "shell";
180
- var LOGIN_STREAM = "login";
181
- function streamKey(worktreeId, stream) {
182
- return `${worktreeId}/${stream}`;
183
- }
184
-
185
289
  // ../shared/src/protocol/limits.ts
186
- var PROTOCOL_VERSION = 43;
290
+ var PROTOCOL_VERSION = 56;
187
291
  var FILE_MAX_CHARS = 5000000;
188
292
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
189
293
  var IMAGE_MAX_EDGE = 2576;
@@ -1126,7 +1230,7 @@ var nanoid = /^[a-zA-Z0-9_-]{21}$/;
1126
1230
  function nanoidOfLength(length) {
1127
1231
  return new RegExp(`^[a-zA-Z0-9_-]{${length}}$`);
1128
1232
  }
1129
- var duration = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
1233
+ var duration2 = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
1130
1234
  var guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
1131
1235
  var uuid = (version) => {
1132
1236
  if (!version)
@@ -1919,7 +2023,7 @@ var $ZodISOTime = /* @__PURE__ */ $constructor("$ZodISOTime", (inst, def) => {
1919
2023
  $ZodStringFormat.init(inst, def);
1920
2024
  });
1921
2025
  var $ZodISODuration = /* @__PURE__ */ $constructor("$ZodISODuration", (inst, def) => {
1922
- def.pattern ?? (def.pattern = duration);
2026
+ def.pattern ?? (def.pattern = duration2);
1923
2027
  $ZodStringFormat.init(inst, def);
1924
2028
  });
1925
2029
  var $ZodIPv4 = /* @__PURE__ */ $constructor("$ZodIPv4", (inst, def) => {
@@ -6221,6 +6325,7 @@ var toyonConfigSchema = object({
6221
6325
  setup: array(shellCommand).max(50).optional(),
6222
6326
  run: record(declaredProcName, shellCommand),
6223
6327
  check: shellCommand.optional(),
6328
+ afterLand: array(shellCommand).max(50).optional(),
6224
6329
  land: landConfigSchema.optional(),
6225
6330
  preview: procName.optional(),
6226
6331
  profiles: record(procName, runProfileSchema).optional(),
@@ -6255,9 +6360,11 @@ var variantSchema = object({ group: string2(), index: number2().int().min(1), of
6255
6360
  var clientMsgSchema = discriminatedUnion("t", [
6256
6361
  object({ t: literal("subscribe"), worktreeId: id }),
6257
6362
  object({ t: literal("unsubscribe"), worktreeId: id }),
6363
+ object({ t: literal("view"), worktreeId: id.nullable() }),
6258
6364
  object({
6259
6365
  t: literal("chat"),
6260
6366
  worktreeId: id,
6367
+ clientId: string2().max(64).optional(),
6261
6368
  text: prose,
6262
6369
  context: prose.optional(),
6263
6370
  attachments
@@ -6267,7 +6374,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6267
6374
  clientId: string2().max(64).optional(),
6268
6375
  repoId: id,
6269
6376
  prompt,
6270
- baseWorktreeId: id.optional(),
6271
6377
  variant: variantSchema.optional(),
6272
6378
  context: prose.optional(),
6273
6379
  attachments,
@@ -6275,7 +6381,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6275
6381
  profile: string2().max(100).optional(),
6276
6382
  mode: permissionModeSchema.optional(),
6277
6383
  model: string2().max(200).optional(),
6278
- effort: string2().max(100).optional()
6384
+ effort: string2().max(100).optional(),
6385
+ carry: boolean2().optional()
6279
6386
  }),
6280
6387
  object({
6281
6388
  t: literal("batch-worktrees"),
@@ -6289,12 +6396,13 @@ var clientMsgSchema = discriminatedUnion("t", [
6289
6396
  object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
6290
6397
  object({ t: literal("set-worktree-effort"), worktreeId: id, effort: string2().max(100) }),
6291
6398
  object({ t: literal("set-worktree-profile"), worktreeId: id, profile: string2().max(100) }),
6292
- object({ t: literal("remove-worktree"), worktreeId: id }),
6399
+ object({ t: literal("archive-worktree"), worktreeId: id }),
6293
6400
  object({ t: literal("list-archived"), repoId: id }),
6294
6401
  object({
6295
6402
  t: literal("restore-worktree"),
6296
6403
  archiveId: id,
6297
- clientId: string2().max(64).optional()
6404
+ clientId: string2().max(64).optional(),
6405
+ message: object({ text: prose, attachments }).optional()
6298
6406
  }),
6299
6407
  object({ t: literal("delete-archived"), archiveId: id }),
6300
6408
  object({
@@ -6310,6 +6418,9 @@ var clientMsgSchema = discriminatedUnion("t", [
6310
6418
  object({ t: literal("commit"), worktreeId: id, message: string2().max(5000) }),
6311
6419
  object({ t: literal("graft"), targetId: id, sourceIds: array(id).min(1).max(20) }),
6312
6420
  object({ t: literal("sync-main"), worktreeId: id }),
6421
+ object({ t: literal("run-after-land"), repoId: id }),
6422
+ object({ t: literal("restart-daemon") }),
6423
+ object({ t: literal("update-now") }),
6313
6424
  object({ t: literal("pull-main"), worktreeId: id }),
6314
6425
  object({
6315
6426
  t: literal("write-file"),
@@ -6329,6 +6440,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6329
6440
  object({ t: literal("stop-agent"), worktreeId: id }),
6330
6441
  object({ t: literal("seen"), worktreeId: id }),
6331
6442
  object({ t: literal("mark-unread"), worktreeId: id }),
6443
+ object({ t: literal("set-draft"), boxId: id, text: prose, clientId: string2().max(64).optional() }),
6332
6444
  object({ t: literal("refresh-git"), repoId: id }),
6333
6445
  object({ t: literal("visit"), worktreeId: id, path: routePath, title: pageTitle.optional() }),
6334
6446
  object({ t: literal("page-title"), worktreeId: id, path: routePath, title: pageTitle }),
@@ -6337,7 +6449,12 @@ var clientMsgSchema = discriminatedUnion("t", [
6337
6449
  object({ t: literal("unqueue"), worktreeId: id, index: number2().int().min(0) }),
6338
6450
  object({ t: literal("changed-ranges"), worktreeId: id, path: relPath }),
6339
6451
  object({ t: literal("rename-worktree"), worktreeId: id, title: string2().min(1).max(200) }),
6340
- object({ t: literal("confirm-config"), repoId: id, config: toyonConfigSchema }),
6452
+ object({
6453
+ t: literal("confirm-config"),
6454
+ repoId: id,
6455
+ config: toyonConfigSchema,
6456
+ kind: _enum(["shared", "local"])
6457
+ }),
6341
6458
  object({ t: literal("register-repo"), path: string2().min(1).max(4000) }),
6342
6459
  object({
6343
6460
  t: literal("create-repo"),
@@ -6393,6 +6510,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6393
6510
  object({ t: literal("exec"), worktreeId: id, command: shellCommand.min(1) }),
6394
6511
  object({ t: literal("exec-stop"), worktreeId: id }),
6395
6512
  object({ t: literal("search-refs"), repoId: id, query: string2().max(200) }),
6513
+ object({ t: literal("search-chats"), repoId: id, query: string2().max(200) }),
6396
6514
  object({
6397
6515
  t: literal("open-ref"),
6398
6516
  clientId: string2().max(64).optional(),
@@ -7362,6 +7480,52 @@ function isDark(hex) {
7362
7480
  return luminance(hex) < 0.4;
7363
7481
  }
7364
7482
 
7483
+ // ../shared/src/update.ts
7484
+ function installMethod(packageJson) {
7485
+ if (!packageJson)
7486
+ return "none";
7487
+ const p = packageJson.replaceAll("\\", "/");
7488
+ if (p.includes("/_npx/"))
7489
+ return "npx";
7490
+ if (p.includes("/.bun/install/global/"))
7491
+ return "bun";
7492
+ if (p.includes("/node_modules/toyon/"))
7493
+ return "npm";
7494
+ return "none";
7495
+ }
7496
+ function installCommand(method, version) {
7497
+ if (method === "npm")
7498
+ return ["npm", "install", "-g", `toyon@${version}`];
7499
+ if (method === "bun")
7500
+ return ["bun", "add", "-g", `toyon@${version}`];
7501
+ return null;
7502
+ }
7503
+ function newer(a, b) {
7504
+ const pa = parseVersion(a);
7505
+ const pb = parseVersion(b);
7506
+ if (!pa || !pb)
7507
+ return false;
7508
+ for (let i = 0;i < 3; i++) {
7509
+ const x = pa.nums[i] ?? 0;
7510
+ const y = pb.nums[i] ?? 0;
7511
+ if (x !== y)
7512
+ return x > y;
7513
+ }
7514
+ if (pa.pre === pb.pre)
7515
+ return false;
7516
+ if (pa.pre === null)
7517
+ return true;
7518
+ if (pb.pre === null)
7519
+ return false;
7520
+ return pa.pre > pb.pre;
7521
+ }
7522
+ function parseVersion(v) {
7523
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v.trim());
7524
+ if (!m)
7525
+ return null;
7526
+ return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
7527
+ }
7528
+
7365
7529
  // ../shared/src/vscode-theme.ts
7366
7530
  var workbenchKeys = {
7367
7531
  surface0: ["editor.background"],
@@ -7538,27 +7702,10 @@ function pickSyntax(tokenColors) {
7538
7702
  function slug(s) {
7539
7703
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "theme";
7540
7704
  }
7541
-
7542
- // ../shared/src/worktree-caps.ts
7543
- function isMain(wt) {
7544
- return wt.kind === "main";
7545
- }
7546
- function canRemove(wt) {
7547
- return wt.kind === "worktree";
7548
- }
7549
- function canRename(wt) {
7550
- return wt.kind === "worktree" && hasOwnBranch(wt);
7551
- }
7552
- function canLand(wt) {
7553
- return wt.kind === "worktree" && wt.from?.kind !== "pr";
7554
- }
7555
- function canGraft(wt) {
7556
- return wt.kind === "worktree";
7557
- }
7558
7705
  // ../daemon/package.json
7559
7706
  var package_default = {
7560
7707
  name: "@toyon/daemon",
7561
- version: "0.1.0",
7708
+ version: "0.3.0",
7562
7709
  private: true,
7563
7710
  type: "module",
7564
7711
  main: "src/index.ts",
@@ -11885,6 +12032,7 @@ var EXT = {
11885
12032
  };
11886
12033
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
11887
12034
  var FILE = /^\d{1,6}\.(png|jpg|gif|webp|txt)$/;
12035
+ var isAttachmentFile = (file) => FILE.test(file);
11888
12036
  function attachmentsDirFor(attachmentsDir, worktreeId) {
11889
12037
  return join(attachmentsDir, worktreeId);
11890
12038
  }
@@ -12478,8 +12626,10 @@ function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon"))
12478
12626
  return {
12479
12627
  home,
12480
12628
  stateFile: join3(home, DAEMON_FILES.state),
12629
+ draftsFile: join3(home, DAEMON_FILES.drafts),
12481
12630
  tokenFile: join3(home, DAEMON_FILES.token),
12482
12631
  pidFile: join3(home, DAEMON_FILES.pid),
12632
+ logFile: join3(home, DAEMON_FILES.log),
12483
12633
  remoteFile: join3(home, DAEMON_FILES.remote),
12484
12634
  transcriptsDir: join3(home, "transcripts"),
12485
12635
  attachmentsDir: join3(home, "attachments"),
@@ -12506,11 +12656,17 @@ function ensureDirs(p) {
12506
12656
  // ../daemon/src/git/exclude.ts
12507
12657
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
12508
12658
  import { dirname } from "path";
12509
- async function excludeFromGit(cwd, pattern) {
12659
+ async function excludeFile(cwd) {
12510
12660
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
12511
- if (!r.ok || !r.out)
12512
- return log.warn("git", `cannot locate info/exclude for ${cwd}: ${r.err}`);
12513
- const file = r.out;
12661
+ if (r.ok && r.out)
12662
+ return r.out;
12663
+ log.warn("git", `cannot locate info/exclude for ${cwd}: ${r.err}`);
12664
+ return null;
12665
+ }
12666
+ async function excludeFromGit(cwd, pattern) {
12667
+ const file = await excludeFile(cwd);
12668
+ if (!file)
12669
+ return;
12514
12670
  const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
12515
12671
  if (current.split(`
12516
12672
  `).some((l) => l.trim() === pattern))
@@ -12521,6 +12677,18 @@ async function excludeFromGit(cwd, pattern) {
12521
12677
  ` : ""}${pattern}
12522
12678
  `);
12523
12679
  }
12680
+ async function unexcludeFromGit(cwd, pattern) {
12681
+ const file = await excludeFile(cwd);
12682
+ if (!file || !existsSync3(file))
12683
+ return;
12684
+ const lines = readFileSync3(file, "utf8").split(`
12685
+ `);
12686
+ const kept = lines.filter((l) => l.trim() !== pattern);
12687
+ if (kept.length === lines.length)
12688
+ return;
12689
+ writeFileSync(file, kept.join(`
12690
+ `));
12691
+ }
12524
12692
 
12525
12693
  // ../daemon/src/agent/bounds.ts
12526
12694
  import { existsSync as existsSync4, realpathSync } from "fs";
@@ -13013,7 +13181,7 @@ var SYSTEM_APPEND = [
13013
13181
  "Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
13014
13182
  "Keep the scope tight: do the asked task well, then stop. Suggest follow-ups in chat instead of expanding scope.",
13015
13183
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
13016
- `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 be merged, and land.route (optional: "merge", "push" or "pr") says how the user lands work on main. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed.`,
13184
+ `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.`,
13017
13185
  "When you scaffold a project, write its .gitignore (dependencies, build output, local env files) before installing anything, so an install never leaves thousands of untracked files for the user to wade through or commit.",
13018
13186
  "When you scaffold a project or change how it installs or starts, finish by updating the settings file Toyon already reads, or writing .toyon/settings.json when there is none, so the preview can run it."
13019
13187
  ].join(" ");
@@ -13050,10 +13218,11 @@ function buildPrompt(text, context, prefix, attachments = []) {
13050
13218
  }
13051
13219
  for (const a of attachments)
13052
13220
  blocks.push(...attachmentBlocks(a));
13053
- if (!leads)
13054
- blocks.push(textBlock(prefix ? `${prefix}
13221
+ const body = [prefix, text].filter(Boolean).join(`
13055
13222
 
13056
- ${text}` : text));
13223
+ `);
13224
+ if (!leads && body)
13225
+ blocks.push(textBlock(body));
13057
13226
  if (context && !leads)
13058
13227
  blocks.push(textBlock(context));
13059
13228
  return blocks;
@@ -13077,26 +13246,41 @@ ${a.text}
13077
13246
  }
13078
13247
 
13079
13248
  // ../daemon/src/agent/transcript.ts
13080
- import { existsSync as existsSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
13081
- import { appendFile, rename, writeFile as writeFile2 } from "fs/promises";
13249
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
13250
+ import { appendFile } from "fs/promises";
13082
13251
  import { join as join6 } from "path";
13083
- var MAX_ENTRIES = 4000;
13084
- var KEEP_ENTRIES = 2000;
13085
13252
  function transcriptPathFor(transcriptsDir, worktreeId) {
13086
13253
  return join6(transcriptsDir, `${worktreeId}.jsonl`);
13087
13254
  }
13088
- function cutPoint(entries) {
13089
- const from = Math.max(0, entries.length - KEEP_ENTRIES);
13090
- for (let i = from;i < entries.length; i++) {
13091
- const t = entries[i].event.type;
13092
- if (t === "user-message" || t === "turn-start")
13093
- return i;
13255
+ function coalesce(entries) {
13256
+ const out = [];
13257
+ for (const entry of entries) {
13258
+ const last = out.at(-1);
13259
+ const { event } = entry;
13260
+ if ((event.type === "text-delta" || event.type === "thinking-delta") && last?.event.type === event.type) {
13261
+ out[out.length - 1] = { seq: last.seq, event: { type: event.type, text: last.event.text + event.text } };
13262
+ } else {
13263
+ out.push(entry);
13264
+ }
13094
13265
  }
13095
- return from;
13266
+ return out;
13096
13267
  }
13097
- function serialize(entries) {
13098
- return entries.map((e) => `${JSON.stringify(e)}
13099
- `).join("");
13268
+ function parseTranscript(text, tag) {
13269
+ const out = [];
13270
+ let torn = 0;
13271
+ for (const line of text.split(`
13272
+ `)) {
13273
+ if (!line)
13274
+ continue;
13275
+ try {
13276
+ out.push(JSON.parse(line));
13277
+ } catch {
13278
+ torn++;
13279
+ }
13280
+ }
13281
+ if (torn)
13282
+ log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
13283
+ return out;
13100
13284
  }
13101
13285
 
13102
13286
  class Transcript {
@@ -13112,31 +13296,7 @@ class Transcript {
13112
13296
  this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
13113
13297
  }
13114
13298
  read() {
13115
- if (!existsSync7(this.path))
13116
- return [];
13117
- const out = [];
13118
- let torn = 0;
13119
- for (const line of readFileSync6(this.path, "utf8").split(`
13120
- `)) {
13121
- if (!line)
13122
- continue;
13123
- try {
13124
- out.push(JSON.parse(line));
13125
- } catch {
13126
- torn++;
13127
- }
13128
- }
13129
- if (torn)
13130
- log.warn(this.tag, `transcript: skipped ${torn} unparsable line(s)`);
13131
- if (out.length > MAX_ENTRIES) {
13132
- const kept = out.slice(cutPoint(out));
13133
- const tmp = `${this.path}.tmp`;
13134
- writeFileSync4(tmp, serialize(kept));
13135
- renameSync2(tmp, this.path);
13136
- log.info(this.tag, `transcript: compacted ${out.length} entries to ${kept.length}`);
13137
- return kept;
13138
- }
13139
- return out;
13299
+ return existsSync7(this.path) ? parseTranscript(readFileSync6(this.path, "utf8"), this.tag) : [];
13140
13300
  }
13141
13301
  append(event) {
13142
13302
  const entry = { seq: this.seq++, event };
@@ -13144,19 +13304,8 @@ class Transcript {
13144
13304
  const line = `${JSON.stringify(entry)}
13145
13305
  `;
13146
13306
  this.writes = this.writes.then(() => appendFile(this.path, line)).catch((e) => log.warn(this.tag, "transcript append failed", e));
13147
- if (this.entries.length > MAX_ENTRIES)
13148
- this.compact();
13149
13307
  return entry;
13150
13308
  }
13151
- compact() {
13152
- this.entries.splice(0, cutPoint(this.entries));
13153
- const snapshot = serialize(this.entries);
13154
- const tmp = `${this.path}.tmp`;
13155
- this.writes = this.writes.then(async () => {
13156
- await writeFile2(tmp, snapshot);
13157
- await rename(tmp, this.path);
13158
- }).catch((e) => log.warn(this.tag, "transcript compaction failed", e));
13159
- }
13160
13309
  flush() {
13161
13310
  return this.writes;
13162
13311
  }
@@ -13692,6 +13841,9 @@ class AcpSession {
13692
13841
  get queueLength() {
13693
13842
  return this.queue.length;
13694
13843
  }
13844
+ get unsettled() {
13845
+ return this.queue.length > 0 || this.steered.length > 0 || this.refused !== null || this.asks.size > 0;
13846
+ }
13695
13847
  get queueItems() {
13696
13848
  return this.waiting().map((q) => q.text);
13697
13849
  }
@@ -13929,7 +14081,7 @@ class AcpSession {
13929
14081
  describe(e) {
13930
14082
  if (isAuthRequired(e))
13931
14083
  return this.conn?.spec.loginHint ?? this.d.spec().loginHint;
13932
- const message = e instanceof Error ? e.message : String(e);
14084
+ const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
13933
14085
  return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
13934
14086
  }
13935
14087
  async record({ text, attachments }) {
@@ -14214,6 +14366,8 @@ class AcpSession {
14214
14366
  }
14215
14367
  askPermission(params) {
14216
14368
  const plan = params.toolCall.kind === "switch_mode";
14369
+ if (plan)
14370
+ this.d.onPlan?.();
14217
14371
  const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
14218
14372
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
14219
14373
  return this.openAsk((id) => ({
@@ -14611,24 +14765,31 @@ function setsGrant(line) {
14611
14765
  // ../daemon/src/runtime/proxy.ts
14612
14766
  var DIAL_TIMEOUT_MS = 5000;
14613
14767
  var frame = (ev) => typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
14768
+ var VITE_PING = "text/x-vite-ping";
14614
14769
  function previewHandler(opts) {
14615
14770
  const hostPart = (t) => t.host.includes(":") ? `[${t.host}]` : t.host;
14616
14771
  return {
14617
14772
  async fetch(req, upgrade) {
14618
14773
  const url = new URL(req.url);
14619
- const target = opts.getTarget();
14620
14774
  if (url.pathname === "/__toyon/bridge.js") {
14621
14775
  return new Response(opts.bridgeScript(), {
14622
14776
  headers: { "content-type": "text/javascript", "cache-control": "no-store" }
14623
14777
  });
14624
14778
  }
14779
+ const isUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
14780
+ const machine = isUpgrade || req.headers.get("accept") === VITE_PING;
14781
+ if (!machine)
14782
+ opts.onRequest?.();
14783
+ let target = opts.getTarget();
14784
+ if (target == null && !machine && opts.ready)
14785
+ target = await opts.ready();
14625
14786
  if (target == null) {
14626
14787
  return new Response(waitingPage(), {
14627
14788
  status: 503,
14628
14789
  headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
14629
14790
  });
14630
14791
  }
14631
- if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
14792
+ if (isUpgrade) {
14632
14793
  const proto = req.headers.get("sec-websocket-protocol") ?? undefined;
14633
14794
  const upstreamUrl = `ws://${hostPart(target)}:${target.port}${url.pathname}${url.search}`;
14634
14795
  const pending = [];
@@ -14982,6 +15143,8 @@ async function reachableHost(port) {
14982
15143
  var LOG_RING_SIZE = 500;
14983
15144
  var POLL_INTERVAL_MS = 500;
14984
15145
  var POLL_ATTEMPTS = 120;
15146
+ var FAST_POLL_MS = 100;
15147
+ var FAST_POLL_WINDOW_MS = 3000;
14985
15148
  var PROC_RING = 128 * 1024;
14986
15149
  var PROC_COLS = 120;
14987
15150
  var PROC_ROWS = 30;
@@ -14995,6 +15158,7 @@ class WorktreeProcs {
14995
15158
  opts;
14996
15159
  procs = new Map;
14997
15160
  stopped = false;
15161
+ asleep = false;
14998
15162
  lines = [];
14999
15163
  constructor(worktreePath, onProc, onLog, onData = () => {}, onStreamExit = () => {}, opts = {}) {
15000
15164
  this.worktreePath = worktreePath;
@@ -15041,7 +15205,7 @@ class WorktreeProcs {
15041
15205
  cols: mp.pty?.cols ?? PROC_COLS,
15042
15206
  rows: mp.pty?.rows ?? PROC_ROWS,
15043
15207
  file: "sh",
15044
- args: ["-c", command],
15208
+ args: ["-c", execForm(command)],
15045
15209
  ring: PROC_RING
15046
15210
  }, (data) => {
15047
15211
  this.onData(name, data);
@@ -15062,7 +15226,7 @@ class WorktreeProcs {
15062
15226
  const { name } = mp.state;
15063
15227
  mp.state.exitCode = code;
15064
15228
  this.onStreamExit(name, code);
15065
- if (this.stopped || mp.state.status === "stopped")
15229
+ if (this.stopped || mp.state.status === "stopped" || mp.state.status === "asleep")
15066
15230
  return;
15067
15231
  if (mp.handInput) {
15068
15232
  mp.state.status = "stopped";
@@ -15088,8 +15252,9 @@ class WorktreeProcs {
15088
15252
  }
15089
15253
  }
15090
15254
  async pollPort(mp) {
15091
- const attempts = this.opts.pollAttempts ?? POLL_ATTEMPTS;
15092
- for (let i = 0;i < attempts; i++) {
15255
+ const deadline = Date.now() + (this.opts.pollAttempts ?? POLL_ATTEMPTS) * POLL_INTERVAL_MS;
15256
+ const started = Date.now();
15257
+ while (Date.now() < deadline) {
15093
15258
  if (this.stopped || mp.state.status !== "starting")
15094
15259
  return;
15095
15260
  const host = await reachableHost(mp.state.port);
@@ -15099,7 +15264,7 @@ class WorktreeProcs {
15099
15264
  this.onProc({ ...mp.state });
15100
15265
  return;
15101
15266
  }
15102
- await Bun.sleep(POLL_INTERVAL_MS);
15267
+ await Bun.sleep(Date.now() - started < FAST_POLL_WINDOW_MS ? FAST_POLL_MS : POLL_INTERVAL_MS);
15103
15268
  }
15104
15269
  await this.diagnose(mp);
15105
15270
  }
@@ -15146,7 +15311,7 @@ class WorktreeProcs {
15146
15311
  }
15147
15312
  restart(name) {
15148
15313
  const mp = this.procs.get(name);
15149
- if (!mp)
15314
+ if (!mp || this.asleep)
15150
15315
  return;
15151
15316
  mp.restarts = 0;
15152
15317
  if (mp.state.status === "crashed") {
@@ -15162,6 +15327,35 @@ class WorktreeProcs {
15162
15327
  async killProc(mp) {
15163
15328
  await mp.pty?.kill();
15164
15329
  }
15330
+ async sleep() {
15331
+ if (this.asleep || this.stopped)
15332
+ return;
15333
+ this.asleep = true;
15334
+ const exits = [];
15335
+ for (const mp of this.procs.values()) {
15336
+ mp.state.status = "asleep";
15337
+ mp.state.pid = undefined;
15338
+ this.onProc({ ...mp.state });
15339
+ exits.push(this.killProc(mp));
15340
+ }
15341
+ await Promise.all(exits);
15342
+ }
15343
+ wake() {
15344
+ if (!this.asleep || this.stopped)
15345
+ return;
15346
+ this.asleep = false;
15347
+ for (const mp of this.procs.values()) {
15348
+ mp.restarts = 0;
15349
+ this.spawnProc(mp);
15350
+ }
15351
+ }
15352
+ pgids() {
15353
+ const out = [];
15354
+ for (const mp of this.procs.values())
15355
+ if (mp.pty?.alive)
15356
+ out.push(mp.pty.pid);
15357
+ return out;
15358
+ }
15165
15359
  async stopAll() {
15166
15360
  this.stopped = true;
15167
15361
  const exits = [];
@@ -15185,6 +15379,12 @@ class WorktreeProcs {
15185
15379
  this.onLog(proc, line);
15186
15380
  }
15187
15381
  }
15382
+ function execForm(command) {
15383
+ const trimmed = command.trim();
15384
+ if (/[;&|<>()`\n]|\$\(/.test(trimmed) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed))
15385
+ return command;
15386
+ return `exec ${trimmed}`;
15387
+ }
15188
15388
  function envStrings(base) {
15189
15389
  const env = {};
15190
15390
  for (const [k, v] of Object.entries(base))
@@ -15222,6 +15422,8 @@ function terminalEnv(base, own, urls) {
15222
15422
  return { ...env, ...urls, TERM: "xterm-256color", COLORTERM: "truecolor", ...own };
15223
15423
  }
15224
15424
  var DEFAULT_AGENT_ID = "claude";
15425
+ var PREVIEW_WAKE_MS = 8000;
15426
+ var PREVIEW_POLL_MS = 100;
15225
15427
  function defaultAgent(wt, d) {
15226
15428
  const agent = new AcpSession({
15227
15429
  worktreeId: wt.id,
@@ -15255,6 +15457,13 @@ function defaultAgent(wt, d) {
15255
15457
  d.state.save();
15256
15458
  d.hub.emit("worktreesChanged");
15257
15459
  },
15460
+ onPlan: () => {
15461
+ const w = d.state.worktree(wt.id);
15462
+ if (!w || w.planned)
15463
+ return;
15464
+ w.planned = true;
15465
+ d.state.save();
15466
+ },
15258
15467
  option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
15259
15468
  onOptionsLearned: (category, choices) => {
15260
15469
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
@@ -15272,19 +15481,24 @@ function defaultProcs(wt, d) {
15272
15481
  function defaultTerminal(_wt, opts, onData, onExit) {
15273
15482
  return new PtyStream(opts, onData, onExit);
15274
15483
  }
15275
- function defaultProxy(wt, previewName, procs, d) {
15484
+ function defaultProxy(wt, previewName, procs, d, wake) {
15276
15485
  return startProxy({
15277
15486
  port: wt.proxyPort,
15278
15487
  hostname: cloud.bindHost,
15279
15488
  remote: d.remote ?? null,
15280
15489
  grant: d.grant ?? "",
15281
15490
  bridgeScript: d.bridgeScript,
15282
- getTarget: () => previewTargetOf(procs, previewName)
15491
+ getTarget: () => previewTargetOf(procs, previewName),
15492
+ onRequest: wake.onRequest,
15493
+ ready: wake.ready
15283
15494
  });
15284
15495
  }
15496
+ function previewProcOf(procs, previewName) {
15497
+ return procs.states().find((p) => p.name === previewName) ?? procs.states()[0];
15498
+ }
15285
15499
  function previewTargetOf(procs, previewName) {
15286
- const st = procs.states().find((p) => p.name === previewName) ?? procs.states()[0];
15287
- if (!st || st.status === "crashed" || st.status === "unreachable")
15500
+ const st = previewProcOf(procs, previewName);
15501
+ if (st?.status !== "running")
15288
15502
  return null;
15289
15503
  return { port: st.boundPort ?? st.port, host: st.host ?? "127.0.0.1" };
15290
15504
  }
@@ -15293,9 +15507,118 @@ class RuntimeRegistry {
15293
15507
  deps;
15294
15508
  runtimes = new Map;
15295
15509
  looseShells = new Map;
15510
+ holds = new Map;
15511
+ settingUp = new Set;
15512
+ starting = new Set;
15296
15513
  constructor(deps) {
15297
15514
  this.deps = deps;
15298
15515
  }
15516
+ hold(id, tag) {
15517
+ let tags = this.holds.get(id);
15518
+ if (!tags) {
15519
+ tags = new Set;
15520
+ this.holds.set(id, tags);
15521
+ }
15522
+ tags.add(tag);
15523
+ this.deps.hub.emit("holdsChanged", id, tags.size);
15524
+ }
15525
+ release(id, tag) {
15526
+ const tags = this.holds.get(id);
15527
+ if (!tags?.delete(tag))
15528
+ return;
15529
+ if (tags.size === 0)
15530
+ this.holds.delete(id);
15531
+ this.deps.hub.emit("holdsChanged", id, tags.size);
15532
+ }
15533
+ holdCount(id) {
15534
+ return this.holds.get(id)?.size ?? 0;
15535
+ }
15536
+ busy(id) {
15537
+ const agent = this.agentFor(id);
15538
+ if (agent && (agent.status !== "idle" || agent.unsettled))
15539
+ return true;
15540
+ return this.holdCount(id) > 0;
15541
+ }
15542
+ anyBusy() {
15543
+ for (const id of this.runtimes.keys())
15544
+ if (this.busy(id))
15545
+ return true;
15546
+ for (const id of this.holds.keys())
15547
+ if (this.busy(id))
15548
+ return true;
15549
+ return false;
15550
+ }
15551
+ markSetup(id, on) {
15552
+ if (on)
15553
+ this.settingUp.add(id);
15554
+ else
15555
+ this.settingUp.delete(id);
15556
+ }
15557
+ async wake(id) {
15558
+ const wt = this.deps.state.worktree(id);
15559
+ if (!wt)
15560
+ return;
15561
+ const rt = this.runtimes.get(id);
15562
+ if (rt?.procs) {
15563
+ if (!rt.procs.asleep)
15564
+ return;
15565
+ rt.procs.wake();
15566
+ log.info(id, "awake");
15567
+ this.deps.hub.emit("worktreesChanged");
15568
+ return;
15569
+ }
15570
+ if (this.settingUp.has(id))
15571
+ return;
15572
+ const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
15573
+ if (repo)
15574
+ await this.start(wt, repo);
15575
+ }
15576
+ async sleep(id) {
15577
+ const rt = this.runtimes.get(id);
15578
+ if (!rt?.procs || rt.procs.asleep)
15579
+ return;
15580
+ await rt.procs.sleep();
15581
+ this.deps.hub.emit("worktreesChanged");
15582
+ }
15583
+ async awaitPreview(id, ms) {
15584
+ const deadline = Date.now() + ms;
15585
+ for (;; ) {
15586
+ const rt = this.runtimes.get(id);
15587
+ if (!rt?.procs)
15588
+ return null;
15589
+ const st = previewProcOf(rt.procs, rt.previewName);
15590
+ if (st?.status === "running")
15591
+ return previewTargetOf(rt.procs, rt.previewName);
15592
+ const coming = st ? st.status === "starting" : this.starting.has(id);
15593
+ if (!coming || Date.now() >= deadline)
15594
+ return null;
15595
+ await Bun.sleep(PREVIEW_POLL_MS);
15596
+ }
15597
+ }
15598
+ tiers() {
15599
+ let awake = 0;
15600
+ let asleep = 0;
15601
+ for (const rt of this.runtimes.values()) {
15602
+ if (!rt.procs)
15603
+ continue;
15604
+ if (rt.procs.asleep)
15605
+ asleep++;
15606
+ else
15607
+ awake++;
15608
+ }
15609
+ return { awake, asleep };
15610
+ }
15611
+ awake() {
15612
+ const out = [];
15613
+ for (const [id, rt] of this.runtimes) {
15614
+ if (rt.procs && !rt.procs.asleep)
15615
+ out.push({ id, pgids: rt.procs.pgids() });
15616
+ }
15617
+ return out;
15618
+ }
15619
+ isAsleep(id) {
15620
+ return this.runtimes.get(id)?.procs?.asleep ?? false;
15621
+ }
15299
15622
  openLooseShell(id, cwd, cols, rows) {
15300
15623
  let term = this.looseShells.get(id);
15301
15624
  if (!term?.alive) {
@@ -15380,19 +15703,30 @@ class RuntimeRegistry {
15380
15703
  const urls = { ...procUrlEnv(procs.states(), previewName), ...worktreeEnv(wt, repo) };
15381
15704
  return { ...urls, ...expandEnv(run.env, urls) };
15382
15705
  };
15383
- for (const [name, cmd] of Object.entries(run.procs)) {
15384
- if (name !== previewName)
15385
- await procs.start(name, cmd, envFor());
15386
- }
15387
- if (previewName && run.procs[previewName]) {
15388
- await procs.start(previewName, run.procs[previewName], envFor());
15706
+ this.starting.add(wt.id);
15707
+ try {
15708
+ for (const [name, cmd] of Object.entries(run.procs)) {
15709
+ if (name !== previewName)
15710
+ await procs.start(name, cmd, envFor());
15711
+ }
15712
+ if (previewName && run.procs[previewName]) {
15713
+ await procs.start(previewName, run.procs[previewName], envFor());
15714
+ }
15715
+ } finally {
15716
+ this.starting.delete(wt.id);
15389
15717
  }
15390
15718
  if (!this.deps.state.worktree(wt.id) || this.runtimes.get(wt.id) !== rt) {
15391
15719
  this.returnLease(wt.id);
15392
15720
  await procs.stopAll();
15393
15721
  return;
15394
15722
  }
15395
- rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps);
15723
+ rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps, {
15724
+ onRequest: () => {
15725
+ this.deps.hub.emit("previewRequest", wt.id);
15726
+ fireAndForget(wt.id, this.wake(wt.id), "wake on request");
15727
+ },
15728
+ ready: () => this.awaitPreview(wt.id, PREVIEW_WAKE_MS)
15729
+ });
15396
15730
  this.deps.hub.emit("worktreesChanged");
15397
15731
  }
15398
15732
  leased = new Map;
@@ -15415,6 +15749,8 @@ class RuntimeRegistry {
15415
15749
  await procs?.stopAll();
15416
15750
  }
15417
15751
  async stop(id) {
15752
+ this.holds.delete(id);
15753
+ this.settingUp.delete(id);
15418
15754
  const rt = this.runtimes.get(id);
15419
15755
  if (!rt)
15420
15756
  return;
@@ -15537,6 +15873,8 @@ class RuntimeRegistry {
15537
15873
  this.startLogin(id, rt.login.run);
15538
15874
  } else if (stream === SHELL_STREAM)
15539
15875
  await rt?.shell?.kill();
15876
+ else if (rt?.procs?.asleep)
15877
+ await this.wake(id);
15540
15878
  else
15541
15879
  rt?.procs?.restart(stream);
15542
15880
  }
@@ -15545,11 +15883,7 @@ class RuntimeRegistry {
15545
15883
  }
15546
15884
  previewTarget(id) {
15547
15885
  const rt = this.runtimes.get(id);
15548
- if (!rt?.procs)
15549
- return null;
15550
- const t = previewTargetOf(rt.procs, rt.previewName);
15551
- const st = rt.procs.states().find((p) => p.port === t?.port);
15552
- return st?.status === "running" ? t : null;
15886
+ return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
15553
15887
  }
15554
15888
  async shutdown() {
15555
15889
  this.pruneLooseShells(new Set);
@@ -15859,6 +16193,20 @@ var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested nam
15859
16193
  var namePrompt = (task) => `Name this coding task in 2 to 4 lowercase kebab-case words (like "sticky-header" or "dark-mode-toggle"). Reply with ONLY the name, nothing else.
15860
16194
 
15861
16195
  Task: ${task.slice(0, 500)}`;
16196
+ function taskText(text, attachments = []) {
16197
+ if (text.trim())
16198
+ return text;
16199
+ return attachments.map(attachmentText).join(`
16200
+
16201
+ `);
16202
+ }
16203
+ function attachmentText(a) {
16204
+ if (a.kind === "paste")
16205
+ return a.text;
16206
+ if (a.kind === "image")
16207
+ return a.name ? `image ${a.name}` : "an image";
16208
+ return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
16209
+ }
15862
16210
  function parseName(text) {
15863
16211
  if (!text)
15864
16212
  return null;
@@ -15932,15 +16280,47 @@ function makePlanner(agents) {
15932
16280
  }
15933
16281
 
15934
16282
  // ../daemon/src/core/assets.ts
15935
- import { existsSync as existsSync9 } from "fs";
15936
- import { join as join7 } from "path";
16283
+ import { existsSync as existsSync9, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
16284
+ import { join as join7, resolve as resolve5 } from "path";
15937
16285
  function locateAssets(here, env = process.env) {
15938
16286
  const packaged = existsSync9(join7(here, "shell", "index.html"));
15939
16287
  return {
15940
16288
  shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join7(here, "shell") : join7(here, "../../shell/dist")),
15941
- bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js"))
16289
+ bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js")),
16290
+ sourceRoot: packaged ? null : resolve5(here, "../../.."),
16291
+ packageJson: packaged ? join7(here, "..", "package.json") : null
15942
16292
  };
15943
16293
  }
16294
+ var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
16295
+ function pruneAssets(shellDist) {
16296
+ const dir = join7(shellDist, "assets");
16297
+ let names;
16298
+ try {
16299
+ names = readdirSync2(dir);
16300
+ } catch {
16301
+ return 0;
16302
+ }
16303
+ const found = [];
16304
+ for (const name of names) {
16305
+ const path = join7(dir, name);
16306
+ try {
16307
+ found.push({ path, mtime: statSync2(path).mtimeMs });
16308
+ } catch {}
16309
+ }
16310
+ if (found.length === 0)
16311
+ return 0;
16312
+ const newest = Math.max(...found.map((f) => f.mtime));
16313
+ let gone = 0;
16314
+ for (const f of found) {
16315
+ if (newest - f.mtime <= KEEP_MS)
16316
+ continue;
16317
+ try {
16318
+ rmSync(f.path, { recursive: true, force: true });
16319
+ gone++;
16320
+ } catch {}
16321
+ }
16322
+ return gone;
16323
+ }
15944
16324
 
15945
16325
  // ../daemon/src/core/dialog.ts
15946
16326
  import { existsSync as existsSync10 } from "fs";
@@ -16030,38 +16410,181 @@ class Hub {
16030
16410
  }
16031
16411
  }
16032
16412
 
16033
- // ../daemon/src/core/state.ts
16034
- import { randomBytes } from "crypto";
16035
- import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
16036
- var SEEN_WORKTREES = 50;
16037
- var empty = { repos: [], worktrees: [], sessions: {} };
16038
- function loadState(paths) {
16039
- ensureDirs(paths);
16040
- const STATE_FILE = paths.stateFile;
16041
- if (!existsSync11(STATE_FILE))
16042
- return structuredClone(empty);
16043
- let raw;
16044
- try {
16045
- raw = readFileSync8(STATE_FILE, "utf8");
16046
- } catch (e) {
16047
- log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
16048
- return structuredClone(empty);
16049
- }
16050
- let state;
16051
- try {
16052
- state = { ...structuredClone(empty), ...JSON.parse(raw) };
16053
- } catch (e) {
16054
- const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
16055
- try {
16056
- writeFileSync5(backup, raw);
16057
- } catch (be) {
16058
- log.warn("state", "could not write the corrupt-state backup", be);
16059
- }
16060
- log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
16061
- return structuredClone(empty);
16413
+ // ../daemon/src/core/restart.ts
16414
+ import { spawn as spawn2 } from "child_process";
16415
+ import { openSync } from "fs";
16416
+ function restartable(env = process.env) {
16417
+ if (env.TOYON_WORKTREE !== undefined) {
16418
+ return { ok: false, reason: "this daemon runs as a toyon process; restart it from its terminal tab" };
16062
16419
  }
16063
- const ids = new Set(state.worktrees.map((w) => w.id));
16064
- for (const id of Object.keys(state.sessions))
16420
+ return { ok: true };
16421
+ }
16422
+ function respawn(logFile) {
16423
+ const logFd = openSync(logFile, "a");
16424
+ const child = spawn2(process.execPath, process.argv.slice(1), {
16425
+ detached: true,
16426
+ stdio: ["ignore", logFd, logFd],
16427
+ env: { ...process.env }
16428
+ });
16429
+ child.on("error", (e) => log.warn("daemon", `the replacement did not start: ${e.message}`));
16430
+ child.unref();
16431
+ }
16432
+
16433
+ // ../daemon/src/core/restarter.ts
16434
+ class Restarter {
16435
+ d;
16436
+ asked = false;
16437
+ gone = false;
16438
+ announced = null;
16439
+ constructor(d) {
16440
+ this.d = d;
16441
+ const settle = () => {
16442
+ if (this.asked)
16443
+ this.attempt();
16444
+ };
16445
+ d.hub.on("agentStatus", settle);
16446
+ d.hub.on("worktreesChanged", settle);
16447
+ }
16448
+ request() {
16449
+ const refused = this.d.refusal();
16450
+ if (refused)
16451
+ return refused;
16452
+ this.asked = true;
16453
+ this.attempt();
16454
+ return null;
16455
+ }
16456
+ waitingOn() {
16457
+ if (!this.asked)
16458
+ return null;
16459
+ return this.gone ? [] : this.working();
16460
+ }
16461
+ working() {
16462
+ return this.d.state.worktrees.filter((w) => this.d.runtime.agentFor(w.id)?.status === "working").map((w) => w.title);
16463
+ }
16464
+ attempt() {
16465
+ if (this.gone)
16466
+ return;
16467
+ const names = this.working();
16468
+ if (names.length === 0)
16469
+ this.gone = true;
16470
+ const key = JSON.stringify(this.waitingOn());
16471
+ if (key !== this.announced) {
16472
+ this.announced = key;
16473
+ this.d.hub.emit("updateChanged");
16474
+ }
16475
+ if (this.gone)
16476
+ this.d.go();
16477
+ }
16478
+ }
16479
+
16480
+ // ../daemon/src/core/self.ts
16481
+ function needsRestart(path) {
16482
+ return path.startsWith("packages/daemon/") || path.startsWith("packages/shared/") || path === "package.json" || path === "bun.lock";
16483
+ }
16484
+ function needsRebuild(path) {
16485
+ return path.startsWith("packages/shell/") || path.startsWith("packages/bridge/") || path.startsWith("packages/shared/");
16486
+ }
16487
+ function classify(paths) {
16488
+ let rebuild = false;
16489
+ let restart = false;
16490
+ for (const path of paths) {
16491
+ if (needsRebuild(path))
16492
+ rebuild = true;
16493
+ if (needsRestart(path))
16494
+ restart = true;
16495
+ }
16496
+ return { rebuild, restart };
16497
+ }
16498
+
16499
+ class SelfWatch {
16500
+ root;
16501
+ booted = null;
16502
+ state = null;
16503
+ constructor(root) {
16504
+ this.root = root;
16505
+ }
16506
+ async start() {
16507
+ if (!this.root)
16508
+ return;
16509
+ const head = await git(this.root, "rev-parse", "HEAD");
16510
+ if (!head.ok) {
16511
+ log.debug("self", `no HEAD in ${this.root}; not watching it`);
16512
+ return;
16513
+ }
16514
+ this.booted = head.out.trim();
16515
+ }
16516
+ own(repo) {
16517
+ return this.root !== null && repo.path === this.root;
16518
+ }
16519
+ async check(repo) {
16520
+ if (this.booted === null || !this.own(repo))
16521
+ return false;
16522
+ const names = await git(this.root ?? "", "diff", "--name-only", `${this.booted}..${repo.defaultBranch}`);
16523
+ if (!names.ok) {
16524
+ log.debug("self", `could not diff ${this.booted}..${repo.defaultBranch}: ${names.err}`);
16525
+ return false;
16526
+ }
16527
+ const paths = names.out.split(`
16528
+ `).filter((l) => l !== "");
16529
+ const { rebuild, restart } = classify(paths);
16530
+ return this.set({ repoId: repo.id, rebuild, restart, building: this.state?.building ?? false });
16531
+ }
16532
+ building(on, failure) {
16533
+ if (!this.state)
16534
+ return false;
16535
+ const done = !on && failure === undefined;
16536
+ return this.set({
16537
+ ...this.state,
16538
+ building: on,
16539
+ rebuild: done ? false : this.state.rebuild,
16540
+ buildFailed: on ? undefined : failure
16541
+ });
16542
+ }
16543
+ get() {
16544
+ return this.state;
16545
+ }
16546
+ set(next) {
16547
+ const quiet = !next.rebuild && !next.restart && !next.building && next.buildFailed === undefined;
16548
+ const value = quiet ? null : next;
16549
+ if (JSON.stringify(value) === JSON.stringify(this.state))
16550
+ return false;
16551
+ this.state = value;
16552
+ return true;
16553
+ }
16554
+ }
16555
+
16556
+ // ../daemon/src/core/state.ts
16557
+ import { randomBytes } from "crypto";
16558
+ import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
16559
+ var SEEN_WORKTREES = 50;
16560
+ var empty = { repos: [], worktrees: [], sessions: {} };
16561
+ function loadState(paths) {
16562
+ ensureDirs(paths);
16563
+ const STATE_FILE = paths.stateFile;
16564
+ if (!existsSync11(STATE_FILE))
16565
+ return structuredClone(empty);
16566
+ let raw;
16567
+ try {
16568
+ raw = readFileSync8(STATE_FILE, "utf8");
16569
+ } catch (e) {
16570
+ log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
16571
+ return structuredClone(empty);
16572
+ }
16573
+ let state;
16574
+ try {
16575
+ state = { ...structuredClone(empty), ...JSON.parse(raw) };
16576
+ } catch (e) {
16577
+ const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
16578
+ try {
16579
+ writeFileSync4(backup, raw);
16580
+ } catch (be) {
16581
+ log.warn("state", "could not write the corrupt-state backup", be);
16582
+ }
16583
+ log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
16584
+ return structuredClone(empty);
16585
+ }
16586
+ const ids = new Set(state.worktrees.map((w) => w.id));
16587
+ for (const id of Object.keys(state.sessions))
16065
16588
  if (!ids.has(id))
16066
16589
  delete state.sessions[id];
16067
16590
  for (const id of Object.keys(state.seen ?? {})) {
@@ -16080,8 +16603,8 @@ function loadState(paths) {
16080
16603
  }
16081
16604
  function saveState(paths, state) {
16082
16605
  const tmp = `${paths.stateFile}.tmp`;
16083
- writeFileSync5(tmp, JSON.stringify(state, null, 2));
16084
- renameSync3(tmp, paths.stateFile);
16606
+ writeFileSync4(tmp, JSON.stringify(state, null, 2));
16607
+ renameSync2(tmp, paths.stateFile);
16085
16608
  }
16086
16609
  function loadOrCreateToken(paths) {
16087
16610
  ensureDirs(paths);
@@ -16089,7 +16612,7 @@ function loadOrCreateToken(paths) {
16089
16612
  if (existsSync11(TOKEN_FILE))
16090
16613
  return readFileSync8(TOKEN_FILE, "utf8").trim();
16091
16614
  const token = randomBytes(32).toString("hex");
16092
- writeFileSync5(TOKEN_FILE, token, { mode: 384 });
16615
+ writeFileSync4(TOKEN_FILE, token, { mode: 384 });
16093
16616
  return token;
16094
16617
  }
16095
16618
 
@@ -16221,6 +16744,13 @@ class StateStore {
16221
16744
  this.state.theme = prefs;
16222
16745
  this.save();
16223
16746
  }
16747
+ get updateFailed() {
16748
+ return this.state.updateFailed;
16749
+ }
16750
+ setUpdateFailed(failed) {
16751
+ this.state.updateFailed = failed;
16752
+ this.save();
16753
+ }
16224
16754
  save() {
16225
16755
  saveState(this.paths, this.state);
16226
16756
  }
@@ -16798,6 +17328,87 @@ async function loadProjectTypescript(root) {
16798
17328
  }
16799
17329
  }
16800
17330
 
17331
+ // ../daemon/src/drafts/store.ts
17332
+ import { existsSync as existsSync12, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
17333
+ var SAVE_DELAY_MS = 1000;
17334
+
17335
+ class DraftStore {
17336
+ d;
17337
+ drafts = {};
17338
+ saveTimer = null;
17339
+ constructor(d) {
17340
+ this.d = d;
17341
+ if (!existsSync12(d.file))
17342
+ return;
17343
+ try {
17344
+ const raw = JSON.parse(readFileSync9(d.file, "utf8"));
17345
+ if (!raw || typeof raw !== "object")
17346
+ return;
17347
+ for (const [id, text] of Object.entries(raw)) {
17348
+ if (typeof text === "string" && text)
17349
+ this.drafts[id] = text;
17350
+ }
17351
+ } catch (e) {
17352
+ log.warn("drafts", `skipped an unreadable ${d.file}`, e);
17353
+ }
17354
+ }
17355
+ all() {
17356
+ return { ...this.drafts };
17357
+ }
17358
+ has(boxId) {
17359
+ return !!this.drafts[boxId]?.trim();
17360
+ }
17361
+ set(boxId, text, clientId) {
17362
+ if ((this.drafts[boxId] ?? "") === text)
17363
+ return;
17364
+ if (text)
17365
+ this.drafts[boxId] = text;
17366
+ else
17367
+ delete this.drafts[boxId];
17368
+ this.scheduleSave();
17369
+ this.d.hub.emit("draftChanged", boxId, text, clientId);
17370
+ }
17371
+ drop(boxId) {
17372
+ this.set(boxId, "");
17373
+ }
17374
+ prune(known) {
17375
+ let changed = false;
17376
+ for (const id of Object.keys(this.drafts)) {
17377
+ if (known(id))
17378
+ continue;
17379
+ delete this.drafts[id];
17380
+ changed = true;
17381
+ }
17382
+ if (changed)
17383
+ this.scheduleSave();
17384
+ }
17385
+ flush() {
17386
+ if (!this.saveTimer)
17387
+ return;
17388
+ clearTimeout(this.saveTimer);
17389
+ this.saveTimer = null;
17390
+ this.write();
17391
+ }
17392
+ scheduleSave() {
17393
+ if (this.saveTimer)
17394
+ return;
17395
+ this.saveTimer = setTimeout(() => {
17396
+ this.saveTimer = null;
17397
+ this.write();
17398
+ }, this.d.saveDelayMs ?? SAVE_DELAY_MS);
17399
+ this.saveTimer.unref?.();
17400
+ }
17401
+ write() {
17402
+ const tmp = `${this.d.file}.tmp`;
17403
+ try {
17404
+ writeFileSync5(tmp, JSON.stringify(this.drafts));
17405
+ renameSync3(tmp, this.d.file);
17406
+ } catch (e) {
17407
+ log.error("drafts", "could not write the drafts", e);
17408
+ }
17409
+ }
17410
+ }
17411
+
16801
17412
  // ../daemon/src/exec/service.ts
16802
17413
  var OUTPUT_CAP = 200000;
16803
17414
  var TIMEOUT_MS = 10 * 60000;
@@ -16841,6 +17452,7 @@ class ExecService {
16841
17452
  }
16842
17453
  const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
16843
17454
  this.track(worktreeId, toolId, { proc, timer });
17455
+ this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
16844
17456
  return this.collect(worktreeId, toolId, proc, agent);
16845
17457
  }
16846
17458
  stop(worktreeId) {
@@ -16906,6 +17518,7 @@ class ExecService {
16906
17518
  const r = this.untrack(worktreeId, toolId);
16907
17519
  if (r)
16908
17520
  clearTimeout(r.timer);
17521
+ this.deps.runtime.release(worktreeId, `exec:${toolId}`);
16909
17522
  }
16910
17523
  agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
16911
17524
  return { exit, text };
@@ -16931,7 +17544,7 @@ ${body}
16931
17544
  }
16932
17545
 
16933
17546
  // ../daemon/src/files/service.ts
16934
- import { spawn as spawn2 } from "child_process";
17547
+ import { spawn as spawn3 } from "child_process";
16935
17548
  import { stat, unlink } from "fs/promises";
16936
17549
 
16937
17550
  // ../daemon/src/git/lock.ts
@@ -17047,10 +17660,13 @@ async function committedFiles(worktreePath, defaultBr) {
17047
17660
  const base = await git(worktreePath, "merge-base", "HEAD", defaultBr);
17048
17661
  if (!base.ok || !base.out)
17049
17662
  return [];
17050
- const r = await git(worktreePath, "diff", "--name-status", base.out, "HEAD");
17663
+ return filesBetween(worktreePath, base.out, "HEAD");
17664
+ }
17665
+ async function filesBetween(cwd, from, to) {
17666
+ const r = await git(cwd, "diff", "--name-status", from, to);
17051
17667
  if (!r.ok || !r.out)
17052
17668
  return [];
17053
- const counts = await numstat(worktreePath, base.out, "HEAD");
17669
+ const counts = await numstat(cwd, from, to);
17054
17670
  return r.out.split(`
17055
17671
  `).filter(Boolean).map((line) => {
17056
17672
  const [status, ...rest] = line.split("\t");
@@ -17080,6 +17696,9 @@ async function treeEmpty(worktreePath) {
17080
17696
  const r = await git(worktreePath, "ls-tree", "--name-only", "HEAD");
17081
17697
  return !r.ok || r.out === "";
17082
17698
  }
17699
+ async function isTracked(worktreePath, rel) {
17700
+ return (await git(worktreePath, "ls-files", "--error-unmatch", "--", rel)).ok;
17701
+ }
17083
17702
  async function behindUpstream(worktreePath) {
17084
17703
  const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
17085
17704
  return r.ok ? Number(r.out) || 0 : null;
@@ -17100,10 +17719,15 @@ async function logCommits(worktreePath, defaultBr, limit = LOG_LIMIT) {
17100
17719
  git(worktreePath, "log", `-n${limit}`, FORMAT),
17101
17720
  aheadShas(worktreePath, defaultBr)
17102
17721
  ]);
17103
- if (!r.ok || !r.out)
17104
- return [];
17722
+ return r.ok && r.out ? parseLog(r.out, (sha) => ahead.has(sha)) : [];
17723
+ }
17724
+ async function logRange(cwd, from, to, limit = LOG_LIMIT) {
17725
+ const r = await git(cwd, "log", `-n${limit}`, FORMAT, `${from}..${to}`);
17726
+ return r.ok && r.out ? parseLog(r.out, () => true) : [];
17727
+ }
17728
+ function parseLog(out, ahead) {
17105
17729
  const commits = [];
17106
- for (const rec of r.out.split("\x1E")) {
17730
+ for (const rec of out.split("\x1E")) {
17107
17731
  const [sha, short, author, at, subject] = rec.trim().split("\x00");
17108
17732
  if (!sha || !short || subject === undefined)
17109
17733
  continue;
@@ -17113,7 +17737,7 @@ async function logCommits(worktreePath, defaultBr, limit = LOG_LIMIT) {
17113
17737
  subject,
17114
17738
  author: author ?? "",
17115
17739
  at: Number(at) * 1000 || 0,
17116
- ahead: ahead.has(sha)
17740
+ ahead: ahead(sha)
17117
17741
  });
17118
17742
  }
17119
17743
  return commits;
@@ -17149,12 +17773,12 @@ async function fileAtCommit(worktreePath, sha, file) {
17149
17773
  }
17150
17774
 
17151
17775
  // ../daemon/src/worktrees/paths.ts
17152
- import { resolve as resolve5 } from "path";
17776
+ import { resolve as resolve6 } from "path";
17153
17777
  function resolveInside(worktreePath, rel, opts = {}) {
17154
17778
  if (typeof rel !== "string" || rel.includes("\x00"))
17155
17779
  throw new UserError("invalid path");
17156
- const root = canonical(resolve5(worktreePath));
17157
- const target = canonical(resolve5(worktreePath, rel));
17780
+ const root = canonical(resolve6(worktreePath));
17781
+ const target = canonical(resolve6(worktreePath, rel));
17158
17782
  if (target === root) {
17159
17783
  if (opts.allowRoot)
17160
17784
  return target;
@@ -17294,6 +17918,30 @@ function rankElementSources(el, rows) {
17294
17918
  };
17295
17919
  }
17296
17920
 
17921
+ // ../daemon/src/files/listing.ts
17922
+ var SUBMODULE_MODE = "160000";
17923
+ var entries = (out) => out.split("\x00").filter(Boolean);
17924
+ function parseListing(staged, untracked, deleted) {
17925
+ const gone = new Set(entries(deleted));
17926
+ const paths = new Set;
17927
+ const submodules = new Set;
17928
+ for (const line of entries(staged)) {
17929
+ const tab = line.indexOf("\t");
17930
+ if (tab === -1)
17931
+ continue;
17932
+ const path = line.slice(tab + 1);
17933
+ if (gone.has(path))
17934
+ continue;
17935
+ if (line.startsWith(SUBMODULE_MODE))
17936
+ submodules.add(path);
17937
+ else
17938
+ paths.add(path);
17939
+ }
17940
+ for (const path of entries(untracked))
17941
+ paths.add(path);
17942
+ return { paths: [...paths], submodules: [...submodules] };
17943
+ }
17944
+
17297
17945
  // ../daemon/src/files/vite-offset.ts
17298
17946
  async function viteLineOffset(worktreePath, path, target) {
17299
17947
  if (!target)
@@ -17374,6 +18022,9 @@ function sides(before, after) {
17374
18022
  }
17375
18023
  return { before, after, binary: false, tooLarge: false };
17376
18024
  }
18025
+ function keptRead(before, after) {
18026
+ return { ...sides(before, after), version: null, writable: false };
18027
+ }
17377
18028
 
17378
18029
  class FileService {
17379
18030
  state;
@@ -17452,9 +18103,13 @@ class FileService {
17452
18103
  });
17453
18104
  }
17454
18105
  async list(worktreeId) {
17455
- const r = await git(this.require(worktreeId).path, "ls-files", "-co", "--exclude-standard");
17456
- return r.out.split(`
17457
- `).filter(Boolean);
18106
+ const cwd = this.require(worktreeId).path;
18107
+ const [staged, untracked, deleted] = await Promise.all([
18108
+ git(cwd, "ls-files", "-z", "-s"),
18109
+ git(cwd, "ls-files", "-z", "-o", "--exclude-standard"),
18110
+ git(cwd, "ls-files", "-z", "-d")
18111
+ ]);
18112
+ return parseListing(staged.out, untracked.out, deleted.out);
17458
18113
  }
17459
18114
  async search(worktreeId, query) {
17460
18115
  const { path: cwd } = this.require(worktreeId);
@@ -17521,7 +18176,7 @@ class FileService {
17521
18176
  revealPath(target) {
17522
18177
  if (process.platform !== "darwin")
17523
18178
  throw new UserError("reveal is only available on macOS");
17524
- const child = spawn2("open", ["-R", target], { stdio: "ignore" });
18179
+ const child = spawn3("open", ["-R", target], { stdio: "ignore" });
17525
18180
  child.on("error", () => {});
17526
18181
  child.unref();
17527
18182
  }
@@ -17614,8 +18269,89 @@ function prNumberOf(url) {
17614
18269
  return m ? Number(m[1]) : null;
17615
18270
  }
17616
18271
 
18272
+ // ../daemon/src/runtime/setup.ts
18273
+ var SETUP_RING = 64 * 1024;
18274
+ var SETUP_COLS = 120;
18275
+ var SETUP_ROWS = 30;
18276
+ function runSetup(cmd, cwd, onLine, extra = {}) {
18277
+ return new Promise((resolve) => {
18278
+ const lines = new LineSplitter;
18279
+ try {
18280
+ new PtyStream({
18281
+ cwd,
18282
+ env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
18283
+ cols: SETUP_COLS,
18284
+ rows: SETUP_ROWS,
18285
+ file: "sh",
18286
+ args: ["-c", cmd],
18287
+ ring: SETUP_RING
18288
+ }, (data) => {
18289
+ for (const line of lines.feed(data))
18290
+ onLine(line);
18291
+ }, resolve);
18292
+ } catch (e) {
18293
+ onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
18294
+ resolve(1);
18295
+ }
18296
+ });
18297
+ }
18298
+ function envStrings2(base) {
18299
+ const env = {};
18300
+ for (const [k, v] of Object.entries(base))
18301
+ if (typeof v === "string")
18302
+ env[k] = v;
18303
+ return env;
18304
+ }
18305
+
18306
+ // ../daemon/src/repos/afterLand.ts
18307
+ class AfterLand {
18308
+ d;
18309
+ running = new Set;
18310
+ constructor(d) {
18311
+ this.d = d;
18312
+ }
18313
+ busy(repoId) {
18314
+ return this.running.has(repoId);
18315
+ }
18316
+ run(repoId) {
18317
+ const repo = this.d.state.repo(repoId);
18318
+ const commands = repo?.config.afterLand ?? [];
18319
+ if (!repo || commands.length === 0 || this.running.has(repoId))
18320
+ return;
18321
+ this.running.add(repoId);
18322
+ const done = this.exec(repo, commands).finally(() => this.running.delete(repoId));
18323
+ fireAndForget(repo.id, done, "afterLand");
18324
+ }
18325
+ async exec(repo, commands) {
18326
+ if (this.d.self.building(true))
18327
+ this.d.hub.emit("selfChanged");
18328
+ let failure;
18329
+ for (const command of commands) {
18330
+ this.tellMain(repo, `afterLand: ${command}`);
18331
+ let last = "";
18332
+ const code = await runSetup(command, repo.path, (line) => {
18333
+ last = line;
18334
+ this.tellMain(repo, line);
18335
+ });
18336
+ if (code === 0)
18337
+ continue;
18338
+ failure = last.trim() === "" ? `${command} (exit ${code})` : last.trim();
18339
+ this.tellMain(repo, `afterLand stopped: ${command} exited ${code}`);
18340
+ log.warn(repo.id, `afterLand stopped: ${command} exited ${code}`);
18341
+ break;
18342
+ }
18343
+ if (this.d.self.building(false, failure))
18344
+ this.d.hub.emit("selfChanged");
18345
+ }
18346
+ tellMain(repo, line) {
18347
+ const main = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
18348
+ if (main)
18349
+ this.d.hub.emit("log", main.id, "land", line);
18350
+ }
18351
+ }
18352
+
17617
18353
  // ../daemon/src/repos/registry.ts
17618
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
18354
+ import { existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
17619
18355
  import { homedir as homedir7 } from "os";
17620
18356
  import { basename as basename3, dirname as dirname6, join as join14 } from "path";
17621
18357
 
@@ -17643,11 +18379,11 @@ function variantLens(index) {
17643
18379
  }
17644
18380
 
17645
18381
  // ../daemon/src/repos/browse.ts
17646
- import { existsSync as existsSync12, statSync as statSync2 } from "fs";
18382
+ import { existsSync as existsSync13, statSync as statSync3 } from "fs";
17647
18383
  import { readdir } from "fs/promises";
17648
18384
  import { homedir as homedir5 } from "os";
17649
18385
  import { dirname as dirname4, join as join10 } from "path";
17650
- var MAX_ENTRIES2 = 40;
18386
+ var MAX_ENTRIES = 40;
17651
18387
  function expandTilde(raw) {
17652
18388
  return raw === "~" || raw.startsWith("~/") ? join10(homedir5(), raw.slice(1)) : raw;
17653
18389
  }
@@ -17660,7 +18396,7 @@ function holdsNothing(names) {
17660
18396
  }
17661
18397
  async function describeFolder(abs) {
17662
18398
  const path = collapseTilde(abs);
17663
- if (existsSync12(join10(abs, ".git")))
18399
+ if (existsSync13(join10(abs, ".git")))
17664
18400
  return { path, kind: "project" };
17665
18401
  try {
17666
18402
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -17672,13 +18408,13 @@ var NOWHERE = { exists: false, isDir: false, isRepo: false, parentExists: false
17672
18408
  function describe2(typed, parent) {
17673
18409
  let isDir = false;
17674
18410
  try {
17675
- isDir = statSync2(typed).isDirectory();
18411
+ isDir = statSync3(typed).isDirectory();
17676
18412
  } catch {}
17677
18413
  return {
17678
- exists: existsSync12(typed),
18414
+ exists: existsSync13(typed),
17679
18415
  isDir,
17680
- isRepo: isDir && existsSync12(join10(typed, ".git")),
17681
- parentExists: existsSync12(parent)
18416
+ isRepo: isDir && existsSync13(join10(typed, ".git")),
18417
+ parentExists: existsSync13(parent)
17682
18418
  };
17683
18419
  }
17684
18420
  async function browsePath(raw) {
@@ -17703,17 +18439,17 @@ async function browsePath(raw) {
17703
18439
  if (!name.toLowerCase().startsWith(prefix))
17704
18440
  continue;
17705
18441
  const abs = join10(dir, name);
17706
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync12(join10(abs, ".git")) });
18442
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync13(join10(abs, ".git")) });
17707
18443
  }
17708
18444
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
17709
- return { entries: entries.slice(0, MAX_ENTRIES2), target };
18445
+ return { entries: entries.slice(0, MAX_ENTRIES), target };
17710
18446
  }
17711
18447
 
17712
18448
  // ../daemon/src/repos/config.ts
17713
- import { closeSync, existsSync as existsSync13, openSync, readdirSync as readdirSync2, readFileSync as readFileSync9, readSync } from "fs";
18449
+ import { closeSync, existsSync as existsSync14, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, readSync } from "fs";
17714
18450
  import { join as join11 } from "path";
17715
18451
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
17716
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync13(join11(repoPath, rel)));
18452
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync14(join11(repoPath, rel)));
17717
18453
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
17718
18454
  function readConfigFile(repoPath) {
17719
18455
  const found = PLACES.map((p) => present(repoPath, p)).filter((files) => files.length > 0);
@@ -17728,7 +18464,7 @@ function readConfigFile(repoPath) {
17728
18464
  for (const rel of files) {
17729
18465
  let raw;
17730
18466
  try {
17731
- raw = JSON.parse(readFileSync9(join11(repoPath, rel), "utf8"));
18467
+ raw = JSON.parse(readFileSync10(join11(repoPath, rel), "utf8"));
17732
18468
  } catch (e) {
17733
18469
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
17734
18470
  }
@@ -17786,11 +18522,11 @@ function configTarget(repoPath, made) {
17786
18522
  function configBody(repoPath, rel, config) {
17787
18523
  const place = PLACES.find((p) => p.local === rel);
17788
18524
  const abs = place && join11(repoPath, place.shared);
17789
- if (!abs || !existsSync13(abs))
18525
+ if (!abs || !existsSync14(abs))
17790
18526
  return config;
17791
18527
  let shared;
17792
18528
  try {
17793
- shared = JSON.parse(readFileSync9(abs, "utf8"));
18529
+ shared = JSON.parse(readFileSync10(abs, "utf8"));
17794
18530
  } catch {
17795
18531
  return config;
17796
18532
  }
@@ -17803,8 +18539,8 @@ function detectConfig(repoPath) {
17803
18539
  if (explicit)
17804
18540
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
17805
18541
  const pkgPath = join11(repoPath, "package.json");
17806
- if (existsSync13(pkgPath)) {
17807
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
18542
+ if (existsSync14(pkgPath)) {
18543
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
17808
18544
  const scripts = pkg.scripts ?? {};
17809
18545
  const runner = detectRunner(repoPath, pkg.packageManager);
17810
18546
  const pages = detectPages(repoPath, scripts, runner);
@@ -17825,7 +18561,7 @@ function detectConfig(repoPath) {
17825
18561
  };
17826
18562
  }
17827
18563
  }
17828
- if (existsSync13(join11(repoPath, "start.sh"))) {
18564
+ if (existsSync14(join11(repoPath, "start.sh"))) {
17829
18565
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
17830
18566
  }
17831
18567
  const assumed = assumeNothingToRun(repoPath);
@@ -17868,19 +18604,19 @@ function assumeNothingToRun(repoPath) {
17868
18604
  return marker && !servesPage(repoPath) ? marker : undefined;
17869
18605
  }
17870
18606
  function markerIn(repoPath) {
17871
- const named = NO_PAGE_MARKERS.find((f) => existsSync13(join11(repoPath, f)));
18607
+ const named = NO_PAGE_MARKERS.find((f) => existsSync14(join11(repoPath, f)));
17872
18608
  if (named)
17873
18609
  return named;
17874
18610
  let entries;
17875
18611
  try {
17876
- entries = readdirSync2(repoPath);
18612
+ entries = readdirSync3(repoPath);
17877
18613
  } catch {
17878
18614
  return;
17879
18615
  }
17880
18616
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
17881
18617
  }
17882
18618
  function servesPage(repoPath) {
17883
- if (WEB_FILES.some((f) => existsSync13(join11(repoPath, f))))
18619
+ if (WEB_FILES.some((f) => existsSync14(join11(repoPath, f))))
17884
18620
  return true;
17885
18621
  if (/^web:/m.test(head(join11(repoPath, "Procfile"))))
17886
18622
  return true;
@@ -17895,7 +18631,7 @@ function servesPage(repoPath) {
17895
18631
  function frontEndBeside(repoPath) {
17896
18632
  let dirs;
17897
18633
  try {
17898
- dirs = readdirSync2(repoPath, { withFileTypes: true });
18634
+ dirs = readdirSync3(repoPath, { withFileTypes: true });
17899
18635
  } catch {
17900
18636
  return false;
17901
18637
  }
@@ -17917,10 +18653,10 @@ function mentions(text, name) {
17917
18653
  return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
17918
18654
  }
17919
18655
  function head(path) {
17920
- if (!existsSync13(path))
18656
+ if (!existsSync14(path))
17921
18657
  return "";
17922
18658
  try {
17923
- const fd = openSync(path, "r");
18659
+ const fd = openSync2(path, "r");
17924
18660
  try {
17925
18661
  const buf = Buffer.alloc(READ_CAP);
17926
18662
  return buf.toString("utf8", 0, readSync(fd, buf, 0, READ_CAP, 0));
@@ -17945,31 +18681,31 @@ function procCommand(runner, script, body) {
17945
18681
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
17946
18682
  }
17947
18683
  function detectPages(repoPath, scripts, runner) {
17948
- if (!scripts.build || !existsSync13(join11(repoPath, "functions")))
18684
+ if (!scripts.build || !existsSync14(join11(repoPath, "functions")))
17949
18685
  return null;
17950
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join11(repoPath, f)).find((f) => existsSync13(f));
18686
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join11(repoPath, f)).find((f) => existsSync14(f));
17951
18687
  if (!cfg)
17952
18688
  return null;
17953
- const out = readFileSync9(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
18689
+ const out = readFileSync10(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
17954
18690
  if (!out)
17955
18691
  return null;
17956
18692
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
17957
18693
  }
17958
18694
  function detectRunner(repoPath, packageManager) {
17959
- if (existsSync13(join11(repoPath, "bun.lock")) || existsSync13(join11(repoPath, "bun.lockb")))
18695
+ if (existsSync14(join11(repoPath, "bun.lock")) || existsSync14(join11(repoPath, "bun.lockb")))
17960
18696
  return "bun";
17961
- if (existsSync13(join11(repoPath, "pnpm-lock.yaml")))
18697
+ if (existsSync14(join11(repoPath, "pnpm-lock.yaml")))
17962
18698
  return "pnpm";
17963
- if (existsSync13(join11(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
18699
+ if (existsSync14(join11(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
17964
18700
  return "yarn";
17965
18701
  return "npm";
17966
18702
  }
17967
18703
 
17968
18704
  // ../daemon/src/repos/create.ts
17969
- import { existsSync as existsSync14, readdirSync as readdirSync3, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
18705
+ import { existsSync as existsSync15, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
17970
18706
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
17971
18707
  import { homedir as homedir6 } from "os";
17972
- import { dirname as dirname5, join as join12, resolve as resolve6 } from "path";
18708
+ import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
17973
18709
  function isInside(dir, parent) {
17974
18710
  const p = parent.replace(/\/+$/, "");
17975
18711
  return dir === p || dir.startsWith(`${p}/`);
@@ -17981,13 +18717,13 @@ function planProject(opts, home = homedir6()) {
17981
18717
  const first = firstProjectsFolder(opts.parent, home);
17982
18718
  const parent = first ?? resolveParent(opts.parent);
17983
18719
  const dir = join12(parent, opts.name.trim());
17984
- if (existsSync14(dir))
18720
+ if (existsSync15(dir))
17985
18721
  throw new UserError(`${dir} already exists`);
17986
18722
  return { parent, dir, makeParent: first !== null };
17987
18723
  }
17988
18724
  function firstProjectsFolder(raw, home) {
17989
18725
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
17990
- if (typed !== join12(home, PROJECTS_FOLDER) || existsSync14(typed) || !existsSync14(home))
18726
+ if (typed !== join12(home, PROJECTS_FOLDER) || existsSync15(typed) || !existsSync15(home))
17991
18727
  return null;
17992
18728
  return join12(realpathSync2(home), PROJECTS_FOLDER);
17993
18729
  }
@@ -17997,12 +18733,12 @@ function planInPlace(opts) {
17997
18733
  throw new UserError(`${opts.name} is not a folder name`);
17998
18734
  }
17999
18735
  const typed = join12(resolveParent(opts.parent), name);
18000
- if (!existsSync14(typed) || !statSync3(typed).isDirectory())
18736
+ if (!existsSync15(typed) || !statSync4(typed).isDirectory())
18001
18737
  throw new UserError(`${typed} is not a folder`);
18002
18738
  const dir = realpathSync2(typed);
18003
- if (existsSync14(join12(dir, ".git")))
18739
+ if (existsSync15(join12(dir, ".git")))
18004
18740
  throw new UserError(`${dir} is already a project; open it instead`);
18005
- if (!holdsNothing(readdirSync3(dir))) {
18741
+ if (!holdsNothing(readdirSync4(dir))) {
18006
18742
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
18007
18743
  }
18008
18744
  return { parent: dirname5(dir), dir, makeParent: false };
@@ -18011,9 +18747,9 @@ function resolveParent(raw) {
18011
18747
  const typed = expandTilde(raw.trim());
18012
18748
  if (!typed.startsWith("/"))
18013
18749
  throw new UserError(`${raw} is not a folder path`);
18014
- if (!existsSync14(typed))
18750
+ if (!existsSync15(typed))
18015
18751
  throw new UserError(`${raw} does not exist; make it first`);
18016
- if (!statSync3(typed).isDirectory())
18752
+ if (!statSync4(typed).isDirectory())
18017
18753
  throw new UserError(`${raw} is not a folder`);
18018
18754
  return realpathSync2(typed);
18019
18755
  }
@@ -18051,7 +18787,7 @@ async function initWithEmptyCommit(dir) {
18051
18787
  const exclude = await git(dir, "rev-parse", "--git-path", "info/exclude");
18052
18788
  if (!exclude.ok)
18053
18789
  throw new UserError(`git rev-parse failed: ${exclude.err}`);
18054
- const excludePath = resolve6(dir, exclude.out);
18790
+ const excludePath = resolve7(dir, exclude.out);
18055
18791
  await mkdir2(dirname5(excludePath), { recursive: true });
18056
18792
  await appendFile2(excludePath, `
18057
18793
  ${LOCAL_EXCLUDES.join(`
@@ -18127,7 +18863,7 @@ async function ok(p, what) {
18127
18863
  }
18128
18864
 
18129
18865
  // ../daemon/src/repos/watcher.ts
18130
- import { existsSync as existsSync15, watch } from "fs";
18866
+ import { existsSync as existsSync16, watch } from "fs";
18131
18867
  import { basename as basename2, join as join13 } from "path";
18132
18868
  function watchDefaultBranch(repoPath, branch, onMove) {
18133
18869
  let last = null;
@@ -18239,7 +18975,7 @@ function watchConfigFile(repoPath, onChange) {
18239
18975
  const armInner = () => {
18240
18976
  inner?.close();
18241
18977
  inner = null;
18242
- if (!existsSync15(dir))
18978
+ if (!existsSync16(dir))
18243
18979
  return;
18244
18980
  try {
18245
18981
  inner = watch(dir, (_event, filename) => {
@@ -18293,7 +19029,7 @@ class RepoRegistry {
18293
19029
  }
18294
19030
  async boot() {
18295
19031
  const { state } = this.d;
18296
- const gone = state.worktrees.filter((wt) => !existsSync16(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
19032
+ const gone = state.worktrees.filter((wt) => !existsSync17(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
18297
19033
  for (const wt of gone)
18298
19034
  await this.d.worktrees.forgetGone(wt);
18299
19035
  for (const repo of state.repos) {
@@ -18313,25 +19049,15 @@ class RepoRegistry {
18313
19049
  log.info("daemon", `${cold} worktrees across ${state.repos.length} repos start when opened`);
18314
19050
  }
18315
19051
  warm(repoId) {
18316
- if (this.warmed.has(repoId))
19052
+ if (this.warmed.has(repoId) || !this.d.state.repo(repoId))
18317
19053
  return;
18318
19054
  this.warmed.add(repoId);
18319
- const repo = this.d.state.requireRepo(repoId);
18320
- const mine = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind !== "spare");
18321
- fireAndForget(repoId, (async () => {
18322
- for (const wt of mine)
18323
- await this.d.runtime.start(wt, repo);
18324
- })(), "warm");
18325
19055
  this.d.worktrees.spare.warm(repoId);
18326
19056
  }
18327
19057
  touch(worktreeId) {
18328
- const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
18329
- if (!this.warmed.has(repo.id)) {
18330
- this.warm(repo.id);
19058
+ if (!this.d.state.worktree(worktreeId))
18331
19059
  return;
18332
- }
18333
- if (!this.d.runtime.get(wt.id)?.procs)
18334
- fireAndForget(wt.id, this.d.runtime.start(wt, repo), "start on open");
19060
+ fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start on open");
18335
19061
  }
18336
19062
  async create(opts) {
18337
19063
  const inPlace = opts.mode === "init";
@@ -18370,7 +19096,7 @@ class RepoRegistry {
18370
19096
  return `${repo.name} is set up now`;
18371
19097
  if (main.promptedAt || main.lastTurn)
18372
19098
  return `${repo.name} has a chat now`;
18373
- if (existsSync16(join14(repo.path, ".claude")))
19099
+ if (existsSync17(join14(repo.path, ".claude")))
18374
19100
  return `an agent has already run in ${repo.name}`;
18375
19101
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
18376
19102
  return `${repo.name} has files in it now`;
@@ -18447,7 +19173,7 @@ class RepoRegistry {
18447
19173
  }
18448
19174
  async register(rawPath, made) {
18449
19175
  const path = expandTilde(rawPath);
18450
- if (!existsSync16(path))
19176
+ if (!existsSync17(path))
18451
19177
  throw new UserError(`${path} does not exist`);
18452
19178
  if (!await isGitRepo(path))
18453
19179
  throw new UserError(`${path} is not a git repository`);
@@ -18497,28 +19223,39 @@ class RepoRegistry {
18497
19223
  const mine = this.d.state.worktrees.filter((w) => w.repoId === repoId);
18498
19224
  const tasks = mine.filter((w) => w.kind !== "main" && w.kind !== "spare");
18499
19225
  if (tasks.length > 0) {
18500
- throw new UserError(`${repo.name} still has ${tasks.length} worktree${tasks.length === 1 ? "" : "s"}; remove them first`);
19226
+ throw new UserError(`${repo.name} still has ${tasks.length} worktree${tasks.length === 1 ? "" : "s"}; archive them first`);
18501
19227
  }
18502
19228
  this.stopWatcher(repoId);
18503
19229
  for (const wt of mine.filter((w) => w.kind === "spare"))
18504
- await this.d.worktrees.remove(wt.id, { spare: true });
19230
+ await this.d.worktrees.discardWorktree(wt.id);
18505
19231
  for (const wt of mine.filter((w) => w.kind === "main")) {
18506
19232
  await this.d.runtime.stop(wt.id);
18507
19233
  this.d.state.removeWorktree(wt.id);
18508
19234
  this.d.worktrees.deleteChat(wt.id);
18509
19235
  releasePort(wt.proxyPort);
18510
19236
  }
19237
+ this.d.drafts?.drop(draftKey(repoId));
18511
19238
  this.d.state.removeRepo(repoId);
18512
19239
  this.d.hub.emit("reposChanged");
18513
19240
  this.d.hub.emit("worktreesChanged");
18514
19241
  }
18515
- async confirmConfig(repoId, config) {
19242
+ async confirmConfig(repoId, config, kind) {
18516
19243
  const repo = this.d.state.requireRepo(repoId);
18517
19244
  const current = readConfigFile(repo.path);
18518
19245
  if (current && !current.ok && current.conflict)
18519
19246
  throw new UserError(current.reason);
18520
- this.placeConfig(repo);
18521
- const rel = repo.configFile;
19247
+ const was = configTarget(repo.path, !!repo.made);
19248
+ const rel = configSibling(was, kind);
19249
+ const moved = rel !== was && existsSync17(join14(repo.path, was));
19250
+ if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
19251
+ try {
19252
+ rmSync2(join14(repo.path, was));
19253
+ } catch (e) {
19254
+ log.warn(repoId, `could not remove ${was}`, e);
19255
+ }
19256
+ if (isLocalConfigFile(was))
19257
+ await unexcludeFromGit(repo.path, was);
19258
+ }
18522
19259
  if (isLocalConfigFile(rel))
18523
19260
  await excludeFromGit(repo.path, rel);
18524
19261
  const file = join14(repo.path, rel);
@@ -18529,6 +19266,7 @@ class RepoRegistry {
18529
19266
  } catch (e) {
18530
19267
  log.warn(repoId, `could not write ${rel}`, e);
18531
19268
  }
19269
+ this.placeConfig(repo);
18532
19270
  const back = readConfigFile(repo.path);
18533
19271
  repo.config = back?.ok ? back.config : config;
18534
19272
  repo.needsSetup = false;
@@ -18598,7 +19336,7 @@ class RepoRegistry {
18598
19336
  }
18599
19337
  placeConfig(repo) {
18600
19338
  const rel = configTarget(repo.path, !!repo.made);
18601
- if (isLocalConfigFile(rel) && existsSync16(join14(repo.path, rel))) {
19339
+ if (isLocalConfigFile(rel) && existsSync17(join14(repo.path, rel))) {
18602
19340
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
18603
19341
  }
18604
19342
  if (repo.configFile === rel)
@@ -18637,6 +19375,10 @@ class RepoRegistry {
18637
19375
  if (main)
18638
19376
  this.d.hub.emit("log", main.id, "config", line);
18639
19377
  }
19378
+ async checkSelf(repo) {
19379
+ if (await this.d.self.check(repo))
19380
+ this.d.hub.emit("selfChanged");
19381
+ }
18640
19382
  startWatcher(repo) {
18641
19383
  if (this.watchers.has(repo.id))
18642
19384
  return;
@@ -18644,6 +19386,7 @@ class RepoRegistry {
18644
19386
  this.d.worktrees.invalidateCounts();
18645
19387
  this.d.hub.emit("repoTick", repo.id);
18646
19388
  fireAndForget(repo.id, this.d.worktrees.spare.refresh(repo.id), "spare refresh");
19389
+ fireAndForget(repo.id, this.checkSelf(repo).then(() => this.d.afterLand.run(repo.id)), "self check");
18647
19390
  });
18648
19391
  const stopCfg = watchConfigFile(repo.path, () => this.reloadConfig(repo.id));
18649
19392
  const stopWt = watchWorktreeDir(repo.path, () => {
@@ -18930,7 +19673,7 @@ function retitle(pages, key, title) {
18930
19673
  function rank(pages, now, n = KEEP) {
18931
19674
  return Object.keys(pages).sort(byStanding(pages, now)).slice(0, n);
18932
19675
  }
18933
- function entries(pages, now) {
19676
+ function entries2(pages, now) {
18934
19677
  return rank(pages, now).map((path) => {
18935
19678
  const v = pages[path];
18936
19679
  return { path, score: rounded(decayed(v, now)), last: v.last, ...v.title ? { title: v.title } : {} };
@@ -19625,7 +20368,7 @@ function unseenOf(candidates, seen, hashes) {
19625
20368
  }
19626
20369
 
19627
20370
  // ../daemon/src/routes/service.ts
19628
- var SAVE_DELAY_MS = 5000;
20371
+ var SAVE_DELAY_MS2 = 5000;
19629
20372
  var MAX_MANIFESTS = 50;
19630
20373
  var SCAN_CACHE = 20;
19631
20374
  var MAX_HASHED = 64;
@@ -19700,7 +20443,7 @@ class RouteService {
19700
20443
  }
19701
20444
  history(repoId) {
19702
20445
  const pages = this.d.state.visitsOf(repoId);
19703
- return pages ? entries(pages, this.now()) : [];
20446
+ return pages ? entries2(pages, this.now()) : [];
19704
20447
  }
19705
20448
  historyAll() {
19706
20449
  const out = {};
@@ -19925,7 +20668,7 @@ class RouteService {
19925
20668
  } catch (e) {
19926
20669
  log.error("routes", "could not write the page lists", e);
19927
20670
  }
19928
- }, this.d.saveDelayMs ?? SAVE_DELAY_MS);
20671
+ }, this.d.saveDelayMs ?? SAVE_DELAY_MS2);
19929
20672
  this.saveTimer.unref();
19930
20673
  }
19931
20674
  repoOf(worktreeId) {
@@ -19937,7 +20680,7 @@ class RouteService {
19937
20680
  }
19938
20681
 
19939
20682
  // ../daemon/src/runtime/bridge-script.ts
19940
- import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
20683
+ import { existsSync as existsSync18, readFileSync as readFileSync11, statSync as statSync5 } from "fs";
19941
20684
  var LEARNED_MAX = 8;
19942
20685
 
19943
20686
  class BridgeScript {
@@ -19967,16 +20710,16 @@ class BridgeScript {
19967
20710
  this.cache = null;
19968
20711
  }
19969
20712
  get() {
19970
- if (!existsSync17(this.path))
20713
+ if (!existsSync18(this.path))
19971
20714
  return "// toyon bridge not built";
19972
- const s = statSync4(this.path);
20715
+ const s = statSync5(this.path);
19973
20716
  const stamp = `${s.mtimeMs}:${s.size}`;
19974
20717
  if (this.cache && stamp === this.stamp)
19975
20718
  return this.cache;
19976
20719
  this.stamp = stamp;
19977
20720
  const origins = [...this.origins, ...this.learned];
19978
20721
  this.cache = `window.__toyonShellOrigins=${JSON.stringify(origins)};
19979
- ${readFileSync10(this.path, "utf8")}`;
20722
+ ${readFileSync11(this.path, "utf8")}`;
19980
20723
  return this.cache;
19981
20724
  }
19982
20725
  }
@@ -19993,62 +20736,394 @@ function servedLocally(origin) {
19993
20736
  return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
19994
20737
  }
19995
20738
 
19996
- // ../daemon/src/server/ws.ts
19997
- import { homedir as homedir9 } from "os";
19998
-
19999
- // ../daemon/src/agent/config.ts
20000
- import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
20001
- import { homedir as homedir8 } from "os";
20002
- import { join as join16 } from "path";
20003
- function agentConfigFiles(spec, repoPath, home = homedir8()) {
20004
- const file = (id, label, path) => ({
20005
- id,
20006
- label,
20007
- path,
20008
- exists: existsSync18(path)
20009
- });
20010
- if (spec.id === "claude") {
20011
- return [
20012
- file("user-config", "user config (MCP servers, projects)", join16(home, ".claude.json")),
20013
- file("user-settings", "user settings (permissions, hooks)", join16(home, ".claude", "settings.json")),
20014
- ...repoPath ? [file("project-mcp", "project MCP servers", join16(repoPath, ".mcp.json"))] : [],
20015
- ...repoPath ? [file("project-settings", "project settings", join16(repoPath, ".claude", "settings.json"))] : []
20016
- ];
20017
- }
20018
- if (spec.id === "codex")
20019
- return [file("user-config", "config", join16(home, ".codex", "config.toml"))];
20020
- return [];
20739
+ // ../daemon/src/runtime/memory.ts
20740
+ var MAC_PRESSURE_CRITICAL = 4;
20741
+ var AVAILABLE_FLOOR = 0.15;
20742
+ var unsupportedLogged = false;
20743
+ async function memoryTight() {
20744
+ const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
20745
+ if (signal === null && !unsupportedLogged) {
20746
+ unsupportedLogged = true;
20747
+ log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
20748
+ }
20749
+ return signal;
20750
+ }
20751
+ async function darwin() {
20752
+ const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
20753
+ if (!r.ok)
20754
+ return null;
20755
+ const [level, available] = r.out.split(/\s+/).map(Number);
20756
+ if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
20757
+ return null;
20758
+ return darwinSignal(level, available);
20021
20759
  }
20022
- function describeAgentConfig(spec, repoPath, home = homedir8()) {
20023
- const files = agentConfigFiles(spec, repoPath, home);
20024
- const servers = [];
20025
- if (spec.id === "claude") {
20026
- const user = readJson(join16(home, ".claude.json"));
20027
- servers.push(...serversOf(user?.mcpServers, "user"));
20028
- if (repoPath) {
20029
- const projects = user?.projects;
20030
- const mine = projects?.[repoPath];
20031
- servers.push(...serversOf(mine?.mcpServers, "local"));
20032
- servers.push(...serversOf(readJson(join16(repoPath, ".mcp.json"))?.mcpServers, "project"));
20033
- }
20034
- } else if (spec.id === "codex") {
20035
- servers.push(...codexServers(join16(home, ".codex", "config.toml")));
20036
- }
20037
- return { agent: spec.id, files, servers };
20760
+ function darwinSignal(level, available) {
20761
+ const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
20762
+ return {
20763
+ tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
20764
+ why: `memory pressure ${pressure} with ${available}% available`
20765
+ };
20038
20766
  }
20039
- function readJson(path) {
20040
- if (!existsSync18(path))
20041
- return null;
20767
+ async function linux() {
20768
+ let text;
20042
20769
  try {
20043
- const v = JSON.parse(readFileSync11(path, "utf8"));
20044
- return v && typeof v === "object" && !Array.isArray(v) ? v : null;
20045
- } catch (e) {
20046
- log.warn("agent-config", `${path} is not valid JSON`, e);
20770
+ text = await Bun.file("/proc/meminfo").text();
20771
+ } catch {
20047
20772
  return null;
20048
20773
  }
20049
- }
20050
- function serversOf(raw, scope) {
20051
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
20774
+ const field = (name) => {
20775
+ const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
20776
+ return m ? Number(m[1]) : null;
20777
+ };
20778
+ const total = field("MemTotal");
20779
+ const available = field("MemAvailable");
20780
+ if (!total || available === null)
20781
+ return null;
20782
+ const percent = Math.round(available / total * 100);
20783
+ return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
20784
+ }
20785
+ async function sampleCosts(pgids) {
20786
+ const costs = new Map;
20787
+ if (pgids.length === 0)
20788
+ return costs;
20789
+ const wanted = new Set(pgids);
20790
+ const r = await run("ps", ["-axo", "pgid=,rss="], "/");
20791
+ if (!r.ok)
20792
+ return costs;
20793
+ for (const line of r.out.split(`
20794
+ `)) {
20795
+ const [pgid, rss] = line.trim().split(/\s+/).map(Number);
20796
+ if (pgid === undefined || rss === undefined || !wanted.has(pgid))
20797
+ continue;
20798
+ costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
20799
+ }
20800
+ return costs;
20801
+ }
20802
+
20803
+ // ../daemon/src/runtime/idle.ts
20804
+ var LOCAL_SLEEP_MS = 2 * 60 * 60000;
20805
+ var CLOUD_SLEEP_MS = 5 * 60000;
20806
+ function sleepMsFrom(raw, inCloud) {
20807
+ if (raw === "off")
20808
+ return null;
20809
+ const n = Number(raw);
20810
+ if (raw && Number.isFinite(n) && n > 0)
20811
+ return n;
20812
+ return inCloud ? CLOUD_SLEEP_MS : LOCAL_SLEEP_MS;
20813
+ }
20814
+ var DEFAULT_SLEEP_MS = sleepMsFrom(process.env.TOYON_PROC_SLEEP_MS, cloud.enabled);
20815
+ var PRESSURE_EVERY_MS = 15000;
20816
+ var COSTS_EVERY_MS = 60000;
20817
+ var STAGGER_MS = 3000;
20818
+
20819
+ class IdlePolicy {
20820
+ d;
20821
+ entries = new Map;
20822
+ viewing = new Map;
20823
+ queue = [];
20824
+ draining = false;
20825
+ sleepMs;
20826
+ now;
20827
+ constructor(d) {
20828
+ this.d = d;
20829
+ this.sleepMs = d.sleepMs === undefined ? DEFAULT_SLEEP_MS : d.sleepMs;
20830
+ this.now = d.now ?? Date.now;
20831
+ d.hub.on("previewRequest", (id) => this.touch(id));
20832
+ d.hub.on("agentStatus", (id) => this.stamp(id));
20833
+ d.hub.on("holdsChanged", (id) => this.stamp(id));
20834
+ d.hub.on("turnSettled", (id) => {
20835
+ if (this.recentlyViewed(id))
20836
+ d.wake(id);
20837
+ });
20838
+ d.hub.on("proc", (id, p) => {
20839
+ if (p.status !== "starting")
20840
+ this.reconsider(id);
20841
+ });
20842
+ d.hub.on("worktreesChanged", () => this.reconsiderAll());
20843
+ const every = d.setInterval ?? ((fn, ms) => {
20844
+ setInterval(fn, ms).unref?.();
20845
+ });
20846
+ every(() => fireAndForget("idle", this.checkPressure(), "memory check"), PRESSURE_EVERY_MS);
20847
+ every(() => fireAndForget("idle", this.sampleCosts(), "cost sample"), COSTS_EVERY_MS);
20848
+ }
20849
+ view(key, id) {
20850
+ const prev = this.viewing.get(key) ?? null;
20851
+ if (prev === id)
20852
+ return;
20853
+ if (prev !== null) {
20854
+ this.viewing.delete(key);
20855
+ this.entry(prev).viewers.delete(key);
20856
+ this.record(prev);
20857
+ this.stamp(prev);
20858
+ }
20859
+ if (id === null)
20860
+ return;
20861
+ this.viewing.set(key, id);
20862
+ this.entry(id).viewers.add(key);
20863
+ const wt = this.record(id);
20864
+ this.stamp(id);
20865
+ this.d.wake(id);
20866
+ if (wt && wt.kind !== "spare") {
20867
+ this.enqueue(`spare:${wt.repoId}`, async () => {
20868
+ await this.d.runtime.awaitPreview(id, STAGGER_MS);
20869
+ this.d.warmSpare(wt.repoId);
20870
+ const spare = this.spareOf(wt.repoId);
20871
+ if (spare)
20872
+ this.d.wake(spare.id);
20873
+ });
20874
+ }
20875
+ }
20876
+ drop(key) {
20877
+ this.view(key, null);
20878
+ }
20879
+ shutdown() {
20880
+ this.recordViewed();
20881
+ }
20882
+ record(id) {
20883
+ const wt = this.d.state.worktree(id);
20884
+ if (!wt)
20885
+ return;
20886
+ wt.viewedAt = this.now();
20887
+ this.d.state.save();
20888
+ return wt;
20889
+ }
20890
+ recordViewed() {
20891
+ let changed = false;
20892
+ for (const id of new Set(this.viewing.values())) {
20893
+ const wt = this.d.state.worktree(id);
20894
+ if (!wt)
20895
+ continue;
20896
+ wt.viewedAt = this.now();
20897
+ changed = true;
20898
+ }
20899
+ if (changed)
20900
+ this.d.state.save();
20901
+ }
20902
+ touch(id) {
20903
+ this.stamp(id);
20904
+ }
20905
+ boot() {
20906
+ const recent = this.d.state.worktrees.filter((w) => w.kind !== "spare" && this.recentlyViewed(w.id)).sort((a, b) => (b.viewedAt ?? 0) - (a.viewedAt ?? 0));
20907
+ for (const wt of recent) {
20908
+ this.enqueue(`boot:${wt.id}`, async () => {
20909
+ this.d.wake(wt.id);
20910
+ await this.d.runtime.awaitPreview(wt.id, STAGGER_MS);
20911
+ });
20912
+ }
20913
+ }
20914
+ recentlyViewed(id) {
20915
+ const wt = this.d.state.worktree(id);
20916
+ return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
20917
+ }
20918
+ isViewed(id) {
20919
+ if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
20920
+ return true;
20921
+ const wt = this.d.state.worktree(id);
20922
+ if (wt?.kind !== "spare")
20923
+ return false;
20924
+ return this.d.state.worktrees.some((w) => w.repoId === wt.repoId && (this.entries.get(w.id)?.viewers.size ?? 0) > 0);
20925
+ }
20926
+ costs() {
20927
+ const out = {};
20928
+ for (const [id, e] of this.entries)
20929
+ if (e.costKb !== undefined)
20930
+ out[id] = e.costKb;
20931
+ return out;
20932
+ }
20933
+ async checkPressure() {
20934
+ const signal = await (this.d.memory ?? memoryTight)();
20935
+ if (!signal?.tight)
20936
+ return;
20937
+ 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];
20938
+ if (!pick)
20939
+ return;
20940
+ this.sleep(pick.id, `${signal.why}; least recently used`);
20941
+ }
20942
+ async sampleCosts() {
20943
+ this.recordViewed();
20944
+ const awake = this.d.runtime.awake();
20945
+ const pgids = awake.flatMap((a) => a.pgids);
20946
+ if (pgids.length === 0)
20947
+ return;
20948
+ const rss = await (this.d.costs ?? sampleCosts)(pgids);
20949
+ for (const a of awake)
20950
+ this.entry(a.id).costKb = a.pgids.reduce((sum, g) => sum + (rss.get(g) ?? 0), 0);
20951
+ }
20952
+ entry(id) {
20953
+ let e = this.entries.get(id);
20954
+ if (!e) {
20955
+ e = { viewers: new Set, activeAt: this.now(), timer: null };
20956
+ this.entries.set(id, e);
20957
+ }
20958
+ return e;
20959
+ }
20960
+ spareOf(repoId) {
20961
+ return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
20962
+ }
20963
+ stamp(id) {
20964
+ this.entry(id).activeAt = this.now();
20965
+ this.reconsider(id);
20966
+ const wt = this.d.state.worktree(id);
20967
+ if (!wt || wt.kind === "spare")
20968
+ return;
20969
+ const spare = this.spareOf(wt.repoId);
20970
+ if (!spare)
20971
+ return;
20972
+ this.entry(spare.id).activeAt = this.now();
20973
+ this.reconsider(spare.id);
20974
+ }
20975
+ candidate(id) {
20976
+ const wt = this.d.state.worktree(id);
20977
+ if (!wt || this.isViewed(id) || this.busy(id))
20978
+ return false;
20979
+ const procs = this.d.runtime.get(id)?.procs;
20980
+ if (!procs || procs.asleep)
20981
+ return false;
20982
+ const states = procs.states();
20983
+ if (states.length === 0 || states.some((s) => s.status === "starting"))
20984
+ return false;
20985
+ if (wt.kind === "spare") {
20986
+ return !this.d.state.worktrees.some((w) => w.repoId === wt.repoId && w.id !== id && this.busy(w.id));
20987
+ }
20988
+ return true;
20989
+ }
20990
+ busy(id) {
20991
+ return this.d.runtime.busy(id);
20992
+ }
20993
+ reconsider(id) {
20994
+ if (!this.candidate(id)) {
20995
+ this.disarm(id);
20996
+ return;
20997
+ }
20998
+ if (this.sleepMs === null)
20999
+ return;
21000
+ this.arm(id, Math.max(0, this.entry(id).activeAt + this.sleepMs - this.now()));
21001
+ }
21002
+ reconsiderAll() {
21003
+ for (const id of this.entries.keys()) {
21004
+ if (this.d.state.worktree(id))
21005
+ continue;
21006
+ this.disarm(id);
21007
+ this.entries.delete(id);
21008
+ }
21009
+ for (const wt of this.d.state.worktrees)
21010
+ this.reconsider(wt.id);
21011
+ }
21012
+ arm(id, ms) {
21013
+ this.disarm(id);
21014
+ const set = this.d.setTimer ?? ((fn, delay) => {
21015
+ const t = setTimeout(fn, delay);
21016
+ t.unref?.();
21017
+ return t;
21018
+ });
21019
+ this.entry(id).timer = set(() => this.fire(id), ms);
21020
+ }
21021
+ disarm(id) {
21022
+ const e = this.entries.get(id);
21023
+ if (!e?.timer)
21024
+ return;
21025
+ (this.d.clearTimer ?? ((t) => clearTimeout(t)))(e.timer);
21026
+ e.timer = null;
21027
+ }
21028
+ fire(id) {
21029
+ const e = this.entry(id);
21030
+ e.timer = null;
21031
+ if (this.sleepMs === null || !this.candidate(id))
21032
+ return;
21033
+ const idle = this.now() - e.activeAt;
21034
+ if (idle < this.sleepMs) {
21035
+ this.arm(id, this.sleepMs - idle);
21036
+ return;
21037
+ }
21038
+ this.sleep(id, `idle ${humanMs(this.sleepMs)}`);
21039
+ }
21040
+ sleep(id, why) {
21041
+ this.disarm(id);
21042
+ this.entry(id).costKb = undefined;
21043
+ log.info(id, `asleep: ${why}`);
21044
+ fireAndForget(id, this.d.runtime.sleep(id), "sleep");
21045
+ }
21046
+ enqueue(key, run) {
21047
+ if (this.queue.some((q) => q.key === key))
21048
+ return;
21049
+ this.queue.push({ key, run });
21050
+ if (!this.draining)
21051
+ fireAndForget("idle", this.drain(), "wake queue");
21052
+ }
21053
+ async drain() {
21054
+ this.draining = true;
21055
+ try {
21056
+ for (let next = this.queue.shift();next; next = this.queue.shift())
21057
+ await next.run();
21058
+ } finally {
21059
+ this.draining = false;
21060
+ }
21061
+ }
21062
+ }
21063
+ function humanMs(ms) {
21064
+ if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
21065
+ return `${ms / (60 * 60000)}h`;
21066
+ if (ms >= 60000)
21067
+ return `${Math.round(ms / 60000)} min`;
21068
+ return `${Math.round(ms / 1000)}s`;
21069
+ }
21070
+
21071
+ // ../daemon/src/server/ws.ts
21072
+ import { homedir as homedir9 } from "os";
21073
+
21074
+ // ../daemon/src/agent/config.ts
21075
+ import { existsSync as existsSync19, readFileSync as readFileSync12 } from "fs";
21076
+ import { homedir as homedir8 } from "os";
21077
+ import { join as join16 } from "path";
21078
+ function agentConfigFiles(spec, repoPath, home = homedir8()) {
21079
+ const file = (id, label, path) => ({
21080
+ id,
21081
+ label,
21082
+ path,
21083
+ exists: existsSync19(path)
21084
+ });
21085
+ if (spec.id === "claude") {
21086
+ return [
21087
+ file("user-config", "user config (MCP servers, projects)", join16(home, ".claude.json")),
21088
+ file("user-settings", "user settings (permissions, hooks)", join16(home, ".claude", "settings.json")),
21089
+ ...repoPath ? [file("project-mcp", "project MCP servers", join16(repoPath, ".mcp.json"))] : [],
21090
+ ...repoPath ? [file("project-settings", "project settings", join16(repoPath, ".claude", "settings.json"))] : []
21091
+ ];
21092
+ }
21093
+ if (spec.id === "codex")
21094
+ return [file("user-config", "config", join16(home, ".codex", "config.toml"))];
21095
+ return [];
21096
+ }
21097
+ function describeAgentConfig(spec, repoPath, home = homedir8()) {
21098
+ const files = agentConfigFiles(spec, repoPath, home);
21099
+ const servers = [];
21100
+ if (spec.id === "claude") {
21101
+ const user = readJson(join16(home, ".claude.json"));
21102
+ servers.push(...serversOf(user?.mcpServers, "user"));
21103
+ if (repoPath) {
21104
+ const projects = user?.projects;
21105
+ const mine = projects?.[repoPath];
21106
+ servers.push(...serversOf(mine?.mcpServers, "local"));
21107
+ servers.push(...serversOf(readJson(join16(repoPath, ".mcp.json"))?.mcpServers, "project"));
21108
+ }
21109
+ } else if (spec.id === "codex") {
21110
+ servers.push(...codexServers(join16(home, ".codex", "config.toml")));
21111
+ }
21112
+ return { agent: spec.id, files, servers };
21113
+ }
21114
+ function readJson(path) {
21115
+ if (!existsSync19(path))
21116
+ return null;
21117
+ try {
21118
+ const v = JSON.parse(readFileSync12(path, "utf8"));
21119
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
21120
+ } catch (e) {
21121
+ log.warn("agent-config", `${path} is not valid JSON`, e);
21122
+ return null;
21123
+ }
21124
+ }
21125
+ function serversOf(raw, scope) {
21126
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
20052
21127
  return [];
20053
21128
  return Object.entries(raw).map(([name, v]) => {
20054
21129
  const rec = v && typeof v === "object" ? v : {};
@@ -20058,11 +21133,11 @@ function serversOf(raw, scope) {
20058
21133
  });
20059
21134
  }
20060
21135
  function codexServers(path) {
20061
- if (!existsSync18(path))
21136
+ if (!existsSync19(path))
20062
21137
  return [];
20063
21138
  const out = [];
20064
21139
  let current = null;
20065
- for (const raw of readFileSync11(path, "utf8").split(`
21140
+ for (const raw of readFileSync12(path, "utf8").split(`
20066
21141
  `)) {
20067
21142
  const line = raw.trim();
20068
21143
  const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
@@ -20597,7 +21672,7 @@ function daylightNow(tz, at = new Date) {
20597
21672
  }
20598
21673
 
20599
21674
  // ../daemon/src/server/handlers.ts
20600
- var toast = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
21675
+ var shipped = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
20601
21676
  var errorText = (e) => e instanceof Error ? e.message : String(e);
20602
21677
  var gitStatus = async (s, ctx, worktreeId) => {
20603
21678
  const info = await s.worktrees.gitStatus(worktreeId);
@@ -20611,6 +21686,8 @@ var gitAndPages = async (s, ctx, worktreeId) => {
20611
21686
  };
20612
21687
  var notify = async (s, ctx, worktreeId, msg) => {
20613
21688
  ctx.reply(msg);
21689
+ if (!msg.ok)
21690
+ s.turns.markUnread(worktreeId);
20614
21691
  await gitStatus(s, ctx, worktreeId);
20615
21692
  };
20616
21693
  var requireRun = (s, id) => {
@@ -20622,8 +21699,18 @@ var requireRun = (s, id) => {
20622
21699
  var handlers = {
20623
21700
  async subscribe(msg, ctx, s) {
20624
21701
  const r = s.worktrees.readable(msg.worktreeId);
20625
- if (!r)
20626
- throw new UserError("unknown worktree");
21702
+ if (!r) {
21703
+ const kept = s.worktrees.archivedTranscript(msg.worktreeId);
21704
+ if (!kept)
21705
+ throw new UserError("unknown worktree");
21706
+ if (!ctx.subscribe(msg.worktreeId))
21707
+ return;
21708
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
21709
+ ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
21710
+ ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
21711
+ await gitStatus(s, ctx, msg.worktreeId);
21712
+ return;
21713
+ }
20627
21714
  if (!ctx.subscribe(msg.worktreeId))
20628
21715
  return;
20629
21716
  if (!r.wt) {
@@ -20633,13 +21720,11 @@ var handlers = {
20633
21720
  await gitAndPages(s, ctx, msg.worktreeId);
20634
21721
  return;
20635
21722
  }
20636
- s.repos.touch(msg.worktreeId);
20637
21723
  const agent = s.runtime.ensureAgent(r.wt).agent;
20638
- const events = agent.transcript();
20639
21724
  ctx.reply({
20640
21725
  t: "backfill",
20641
21726
  worktreeId: msg.worktreeId,
20642
- events: events.slice(-1000),
21727
+ events: coalesce(agent.transcript()),
20643
21728
  log: s.runtime.recentLogs(msg.worktreeId)
20644
21729
  });
20645
21730
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
@@ -20649,12 +21734,18 @@ var handlers = {
20649
21734
  unsubscribe(msg, ctx) {
20650
21735
  ctx.unsubscribe(msg.worktreeId);
20651
21736
  },
21737
+ view(msg, ctx) {
21738
+ ctx.view(msg.worktreeId);
21739
+ },
20652
21740
  seen(msg, _ctx, s) {
20653
21741
  s.turns.markSeen(msg.worktreeId);
20654
21742
  },
20655
21743
  "mark-unread"(msg, _ctx, s) {
20656
21744
  s.turns.markUnread(msg.worktreeId);
20657
21745
  },
21746
+ "set-draft"(msg, _ctx, s) {
21747
+ s.drafts.set(msg.boxId, msg.text, msg.clientId);
21748
+ },
20658
21749
  "refresh-git"(msg, _ctx, s) {
20659
21750
  s.worktrees.recount(msg.repoId);
20660
21751
  },
@@ -20667,7 +21758,12 @@ var handlers = {
20667
21758
  "forget-visit"(msg, _ctx, s) {
20668
21759
  s.routes.forget(msg.repoId, msg.path);
20669
21760
  },
20670
- chat(msg, _ctx, s) {
21761
+ async chat(msg, _ctx, s) {
21762
+ await s.worktrees.archivingNow(msg.worktreeId);
21763
+ if (!s.state.worktree(msg.worktreeId) && s.worktrees.hasArchived(msg.worktreeId)) {
21764
+ await s.worktrees.restore(msg.worktreeId, msg.clientId, { text: msg.text, attachments: msg.attachments });
21765
+ return;
21766
+ }
20671
21767
  requireRun(s, msg.worktreeId);
20672
21768
  const agent = s.runtime.agentFor(msg.worktreeId);
20673
21769
  if (!agent)
@@ -20678,7 +21774,6 @@ var handlers = {
20678
21774
  async "create-worktree"(msg, _ctx, s) {
20679
21775
  await s.worktrees.create(msg.repoId, msg.prompt, {
20680
21776
  createdBy: msg.clientId,
20681
- baseWorktreeId: msg.baseWorktreeId,
20682
21777
  variant: msg.variant,
20683
21778
  context: msg.context,
20684
21779
  attachments: msg.attachments,
@@ -20686,7 +21781,8 @@ var handlers = {
20686
21781
  profile: msg.profile,
20687
21782
  mode: msg.mode,
20688
21783
  model: msg.model,
20689
- effort: msg.effort
21784
+ effort: msg.effort,
21785
+ carry: msg.carry
20690
21786
  });
20691
21787
  },
20692
21788
  "set-worktree-profile"(msg, _ctx, s) {
@@ -20703,7 +21799,6 @@ var handlers = {
20703
21799
  },
20704
21800
  "batch-worktrees"(msg, ctx, s) {
20705
21801
  const repo = s.state.requireRepo(msg.repoId);
20706
- ctx.reply(toast("", true, "batch: planning tasks\u2026"));
20707
21802
  fireAndForget(msg.repoId, (async () => {
20708
21803
  const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
20709
21804
  const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
@@ -20717,22 +21812,18 @@ var handlers = {
20717
21812
  }
20718
21813
  }
20719
21814
  const started = tasks.length - failed;
20720
- ctx.reply(toast("", failed === 0, failed ? `batch: ${started} started, ${failed} failed (see daemon log)` : `batch: ${started} worktree(s) started`));
21815
+ if (failed)
21816
+ ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
20721
21817
  })(), "batch");
20722
21818
  },
20723
- async "remove-worktree"(msg, ctx, s) {
20724
- const archived = await s.worktrees.remove(msg.worktreeId);
20725
- if (archived) {
20726
- ctx.reply(toast(msg.worktreeId, true, `removed ${archived.title}: archived`, {
20727
- ...archived.restorable ? { restoreId: archived.id } : {}
20728
- }));
20729
- }
21819
+ async "archive-worktree"(msg, _ctx, s) {
21820
+ await s.worktrees.archiveWorktree(msg.worktreeId);
20730
21821
  },
20731
21822
  "list-archived"(msg, ctx, s) {
20732
21823
  ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
20733
21824
  },
20734
21825
  async "restore-worktree"(msg, _ctx, s) {
20735
- await s.worktrees.restore(msg.archiveId, msg.clientId);
21826
+ await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
20736
21827
  },
20737
21828
  async "delete-archived"(msg, _ctx, s) {
20738
21829
  await s.worktrees.deleteArchived(msg.archiveId);
@@ -20746,7 +21837,9 @@ var handlers = {
20746
21837
  async "read-file"(msg, ctx, s) {
20747
21838
  const head = { t: "file-read", worktreeId: msg.worktreeId, path: msg.path, ref: msg.ref, seq: msg.seq };
20748
21839
  try {
20749
- ctx.reply({ ...head, ...await s.files.read(msg.worktreeId, msg.path, msg.ref) });
21840
+ const kept = s.worktrees.readable(msg.worktreeId) ? null : await s.worktrees.archivedFile(msg.worktreeId, msg.path, msg.ref);
21841
+ const read = kept ? keptRead(kept.before, kept.after) : await s.files.read(msg.worktreeId, msg.path, msg.ref);
21842
+ ctx.reply({ ...head, ...read });
20750
21843
  } catch (e) {
20751
21844
  if (!(e instanceof UserError))
20752
21845
  log.error(msg.worktreeId, "read-file failed", e);
@@ -20763,36 +21856,54 @@ var handlers = {
20763
21856
  ctx.reply({ t: "git-commit", worktreeId: msg.worktreeId, sha: msg.sha, files });
20764
21857
  },
20765
21858
  async land(msg, ctx, s) {
20766
- const { result, removeIds } = await s.worktrees.land(msg.worktreeId, msg.message);
21859
+ const { result, archiveIds } = await s.worktrees.land(msg.worktreeId, msg.message);
20767
21860
  const suggestion = !result.ok && result.conflict ? "Bring main into this branch and resolve the conflicts, then verify the app still works." : undefined;
20768
21861
  if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
20769
21862
  fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
20770
21863
  }
20771
- await notify(s, ctx, msg.worktreeId, toast(msg.worktreeId, result.ok, result.message, {
21864
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
20772
21865
  merged: result.ok,
20773
21866
  url: result.url,
20774
- removeIds: removeIds ?? [],
21867
+ archiveIds: archiveIds ?? [],
20775
21868
  ...suggestion ? { suggestion } : {}
20776
21869
  }));
20777
21870
  },
20778
21871
  async "sync-main"(msg, ctx, s) {
20779
21872
  const { result, defaultBranch } = await s.worktrees.sync(msg.worktreeId);
20780
21873
  const prompt = !result.ok && result.conflict && s.state.worktree(msg.worktreeId);
20781
- await notify(s, ctx, msg.worktreeId, prompt ? toast(msg.worktreeId, false, `sync conflicts with ${defaultBranch}: prompt prefilled in chat`, {
21874
+ await notify(s, ctx, msg.worktreeId, prompt ? shipped(msg.worktreeId, false, `sync conflicts with ${defaultBranch}: prompt prefilled in chat`, {
20782
21875
  suggestion: `Merge ${defaultBranch} into this branch and resolve the conflicts, then verify the app still works.`
20783
- }) : toast(msg.worktreeId, result.ok, result.message));
21876
+ }) : shipped(msg.worktreeId, result.ok, result.message));
20784
21877
  },
20785
21878
  async "pull-main"(msg, ctx, s) {
20786
21879
  const result = await s.worktrees.pull(msg.worktreeId);
20787
- await notify(s, ctx, msg.worktreeId, toast(msg.worktreeId, result.ok, result.message));
21880
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
21881
+ },
21882
+ "run-after-land"(msg, ctx, s) {
21883
+ const repo = s.state.repo(msg.repoId);
21884
+ if (!repo)
21885
+ throw new UserError("no such project");
21886
+ if ((repo.config.afterLand ?? []).length === 0) {
21887
+ throw new UserError(`${repo.name} has no afterLand commands in ${repo.configFile}`);
21888
+ }
21889
+ s.afterLand.run(msg.repoId);
21890
+ ctx.reply({ t: "self", self: s.self.get() });
21891
+ },
21892
+ "restart-daemon"(_msg, _ctx, s) {
21893
+ const refused = s.restarter.request();
21894
+ if (refused)
21895
+ throw new UserError(refused);
21896
+ },
21897
+ "update-now"(_msg, _ctx, s) {
21898
+ return s.update.updateNow();
20788
21899
  },
20789
21900
  async commit(msg, ctx, s) {
20790
21901
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
20791
- await notify(s, ctx, msg.worktreeId, toast(msg.worktreeId, result.ok, result.message));
21902
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
20792
21903
  },
20793
21904
  async graft(msg, ctx, s) {
20794
- const { target, grafted } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
20795
- await notify(s, ctx, target.id, toast(target.id, true, `grafted ${grafted.join(", ")} into ${target.title}; merged locally, nothing pushed`));
21905
+ const { target } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
21906
+ await gitStatus(s, ctx, target.id);
20796
21907
  },
20797
21908
  async "rename-worktree"(msg, _ctx, s) {
20798
21909
  await s.worktrees.rename(msg.worktreeId, msg.title);
@@ -20804,7 +21915,7 @@ var handlers = {
20804
21915
  fireAndForget(msg.worktreeId, agent.warmCommands(), "warm commands");
20805
21916
  },
20806
21917
  async "list-files"(msg, ctx, s) {
20807
- ctx.reply({ t: "files", worktreeId: msg.worktreeId, paths: await s.files.list(msg.worktreeId) });
21918
+ ctx.reply({ t: "files", worktreeId: msg.worktreeId, ...await s.files.list(msg.worktreeId) });
20808
21919
  },
20809
21920
  async search(msg, ctx, s) {
20810
21921
  const { hits, truncated } = await s.files.search(msg.worktreeId, msg.query);
@@ -20837,9 +21948,8 @@ var handlers = {
20837
21948
  reveal(msg, _ctx, s) {
20838
21949
  s.files.reveal(msg.worktreeId, msg.path);
20839
21950
  },
20840
- async "discard-file"(msg, ctx, s) {
21951
+ async "discard-file"(msg, _ctx, s) {
20841
21952
  await s.files.discard(msg.worktreeId, msg.path);
20842
- ctx.reply(toast(msg.worktreeId, true, `discarded ${msg.path}`));
20843
21953
  s.hub.emit("filesChanged", msg.worktreeId);
20844
21954
  },
20845
21955
  async "write-file"(msg, ctx, s) {
@@ -20856,19 +21966,17 @@ var handlers = {
20856
21966
  }
20857
21967
  },
20858
21968
  async "confirm-config"(msg, _ctx, s) {
20859
- await s.repos.confirmConfig(msg.repoId, msg.config);
21969
+ await s.repos.confirmConfig(msg.repoId, msg.config, msg.kind);
20860
21970
  },
20861
- async "register-repo"(msg, ctx, s) {
20862
- const repo = await s.repos.register(msg.path);
20863
- ctx.reply(toast("", true, `opened ${repo.name}`));
21971
+ async "register-repo"(msg, _ctx, s) {
21972
+ await s.repos.register(msg.path);
20864
21973
  },
20865
- async "create-repo"(msg, ctx, s) {
21974
+ async "create-repo"(msg, _ctx, s) {
20866
21975
  if (msg.mode === "clone") {
20867
21976
  s.repos.startImport({ parent: msg.parent, name: msg.name, url: msg.url ?? "" });
20868
21977
  return;
20869
21978
  }
20870
- const repo = await s.repos.create(msg);
20871
- ctx.reply(toast("", true, `created ${repo.name}`));
21979
+ await s.repos.create(msg);
20872
21980
  },
20873
21981
  async "unmake-repo"(msg, _ctx, s) {
20874
21982
  await s.repos.unmake(msg.repoId);
@@ -20892,10 +22000,8 @@ var handlers = {
20892
22000
  "cancel-folder"(_msg, _ctx, s) {
20893
22001
  s.folderDialog.cancel();
20894
22002
  },
20895
- async "forget-repo"(msg, ctx, s) {
20896
- const name = s.state.requireRepo(msg.repoId).name;
22003
+ async "forget-repo"(msg, _ctx, s) {
20897
22004
  await s.repos.forget(msg.repoId);
20898
- ctx.reply(toast("", true, `forgot ${name}`));
20899
22005
  },
20900
22006
  "set-theme"(msg, _ctx, s) {
20901
22007
  s.themes.setPrefs(msg.prefs);
@@ -21005,6 +22111,10 @@ var handlers = {
21005
22111
  const refs = await s.refs.search(msg.repoId, msg.query);
21006
22112
  ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
21007
22113
  },
22114
+ async "search-chats"(msg, ctx, s) {
22115
+ const { hits, truncated } = await s.chats.search(msg.repoId, msg.query);
22116
+ ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated });
22117
+ },
21008
22118
  async "open-ref"(msg, _ctx, s) {
21009
22119
  await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
21010
22120
  }
@@ -21023,7 +22133,7 @@ async function dispatch(msg, ctx, s) {
21023
22133
  }
21024
22134
 
21025
22135
  // ../daemon/src/server/http.ts
21026
- import { existsSync as existsSync19 } from "fs";
22136
+ import { existsSync as existsSync20 } from "fs";
21027
22137
  import { join as join17 } from "path";
21028
22138
  var IMMUTABLE = "private, max-age=31536000, immutable";
21029
22139
  var NO_STORE = "no-store";
@@ -21061,6 +22171,7 @@ function createFetch(opts) {
21061
22171
  dropped: new Set,
21062
22172
  sent: 0,
21063
22173
  bytes: 0,
22174
+ view: null,
21064
22175
  preview: { handler, data }
21065
22176
  }
21066
22177
  }));
@@ -21069,7 +22180,15 @@ function createFetch(opts) {
21069
22180
  const authed = sameSecret(url.searchParams.get("token"), opts.token);
21070
22181
  if (authed)
21071
22182
  opts.noteShellOrigin(req.headers.get("origin"));
21072
- const data = { authed, subs: new Set, terms: new Set, dropped: new Set, sent: 0, bytes: 0 };
22183
+ const data = {
22184
+ authed,
22185
+ subs: new Set,
22186
+ terms: new Set,
22187
+ dropped: new Set,
22188
+ sent: 0,
22189
+ bytes: 0,
22190
+ view: null
22191
+ };
21073
22192
  const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
21074
22193
  if (srv.upgrade(req, { data, headers }))
21075
22194
  return;
@@ -21085,6 +22204,12 @@ function createFetch(opts) {
21085
22204
  headers["set-cookie"] = grantCookie(grant, remote.host);
21086
22205
  return Response.json(await opts.bootstrap(), { headers });
21087
22206
  }
22207
+ if (url.pathname === "/restart" && req.method === "POST") {
22208
+ if (!sameSecret(url.searchParams.get("token"), opts.token))
22209
+ return new Response("unauthorized", { status: 401 });
22210
+ const refused = opts.restart();
22211
+ return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
22212
+ }
21088
22213
  if (url.pathname === "/register" && req.method === "POST") {
21089
22214
  if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
21090
22215
  return new Response("unauthorized", { status: 401 });
@@ -21106,21 +22231,22 @@ function createFetch(opts) {
21106
22231
  if (!sameSecret(url.searchParams.get("token"), opts.token))
21107
22232
  return new Response("unauthorized", { status: 401 });
21108
22233
  const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
21109
- const path = worktreeId && file && !extra ? opts.attachments.fileFor(worktreeId, file) : null;
21110
- if (!path || !existsSync19(path))
22234
+ const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
22235
+ const path = places.find((p) => !!p && existsSync20(p));
22236
+ if (!path)
21111
22237
  return new Response("not found", { status: 404 });
21112
22238
  return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
21113
22239
  }
21114
22240
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
21115
22241
  const file = join17(opts.shellDist, rel.replaceAll("..", ""));
21116
22242
  const hashed = rel.startsWith("/assets/");
21117
- if (existsSync19(file) && Bun.file(file).size > 0) {
22243
+ if (existsSync20(file) && Bun.file(file).size > 0) {
21118
22244
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
21119
22245
  }
21120
22246
  if (hashed)
21121
22247
  return new Response("not found", { status: 404 });
21122
22248
  const index = join17(opts.shellDist, "index.html");
21123
- if (existsSync19(index))
22249
+ if (existsSync20(index))
21124
22250
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
21125
22251
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
21126
22252
  };
@@ -21214,8 +22340,11 @@ function startServer(opts) {
21214
22340
  lag,
21215
22341
  worktrees: {
21216
22342
  total: s.state.worktrees.filter((w) => w.kind !== "spare").length,
21217
- running: s.runtime.runningCount()
22343
+ running: s.runtime.runningCount(),
22344
+ ...s.runtime.tiers()
21218
22345
  },
22346
+ costs: s.idle.costs(),
22347
+ updates: s.update.status(),
21219
22348
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
21220
22349
  });
21221
22350
  let statusesInFlight = false;
@@ -21320,29 +22449,40 @@ function startServer(opts) {
21320
22449
  const agentsMsg = () => ({
21321
22450
  t: "agents",
21322
22451
  agents: agentInfos(),
21323
- defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID
22452
+ defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
22453
+ agentChosen: s.state.defaultAgent !== undefined
21324
22454
  });
21325
22455
  s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
22456
+ s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
22457
+ s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
21326
22458
  s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
21327
22459
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
21328
- const helloFrame = async () => ({
21329
- t: "hello",
21330
- version,
21331
- protocol: PROTOCOL_VERSION,
21332
- repos: s.state.repos,
21333
- rows: await s.worktrees.rows({ quick: true }),
21334
- spares: s.worktrees.spares(),
21335
- themes: s.themes.themes,
21336
- themePrefs: s.themes.prefs,
21337
- agents: agentInfos(),
21338
- defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
21339
- home: homedir9(),
21340
- folderDialog: process.platform === "darwin" && !cloud.enabled,
21341
- remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
21342
- gitIdentity: await s.repos.gitIdentity(),
21343
- pending: s.repos.pending,
21344
- visits: s.routes.historyAll()
21345
- });
22460
+ s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
22461
+ const helloFrame = async () => {
22462
+ await s.update.refresh();
22463
+ return {
22464
+ t: "hello",
22465
+ version,
22466
+ protocol: PROTOCOL_VERSION,
22467
+ repos: s.state.repos,
22468
+ rows: await s.worktrees.rows({ quick: true }),
22469
+ spares: s.worktrees.spares(),
22470
+ themes: s.themes.themes,
22471
+ themePrefs: s.themes.prefs,
22472
+ agents: agentInfos(),
22473
+ defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
22474
+ agentChosen: s.state.defaultAgent !== undefined,
22475
+ home: homedir9(),
22476
+ folderDialog: process.platform === "darwin" && !cloud.enabled,
22477
+ remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
22478
+ gitIdentity: await s.repos.gitIdentity(),
22479
+ pending: s.repos.pending,
22480
+ visits: s.routes.historyAll(),
22481
+ self: s.self.get(),
22482
+ update: s.update.get(),
22483
+ drafts: s.drafts.all()
22484
+ };
22485
+ };
21346
22486
  let branded = false;
21347
22487
  const serverConfig = {
21348
22488
  hostname: cloud.bindHost,
@@ -21352,12 +22492,14 @@ function startServer(opts) {
21352
22492
  version,
21353
22493
  repos: s.repos,
21354
22494
  attachments: s.attachments,
22495
+ archivedAttachment: (id, file) => s.worktrees.archivedAttachment(id, file),
21355
22496
  branded: () => branded,
21356
22497
  metrics,
21357
22498
  noteShellOrigin: opts.noteShellOrigin,
21358
22499
  remote: opts.remote,
21359
22500
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
21360
- bootstrap: helloFrame
22501
+ bootstrap: helloFrame,
22502
+ restart: () => s.restarter.request()
21361
22503
  }),
21362
22504
  websocket: {
21363
22505
  maxPayloadLength: 64 * 1024 * 1024,
@@ -21377,6 +22519,8 @@ function startServer(opts) {
21377
22519
  close(ws) {
21378
22520
  if (ws.data.preview)
21379
22521
  ws.data.preview.handler.close(ws.data.preview.data);
22522
+ else
22523
+ s.idle.drop(ws.data);
21380
22524
  sockets.delete(ws);
21381
22525
  },
21382
22526
  async message(ws, raw) {
@@ -21409,6 +22553,14 @@ function startServer(opts) {
21409
22553
  },
21410
22554
  unsubscribe: (id) => {
21411
22555
  ws.data.subs.delete(id);
22556
+ if (ws.data.view === id) {
22557
+ ws.data.view = null;
22558
+ s.idle.view(ws.data, null);
22559
+ }
22560
+ },
22561
+ view: (id) => {
22562
+ ws.data.view = id;
22563
+ s.idle.view(ws.data, id);
21412
22564
  },
21413
22565
  watchTerminal: (id, stream) => {
21414
22566
  ws.data.terms.add(streamKey(id, stream));
@@ -21424,7 +22576,13 @@ function startServer(opts) {
21424
22576
  } catch (e) {
21425
22577
  if (!(e instanceof UserError || e instanceof ThemeImportError))
21426
22578
  log.error("ws", `${parsed.msg.t} failed`, e);
21427
- send(ws, { t: "error", message: e instanceof Error ? e.message : String(e) });
22579
+ const m = parsed.msg;
22580
+ const worktreeId = "worktreeId" in m ? m.worktreeId : ("archiveId" in m) ? m.archiveId : undefined;
22581
+ send(ws, {
22582
+ t: "error",
22583
+ message: e instanceof Error ? e.message : String(e),
22584
+ ...worktreeId ? { worktreeId } : {}
22585
+ });
21428
22586
  }
21429
22587
  }
21430
22588
  }
@@ -21448,9 +22606,9 @@ function startServer(opts) {
21448
22606
  }
21449
22607
 
21450
22608
  // ../daemon/src/themes/store.ts
21451
- import { existsSync as existsSync20, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
22609
+ import { existsSync as existsSync21, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
21452
22610
  import { homedir as homedir10 } from "os";
21453
- import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve7 } from "path";
22611
+ import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve8 } from "path";
21454
22612
 
21455
22613
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
21456
22614
  function createScanner(text, ignoreTrivia = false) {
@@ -22354,7 +23512,7 @@ class ThemeStore {
22354
23512
  }
22355
23513
  }
22356
23514
  function readJsonc(file) {
22357
- return parse7(readFileSync12(file, "utf8"), [], { allowTrailingComma: true });
23515
+ return parse7(readFileSync13(file, "utf8"), [], { allowTrailingComma: true });
22358
23516
  }
22359
23517
  function readVscodeTheme(file, depth = 0) {
22360
23518
  const json = readJsonc(file);
@@ -22362,7 +23520,7 @@ function readVscodeTheme(file, depth = 0) {
22362
23520
  throw new ThemeImportError(`${file}: not a JSON object`);
22363
23521
  const t = json;
22364
23522
  if (typeof t.include === "string" && depth < 5) {
22365
- const parent = readVscodeTheme(resolve7(dirname7(file), t.include), depth + 1);
23523
+ const parent = readVscodeTheme(resolve8(dirname7(file), t.include), depth + 1);
22366
23524
  return {
22367
23525
  ...parent,
22368
23526
  ...t,
@@ -22376,10 +23534,10 @@ function readVscodeTheme(file, depth = 0) {
22376
23534
  return t;
22377
23535
  }
22378
23536
  function loadDir(dir) {
22379
- if (!existsSync20(dir))
23537
+ if (!existsSync21(dir))
22380
23538
  return [];
22381
23539
  const out = [];
22382
- for (const f of readdirSync4(dir)) {
23540
+ for (const f of readdirSync5(dir)) {
22383
23541
  if (!/\.jsonc?$/i.test(f))
22384
23542
  continue;
22385
23543
  const file = join18(dir, f);
@@ -22398,12 +23556,12 @@ function loadDir(dir) {
22398
23556
  return out;
22399
23557
  }
22400
23558
  function discover(dir) {
22401
- if (!existsSync20(dir))
23559
+ if (!existsSync21(dir))
22402
23560
  return [];
22403
23561
  const out = [];
22404
23562
  let entries;
22405
23563
  try {
22406
- entries = readdirSync4(dir);
23564
+ entries = readdirSync5(dir);
22407
23565
  } catch {
22408
23566
  return out;
22409
23567
  }
@@ -22411,18 +23569,18 @@ function discover(dir) {
22411
23569
  const extDir = join18(dir, name);
22412
23570
  const pkgFile = join18(extDir, "package.json");
22413
23571
  try {
22414
- if (!statSync5(extDir).isDirectory() || !existsSync20(pkgFile))
23572
+ if (!statSync6(extDir).isDirectory() || !existsSync21(pkgFile))
22415
23573
  continue;
22416
- const pkg = JSON.parse(readFileSync12(pkgFile, "utf8"));
23574
+ const pkg = JSON.parse(readFileSync13(pkgFile, "utf8"));
22417
23575
  const themes = pkg.contributes?.themes;
22418
23576
  if (!Array.isArray(themes))
22419
23577
  continue;
22420
23578
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
22421
23579
  let nls = {};
22422
23580
  const nlsFile = join18(extDir, "package.nls.json");
22423
- if (existsSync20(nlsFile)) {
23581
+ if (existsSync21(nlsFile)) {
22424
23582
  try {
22425
- nls = JSON.parse(readFileSync12(nlsFile, "utf8"));
23583
+ nls = JSON.parse(readFileSync13(nlsFile, "utf8"));
22426
23584
  } catch {}
22427
23585
  }
22428
23586
  for (const c of themes) {
@@ -22431,7 +23589,7 @@ function discover(dir) {
22431
23589
  const m = /^%(.+)%$/.exec(c.label);
22432
23590
  if (m)
22433
23591
  c.label = nls[m[1]] ?? m[1];
22434
- const file = resolve7(extDir, c.path);
23592
+ const file = resolve8(extDir, c.path);
22435
23593
  try {
22436
23594
  const json = readVscodeTheme(file);
22437
23595
  out.push(vscodeToTheme(json, { id: `vscode:${ext}:${slug(c.label)}`, name: c.label, source: "vscode" }));
@@ -22446,6 +23604,334 @@ function discover(dir) {
22446
23604
  return out;
22447
23605
  }
22448
23606
 
23607
+ // ../daemon/src/update/infra.ts
23608
+ import { homedir as homedir11 } from "os";
23609
+ async function latestVersion() {
23610
+ const config = await run("npm", ["config", "get", "registry"], homedir11());
23611
+ const registry = config.ok && config.out !== "" ? config.out : "the npm registry";
23612
+ const r = await run("npm", ["view", "toyon", "version", "--json"], homedir11());
23613
+ if (!r.ok)
23614
+ return { version: null, registry };
23615
+ try {
23616
+ const v = JSON.parse(r.out);
23617
+ return { version: typeof v === "string" ? v : null, registry };
23618
+ } catch {
23619
+ return { version: null, registry };
23620
+ }
23621
+ }
23622
+ var LOG_POINTER = /complete log of this run/i;
23623
+ async function runInstall(command) {
23624
+ const [cmd, ...args] = command;
23625
+ if (!cmd)
23626
+ return { ok: false, line: "nothing to run" };
23627
+ const r = await runLive(cmd, args, homedir11());
23628
+ const lines = `${r.out}
23629
+ ${r.err}`.split(`
23630
+ `).map((l) => l.trim()).filter((l) => l !== "" && !LOG_POINTER.test(l));
23631
+ return { ok: r.ok, line: lines.at(-1) ?? String(r.exit) };
23632
+ }
23633
+
23634
+ // ../daemon/src/update/installed.ts
23635
+ async function readVersion(path) {
23636
+ try {
23637
+ const json = await Bun.file(path).json();
23638
+ return typeof json.version === "string" ? json.version : null;
23639
+ } catch {
23640
+ return null;
23641
+ }
23642
+ }
23643
+
23644
+ // ../daemon/src/update/service.ts
23645
+ var REFRESH_EVERY_MS = 60000;
23646
+ var CHECK_EVERY_MS = 6 * 60 * 60000;
23647
+ var FIRST_CHECK_MS = 2 * 60000;
23648
+ var SETTLED_MS = 2 * 60000;
23649
+ var RETRY_AFTER_MS = 24 * 60 * 60000;
23650
+
23651
+ class UpdateService {
23652
+ d;
23653
+ installed = null;
23654
+ latest = null;
23655
+ unreachable = null;
23656
+ installing = false;
23657
+ failed = null;
23658
+ wanted = false;
23659
+ busyAt;
23660
+ announced;
23661
+ now;
23662
+ constructor(d) {
23663
+ this.d = d;
23664
+ this.now = d.now ?? Date.now;
23665
+ this.busyAt = this.now();
23666
+ this.announced = JSON.stringify(this.get());
23667
+ const every = d.setInterval ?? ((fn, ms) => {
23668
+ setInterval(fn, ms).unref?.();
23669
+ });
23670
+ const later = d.setTimeout ?? ((fn, ms) => {
23671
+ setTimeout(fn, ms).unref?.();
23672
+ });
23673
+ every(() => fireAndForget("update", this.tick(), "update tick"), REFRESH_EVERY_MS);
23674
+ every(() => fireAndForget("update", this.check(), "update check"), CHECK_EVERY_MS);
23675
+ later(() => fireAndForget("update", this.check(), "update check"), FIRST_CHECK_MS);
23676
+ d.hub.on("agentStatus", () => {
23677
+ this.busyAt = this.now();
23678
+ if (this.wanted)
23679
+ fireAndForget("update", this.proceed(), "update");
23680
+ });
23681
+ d.hub.on("holdsChanged", () => {
23682
+ this.busyAt = this.now();
23683
+ });
23684
+ }
23685
+ async tick() {
23686
+ await this.refresh();
23687
+ await this.auto();
23688
+ }
23689
+ async refresh() {
23690
+ const v = await this.d.installed();
23691
+ if (v === null)
23692
+ return;
23693
+ this.installed = v === this.d.running ? null : v;
23694
+ this.announce();
23695
+ }
23696
+ async check() {
23697
+ if (this.d.method === "none" || this.d.managed)
23698
+ return;
23699
+ const answer = await this.d.latest();
23700
+ if (answer.version === null) {
23701
+ this.unreachable = answer.registry;
23702
+ return;
23703
+ }
23704
+ this.unreachable = null;
23705
+ this.latest = newer(answer.version, this.d.running) ? answer.version : null;
23706
+ this.announce();
23707
+ await this.auto();
23708
+ }
23709
+ async updateNow() {
23710
+ if (this.d.managed)
23711
+ throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
23712
+ const target = this.target();
23713
+ if (target === null)
23714
+ throw new UserError("Toyon is up to date");
23715
+ if (this.needsInstall() && this.d.command(target) === null) {
23716
+ throw new UserError(`Run npx toyon@${target} to use it`);
23717
+ }
23718
+ this.failed = null;
23719
+ this.wanted = true;
23720
+ await this.proceed();
23721
+ }
23722
+ get() {
23723
+ const restarting = this.d.restarter.waitingOn() ?? (this.wanted && !this.installing ? this.d.restarter.working() : null);
23724
+ if (this.installed === null && this.latest === null && !this.installing && this.failed === null && restarting === null) {
23725
+ return null;
23726
+ }
23727
+ return {
23728
+ running: this.d.running,
23729
+ latest: this.latest,
23730
+ installed: this.installed,
23731
+ method: this.d.method,
23732
+ installing: this.installing,
23733
+ failed: this.failed,
23734
+ restarting
23735
+ };
23736
+ }
23737
+ status() {
23738
+ return { managed: this.d.managed, unreachable: this.unreachable, latest: this.latest };
23739
+ }
23740
+ async auto() {
23741
+ if (this.wanted || this.installing || this.d.managed)
23742
+ return;
23743
+ const target = this.target();
23744
+ if (target === null)
23745
+ return;
23746
+ if (this.needsInstall() && this.d.command(target) === null)
23747
+ return;
23748
+ const failed = this.d.state.updateFailed;
23749
+ if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS)
23750
+ return;
23751
+ if (this.d.busy()) {
23752
+ this.busyAt = this.now();
23753
+ return;
23754
+ }
23755
+ if (this.now() - this.busyAt < SETTLED_MS)
23756
+ return;
23757
+ log.info("update", `nothing is busy: updating to ${target}`);
23758
+ this.wanted = true;
23759
+ await this.proceed();
23760
+ }
23761
+ target() {
23762
+ if (this.latest && (this.installed === null || newer(this.latest, this.installed)))
23763
+ return this.latest;
23764
+ return this.installed;
23765
+ }
23766
+ needsInstall() {
23767
+ const target = this.target();
23768
+ return target !== null && target !== this.installed;
23769
+ }
23770
+ async proceed() {
23771
+ if (!this.wanted || this.installing)
23772
+ return;
23773
+ if (this.d.restarter.working().length > 0) {
23774
+ this.announce();
23775
+ return;
23776
+ }
23777
+ const target = this.target();
23778
+ const command = target === null ? null : this.d.command(target);
23779
+ if (target !== null && command !== null && this.needsInstall()) {
23780
+ this.installing = true;
23781
+ this.announce();
23782
+ log.info("update", `installing ${target}: ${command.join(" ")}`);
23783
+ const r = await this.d.install(command);
23784
+ this.installing = false;
23785
+ if (!r.ok) {
23786
+ this.wanted = false;
23787
+ this.failed = { version: target, line: r.line, command: command.join(" ") };
23788
+ this.d.state.setUpdateFailed({ version: target, at: this.now() });
23789
+ log.warn("update", `installing ${target} failed: ${r.line}`);
23790
+ this.announce();
23791
+ return;
23792
+ }
23793
+ await this.refresh();
23794
+ }
23795
+ this.wanted = false;
23796
+ const refused = this.d.restarter.request();
23797
+ if (refused) {
23798
+ this.failed = { version: target ?? this.d.running, line: refused, command: "toyon restart" };
23799
+ log.warn("update", `restart refused: ${refused}`);
23800
+ }
23801
+ this.announce();
23802
+ }
23803
+ announce() {
23804
+ const key = JSON.stringify(this.get());
23805
+ if (key === this.announced)
23806
+ return;
23807
+ this.announced = key;
23808
+ this.d.hub.emit("updateChanged");
23809
+ }
23810
+ }
23811
+
23812
+ // ../daemon/src/worktrees/chats.ts
23813
+ var CHAT_HITS_PER_WORKTREE = 20;
23814
+ var CHAT_HITS_MAX = 200;
23815
+ var MIN_QUERY = 2;
23816
+ var SNIPPET = 160;
23817
+ var BEFORE = 50;
23818
+ var bunFs2 = {
23819
+ async stat(path) {
23820
+ try {
23821
+ const s = await Bun.file(path).stat();
23822
+ return { size: s.size, mtimeMs: s.mtimeMs };
23823
+ } catch {
23824
+ return null;
23825
+ }
23826
+ },
23827
+ async text(path) {
23828
+ try {
23829
+ return await Bun.file(path).text();
23830
+ } catch {
23831
+ return null;
23832
+ }
23833
+ }
23834
+ };
23835
+ var flat = (text) => text.replace(/\s+/g, " ").trim();
23836
+ function saidRows(entries) {
23837
+ const out = [];
23838
+ let turnTs = 0;
23839
+ const push = (seq, role, said, ts) => {
23840
+ const text = flat(said);
23841
+ if (text)
23842
+ out.push({ seq, role, text, lower: text.toLowerCase(), ts });
23843
+ };
23844
+ for (const { seq, event } of coalesce(entries)) {
23845
+ if (event.type === "user-message") {
23846
+ turnTs = event.ts;
23847
+ push(seq, "user", event.text, event.ts);
23848
+ } else if (event.type === "turn-start") {
23849
+ turnTs = event.ts;
23850
+ } else if (event.type === "text-delta") {
23851
+ push(seq, "assistant", event.text, turnTs);
23852
+ }
23853
+ }
23854
+ return out;
23855
+ }
23856
+ function snippet(text, at, length) {
23857
+ const start = Math.max(0, Math.min(at - BEFORE, text.length - SNIPPET));
23858
+ const end = Math.min(text.length, start + SNIPPET);
23859
+ const head = start > 0 ? "\u2026" : "";
23860
+ const tail = end < text.length ? "\u2026" : "";
23861
+ return {
23862
+ text: `${head}${text.slice(start, end)}${tail}`,
23863
+ match: [at - start + head.length, Math.min(length, end - at)]
23864
+ };
23865
+ }
23866
+ function searchRows(rows, needle, chat, limit = CHAT_HITS_PER_WORKTREE) {
23867
+ const hits = [];
23868
+ for (let i = rows.length - 1;i >= 0; i--) {
23869
+ const row = rows[i];
23870
+ const at = row.lower.indexOf(needle);
23871
+ if (at < 0)
23872
+ continue;
23873
+ if (hits.length === limit)
23874
+ return { hits, more: true };
23875
+ hits.push({ ...chat, seq: row.seq, role: row.role, ts: row.ts, ...snippet(row.text, at, needle.length) });
23876
+ }
23877
+ return { hits, more: false };
23878
+ }
23879
+
23880
+ class ChatSearch {
23881
+ d;
23882
+ said = new Map;
23883
+ constructor(d) {
23884
+ this.d = d;
23885
+ }
23886
+ async search(repoId, query) {
23887
+ this.d.state.requireRepo(repoId);
23888
+ const needle = flat(query).toLowerCase();
23889
+ if (needle.length < MIN_QUERY)
23890
+ return { hits: [], truncated: false };
23891
+ const chats = [
23892
+ ...this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree").map((w) => ({ id: w.id, archived: false, path: this.d.transcriptPath(w.id) })),
23893
+ ...this.d.archivedChats(repoId).map((a) => ({ id: a.id, archived: true, path: a.transcript }))
23894
+ ];
23895
+ const read = new Set;
23896
+ const found = await Promise.all(chats.map(async (c) => {
23897
+ const rows = await this.rowsOf(c.id, c.archived, c.path, read);
23898
+ return searchRows(rows, needle, { worktreeId: c.id, archived: c.archived });
23899
+ }));
23900
+ for (const key of this.said.keys())
23901
+ if (!read.has(key))
23902
+ this.said.delete(key);
23903
+ const all = found.filter((f) => f.hits.length > 0).sort((a, b) => b.hits[0].ts - a.hits[0].ts).flatMap((f) => f.hits);
23904
+ return { hits: all.slice(0, CHAT_HITS_MAX), truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more) };
23905
+ }
23906
+ async rowsOf(id, archived, path, read) {
23907
+ const live = archived ? undefined : this.d.live(id);
23908
+ if (live) {
23909
+ const key = `live:${id}`;
23910
+ read.add(key);
23911
+ return this.remember(key, `n:${live.length}`, () => saidRows(live));
23912
+ }
23913
+ read.add(path);
23914
+ const fs = this.d.fs ?? bunFs2;
23915
+ const at = await fs.stat(path);
23916
+ if (!at)
23917
+ return [];
23918
+ const sig = `${at.mtimeMs}:${at.size}`;
23919
+ const known = this.said.get(path);
23920
+ if (known?.sig === sig)
23921
+ return known.rows;
23922
+ const text = await fs.text(path);
23923
+ return text === null ? [] : this.remember(path, sig, () => saidRows(parseTranscript(text, id)));
23924
+ }
23925
+ remember(key, sig, rows) {
23926
+ const known = this.said.get(key);
23927
+ if (known?.sig === sig)
23928
+ return known.rows;
23929
+ const fresh = rows();
23930
+ this.said.set(key, { sig, rows: fresh });
23931
+ return fresh;
23932
+ }
23933
+ }
23934
+
22449
23935
  // ../daemon/src/worktrees/landing.ts
22450
23936
  var TAIL_CHARS = 400;
22451
23937
  var DIFF_CHARS = 2000;
@@ -22918,11 +24404,11 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
22918
24404
  }
22919
24405
 
22920
24406
  // ../daemon/src/worktrees/service.ts
22921
- import { existsSync as existsSync22, lstatSync, readFileSync as readFileSync14, readlinkSync, rmSync as rmSync3, symlinkSync, unlinkSync } from "fs";
24407
+ import { existsSync as existsSync23, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
22922
24408
  import { basename as basename6, dirname as dirname8, join as join21 } from "path";
22923
24409
 
22924
24410
  // ../daemon/src/git/archive.ts
22925
- import { rmSync } from "fs";
24411
+ import { rmSync as rmSync3 } from "fs";
22926
24412
  var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
22927
24413
  async function keepState(repoPath, wtPath, ref, indexFile) {
22928
24414
  const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
@@ -22959,7 +24445,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
22959
24445
  ], wtPath);
22960
24446
  return commit.ok ? commit.out : null;
22961
24447
  } finally {
22962
- rmSync(indexFile, { force: true });
24448
+ rmSync3(indexFile, { force: true });
22963
24449
  }
22964
24450
  }
22965
24451
  async function checkOutKept(repoPath, wtPath, kept, branch) {
@@ -22975,6 +24461,24 @@ async function commitOf(repoPath, ref) {
22975
24461
  const r = await git(repoPath, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`);
22976
24462
  return r.ok ? r.out : null;
22977
24463
  }
24464
+ var landRef = (worktreeId, n) => `refs/toyon/lands/${worktreeId}/${n}`;
24465
+ async function landingMark(worktreePath, defaultBr) {
24466
+ const [tip, base] = await Promise.all([
24467
+ git(worktreePath, "rev-parse", "HEAD"),
24468
+ git(worktreePath, "merge-base", "HEAD", defaultBr)
24469
+ ]);
24470
+ if (!tip.ok || !base.ok || !tip.out || tip.out === base.out)
24471
+ return null;
24472
+ return { base: base.out, tip: tip.out };
24473
+ }
24474
+ async function dropLandRefs(repoPath, worktreeId) {
24475
+ const r = await git(repoPath, "for-each-ref", "--format=%(refname)", `refs/toyon/lands/${worktreeId}/`);
24476
+ if (!r.ok || !r.out)
24477
+ return;
24478
+ for (const ref of r.out.split(`
24479
+ `).filter(Boolean))
24480
+ await git(repoPath, "update-ref", "-d", ref);
24481
+ }
22978
24482
 
22979
24483
  // ../daemon/src/git/land.ts
22980
24484
  async function commitWorktree(worktreePath, message) {
@@ -23186,42 +24690,8 @@ function compareUrl(remoteUrl, base, branch) {
23186
24690
  return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
23187
24691
  }
23188
24692
 
23189
- // ../daemon/src/runtime/setup.ts
23190
- var SETUP_RING = 64 * 1024;
23191
- var SETUP_COLS = 120;
23192
- var SETUP_ROWS = 30;
23193
- function runSetup(cmd, cwd, onLine, extra = {}) {
23194
- return new Promise((resolve) => {
23195
- const lines = new LineSplitter;
23196
- try {
23197
- new PtyStream({
23198
- cwd,
23199
- env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
23200
- cols: SETUP_COLS,
23201
- rows: SETUP_ROWS,
23202
- file: "sh",
23203
- args: ["-c", cmd],
23204
- ring: SETUP_RING
23205
- }, (data) => {
23206
- for (const line of lines.feed(data))
23207
- onLine(line);
23208
- }, resolve);
23209
- } catch (e) {
23210
- onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
23211
- resolve(1);
23212
- }
23213
- });
23214
- }
23215
- function envStrings2(base) {
23216
- const env = {};
23217
- for (const [k, v] of Object.entries(base))
23218
- if (typeof v === "string")
23219
- env[k] = v;
23220
- return env;
23221
- }
23222
-
23223
24693
  // ../daemon/src/worktrees/archive.ts
23224
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync13, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
24694
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
23225
24695
  import { join as join19 } from "path";
23226
24696
  class WorktreeArchive {
23227
24697
  dir;
@@ -23229,12 +24699,12 @@ class WorktreeArchive {
23229
24699
  constructor(dir) {
23230
24700
  this.dir = dir;
23231
24701
  mkdirSync6(dir, { recursive: true });
23232
- for (const id of readdirSync5(dir)) {
24702
+ for (const id of readdirSync6(dir)) {
23233
24703
  const file = this.filesOf(id).record;
23234
- if (!existsSync21(file))
24704
+ if (!existsSync22(file))
23235
24705
  continue;
23236
24706
  try {
23237
- this.records.set(id, JSON.parse(readFileSync13(file, "utf8")));
24707
+ this.records.set(id, JSON.parse(readFileSync14(file, "utf8")));
23238
24708
  } catch (e) {
23239
24709
  log.warn("archive", `skipped an unreadable record: ${file}`, e);
23240
24710
  }
@@ -23262,12 +24732,18 @@ class WorktreeArchive {
23262
24732
  delete(id) {
23263
24733
  if (!this.records.has(id))
23264
24734
  return;
23265
- rmSync2(this.filesOf(id).dir, { recursive: true, force: true });
24735
+ rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
23266
24736
  this.records.delete(id);
23267
24737
  }
23268
24738
  list(repo) {
23269
24739
  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));
23270
24740
  }
24741
+ chatFiles(id) {
24742
+ if (!this.records.has(id))
24743
+ return;
24744
+ const at = this.filesOf(id);
24745
+ return { transcript: at.transcript, attachments: at.attachments };
24746
+ }
23271
24747
  filesOf(id) {
23272
24748
  const dir = join19(this.dir, id);
23273
24749
  return {
@@ -23284,18 +24760,37 @@ function summarize(r, repoId) {
23284
24760
  repoId,
23285
24761
  title: r.worktree.title,
23286
24762
  branch: r.worktree.branch,
24763
+ path: r.worktree.path,
23287
24764
  createdAt: r.worktree.createdAt,
23288
24765
  archivedAt: r.archivedAt,
23289
24766
  ...r.prompt ? { prompt: r.prompt } : {},
23290
24767
  restorable: !!r.kept,
23291
24768
  ...r.kept?.snapshot ? { uncommitted: true } : {},
23292
- ...r.worktree.landed ? { landed: true } : {}
24769
+ ...r.worktree.landed ? { landed: true } : {},
24770
+ ...r.cost !== undefined ? { cost: r.cost } : {},
24771
+ ...r.auto ? { auto: r.auto } : {}
23293
24772
  };
23294
24773
  }
24774
+ function lastUsage(transcript) {
24775
+ if (!existsSync22(transcript))
24776
+ return null;
24777
+ const lines = readFileSync14(transcript, "utf8").split(`
24778
+ `);
24779
+ for (let i = lines.length - 1;i >= 0; i--) {
24780
+ if (!lines[i]?.includes('"usage"'))
24781
+ continue;
24782
+ try {
24783
+ const e = JSON.parse(lines[i]).event;
24784
+ if (e?.type === "usage")
24785
+ return { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
24786
+ } catch {}
24787
+ }
24788
+ return null;
24789
+ }
23295
24790
  function firstPrompt(transcript) {
23296
- if (!existsSync21(transcript))
24791
+ if (!existsSync22(transcript))
23297
24792
  return;
23298
- for (const line of readFileSync13(transcript, "utf8").split(`
24793
+ for (const line of readFileSync14(transcript, "utf8").split(`
23299
24794
  `)) {
23300
24795
  if (!line.includes('"user-message"'))
23301
24796
  continue;
@@ -23308,12 +24803,69 @@ function firstPrompt(transcript) {
23308
24803
  return;
23309
24804
  }
23310
24805
  function move(from, to) {
23311
- if (!existsSync21(from))
24806
+ if (!existsSync22(from))
23312
24807
  return;
23313
- rmSync2(to, { recursive: true, force: true });
24808
+ rmSync4(to, { recursive: true, force: true });
23314
24809
  renameSync4(from, to);
23315
24810
  }
23316
24811
 
24812
+ // ../daemon/src/worktrees/archivedGit.ts
24813
+ class ArchivedGit {
24814
+ repoPath;
24815
+ kept;
24816
+ lands;
24817
+ defaultBr;
24818
+ constructor(repoPath, kept, lands, defaultBr) {
24819
+ this.repoPath = repoPath;
24820
+ this.kept = kept;
24821
+ this.lands = lands;
24822
+ this.defaultBr = defaultBr;
24823
+ }
24824
+ async tailBase() {
24825
+ const r = await git(this.repoPath, "merge-base", this.kept.head, this.defaultBr);
24826
+ return r.ok && r.out ? r.out : this.kept.head;
24827
+ }
24828
+ async status() {
24829
+ const base = await this.tailBase();
24830
+ const { head, snapshot } = this.kept;
24831
+ const [files, committed] = await Promise.all([
24832
+ snapshot ? filesBetween(this.repoPath, head, snapshot) : [],
24833
+ base === head ? [] : filesBetween(this.repoPath, base, head)
24834
+ ]);
24835
+ return { files, committed, head: snapshot ?? head };
24836
+ }
24837
+ async log() {
24838
+ const seen = new Set;
24839
+ const unseen = (list) => {
24840
+ const out = list.filter((c) => !seen.has(c.sha));
24841
+ for (const c of out)
24842
+ seen.add(c.sha);
24843
+ return out;
24844
+ };
24845
+ const landed = [];
24846
+ for (const land of this.lands) {
24847
+ const commits = unseen(await logRange(this.repoPath, land.base, land.tip));
24848
+ landed.push(commits.map((c) => ({ ...c, landedAt: land.at })));
24849
+ }
24850
+ const base = await this.tailBase();
24851
+ const tail = base === this.kept.head ? [] : unseen(await logRange(this.repoPath, base, this.kept.head));
24852
+ return [...tail, ...landed.reverse().flat()];
24853
+ }
24854
+ commitFiles(sha) {
24855
+ return commitFiles(this.repoPath, sha);
24856
+ }
24857
+ async file(path, ref) {
24858
+ if (ref)
24859
+ return fileAtCommit(this.repoPath, ref, path);
24860
+ const base = await this.tailBase();
24861
+ const [before, after] = await Promise.all([
24862
+ gitRaw(this.repoPath, "show", `${base}:${path}`),
24863
+ gitRaw(this.repoPath, "show", `${this.kept.snapshot ?? this.kept.head}:${path}`)
24864
+ ]);
24865
+ return { before: before.ok ? before.out : "", after: after.ok ? after.out : "" };
24866
+ }
24867
+ }
24868
+
23317
24869
  // ../daemon/src/worktrees/spare.ts
23318
24870
  import { join as join20 } from "path";
23319
24871
  class SparePool {
@@ -23329,7 +24881,7 @@ class SparePool {
23329
24881
  adopt(repoId) {
23330
24882
  const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
23331
24883
  for (const extra of persisted.slice(1))
23332
- fireAndForget(extra.id, this.d.remove(extra.id), "stale spare removal");
24884
+ fireAndForget(extra.id, this.d.discard(extra.id), "stale spare removal");
23333
24885
  const spare = persisted[0];
23334
24886
  if (!spare)
23335
24887
  return;
@@ -23386,7 +24938,7 @@ class SparePool {
23386
24938
  log.warn(repoId, "spare warm-up failed; rolling back", e);
23387
24939
  this.spares.delete(repoId);
23388
24940
  if (entry.worktreeId && this.d.state.worktree(entry.worktreeId)) {
23389
- await this.d.remove(entry.worktreeId).catch((re) => log.warn(repoId, "spare rollback failed", re));
24941
+ await this.d.discard(entry.worktreeId).catch((re) => log.warn(repoId, "spare rollback failed", re));
23390
24942
  }
23391
24943
  }
23392
24944
  }
@@ -23442,12 +24994,14 @@ class SparePool {
23442
24994
  wt.title = slug;
23443
24995
  wt.createdAt = Date.now();
23444
24996
  this.d.state.save();
24997
+ fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
23445
24998
  fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
23446
24999
  return wt;
23447
25000
  }
23448
25001
  }
23449
25002
 
23450
25003
  // ../daemon/src/worktrees/turns.ts
25004
+ var TURN_HOLD = "turn";
23451
25005
  var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
23452
25006
 
23453
25007
  class TurnService {
@@ -23485,11 +25039,14 @@ class TurnService {
23485
25039
  onStatus(worktreeId, status) {
23486
25040
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
23487
25041
  this.lastStatus.set(worktreeId, status);
23488
- if (status === "working")
25042
+ if (status === "working") {
23489
25043
  this.disarm(worktreeId);
25044
+ this.d.hold?.(worktreeId, TURN_HOLD);
25045
+ }
23490
25046
  const edge = stopOf(prev, status);
23491
25047
  if (!edge)
23492
25048
  return;
25049
+ this.d.release?.(worktreeId, TURN_HOLD);
23493
25050
  const wt = this.d.state.worktree(worktreeId);
23494
25051
  if (!wt || wt.kind === "spare")
23495
25052
  return;
@@ -23587,9 +25144,6 @@ function stopOf(prev, next) {
23587
25144
  return "failed";
23588
25145
  return null;
23589
25146
  }
23590
- function isUnseen(wt) {
23591
- return wt.unread === true || wt.lastTurn != null && (wt.seenAt == null || wt.seenAt < wt.lastTurn.at);
23592
- }
23593
25147
 
23594
25148
  // ../daemon/src/worktrees/service.ts
23595
25149
  var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
@@ -23599,12 +25153,22 @@ function withoutAttachments(event) {
23599
25153
  const { attachments: _attachments, ...rest } = event;
23600
25154
  return rest;
23601
25155
  }
25156
+ function withCarry(context, c) {
25157
+ if (c.count === 0)
25158
+ return context;
25159
+ const files = `${c.count} uncommitted ${c.count === 1 ? "file" : "files"}`;
25160
+ 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.]`;
25161
+ return context ? `${context}
25162
+
25163
+ ${line}` : line;
25164
+ }
23602
25165
  var FETCH_EVERY_MS = 5 * 60000;
23603
25166
 
23604
25167
  class WorktreeService {
23605
25168
  d;
23606
25169
  spare;
23607
25170
  archive;
25171
+ archiving = new Map;
23608
25172
  countsCache = new Map;
23609
25173
  discoverCache = new Map;
23610
25174
  usage = new Map;
@@ -23618,8 +25182,8 @@ class WorktreeService {
23618
25182
  runtime: d.runtime,
23619
25183
  paths: d.paths,
23620
25184
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
23621
- remove: async (id) => {
23622
- await this.remove(id, { spare: true });
25185
+ discard: async (id) => {
25186
+ await this.discardWorktree(id);
23623
25187
  }
23624
25188
  });
23625
25189
  for (const wt of d.state.worktrees)
@@ -23647,46 +25211,48 @@ class WorktreeService {
23647
25211
  const repo = this.d.state.requireRepo(repoId);
23648
25212
  const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
23649
25213
  const profile = this.checkProfile(repo, opts.profile);
23650
- let slug = variant ? `${slugify2(prompt, false)}-v${variant.index}` : slugify2(prompt);
25214
+ const task = taskText(prompt, attachments);
25215
+ let slug = variant ? `${slugify2(task, false)}-v${variant.index}` : slugify2(task);
23651
25216
  if (variant && (await git(repo.path, "show-ref", "--verify", `refs/heads/toyon/${slug}`)).ok) {
23652
25217
  slug = `${slug}-${shortId().slice(0, 3)}`;
23653
25218
  }
23654
25219
  const branch = `toyon/${slug}`;
23655
- const base = opts.baseWorktreeId ? this.d.state.worktree(opts.baseWorktreeId) : undefined;
23656
- const fromMain = !base || base.kind === "main";
23657
- const agentPrompt = variant && variant.of >= 2 ? `${prompt}
25220
+ if (opts.carry && variant && variant.of > 1) {
25221
+ throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
25222
+ }
25223
+ const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
23658
25224
 
23659
- ${variantLens(variant.index)}` : prompt;
23660
- if (fromMain) {
23661
- const claimed = await this.spare.claim(repoId, branch, slug);
23662
- if (claimed) {
23663
- if (variant)
23664
- claimed.variant = variant;
23665
- if (opts.createdBy)
23666
- claimed.createdBy = opts.createdBy;
23667
- claimed.promptedAt = claimed.createdAt;
23668
- claimed.agent = agent;
23669
- if (opts.mode)
23670
- claimed.mode = opts.mode;
23671
- if (opts.model)
23672
- claimed.model = opts.model;
23673
- if (opts.effort)
23674
- claimed.effort = opts.effort;
23675
- this.refreshLink(claimed);
23676
- if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
23677
- claimed.profile = profile;
23678
- this.restartProcs(claimed, repo);
23679
- }
23680
- this.d.state.save();
23681
- this.d.hub.emit("worktreesChanged");
23682
- this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context, attachments });
23683
- this.scheduleNaming(claimed, prompt, variant);
23684
- return claimed;
25225
+ `) : prompt;
25226
+ const claimed = await this.spare.claim(repoId, branch, slug);
25227
+ if (claimed) {
25228
+ if (variant)
25229
+ claimed.variant = variant;
25230
+ if (opts.createdBy)
25231
+ claimed.createdBy = opts.createdBy;
25232
+ claimed.promptedAt = claimed.createdAt;
25233
+ claimed.agent = agent;
25234
+ if (opts.mode)
25235
+ claimed.mode = opts.mode;
25236
+ if (opts.model)
25237
+ claimed.model = opts.model;
25238
+ if (opts.effort)
25239
+ claimed.effort = opts.effort;
25240
+ this.refreshLink(claimed);
25241
+ if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
25242
+ claimed.profile = profile;
25243
+ this.restartProcs(claimed, repo);
23685
25244
  }
25245
+ const carried = await this.carryMain(repo, claimed, !!opts.carry);
25246
+ this.d.state.save();
25247
+ this.d.hub.emit("worktreesChanged");
25248
+ this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
25249
+ this.scheduleNaming(claimed, task, variant);
25250
+ if (carried.unmoved)
25251
+ throw new UserError(carried.unmoved);
25252
+ return claimed;
23686
25253
  }
23687
25254
  const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
23688
- const baseBranch = fromMain ? repo.defaultBranch : base.branch;
23689
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, baseBranch));
25255
+ await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
23690
25256
  const wt = {
23691
25257
  id: shortId(),
23692
25258
  repoId,
@@ -23705,11 +25271,56 @@ ${variantLens(variant.index)}` : prompt;
23705
25271
  ...opts.model ? { model: opts.model } : {},
23706
25272
  ...opts.effort ? { effort: opts.effort } : {}
23707
25273
  };
23708
- this.launch(wt, repo, base?.path ?? repo.path);
23709
- this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context, attachments });
23710
- this.scheduleNaming(wt, prompt, variant);
25274
+ const carried = await this.carryMain(repo, wt, !!opts.carry);
25275
+ this.launch(wt, repo, repo.path);
25276
+ this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
25277
+ this.scheduleNaming(wt, task, variant);
25278
+ if (carried.unmoved)
25279
+ throw new UserError(carried.unmoved);
23711
25280
  return wt;
23712
25281
  }
25282
+ async carryMain(repo, wt, move) {
25283
+ const main = repo.defaultBranch;
25284
+ if (!move)
25285
+ return { branch: main, moved: false, count: (await statusFiles(repo.path)).length };
25286
+ let count = 0;
25287
+ const unmoved = await withRepoLock(repo.path, async () => {
25288
+ count = (await statusFiles(repo.path)).length;
25289
+ if (count === 0)
25290
+ return;
25291
+ const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
25292
+ const pushed = await git(repo.path, "stash", "push", "--include-untracked", "-m", `toyon: into ${wt.title}`);
25293
+ if (!pushed.ok)
25294
+ return `the changes on ${main} could not be moved: ${pushed.err.trim()}`;
25295
+ const after = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
25296
+ const sha = after.out.trim();
25297
+ if (!after.ok || before.ok && before.out.trim() === sha)
25298
+ return;
25299
+ const entry = async () => {
25300
+ const list = await git(repo.path, "stash", "list", "--format=%H");
25301
+ const i = list.out.split(`
25302
+ `).indexOf(sha);
25303
+ return i < 0 ? null : `stash@{${i}}`;
25304
+ };
25305
+ const applied = await git(wt.path, "stash", "apply", sha);
25306
+ if (applied.ok) {
25307
+ const ref = await entry();
25308
+ if (ref)
25309
+ await git(repo.path, "stash", "drop", ref);
25310
+ return;
25311
+ }
25312
+ log.warn(wt.id, `moving ${main}'s changes failed`, applied.err);
25313
+ await git(wt.path, "reset", "--hard");
25314
+ await git(wt.path, "clean", "-fd");
25315
+ const ref = await entry();
25316
+ const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
25317
+ 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}"`;
25318
+ });
25319
+ const mainWt = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
25320
+ if (mainWt)
25321
+ this.countsCache.delete(mainWt.id);
25322
+ return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
25323
+ }
23713
25324
  launch(wt, repo, depsSource, opts = {}) {
23714
25325
  this.d.state.addWorktree(wt);
23715
25326
  this.d.hub.emit("worktreesChanged");
@@ -23724,7 +25335,7 @@ ${variantLens(variant.index)}` : prompt;
23724
25335
  const branch = kind === "pr" ? `pr/${number}` : ref;
23725
25336
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
23726
25337
  let slug = title;
23727
- if (existsSync22(join21(this.d.paths.worktreesDir, repo.name, slug)))
25338
+ if (existsSync23(join21(this.d.paths.worktreesDir, repo.name, slug)))
23728
25339
  slug = `${slug}-${shortId().slice(0, 4)}`;
23729
25340
  const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
23730
25341
  await withRepoLock(repo.path, async () => {
@@ -23859,6 +25470,8 @@ ${variantLens(variant.index)}` : prompt;
23859
25470
  this.d.hub.emit("worktreesChanged");
23860
25471
  }
23861
25472
  scheduleNaming(wt, prompt, variant) {
25473
+ if (!prompt.trim())
25474
+ return;
23862
25475
  if (!variant) {
23863
25476
  fireAndForget(wt.id, (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt).then((name) => {
23864
25477
  if (name)
@@ -23878,12 +25491,32 @@ ${variantLens(variant.index)}` : prompt;
23878
25491
  }
23879
25492
  }), "auto-naming");
23880
25493
  }
23881
- async remove(worktreeId, opts = {}) {
25494
+ archiveWorktree(worktreeId, opts = {}) {
25495
+ const running = this.archiving.get(worktreeId);
25496
+ if (running)
25497
+ return running;
23882
25498
  const wt = this.d.state.worktree(worktreeId);
23883
- if (!wt || !(canRemove(wt) || opts.spare && wt.kind === "spare"))
23884
- return null;
25499
+ if (!wt || !canArchive(wt))
25500
+ return Promise.resolve(null);
25501
+ if (opts.still && !opts.still())
25502
+ return Promise.resolve(null);
25503
+ const done = this.takeDown(wt, true, opts.reason).finally(() => this.archiving.delete(worktreeId));
25504
+ this.archiving.set(worktreeId, done);
25505
+ return done;
25506
+ }
25507
+ archivingNow(worktreeId) {
25508
+ return this.archiving.get(worktreeId);
25509
+ }
25510
+ async discardWorktree(worktreeId) {
25511
+ const wt = this.d.state.worktree(worktreeId);
25512
+ if (!wt || !(canArchive(wt) || wt.kind === "spare"))
25513
+ return;
25514
+ await this.takeDown(wt, false);
25515
+ this.d.drafts?.drop(worktreeId);
25516
+ }
25517
+ async takeDown(wt, archive, reason) {
25518
+ const worktreeId = wt.id;
23885
25519
  const repo = this.d.state.requireRepo(wt.repoId);
23886
- const archive = wt.kind !== "spare" && opts.archive !== false;
23887
25520
  await this.d.runtime.stop(worktreeId);
23888
25521
  const kept = await withRepoLock(repo.path, async () => {
23889
25522
  const k = archive ? await this.keep(repo, wt) : null;
@@ -23901,9 +25534,12 @@ ${variantLens(variant.index)}` : prompt;
23901
25534
  releasePort(wt.proxyPort);
23902
25535
  let archived = null;
23903
25536
  if (archive)
23904
- archived = this.archiveChat(wt, repo, kept, sessionId);
23905
- else
25537
+ archived = this.archiveChat(wt, repo, kept, sessionId, reason);
25538
+ else {
23906
25539
  this.deleteChat(worktreeId);
25540
+ if (wt.lands?.length)
25541
+ await dropLandRefs(repo.path, worktreeId);
25542
+ }
23907
25543
  this.d.hub.emit("worktreesChanged");
23908
25544
  return archived;
23909
25545
  }
@@ -23916,16 +25552,19 @@ ${variantLens(variant.index)}` : prompt;
23916
25552
  log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
23917
25553
  return kept;
23918
25554
  }
23919
- archiveChat(wt, repo, kept, sessionId) {
25555
+ archiveChat(wt, repo, kept, sessionId, reason) {
23920
25556
  const files = this.chatFiles(wt.id);
23921
25557
  const prompt = firstPrompt(files.transcript);
25558
+ const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
23922
25559
  const rec = {
23923
25560
  worktree: wt,
23924
25561
  repoPath: repo.path,
23925
25562
  archivedAt: Date.now(),
23926
25563
  ...sessionId ? { sessionId } : {},
23927
25564
  ...prompt ? { prompt } : {},
23928
- ...kept ? { kept } : {}
25565
+ ...cost !== undefined ? { cost } : {},
25566
+ ...kept ? { kept } : {},
25567
+ ...reason ? { auto: reason } : {}
23929
25568
  };
23930
25569
  try {
23931
25570
  this.archive.put(rec, files);
@@ -23945,8 +25584,8 @@ ${variantLens(variant.index)}` : prompt;
23945
25584
  deleteChat(worktreeId) {
23946
25585
  const files = this.chatFiles(worktreeId);
23947
25586
  try {
23948
- rmSync3(files.transcript, { force: true });
23949
- rmSync3(files.attachments, { recursive: true, force: true });
25587
+ rmSync5(files.transcript, { force: true });
25588
+ rmSync5(files.attachments, { recursive: true, force: true });
23950
25589
  } catch (e) {
23951
25590
  log.warn(worktreeId, "could not delete transcript or attachments", e);
23952
25591
  }
@@ -23971,14 +25610,56 @@ ${variantLens(variant.index)}` : prompt;
23971
25610
  });
23972
25611
  this.archiveChat(wt, repo, kept, sessionId);
23973
25612
  }
25613
+ hasArchived(archiveId) {
25614
+ return !!this.archive.get(archiveId);
25615
+ }
23974
25616
  archived(repoId) {
23975
25617
  const repo = this.d.state.repo(repoId);
23976
25618
  return repo ? this.archive.list(repo) : [];
23977
25619
  }
25620
+ archivedChats(repoId) {
25621
+ const repo = this.d.state.repo(repoId);
25622
+ if (!repo)
25623
+ return [];
25624
+ return this.archive.list(repo).flatMap((a) => {
25625
+ const files = this.archive.chatFiles(a.id);
25626
+ return files ? [{ id: a.id, transcript: files.transcript }] : [];
25627
+ });
25628
+ }
25629
+ archivedTranscript(archiveId) {
25630
+ const files = this.archive.chatFiles(archiveId);
25631
+ return files ? new Transcript(files.transcript, archiveId).entries : null;
25632
+ }
25633
+ archivedAttachment(archiveId, file) {
25634
+ const files = this.archive.chatFiles(archiveId);
25635
+ return files && isAttachmentFile(file) ? join21(files.attachments, file) : null;
25636
+ }
25637
+ archivedGit(archiveId) {
25638
+ const rec = this.archive.get(archiveId);
25639
+ const repo = rec && this.repoOf(rec);
25640
+ if (!rec?.kept || !repo)
25641
+ return null;
25642
+ return new ArchivedGit(repo.path, rec.kept, rec.worktree.lands ?? [], repo.defaultBranch);
25643
+ }
25644
+ async archivedStatus(archiveId) {
25645
+ const kept = this.archivedGit(archiveId);
25646
+ if (!kept)
25647
+ return null;
25648
+ try {
25649
+ return await kept.status();
25650
+ } catch (e) {
25651
+ log.warn(archiveId, "reading its kept changes failed", e);
25652
+ return null;
25653
+ }
25654
+ }
25655
+ async archivedFile(archiveId, path, ref) {
25656
+ const kept = this.archivedGit(archiveId);
25657
+ return kept ? kept.file(path, ref) : null;
25658
+ }
23978
25659
  repoOf(rec) {
23979
25660
  return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
23980
25661
  }
23981
- async restore(archiveId, createdBy) {
25662
+ async restore(archiveId, createdBy, message) {
23982
25663
  const rec = this.archive.get(archiveId);
23983
25664
  if (!rec)
23984
25665
  throw new UserError("that archived worktree is gone");
@@ -23989,7 +25670,9 @@ ${variantLens(variant.index)}` : prompt;
23989
25670
  const kept = rec.kept;
23990
25671
  if (!kept)
23991
25672
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
23992
- const path = existsSync22(old.path) ? join21(dirname8(old.path), `${basename6(old.path)}-${shortId().slice(0, 4)}`) : old.path;
25673
+ if (rec.auto)
25674
+ log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
25675
+ const path = existsSync23(old.path) ? join21(dirname8(old.path), `${basename6(old.path)}-${shortId().slice(0, 4)}`) : old.path;
23993
25676
  const branch = await withRepoLock(repo.path, async () => {
23994
25677
  if (!await commitOf(repo.path, archiveRef(old.id))) {
23995
25678
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -24038,6 +25721,8 @@ ${variantLens(variant.index)}` : prompt;
24038
25721
  this.countsCache.delete(wt.id);
24039
25722
  this.refreshLink(wt);
24040
25723
  this.launch(wt, repo, repo.path);
25724
+ if (message)
25725
+ this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
24041
25726
  this.d.hub.emit("archiveChanged", repo.id);
24042
25727
  return wt;
24043
25728
  }
@@ -24047,12 +25732,15 @@ ${variantLens(variant.index)}` : prompt;
24047
25732
  return;
24048
25733
  const repo = this.repoOf(rec);
24049
25734
  const repoPath = repo?.path ?? rec.repoPath;
24050
- if (rec.kept && existsSync22(repoPath)) {
25735
+ if (rec.kept && existsSync23(repoPath)) {
24051
25736
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
24052
25737
  if (!r.ok)
24053
25738
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
24054
25739
  }
25740
+ if (rec.worktree.lands?.length && existsSync23(repoPath))
25741
+ await dropLandRefs(repoPath, archiveId);
24055
25742
  this.archive.delete(archiveId);
25743
+ this.d.drafts?.drop(archiveId);
24056
25744
  if (repo)
24057
25745
  this.d.hub.emit("archiveChanged", repo.id);
24058
25746
  }
@@ -24122,7 +25810,7 @@ ${variantLens(variant.index)}` : prompt;
24122
25810
  const group = wt.variant.group;
24123
25811
  const siblings = this.d.state.worktrees.filter((w) => w.variant?.group === group && w.id !== worktreeId);
24124
25812
  for (const sibling of siblings) {
24125
- await this.remove(sibling.id).catch((e) => log.warn(sibling.id, "could not remove variant sibling", e));
25813
+ await this.archiveWorktree(sibling.id).catch((e) => log.warn(sibling.id, "could not archive variant sibling", e));
24126
25814
  }
24127
25815
  delete wt.variant;
24128
25816
  this.d.state.save();
@@ -24163,20 +25851,28 @@ ${variantLens(variant.index)}` : prompt;
24163
25851
  for (const w of sources) {
24164
25852
  const entries = this.d.runtime.ensureAgent(w).agent.transcript();
24165
25853
  agent.note({ type: "grafted", title: w.title, branch: w.branch, ts: Date.now() });
24166
- for (const { event } of entries.slice(cutPoint(entries)))
25854
+ for (const { event } of coalesce(entries))
24167
25855
  agent.note(withoutAttachments(event));
24168
25856
  }
24169
25857
  for (const w of sources)
24170
- await this.remove(w.id, { archive: false });
25858
+ await this.discardWorktree(w.id);
24171
25859
  this.setLanded(target.id, false);
24172
25860
  this.countsCache.delete(target.id);
24173
25861
  this.d.hub.emit("worktreesChanged");
24174
25862
  return { target, grafted: sources.map((w) => w.title) };
24175
25863
  }
24176
- async setupAndStart(wt, repo, depsSource = repo.path, { setupCommands = true } = {}) {
25864
+ async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
25865
+ this.d.runtime.markSetup(wt.id, true);
25866
+ try {
25867
+ await this.setupThenStart(wt, repo, depsSource, opts);
25868
+ } finally {
25869
+ this.d.runtime.markSetup(wt.id, false);
25870
+ }
25871
+ }
25872
+ async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
24177
25873
  const srcNm = join21(depsSource, "node_modules");
24178
25874
  const dstNm = join21(wt.path, "node_modules");
24179
- if (existsSync22(srcNm) && !existsSync22(dstNm)) {
25875
+ if (existsSync23(srcNm) && !existsSync23(dstNm)) {
24180
25876
  const started = Date.now();
24181
25877
  const attempts = [
24182
25878
  ["clonefile", ["-Rc", srcNm, dstNm]],
@@ -24193,7 +25889,7 @@ ${variantLens(variant.index)}` : prompt;
24193
25889
  }
24194
25890
  for (const f of LOCAL_CONFIG_FILES) {
24195
25891
  const src = join21(depsSource, f);
24196
- if (!existsSync22(src) || existsSync22(join21(wt.path, f)))
25892
+ if (!existsSync23(src) || existsSync23(join21(wt.path, f)))
24197
25893
  continue;
24198
25894
  const r = await run("cp", [src, join21(wt.path, f)], wt.path);
24199
25895
  if (r.ok)
@@ -24313,12 +26009,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24313
26009
  const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
24314
26010
  if (!taken.ok)
24315
26011
  return taken;
26012
+ const mark = await landingMark(wt.path, repo.defaultBranch);
24316
26013
  const method = policy.merge ?? DEFAULT_MERGE_METHOD;
24317
26014
  const squash = method === "squash" ? await squashMessage(wt.path, repo.defaultBranch, suggested) : "";
24318
26015
  const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash);
24319
26016
  if (!landed.ok)
24320
26017
  return landed;
24321
26018
  mergedHere = true;
26019
+ await this.noteLand(repo, wt, mark);
24322
26020
  await this.restartFromMain(wt, repo.defaultBranch);
24323
26021
  if (policy.land === "push") {
24324
26022
  const pushed = await pushMain(repo.path, repo.defaultBranch);
@@ -24334,9 +26032,21 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24334
26032
  }
24335
26033
  if (!result.ok)
24336
26034
  return { result };
24337
- const removeIds = wt.variant ? this.d.state.worktrees.filter((w) => w.variant?.group === wt.variant?.group && w.id !== wt.id).map((w) => w.id) : [];
26035
+ const archiveIds = wt.variant ? this.d.state.worktrees.filter((w) => w.variant?.group === wt.variant?.group && w.id !== wt.id).map((w) => w.id) : [];
24338
26036
  const where = policy.land === "push" ? `${repo.defaultBranch}, pushed` : repo.defaultBranch;
24339
- return { result: { ...result, message: `${wt.title} is on ${where}` }, removeIds };
26037
+ return { result: { ...result, message: `${wt.title} is on ${where}` }, archiveIds };
26038
+ }
26039
+ async noteLand(repo, wt, mark) {
26040
+ if (!mark)
26041
+ return;
26042
+ const n = wt.lands?.length ?? 0;
26043
+ const pinned = await git(repo.path, "update-ref", landRef(wt.id, n), mark.tip);
26044
+ if (!pinned.ok) {
26045
+ log.warn(wt.id, `could not keep the commits it landed: ${pinned.err}`);
26046
+ return;
26047
+ }
26048
+ wt.lands = [...wt.lands ?? [], { ...mark, at: Date.now() }];
26049
+ this.d.state.save();
24340
26050
  }
24341
26051
  async restartFromMain(wt, defaultBr) {
24342
26052
  if (!hasOwnBranch(wt))
@@ -24349,9 +26059,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24349
26059
  async prMerged(worktreeId) {
24350
26060
  const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
24351
26061
  const result = await withRepoLock(repo.path, async () => {
26062
+ const mark = await landingMark(wt.path, repo.defaultBranch);
24352
26063
  const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
24353
26064
  if (!pulled.ok)
24354
26065
  return pulled;
26066
+ await this.noteLand(repo, wt, mark);
24355
26067
  await this.restartFromMain(wt, repo.defaultBranch);
24356
26068
  return pulled;
24357
26069
  });
@@ -24430,21 +26142,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24430
26142
  const known = this.usage.get(worktreeId);
24431
26143
  if (known !== undefined)
24432
26144
  return known ?? undefined;
24433
- let found = null;
24434
- const file = transcriptPathFor(this.d.paths.transcriptsDir, worktreeId);
24435
- if (existsSync22(file)) {
24436
- const lines = readFileSync14(file, "utf8").split(`
24437
- `);
24438
- for (let i = lines.length - 1;i >= 0 && !found; i--) {
24439
- if (!lines[i]?.includes('"usage"'))
24440
- continue;
24441
- try {
24442
- const e = JSON.parse(lines[i]).event;
24443
- if (e?.type === "usage")
24444
- found = { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
24445
- } catch {}
24446
- }
24447
- }
26145
+ const found = lastUsage(transcriptPathFor(this.d.paths.transcriptsDir, worktreeId));
24448
26146
  this.usage.set(worktreeId, found);
24449
26147
  return found ?? undefined;
24450
26148
  }
@@ -24513,6 +26211,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24513
26211
  return cached ?? {};
24514
26212
  }
24515
26213
  }
26214
+ async freshCounts(worktreeId) {
26215
+ const wt = this.d.state.worktree(worktreeId);
26216
+ const repo = wt && this.d.state.repo(wt.repoId);
26217
+ if (!wt || !repo)
26218
+ return {};
26219
+ this.countsCache.delete(worktreeId);
26220
+ return this.counts(wt.id, wt.path, repo.defaultBranch, true);
26221
+ }
24516
26222
  readable(id) {
24517
26223
  const wt = this.d.state.worktree(id);
24518
26224
  if (wt) {
@@ -24538,7 +26244,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24538
26244
  async gitStatus(worktreeId) {
24539
26245
  const r = this.readable(worktreeId);
24540
26246
  if (!r)
24541
- return null;
26247
+ return this.archivedStatus(worktreeId);
24542
26248
  try {
24543
26249
  const isMain = r.wt?.kind === "main";
24544
26250
  const [files, counts, head] = await Promise.all([
@@ -24576,11 +26282,15 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24576
26282
  }
24577
26283
  async gitLog(worktreeId) {
24578
26284
  const r = this.readable(worktreeId);
24579
- return r ? logCommits(r.path, r.defaultBranch) : [];
26285
+ if (r)
26286
+ return logCommits(r.path, r.defaultBranch);
26287
+ return await this.archivedGit(worktreeId)?.log() ?? [];
24580
26288
  }
24581
26289
  async commitFiles(worktreeId, sha) {
24582
26290
  const r = this.readable(worktreeId);
24583
- return r ? commitFiles(r.path, sha) : [];
26291
+ if (r)
26292
+ return commitFiles(r.path, sha);
26293
+ return await this.archivedGit(worktreeId)?.commitFiles(sha) ?? [];
24584
26294
  }
24585
26295
  markPrompted(worktreeId) {
24586
26296
  const wt = this.d.state.worktree(worktreeId);
@@ -24647,11 +26357,115 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24647
26357
  }
24648
26358
  }
24649
26359
 
26360
+ // ../daemon/src/worktrees/sweep.ts
26361
+ var ARCHIVE_AFTER_MS = 2 * 60 * 60000;
26362
+ var SWEEP_EVERY_MS = 10 * 60000;
26363
+ function archiveAfterFrom(raw) {
26364
+ if (raw === "off")
26365
+ return null;
26366
+ const n = Number(raw);
26367
+ return raw && Number.isFinite(n) && n > 0 ? n : ARCHIVE_AFTER_MS;
26368
+ }
26369
+
26370
+ class ArchiveSweep {
26371
+ d;
26372
+ timer = null;
26373
+ running = false;
26374
+ again = false;
26375
+ afterMs;
26376
+ constructor(d) {
26377
+ this.d = d;
26378
+ this.afterMs = d.afterMs === undefined ? ARCHIVE_AFTER_MS : d.afterMs;
26379
+ if (this.afterMs === null)
26380
+ return;
26381
+ const every = d.setInterval ?? ((fn, ms) => {
26382
+ const t = setInterval(fn, ms);
26383
+ t.unref?.();
26384
+ return t;
26385
+ });
26386
+ this.timer = every(() => this.request(), Math.min(d.everyMs ?? SWEEP_EVERY_MS, this.afterMs));
26387
+ }
26388
+ stop() {
26389
+ if (this.timer && !this.d.setInterval)
26390
+ clearInterval(this.timer);
26391
+ this.timer = null;
26392
+ }
26393
+ request() {
26394
+ if (this.running) {
26395
+ this.again = true;
26396
+ return;
26397
+ }
26398
+ this.running = true;
26399
+ fireAndForget("sweep", this.sweep().finally(() => {
26400
+ this.running = false;
26401
+ if (!this.again)
26402
+ return;
26403
+ this.again = false;
26404
+ this.request();
26405
+ }), "archive sweep");
26406
+ }
26407
+ async sweep() {
26408
+ if (this.afterMs === null)
26409
+ return;
26410
+ for (const repo of this.d.state.repos) {
26411
+ const beforeGit = this.facts(repo.id, () => ({ dirty: 0, ahead: 0 }));
26412
+ const maybe = beforeGit.rows.filter((w) => archiveReason(w, beforeGit) !== null);
26413
+ if (maybe.length === 0)
26414
+ continue;
26415
+ const fresh = new Map;
26416
+ for (const w of beforeGit.rows) {
26417
+ if (!maybe.some((m) => m.id === w.id || !!m.variant && m.variant.group === w.variant?.group))
26418
+ continue;
26419
+ fresh.set(w.id, await this.d.worktrees.freshCounts(w.id));
26420
+ }
26421
+ const counts = (id) => fresh.get(id) ?? {};
26422
+ for (const w of maybe) {
26423
+ const reason = archiveReason(w, this.facts(repo.id, counts));
26424
+ if (!reason)
26425
+ continue;
26426
+ log.info(w.id, `archiving on its own: ${reason}`);
26427
+ try {
26428
+ await this.d.worktrees.archiveWorktree(w.id, {
26429
+ reason,
26430
+ still: () => this.stillGoes(repo.id, w.id, counts)
26431
+ });
26432
+ } catch (e) {
26433
+ log.warn(w.id, "could not archive it", e);
26434
+ }
26435
+ }
26436
+ }
26437
+ }
26438
+ facts(repoId, counts) {
26439
+ return {
26440
+ now: (this.d.now ?? Date.now)(),
26441
+ afterMs: this.afterMs ?? ARCHIVE_AFTER_MS,
26442
+ rows: this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind !== "spare"),
26443
+ viewed: this.d.viewed,
26444
+ pending: this.d.busy,
26445
+ drafted: (id) => this.d.drafts.has(id),
26446
+ counts
26447
+ };
26448
+ }
26449
+ stillGoes(repoId, id, counts) {
26450
+ const f = this.facts(repoId, counts);
26451
+ const wt = f.rows.find((w) => w.id === id);
26452
+ return !!wt && archiveReason(wt, f) !== null;
26453
+ }
26454
+ }
26455
+
24650
26456
  // ../daemon/src/index.ts
24651
26457
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
24652
26458
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
24653
26459
  var here = dirname9(fileURLToPath(import.meta.url));
24654
- var { shellDist: SHELL_DIST, bridgeJs: BRIDGE_JS } = locateAssets(here);
26460
+ var {
26461
+ shellDist: SHELL_DIST,
26462
+ bridgeJs: BRIDGE_JS,
26463
+ sourceRoot: SOURCE_ROOT,
26464
+ packageJson: PACKAGE_JSON
26465
+ } = locateAssets(here);
26466
+ var pruned = pruneAssets(SHELL_DIST);
26467
+ if (pruned > 0)
26468
+ log.debug("daemon", `pruned ${pruned} assets from builds this one has outlived`);
24655
26469
  var paths = makePaths();
24656
26470
  ensureDirs(paths);
24657
26471
  var token = loadOrCreateToken(paths);
@@ -24697,12 +26511,15 @@ var runtime = new RuntimeRegistry({
24697
26511
  remote,
24698
26512
  grant: previewGrant(token)
24699
26513
  });
24700
- var worktrees = new WorktreeService({ state, hub, runtime, paths, agents });
26514
+ var drafts = new DraftStore({ file: paths.draftsFile, hub });
26515
+ var worktrees = new WorktreeService({ state, hub, runtime, paths, agents, drafts });
24701
26516
  var turns = new TurnService({
24702
26517
  state,
24703
26518
  hub,
24704
26519
  transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
24705
- summarize: makeRecapper(runtime, agents, state)
26520
+ summarize: makeRecapper(runtime, agents, state),
26521
+ hold: (id, tag) => runtime.hold(id, tag),
26522
+ release: (id, tag) => runtime.release(id, tag)
24706
26523
  });
24707
26524
  var files = new FileService(state, runtime, (id) => worktrees.readable(id));
24708
26525
  var design = new DesignService((id) => worktrees.readable(id));
@@ -24717,10 +26534,61 @@ new LandingService({
24717
26534
  judge: makeLander(runtime, agents, state)
24718
26535
  });
24719
26536
  var refs = new RefSearch({ state });
26537
+ var chats = new ChatSearch({
26538
+ state,
26539
+ live: (id) => runtime.agentFor(id)?.transcript(),
26540
+ transcriptPath: (id) => transcriptPathFor(paths.transcriptsDir, id),
26541
+ archivedChats: (repoId) => worktrees.archivedChats(repoId)
26542
+ });
24720
26543
  var prs = new PrService({ state, hub, worktrees, view: viewPr });
24721
- var repos = new RepoRegistry({ state, hub, runtime, worktrees });
26544
+ var self = new SelfWatch(SOURCE_ROOT);
26545
+ await self.start();
26546
+ var afterLand = new AfterLand({ state, hub, self });
26547
+ var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
26548
+ var idle = new IdlePolicy({
26549
+ runtime,
26550
+ state,
26551
+ hub,
26552
+ wake: (id) => repos.touch(id),
26553
+ warmSpare: (repoId) => repos.warm(repoId)
26554
+ });
26555
+ var sweep = new ArchiveSweep({
26556
+ state,
26557
+ viewed: (id) => idle.isViewed(id),
26558
+ busy: (id) => runtime.busy(id),
26559
+ drafts,
26560
+ worktrees,
26561
+ afterMs: archiveAfterFrom(process.env.TOYON_ARCHIVE_AFTER_MS)
26562
+ });
24722
26563
  var themes = new ThemeStore({ get: () => state.theme, set: (p) => state.setTheme(p) }, paths.themesDir);
24723
26564
  themes.load();
26565
+ var restarter = new Restarter({
26566
+ hub,
26567
+ state,
26568
+ runtime,
26569
+ refusal: () => {
26570
+ const can = restartable();
26571
+ return can.ok ? null : can.reason;
26572
+ },
26573
+ go: () => {
26574
+ setTimeout(() => fireAndForget("daemon", shutdown("restart", { respawn: true }), "restart"), 100);
26575
+ }
26576
+ });
26577
+ var installedPackage = cloud.enabled || process.env.TOYON_WORKTREE !== undefined ? null : PACKAGE_JSON;
26578
+ var method = installMethod(installedPackage);
26579
+ var update = new UpdateService({
26580
+ hub,
26581
+ state,
26582
+ running: package_default.version,
26583
+ method,
26584
+ restarter,
26585
+ installed: async () => installedPackage ? readVersion(installedPackage) : null,
26586
+ latest: latestVersion,
26587
+ command: (version) => installCommand(method, version),
26588
+ install: runInstall,
26589
+ busy: () => runtime.anyBusy(),
26590
+ managed: process.env.TOYON_UPDATES === "off"
26591
+ });
24724
26592
  var { branded, stop: stopServer } = startServer({
24725
26593
  port,
24726
26594
  token,
@@ -24738,13 +26606,20 @@ var { branded, stop: stopServer } = startServer({
24738
26606
  design,
24739
26607
  routes,
24740
26608
  runtime,
26609
+ idle,
24741
26610
  exec,
24742
26611
  refs,
26612
+ chats,
26613
+ drafts,
24743
26614
  prs,
24744
26615
  themes,
24745
26616
  agents,
24746
26617
  accounts,
24747
26618
  attachments: attachments2,
26619
+ self,
26620
+ afterLand,
26621
+ restarter,
26622
+ update,
24748
26623
  planTasks: makePlanner(agents),
24749
26624
  folderDialog: folderDialog()
24750
26625
  }
@@ -24764,6 +26639,11 @@ writeFileSync9(paths.pidFile, `${process.pid}
24764
26639
  `);
24765
26640
  var stopLagSampler = startLagSampler();
24766
26641
  await repos.boot();
26642
+ drafts.prune((id) => {
26643
+ const repoId = draftRepoOf(id);
26644
+ return repoId ? !!state.repo(repoId) : !!state.worktree(id) || worktrees.hasArchived(id);
26645
+ });
26646
+ idle.boot();
24767
26647
  fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
24768
26648
  var repoArg = process.argv[2];
24769
26649
  if (repoArg) {
@@ -24788,19 +26668,27 @@ if (remote?.front === "edge") {
24788
26668
  }
24789
26669
  }
24790
26670
  var shuttingDown = false;
24791
- async function shutdown(signal) {
26671
+ async function shutdown(signal, opts = {}) {
24792
26672
  if (shuttingDown)
24793
26673
  return;
24794
26674
  shuttingDown = true;
24795
26675
  log.info("daemon", `${signal}: stopping dev servers`);
26676
+ idle.shutdown();
24796
26677
  repos.stopWatchers();
24797
26678
  prs.stop();
26679
+ sweep.stop();
24798
26680
  stopLagSampler();
24799
26681
  stopServer();
24800
26682
  routes.flush();
26683
+ drafts.flush();
24801
26684
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
24802
26685
  await Promise.race([runtime.shutdown(), deadline]);
24803
- rmSync4(paths.pidFile, { force: true });
26686
+ rmSync6(paths.pidFile, { force: true });
26687
+ if (opts.respawn) {
26688
+ await Bun.sleep(250);
26689
+ log.info("daemon", "starting the replacement");
26690
+ respawn(paths.logFile);
26691
+ }
24804
26692
  process.exit(0);
24805
26693
  }
24806
26694
  process.on("SIGINT", () => fireAndForget("daemon", shutdown("SIGINT"), "shutdown"));