toyon 0.2.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 +233 -70
  4. package/dist/daemon.js +1067 -210
  5. package/dist/shell/assets/{Editor-D-PlbMSY.js → Editor-Lle8R8mY.js} +3 -3
  6. package/dist/shell/assets/{XTerm-CM_FJEwN.js → XTerm-DcWpV1FL.js} +1 -1
  7. package/dist/shell/assets/{cssMode-Bkqyr9Lr.js → cssMode-Ck0mAapo.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-DVAK5zoJ.js → freemarker2-B5OkSZOW.js} +1 -1
  9. package/dist/shell/assets/{handlebars-Bdtj7uMl.js → handlebars-C6vrFwYj.js} +1 -1
  10. package/dist/shell/assets/{html-B1pqTvyA.js → html-CE92cnIr.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-CBtvzewS.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-FNavUzsm.js → javascript-C7Q1bCJn.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-HgW1GXQH.js → jsonMode-Cq43R9DW.js} +1 -1
  16. package/dist/shell/assets/{liquid-6wkVlojW.js → liquid-CnxQAGJm.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-CvNG6b_1.js → lspLanguageFeatures-BtjVqb14.js} +1 -1
  18. package/dist/shell/assets/{mdx-C-byKky3.js → mdx-ycMLgnto.js} +1 -1
  19. package/dist/shell/assets/{python-BEJMbP4i.js → python-se_0lOPN.js} +1 -1
  20. package/dist/shell/assets/{razor-DeXWW25R.js → razor-DjByizBq.js} +1 -1
  21. package/dist/shell/assets/{tsMode-DMwJnvD-.js → tsMode-D1mOZbLC.js} +1 -1
  22. package/dist/shell/assets/{typescript-Cj57ccSp.js → typescript-B-dZdvXG.js} +1 -1
  23. package/dist/shell/assets/{workers-BZi9jqBp.js → workers-DXwGNRtC.js} +1 -1
  24. package/dist/shell/assets/{xml-DUDeQI0I.js → xml-CoqNFco9.js} +1 -1
  25. package/dist/shell/assets/{yaml-BCD8Shg5.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-GBQLHG7z.css +0 -1
  30. package/dist/shell/assets/index-jy_zeFGs.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 rmSync6, writeFileSync as writeFileSync8 } 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 };
@@ -50,6 +155,7 @@ var DAEMON_FILES = {
50
155
  token: "token",
51
156
  pid: "daemon.pid",
52
157
  state: "state.json",
158
+ drafts: "drafts.json",
53
159
  log: "daemon.log",
54
160
  remote: "remote.json"
55
161
  };
@@ -163,6 +269,11 @@ function middle(a, b) {
163
269
  return ops;
164
270
  }
165
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
+
166
277
  // ../shared/src/land.ts
167
278
  var DEFAULT_LAND_ROUTE = "merge";
168
279
  var DEFAULT_MERGE_METHOD = "merge";
@@ -175,19 +286,8 @@ function landPolicy(config) {
175
286
  };
176
287
  }
177
288
 
178
- // ../shared/src/model.ts
179
- var DEFAULT_PERMISSION_MODE = "auto";
180
- function hasOwnBranch(wt) {
181
- return wt.branch.startsWith("toyon/");
182
- }
183
- var SHELL_STREAM = "shell";
184
- var LOGIN_STREAM = "login";
185
- function streamKey(worktreeId, stream) {
186
- return `${worktreeId}/${stream}`;
187
- }
188
-
189
289
  // ../shared/src/protocol/limits.ts
190
- var PROTOCOL_VERSION = 50;
290
+ var PROTOCOL_VERSION = 56;
191
291
  var FILE_MAX_CHARS = 5000000;
192
292
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
193
293
  var IMAGE_MAX_EDGE = 2576;
@@ -1130,7 +1230,7 @@ var nanoid = /^[a-zA-Z0-9_-]{21}$/;
1130
1230
  function nanoidOfLength(length) {
1131
1231
  return new RegExp(`^[a-zA-Z0-9_-]{${length}}$`);
1132
1232
  }
1133
- 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)?)?)$/;
1134
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})$/;
1135
1235
  var uuid = (version) => {
1136
1236
  if (!version)
@@ -1923,7 +2023,7 @@ var $ZodISOTime = /* @__PURE__ */ $constructor("$ZodISOTime", (inst, def) => {
1923
2023
  $ZodStringFormat.init(inst, def);
1924
2024
  });
1925
2025
  var $ZodISODuration = /* @__PURE__ */ $constructor("$ZodISODuration", (inst, def) => {
1926
- def.pattern ?? (def.pattern = duration);
2026
+ def.pattern ?? (def.pattern = duration2);
1927
2027
  $ZodStringFormat.init(inst, def);
1928
2028
  });
1929
2029
  var $ZodIPv4 = /* @__PURE__ */ $constructor("$ZodIPv4", (inst, def) => {
@@ -6264,6 +6364,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6264
6364
  object({
6265
6365
  t: literal("chat"),
6266
6366
  worktreeId: id,
6367
+ clientId: string2().max(64).optional(),
6267
6368
  text: prose,
6268
6369
  context: prose.optional(),
6269
6370
  attachments
@@ -6295,7 +6396,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6295
6396
  object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
6296
6397
  object({ t: literal("set-worktree-effort"), worktreeId: id, effort: string2().max(100) }),
6297
6398
  object({ t: literal("set-worktree-profile"), worktreeId: id, profile: string2().max(100) }),
6298
- object({ t: literal("remove-worktree"), worktreeId: id }),
6399
+ object({ t: literal("archive-worktree"), worktreeId: id }),
6299
6400
  object({ t: literal("list-archived"), repoId: id }),
6300
6401
  object({
6301
6402
  t: literal("restore-worktree"),
@@ -6319,6 +6420,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6319
6420
  object({ t: literal("sync-main"), worktreeId: id }),
6320
6421
  object({ t: literal("run-after-land"), repoId: id }),
6321
6422
  object({ t: literal("restart-daemon") }),
6423
+ object({ t: literal("update-now") }),
6322
6424
  object({ t: literal("pull-main"), worktreeId: id }),
6323
6425
  object({
6324
6426
  t: literal("write-file"),
@@ -6338,6 +6440,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6338
6440
  object({ t: literal("stop-agent"), worktreeId: id }),
6339
6441
  object({ t: literal("seen"), worktreeId: id }),
6340
6442
  object({ t: literal("mark-unread"), worktreeId: id }),
6443
+ object({ t: literal("set-draft"), boxId: id, text: prose, clientId: string2().max(64).optional() }),
6341
6444
  object({ t: literal("refresh-git"), repoId: id }),
6342
6445
  object({ t: literal("visit"), worktreeId: id, path: routePath, title: pageTitle.optional() }),
6343
6446
  object({ t: literal("page-title"), worktreeId: id, path: routePath, title: pageTitle }),
@@ -7377,6 +7480,52 @@ function isDark(hex) {
7377
7480
  return luminance(hex) < 0.4;
7378
7481
  }
7379
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
+
7380
7529
  // ../shared/src/vscode-theme.ts
7381
7530
  var workbenchKeys = {
7382
7531
  surface0: ["editor.background"],
@@ -7553,27 +7702,10 @@ function pickSyntax(tokenColors) {
7553
7702
  function slug(s) {
7554
7703
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "theme";
7555
7704
  }
7556
-
7557
- // ../shared/src/worktree-caps.ts
7558
- function isMain(wt) {
7559
- return wt.kind === "main";
7560
- }
7561
- function canRemove(wt) {
7562
- return wt.kind === "worktree";
7563
- }
7564
- function canRename(wt) {
7565
- return wt.kind === "worktree" && hasOwnBranch(wt);
7566
- }
7567
- function canLand(wt) {
7568
- return wt.kind === "worktree" && wt.from?.kind !== "pr";
7569
- }
7570
- function canGraft(wt) {
7571
- return wt.kind === "worktree";
7572
- }
7573
7705
  // ../daemon/package.json
7574
7706
  var package_default = {
7575
7707
  name: "@toyon/daemon",
7576
- version: "0.2.0",
7708
+ version: "0.3.0",
7577
7709
  private: true,
7578
7710
  type: "module",
7579
7711
  main: "src/index.ts",
@@ -12494,6 +12626,7 @@ function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon"))
12494
12626
  return {
12495
12627
  home,
12496
12628
  stateFile: join3(home, DAEMON_FILES.state),
12629
+ draftsFile: join3(home, DAEMON_FILES.drafts),
12497
12630
  tokenFile: join3(home, DAEMON_FILES.token),
12498
12631
  pidFile: join3(home, DAEMON_FILES.pid),
12499
12632
  logFile: join3(home, DAEMON_FILES.log),
@@ -13708,6 +13841,9 @@ class AcpSession {
13708
13841
  get queueLength() {
13709
13842
  return this.queue.length;
13710
13843
  }
13844
+ get unsettled() {
13845
+ return this.queue.length > 0 || this.steered.length > 0 || this.refused !== null || this.asks.size > 0;
13846
+ }
13711
13847
  get queueItems() {
13712
13848
  return this.waiting().map((q) => q.text);
13713
13849
  }
@@ -14230,6 +14366,8 @@ class AcpSession {
14230
14366
  }
14231
14367
  askPermission(params) {
14232
14368
  const plan = params.toolCall.kind === "switch_mode";
14369
+ if (plan)
14370
+ this.d.onPlan?.();
14233
14371
  const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
14234
14372
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
14235
14373
  return this.openAsk((id) => ({
@@ -15319,6 +15457,13 @@ function defaultAgent(wt, d) {
15319
15457
  d.state.save();
15320
15458
  d.hub.emit("worktreesChanged");
15321
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
+ },
15322
15467
  option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
15323
15468
  onOptionsLearned: (category, choices) => {
15324
15469
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
@@ -15388,6 +15533,21 @@ class RuntimeRegistry {
15388
15533
  holdCount(id) {
15389
15534
  return this.holds.get(id)?.size ?? 0;
15390
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
+ }
15391
15551
  markSetup(id, on) {
15392
15552
  if (on)
15393
15553
  this.settingUp.add(id);
@@ -16127,7 +16287,8 @@ function locateAssets(here, env = process.env) {
16127
16287
  return {
16128
16288
  shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join7(here, "shell") : join7(here, "../../shell/dist")),
16129
16289
  bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js")),
16130
- sourceRoot: packaged ? null : resolve5(here, "../../..")
16290
+ sourceRoot: packaged ? null : resolve5(here, "../../.."),
16291
+ packageJson: packaged ? join7(here, "..", "package.json") : null
16131
16292
  };
16132
16293
  }
16133
16294
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
@@ -16265,9 +16426,57 @@ function respawn(logFile) {
16265
16426
  stdio: ["ignore", logFd, logFd],
16266
16427
  env: { ...process.env }
16267
16428
  });
16429
+ child.on("error", (e) => log.warn("daemon", `the replacement did not start: ${e.message}`));
16268
16430
  child.unref();
16269
16431
  }
16270
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
+
16271
16480
  // ../daemon/src/core/self.ts
16272
16481
  function needsRestart(path) {
16273
16482
  return path.startsWith("packages/daemon/") || path.startsWith("packages/shared/") || path === "package.json" || path === "bun.lock";
@@ -16535,6 +16744,13 @@ class StateStore {
16535
16744
  this.state.theme = prefs;
16536
16745
  this.save();
16537
16746
  }
16747
+ get updateFailed() {
16748
+ return this.state.updateFailed;
16749
+ }
16750
+ setUpdateFailed(failed) {
16751
+ this.state.updateFailed = failed;
16752
+ this.save();
16753
+ }
16538
16754
  save() {
16539
16755
  saveState(this.paths, this.state);
16540
16756
  }
@@ -17112,6 +17328,87 @@ async function loadProjectTypescript(root) {
17112
17328
  }
17113
17329
  }
17114
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
+
17115
17412
  // ../daemon/src/exec/service.ts
17116
17413
  var OUTPUT_CAP = 200000;
17117
17414
  var TIMEOUT_MS = 10 * 60000;
@@ -17363,10 +17660,13 @@ async function committedFiles(worktreePath, defaultBr) {
17363
17660
  const base = await git(worktreePath, "merge-base", "HEAD", defaultBr);
17364
17661
  if (!base.ok || !base.out)
17365
17662
  return [];
17366
- 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);
17367
17667
  if (!r.ok || !r.out)
17368
17668
  return [];
17369
- const counts = await numstat(worktreePath, base.out, "HEAD");
17669
+ const counts = await numstat(cwd, from, to);
17370
17670
  return r.out.split(`
17371
17671
  `).filter(Boolean).map((line) => {
17372
17672
  const [status, ...rest] = line.split("\t");
@@ -17419,10 +17719,15 @@ async function logCommits(worktreePath, defaultBr, limit = LOG_LIMIT) {
17419
17719
  git(worktreePath, "log", `-n${limit}`, FORMAT),
17420
17720
  aheadShas(worktreePath, defaultBr)
17421
17721
  ]);
17422
- if (!r.ok || !r.out)
17423
- 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) {
17424
17729
  const commits = [];
17425
- for (const rec of r.out.split("\x1E")) {
17730
+ for (const rec of out.split("\x1E")) {
17426
17731
  const [sha, short, author, at, subject] = rec.trim().split("\x00");
17427
17732
  if (!sha || !short || subject === undefined)
17428
17733
  continue;
@@ -17432,7 +17737,7 @@ async function logCommits(worktreePath, defaultBr, limit = LOG_LIMIT) {
17432
17737
  subject,
17433
17738
  author: author ?? "",
17434
17739
  at: Number(at) * 1000 || 0,
17435
- ahead: ahead.has(sha)
17740
+ ahead: ahead(sha)
17436
17741
  });
17437
17742
  }
17438
17743
  return commits;
@@ -17613,6 +17918,30 @@ function rankElementSources(el, rows) {
17613
17918
  };
17614
17919
  }
17615
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
+
17616
17945
  // ../daemon/src/files/vite-offset.ts
17617
17946
  async function viteLineOffset(worktreePath, path, target) {
17618
17947
  if (!target)
@@ -17693,6 +18022,9 @@ function sides(before, after) {
17693
18022
  }
17694
18023
  return { before, after, binary: false, tooLarge: false };
17695
18024
  }
18025
+ function keptRead(before, after) {
18026
+ return { ...sides(before, after), version: null, writable: false };
18027
+ }
17696
18028
 
17697
18029
  class FileService {
17698
18030
  state;
@@ -17771,9 +18103,13 @@ class FileService {
17771
18103
  });
17772
18104
  }
17773
18105
  async list(worktreeId) {
17774
- const r = await git(this.require(worktreeId).path, "ls-files", "-co", "--exclude-standard");
17775
- return r.out.split(`
17776
- `).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);
17777
18113
  }
17778
18114
  async search(worktreeId, query) {
17779
18115
  const { path: cwd } = this.require(worktreeId);
@@ -18015,7 +18351,7 @@ class AfterLand {
18015
18351
  }
18016
18352
 
18017
18353
  // ../daemon/src/repos/registry.ts
18018
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
18354
+ import { existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
18019
18355
  import { homedir as homedir7 } from "os";
18020
18356
  import { basename as basename3, dirname as dirname6, join as join14 } from "path";
18021
18357
 
@@ -18043,7 +18379,7 @@ function variantLens(index) {
18043
18379
  }
18044
18380
 
18045
18381
  // ../daemon/src/repos/browse.ts
18046
- import { existsSync as existsSync12, statSync as statSync3 } from "fs";
18382
+ import { existsSync as existsSync13, statSync as statSync3 } from "fs";
18047
18383
  import { readdir } from "fs/promises";
18048
18384
  import { homedir as homedir5 } from "os";
18049
18385
  import { dirname as dirname4, join as join10 } from "path";
@@ -18060,7 +18396,7 @@ function holdsNothing(names) {
18060
18396
  }
18061
18397
  async function describeFolder(abs) {
18062
18398
  const path = collapseTilde(abs);
18063
- if (existsSync12(join10(abs, ".git")))
18399
+ if (existsSync13(join10(abs, ".git")))
18064
18400
  return { path, kind: "project" };
18065
18401
  try {
18066
18402
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -18075,10 +18411,10 @@ function describe2(typed, parent) {
18075
18411
  isDir = statSync3(typed).isDirectory();
18076
18412
  } catch {}
18077
18413
  return {
18078
- exists: existsSync12(typed),
18414
+ exists: existsSync13(typed),
18079
18415
  isDir,
18080
- isRepo: isDir && existsSync12(join10(typed, ".git")),
18081
- parentExists: existsSync12(parent)
18416
+ isRepo: isDir && existsSync13(join10(typed, ".git")),
18417
+ parentExists: existsSync13(parent)
18082
18418
  };
18083
18419
  }
18084
18420
  async function browsePath(raw) {
@@ -18103,17 +18439,17 @@ async function browsePath(raw) {
18103
18439
  if (!name.toLowerCase().startsWith(prefix))
18104
18440
  continue;
18105
18441
  const abs = join10(dir, name);
18106
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync12(join10(abs, ".git")) });
18442
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync13(join10(abs, ".git")) });
18107
18443
  }
18108
18444
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
18109
18445
  return { entries: entries.slice(0, MAX_ENTRIES), target };
18110
18446
  }
18111
18447
 
18112
18448
  // ../daemon/src/repos/config.ts
18113
- import { closeSync, existsSync as existsSync13, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync9, readSync } from "fs";
18449
+ import { closeSync, existsSync as existsSync14, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, readSync } from "fs";
18114
18450
  import { join as join11 } from "path";
18115
18451
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
18116
- 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)));
18117
18453
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
18118
18454
  function readConfigFile(repoPath) {
18119
18455
  const found = PLACES.map((p) => present(repoPath, p)).filter((files) => files.length > 0);
@@ -18128,7 +18464,7 @@ function readConfigFile(repoPath) {
18128
18464
  for (const rel of files) {
18129
18465
  let raw;
18130
18466
  try {
18131
- raw = JSON.parse(readFileSync9(join11(repoPath, rel), "utf8"));
18467
+ raw = JSON.parse(readFileSync10(join11(repoPath, rel), "utf8"));
18132
18468
  } catch (e) {
18133
18469
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
18134
18470
  }
@@ -18186,11 +18522,11 @@ function configTarget(repoPath, made) {
18186
18522
  function configBody(repoPath, rel, config) {
18187
18523
  const place = PLACES.find((p) => p.local === rel);
18188
18524
  const abs = place && join11(repoPath, place.shared);
18189
- if (!abs || !existsSync13(abs))
18525
+ if (!abs || !existsSync14(abs))
18190
18526
  return config;
18191
18527
  let shared;
18192
18528
  try {
18193
- shared = JSON.parse(readFileSync9(abs, "utf8"));
18529
+ shared = JSON.parse(readFileSync10(abs, "utf8"));
18194
18530
  } catch {
18195
18531
  return config;
18196
18532
  }
@@ -18203,8 +18539,8 @@ function detectConfig(repoPath) {
18203
18539
  if (explicit)
18204
18540
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
18205
18541
  const pkgPath = join11(repoPath, "package.json");
18206
- if (existsSync13(pkgPath)) {
18207
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
18542
+ if (existsSync14(pkgPath)) {
18543
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
18208
18544
  const scripts = pkg.scripts ?? {};
18209
18545
  const runner = detectRunner(repoPath, pkg.packageManager);
18210
18546
  const pages = detectPages(repoPath, scripts, runner);
@@ -18225,7 +18561,7 @@ function detectConfig(repoPath) {
18225
18561
  };
18226
18562
  }
18227
18563
  }
18228
- if (existsSync13(join11(repoPath, "start.sh"))) {
18564
+ if (existsSync14(join11(repoPath, "start.sh"))) {
18229
18565
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
18230
18566
  }
18231
18567
  const assumed = assumeNothingToRun(repoPath);
@@ -18268,7 +18604,7 @@ function assumeNothingToRun(repoPath) {
18268
18604
  return marker && !servesPage(repoPath) ? marker : undefined;
18269
18605
  }
18270
18606
  function markerIn(repoPath) {
18271
- const named = NO_PAGE_MARKERS.find((f) => existsSync13(join11(repoPath, f)));
18607
+ const named = NO_PAGE_MARKERS.find((f) => existsSync14(join11(repoPath, f)));
18272
18608
  if (named)
18273
18609
  return named;
18274
18610
  let entries;
@@ -18280,7 +18616,7 @@ function markerIn(repoPath) {
18280
18616
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
18281
18617
  }
18282
18618
  function servesPage(repoPath) {
18283
- if (WEB_FILES.some((f) => existsSync13(join11(repoPath, f))))
18619
+ if (WEB_FILES.some((f) => existsSync14(join11(repoPath, f))))
18284
18620
  return true;
18285
18621
  if (/^web:/m.test(head(join11(repoPath, "Procfile"))))
18286
18622
  return true;
@@ -18317,7 +18653,7 @@ function mentions(text, name) {
18317
18653
  return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
18318
18654
  }
18319
18655
  function head(path) {
18320
- if (!existsSync13(path))
18656
+ if (!existsSync14(path))
18321
18657
  return "";
18322
18658
  try {
18323
18659
  const fd = openSync2(path, "r");
@@ -18345,28 +18681,28 @@ function procCommand(runner, script, body) {
18345
18681
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
18346
18682
  }
18347
18683
  function detectPages(repoPath, scripts, runner) {
18348
- if (!scripts.build || !existsSync13(join11(repoPath, "functions")))
18684
+ if (!scripts.build || !existsSync14(join11(repoPath, "functions")))
18349
18685
  return null;
18350
- 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));
18351
18687
  if (!cfg)
18352
18688
  return null;
18353
- 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];
18354
18690
  if (!out)
18355
18691
  return null;
18356
18692
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
18357
18693
  }
18358
18694
  function detectRunner(repoPath, packageManager) {
18359
- if (existsSync13(join11(repoPath, "bun.lock")) || existsSync13(join11(repoPath, "bun.lockb")))
18695
+ if (existsSync14(join11(repoPath, "bun.lock")) || existsSync14(join11(repoPath, "bun.lockb")))
18360
18696
  return "bun";
18361
- if (existsSync13(join11(repoPath, "pnpm-lock.yaml")))
18697
+ if (existsSync14(join11(repoPath, "pnpm-lock.yaml")))
18362
18698
  return "pnpm";
18363
- 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"))
18364
18700
  return "yarn";
18365
18701
  return "npm";
18366
18702
  }
18367
18703
 
18368
18704
  // ../daemon/src/repos/create.ts
18369
- import { existsSync as existsSync14, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
18705
+ import { existsSync as existsSync15, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
18370
18706
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
18371
18707
  import { homedir as homedir6 } from "os";
18372
18708
  import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
@@ -18381,13 +18717,13 @@ function planProject(opts, home = homedir6()) {
18381
18717
  const first = firstProjectsFolder(opts.parent, home);
18382
18718
  const parent = first ?? resolveParent(opts.parent);
18383
18719
  const dir = join12(parent, opts.name.trim());
18384
- if (existsSync14(dir))
18720
+ if (existsSync15(dir))
18385
18721
  throw new UserError(`${dir} already exists`);
18386
18722
  return { parent, dir, makeParent: first !== null };
18387
18723
  }
18388
18724
  function firstProjectsFolder(raw, home) {
18389
18725
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
18390
- if (typed !== join12(home, PROJECTS_FOLDER) || existsSync14(typed) || !existsSync14(home))
18726
+ if (typed !== join12(home, PROJECTS_FOLDER) || existsSync15(typed) || !existsSync15(home))
18391
18727
  return null;
18392
18728
  return join12(realpathSync2(home), PROJECTS_FOLDER);
18393
18729
  }
@@ -18397,10 +18733,10 @@ function planInPlace(opts) {
18397
18733
  throw new UserError(`${opts.name} is not a folder name`);
18398
18734
  }
18399
18735
  const typed = join12(resolveParent(opts.parent), name);
18400
- if (!existsSync14(typed) || !statSync4(typed).isDirectory())
18736
+ if (!existsSync15(typed) || !statSync4(typed).isDirectory())
18401
18737
  throw new UserError(`${typed} is not a folder`);
18402
18738
  const dir = realpathSync2(typed);
18403
- if (existsSync14(join12(dir, ".git")))
18739
+ if (existsSync15(join12(dir, ".git")))
18404
18740
  throw new UserError(`${dir} is already a project; open it instead`);
18405
18741
  if (!holdsNothing(readdirSync4(dir))) {
18406
18742
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
@@ -18411,7 +18747,7 @@ function resolveParent(raw) {
18411
18747
  const typed = expandTilde(raw.trim());
18412
18748
  if (!typed.startsWith("/"))
18413
18749
  throw new UserError(`${raw} is not a folder path`);
18414
- if (!existsSync14(typed))
18750
+ if (!existsSync15(typed))
18415
18751
  throw new UserError(`${raw} does not exist; make it first`);
18416
18752
  if (!statSync4(typed).isDirectory())
18417
18753
  throw new UserError(`${raw} is not a folder`);
@@ -18527,7 +18863,7 @@ async function ok(p, what) {
18527
18863
  }
18528
18864
 
18529
18865
  // ../daemon/src/repos/watcher.ts
18530
- import { existsSync as existsSync15, watch } from "fs";
18866
+ import { existsSync as existsSync16, watch } from "fs";
18531
18867
  import { basename as basename2, join as join13 } from "path";
18532
18868
  function watchDefaultBranch(repoPath, branch, onMove) {
18533
18869
  let last = null;
@@ -18639,7 +18975,7 @@ function watchConfigFile(repoPath, onChange) {
18639
18975
  const armInner = () => {
18640
18976
  inner?.close();
18641
18977
  inner = null;
18642
- if (!existsSync15(dir))
18978
+ if (!existsSync16(dir))
18643
18979
  return;
18644
18980
  try {
18645
18981
  inner = watch(dir, (_event, filename) => {
@@ -18693,7 +19029,7 @@ class RepoRegistry {
18693
19029
  }
18694
19030
  async boot() {
18695
19031
  const { state } = this.d;
18696
- 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));
18697
19033
  for (const wt of gone)
18698
19034
  await this.d.worktrees.forgetGone(wt);
18699
19035
  for (const repo of state.repos) {
@@ -18760,7 +19096,7 @@ class RepoRegistry {
18760
19096
  return `${repo.name} is set up now`;
18761
19097
  if (main.promptedAt || main.lastTurn)
18762
19098
  return `${repo.name} has a chat now`;
18763
- if (existsSync16(join14(repo.path, ".claude")))
19099
+ if (existsSync17(join14(repo.path, ".claude")))
18764
19100
  return `an agent has already run in ${repo.name}`;
18765
19101
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
18766
19102
  return `${repo.name} has files in it now`;
@@ -18837,7 +19173,7 @@ class RepoRegistry {
18837
19173
  }
18838
19174
  async register(rawPath, made) {
18839
19175
  const path = expandTilde(rawPath);
18840
- if (!existsSync16(path))
19176
+ if (!existsSync17(path))
18841
19177
  throw new UserError(`${path} does not exist`);
18842
19178
  if (!await isGitRepo(path))
18843
19179
  throw new UserError(`${path} is not a git repository`);
@@ -18887,17 +19223,18 @@ class RepoRegistry {
18887
19223
  const mine = this.d.state.worktrees.filter((w) => w.repoId === repoId);
18888
19224
  const tasks = mine.filter((w) => w.kind !== "main" && w.kind !== "spare");
18889
19225
  if (tasks.length > 0) {
18890
- 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`);
18891
19227
  }
18892
19228
  this.stopWatcher(repoId);
18893
19229
  for (const wt of mine.filter((w) => w.kind === "spare"))
18894
- await this.d.worktrees.remove(wt.id, { spare: true });
19230
+ await this.d.worktrees.discardWorktree(wt.id);
18895
19231
  for (const wt of mine.filter((w) => w.kind === "main")) {
18896
19232
  await this.d.runtime.stop(wt.id);
18897
19233
  this.d.state.removeWorktree(wt.id);
18898
19234
  this.d.worktrees.deleteChat(wt.id);
18899
19235
  releasePort(wt.proxyPort);
18900
19236
  }
19237
+ this.d.drafts?.drop(draftKey(repoId));
18901
19238
  this.d.state.removeRepo(repoId);
18902
19239
  this.d.hub.emit("reposChanged");
18903
19240
  this.d.hub.emit("worktreesChanged");
@@ -18909,7 +19246,7 @@ class RepoRegistry {
18909
19246
  throw new UserError(current.reason);
18910
19247
  const was = configTarget(repo.path, !!repo.made);
18911
19248
  const rel = configSibling(was, kind);
18912
- const moved = rel !== was && existsSync16(join14(repo.path, was));
19249
+ const moved = rel !== was && existsSync17(join14(repo.path, was));
18913
19250
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
18914
19251
  try {
18915
19252
  rmSync2(join14(repo.path, was));
@@ -18924,7 +19261,7 @@ class RepoRegistry {
18924
19261
  const file = join14(repo.path, rel);
18925
19262
  try {
18926
19263
  mkdirSync5(dirname6(file), { recursive: true });
18927
- writeFileSync5(file, `${JSON.stringify(configBody(repo.path, rel, config), null, 2)}
19264
+ writeFileSync6(file, `${JSON.stringify(configBody(repo.path, rel, config), null, 2)}
18928
19265
  `);
18929
19266
  } catch (e) {
18930
19267
  log.warn(repoId, `could not write ${rel}`, e);
@@ -18999,7 +19336,7 @@ class RepoRegistry {
18999
19336
  }
19000
19337
  placeConfig(repo) {
19001
19338
  const rel = configTarget(repo.path, !!repo.made);
19002
- if (isLocalConfigFile(rel) && existsSync16(join14(repo.path, rel))) {
19339
+ if (isLocalConfigFile(rel) && existsSync17(join14(repo.path, rel))) {
19003
19340
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
19004
19341
  }
19005
19342
  if (repo.configFile === rel)
@@ -19336,7 +19673,7 @@ function retitle(pages, key, title) {
19336
19673
  function rank(pages, now, n = KEEP) {
19337
19674
  return Object.keys(pages).sort(byStanding(pages, now)).slice(0, n);
19338
19675
  }
19339
- function entries(pages, now) {
19676
+ function entries2(pages, now) {
19340
19677
  return rank(pages, now).map((path) => {
19341
19678
  const v = pages[path];
19342
19679
  return { path, score: rounded(decayed(v, now)), last: v.last, ...v.title ? { title: v.title } : {} };
@@ -20031,7 +20368,7 @@ function unseenOf(candidates, seen, hashes) {
20031
20368
  }
20032
20369
 
20033
20370
  // ../daemon/src/routes/service.ts
20034
- var SAVE_DELAY_MS = 5000;
20371
+ var SAVE_DELAY_MS2 = 5000;
20035
20372
  var MAX_MANIFESTS = 50;
20036
20373
  var SCAN_CACHE = 20;
20037
20374
  var MAX_HASHED = 64;
@@ -20106,7 +20443,7 @@ class RouteService {
20106
20443
  }
20107
20444
  history(repoId) {
20108
20445
  const pages = this.d.state.visitsOf(repoId);
20109
- return pages ? entries(pages, this.now()) : [];
20446
+ return pages ? entries2(pages, this.now()) : [];
20110
20447
  }
20111
20448
  historyAll() {
20112
20449
  const out = {};
@@ -20331,7 +20668,7 @@ class RouteService {
20331
20668
  } catch (e) {
20332
20669
  log.error("routes", "could not write the page lists", e);
20333
20670
  }
20334
- }, this.d.saveDelayMs ?? SAVE_DELAY_MS);
20671
+ }, this.d.saveDelayMs ?? SAVE_DELAY_MS2);
20335
20672
  this.saveTimer.unref();
20336
20673
  }
20337
20674
  repoOf(worktreeId) {
@@ -20343,7 +20680,7 @@ class RouteService {
20343
20680
  }
20344
20681
 
20345
20682
  // ../daemon/src/runtime/bridge-script.ts
20346
- import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
20683
+ import { existsSync as existsSync18, readFileSync as readFileSync11, statSync as statSync5 } from "fs";
20347
20684
  var LEARNED_MAX = 8;
20348
20685
 
20349
20686
  class BridgeScript {
@@ -20373,7 +20710,7 @@ class BridgeScript {
20373
20710
  this.cache = null;
20374
20711
  }
20375
20712
  get() {
20376
- if (!existsSync17(this.path))
20713
+ if (!existsSync18(this.path))
20377
20714
  return "// toyon bridge not built";
20378
20715
  const s = statSync5(this.path);
20379
20716
  const stamp = `${s.mtimeMs}:${s.size}`;
@@ -20382,7 +20719,7 @@ class BridgeScript {
20382
20719
  this.stamp = stamp;
20383
20720
  const origins = [...this.origins, ...this.learned];
20384
20721
  this.cache = `window.__toyonShellOrigins=${JSON.stringify(origins)};
20385
- ${readFileSync10(this.path, "utf8")}`;
20722
+ ${readFileSync11(this.path, "utf8")}`;
20386
20723
  return this.cache;
20387
20724
  }
20388
20725
  }
@@ -20400,7 +20737,7 @@ function servedLocally(origin) {
20400
20737
  }
20401
20738
 
20402
20739
  // ../daemon/src/runtime/memory.ts
20403
- var MAC_PRESSURE_NORMAL = 1;
20740
+ var MAC_PRESSURE_CRITICAL = 4;
20404
20741
  var AVAILABLE_FLOOR = 0.15;
20405
20742
  var unsupportedLogged = false;
20406
20743
  async function memoryTight() {
@@ -20418,10 +20755,12 @@ async function darwin() {
20418
20755
  const [level, available] = r.out.split(/\s+/).map(Number);
20419
20756
  if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
20420
20757
  return null;
20421
- const pressure = level === MAC_PRESSURE_NORMAL ? "normal" : level === 2 ? "warn" : level === 4 ? "critical" : `level ${level}`;
20758
+ return darwinSignal(level, available);
20759
+ }
20760
+ function darwinSignal(level, available) {
20761
+ const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
20422
20762
  return {
20423
- tight: level !== MAC_PRESSURE_NORMAL,
20424
- backstop: level === MAC_PRESSURE_NORMAL && available < AVAILABLE_FLOOR * 100,
20763
+ tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
20425
20764
  why: `memory pressure ${pressure} with ${available}% available`
20426
20765
  };
20427
20766
  }
@@ -20441,7 +20780,7 @@ async function linux() {
20441
20780
  if (!total || available === null)
20442
20781
  return null;
20443
20782
  const percent = Math.round(available / total * 100);
20444
- return { tight: available / total < AVAILABLE_FLOOR, backstop: false, why: `${percent}% of memory available` };
20783
+ return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
20445
20784
  }
20446
20785
  async function sampleCosts(pgids) {
20447
20786
  const costs = new Map;
@@ -20593,16 +20932,12 @@ class IdlePolicy {
20593
20932
  }
20594
20933
  async checkPressure() {
20595
20934
  const signal = await (this.d.memory ?? memoryTight)();
20596
- if (!signal || !signal.tight && !signal.backstop)
20935
+ if (!signal?.tight)
20597
20936
  return;
20598
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];
20599
20938
  if (!pick)
20600
20939
  return;
20601
- const why = `${signal.why}; least recently used`;
20602
- if (signal.tight)
20603
- this.sleep(pick.id, why);
20604
- else
20605
- log.info(pick.id, `would sleep on the backstop: ${why}`);
20940
+ this.sleep(pick.id, `${signal.why}; least recently used`);
20606
20941
  }
20607
20942
  async sampleCosts() {
20608
20943
  this.recordViewed();
@@ -20653,10 +20988,7 @@ class IdlePolicy {
20653
20988
  return true;
20654
20989
  }
20655
20990
  busy(id) {
20656
- const agent = this.d.runtime.agentFor(id);
20657
- if (agent && (agent.status !== "idle" || agent.queueItems.length > 0))
20658
- return true;
20659
- return this.d.runtime.holdCount(id) > 0;
20991
+ return this.d.runtime.busy(id);
20660
20992
  }
20661
20993
  reconsider(id) {
20662
20994
  if (!this.candidate(id)) {
@@ -20740,7 +21072,7 @@ function humanMs(ms) {
20740
21072
  import { homedir as homedir9 } from "os";
20741
21073
 
20742
21074
  // ../daemon/src/agent/config.ts
20743
- import { existsSync as existsSync18, readFileSync as readFileSync11 } from "fs";
21075
+ import { existsSync as existsSync19, readFileSync as readFileSync12 } from "fs";
20744
21076
  import { homedir as homedir8 } from "os";
20745
21077
  import { join as join16 } from "path";
20746
21078
  function agentConfigFiles(spec, repoPath, home = homedir8()) {
@@ -20748,7 +21080,7 @@ function agentConfigFiles(spec, repoPath, home = homedir8()) {
20748
21080
  id,
20749
21081
  label,
20750
21082
  path,
20751
- exists: existsSync18(path)
21083
+ exists: existsSync19(path)
20752
21084
  });
20753
21085
  if (spec.id === "claude") {
20754
21086
  return [
@@ -20780,10 +21112,10 @@ function describeAgentConfig(spec, repoPath, home = homedir8()) {
20780
21112
  return { agent: spec.id, files, servers };
20781
21113
  }
20782
21114
  function readJson(path) {
20783
- if (!existsSync18(path))
21115
+ if (!existsSync19(path))
20784
21116
  return null;
20785
21117
  try {
20786
- const v = JSON.parse(readFileSync11(path, "utf8"));
21118
+ const v = JSON.parse(readFileSync12(path, "utf8"));
20787
21119
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
20788
21120
  } catch (e) {
20789
21121
  log.warn("agent-config", `${path} is not valid JSON`, e);
@@ -20801,11 +21133,11 @@ function serversOf(raw, scope) {
20801
21133
  });
20802
21134
  }
20803
21135
  function codexServers(path) {
20804
- if (!existsSync18(path))
21136
+ if (!existsSync19(path))
20805
21137
  return [];
20806
21138
  const out = [];
20807
21139
  let current = null;
20808
- for (const raw of readFileSync11(path, "utf8").split(`
21140
+ for (const raw of readFileSync12(path, "utf8").split(`
20809
21141
  `)) {
20810
21142
  const line = raw.trim();
20811
21143
  const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
@@ -21376,6 +21708,7 @@ var handlers = {
21376
21708
  ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
21377
21709
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
21378
21710
  ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
21711
+ await gitStatus(s, ctx, msg.worktreeId);
21379
21712
  return;
21380
21713
  }
21381
21714
  if (!ctx.subscribe(msg.worktreeId))
@@ -21410,6 +21743,9 @@ var handlers = {
21410
21743
  "mark-unread"(msg, _ctx, s) {
21411
21744
  s.turns.markUnread(msg.worktreeId);
21412
21745
  },
21746
+ "set-draft"(msg, _ctx, s) {
21747
+ s.drafts.set(msg.boxId, msg.text, msg.clientId);
21748
+ },
21413
21749
  "refresh-git"(msg, _ctx, s) {
21414
21750
  s.worktrees.recount(msg.repoId);
21415
21751
  },
@@ -21422,7 +21758,12 @@ var handlers = {
21422
21758
  "forget-visit"(msg, _ctx, s) {
21423
21759
  s.routes.forget(msg.repoId, msg.path);
21424
21760
  },
21425
- 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
+ }
21426
21767
  requireRun(s, msg.worktreeId);
21427
21768
  const agent = s.runtime.agentFor(msg.worktreeId);
21428
21769
  if (!agent)
@@ -21475,8 +21816,8 @@ var handlers = {
21475
21816
  ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
21476
21817
  })(), "batch");
21477
21818
  },
21478
- async "remove-worktree"(msg, _ctx, s) {
21479
- await s.worktrees.remove(msg.worktreeId);
21819
+ async "archive-worktree"(msg, _ctx, s) {
21820
+ await s.worktrees.archiveWorktree(msg.worktreeId);
21480
21821
  },
21481
21822
  "list-archived"(msg, ctx, s) {
21482
21823
  ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
@@ -21496,7 +21837,9 @@ var handlers = {
21496
21837
  async "read-file"(msg, ctx, s) {
21497
21838
  const head = { t: "file-read", worktreeId: msg.worktreeId, path: msg.path, ref: msg.ref, seq: msg.seq };
21498
21839
  try {
21499
- 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 });
21500
21843
  } catch (e) {
21501
21844
  if (!(e instanceof UserError))
21502
21845
  log.error(msg.worktreeId, "read-file failed", e);
@@ -21513,7 +21856,7 @@ var handlers = {
21513
21856
  ctx.reply({ t: "git-commit", worktreeId: msg.worktreeId, sha: msg.sha, files });
21514
21857
  },
21515
21858
  async land(msg, ctx, s) {
21516
- const { result, removeIds } = await s.worktrees.land(msg.worktreeId, msg.message);
21859
+ const { result, archiveIds } = await s.worktrees.land(msg.worktreeId, msg.message);
21517
21860
  const suggestion = !result.ok && result.conflict ? "Bring main into this branch and resolve the conflicts, then verify the app still works." : undefined;
21518
21861
  if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
21519
21862
  fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
@@ -21521,7 +21864,7 @@ var handlers = {
21521
21864
  await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
21522
21865
  merged: result.ok,
21523
21866
  url: result.url,
21524
- removeIds: removeIds ?? [],
21867
+ archiveIds: archiveIds ?? [],
21525
21868
  ...suggestion ? { suggestion } : {}
21526
21869
  }));
21527
21870
  },
@@ -21547,15 +21890,13 @@ var handlers = {
21547
21890
  ctx.reply({ t: "self", self: s.self.get() });
21548
21891
  },
21549
21892
  "restart-daemon"(_msg, _ctx, s) {
21550
- const busy = s.state.worktrees.filter((w) => s.runtime.agentFor(w.id)?.status === "working");
21551
- if (busy.length > 0) {
21552
- const names = busy.map((w) => w.title).join(", ");
21553
- throw new UserError(`still working on ${names}; restart once that settles`);
21554
- }
21555
- const refused = s.restart();
21893
+ const refused = s.restarter.request();
21556
21894
  if (refused)
21557
21895
  throw new UserError(refused);
21558
21896
  },
21897
+ "update-now"(_msg, _ctx, s) {
21898
+ return s.update.updateNow();
21899
+ },
21559
21900
  async commit(msg, ctx, s) {
21560
21901
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
21561
21902
  await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
@@ -21574,7 +21915,7 @@ var handlers = {
21574
21915
  fireAndForget(msg.worktreeId, agent.warmCommands(), "warm commands");
21575
21916
  },
21576
21917
  async "list-files"(msg, ctx, s) {
21577
- 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) });
21578
21919
  },
21579
21920
  async search(msg, ctx, s) {
21580
21921
  const { hits, truncated } = await s.files.search(msg.worktreeId, msg.query);
@@ -21792,7 +22133,7 @@ async function dispatch(msg, ctx, s) {
21792
22133
  }
21793
22134
 
21794
22135
  // ../daemon/src/server/http.ts
21795
- import { existsSync as existsSync19 } from "fs";
22136
+ import { existsSync as existsSync20 } from "fs";
21796
22137
  import { join as join17 } from "path";
21797
22138
  var IMMUTABLE = "private, max-age=31536000, immutable";
21798
22139
  var NO_STORE = "no-store";
@@ -21863,6 +22204,12 @@ function createFetch(opts) {
21863
22204
  headers["set-cookie"] = grantCookie(grant, remote.host);
21864
22205
  return Response.json(await opts.bootstrap(), { headers });
21865
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
+ }
21866
22213
  if (url.pathname === "/register" && req.method === "POST") {
21867
22214
  if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
21868
22215
  return new Response("unauthorized", { status: 401 });
@@ -21885,7 +22232,7 @@ function createFetch(opts) {
21885
22232
  return new Response("unauthorized", { status: 401 });
21886
22233
  const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
21887
22234
  const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
21888
- const path = places.find((p) => !!p && existsSync19(p));
22235
+ const path = places.find((p) => !!p && existsSync20(p));
21889
22236
  if (!path)
21890
22237
  return new Response("not found", { status: 404 });
21891
22238
  return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
@@ -21893,13 +22240,13 @@ function createFetch(opts) {
21893
22240
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
21894
22241
  const file = join17(opts.shellDist, rel.replaceAll("..", ""));
21895
22242
  const hashed = rel.startsWith("/assets/");
21896
- if (existsSync19(file) && Bun.file(file).size > 0) {
22243
+ if (existsSync20(file) && Bun.file(file).size > 0) {
21897
22244
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
21898
22245
  }
21899
22246
  if (hashed)
21900
22247
  return new Response("not found", { status: 404 });
21901
22248
  const index = join17(opts.shellDist, "index.html");
21902
- if (existsSync19(index))
22249
+ if (existsSync20(index))
21903
22250
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
21904
22251
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
21905
22252
  };
@@ -21997,6 +22344,7 @@ function startServer(opts) {
21997
22344
  ...s.runtime.tiers()
21998
22345
  },
21999
22346
  costs: s.idle.costs(),
22347
+ updates: s.update.status(),
22000
22348
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
22001
22349
  });
22002
22350
  let statusesInFlight = false;
@@ -22106,28 +22454,35 @@ function startServer(opts) {
22106
22454
  });
22107
22455
  s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
22108
22456
  s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
22457
+ s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
22109
22458
  s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
22110
22459
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
22111
- const helloFrame = async () => ({
22112
- t: "hello",
22113
- version,
22114
- protocol: PROTOCOL_VERSION,
22115
- repos: s.state.repos,
22116
- rows: await s.worktrees.rows({ quick: true }),
22117
- spares: s.worktrees.spares(),
22118
- themes: s.themes.themes,
22119
- themePrefs: s.themes.prefs,
22120
- agents: agentInfos(),
22121
- defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
22122
- agentChosen: s.state.defaultAgent !== undefined,
22123
- home: homedir9(),
22124
- folderDialog: process.platform === "darwin" && !cloud.enabled,
22125
- remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
22126
- gitIdentity: await s.repos.gitIdentity(),
22127
- pending: s.repos.pending,
22128
- visits: s.routes.historyAll(),
22129
- self: s.self.get()
22130
- });
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
+ };
22131
22486
  let branded = false;
22132
22487
  const serverConfig = {
22133
22488
  hostname: cloud.bindHost,
@@ -22143,7 +22498,8 @@ function startServer(opts) {
22143
22498
  noteShellOrigin: opts.noteShellOrigin,
22144
22499
  remote: opts.remote,
22145
22500
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
22146
- bootstrap: helloFrame
22501
+ bootstrap: helloFrame,
22502
+ restart: () => s.restarter.request()
22147
22503
  }),
22148
22504
  websocket: {
22149
22505
  maxPayloadLength: 64 * 1024 * 1024,
@@ -22220,7 +22576,8 @@ function startServer(opts) {
22220
22576
  } catch (e) {
22221
22577
  if (!(e instanceof UserError || e instanceof ThemeImportError))
22222
22578
  log.error("ws", `${parsed.msg.t} failed`, e);
22223
- const worktreeId = "worktreeId" in parsed.msg ? parsed.msg.worktreeId : undefined;
22579
+ const m = parsed.msg;
22580
+ const worktreeId = "worktreeId" in m ? m.worktreeId : ("archiveId" in m) ? m.archiveId : undefined;
22224
22581
  send(ws, {
22225
22582
  t: "error",
22226
22583
  message: e instanceof Error ? e.message : String(e),
@@ -22249,7 +22606,7 @@ function startServer(opts) {
22249
22606
  }
22250
22607
 
22251
22608
  // ../daemon/src/themes/store.ts
22252
- import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
22609
+ import { existsSync as existsSync21, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
22253
22610
  import { homedir as homedir10 } from "os";
22254
22611
  import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve8 } from "path";
22255
22612
 
@@ -23148,14 +23505,14 @@ class ThemeStore {
23148
23505
  name: json?.name ?? base,
23149
23506
  source: "file"
23150
23507
  });
23151
- writeFileSync6(join18(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
23508
+ writeFileSync7(join18(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
23152
23509
  `);
23153
23510
  this.load();
23154
23511
  return theme;
23155
23512
  }
23156
23513
  }
23157
23514
  function readJsonc(file) {
23158
- return parse7(readFileSync12(file, "utf8"), [], { allowTrailingComma: true });
23515
+ return parse7(readFileSync13(file, "utf8"), [], { allowTrailingComma: true });
23159
23516
  }
23160
23517
  function readVscodeTheme(file, depth = 0) {
23161
23518
  const json = readJsonc(file);
@@ -23177,7 +23534,7 @@ function readVscodeTheme(file, depth = 0) {
23177
23534
  return t;
23178
23535
  }
23179
23536
  function loadDir(dir) {
23180
- if (!existsSync20(dir))
23537
+ if (!existsSync21(dir))
23181
23538
  return [];
23182
23539
  const out = [];
23183
23540
  for (const f of readdirSync5(dir)) {
@@ -23199,7 +23556,7 @@ function loadDir(dir) {
23199
23556
  return out;
23200
23557
  }
23201
23558
  function discover(dir) {
23202
- if (!existsSync20(dir))
23559
+ if (!existsSync21(dir))
23203
23560
  return [];
23204
23561
  const out = [];
23205
23562
  let entries;
@@ -23212,18 +23569,18 @@ function discover(dir) {
23212
23569
  const extDir = join18(dir, name);
23213
23570
  const pkgFile = join18(extDir, "package.json");
23214
23571
  try {
23215
- if (!statSync6(extDir).isDirectory() || !existsSync20(pkgFile))
23572
+ if (!statSync6(extDir).isDirectory() || !existsSync21(pkgFile))
23216
23573
  continue;
23217
- const pkg = JSON.parse(readFileSync12(pkgFile, "utf8"));
23574
+ const pkg = JSON.parse(readFileSync13(pkgFile, "utf8"));
23218
23575
  const themes = pkg.contributes?.themes;
23219
23576
  if (!Array.isArray(themes))
23220
23577
  continue;
23221
23578
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
23222
23579
  let nls = {};
23223
23580
  const nlsFile = join18(extDir, "package.nls.json");
23224
- if (existsSync20(nlsFile)) {
23581
+ if (existsSync21(nlsFile)) {
23225
23582
  try {
23226
- nls = JSON.parse(readFileSync12(nlsFile, "utf8"));
23583
+ nls = JSON.parse(readFileSync13(nlsFile, "utf8"));
23227
23584
  } catch {}
23228
23585
  }
23229
23586
  for (const c of themes) {
@@ -23247,6 +23604,211 @@ function discover(dir) {
23247
23604
  return out;
23248
23605
  }
23249
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
+
23250
23812
  // ../daemon/src/worktrees/chats.ts
23251
23813
  var CHAT_HITS_PER_WORKTREE = 20;
23252
23814
  var CHAT_HITS_MAX = 200;
@@ -23842,7 +24404,7 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
23842
24404
  }
23843
24405
 
23844
24406
  // ../daemon/src/worktrees/service.ts
23845
- import { existsSync as existsSync22, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
24407
+ import { existsSync as existsSync23, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
23846
24408
  import { basename as basename6, dirname as dirname8, join as join21 } from "path";
23847
24409
 
23848
24410
  // ../daemon/src/git/archive.ts
@@ -23899,6 +24461,24 @@ async function commitOf(repoPath, ref) {
23899
24461
  const r = await git(repoPath, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`);
23900
24462
  return r.ok ? r.out : null;
23901
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
+ }
23902
24482
 
23903
24483
  // ../daemon/src/git/land.ts
23904
24484
  async function commitWorktree(worktreePath, message) {
@@ -24111,7 +24691,7 @@ function compareUrl(remoteUrl, base, branch) {
24111
24691
  }
24112
24692
 
24113
24693
  // ../daemon/src/worktrees/archive.ts
24114
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } 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";
24115
24695
  import { join as join19 } from "path";
24116
24696
  class WorktreeArchive {
24117
24697
  dir;
@@ -24121,10 +24701,10 @@ class WorktreeArchive {
24121
24701
  mkdirSync6(dir, { recursive: true });
24122
24702
  for (const id of readdirSync6(dir)) {
24123
24703
  const file = this.filesOf(id).record;
24124
- if (!existsSync21(file))
24704
+ if (!existsSync22(file))
24125
24705
  continue;
24126
24706
  try {
24127
- this.records.set(id, JSON.parse(readFileSync13(file, "utf8")));
24707
+ this.records.set(id, JSON.parse(readFileSync14(file, "utf8")));
24128
24708
  } catch (e) {
24129
24709
  log.warn("archive", `skipped an unreadable record: ${file}`, e);
24130
24710
  }
@@ -24138,7 +24718,7 @@ class WorktreeArchive {
24138
24718
  mkdirSync6(at.dir, { recursive: true });
24139
24719
  move(from.transcript, at.transcript);
24140
24720
  move(from.attachments, at.attachments);
24141
- writeFileSync7(at.record, JSON.stringify(rec, null, 2));
24721
+ writeFileSync8(at.record, JSON.stringify(rec, null, 2));
24142
24722
  this.records.set(rec.worktree.id, rec);
24143
24723
  }
24144
24724
  take(id, to) {
@@ -24187,13 +24767,14 @@ function summarize(r, repoId) {
24187
24767
  restorable: !!r.kept,
24188
24768
  ...r.kept?.snapshot ? { uncommitted: true } : {},
24189
24769
  ...r.worktree.landed ? { landed: true } : {},
24190
- ...r.cost !== undefined ? { cost: r.cost } : {}
24770
+ ...r.cost !== undefined ? { cost: r.cost } : {},
24771
+ ...r.auto ? { auto: r.auto } : {}
24191
24772
  };
24192
24773
  }
24193
24774
  function lastUsage(transcript) {
24194
- if (!existsSync21(transcript))
24775
+ if (!existsSync22(transcript))
24195
24776
  return null;
24196
- const lines = readFileSync13(transcript, "utf8").split(`
24777
+ const lines = readFileSync14(transcript, "utf8").split(`
24197
24778
  `);
24198
24779
  for (let i = lines.length - 1;i >= 0; i--) {
24199
24780
  if (!lines[i]?.includes('"usage"'))
@@ -24207,9 +24788,9 @@ function lastUsage(transcript) {
24207
24788
  return null;
24208
24789
  }
24209
24790
  function firstPrompt(transcript) {
24210
- if (!existsSync21(transcript))
24791
+ if (!existsSync22(transcript))
24211
24792
  return;
24212
- for (const line of readFileSync13(transcript, "utf8").split(`
24793
+ for (const line of readFileSync14(transcript, "utf8").split(`
24213
24794
  `)) {
24214
24795
  if (!line.includes('"user-message"'))
24215
24796
  continue;
@@ -24222,10 +24803,67 @@ function firstPrompt(transcript) {
24222
24803
  return;
24223
24804
  }
24224
24805
  function move(from, to) {
24225
- if (!existsSync21(from))
24806
+ if (!existsSync22(from))
24226
24807
  return;
24227
24808
  rmSync4(to, { recursive: true, force: true });
24228
- renameSync3(from, to);
24809
+ renameSync4(from, to);
24810
+ }
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
+ }
24229
24867
  }
24230
24868
 
24231
24869
  // ../daemon/src/worktrees/spare.ts
@@ -24243,7 +24881,7 @@ class SparePool {
24243
24881
  adopt(repoId) {
24244
24882
  const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
24245
24883
  for (const extra of persisted.slice(1))
24246
- fireAndForget(extra.id, this.d.remove(extra.id), "stale spare removal");
24884
+ fireAndForget(extra.id, this.d.discard(extra.id), "stale spare removal");
24247
24885
  const spare = persisted[0];
24248
24886
  if (!spare)
24249
24887
  return;
@@ -24300,7 +24938,7 @@ class SparePool {
24300
24938
  log.warn(repoId, "spare warm-up failed; rolling back", e);
24301
24939
  this.spares.delete(repoId);
24302
24940
  if (entry.worktreeId && this.d.state.worktree(entry.worktreeId)) {
24303
- 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));
24304
24942
  }
24305
24943
  }
24306
24944
  }
@@ -24506,9 +25144,6 @@ function stopOf(prev, next) {
24506
25144
  return "failed";
24507
25145
  return null;
24508
25146
  }
24509
- function isUnseen(wt) {
24510
- return wt.unread === true || wt.lastTurn != null && (wt.seenAt == null || wt.seenAt < wt.lastTurn.at);
24511
- }
24512
25147
 
24513
25148
  // ../daemon/src/worktrees/service.ts
24514
25149
  var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
@@ -24533,6 +25168,7 @@ class WorktreeService {
24533
25168
  d;
24534
25169
  spare;
24535
25170
  archive;
25171
+ archiving = new Map;
24536
25172
  countsCache = new Map;
24537
25173
  discoverCache = new Map;
24538
25174
  usage = new Map;
@@ -24546,8 +25182,8 @@ class WorktreeService {
24546
25182
  runtime: d.runtime,
24547
25183
  paths: d.paths,
24548
25184
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
24549
- remove: async (id) => {
24550
- await this.remove(id, { spare: true });
25185
+ discard: async (id) => {
25186
+ await this.discardWorktree(id);
24551
25187
  }
24552
25188
  });
24553
25189
  for (const wt of d.state.worktrees)
@@ -24699,7 +25335,7 @@ class WorktreeService {
24699
25335
  const branch = kind === "pr" ? `pr/${number}` : ref;
24700
25336
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
24701
25337
  let slug = title;
24702
- if (existsSync22(join21(this.d.paths.worktreesDir, repo.name, slug)))
25338
+ if (existsSync23(join21(this.d.paths.worktreesDir, repo.name, slug)))
24703
25339
  slug = `${slug}-${shortId().slice(0, 4)}`;
24704
25340
  const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
24705
25341
  await withRepoLock(repo.path, async () => {
@@ -24855,12 +25491,32 @@ class WorktreeService {
24855
25491
  }
24856
25492
  }), "auto-naming");
24857
25493
  }
24858
- async remove(worktreeId, opts = {}) {
25494
+ archiveWorktree(worktreeId, opts = {}) {
25495
+ const running = this.archiving.get(worktreeId);
25496
+ if (running)
25497
+ return running;
24859
25498
  const wt = this.d.state.worktree(worktreeId);
24860
- if (!wt || !(canRemove(wt) || opts.spare && wt.kind === "spare"))
24861
- 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;
24862
25519
  const repo = this.d.state.requireRepo(wt.repoId);
24863
- const archive = wt.kind !== "spare" && opts.archive !== false;
24864
25520
  await this.d.runtime.stop(worktreeId);
24865
25521
  const kept = await withRepoLock(repo.path, async () => {
24866
25522
  const k = archive ? await this.keep(repo, wt) : null;
@@ -24878,9 +25534,12 @@ class WorktreeService {
24878
25534
  releasePort(wt.proxyPort);
24879
25535
  let archived = null;
24880
25536
  if (archive)
24881
- archived = this.archiveChat(wt, repo, kept, sessionId);
24882
- else
25537
+ archived = this.archiveChat(wt, repo, kept, sessionId, reason);
25538
+ else {
24883
25539
  this.deleteChat(worktreeId);
25540
+ if (wt.lands?.length)
25541
+ await dropLandRefs(repo.path, worktreeId);
25542
+ }
24884
25543
  this.d.hub.emit("worktreesChanged");
24885
25544
  return archived;
24886
25545
  }
@@ -24893,7 +25552,7 @@ class WorktreeService {
24893
25552
  log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
24894
25553
  return kept;
24895
25554
  }
24896
- archiveChat(wt, repo, kept, sessionId) {
25555
+ archiveChat(wt, repo, kept, sessionId, reason) {
24897
25556
  const files = this.chatFiles(wt.id);
24898
25557
  const prompt = firstPrompt(files.transcript);
24899
25558
  const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
@@ -24904,7 +25563,8 @@ class WorktreeService {
24904
25563
  ...sessionId ? { sessionId } : {},
24905
25564
  ...prompt ? { prompt } : {},
24906
25565
  ...cost !== undefined ? { cost } : {},
24907
- ...kept ? { kept } : {}
25566
+ ...kept ? { kept } : {},
25567
+ ...reason ? { auto: reason } : {}
24908
25568
  };
24909
25569
  try {
24910
25570
  this.archive.put(rec, files);
@@ -24950,6 +25610,9 @@ class WorktreeService {
24950
25610
  });
24951
25611
  this.archiveChat(wt, repo, kept, sessionId);
24952
25612
  }
25613
+ hasArchived(archiveId) {
25614
+ return !!this.archive.get(archiveId);
25615
+ }
24953
25616
  archived(repoId) {
24954
25617
  const repo = this.d.state.repo(repoId);
24955
25618
  return repo ? this.archive.list(repo) : [];
@@ -24971,6 +25634,28 @@ class WorktreeService {
24971
25634
  const files = this.archive.chatFiles(archiveId);
24972
25635
  return files && isAttachmentFile(file) ? join21(files.attachments, file) : null;
24973
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
+ }
24974
25659
  repoOf(rec) {
24975
25660
  return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
24976
25661
  }
@@ -24985,7 +25670,9 @@ class WorktreeService {
24985
25670
  const kept = rec.kept;
24986
25671
  if (!kept)
24987
25672
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
24988
- 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;
24989
25676
  const branch = await withRepoLock(repo.path, async () => {
24990
25677
  if (!await commitOf(repo.path, archiveRef(old.id))) {
24991
25678
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -25045,12 +25732,15 @@ class WorktreeService {
25045
25732
  return;
25046
25733
  const repo = this.repoOf(rec);
25047
25734
  const repoPath = repo?.path ?? rec.repoPath;
25048
- if (rec.kept && existsSync22(repoPath)) {
25735
+ if (rec.kept && existsSync23(repoPath)) {
25049
25736
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
25050
25737
  if (!r.ok)
25051
25738
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
25052
25739
  }
25740
+ if (rec.worktree.lands?.length && existsSync23(repoPath))
25741
+ await dropLandRefs(repoPath, archiveId);
25053
25742
  this.archive.delete(archiveId);
25743
+ this.d.drafts?.drop(archiveId);
25054
25744
  if (repo)
25055
25745
  this.d.hub.emit("archiveChanged", repo.id);
25056
25746
  }
@@ -25120,7 +25810,7 @@ class WorktreeService {
25120
25810
  const group = wt.variant.group;
25121
25811
  const siblings = this.d.state.worktrees.filter((w) => w.variant?.group === group && w.id !== worktreeId);
25122
25812
  for (const sibling of siblings) {
25123
- 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));
25124
25814
  }
25125
25815
  delete wt.variant;
25126
25816
  this.d.state.save();
@@ -25165,7 +25855,7 @@ class WorktreeService {
25165
25855
  agent.note(withoutAttachments(event));
25166
25856
  }
25167
25857
  for (const w of sources)
25168
- await this.remove(w.id, { archive: false });
25858
+ await this.discardWorktree(w.id);
25169
25859
  this.setLanded(target.id, false);
25170
25860
  this.countsCache.delete(target.id);
25171
25861
  this.d.hub.emit("worktreesChanged");
@@ -25182,7 +25872,7 @@ class WorktreeService {
25182
25872
  async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
25183
25873
  const srcNm = join21(depsSource, "node_modules");
25184
25874
  const dstNm = join21(wt.path, "node_modules");
25185
- if (existsSync22(srcNm) && !existsSync22(dstNm)) {
25875
+ if (existsSync23(srcNm) && !existsSync23(dstNm)) {
25186
25876
  const started = Date.now();
25187
25877
  const attempts = [
25188
25878
  ["clonefile", ["-Rc", srcNm, dstNm]],
@@ -25199,7 +25889,7 @@ class WorktreeService {
25199
25889
  }
25200
25890
  for (const f of LOCAL_CONFIG_FILES) {
25201
25891
  const src = join21(depsSource, f);
25202
- if (!existsSync22(src) || existsSync22(join21(wt.path, f)))
25892
+ if (!existsSync23(src) || existsSync23(join21(wt.path, f)))
25203
25893
  continue;
25204
25894
  const r = await run("cp", [src, join21(wt.path, f)], wt.path);
25205
25895
  if (r.ok)
@@ -25319,12 +26009,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25319
26009
  const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
25320
26010
  if (!taken.ok)
25321
26011
  return taken;
26012
+ const mark = await landingMark(wt.path, repo.defaultBranch);
25322
26013
  const method = policy.merge ?? DEFAULT_MERGE_METHOD;
25323
26014
  const squash = method === "squash" ? await squashMessage(wt.path, repo.defaultBranch, suggested) : "";
25324
26015
  const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash);
25325
26016
  if (!landed.ok)
25326
26017
  return landed;
25327
26018
  mergedHere = true;
26019
+ await this.noteLand(repo, wt, mark);
25328
26020
  await this.restartFromMain(wt, repo.defaultBranch);
25329
26021
  if (policy.land === "push") {
25330
26022
  const pushed = await pushMain(repo.path, repo.defaultBranch);
@@ -25340,9 +26032,21 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25340
26032
  }
25341
26033
  if (!result.ok)
25342
26034
  return { result };
25343
- 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) : [];
25344
26036
  const where = policy.land === "push" ? `${repo.defaultBranch}, pushed` : repo.defaultBranch;
25345
- 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();
25346
26050
  }
25347
26051
  async restartFromMain(wt, defaultBr) {
25348
26052
  if (!hasOwnBranch(wt))
@@ -25355,9 +26059,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25355
26059
  async prMerged(worktreeId) {
25356
26060
  const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
25357
26061
  const result = await withRepoLock(repo.path, async () => {
26062
+ const mark = await landingMark(wt.path, repo.defaultBranch);
25358
26063
  const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
25359
26064
  if (!pulled.ok)
25360
26065
  return pulled;
26066
+ await this.noteLand(repo, wt, mark);
25361
26067
  await this.restartFromMain(wt, repo.defaultBranch);
25362
26068
  return pulled;
25363
26069
  });
@@ -25505,6 +26211,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25505
26211
  return cached ?? {};
25506
26212
  }
25507
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
+ }
25508
26222
  readable(id) {
25509
26223
  const wt = this.d.state.worktree(id);
25510
26224
  if (wt) {
@@ -25530,7 +26244,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25530
26244
  async gitStatus(worktreeId) {
25531
26245
  const r = this.readable(worktreeId);
25532
26246
  if (!r)
25533
- return null;
26247
+ return this.archivedStatus(worktreeId);
25534
26248
  try {
25535
26249
  const isMain = r.wt?.kind === "main";
25536
26250
  const [files, counts, head] = await Promise.all([
@@ -25568,11 +26282,15 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25568
26282
  }
25569
26283
  async gitLog(worktreeId) {
25570
26284
  const r = this.readable(worktreeId);
25571
- return r ? logCommits(r.path, r.defaultBranch) : [];
26285
+ if (r)
26286
+ return logCommits(r.path, r.defaultBranch);
26287
+ return await this.archivedGit(worktreeId)?.log() ?? [];
25572
26288
  }
25573
26289
  async commitFiles(worktreeId, sha) {
25574
26290
  const r = this.readable(worktreeId);
25575
- return r ? commitFiles(r.path, sha) : [];
26291
+ if (r)
26292
+ return commitFiles(r.path, sha);
26293
+ return await this.archivedGit(worktreeId)?.commitFiles(sha) ?? [];
25576
26294
  }
25577
26295
  markPrompted(worktreeId) {
25578
26296
  const wt = this.d.state.worktree(worktreeId);
@@ -25639,11 +26357,112 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
25639
26357
  }
25640
26358
  }
25641
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
+
25642
26456
  // ../daemon/src/index.ts
25643
26457
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
25644
26458
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
25645
26459
  var here = dirname9(fileURLToPath(import.meta.url));
25646
- var { shellDist: SHELL_DIST, bridgeJs: BRIDGE_JS, sourceRoot: SOURCE_ROOT } = locateAssets(here);
26460
+ var {
26461
+ shellDist: SHELL_DIST,
26462
+ bridgeJs: BRIDGE_JS,
26463
+ sourceRoot: SOURCE_ROOT,
26464
+ packageJson: PACKAGE_JSON
26465
+ } = locateAssets(here);
25647
26466
  var pruned = pruneAssets(SHELL_DIST);
25648
26467
  if (pruned > 0)
25649
26468
  log.debug("daemon", `pruned ${pruned} assets from builds this one has outlived`);
@@ -25692,7 +26511,8 @@ var runtime = new RuntimeRegistry({
25692
26511
  remote,
25693
26512
  grant: previewGrant(token)
25694
26513
  });
25695
- 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 });
25696
26516
  var turns = new TurnService({
25697
26517
  state,
25698
26518
  hub,
@@ -25724,7 +26544,7 @@ var prs = new PrService({ state, hub, worktrees, view: viewPr });
25724
26544
  var self = new SelfWatch(SOURCE_ROOT);
25725
26545
  await self.start();
25726
26546
  var afterLand = new AfterLand({ state, hub, self });
25727
- var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self });
26547
+ var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
25728
26548
  var idle = new IdlePolicy({
25729
26549
  runtime,
25730
26550
  state,
@@ -25732,8 +26552,43 @@ var idle = new IdlePolicy({
25732
26552
  wake: (id) => repos.touch(id),
25733
26553
  warmSpare: (repoId) => repos.warm(repoId)
25734
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
+ });
25735
26563
  var themes = new ThemeStore({ get: () => state.theme, set: (p) => state.setTheme(p) }, paths.themesDir);
25736
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
+ });
25737
26592
  var { branded, stop: stopServer } = startServer({
25738
26593
  port,
25739
26594
  token,
@@ -25755,6 +26610,7 @@ var { branded, stop: stopServer } = startServer({
25755
26610
  exec,
25756
26611
  refs,
25757
26612
  chats,
26613
+ drafts,
25758
26614
  prs,
25759
26615
  themes,
25760
26616
  agents,
@@ -25762,13 +26618,8 @@ var { branded, stop: stopServer } = startServer({
25762
26618
  attachments: attachments2,
25763
26619
  self,
25764
26620
  afterLand,
25765
- restart: () => {
25766
- const can = restartable();
25767
- if (!can.ok)
25768
- return can.reason;
25769
- fireAndForget("daemon", shutdown("restart", { respawn: true }), "restart");
25770
- return null;
25771
- },
26621
+ restarter,
26622
+ update,
25772
26623
  planTasks: makePlanner(agents),
25773
26624
  folderDialog: folderDialog()
25774
26625
  }
@@ -25784,10 +26635,14 @@ bridge.setShellOrigins([
25784
26635
  ],
25785
26636
  ...remote ? [`https://${remote.host}`] : []
25786
26637
  ]);
25787
- writeFileSync8(paths.pidFile, `${process.pid}
26638
+ writeFileSync9(paths.pidFile, `${process.pid}
25788
26639
  `);
25789
26640
  var stopLagSampler = startLagSampler();
25790
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
+ });
25791
26646
  idle.boot();
25792
26647
  fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
25793
26648
  var repoArg = process.argv[2];
@@ -25821,9 +26676,11 @@ async function shutdown(signal, opts = {}) {
25821
26676
  idle.shutdown();
25822
26677
  repos.stopWatchers();
25823
26678
  prs.stop();
26679
+ sweep.stop();
25824
26680
  stopLagSampler();
25825
26681
  stopServer();
25826
26682
  routes.flush();
26683
+ drafts.flush();
25827
26684
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
25828
26685
  await Promise.race([runtime.shutdown(), deadline]);
25829
26686
  rmSync6(paths.pidFile, { force: true });