toyon 0.8.1 → 0.10.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 (27) hide show
  1. package/dist/bridge.js +1 -1
  2. package/dist/cli.js +68 -15
  3. package/dist/daemon.js +1882 -561
  4. package/dist/shell/assets/{Editor-CrfFlcLA.js → Editor-DL0q1Bu5.js} +170 -170
  5. package/dist/shell/assets/{XTerm-CsuyFSMK.js → XTerm-C9YH4Lh2.js} +16 -16
  6. package/dist/shell/assets/{cssMode-CWkICf31.js → cssMode-Et22hXpa.js} +1 -1
  7. package/dist/shell/assets/{freemarker2-C2fP12sE.js → freemarker2-Clz20PcM.js} +1 -1
  8. package/dist/shell/assets/{grammar-5S_Kl1e8.js → grammar-CPvdB3az.js} +5 -5
  9. package/dist/shell/assets/{handlebars-CkRyqZGC.js → handlebars-B4nvXAx8.js} +1 -1
  10. package/dist/shell/assets/{html-C9iJKWKA.js → html-DPIGgBLK.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-Dps-6NJd.js → htmlMode-C2LaVuEX.js} +1 -1
  12. package/dist/shell/assets/index-2QusTgw-.js +262 -0
  13. package/dist/shell/assets/index-TaJLoLSd.css +1 -0
  14. package/dist/shell/assets/{javascript-DM5EiBY7.js → javascript-CnwJfTbN.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-ampRCTsW.js → jsonMode-8znCTiqR.js} +1 -1
  16. package/dist/shell/assets/{liquid-CEUat1ZS.js → liquid-Bdm-1CUh.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-ChupARNW.js → lspLanguageFeatures-B1DMU7yH.js} +1 -1
  18. package/dist/shell/assets/{mdx-DAglTq4w.js → mdx-Ktcqo9Jf.js} +1 -1
  19. package/dist/shell/assets/{python-DOPtAG-1.js → python-QX--d8LR.js} +1 -1
  20. package/dist/shell/assets/{razor-qRDOBjWc.js → razor-C0LrXteG.js} +1 -1
  21. package/dist/shell/assets/{tsMode-BD49NPBt.js → tsMode-BtYCp6db.js} +1 -1
  22. package/dist/shell/assets/{xml-p5Js1H7t.js → xml-BXbaQ9MG.js} +1 -1
  23. package/dist/shell/assets/{yaml-CctH9qfh.js → yaml-C6FhaU_C.js} +1 -1
  24. package/dist/shell/index.html +26 -2
  25. package/package.json +1 -1
  26. package/dist/shell/assets/index-BKyLPsvB.js +0 -258
  27. package/dist/shell/assets/index-DIDGdnCY.css +0 -1
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 writeFileSync10 } from "fs";
3
+ import { rmSync as rmSync8, writeFileSync as writeFileSync10 } from "fs";
4
4
  import { dirname as dirname12 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
@@ -58,6 +58,7 @@ function applyLog(lines, proc, line, retract = 0) {
58
58
  return out;
59
59
  return [...out, { proc, line }];
60
60
  }
61
+ var KEEP_AWAKE_MODES = ["use", "always", "off"];
61
62
 
62
63
  // ../shared/src/worktree-caps.ts
63
64
  function isMain(wt) {
@@ -119,9 +120,10 @@ function archiveReason(wt, f) {
119
120
  return null;
120
121
  if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
121
122
  return null;
122
- const what = lost ? "a sibling landed" : wt.landed ? "landed" : "no changes";
123
- return `${what}, not opened in ${duration(f.afterMs)}`;
123
+ const what = lost ? "sibling landed" : wt.landed ? "landed" : "no changes";
124
+ return `${what}, ${UNOPENED}${duration(f.afterMs)}`;
124
125
  }
126
+ var UNOPENED = "not opened in ";
125
127
  function nothingToKeep(w, f, lost) {
126
128
  if (w.kind !== "worktree" || !hasOwnBranch(w))
127
129
  return false;
@@ -157,22 +159,30 @@ function duration(ms) {
157
159
  }
158
160
 
159
161
  // ../shared/src/attachment.ts
160
- var ATTACHMENT_KINDS = ["image", "paste", "pick"];
161
- var ATTACHMENT_LIMITS = { image: 6, paste: 4, pick: 8 };
162
+ var ATTACHMENT_KINDS = ["image", "paste", "file", "pick"];
163
+ var ATTACHMENT_LIMITS = { image: 6, paste: 4, file: 8, pick: 8 };
162
164
  var ATTACHMENTS_PER_MESSAGE = ATTACHMENT_KINDS.reduce((sum, k) => sum + ATTACHMENT_LIMITS[k], 0);
163
- var KIND_NOUN = { image: "image", paste: "paste", pick: "element" };
165
+ var KIND_NOUN = {
166
+ image: "image",
167
+ paste: "paste",
168
+ file: "file",
169
+ pick: "element"
170
+ };
164
171
  var KIND_LABEL = { image: "Image", paste: "Pasted text" };
165
172
  var attachmentLabel = (kind, n) => `${KIND_LABEL[kind]} ${n}`;
166
173
  var limitMessage = (kind) => `at most ${ATTACHMENT_LIMITS[kind]} ${KIND_NOUN[kind]}s per message`;
167
174
  var countOf = (list, kind) => list.filter((a) => a.kind === kind).length;
168
175
  var overLimit = (list) => ATTACHMENT_KINDS.find((k) => countOf(list, k) > ATTACHMENT_LIMITS[k]) ?? null;
169
176
  function nextNumbers(sent) {
170
- const next = { image: 1, paste: 1, pick: 1 };
177
+ const next = { image: 1, paste: 1, file: 1, pick: 1 };
171
178
  for (const list of sent)
172
179
  for (const a of list ?? [])
173
180
  next[a.kind] = Math.max(next[a.kind], a.n + 1);
174
181
  return next;
175
182
  }
183
+ function fmtBytes(n) {
184
+ return n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
185
+ }
176
186
 
177
187
  // ../shared/src/cache.ts
178
188
  function cachePolicy(config) {
@@ -6464,17 +6474,33 @@ function describeManaged(p) {
6464
6474
  return out;
6465
6475
  }
6466
6476
 
6477
+ // ../shared/src/output.ts
6478
+ var CONTEXT_CHARS = 6000;
6479
+ var HEAD_CHARS = 1500;
6480
+ function clipOutput(text) {
6481
+ if (text.length <= CONTEXT_CHARS)
6482
+ return text;
6483
+ const cut = text.length - CONTEXT_CHARS;
6484
+ return `${text.slice(0, HEAD_CHARS)}
6485
+ [${cut} characters cut here]
6486
+ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6487
+ }
6488
+
6467
6489
  // ../shared/src/pair.ts
6468
6490
  var PAIR_TTL_MS = 120000;
6469
6491
  var pairLink = (host, code) => `https://${host}/#pair=${code}`;
6492
+ var isTailnetName = (host) => host.endsWith(".ts.net");
6470
6493
 
6471
6494
  // ../shared/src/protocol/limits.ts
6472
- var PROTOCOL_VERSION = 83;
6495
+ var PROTOCOL_VERSION = 89;
6473
6496
  var FILE_MAX_CHARS = 5000000;
6474
6497
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
6475
6498
  var IMAGE_MAX_EDGE = 2576;
6476
6499
  var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
6477
6500
  var PASTE_MAX_CHARS = 1e5;
6501
+ var FILE_INLINE_CHARS = 20000;
6502
+ var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
6503
+ var VARIANTS_MAX = 3;
6478
6504
 
6479
6505
  // ../shared/src/paste.ts
6480
6506
  var PREVIEW_CHARS = 80;
@@ -6559,11 +6585,13 @@ var termInput = string2().max(65536);
6559
6585
  var streamName = string2().min(1).max(100);
6560
6586
  var routePath = string2().min(1).max(2000);
6561
6587
  var pageTitle = string2().max(1000);
6588
+ var uploadId = string2().regex(/^[A-Za-z0-9_-]{1,64}$/);
6562
6589
  var imageInputSchema = object({
6563
6590
  kind: literal("image"),
6564
6591
  name: string2().max(200),
6565
6592
  mimeType: _enum(IMAGE_MIME_TYPES),
6566
- data: string2().max(Math.ceil(IMAGE_MAX_BYTES * 4 / 3 / 4) * 4),
6593
+ upload: uploadId,
6594
+ bytes: number2().int().min(1).max(IMAGE_MAX_BYTES),
6567
6595
  width: number2().int().min(1).max(IMAGE_MAX_EDGE),
6568
6596
  height: number2().int().min(1).max(IMAGE_MAX_EDGE)
6569
6597
  });
@@ -6579,6 +6607,13 @@ var pasteInputSchema = object({
6579
6607
  name: string2().max(200).optional(),
6580
6608
  source: pasteSourceSchema.optional()
6581
6609
  });
6610
+ var fileInputSchema = object({
6611
+ kind: literal("file"),
6612
+ upload: uploadId,
6613
+ name: string2().min(1).max(200),
6614
+ bytes: number2().int().min(1).max(UPLOAD_MAX_BYTES),
6615
+ text: boolean2()
6616
+ });
6582
6617
  var pickInputSchema = pickMetaSchema.extend({
6583
6618
  kind: literal("pick"),
6584
6619
  text: string2().max(120),
@@ -6587,13 +6622,16 @@ var pickInputSchema = pickMetaSchema.extend({
6587
6622
  var attachmentInputSchema = discriminatedUnion("kind", [
6588
6623
  imageInputSchema,
6589
6624
  pasteInputSchema,
6625
+ fileInputSchema,
6590
6626
  pickInputSchema
6591
6627
  ]);
6592
- var attachments = array(attachmentInputSchema).max(ATTACHMENTS_PER_MESSAGE).superRefine((list, ctx) => {
6628
+ var attachmentList = array(attachmentInputSchema).max(ATTACHMENTS_PER_MESSAGE).superRefine((list, ctx) => {
6593
6629
  const over = overLimit(list);
6594
6630
  if (over)
6595
6631
  ctx.addIssue({ code: "custom", message: limitMessage(over) });
6596
- }).optional();
6632
+ });
6633
+ var attachments = attachmentList.optional();
6634
+ var boxId = id.optional();
6597
6635
  var askAnswerSchema = object({
6598
6636
  selected: array(string2().max(2000)).max(32),
6599
6637
  note: string2().max(1e4).optional()
@@ -6702,7 +6740,6 @@ var themePrefsSchema = object({
6702
6740
  light: string2(),
6703
6741
  dark: string2()
6704
6742
  });
6705
- var variantSchema = object({ group: string2(), index: number2().int().min(1), of: number2().int().min(1) });
6706
6743
  var clientMsgSchema = discriminatedUnion("t", [
6707
6744
  object({ t: literal("subscribe"), worktreeId: id }),
6708
6745
  object({ t: literal("unsubscribe"), worktreeId: id }),
@@ -6713,7 +6750,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6713
6750
  clientId: string2().max(64).optional(),
6714
6751
  text: prose,
6715
6752
  context: ambient.optional(),
6716
- attachments
6753
+ attachments,
6754
+ boxId
6717
6755
  }),
6718
6756
  object({
6719
6757
  t: literal("create-worktree"),
@@ -6721,7 +6759,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6721
6759
  repoId: id,
6722
6760
  worktreeId: id.optional(),
6723
6761
  prompt,
6724
- variant: variantSchema.optional(),
6762
+ variants: number2().int().min(1).max(VARIANTS_MAX).optional(),
6725
6763
  context: ambient.optional(),
6726
6764
  attachments,
6727
6765
  agent: id.optional(),
@@ -6729,7 +6767,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6729
6767
  mode: permissionModeSchema.optional(),
6730
6768
  model: string2().max(200).optional(),
6731
6769
  effort: string2().max(100).optional(),
6732
- carry: boolean2().optional()
6770
+ carry: boolean2().optional(),
6771
+ boxId
6733
6772
  }),
6734
6773
  object({
6735
6774
  t: literal("batch-worktrees"),
@@ -6737,7 +6776,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6737
6776
  prompt,
6738
6777
  agent: id.optional(),
6739
6778
  model: string2().max(200).optional(),
6740
- effort: string2().max(100).optional()
6779
+ effort: string2().max(100).optional(),
6780
+ boxId
6741
6781
  }),
6742
6782
  object({ t: literal("set-worktree-mode"), worktreeId: id, mode: permissionModeSchema }),
6743
6783
  object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
@@ -6751,16 +6791,26 @@ var clientMsgSchema = discriminatedUnion("t", [
6751
6791
  t: literal("restore-worktree"),
6752
6792
  archiveId: id,
6753
6793
  clientId: string2().max(64).optional(),
6754
- message: object({ text: prose, attachments }).optional()
6794
+ message: object({ text: prose, attachments }).optional(),
6795
+ boxId
6755
6796
  }),
6756
6797
  object({ t: literal("delete-archived"), archiveId: id }),
6757
6798
  object({
6758
6799
  t: literal("adopt-worktree"),
6759
6800
  clientId: string2().max(64).optional(),
6760
- worktreeId: id
6801
+ worktreeId: id,
6802
+ message: object({ text: prose, attachments }).optional(),
6803
+ boxId
6761
6804
  }),
6762
6805
  object({ t: literal("git-status"), worktreeId: id }),
6763
- object({ t: literal("read-file"), worktreeId: id, path: relPath, ref: sha.optional(), seq }),
6806
+ object({
6807
+ t: literal("read-file"),
6808
+ worktreeId: id,
6809
+ path: relPath,
6810
+ ref: sha.optional(),
6811
+ since: sha.optional(),
6812
+ seq
6813
+ }),
6764
6814
  object({ t: literal("blame-file"), worktreeId: id, path: relPath, ref: sha.optional(), seq }),
6765
6815
  object({ t: literal("git-log"), worktreeId: id }),
6766
6816
  object({ t: literal("git-commit"), worktreeId: id, sha }),
@@ -6796,17 +6846,31 @@ var clientMsgSchema = discriminatedUnion("t", [
6796
6846
  object({ t: literal("design-scan"), worktreeId: id }),
6797
6847
  object({ t: literal("discard-files"), worktreeId: id, paths: array(relPath).min(1).max(500) }),
6798
6848
  object({ t: literal("judge"), worktreeId: id, note: string2().max(500).optional() }),
6849
+ object({ t: literal("fix-preview"), worktreeId: id, clientId: string2().max(64).optional() }),
6850
+ object({ t: literal("fix"), worktreeId: id, toolId: string2().min(1).max(200) }),
6799
6851
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6800
6852
  object({ t: literal("stop-agent"), worktreeId: id }),
6801
6853
  object({ t: literal("seen"), worktreeId: id }),
6802
6854
  object({ t: literal("mark-unread"), worktreeId: id }),
6803
6855
  object({ t: literal("set-draft"), boxId: id, text: prose, clientId: string2().max(64).optional() }),
6856
+ object({
6857
+ t: literal("set-attachments"),
6858
+ boxId: id,
6859
+ items: attachmentList,
6860
+ clientId: string2().max(64).optional()
6861
+ }),
6804
6862
  object({ t: literal("refresh-git"), repoId: id }),
6805
6863
  object({ t: literal("visit"), worktreeId: id, path: routePath, title: pageTitle.optional() }),
6806
6864
  object({ t: literal("page-title"), worktreeId: id, path: routePath, title: pageTitle }),
6807
6865
  object({ t: literal("forget-visit"), repoId: id, path: routePath }),
6808
6866
  object({ t: literal("pick-variant"), worktreeId: id }),
6809
- object({ t: literal("unqueue"), worktreeId: id, index: number2().int().min(0) }),
6867
+ object({
6868
+ t: literal("unqueue"),
6869
+ worktreeId: id,
6870
+ index: number2().int().min(0),
6871
+ edit: boolean2().optional()
6872
+ }),
6873
+ object({ t: literal("redraft"), worktreeId: id, seq: number2().int().min(0) }),
6810
6874
  object({ t: literal("changed-ranges"), worktreeId: id, path: relPath }),
6811
6875
  object({ t: literal("rename-worktree"), worktreeId: id, title: string2().min(1).max(200) }),
6812
6876
  object({
@@ -6842,6 +6906,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6842
6906
  object({ t: literal("set-theme"), prefs: themePrefsSchema }),
6843
6907
  object({ t: literal("zone"), tz: string2().max(100) }),
6844
6908
  object({ t: literal("set-default-agent"), agent: id }),
6909
+ object({ t: literal("set-keep-awake"), mode: _enum(KEEP_AWAKE_MODES) }),
6845
6910
  object({ t: literal("install-agent"), agent: id }),
6846
6911
  object({
6847
6912
  t: literal("agent-auth"),
@@ -6870,6 +6935,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6870
6935
  object({ t: literal("exec"), worktreeId: id, command: shellCommand.min(1) }),
6871
6936
  object({ t: literal("exec-stop"), worktreeId: id, toolId: id }),
6872
6937
  object({ t: literal("run-stop"), worktreeId: id, kind: _enum(["setup", "check", "commit"]) }),
6938
+ object({ t: literal("preview-start"), worktreeId: id }),
6873
6939
  object({ t: literal("search-refs"), repoId: id, query: string2().max(200) }),
6874
6940
  object({ t: literal("search-chats"), repoId: id, query: string2().max(200) }),
6875
6941
  object({
@@ -8154,7 +8220,7 @@ function safeUser() {
8154
8220
  // ../daemon/package.json
8155
8221
  var package_default = {
8156
8222
  name: "@toyon/daemon",
8157
- version: "0.8.1",
8223
+ version: "0.10.0",
8158
8224
  private: true,
8159
8225
  type: "module",
8160
8226
  main: "src/index.ts",
@@ -12248,9 +12314,11 @@ var legacyClientNotificationMethods = new Set([
12248
12314
 
12249
12315
  // ../daemon/src/core/errors.ts
12250
12316
  class UserError extends Error {
12251
- constructor(message) {
12317
+ delivered;
12318
+ constructor(message, about = {}) {
12252
12319
  super(message);
12253
12320
  this.name = "UserError";
12321
+ this.delivered = about.delivered ?? false;
12254
12322
  }
12255
12323
  }
12256
12324
 
@@ -12503,18 +12571,177 @@ function spawnAcp(app, launch, cwd, tag) {
12503
12571
 
12504
12572
  // ../daemon/src/agent/attachments.ts
12505
12573
  import { createHash } from "crypto";
12506
- import { mkdir, writeFile } from "fs/promises";
12574
+ import { copyFile as copyFile2, mkdir, readFile, stat, writeFile } from "fs/promises";
12575
+ import { join as join2 } from "path";
12576
+
12577
+ // ../daemon/src/agent/uploads.ts
12578
+ import { randomBytes } from "crypto";
12579
+ import { mkdirSync, readdirSync, rmSync } from "fs";
12580
+ import { copyFile } from "fs/promises";
12507
12581
  import { join } from "path";
12508
- var EXT = {
12582
+ var IMAGE_EXT = {
12509
12583
  "image/png": "png",
12510
12584
  "image/jpeg": "jpg",
12511
12585
  "image/gif": "gif",
12512
12586
  "image/webp": "webp"
12513
12587
  };
12588
+ var MIME_OF_EXT = new Map(IMAGE_MIME_TYPES.map((mime) => [IMAGE_EXT[mime], mime]));
12589
+ var OTHER_EXT = "bin";
12590
+ var NAME = /^([A-Za-z0-9_-]{1,64})\.(png|jpg|gif|webp|bin)$/;
12591
+ var UNNAMED_MS = 10 * 60000;
12592
+ var GONE = "an attachment is no longer on this machine; attach it again";
12593
+ var isUploadKind = (kind) => kind === "image" || kind === "file";
12594
+ function uploadIds(items) {
12595
+ return (items ?? []).flatMap((a) => a.kind === "image" || a.kind === "file" ? [a.upload] : []);
12596
+ }
12597
+
12598
+ class UploadStore {
12599
+ dir;
12600
+ unnamedMs;
12601
+ files = new Map;
12602
+ holds = new Map;
12603
+ named = new Set;
12604
+ constructor(dir, unnamedMs = UNNAMED_MS) {
12605
+ this.dir = dir;
12606
+ this.unnamedMs = unnamedMs;
12607
+ mkdirSync(dir, { recursive: true });
12608
+ for (const file of readdirSync(dir)) {
12609
+ const id = NAME.exec(file)?.[1];
12610
+ if (id)
12611
+ this.files.set(id, file);
12612
+ }
12613
+ }
12614
+ async put(kind, mime, body) {
12615
+ const image = IMAGE_EXT[mime];
12616
+ if (kind === "image" && !image)
12617
+ throw new UserError("not an image format the models accept");
12618
+ const cap = kind === "image" ? IMAGE_MAX_BYTES : UPLOAD_MAX_BYTES;
12619
+ const upload = randomBytes(12).toString("base64url");
12620
+ const file = `${upload}.${kind === "image" ? image : OTHER_EXT}`;
12621
+ const path = join(this.dir, file);
12622
+ const writer = Bun.file(path).writer();
12623
+ let decoder = kind === "file" ? new TextDecoder("utf-8", { fatal: true }) : null;
12624
+ let bytes = 0;
12625
+ try {
12626
+ for await (const chunk of body ?? []) {
12627
+ bytes += chunk.byteLength;
12628
+ if (bytes > cap)
12629
+ throw new UserError(`larger than ${fmtBytes(cap)}`);
12630
+ writer.write(chunk);
12631
+ await writer.flush();
12632
+ if (decoder && (bytes > FILE_MAX_CHARS || !decodes(decoder, chunk)))
12633
+ decoder = null;
12634
+ }
12635
+ if (bytes === 0)
12636
+ throw new UserError("empty file");
12637
+ await writer.end();
12638
+ } catch (e) {
12639
+ await Promise.resolve(writer.end()).catch(() => {});
12640
+ rmSync(path, { force: true });
12641
+ throw e;
12642
+ }
12643
+ this.files.set(upload, file);
12644
+ this.expireUnnamed(upload);
12645
+ return { upload, bytes, text: decoder !== null };
12646
+ }
12647
+ async adopt(from, image) {
12648
+ const upload = randomBytes(12).toString("base64url");
12649
+ const file = `${upload}.${image ? IMAGE_EXT[image] : OTHER_EXT}`;
12650
+ await copyFile(from, join(this.dir, file));
12651
+ this.files.set(upload, file);
12652
+ this.expireUnnamed(upload);
12653
+ return upload;
12654
+ }
12655
+ expireUnnamed(id) {
12656
+ const timer = setTimeout(() => {
12657
+ if (!this.named.has(id) && !this.holds.has(id))
12658
+ this.remove(id);
12659
+ }, this.unnamedMs);
12660
+ timer.unref?.();
12661
+ }
12662
+ path(id) {
12663
+ const file = this.files.get(id);
12664
+ return file ? join(this.dir, file) : null;
12665
+ }
12666
+ imageType(id) {
12667
+ const file = this.files.get(id);
12668
+ return file && MIME_OF_EXT.get(file.slice(file.lastIndexOf(".") + 1)) || null;
12669
+ }
12670
+ has(id) {
12671
+ return this.files.has(id);
12672
+ }
12673
+ hold(ids) {
12674
+ if (ids.some((id) => !this.files.has(id)))
12675
+ throw new UserError(GONE);
12676
+ for (const id of ids)
12677
+ this.holds.set(id, (this.holds.get(id) ?? 0) + 1);
12678
+ }
12679
+ release(ids) {
12680
+ for (const id of ids) {
12681
+ const left = (this.holds.get(id) ?? 0) - 1;
12682
+ if (left > 0) {
12683
+ this.holds.set(id, left);
12684
+ continue;
12685
+ }
12686
+ this.holds.delete(id);
12687
+ if (!this.named.has(id))
12688
+ this.remove(id);
12689
+ }
12690
+ }
12691
+ keep(named) {
12692
+ const before = this.named;
12693
+ this.named = new Set(named);
12694
+ for (const id of before)
12695
+ if (!this.named.has(id) && !this.holds.has(id))
12696
+ this.remove(id);
12697
+ }
12698
+ sweep(named) {
12699
+ this.named = new Set(named);
12700
+ for (const id of [...this.files.keys()])
12701
+ if (!this.named.has(id))
12702
+ this.remove(id);
12703
+ }
12704
+ remove(id) {
12705
+ const file = this.files.get(id);
12706
+ if (!file)
12707
+ return;
12708
+ this.files.delete(id);
12709
+ try {
12710
+ rmSync(join(this.dir, file), { force: true });
12711
+ } catch (e) {
12712
+ log.warn("uploads", `could not delete ${file}`, e);
12713
+ }
12714
+ }
12715
+ }
12716
+ function decodes(decoder, chunk) {
12717
+ try {
12718
+ decoder.decode(chunk, { stream: true });
12719
+ return !chunk.includes(0);
12720
+ } catch {
12721
+ return false;
12722
+ }
12723
+ }
12724
+
12725
+ // ../daemon/src/agent/attachments.ts
12514
12726
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
12515
- var FILE = /^(\d{1,6}|[0-9a-f]{16})\.(png|jpg|gif|webp|txt)$/;
12727
+ var NAME_MAX = 80;
12728
+ var DRAWN = /^(?:\d{1,6}|[0-9a-f]{16})\.(png|jpg|gif|webp|txt)$/;
12729
+ var NAMED = new RegExp(`^\\d{1,6}-[A-Za-z0-9._-]{1,${NAME_MAX}}$`);
12730
+ var isFile = (file) => DRAWN.test(file) || NAMED.test(file);
12731
+ var MIME = { png: "image/png", jpg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
12732
+ function drawnType(file) {
12733
+ return MIME[DRAWN.exec(file)?.[1] ?? ""] ?? null;
12734
+ }
12735
+ function storedName(name) {
12736
+ const safe = name.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "") || "file";
12737
+ if (safe.length <= NAME_MAX)
12738
+ return safe;
12739
+ const dot = safe.lastIndexOf(".");
12740
+ const ext = dot > 0 && safe.length - dot <= 16 ? safe.slice(dot) : "";
12741
+ return safe.slice(0, NAME_MAX - ext.length) + ext;
12742
+ }
12516
12743
  function toolImage(data, mimeType) {
12517
- const ext = EXT[mimeType];
12744
+ const ext = IMAGE_EXT[mimeType];
12518
12745
  if (!ext)
12519
12746
  return null;
12520
12747
  const bytes = Buffer.from(data, "base64");
@@ -12523,27 +12750,40 @@ function toolImage(data, mimeType) {
12523
12750
  const file = `${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}.${ext}`;
12524
12751
  return { ref: { file, mimeType, bytes: bytes.length }, bytes };
12525
12752
  }
12526
- var isAttachmentFile = (file) => FILE.test(file);
12753
+ var isAttachmentFile = (file) => isFile(file);
12527
12754
  function attachmentsDirFor(attachmentsDir, worktreeId) {
12528
- return join(attachmentsDir, worktreeId);
12755
+ return join2(attachmentsDir, worktreeId);
12756
+ }
12757
+ var GONE2 = "the upload is gone; attach it again";
12758
+ async function shortText(path, bytes) {
12759
+ if (bytes > FILE_INLINE_CHARS * 4)
12760
+ return null;
12761
+ try {
12762
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(await readFile(path));
12763
+ return text.length <= FILE_INLINE_CHARS ? text : null;
12764
+ } catch {
12765
+ return null;
12766
+ }
12529
12767
  }
12530
12768
 
12531
12769
  class AttachmentStore {
12532
12770
  dir;
12771
+ uploads;
12533
12772
  pending = new Map;
12534
- constructor(dir) {
12773
+ constructor(dir, uploads) {
12535
12774
  this.dir = dir;
12775
+ this.uploads = uploads;
12536
12776
  }
12537
12777
  async putToolImage(worktreeId, file, bytes) {
12538
- if (!ID.test(worktreeId) || !FILE.test(file))
12778
+ if (!ID.test(worktreeId) || !DRAWN.test(file))
12539
12779
  throw new UserError("bad tool image");
12540
- const path = join(attachmentsDirFor(this.dir, worktreeId), file);
12780
+ const path = join2(attachmentsDirFor(this.dir, worktreeId), file);
12541
12781
  if (this.pending.has(path) || await Bun.file(path).exists())
12542
12782
  return;
12543
- await this.write(worktreeId, file, bytes);
12783
+ await this.write(worktreeId, file, (path) => writeFile(path, bytes));
12544
12784
  }
12545
12785
  whenWritten(worktreeId, file) {
12546
- return this.pending.get(join(attachmentsDirFor(this.dir, worktreeId), file)) ?? Promise.resolve();
12786
+ return this.pending.get(join2(attachmentsDirFor(this.dir, worktreeId), file)) ?? Promise.resolve();
12547
12787
  }
12548
12788
  async put(worktreeId, n, input) {
12549
12789
  if (!ID.test(worktreeId))
@@ -12553,19 +12793,23 @@ class AttachmentStore {
12553
12793
  return this.putImage(worktreeId, n, input);
12554
12794
  case "paste":
12555
12795
  return this.putText(worktreeId, n, input);
12796
+ case "file":
12797
+ return this.putFile(worktreeId, n, input);
12556
12798
  case "pick":
12557
12799
  return { kind: "pick", ref: { ...input, n } };
12558
12800
  }
12559
12801
  }
12560
12802
  async putImage(worktreeId, n, img) {
12561
- const bytes = Buffer.from(img.data, "base64");
12562
- if (bytes.length === 0)
12563
- throw new UserError(`${img.name}: empty image`);
12564
- const file = `${n}.${EXT[img.mimeType]}`;
12565
- await this.write(worktreeId, file, bytes);
12803
+ const from = this.uploads.path(img.upload);
12804
+ if (!from || this.uploads.imageType(img.upload) !== img.mimeType)
12805
+ throw new UserError(`${img.name}: ${GONE2}`);
12806
+ const bytes = await readFile(from);
12807
+ const file = `${n}.${IMAGE_EXT[img.mimeType]}`;
12808
+ const path = await this.write(worktreeId, file, (to) => writeFile(to, bytes));
12566
12809
  return {
12567
12810
  kind: "image",
12568
12811
  bytes,
12812
+ path,
12569
12813
  ref: {
12570
12814
  kind: "image",
12571
12815
  n,
@@ -12578,28 +12822,78 @@ class AttachmentStore {
12578
12822
  }
12579
12823
  };
12580
12824
  }
12825
+ async putFile(worktreeId, n, input) {
12826
+ const from = this.uploads.path(input.upload);
12827
+ if (!from)
12828
+ throw new UserError(`${input.name}: ${GONE2}`);
12829
+ const file = `${n}-${storedName(input.name)}`;
12830
+ const path = await this.write(worktreeId, file, (to) => copyFile2(from, to));
12831
+ const bytes = (await stat(path)).size;
12832
+ const text = input.text ? await shortText(path, bytes) : null;
12833
+ return {
12834
+ kind: "file",
12835
+ path,
12836
+ ...text === null ? {} : { text },
12837
+ ref: { kind: "file", n, name: input.name, bytes, text: input.text, file }
12838
+ };
12839
+ }
12581
12840
  async putText(worktreeId, n, { text, name, source }) {
12582
12841
  if (text.length === 0)
12583
12842
  throw new UserError("empty paste");
12584
12843
  const file = `${n}.txt`;
12585
- await this.write(worktreeId, file, text);
12844
+ await this.write(worktreeId, file, (to) => writeFile(to, text));
12586
12845
  return {
12587
12846
  kind: "paste",
12588
12847
  text,
12589
12848
  ref: { kind: "paste", n, ...name ? { name } : {}, ...source ? { source } : {}, ...pasteSummary(text), file }
12590
12849
  };
12591
12850
  }
12592
- write(worktreeId, file, data) {
12851
+ async reattach(worktreeId, ref, archived) {
12852
+ const again = await this.inputOf(worktreeId, ref, archived);
12853
+ const parsed = attachmentInputSchema.safeParse(again);
12854
+ return parsed.success ? parsed.data : null;
12855
+ }
12856
+ async inputOf(worktreeId, ref, archived) {
12857
+ if (ref.kind === "pick") {
12858
+ const { n: _n, ...pick } = ref;
12859
+ return pick;
12860
+ }
12861
+ let path = null;
12862
+ for (const at of [this.fileFor(worktreeId, ref.file), archived?.(ref.file) ?? null]) {
12863
+ if (at && await Bun.file(at).exists()) {
12864
+ path = at;
12865
+ break;
12866
+ }
12867
+ }
12868
+ if (!path)
12869
+ return null;
12870
+ if (ref.kind === "paste") {
12871
+ const { name, source } = ref;
12872
+ return { kind: "paste", text: await readFile(path, "utf8"), name, source };
12873
+ }
12874
+ if (ref.kind === "file") {
12875
+ const upload = await this.uploads.adopt(path, null);
12876
+ return { kind: "file", upload, name: ref.name, bytes: ref.bytes, text: ref.text };
12877
+ }
12878
+ const mimeType = IMAGE_MIME_TYPES.find((m) => m === ref.mimeType);
12879
+ if (!mimeType)
12880
+ return null;
12881
+ const upload = await this.uploads.adopt(path, mimeType);
12882
+ const { name, bytes, width, height } = ref;
12883
+ return { kind: "image", upload, name, mimeType, bytes, width, height };
12884
+ }
12885
+ async write(worktreeId, file, fill) {
12593
12886
  const wtDir = attachmentsDirFor(this.dir, worktreeId);
12594
- const path = join(wtDir, file);
12595
- const done = mkdir(wtDir, { recursive: true }).then(() => writeFile(path, data)).finally(() => this.pending.delete(path));
12887
+ const path = join2(wtDir, file);
12888
+ const done = mkdir(wtDir, { recursive: true }).then(() => fill(path)).finally(() => this.pending.delete(path));
12596
12889
  this.pending.set(path, done);
12597
- return done;
12890
+ await done;
12891
+ return path;
12598
12892
  }
12599
12893
  fileFor(worktreeId, file) {
12600
- if (!ID.test(worktreeId) || !FILE.test(file))
12894
+ if (!ID.test(worktreeId) || !isFile(file))
12601
12895
  return null;
12602
- return join(attachmentsDirFor(this.dir, worktreeId), file);
12896
+ return join2(attachmentsDirFor(this.dir, worktreeId), file);
12603
12897
  }
12604
12898
  }
12605
12899
 
@@ -12756,9 +13050,9 @@ class OptionProbe {
12756
13050
  }
12757
13051
 
12758
13052
  // ../daemon/src/agent/registry.ts
12759
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
13053
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
12760
13054
  import { homedir as homedir3 } from "os";
12761
- import { dirname as dirname3, isAbsolute, join as join5, resolve as resolve3 } from "path";
13055
+ import { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "path";
12762
13056
 
12763
13057
  // ../daemon/src/core/cloud.ts
12764
13058
  var enabled = process.env.TOYON_CLOUD === "1";
@@ -12785,7 +13079,7 @@ var cloud = {
12785
13079
  import { spawn as spawn2 } from "child_process";
12786
13080
  import { createHash as createHash2 } from "crypto";
12787
13081
  import { existsSync, readFileSync as readFileSync2 } from "fs";
12788
- import { join as join2 } from "path";
13082
+ import { join as join3 } from "path";
12789
13083
 
12790
13084
  // ../daemon/src/core/metrics.ts
12791
13085
  var WARN_MS = 50;
@@ -12827,7 +13121,7 @@ function lockfileHash(dir) {
12827
13121
  const h = createHash2("sha1");
12828
13122
  for (const f of LOCKFILES) {
12829
13123
  try {
12830
- h.update(readFileSync2(join2(dir, f)));
13124
+ h.update(readFileSync2(join3(dir, f)));
12831
13125
  } catch {}
12832
13126
  }
12833
13127
  return h.digest("hex");
@@ -12939,8 +13233,9 @@ async function runLive(cmd, args, cwd, opts = {}) {
12939
13233
  }
12940
13234
  }
12941
13235
  function watchHooks(stream, onHook) {
13236
+ let failed;
12942
13237
  if (!stream)
12943
- return;
13238
+ return () => failed;
12944
13239
  const dec = new TextDecoder;
12945
13240
  let buf = "";
12946
13241
  const open = new Map;
@@ -12961,11 +13256,15 @@ function watchHooks(stream, onHook) {
12961
13256
  if (ev.event === "child_start" && ev.child_class === "hook" && ev.hook_name) {
12962
13257
  open.set(ev.child_id, ev.hook_name);
12963
13258
  onHook(ev.hook_name);
12964
- } else if (ev.event === "child_exit" && open.delete(ev.child_id)) {
13259
+ } else if (ev.event === "child_exit" && open.has(ev.child_id)) {
13260
+ if (ev.code)
13261
+ failed = open.get(ev.child_id);
13262
+ open.delete(ev.child_id);
12965
13263
  onHook(open.size > 0 ? [...open.values()].pop() : undefined);
12966
13264
  }
12967
13265
  }
12968
13266
  });
13267
+ return () => failed;
12969
13268
  }
12970
13269
  function runWatched(cmd, args, cwd, opts = {}) {
12971
13270
  const started = Date.now();
@@ -13010,8 +13309,7 @@ function runWatched(cmd, args, cwd, opts = {}) {
13010
13309
  };
13011
13310
  if (child.pid)
13012
13311
  opts.onSpawn?.(child.pid, kill);
13013
- if (opts.onHook)
13014
- watchHooks(child.stdio[3], opts.onHook);
13312
+ const failedHook = opts.onHook ? watchHooks(child.stdio[3], opts.onHook) : undefined;
13015
13313
  if (opts.timeoutMs) {
13016
13314
  timer = setTimeout(() => {
13017
13315
  timedOut = true;
@@ -13047,7 +13345,8 @@ function runWatched(cmd, args, cwd, opts = {}) {
13047
13345
  err: err.join("").trim(),
13048
13346
  exit,
13049
13347
  text,
13050
- ...timedOut && opts.timeoutMs ? { ceilingMs: opts.timeoutMs } : {}
13348
+ ...timedOut && opts.timeoutMs ? { ceilingMs: opts.timeoutMs } : {},
13349
+ ...failedHook?.() ? { hook: failedHook() } : {}
13051
13350
  });
13052
13351
  });
13053
13352
  });
@@ -13142,7 +13441,7 @@ function confine(launch, b, extraWrite = [], platform = process.platform, which
13142
13441
  }
13143
13442
 
13144
13443
  // ../daemon/src/agent/native.ts
13145
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
13444
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
13146
13445
  var OS = { darwin: "darwin", linux: "linux", win32: "windows" };
13147
13446
  function nativePackage(pkg, host) {
13148
13447
  const os = OS[host.platform];
@@ -13165,7 +13464,7 @@ function hostTraits() {
13165
13464
  }
13166
13465
  function muslLoader() {
13167
13466
  try {
13168
- return existsSync2("/lib") && readdirSync("/lib").some((f) => f.startsWith("ld-musl-"));
13467
+ return existsSync2("/lib") && readdirSync2("/lib").some((f) => f.startsWith("ld-musl-"));
13169
13468
  } catch {
13170
13469
  return false;
13171
13470
  }
@@ -13235,49 +13534,52 @@ function opencodeEnv() {
13235
13534
  }
13236
13535
 
13237
13536
  // ../daemon/src/agent/sandbox.ts
13238
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "fs";
13537
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "fs";
13239
13538
  import { homedir as homedir2, tmpdir } from "os";
13240
- import { join as join4, resolve as resolve2 } from "path";
13539
+ import { join as join5, resolve as resolve2 } from "path";
13241
13540
 
13242
13541
  // ../daemon/src/core/paths.ts
13243
- import { mkdirSync } from "fs";
13542
+ import { mkdirSync as mkdirSync2 } from "fs";
13244
13543
  import { homedir } from "os";
13245
- import { join as join3 } from "path";
13246
- function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon")) {
13544
+ import { join as join4 } from "path";
13545
+ function makePaths(home = process.env.TOYON_HOME ?? join4(homedir(), ".toyon")) {
13247
13546
  return {
13248
13547
  home,
13249
- stateFile: join3(home, DAEMON_FILES.state),
13250
- draftsFile: join3(home, DAEMON_FILES.drafts),
13251
- tokenFile: join3(home, DAEMON_FILES.token),
13252
- pidFile: join3(home, DAEMON_FILES.pid),
13253
- logFile: join3(home, DAEMON_FILES.log),
13254
- remoteFile: join3(home, DAEMON_FILES.remote),
13255
- transcriptsDir: join3(home, "transcripts"),
13256
- attachmentsDir: join3(home, "attachments"),
13257
- archiveDir: join3(home, "archive"),
13258
- worktreesDir: join3(home, "worktrees.noindex"),
13259
- cacheDir: join3(home, "cache.noindex"),
13260
- themesDir: join3(home, "themes"),
13261
- agentsDir: process.env.TOYON_AGENTS_DIR ?? join3(home, "agents"),
13262
- agentsFile: join3(home, "agents.json"),
13263
- scratchDir: join3(home, "scratch"),
13264
- gitCredentialsFile: join3(home, "git-credentials")
13548
+ stateFile: join4(home, DAEMON_FILES.state),
13549
+ draftsFile: join4(home, DAEMON_FILES.drafts),
13550
+ draftAttachmentsFile: join4(home, "draft-attachments.json"),
13551
+ tokenFile: join4(home, DAEMON_FILES.token),
13552
+ pidFile: join4(home, DAEMON_FILES.pid),
13553
+ logFile: join4(home, DAEMON_FILES.log),
13554
+ remoteFile: join4(home, DAEMON_FILES.remote),
13555
+ transcriptsDir: join4(home, "transcripts"),
13556
+ attachmentsDir: join4(home, "attachments"),
13557
+ uploadsDir: join4(home, "uploads"),
13558
+ archiveDir: join4(home, "archive"),
13559
+ worktreesDir: join4(home, "worktrees.noindex"),
13560
+ cacheDir: join4(home, "cache.noindex"),
13561
+ themesDir: join4(home, "themes"),
13562
+ agentsDir: process.env.TOYON_AGENTS_DIR ?? join4(home, "agents"),
13563
+ agentsFile: join4(home, "agents.json"),
13564
+ scratchDir: join4(home, "scratch"),
13565
+ gitCredentialsFile: join4(home, "git-credentials")
13265
13566
  };
13266
13567
  }
13267
13568
  function ensureDirs(p) {
13268
- mkdirSync(p.home, { recursive: true, mode: 448 });
13269
- mkdirSync(p.transcriptsDir, { recursive: true });
13270
- mkdirSync(p.attachmentsDir, { recursive: true });
13271
- mkdirSync(p.archiveDir, { recursive: true });
13272
- mkdirSync(p.worktreesDir, { recursive: true });
13273
- mkdirSync(p.cacheDir, { recursive: true });
13274
- mkdirSync(p.themesDir, { recursive: true });
13275
- mkdirSync(p.agentsDir, { recursive: true });
13276
- mkdirSync(p.scratchDir, { recursive: true });
13569
+ mkdirSync2(p.home, { recursive: true, mode: 448 });
13570
+ mkdirSync2(p.transcriptsDir, { recursive: true });
13571
+ mkdirSync2(p.attachmentsDir, { recursive: true });
13572
+ mkdirSync2(p.uploadsDir, { recursive: true });
13573
+ mkdirSync2(p.archiveDir, { recursive: true });
13574
+ mkdirSync2(p.worktreesDir, { recursive: true });
13575
+ mkdirSync2(p.cacheDir, { recursive: true });
13576
+ mkdirSync2(p.themesDir, { recursive: true });
13577
+ mkdirSync2(p.agentsDir, { recursive: true });
13578
+ mkdirSync2(p.scratchDir, { recursive: true });
13277
13579
  }
13278
13580
 
13279
13581
  // ../daemon/src/git/exclude.ts
13280
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
13582
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "fs";
13281
13583
  import { dirname } from "path";
13282
13584
  async function excludeFile(cwd) {
13283
13585
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
@@ -13294,7 +13596,7 @@ async function excludeFromGit(cwd, pattern) {
13294
13596
  if (current.split(`
13295
13597
  `).some((l) => l.trim() === pattern))
13296
13598
  return;
13297
- mkdirSync2(dirname(file), { recursive: true });
13599
+ mkdirSync3(dirname(file), { recursive: true });
13298
13600
  writeFileSync(file, `${current}${current && !current.endsWith(`
13299
13601
  `) ? `
13300
13602
  ` : ""}${pattern}
@@ -13320,7 +13622,7 @@ async function excludeBlock(cwd, name, patterns) {
13320
13622
  ` : "";
13321
13623
  if (text === current)
13322
13624
  return;
13323
- mkdirSync2(dirname(file), { recursive: true });
13625
+ mkdirSync3(dirname(file), { recursive: true });
13324
13626
  writeFileSync(file, text);
13325
13627
  }
13326
13628
  async function unexcludeFromGit(cwd, pattern) {
@@ -13387,7 +13689,7 @@ async function prepareLaunch(cwd, spec) {
13387
13689
  log.warn(cwd, `agent ${spec.id} runs without an OS sandbox`);
13388
13690
  return { bounds, env: setup?.env ?? {} };
13389
13691
  }
13390
- var SETTINGS_REL = join4(".claude", "settings.local.json");
13692
+ var SETTINGS_REL = join5(".claude", "settings.local.json");
13391
13693
  function claudeLocalSettings(b) {
13392
13694
  return {
13393
13695
  sandbox: {
@@ -13401,8 +13703,8 @@ function claudeLocalSettings(b) {
13401
13703
  };
13402
13704
  }
13403
13705
  async function writeClaudeLocalSettings(cwd, b) {
13404
- const file = join4(cwd, SETTINGS_REL);
13405
- mkdirSync3(join4(cwd, ".claude"), { recursive: true });
13706
+ const file = join5(cwd, SETTINGS_REL);
13707
+ mkdirSync4(join5(cwd, ".claude"), { recursive: true });
13406
13708
  let existing = {};
13407
13709
  if (existsSync5(file)) {
13408
13710
  try {
@@ -13424,7 +13726,7 @@ async function writeClaudeLocalSettings(cwd, b) {
13424
13726
  writeFileSync2(tmp, `${JSON.stringify(next, null, 2)}
13425
13727
  `);
13426
13728
  renameSync(tmp, file);
13427
- if (existsSync5(join4(cwd, ".git")))
13729
+ if (existsSync5(join5(cwd, ".git")))
13428
13730
  await excludeFromGit(cwd, SETTINGS_REL);
13429
13731
  }
13430
13732
 
@@ -13549,12 +13851,12 @@ class AgentRegistry {
13549
13851
  const name = this.packageName(spec);
13550
13852
  if (!name || spec.run.kind === "command")
13551
13853
  return null;
13552
- const pkgDir = join5(this.dir, spec.id, "node_modules", name);
13854
+ const pkgDir = join6(this.dir, spec.id, "node_modules", name);
13553
13855
  if (spec.run.kind === "npm-native") {
13554
- const bin = join5(pkgDir, spec.run.bin);
13856
+ const bin = join6(pkgDir, spec.run.bin);
13555
13857
  return existsSync6(bin) ? bin : null;
13556
13858
  }
13557
- const pkgJson = join5(pkgDir, "package.json");
13859
+ const pkgJson = join6(pkgDir, "package.json");
13558
13860
  if (!existsSync6(pkgJson))
13559
13861
  return null;
13560
13862
  try {
@@ -13562,7 +13864,7 @@ class AgentRegistry {
13562
13864
  const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
13563
13865
  if (!rel)
13564
13866
  return null;
13565
- const script = join5(dirname3(pkgJson), rel);
13867
+ const script = join6(dirname3(pkgJson), rel);
13566
13868
  return existsSync6(script) ? script : null;
13567
13869
  } catch (e) {
13568
13870
  log.warn("agents", `cannot read ${pkgJson}`, e);
@@ -13616,10 +13918,10 @@ class AgentRegistry {
13616
13918
  if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
13617
13919
  return Promise.resolve();
13618
13920
  const { version } = spec.run;
13619
- const dir = join5(this.dir, id);
13921
+ const dir = join6(this.dir, id);
13620
13922
  const p = (async () => {
13621
- mkdirSync4(dir, { recursive: true });
13622
- writeFileSync3(join5(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
13923
+ mkdirSync5(dir, { recursive: true });
13924
+ writeFileSync3(join6(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
13623
13925
  `);
13624
13926
  log.info("agents", `installing ${pkg}@${version} for ${id}`);
13625
13927
  const r = await this.installer(dir, pkg, version);
@@ -13643,7 +13945,7 @@ class AgentRegistry {
13643
13945
  const name = this.packageName(spec);
13644
13946
  if (!name)
13645
13947
  return null;
13646
- const pkgJson = join5(this.dir, spec.id, "node_modules", name, "package.json");
13948
+ const pkgJson = join6(this.dir, spec.id, "node_modules", name, "package.json");
13647
13949
  try {
13648
13950
  return JSON.parse(readFileSync6(pkgJson, "utf8")).version ?? null;
13649
13951
  } catch {
@@ -13773,6 +14075,12 @@ ${body}
13773
14075
  return parts.join(`
13774
14076
  `);
13775
14077
  }
14078
+ function unformatOutput(output) {
14079
+ if (!output.startsWith("```\n"))
14080
+ return "";
14081
+ const close = output.lastIndexOf("\n```");
14082
+ return close < 4 ? "" : output.slice(4, close);
14083
+ }
13776
14084
 
13777
14085
  // ../daemon/src/agent/policy.ts
13778
14086
  import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
@@ -13881,6 +14189,7 @@ var SYSTEM_APPEND = [
13881
14189
  "Never run `git commit` unless the user explicitly asks you to, and never offer to, through the question tool or otherwise; leave changes uncommitted for the user to review and commit themselves.",
13882
14190
  "Keep the scope tight: do the asked task well, then stop. A follow-up the user might want later is a sentence in your reply, not a question.",
13883
14191
  "When the work is done and the only thing left is whether to do something you recommend, do not end your turn on a prose offer waiting for a yes. Ask it with your question tool when you have one: one question, two options with the recommended one first, each saying what it changes, and typed text allowed so the answer can carry instructions. The card is the first thing the user reads, before any prose of yours, so the question itself states what you propose and why in a sentence or two; a question that is only the yes-or-no leaves them guessing what it is. Ask this way only where you would otherwise have stopped and waited; never to confirm work already asked for.",
14192
+ "An answer to a question of yours can carry typed text beside the option picked, and the text outranks the pick. Text that doubts the pick, asks you something or says the user cannot decide is not a yes: answer it in prose and change nothing until they have decided. Text that only adds detail to the pick is an instruction for carrying it out.",
13884
14193
  "When referencing a file in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor, whether it is in this worktree or anywhere else on this machine. A folder, a URL route or a command is not a file: name it in backticks, never as a link.",
13885
14194
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
13886
14195
  'Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, teardown (optional) runs once when a worktree is removed, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself; timeouts (optional) gives setup, check and commit each a ceiling as a duration like "30m" (ten minutes each when unset), for a suite that runs longer. A settings.local.json beside it (toyon.local.json at the root) holds one person\'s overrides and is never committed. Both files may carry comments and trailing commas; leave any you find.',
@@ -13888,6 +14197,113 @@ var SYSTEM_APPEND = [
13888
14197
  "When you scaffold a project, write its .gitignore (dependencies, build output, local env files) before installing anything, so an install never leaves thousands of untracked files for the user to wade through or commit.",
13889
14198
  "When you scaffold a project or change how it installs or starts, finish by updating the settings file Toyon already reads, or writing .toyon/settings.json when there is none, so the preview can run it."
13890
14199
  ].join(" ");
14200
+ function failedRun(f) {
14201
+ switch (f.kind) {
14202
+ case "hook":
14203
+ case "check":
14204
+ case "command":
14205
+ return f;
14206
+ case "conflict":
14207
+ return f.step;
14208
+ case "preview":
14209
+ return;
14210
+ }
14211
+ }
14212
+ function fixPrompt(f) {
14213
+ switch (f.kind) {
14214
+ case "hook":
14215
+ return hookFixPrompt(f.hook);
14216
+ case "conflict":
14217
+ return conflictFixPrompt(f.base, f.how, !!f.step);
14218
+ case "check":
14219
+ return checkFixPrompt(f.command);
14220
+ case "command":
14221
+ return commandFixPrompt(f.command);
14222
+ case "preview":
14223
+ return procFixPrompt(f.procs, f.log);
14224
+ }
14225
+ }
14226
+ var WHY_COMMAND_CHARS = 60;
14227
+ function fixWhy(f) {
14228
+ switch (f.kind) {
14229
+ case "hook":
14230
+ return `the ${f.hook} hook refused the ${f.hook.includes("push") ? "push" : "commit"}`;
14231
+ case "conflict":
14232
+ return f.how === "rebase" ? `the branch needs a rebase onto ${f.base}` : `the branch needs ${f.base} merged in`;
14233
+ case "check":
14234
+ return "the check failed";
14235
+ case "command": {
14236
+ const line = f.command.split(`
14237
+ `)[0] ?? "";
14238
+ return `\`${line.length > WHY_COMMAND_CHARS ? `${line.slice(0, WHY_COMMAND_CHARS)}...` : line}\` failed`;
14239
+ }
14240
+ case "preview":
14241
+ return "the dev server is not reachable";
14242
+ }
14243
+ }
14244
+ function failureContext(f) {
14245
+ const run = failedRun(f);
14246
+ if (!run)
14247
+ return;
14248
+ const shown = clipOutput(run.text.replace(/\n+$/, ""));
14249
+ const what = f.kind === "command" ? "The command that failed" : "What Toyon ran";
14250
+ return `${what}, and what it printed:
14251
+ $ ${run.command}${shown ? `
14252
+ ${shown}` : `
14253
+ (it printed nothing)`}`;
14254
+ }
14255
+ function hookFixPrompt(hook) {
14256
+ return [
14257
+ `The ${hook} hook refused that, and what it printed is attached below.`,
14258
+ "Fix what it complains about, inside this worktree.",
14259
+ "Do not skip the hook, do not edit it or its configuration to get past it, and do not commit: Toyon runs the check when your turn ends, and the user lands from there."
14260
+ ].join(" ");
14261
+ }
14262
+ function conflictFixPrompt(base, how, shown) {
14263
+ const ask = how === "rebase" ? `Rebase this branch onto ${base} and resolve the conflicts, keeping any uncommitted changes, then verify the app still works.` : `Merge ${base} into this branch, resolve the conflicts and commit the merge, then verify the app still works.`;
14264
+ return shown ? `${ask} What git printed when Toyon tried is attached below.` : ask;
14265
+ }
14266
+ function checkFixPrompt(command) {
14267
+ return [
14268
+ `The repo's check, \`${command}\`, failed, and what it printed is attached below.`,
14269
+ "Fix what it reports, inside this worktree.",
14270
+ "Do not weaken the check or its configuration to get past it. Toyon runs it again when your turn ends."
14271
+ ].join(" ");
14272
+ }
14273
+ function commandFixPrompt(command) {
14274
+ return [
14275
+ `The user ran \`${command}\` and it failed; what it printed is attached below.`,
14276
+ "Find the cause and fix it, inside this worktree.",
14277
+ "If the cause is not in this worktree, say what it is instead of changing anything."
14278
+ ].join(" ");
14279
+ }
14280
+ var PROC_TAIL = 40;
14281
+ function procFixPrompt(procs, log) {
14282
+ const bad = procs.filter((p) => p.status === "crashed" || p.status === "unreachable");
14283
+ const lines = bad.map((p) => `- \`${p.name}\`: \`${p.command}\`, started with PORT=${p.port}. ${procDiagnosis(p)}`);
14284
+ const tail = log.slice(-PROC_TAIL).map((l) => `[${l.proc}] ${l.line}`).join(`
14285
+ `);
14286
+ return [
14287
+ "The dev server in this worktree is not reachable, so the preview is empty.",
14288
+ "",
14289
+ ...lines,
14290
+ "",
14291
+ tail ? `Last output:
14292
+ \`\`\`
14293
+ ${tail}
14294
+ \`\`\`` : "It produced no output.",
14295
+ "",
14296
+ "Find the cause and fix it. The command must run in the foreground and listen on the port in the PORT environment variable, which Toyon sets differently for each worktree. If the tool takes its port from a flag instead (Vite does), make the app read PORT, for Vite `server.port: Number(process.env.PORT)` with `strictPort: true`, or add the flag to the start command in toyon's settings file. When your turn ends toyon restarts the process and checks again."
14297
+ ].join(`
14298
+ `);
14299
+ }
14300
+ function procDiagnosis(p) {
14301
+ if (p.detail)
14302
+ return p.detail;
14303
+ if (p.status === "crashed")
14304
+ return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
14305
+ return "It never answered on that port.";
14306
+ }
13891
14307
  function ambientBlock(paragraphs) {
13892
14308
  const said = paragraphs.filter((p) => !!p);
13893
14309
  if (said.length === 0)
@@ -13906,6 +14322,9 @@ function previewContext(p) {
13906
14322
  case "setup":
13907
14323
  line = "Toyon is setting this worktree up and starts the preview when that is done; the next message will say where it answers.";
13908
14324
  break;
14325
+ case "parked":
14326
+ line = "The preview is off: Toyon has not started this worktree's dev servers, and starts them when you first write a file or when the user asks for them. Nothing answers until then.";
14327
+ break;
13909
14328
  case "starting":
13910
14329
  line = `Toyon is starting the preview; it answers${at} once it is up.`;
13911
14330
  break;
@@ -13920,8 +14339,12 @@ function previewContext(p) {
13920
14339
  }
13921
14340
  return line;
13922
14341
  }
13923
- function imageCaption(ref) {
13924
- return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height})`;
14342
+ function imageCaption(ref, path) {
14343
+ return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height}), saved at ${path}`;
14344
+ }
14345
+ function fileCaption(ref, path, inline = false) {
14346
+ const at = `A file the user attached: ${ref.name} (${fmtBytes(ref.bytes)}), saved at ${path}.`;
14347
+ return inline ? at : `${at} Read or search it there.`;
13925
14348
  }
13926
14349
  function pasteCaption(ref) {
13927
14350
  const label = attachmentLabel("paste", ref.n);
@@ -13967,7 +14390,7 @@ function attachmentBlocks(a) {
13967
14390
  switch (a.kind) {
13968
14391
  case "image":
13969
14392
  return [
13970
- textBlock(imageCaption(a.ref)),
14393
+ textBlock(imageCaption(a.ref, a.path)),
13971
14394
  { type: "image", mimeType: a.ref.mimeType, data: a.bytes.toString("base64") }
13972
14395
  ];
13973
14396
  case "paste":
@@ -13975,6 +14398,13 @@ function attachmentBlocks(a) {
13975
14398
  <pasted-text ${a.ref.n}>
13976
14399
  ${a.text}
13977
14400
  </pasted-text>`)];
14401
+ case "file":
14402
+ return [
14403
+ textBlock(a.text === undefined ? fileCaption(a.ref, a.path) : `${fileCaption(a.ref, a.path, true)}
14404
+ <attached-file ${a.ref.n}>
14405
+ ${a.text}
14406
+ </attached-file>`)
14407
+ ];
13978
14408
  case "pick":
13979
14409
  return [textBlock(pickCaption(a.ref))];
13980
14410
  }
@@ -13983,9 +14413,9 @@ ${a.text}
13983
14413
  // ../daemon/src/agent/transcript.ts
13984
14414
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
13985
14415
  import { appendFile } from "fs/promises";
13986
- import { join as join6 } from "path";
14416
+ import { join as join7 } from "path";
13987
14417
  function transcriptPathFor(transcriptsDir, worktreeId) {
13988
- return join6(transcriptsDir, `${worktreeId}.jsonl`);
14418
+ return join7(transcriptsDir, `${worktreeId}.jsonl`);
13989
14419
  }
13990
14420
  function coalesce(entries) {
13991
14421
  const out = [];
@@ -14140,7 +14570,7 @@ async function cleanUp(ctx, a, sessionId) {
14140
14570
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
14141
14571
  import { open as open2 } from "fs/promises";
14142
14572
  import { homedir as homedir4 } from "os";
14143
- import { basename as basename2, dirname as dirname4, join as join7 } from "path";
14573
+ import { basename as basename2, dirname as dirname4, join as join8 } from "path";
14144
14574
  var STARTED = /Command running in background with ID: (\S+)\. Output is being written to: (\S+)\./;
14145
14575
  function backgroundStart(input, output) {
14146
14576
  const flag = input && typeof input === "object" ? input.run_in_background : undefined;
@@ -14153,14 +14583,14 @@ function spawnDetached(output) {
14153
14583
  return output.includes("Async agent launched successfully");
14154
14584
  }
14155
14585
  function claudeConfigDir() {
14156
- return process.env.CLAUDE_CONFIG_DIR || join7(homedir4(), ".claude");
14586
+ return process.env.CLAUDE_CONFIG_DIR || join8(homedir4(), ".claude");
14157
14587
  }
14158
14588
  function sessionLogFor(file, configDir = claudeConfigDir()) {
14159
14589
  const tasks = dirname4(file);
14160
14590
  if (basename2(tasks) !== "tasks")
14161
14591
  return null;
14162
14592
  const session = dirname4(tasks);
14163
- const path = join7(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14593
+ const path = join8(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14164
14594
  return existsSync8(path) ? path : null;
14165
14595
  }
14166
14596
  function taskEnd(text, toolId) {
@@ -14521,11 +14951,32 @@ function endOfAsk(req, res, memos) {
14521
14951
  isError: !allowed
14522
14952
  };
14523
14953
  }
14524
- function abandoned(memos, parent) {
14954
+ function release(toolId, memo) {
14955
+ const held = memo.held;
14956
+ if (!held)
14957
+ return null;
14958
+ delete memo.held;
14959
+ if (!emptyInput(memo.input))
14960
+ memo.writing = false;
14961
+ return memo.ended ? null : { type: "tool-update", toolId, ...held };
14962
+ }
14963
+ function preempted(memos) {
14964
+ for (const memo of memos.values()) {
14965
+ if (!memo.ended && memo.writing && !memo.parent)
14966
+ memo.cut = true;
14967
+ }
14968
+ }
14969
+ function abandoned(memos, parent, opening = false) {
14525
14970
  const out = [];
14526
14971
  for (const [toolId, memo] of memos) {
14527
14972
  if (memo.ended || !memo.writing || memo.parent !== parent)
14528
14973
  continue;
14974
+ if (opening && memo.held && !memo.cut) {
14975
+ const sent = release(toolId, memo);
14976
+ if (sent)
14977
+ out.push(sent);
14978
+ continue;
14979
+ }
14529
14980
  memo.ended = true;
14530
14981
  out.push({ type: "tool-end", toolId });
14531
14982
  }
@@ -14554,7 +15005,7 @@ function mapUpdate(update, memos, tag, sink, background) {
14554
15005
  }
14555
15006
  case "tool_call": {
14556
15007
  const spawn = spawnOf(update._meta);
14557
- const out = abandoned(memos, spawn.parentToolId);
15008
+ const out = abandoned(memos, spawn.parentToolId, true);
14558
15009
  const input = update.rawInput ?? { locations: update.locations ?? [] };
14559
15010
  const head = heading(update);
14560
15011
  const memo = {
@@ -14609,26 +15060,27 @@ function mapUpdate(update, memos, tag, sink, background) {
14609
15060
  ...spawn
14610
15061
  });
14611
15062
  }
14612
- if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
14613
- return out;
14614
- const refined = { type: "tool-update", toolId: update.toolCallId };
15063
+ const refined = { ...memo.held };
14615
15064
  const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : update.title;
14616
- if (title && title !== memo.title) {
14617
- memo.title = title;
14618
- refined.title = title;
14619
- }
14620
- if (update.name && update.name !== memo.name) {
15065
+ if (title && title !== memo.title)
15066
+ refined.title = memo.title = title;
15067
+ if (update.name && update.name !== memo.name)
14621
15068
  refined.name = memo.name = update.name;
14622
- }
14623
15069
  if (update.kind && update.kind !== memo.kind)
14624
15070
  refined.kind = memo.kind = update.kind;
14625
- if (update.rawInput !== undefined) {
15071
+ if (update.rawInput !== undefined)
14626
15072
  refined.input = memo.input = update.rawInput;
14627
- if (!emptyInput(update.rawInput))
14628
- memo.writing = false;
15073
+ if (update.rawInput !== undefined && update.content === undefined && update.status === undefined) {
15074
+ if (out.length === 0)
15075
+ memo.held = refined;
15076
+ return out;
15077
+ }
15078
+ delete memo.held;
15079
+ if (!emptyInput(memo.input))
15080
+ memo.writing = false;
15081
+ if (Object.keys(refined).length > 0 && !memo.ended && out.length === 0) {
15082
+ out.push({ type: "tool-update", toolId: update.toolCallId, ...refined });
14629
15083
  }
14630
- if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
14631
- out.push(refined);
14632
15084
  if (update.content)
14633
15085
  memo.content = [...memo.content, ...update.content];
14634
15086
  if (update.rawOutput !== undefined)
@@ -14776,6 +15228,11 @@ function steerOutcome(value) {
14776
15228
  // ../daemon/src/agent/acp/session.ts
14777
15229
  var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
14778
15230
  var OWN_SETTLE_MS = 1e4;
15231
+ var INPUT_HOLD_MS = 1000;
15232
+ var queued = (q) => ({
15233
+ text: q.text,
15234
+ ...q.attachments ? { attachments: q.attachments } : {}
15235
+ });
14779
15236
  function recorded(reply) {
14780
15237
  if (reply?.kind === "choice")
14781
15238
  return { choiceId: reply.choiceId };
@@ -14794,6 +15251,7 @@ class AcpSession {
14794
15251
  held = false;
14795
15252
  conn = null;
14796
15253
  connecting = null;
15254
+ opening = null;
14797
15255
  live = null;
14798
15256
  reaper = null;
14799
15257
  own = null;
@@ -14891,7 +15349,7 @@ class AcpSession {
14891
15349
  return this.queue.length > 0 || this.steered.length > 0 || this.refused !== null || this.asks.size > 0;
14892
15350
  }
14893
15351
  get queueItems() {
14894
- return this.waiting().map((q) => q.text);
15352
+ return this.waiting().map(queued);
14895
15353
  }
14896
15354
  waiting() {
14897
15355
  return this.queue.filter((q) => !q.recorded);
@@ -14900,13 +15358,19 @@ class AcpSession {
14900
15358
  queueChanged() {
14901
15359
  this.onQueueChange?.();
14902
15360
  }
14903
- unqueue(index) {
15361
+ unqueue(index, taken) {
14904
15362
  const item = this.waiting()[index];
14905
15363
  if (!item)
14906
15364
  return;
14907
15365
  this.queue.splice(this.queue.indexOf(item), 1);
15366
+ taken?.(queued(item));
15367
+ this.letGo(item);
14908
15368
  this.queueChanged();
14909
15369
  }
15370
+ letGo(item) {
15371
+ if (!item.recorded)
15372
+ this.d.attachments.uploads.release(uploadIds(item.attachments));
15373
+ }
14910
15374
  transcript() {
14911
15375
  return this.log.entries;
14912
15376
  }
@@ -14924,8 +15388,19 @@ class AcpSession {
14924
15388
  send(text, opts = {}) {
14925
15389
  if (this.stopped)
14926
15390
  return log.warn(this.d.worktreeId, "send after close dropped");
14927
- const { context, attachments } = opts;
15391
+ if (!opts.asked)
15392
+ for (const ask of [...this.asks.values()])
15393
+ ask.settle("skipped");
15394
+ this.post(text, opts);
15395
+ }
15396
+ post(text, opts = {}) {
15397
+ const { context, attachments, asked } = opts;
14928
15398
  const item = { text, context, ...attachments?.length ? { attachments } : {} };
15399
+ if (asked) {
15400
+ this.emit({ type: "fix-asked", text, ts: Date.now(), ...asked });
15401
+ item.recorded = { attachments: [] };
15402
+ }
15403
+ this.d.attachments.uploads.hold(uploadIds(attachments));
14929
15404
  if ((this.running || this.own) && !this.interrupted && !this.held && this.live?.conn.steering) {
14930
15405
  return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
14931
15406
  }
@@ -14980,7 +15455,10 @@ class AcpSession {
14980
15455
  }
14981
15456
  this.clearReaper();
14982
15457
  log.warn(this.d.worktreeId, "steered message started a turn of the agent's own");
15458
+ return;
14983
15459
  }
15460
+ if (this.live === live)
15461
+ preempted(live.tools);
14984
15462
  }
14985
15463
  unsteer(item) {
14986
15464
  const at = this.steered.indexOf(item);
@@ -15012,6 +15490,8 @@ class AcpSession {
15012
15490
  async close() {
15013
15491
  this.stopped = true;
15014
15492
  this.clearReaper();
15493
+ for (const item of this.queue)
15494
+ this.letGo(item);
15015
15495
  this.queue = [];
15016
15496
  this.steered = [];
15017
15497
  this.queueChanged();
@@ -15135,8 +15615,8 @@ class AcpSession {
15135
15615
  retry() {
15136
15616
  const item = this.refused;
15137
15617
  this.refused = null;
15138
- if (item)
15139
- this.send(item.text, item);
15618
+ if (item && !this.stopped)
15619
+ this.enqueue(item);
15140
15620
  }
15141
15621
  planWithheld(conn, methodId) {
15142
15622
  return this.d.planSignIn?.() === false && conn.spec.terminalLogins?.[methodId]?.plan === true;
@@ -15159,8 +15639,12 @@ class AcpSession {
15159
15639
  }
15160
15640
  async record({ text, attachments }) {
15161
15641
  const stored = [];
15162
- for (const a of attachments ?? [])
15163
- stored.push(await this.d.attachments.put(this.d.worktreeId, this.seq[a.kind]++, a));
15642
+ try {
15643
+ for (const a of attachments ?? [])
15644
+ stored.push(await this.d.attachments.put(this.d.worktreeId, this.seq[a.kind]++, a));
15645
+ } finally {
15646
+ this.d.attachments.uploads.release(uploadIds(attachments));
15647
+ }
15164
15648
  this.emit({
15165
15649
  type: "user-message",
15166
15650
  text,
@@ -15182,6 +15666,11 @@ class AcpSession {
15182
15666
  }
15183
15667
  async runTurn(item) {
15184
15668
  item.recorded ??= await this.record(item);
15669
+ try {
15670
+ await this.d.beforeTurn?.();
15671
+ } catch (e) {
15672
+ log.warn(this.d.worktreeId, "before-turn hook failed", e);
15673
+ }
15185
15674
  this.emit({ type: "turn-start", ts: Date.now() });
15186
15675
  const live = await this.ensureLive("a turn");
15187
15676
  await this.applyMode(live);
@@ -15273,8 +15762,18 @@ class AcpSession {
15273
15762
  }
15274
15763
  async ensureLive(why) {
15275
15764
  const conn = await this.ensureConn(why);
15765
+ if (this.opening?.conn === conn)
15766
+ return this.opening.live;
15276
15767
  if (this.live?.conn === conn)
15277
15768
  return this.live;
15769
+ const live = this.openLive(conn).finally(() => {
15770
+ if (this.opening?.live === live)
15771
+ this.opening = null;
15772
+ });
15773
+ this.opening = { conn, live };
15774
+ return live;
15775
+ }
15776
+ async openLive(conn) {
15278
15777
  const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
15279
15778
  let sessionId = this.d.getSessionId();
15280
15779
  let modes;
@@ -15315,7 +15814,7 @@ class AcpSession {
15315
15814
  this.emit({ type: "session-info", sessionId, ...currentValues(options) });
15316
15815
  this.learn(options);
15317
15816
  const modeOption = modes ? null : readModeOption(configOptions);
15318
- this.live = {
15817
+ const live = {
15319
15818
  conn,
15320
15819
  sessionId,
15321
15820
  prefixPending: !resumed && conn.spec.systemPrompt === "prompt-prefix",
@@ -15325,13 +15824,14 @@ class AcpSession {
15325
15824
  modeConfigId: modeOption?.id ?? null,
15326
15825
  options
15327
15826
  };
15328
- await this.applyMode(this.live);
15329
- await this.applyOption(this.live, "model");
15330
- await this.applyOption(this.live, "thought_level");
15827
+ this.live = live;
15828
+ await this.applyMode(live);
15829
+ await this.applyOption(live, "model");
15830
+ await this.applyOption(live, "thought_level");
15331
15831
  const pushed = conn.commands.get(sessionId);
15332
15832
  if (pushed)
15333
15833
  this.setCommands(pushed);
15334
- return this.live;
15834
+ return live;
15335
15835
  }
15336
15836
  mode() {
15337
15837
  return this.d.mode?.() ?? DEFAULT_PERMISSION_MODE;
@@ -15410,6 +15910,10 @@ class AcpSession {
15410
15910
  if (params.update.sessionUpdate === "config_option_update")
15411
15911
  this.absorb(live, params.update.configOptions);
15412
15912
  const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => this.tasks.start(toolId, start, input));
15913
+ this.forward(live, events);
15914
+ this.holdInput(live, params.update);
15915
+ }
15916
+ forward(live, events) {
15413
15917
  if (!this.running)
15414
15918
  this.trackOwn(events);
15415
15919
  for (const ev of events) {
@@ -15418,6 +15922,23 @@ class AcpSession {
15418
15922
  this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
15419
15923
  }
15420
15924
  }
15925
+ holdInput(live, update) {
15926
+ if (update.sessionUpdate !== "tool_call_update")
15927
+ return;
15928
+ const toolId = update.toolCallId;
15929
+ const memo = live.tools.get(toolId);
15930
+ const held = memo?.held;
15931
+ if (!memo || !held)
15932
+ return;
15933
+ const t = setTimeout(() => {
15934
+ if (this.live !== live || memo.held !== held || memo.cut || !(this.running || this.own))
15935
+ return;
15936
+ const sent = release(toolId, memo);
15937
+ if (sent)
15938
+ this.forward(live, [sent]);
15939
+ }, this.d.inputHoldMs ?? INPUT_HOLD_MS);
15940
+ t.unref?.();
15941
+ }
15421
15942
  onPermission(params, bounds, sandboxed) {
15422
15943
  const tools = this.live?.tools;
15423
15944
  return Promise.resolve(this.answerPermission(params, bounds, sandboxed)).then((res) => {
@@ -15476,7 +15997,7 @@ class AcpSession {
15476
15997
  async sayPlanEdited(proposed, path) {
15477
15998
  if (!await this.d.planEdited?.(path, proposed))
15478
15999
  return;
15479
- this.send(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
16000
+ this.post(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
15480
16001
  }
15481
16002
  onElicit(params, signal) {
15482
16003
  const live = this.live;
@@ -15671,9 +16192,9 @@ function sameCommands(a, b) {
15671
16192
  }
15672
16193
 
15673
16194
  // ../daemon/src/agent/planDoc.ts
15674
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
15675
- import { join as join8 } from "path";
15676
- var PLANS_DIR = join8(".toyon", "plans");
16195
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
16196
+ import { join as join9 } from "path";
16197
+ var PLANS_DIR = join9(".toyon", "plans");
15677
16198
  var PLAN_NAME = /^(\d+)\.md$/;
15678
16199
  function isPlanPath(rel) {
15679
16200
  return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
@@ -15682,7 +16203,7 @@ function nextNumber(dir) {
15682
16203
  if (!existsSync9(dir))
15683
16204
  return 1;
15684
16205
  let max = 0;
15685
- for (const name of readdirSync2(dir)) {
16206
+ for (const name of readdirSync3(dir)) {
15686
16207
  const n = Number(PLAN_NAME.exec(name)?.[1]);
15687
16208
  if (n > max)
15688
16209
  max = n;
@@ -15690,11 +16211,11 @@ function nextNumber(dir) {
15690
16211
  return max + 1;
15691
16212
  }
15692
16213
  async function writePlanDoc(cwd, markdown) {
15693
- const dir = join8(cwd, PLANS_DIR);
15694
- const rel = join8(PLANS_DIR, `${nextNumber(dir)}.md`);
16214
+ const dir = join9(cwd, PLANS_DIR);
16215
+ const rel = join9(PLANS_DIR, `${nextNumber(dir)}.md`);
15695
16216
  try {
15696
- mkdirSync5(dir, { recursive: true });
15697
- writeFileSync4(join8(cwd, rel), markdown.endsWith(`
16217
+ mkdirSync6(dir, { recursive: true });
16218
+ writeFileSync4(join9(cwd, rel), markdown.endsWith(`
15698
16219
  `) ? markdown : `${markdown}
15699
16220
  `);
15700
16221
  } catch (e) {
@@ -15705,7 +16226,7 @@ async function writePlanDoc(cwd, markdown) {
15705
16226
  return rel;
15706
16227
  }
15707
16228
  function planEdited(cwd, rel, proposed) {
15708
- const file = join8(cwd, rel);
16229
+ const file = join9(cwd, rel);
15709
16230
  if (!existsSync9(file))
15710
16231
  return false;
15711
16232
  try {
@@ -17010,11 +17531,12 @@ function defaultAgent(wt, d, preview, onProcess) {
17010
17531
  launch: (spec) => d.agents.command(spec),
17011
17532
  planSignIn: () => d.managed?.planSignIn ?? true,
17012
17533
  transcriptsDir: d.paths.transcriptsDir,
17013
- attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir),
17534
+ attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir, new UploadStore(d.paths.uploadsDir)),
17014
17535
  getSessionId: () => d.state.session(wt.id),
17015
17536
  setSessionId: (id) => d.state.setSession(wt.id, id),
17016
17537
  onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
17017
17538
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
17539
+ beforeTurn: async () => d.turnStarting?.(wt.id),
17018
17540
  onAuth: (agentId, o) => d.accounts?.observe(agentId, o),
17019
17541
  seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
17020
17542
  onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
@@ -17159,6 +17681,18 @@ class RuntimeRegistry {
17159
17681
  return true;
17160
17682
  return false;
17161
17683
  }
17684
+ demand() {
17685
+ if (this.holds.size > 0)
17686
+ return "working";
17687
+ let waiting = false;
17688
+ for (const rt of this.runtimes.values()) {
17689
+ if (rt.agent.status === "working")
17690
+ return "working";
17691
+ if (rt.agent.status === "waiting")
17692
+ waiting = true;
17693
+ }
17694
+ return waiting ? "waiting" : null;
17695
+ }
17162
17696
  markSetup(id, on) {
17163
17697
  if (on)
17164
17698
  this.settingUp.add(id);
@@ -17318,9 +17852,12 @@ class RuntimeRegistry {
17318
17852
  return rt;
17319
17853
  }
17320
17854
  async start(wt, repo) {
17321
- if (!this.deps.state.worktree(wt.id))
17855
+ const record = this.deps.state.worktree(wt.id);
17856
+ if (!record)
17322
17857
  return;
17323
17858
  const rt = this.ensureAgent(wt);
17859
+ if (record.parked)
17860
+ return;
17324
17861
  const run = repo.needsSetup ? EMPTY_RUN : resolveRun(repo, wt);
17325
17862
  const leads = wt.kind !== "main" || !this.deps.mainLeads || this.deps.mainLeads(wt.repoId);
17326
17863
  if (rt.procs) {
@@ -17719,6 +18256,11 @@ class RuntimeRegistry {
17719
18256
  return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
17720
18257
  }
17721
18258
  previewStanding(id) {
18259
+ const wt = this.deps.state.worktree(id);
18260
+ if (wt?.parked) {
18261
+ const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
18262
+ return repo && !repo.needsSetup && resolveRun(repo, wt).preview ? { status: "parked" } : null;
18263
+ }
17722
18264
  if (this.settingUp.has(id))
17723
18265
  return { status: "setup" };
17724
18266
  const rt = this.runtimes.get(id);
@@ -17783,17 +18325,17 @@ class RuntimeRegistry {
17783
18325
  }
17784
18326
 
17785
18327
  // ../daemon/src/worktrees/naming.ts
17786
- import { randomBytes } from "crypto";
18328
+ import { randomBytes as randomBytes2 } from "crypto";
17787
18329
  import { existsSync as existsSync11 } from "fs";
17788
- import { join as join9 } from "path";
18330
+ import { join as join10 } from "path";
17789
18331
  function shortId() {
17790
- return randomBytes(5).toString("hex");
18332
+ return randomBytes2(5).toString("hex");
17791
18333
  }
17792
18334
  function freeSlot(parent) {
17793
18335
  for (;; ) {
17794
18336
  const id = shortId();
17795
18337
  const dir = `wt-${id.slice(0, 4)}`;
17796
- if (!existsSync11(join9(parent, dir)))
18338
+ if (!existsSync11(join10(parent, dir)))
17797
18339
  return { id, dir };
17798
18340
  }
17799
18341
  }
@@ -17827,18 +18369,20 @@ function clip(text, max = CLIP) {
17827
18369
  const line = text.replace(/\s+/g, " ").trim();
17828
18370
  return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
17829
18371
  }
17830
- function slice(asks, ts) {
17831
- return { asks, reply: "", edits: 0, toolErrors: 0, newestTs: ts };
18372
+ function slice(asks, fixes, ts) {
18373
+ return { asks, fixes, reply: "", edits: 0, toolErrors: 0, newestTs: ts };
17832
18374
  }
17833
18375
  function turnsSince(entries, seenAt) {
17834
18376
  const turns = [];
17835
18377
  let pending = [];
18378
+ let pendingFixes = [];
17836
18379
  let open = null;
17837
18380
  const writes = new Set;
17838
18381
  const refusals = new Map;
17839
18382
  const start = (ts) => {
17840
- const t = slice(pending, ts);
18383
+ const t = slice(pending, pendingFixes, ts);
17841
18384
  pending = [];
18385
+ pendingFixes = [];
17842
18386
  turns.push(t);
17843
18387
  return t;
17844
18388
  };
@@ -17846,12 +18390,16 @@ function turnsSince(entries, seenAt) {
17846
18390
  if (open && "ts" in e)
17847
18391
  open.newestTs = Math.max(open.newestTs, e.ts);
17848
18392
  switch (e.type) {
17849
- case "user-message":
17850
- if (open && open.stop === undefined && open.error === undefined && !open.auth)
17851
- open.asks.push(e.text);
17852
- else
17853
- pending.push(e.text);
18393
+ case "user-message": {
18394
+ const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
18395
+ (into?.asks ?? pending).push(e.text);
17854
18396
  break;
18397
+ }
18398
+ case "fix-asked": {
18399
+ const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
18400
+ (into?.fixes ?? pendingFixes).push(e.why);
18401
+ break;
18402
+ }
17855
18403
  case "turn-start":
17856
18404
  open = start(e.ts);
17857
18405
  break;
@@ -18012,6 +18560,8 @@ function turnBlock(t) {
18012
18560
  const lines = [];
18013
18561
  if (t.asks.length)
18014
18562
  lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
18563
+ if (t.fixes.length)
18564
+ lines.push(`Toyon, not the user, asked for a fix: ${clip(t.fixes.join(" / "), 400)}`);
18015
18565
  if (t.reply.trim())
18016
18566
  lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
18017
18567
  return lines.join(`
@@ -18133,6 +18683,8 @@ function attachmentText(a) {
18133
18683
  return a.text;
18134
18684
  if (a.kind === "image")
18135
18685
  return a.name ? `image ${a.name}` : "an image";
18686
+ if (a.kind === "file")
18687
+ return `file ${a.name}`;
18136
18688
  return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
18137
18689
  }
18138
18690
  function parseName(text) {
@@ -18207,29 +18759,29 @@ function makePlanner(agents) {
18207
18759
  }
18208
18760
 
18209
18761
  // ../daemon/src/core/assets.ts
18210
- import { existsSync as existsSync12, readdirSync as readdirSync3, rmSync, statSync as statSync4 } from "fs";
18211
- import { join as join10, resolve as resolve5 } from "path";
18762
+ import { existsSync as existsSync12, readdirSync as readdirSync4, rmSync as rmSync2, statSync as statSync4 } from "fs";
18763
+ import { join as join11, resolve as resolve5 } from "path";
18212
18764
  function locateAssets(here, env = process.env) {
18213
- const packaged = existsSync12(join10(here, "shell", "index.html"));
18765
+ const packaged = existsSync12(join11(here, "shell", "index.html"));
18214
18766
  return {
18215
- shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join10(here, "shell") : join10(here, "../../shell/dist")),
18216
- bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join10(here, "bridge.js") : join10(here, "../../bridge/dist/bridge.js")),
18767
+ shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join11(here, "shell") : join11(here, "../../shell/dist")),
18768
+ bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join11(here, "bridge.js") : join11(here, "../../bridge/dist/bridge.js")),
18217
18769
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
18218
- packageJson: packaged ? join10(here, "..", "package.json") : null
18770
+ packageJson: packaged ? join11(here, "..", "package.json") : null
18219
18771
  };
18220
18772
  }
18221
18773
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
18222
18774
  function pruneAssets(shellDist) {
18223
- const dir = join10(shellDist, "assets");
18775
+ const dir = join11(shellDist, "assets");
18224
18776
  let names;
18225
18777
  try {
18226
- names = readdirSync3(dir);
18778
+ names = readdirSync4(dir);
18227
18779
  } catch {
18228
18780
  return 0;
18229
18781
  }
18230
18782
  const found = [];
18231
18783
  for (const name of names) {
18232
- const path = join10(dir, name);
18784
+ const path = join11(dir, name);
18233
18785
  try {
18234
18786
  found.push({ path, mtime: statSync4(path).mtimeMs });
18235
18787
  } catch {}
@@ -18242,100 +18794,15 @@ function pruneAssets(shellDist) {
18242
18794
  if (newest - f.mtime <= KEEP_MS)
18243
18795
  continue;
18244
18796
  try {
18245
- rmSync(f.path, { recursive: true, force: true });
18797
+ rmSync2(f.path, { recursive: true, force: true });
18246
18798
  gone++;
18247
18799
  } catch {}
18248
18800
  }
18249
18801
  return gone;
18250
18802
  }
18251
18803
 
18252
- // ../daemon/src/core/dialog.ts
18253
- import { existsSync as existsSync13 } from "fs";
18254
- import { homedir as homedir5 } from "os";
18255
- var PROMPTS = {
18256
- location: "Choose where the new project goes",
18257
- open: "Choose a project folder to open"
18258
- };
18259
- var SCRIPT = [
18260
- "on run argv",
18261
- "activate",
18262
- "POSIX path of (choose folder with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))",
18263
- "end run"
18264
- ];
18265
- function folderDialog() {
18266
- let open = null;
18267
- let child = null;
18268
- let cancelled = false;
18269
- const ask = async (start, purpose) => {
18270
- const typed = start === "~" || start.startsWith("~/") ? `${homedir5()}${start.slice(1)}` : start;
18271
- const from = typed.startsWith("/") && existsSync13(typed) ? typed : homedir5();
18272
- cancelled = false;
18273
- const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
18274
- stdout: "pipe",
18275
- stderr: "pipe",
18276
- stdin: "ignore"
18277
- });
18278
- child = p;
18279
- const [out, err, code] = await Promise.all([
18280
- new Response(p.stdout).text(),
18281
- new Response(p.stderr).text(),
18282
- p.exited
18283
- ]);
18284
- child = null;
18285
- if (code === 0)
18286
- return out.trim().replace(/\/+$/, "") || "/";
18287
- if (cancelled || err.includes("(-128)"))
18288
- return null;
18289
- throw new UserError(`the Finder dialog did not open: ${err.trim()}`);
18290
- };
18291
- return {
18292
- choose(start, purpose) {
18293
- if (process.platform !== "darwin") {
18294
- return Promise.reject(new UserError("choosing a folder in Finder is only available on macOS"));
18295
- }
18296
- if (!open) {
18297
- open = ask(start, purpose).finally(() => {
18298
- open = null;
18299
- });
18300
- }
18301
- return open;
18302
- },
18303
- cancel() {
18304
- if (!child)
18305
- return;
18306
- cancelled = true;
18307
- child.kill();
18308
- }
18309
- };
18310
- }
18311
-
18312
- // ../daemon/src/core/hub.ts
18313
- class Hub {
18314
- listeners = new Map;
18315
- on(event, fn) {
18316
- let set = this.listeners.get(event);
18317
- if (!set) {
18318
- set = new Set;
18319
- this.listeners.set(event, set);
18320
- }
18321
- set.add(fn);
18322
- return () => set?.delete(fn);
18323
- }
18324
- emit(event, ...args) {
18325
- const set = this.listeners.get(event);
18326
- if (!set)
18327
- return;
18328
- for (const fn of set) {
18329
- try {
18330
- const r = fn(...args);
18331
- if (r instanceof Promise)
18332
- r.catch((e) => log.error("hub", `async listener for ${String(event)} rejected`, e));
18333
- } catch (e) {
18334
- log.error("hub", `listener for ${String(event)} threw`, e);
18335
- }
18336
- }
18337
- }
18338
- }
18804
+ // ../daemon/src/core/awake.ts
18805
+ import { spawn as spawn3 } from "child_process";
18339
18806
 
18340
18807
  // ../daemon/src/runtime/idle.ts
18341
18808
  var LOCAL_SLEEP_MS = 2 * 60 * 60000;
@@ -18643,21 +19110,223 @@ function humanMs(ms) {
18643
19110
  return `${Math.round(ms / 1000)} s`;
18644
19111
  }
18645
19112
 
18646
- // ../daemon/src/core/idleExit.ts
18647
- var STOP_AFTER_MS = 30 * 60000;
18648
- function stopAfterFrom(raw, opts) {
18649
- if (raw === "off")
19113
+ // ../daemon/src/core/awake.ts
19114
+ var WAIT_MS = 30 * 60000;
19115
+ function idleSleepAssertion(raw, platform = process.platform) {
19116
+ if (raw === "off" || platform !== "darwin")
18650
19117
  return null;
18651
- const n = Number(raw);
18652
- if (raw && Number.isFinite(n) && n > 0)
18653
- return n;
18654
- return opts.remote || opts.terminal || (opts.inCloud ?? cloud.enabled) ? null : STOP_AFTER_MS;
19118
+ return () => {
19119
+ const child = spawn3("/usr/bin/caffeinate", ["-i", "-w", String(process.pid)], { stdio: "ignore" });
19120
+ child.on("error", (e) => log.warn("awake", "caffeinate did not start, so the Mac may sleep mid-turn", e));
19121
+ child.unref();
19122
+ return () => {
19123
+ child.kill();
19124
+ };
19125
+ };
18655
19126
  }
18656
19127
 
18657
- class IdleExit {
19128
+ class KeepAwake {
18658
19129
  d;
18659
- connected = 0;
18660
- quietAt;
19130
+ release = null;
19131
+ waitingAt = null;
19132
+ remote = 0;
19133
+ remoteLeftAt = null;
19134
+ timer = null;
19135
+ gone = false;
19136
+ waitMs;
19137
+ now;
19138
+ constructor(d) {
19139
+ this.d = d;
19140
+ this.waitMs = d.waitMs ?? WAIT_MS;
19141
+ this.now = d.now ?? Date.now;
19142
+ d.hub.on("agentStatus", (_id, status) => {
19143
+ if (status === "waiting")
19144
+ this.waitingAt = this.now();
19145
+ this.reconsider();
19146
+ });
19147
+ d.hub.on("holdsChanged", () => this.reconsider());
19148
+ d.hub.on("keepAwakeChanged", () => this.reconsider());
19149
+ this.reconsider();
19150
+ }
19151
+ setting() {
19152
+ return this.d.assert ? this.d.mode() : null;
19153
+ }
19154
+ remoteShells(n) {
19155
+ if (n === this.remote)
19156
+ return;
19157
+ if (n === 0)
19158
+ this.remoteLeftAt = this.now();
19159
+ this.remote = n;
19160
+ this.reconsider();
19161
+ }
19162
+ stop() {
19163
+ this.gone = true;
19164
+ this.disarm();
19165
+ this.hold(false);
19166
+ }
19167
+ reconsider() {
19168
+ if (this.gone || !this.d.assert)
19169
+ return;
19170
+ this.disarm();
19171
+ const mode = this.d.mode();
19172
+ const demand = this.d.demand();
19173
+ if (demand !== "waiting" || !this.d.answerable)
19174
+ this.waitingAt = null;
19175
+ else
19176
+ this.waitingAt ??= this.now();
19177
+ if (mode === "off") {
19178
+ this.hold(false);
19179
+ return;
19180
+ }
19181
+ if (mode === "always" || demand === "working" || this.remote > 0) {
19182
+ this.hold(true);
19183
+ return;
19184
+ }
19185
+ const since = Math.max(this.waitingAt ?? Number.NEGATIVE_INFINITY, this.remoteLeftAt ?? Number.NEGATIVE_INFINITY);
19186
+ const left = since + this.waitMs - this.now();
19187
+ if (left > 0) {
19188
+ this.hold(true);
19189
+ this.arm(left);
19190
+ return;
19191
+ }
19192
+ if (this.release && Number.isFinite(since)) {
19193
+ log.info("awake", `nobody has answered or connected for ${humanMs(this.waitMs)}; the machine may sleep again`);
19194
+ }
19195
+ this.hold(false);
19196
+ }
19197
+ hold(on) {
19198
+ if (on === (this.release !== null))
19199
+ return;
19200
+ if (on) {
19201
+ this.release = this.d.assert?.() ?? null;
19202
+ } else {
19203
+ this.release?.();
19204
+ this.release = null;
19205
+ }
19206
+ log.debug("awake", on ? "holding off idle sleep" : "idle sleep allowed again");
19207
+ }
19208
+ arm(ms) {
19209
+ const set = this.d.setTimer ?? ((fn, delay) => {
19210
+ const t = setTimeout(fn, delay);
19211
+ t.unref?.();
19212
+ return t;
19213
+ });
19214
+ this.timer = set(() => {
19215
+ this.timer = null;
19216
+ this.reconsider();
19217
+ }, ms);
19218
+ }
19219
+ disarm() {
19220
+ if (!this.timer)
19221
+ return;
19222
+ (this.d.clearTimer ?? ((t) => clearTimeout(t)))(this.timer);
19223
+ this.timer = null;
19224
+ }
19225
+ }
19226
+
19227
+ // ../daemon/src/core/dialog.ts
19228
+ import { existsSync as existsSync13 } from "fs";
19229
+ import { homedir as homedir5 } from "os";
19230
+ var PROMPTS = {
19231
+ location: "Choose where the new project goes",
19232
+ open: "Choose a project folder to open"
19233
+ };
19234
+ var SCRIPT = [
19235
+ "on run argv",
19236
+ "activate",
19237
+ "POSIX path of (choose folder with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))",
19238
+ "end run"
19239
+ ];
19240
+ function folderDialog() {
19241
+ let open = null;
19242
+ let child = null;
19243
+ let cancelled = false;
19244
+ const ask = async (start, purpose) => {
19245
+ const typed = start === "~" || start.startsWith("~/") ? `${homedir5()}${start.slice(1)}` : start;
19246
+ const from = typed.startsWith("/") && existsSync13(typed) ? typed : homedir5();
19247
+ cancelled = false;
19248
+ const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
19249
+ stdout: "pipe",
19250
+ stderr: "pipe",
19251
+ stdin: "ignore"
19252
+ });
19253
+ child = p;
19254
+ const [out, err, code] = await Promise.all([
19255
+ new Response(p.stdout).text(),
19256
+ new Response(p.stderr).text(),
19257
+ p.exited
19258
+ ]);
19259
+ child = null;
19260
+ if (code === 0)
19261
+ return out.trim().replace(/\/+$/, "") || "/";
19262
+ if (cancelled || err.includes("(-128)"))
19263
+ return null;
19264
+ throw new UserError(`the Finder dialog did not open: ${err.trim()}`);
19265
+ };
19266
+ return {
19267
+ choose(start, purpose) {
19268
+ if (process.platform !== "darwin") {
19269
+ return Promise.reject(new UserError("choosing a folder in Finder is only available on macOS"));
19270
+ }
19271
+ if (!open) {
19272
+ open = ask(start, purpose).finally(() => {
19273
+ open = null;
19274
+ });
19275
+ }
19276
+ return open;
19277
+ },
19278
+ cancel() {
19279
+ if (!child)
19280
+ return;
19281
+ cancelled = true;
19282
+ child.kill();
19283
+ }
19284
+ };
19285
+ }
19286
+
19287
+ // ../daemon/src/core/hub.ts
19288
+ class Hub {
19289
+ listeners = new Map;
19290
+ on(event, fn) {
19291
+ let set = this.listeners.get(event);
19292
+ if (!set) {
19293
+ set = new Set;
19294
+ this.listeners.set(event, set);
19295
+ }
19296
+ set.add(fn);
19297
+ return () => set?.delete(fn);
19298
+ }
19299
+ emit(event, ...args) {
19300
+ const set = this.listeners.get(event);
19301
+ if (!set)
19302
+ return;
19303
+ for (const fn of set) {
19304
+ try {
19305
+ const r = fn(...args);
19306
+ if (r instanceof Promise)
19307
+ r.catch((e) => log.error("hub", `async listener for ${String(event)} rejected`, e));
19308
+ } catch (e) {
19309
+ log.error("hub", `listener for ${String(event)} threw`, e);
19310
+ }
19311
+ }
19312
+ }
19313
+ }
19314
+
19315
+ // ../daemon/src/core/idleExit.ts
19316
+ var STOP_AFTER_MS = 30 * 60000;
19317
+ function stopAfterFrom(raw, opts) {
19318
+ if (raw === "off")
19319
+ return null;
19320
+ const n = Number(raw);
19321
+ if (raw && Number.isFinite(n) && n > 0)
19322
+ return n;
19323
+ return opts.remote || opts.terminal || (opts.inCloud ?? cloud.enabled) ? null : STOP_AFTER_MS;
19324
+ }
19325
+
19326
+ class IdleExit {
19327
+ d;
19328
+ connected = 0;
19329
+ quietAt;
18661
19330
  timer = null;
18662
19331
  gone = false;
18663
19332
  afterMs;
@@ -18732,7 +19401,7 @@ class IdleExit {
18732
19401
  }
18733
19402
 
18734
19403
  // ../daemon/src/core/restart.ts
18735
- import { spawn as spawn3 } from "child_process";
19404
+ import { spawn as spawn4 } from "child_process";
18736
19405
  import { openSync } from "fs";
18737
19406
  import { homedir as homedir6 } from "os";
18738
19407
  function restartable(env = process.env) {
@@ -18753,7 +19422,7 @@ async function replacementRuns() {
18753
19422
  }
18754
19423
  function respawn(logFile) {
18755
19424
  const logFd = openSync(logFile, "a");
18756
- const child = spawn3(process.execPath, process.argv.slice(1), {
19425
+ const child = spawn4(process.execPath, process.argv.slice(1), {
18757
19426
  detached: true,
18758
19427
  stdio: ["ignore", logFd, logFd],
18759
19428
  env: { ...process.env }
@@ -18894,7 +19563,7 @@ class SelfWatch {
18894
19563
  }
18895
19564
 
18896
19565
  // ../daemon/src/core/state.ts
18897
- import { randomBytes as randomBytes2 } from "crypto";
19566
+ import { randomBytes as randomBytes3 } from "crypto";
18898
19567
  import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
18899
19568
  var GROUPS_SAVE_MS = 250;
18900
19569
  var SEEN_WORKTREES = 50;
@@ -18956,7 +19625,7 @@ function loadOrCreateToken(paths) {
18956
19625
  const TOKEN_FILE = paths.tokenFile;
18957
19626
  if (existsSync14(TOKEN_FILE))
18958
19627
  return readFileSync10(TOKEN_FILE, "utf8").trim();
18959
- const token = randomBytes2(32).toString("hex");
19628
+ const token = randomBytes3(32).toString("hex");
18960
19629
  writeFileSync5(TOKEN_FILE, token, { mode: 384 });
18961
19630
  return token;
18962
19631
  }
@@ -19138,6 +19807,16 @@ class StateStore {
19138
19807
  delete this.state.defaultAgent;
19139
19808
  this.save();
19140
19809
  }
19810
+ get keepAwake() {
19811
+ return this.state.keepAwake ?? "use";
19812
+ }
19813
+ setKeepAwake(mode) {
19814
+ if (mode === "use")
19815
+ delete this.state.keepAwake;
19816
+ else
19817
+ this.state.keepAwake = mode;
19818
+ this.save();
19819
+ }
19141
19820
  get theme() {
19142
19821
  return this.state.theme;
19143
19822
  }
@@ -19170,7 +19849,7 @@ class StateStore {
19170
19849
  }
19171
19850
 
19172
19851
  // ../daemon/src/design/service.ts
19173
- import { join as join11 } from "path";
19852
+ import { join as join12 } from "path";
19174
19853
 
19175
19854
  // ../daemon/src/design/scan.ts
19176
19855
  var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
@@ -19620,7 +20299,7 @@ class DesignService {
19620
20299
  async function readAll(root, paths) {
19621
20300
  const out = [];
19622
20301
  for (const path of paths) {
19623
- const file = Bun.file(join11(root, path));
20302
+ const file = Bun.file(join12(root, path));
19624
20303
  if (file.size === 0 || file.size > MAX_FILE_BYTES)
19625
20304
  continue;
19626
20305
  try {
@@ -19735,7 +20414,7 @@ function typicalUse(classes) {
19735
20414
  var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
19736
20415
  async function loadProjectTypescript(root) {
19737
20416
  try {
19738
- return await import(join11(root, "node_modules", "typescript", "lib", "typescript.js"));
20417
+ return await import(join12(root, "node_modules", "typescript", "lib", "typescript.js"));
19739
20418
  } catch {
19740
20419
  return null;
19741
20420
  }
@@ -19748,28 +20427,32 @@ var SAVE_DELAY_MS = 1000;
19748
20427
  class DraftStore {
19749
20428
  d;
19750
20429
  drafts = {};
20430
+ lists = {};
20431
+ dirty = { text: false, lists: false };
19751
20432
  saveTimer = null;
19752
20433
  constructor(d) {
19753
20434
  this.d = d;
19754
- if (!existsSync15(d.file))
19755
- return;
19756
- try {
19757
- const raw = JSON.parse(readFileSync11(d.file, "utf8"));
19758
- if (!raw || typeof raw !== "object")
19759
- return;
19760
- for (const [id, text] of Object.entries(raw)) {
19761
- if (typeof text === "string" && text)
19762
- this.drafts[id] = text;
19763
- }
19764
- } catch (e) {
19765
- log.warn("drafts", `skipped an unreadable ${d.file}`, e);
20435
+ for (const [id, text] of readRecord(d.file)) {
20436
+ if (typeof text === "string" && text)
20437
+ this.drafts[id] = text;
20438
+ }
20439
+ for (const [id, list] of readRecord(d.attachmentsFile)) {
20440
+ const items = (Array.isArray(list) ? list : []).flatMap((item) => {
20441
+ const r = attachmentInputSchema.safeParse(item);
20442
+ return r.success ? [r.data] : [];
20443
+ });
20444
+ if (items.length)
20445
+ this.lists[id] = items;
19766
20446
  }
19767
20447
  }
19768
20448
  all() {
19769
20449
  return { ...this.drafts };
19770
20450
  }
20451
+ allAttachments() {
20452
+ return { ...this.lists };
20453
+ }
19771
20454
  has(boxId) {
19772
- return !!this.drafts[boxId]?.trim();
20455
+ return !!this.drafts[boxId]?.trim() || !!this.lists[boxId]?.length;
19773
20456
  }
19774
20457
  text(boxId) {
19775
20458
  return this.drafts[boxId] ?? "";
@@ -19781,22 +20464,67 @@ class DraftStore {
19781
20464
  this.drafts[boxId] = text;
19782
20465
  else
19783
20466
  delete this.drafts[boxId];
19784
- this.scheduleSave();
20467
+ this.scheduleSave("text");
19785
20468
  this.d.hub.emit("draftChanged", boxId, text, clientId);
19786
20469
  }
20470
+ attachments(boxId) {
20471
+ return this.lists[boxId] ?? [];
20472
+ }
20473
+ setAttachments(boxId, items, clientId) {
20474
+ if (JSON.stringify(this.lists[boxId] ?? []) === JSON.stringify(items))
20475
+ return;
20476
+ if (items.length)
20477
+ this.lists[boxId] = items;
20478
+ else
20479
+ delete this.lists[boxId];
20480
+ this.scheduleSave("lists");
20481
+ this.d.uploads?.keep(this.uploadIds());
20482
+ this.d.hub.emit("attachmentsChanged", boxId, items, clientId);
20483
+ }
20484
+ take(boxId, clientId) {
20485
+ const was = { text: this.text(boxId), items: this.attachments(boxId) };
20486
+ this.set(boxId, "", clientId);
20487
+ this.setAttachments(boxId, [], clientId);
20488
+ return was;
20489
+ }
20490
+ putBack(boxId, was, clientId) {
20491
+ if (was.text && !this.text(boxId))
20492
+ this.set(boxId, was.text, clientId);
20493
+ if (was.items.length)
20494
+ this.setAttachments(boxId, [...was.items, ...this.attachments(boxId)], clientId);
20495
+ }
20496
+ uploadIds() {
20497
+ return Object.values(this.lists).flatMap((items) => uploadIds(items));
20498
+ }
19787
20499
  drop(boxId) {
19788
20500
  this.set(boxId, "");
20501
+ this.setAttachments(boxId, []);
20502
+ }
20503
+ move(fromId, toId, over = false) {
20504
+ const text = this.text(fromId);
20505
+ const items = this.attachments(fromId);
20506
+ if (!text && !items.length)
20507
+ return false;
20508
+ if (!over && (this.text(toId) || this.attachments(toId).length))
20509
+ return false;
20510
+ this.set(toId, text);
20511
+ this.setAttachments(toId, items);
20512
+ this.drop(fromId);
20513
+ return true;
19789
20514
  }
19790
20515
  prune(known) {
19791
- let changed = false;
19792
20516
  for (const id of Object.keys(this.drafts)) {
19793
20517
  if (known(id))
19794
20518
  continue;
19795
20519
  delete this.drafts[id];
19796
- changed = true;
20520
+ this.scheduleSave("text");
20521
+ }
20522
+ for (const id of Object.keys(this.lists)) {
20523
+ if (known(id))
20524
+ continue;
20525
+ delete this.lists[id];
20526
+ this.scheduleSave("lists");
19797
20527
  }
19798
- if (changed)
19799
- this.scheduleSave();
19800
20528
  }
19801
20529
  flush() {
19802
20530
  if (!this.saveTimer)
@@ -19805,7 +20533,8 @@ class DraftStore {
19805
20533
  this.saveTimer = null;
19806
20534
  this.write();
19807
20535
  }
19808
- scheduleSave() {
20536
+ scheduleSave(which) {
20537
+ this.dirty[which] = true;
19809
20538
  if (this.saveTimer)
19810
20539
  return;
19811
20540
  this.saveTimer = setTimeout(() => {
@@ -19815,13 +20544,31 @@ class DraftStore {
19815
20544
  this.saveTimer.unref?.();
19816
20545
  }
19817
20546
  write() {
19818
- const tmp = `${this.d.file}.tmp`;
19819
- try {
19820
- writeFileSync6(tmp, JSON.stringify(this.drafts));
19821
- renameSync3(tmp, this.d.file);
19822
- } catch (e) {
19823
- log.error("drafts", "could not write the drafts", e);
19824
- }
20547
+ if (this.dirty.text)
20548
+ writeAtomic(this.d.file, this.drafts);
20549
+ if (this.dirty.lists)
20550
+ writeAtomic(this.d.attachmentsFile, this.lists);
20551
+ this.dirty = { text: false, lists: false };
20552
+ }
20553
+ }
20554
+ function readRecord(file) {
20555
+ if (!existsSync15(file))
20556
+ return [];
20557
+ try {
20558
+ const raw = JSON.parse(readFileSync11(file, "utf8"));
20559
+ return raw && typeof raw === "object" ? Object.entries(raw) : [];
20560
+ } catch (e) {
20561
+ log.warn("drafts", `skipped an unreadable ${file}`, e);
20562
+ return [];
20563
+ }
20564
+ }
20565
+ function writeAtomic(file, value) {
20566
+ const tmp = `${file}.tmp`;
20567
+ try {
20568
+ writeFileSync6(tmp, JSON.stringify(value));
20569
+ renameSync3(tmp, file);
20570
+ } catch (e) {
20571
+ log.error("drafts", `could not write ${file}`, e);
19825
20572
  }
19826
20573
  }
19827
20574
 
@@ -19879,12 +20626,13 @@ class ExecService {
19879
20626
  if (opts.quiet)
19880
20627
  agent.note(start);
19881
20628
  agent.note({ type: "tool-end", toolId, output: `could not run: ${reason}`, isError: true });
19882
- return Promise.resolve({ exit: reason, text: "" });
20629
+ return Promise.resolve({ exit: reason, text: "", toolId });
19883
20630
  }
19884
20631
  const pid = proc.pid;
19885
20632
  let killing;
19886
20633
  const running = {
19887
20634
  stop: () => {
20635
+ running.stopping = true;
19888
20636
  killing ??= killGroup(pid, groupGone(pid)).then((signal) => {
19889
20637
  if (signal)
19890
20638
  running.signal = signal;
@@ -19901,7 +20649,8 @@ class ExecService {
19901
20649
  this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
19902
20650
  this.deps.runtime.noteGroup(worktreeId, `exec:${toolId}`, pid);
19903
20651
  opts.onSpawn?.(pid, () => fireAndForget(worktreeId, running.stop(), `exec ${toolId} stopped`));
19904
- return this.collect(worktreeId, toolId, proc, running, agent, ceiling, opts.quiet ? start : undefined);
20652
+ const kind = name === CHECK_TOOL ? "check" : "command";
20653
+ return this.collect(worktreeId, toolId, proc, running, agent, ceiling, kind, opts.quiet ? start : undefined);
19905
20654
  }
19906
20655
  async watch(worktreeId, command, run) {
19907
20656
  const agent = this.agentFor(worktreeId);
@@ -19939,16 +20688,17 @@ class ExecService {
19939
20688
  this.untrack(worktreeId, toolId);
19940
20689
  }
19941
20690
  if (!up && r.exit === 0)
19942
- return { ...r, shown: false };
20691
+ return { ...r, shown: false, toolId };
19943
20692
  if (!up)
19944
20693
  agent.note(start);
19945
20694
  agent.note({
19946
20695
  type: "tool-end",
19947
20696
  toolId,
19948
20697
  output: formatOutput(text, r.exit, truncated, r.ceilingMs),
19949
- isError: r.exit !== 0
20698
+ isError: r.exit !== 0,
20699
+ ...r.hook && typeof r.exit === "number" && r.exit !== 0 ? { fixable: { kind: "hook", hook: r.hook } } : {}
19950
20700
  });
19951
- return { ...r, shown: true };
20701
+ return { ...r, shown: true, toolId };
19952
20702
  }
19953
20703
  async stop(worktreeId, toolId) {
19954
20704
  const byTool = this.running.get(worktreeId);
@@ -19981,7 +20731,7 @@ class ExecService {
19981
20731
  this.deps.runtime.release(worktreeId, `exec:${toolId}`);
19982
20732
  this.deps.runtime.forgetGroup(worktreeId, pgid);
19983
20733
  }
19984
- async collect(worktreeId, toolId, proc, running, agent, ceiling, heldStart) {
20734
+ async collect(worktreeId, toolId, proc, running, agent, ceiling, kind, heldStart) {
19985
20735
  let text = "";
19986
20736
  let truncated = false;
19987
20737
  const live = !heldStart;
@@ -20019,7 +20769,13 @@ class ExecService {
20019
20769
  log.warn(worktreeId, "exec: reading output failed", e);
20020
20770
  }
20021
20771
  const pgid = proc.pid;
20022
- const end = (how) => agent.note({ type: "tool-end", toolId, output: formatOutput(text, how, truncated, ceiling), isError: how !== 0 });
20772
+ const end = (how) => agent.note({
20773
+ type: "tool-end",
20774
+ toolId,
20775
+ output: formatOutput(text, how, truncated, ceiling),
20776
+ isError: how !== 0,
20777
+ ...typeof how === "number" && how !== 0 && !running.stopping && !running.timedOut ? { fixable: { kind } } : {}
20778
+ });
20023
20779
  const lingers = running.signal === undefined && groupAlive(pgid);
20024
20780
  if (!lingers)
20025
20781
  this.settle(worktreeId, toolId, pgid);
@@ -20030,18 +20786,18 @@ class ExecService {
20030
20786
  }
20031
20787
  if (lingers)
20032
20788
  fireAndForget(worktreeId, this.linger(running, pgid).then(() => this.settle(worktreeId, toolId, pgid)), `exec ${toolId} left running`);
20033
- return { exit: running.timedOut ? "timeout" : exit, text };
20789
+ return { exit: running.timedOut ? "timeout" : exit, text, toolId };
20034
20790
  }
20035
20791
  if (!lingers) {
20036
20792
  end(running.timedOut ? "timeout" : exit);
20037
- return { exit: running.timedOut ? "timeout" : exit, text };
20793
+ return { exit: running.timedOut ? "timeout" : exit, text, toolId };
20038
20794
  }
20039
20795
  agent.note({ type: "tool-update", toolId, background: true });
20040
20796
  fireAndForget(worktreeId, this.linger(running, pgid).then(() => {
20041
20797
  this.settle(worktreeId, toolId, pgid);
20042
20798
  end(running.timedOut ? "timeout" : running.signal ?? exit);
20043
20799
  }), `exec ${toolId} in the background`);
20044
- return { exit, text };
20800
+ return { exit, text, toolId };
20045
20801
  }
20046
20802
  async linger(running, pgid) {
20047
20803
  while (running.signal === undefined && groupAlive(pgid))
@@ -20050,17 +20806,17 @@ class ExecService {
20050
20806
  }
20051
20807
 
20052
20808
  // ../daemon/src/files/open.ts
20053
- import { stat as stat2 } from "fs/promises";
20809
+ import { stat as stat3 } from "fs/promises";
20054
20810
  import { basename as basename3, resolve as resolve7 } from "path";
20055
20811
 
20056
20812
  // ../daemon/src/repos/browse.ts
20057
20813
  import { existsSync as existsSync16, statSync as statSync5 } from "fs";
20058
20814
  import { readdir } from "fs/promises";
20059
20815
  import { homedir as homedir7 } from "os";
20060
- import { dirname as dirname5, join as join12 } from "path";
20816
+ import { dirname as dirname5, join as join13 } from "path";
20061
20817
  var MAX_ENTRIES = 40;
20062
20818
  function expandTilde(raw) {
20063
- return raw === "~" || raw.startsWith("~/") ? join12(homedir7(), raw.slice(1)) : raw;
20819
+ return raw === "~" || raw.startsWith("~/") ? join13(homedir7(), raw.slice(1)) : raw;
20064
20820
  }
20065
20821
  function collapseTilde(abs) {
20066
20822
  const home = homedir7();
@@ -20071,7 +20827,7 @@ function holdsNothing(names) {
20071
20827
  }
20072
20828
  async function describeFolder(abs) {
20073
20829
  const path = collapseTilde(abs);
20074
- if (existsSync16(join12(abs, ".git")))
20830
+ if (existsSync16(join13(abs, ".git")))
20075
20831
  return { path, kind: "project" };
20076
20832
  try {
20077
20833
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -20088,7 +20844,7 @@ function describe2(typed, parent) {
20088
20844
  return {
20089
20845
  exists: existsSync16(typed),
20090
20846
  isDir,
20091
- isRepo: isDir && existsSync16(join12(typed, ".git")),
20847
+ isRepo: isDir && existsSync16(join13(typed, ".git")),
20092
20848
  parentExists: existsSync16(parent)
20093
20849
  };
20094
20850
  }
@@ -20113,8 +20869,8 @@ async function browsePath(raw) {
20113
20869
  continue;
20114
20870
  if (!name.toLowerCase().startsWith(prefix))
20115
20871
  continue;
20116
- const abs = join12(dir, name);
20117
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync16(join12(abs, ".git")) });
20872
+ const abs = join13(dir, name);
20873
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync16(join13(abs, ".git")) });
20118
20874
  }
20119
20875
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
20120
20876
  return { entries: entries.slice(0, MAX_ENTRIES), target };
@@ -20150,8 +20906,8 @@ function looksBinary(text) {
20150
20906
  }
20151
20907
 
20152
20908
  // ../daemon/src/files/service.ts
20153
- import { spawn as spawn4 } from "child_process";
20154
- import { stat, unlink } from "fs/promises";
20909
+ import { spawn as spawn5 } from "child_process";
20910
+ import { stat as stat2, unlink } from "fs/promises";
20155
20911
 
20156
20912
  // ../daemon/src/git/blame.ts
20157
20913
  var NONE = { commits: [], lines: [] };
@@ -20222,10 +20978,13 @@ async function withLock(key, fn) {
20222
20978
  function withRepoLock(repoPath, fn) {
20223
20979
  return withLock(repoPath, fn);
20224
20980
  }
20981
+ function withFetchLock(repoPath, fn) {
20982
+ return withLock(`fetch:${repoPath}`, fn);
20983
+ }
20225
20984
  var fileLockKey = (absPath) => `file:${absPath}`;
20226
20985
 
20227
20986
  // ../daemon/src/git/status.ts
20228
- import { join as join13 } from "path";
20987
+ import { join as join14 } from "path";
20229
20988
  async function treeFingerprint(worktreePath) {
20230
20989
  const [head, diff, untracked] = await Promise.all([
20231
20990
  git(worktreePath, "rev-parse", "HEAD"),
@@ -20235,7 +20994,7 @@ async function treeFingerprint(worktreePath) {
20235
20994
  const others = await Promise.all(untracked.out.split(`
20236
20995
  `).filter(Boolean).map(async (p) => {
20237
20996
  try {
20238
- const f = Bun.file(join13(worktreePath, p));
20997
+ const f = Bun.file(join14(worktreePath, p));
20239
20998
  const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
20240
20999
  return `${p}:${body}`;
20241
21000
  } catch {
@@ -20268,6 +21027,10 @@ async function fileBefore(worktreePath, defaultBr, file) {
20268
21027
  const r = await gitRaw(worktreePath, "show", `${ref}:${file}`);
20269
21028
  return r.ok ? r.out : "";
20270
21029
  }
21030
+ async function fileAt(worktreePath, commit, file) {
21031
+ const r = await gitRaw(worktreePath, "show", `${commit}:${file}`);
21032
+ return r.ok ? r.out : "";
21033
+ }
20271
21034
  function parseNumstat(out) {
20272
21035
  const counts = new Map;
20273
21036
  for (const line of out.split(`
@@ -20286,7 +21049,7 @@ async function numstat(worktreePath, ...range) {
20286
21049
  }
20287
21050
  async function untrackedLines(worktreePath, file) {
20288
21051
  try {
20289
- const f = Bun.file(join13(worktreePath, file));
21052
+ const f = Bun.file(join14(worktreePath, file));
20290
21053
  if (f.size > 2000000)
20291
21054
  return {};
20292
21055
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -20459,7 +21222,7 @@ var COMMON = 40;
20459
21222
  var NEAR = 3;
20460
21223
  var CLEAR_LEAD = 1.5;
20461
21224
  var ELEMENT_SOURCES_MAX = 8;
20462
- var NAME = /^[\w-]+$/;
21225
+ var NAME2 = /^[\w-]+$/;
20463
21226
  var TAG = /^[a-z][a-z0-9-]*$/;
20464
21227
  var NAME_CHAR = /[\w-]/;
20465
21228
  function needlesOf(el) {
@@ -20472,12 +21235,12 @@ function needlesOf(el) {
20472
21235
  out.set(text, { text, weight, word });
20473
21236
  };
20474
21237
  if (el.id)
20475
- add(el.id, 8, NAME.test(el.id));
21238
+ add(el.id, 8, NAME2.test(el.id));
20476
21239
  const words = (el.text.split(/[&<>"'`${}]/)[0] ?? "").trim().split(/\s+/).slice(0, 6).join(" ");
20477
21240
  if (words.length >= 3 && /\p{L}/u.test(words))
20478
21241
  add(words, 6, false);
20479
21242
  for (const [, value] of el.attrs)
20480
- add(value, 5, NAME.test(value));
21243
+ add(value, 5, NAME2.test(value));
20481
21244
  for (const c of el.classes)
20482
21245
  add(c, 3, true);
20483
21246
  return [...out.values()];
@@ -20643,7 +21406,7 @@ var NOT_WHERE_WRITTEN = [
20643
21406
  ];
20644
21407
  var NO_TEXT = { before: "", after: "" };
20645
21408
  async function statFile(target) {
20646
- const st = await stat(target).catch((e) => {
21409
+ const st = await stat2(target).catch((e) => {
20647
21410
  if (e.code === "ENOENT")
20648
21411
  return null;
20649
21412
  throw e;
@@ -20704,14 +21467,14 @@ class FileService {
20704
21467
  throw new UserError("unknown worktree");
20705
21468
  return r;
20706
21469
  }
20707
- async read(worktreeId, path, ref) {
21470
+ async read(worktreeId, path, ref, since) {
20708
21471
  const r = this.require(worktreeId);
20709
21472
  const target = resolveInside(r.path, path);
20710
21473
  if (ref) {
20711
21474
  const { before, after } = await fileAtCommit(r.path, ref, path);
20712
21475
  return { ...sides(before, after), version: null, writable: false };
20713
21476
  }
20714
- const before = await fileBefore(r.path, r.base, path);
21477
+ const before = since ? await fileAt(r.path, since, path) : await fileBefore(r.path, r.base, path);
20715
21478
  const st = await statFile(target);
20716
21479
  if (st && st.size > FILE_MAX_CHARS) {
20717
21480
  return { ...NO_TEXT, version: `${st.size}-${st.mtimeMs}`, writable: false, binary: false, tooLarge: true };
@@ -20842,7 +21605,7 @@ class FileService {
20842
21605
  revealPath(target) {
20843
21606
  if (process.platform !== "darwin")
20844
21607
  throw new UserError("reveal is only available on macOS");
20845
- const child = spawn4("open", ["-R", target], { stdio: "ignore" });
21608
+ const child = spawn5("open", ["-R", target], { stdio: "ignore" });
20846
21609
  child.on("error", () => {});
20847
21610
  child.unref();
20848
21611
  }
@@ -20890,7 +21653,7 @@ class OpenService {
20890
21653
  }
20891
21654
  async locate(raw) {
20892
21655
  const path = canonical(resolve7(expandTilde(raw)));
20893
- const st = await stat2(path).catch((e) => {
21656
+ const st = await stat3(path).catch((e) => {
20894
21657
  if (e.code === "ENOENT")
20895
21658
  return null;
20896
21659
  throw e;
@@ -21169,9 +21932,9 @@ class AfterLand {
21169
21932
  }
21170
21933
 
21171
21934
  // ../daemon/src/repos/registry.ts
21172
- import { existsSync as existsSync20, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
21935
+ import { existsSync as existsSync20, mkdirSync as mkdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
21173
21936
  import { homedir as homedir9 } from "os";
21174
- import { basename as basename5, dirname as dirname7, join as join17 } from "path";
21937
+ import { basename as basename5, dirname as dirname7, join as join18 } from "path";
21175
21938
 
21176
21939
  // ../daemon/src/git/base.ts
21177
21940
  async function resolveBase(repoPath, defaultBranch, route) {
@@ -21182,8 +21945,8 @@ async function resolveBase(repoPath, defaultBranch, route) {
21182
21945
  }
21183
21946
 
21184
21947
  // ../daemon/src/repos/config.ts
21185
- import { closeSync, existsSync as existsSync17, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync12, readSync, statSync as statSync6 } from "fs";
21186
- import { join as join14 } from "path";
21948
+ import { closeSync, existsSync as existsSync17, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync12, readSync, statSync as statSync6 } from "fs";
21949
+ import { join as join15 } from "path";
21187
21950
 
21188
21951
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
21189
21952
  function createScanner(text, ignoreTrivia = false) {
@@ -22547,7 +23310,7 @@ function applyEdits(text, edits) {
22547
23310
 
22548
23311
  // ../daemon/src/repos/config.ts
22549
23312
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
22550
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync17(join14(repoPath, rel)));
23313
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync17(join15(repoPath, rel)));
22551
23314
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
22552
23315
  function parseSettings(text) {
22553
23316
  const errors = [];
@@ -22572,7 +23335,7 @@ function readConfigFile(repoPath) {
22572
23335
  for (const rel of files) {
22573
23336
  let raw;
22574
23337
  try {
22575
- raw = parseSettings(readFileSync12(join14(repoPath, rel), "utf8"));
23338
+ raw = parseSettings(readFileSync12(join15(repoPath, rel), "utf8"));
22576
23339
  } catch (e) {
22577
23340
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
22578
23341
  }
@@ -22631,7 +23394,7 @@ function configTarget(repoPath, made) {
22631
23394
  }
22632
23395
  function configBody(repoPath, rel, config) {
22633
23396
  const place = PLACES.find((p) => p.local === rel);
22634
- const abs = place && join14(repoPath, place.shared);
23397
+ const abs = place && join15(repoPath, place.shared);
22635
23398
  if (!abs || !existsSync17(abs))
22636
23399
  return config;
22637
23400
  let shared;
@@ -22649,7 +23412,7 @@ function configText(repoPath, rel, config) {
22649
23412
  let text;
22650
23413
  let was;
22651
23414
  try {
22652
- text = readFileSync12(join14(repoPath, rel), "utf8");
23415
+ text = readFileSync12(join15(repoPath, rel), "utf8");
22653
23416
  was = parseSettings(text);
22654
23417
  } catch {
22655
23418
  return fresh;
@@ -22679,7 +23442,7 @@ function detectConfig(repoPath) {
22679
23442
  return { config: explicit.config, needsSetup: false };
22680
23443
  if (explicit)
22681
23444
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
22682
- const pkgPath = join14(repoPath, "package.json");
23445
+ const pkgPath = join15(repoPath, "package.json");
22683
23446
  if (existsSync17(pkgPath)) {
22684
23447
  const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
22685
23448
  const scripts = pkg.scripts ?? {};
@@ -22702,7 +23465,7 @@ function detectConfig(repoPath) {
22702
23465
  };
22703
23466
  }
22704
23467
  }
22705
- if (existsSync17(join14(repoPath, "start.sh"))) {
23468
+ if (existsSync17(join15(repoPath, "start.sh"))) {
22706
23469
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
22707
23470
  }
22708
23471
  const assumed = assumeNothingToRun(repoPath);
@@ -22745,24 +23508,24 @@ function assumeNothingToRun(repoPath) {
22745
23508
  return marker && !servesPage(repoPath) ? marker : undefined;
22746
23509
  }
22747
23510
  function markerIn(repoPath) {
22748
- const named = NO_PAGE_MARKERS.find((f) => existsSync17(join14(repoPath, f)));
23511
+ const named = NO_PAGE_MARKERS.find((f) => existsSync17(join15(repoPath, f)));
22749
23512
  if (named)
22750
23513
  return named;
22751
23514
  let entries;
22752
23515
  try {
22753
- entries = readdirSync4(repoPath);
23516
+ entries = readdirSync5(repoPath);
22754
23517
  } catch {
22755
23518
  return;
22756
23519
  }
22757
23520
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
22758
23521
  }
22759
23522
  function servesPage(repoPath) {
22760
- if (WEB_FILES.some((f) => existsSync17(join14(repoPath, f))))
23523
+ if (WEB_FILES.some((f) => existsSync17(join15(repoPath, f))))
22761
23524
  return true;
22762
- if (/^web:/m.test(head(join14(repoPath, "Procfile"))))
23525
+ if (/^web:/m.test(head(join15(repoPath, "Procfile"))))
22763
23526
  return true;
22764
23527
  for (const [files, names] of WEB_DEPENDENCIES) {
22765
- const text = files.map((f) => head(join14(repoPath, f))).join(`
23528
+ const text = files.map((f) => head(join15(repoPath, f))).join(`
22766
23529
  `);
22767
23530
  if (text.trim() && names.some((n) => mentions(text, n)))
22768
23531
  return true;
@@ -22772,14 +23535,14 @@ function servesPage(repoPath) {
22772
23535
  function frontEndBeside(repoPath) {
22773
23536
  let dirs;
22774
23537
  try {
22775
- dirs = readdirSync4(repoPath, { withFileTypes: true });
23538
+ dirs = readdirSync5(repoPath, { withFileTypes: true });
22776
23539
  } catch {
22777
23540
  return false;
22778
23541
  }
22779
23542
  return dirs.some((d) => {
22780
23543
  if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
22781
23544
  return false;
22782
- const pkg = head(join14(repoPath, d.name, "package.json"));
23545
+ const pkg = head(join15(repoPath, d.name, "package.json"));
22783
23546
  if (!pkg)
22784
23547
  return false;
22785
23548
  try {
@@ -22835,11 +23598,11 @@ function isLocalDatabase(url) {
22835
23598
  }
22836
23599
  function detectServices(repoPath) {
22837
23600
  const out = {};
22838
- const compose = COMPOSE_FILES.find((f) => existsSync17(join14(repoPath, f)));
23601
+ const compose = COMPOSE_FILES.find((f) => existsSync17(join15(repoPath, f)));
22839
23602
  if (compose)
22840
23603
  out.compose = compose;
22841
23604
  for (const file of ENV_FILES) {
22842
- const text = head(join14(repoPath, file));
23605
+ const text = head(join15(repoPath, file));
22843
23606
  if (!text)
22844
23607
  continue;
22845
23608
  for (const name of DB_URL_NAMES) {
@@ -22863,7 +23626,7 @@ function inferPaths(cmd, repoPath, depth = 1) {
22863
23626
  return;
22864
23627
  let dir = false;
22865
23628
  try {
22866
- dir = statSync6(join14(repoPath, clean)).isDirectory();
23629
+ dir = statSync6(join15(repoPath, clean)).isDirectory();
22867
23630
  } catch {
22868
23631
  return;
22869
23632
  }
@@ -22874,25 +23637,25 @@ function inferPaths(cmd, repoPath, depth = 1) {
22874
23637
  const t = tokens[i];
22875
23638
  if (t === "cd") {
22876
23639
  const dir = tokens[i + 1];
22877
- if (dir && !dir.startsWith("-") && existsSync17(join14(repoPath, dir)))
23640
+ if (dir && !dir.startsWith("-") && existsSync17(join15(repoPath, dir)))
22878
23641
  add(dir);
22879
23642
  continue;
22880
23643
  }
22881
23644
  if (t.startsWith("-") || t.startsWith("$"))
22882
23645
  continue;
22883
23646
  const mod = /^([A-Za-z_]\w*)(?:\.\w+)*:\w+$/.exec(t);
22884
- if (mod?.[1] && existsSync17(join14(repoPath, mod[1]))) {
23647
+ if (mod?.[1] && existsSync17(join15(repoPath, mod[1]))) {
22885
23648
  add(mod[1]);
22886
23649
  continue;
22887
23650
  }
22888
23651
  const rel = t.replace(/^\.\//, "");
22889
23652
  if (rel.startsWith("/") || rel.startsWith("..") || rel.includes("="))
22890
23653
  continue;
22891
- if (!existsSync17(join14(repoPath, rel)))
23654
+ if (!existsSync17(join15(repoPath, rel)))
22892
23655
  continue;
22893
23656
  add(rel);
22894
23657
  if (depth > 0 && rel.endsWith(".sh")) {
22895
- for (const p of inferPaths(head(join14(repoPath, rel)), repoPath, depth - 1))
23658
+ for (const p of inferPaths(head(join15(repoPath, rel)), repoPath, depth - 1))
22896
23659
  out.add(p);
22897
23660
  }
22898
23661
  }
@@ -22915,9 +23678,9 @@ function procCommand(runner, script, body) {
22915
23678
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
22916
23679
  }
22917
23680
  function detectPages(repoPath, scripts, runner) {
22918
- if (!scripts.build || !existsSync17(join14(repoPath, "functions")))
23681
+ if (!scripts.build || !existsSync17(join15(repoPath, "functions")))
22919
23682
  return null;
22920
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join14(repoPath, f)).find((f) => existsSync17(f));
23683
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join15(repoPath, f)).find((f) => existsSync17(f));
22921
23684
  if (!cfg)
22922
23685
  return null;
22923
23686
  const out = readFileSync12(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
@@ -22926,20 +23689,20 @@ function detectPages(repoPath, scripts, runner) {
22926
23689
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
22927
23690
  }
22928
23691
  function detectRunner(repoPath, packageManager) {
22929
- if (existsSync17(join14(repoPath, "bun.lock")) || existsSync17(join14(repoPath, "bun.lockb")))
23692
+ if (existsSync17(join15(repoPath, "bun.lock")) || existsSync17(join15(repoPath, "bun.lockb")))
22930
23693
  return "bun";
22931
- if (existsSync17(join14(repoPath, "pnpm-lock.yaml")))
23694
+ if (existsSync17(join15(repoPath, "pnpm-lock.yaml")))
22932
23695
  return "pnpm";
22933
- if (existsSync17(join14(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
23696
+ if (existsSync17(join15(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
22934
23697
  return "yarn";
22935
23698
  return "npm";
22936
23699
  }
22937
23700
 
22938
23701
  // ../daemon/src/repos/create.ts
22939
- import { existsSync as existsSync18, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
23702
+ import { existsSync as existsSync18, readdirSync as readdirSync6, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
22940
23703
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
22941
23704
  import { homedir as homedir8 } from "os";
22942
- import { dirname as dirname6, join as join15, resolve as resolve8 } from "path";
23705
+ import { dirname as dirname6, join as join16, resolve as resolve8 } from "path";
22943
23706
  function isInside(dir, parent) {
22944
23707
  const p = parent.replace(/\/+$/, "");
22945
23708
  return dir === p || dir.startsWith(`${p}/`);
@@ -22950,29 +23713,29 @@ function planProject(opts, home = homedir8()) {
22950
23713
  throw new UserError(nameError);
22951
23714
  const first = firstProjectsFolder(opts.parent, home);
22952
23715
  const parent = first ?? resolveParent(opts.parent);
22953
- const dir = join15(parent, opts.name.trim());
23716
+ const dir = join16(parent, opts.name.trim());
22954
23717
  if (existsSync18(dir))
22955
23718
  throw new UserError(`${dir} already exists`);
22956
23719
  return { parent, dir, makeParent: first !== null };
22957
23720
  }
22958
23721
  function firstProjectsFolder(raw, home) {
22959
23722
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
22960
- if (typed !== join15(home, PROJECTS_FOLDER) || existsSync18(typed) || !existsSync18(home))
23723
+ if (typed !== join16(home, PROJECTS_FOLDER) || existsSync18(typed) || !existsSync18(home))
22961
23724
  return null;
22962
- return join15(realpathSync2(home), PROJECTS_FOLDER);
23725
+ return join16(realpathSync2(home), PROJECTS_FOLDER);
22963
23726
  }
22964
23727
  function planInPlace(opts) {
22965
23728
  const name = opts.name.trim();
22966
23729
  if (!name || name === "." || name === ".." || name.includes("/")) {
22967
23730
  throw new UserError(`${opts.name} is not a folder name`);
22968
23731
  }
22969
- const typed = join15(resolveParent(opts.parent), name);
23732
+ const typed = join16(resolveParent(opts.parent), name);
22970
23733
  if (!existsSync18(typed) || !statSync7(typed).isDirectory())
22971
23734
  throw new UserError(`${typed} is not a folder`);
22972
23735
  const dir = realpathSync2(typed);
22973
- if (existsSync18(join15(dir, ".git")))
23736
+ if (existsSync18(join16(dir, ".git")))
22974
23737
  throw new UserError(`${dir} is already a project; open it instead`);
22975
- if (!holdsNothing(readdirSync5(dir))) {
23738
+ if (!holdsNothing(readdirSync6(dir))) {
22976
23739
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
22977
23740
  }
22978
23741
  return { parent: dirname6(dir), dir, makeParent: false };
@@ -23061,13 +23824,13 @@ async function undoCreate(dir) {
23061
23824
  }
23062
23825
  async function undoInit(dir) {
23063
23826
  try {
23064
- await rm(join15(dir, ".git"), { recursive: true, force: true });
23827
+ await rm(join16(dir, ".git"), { recursive: true, force: true });
23065
23828
  } catch (e) {
23066
23829
  log.warn(dir, "could not take back a half-made .git", e);
23067
23830
  }
23068
23831
  }
23069
23832
  async function unmakeProject(dir, made) {
23070
- const target = made === "folder" ? dir : join15(dir, ".git");
23833
+ const target = made === "folder" ? dir : join16(dir, ".git");
23071
23834
  try {
23072
23835
  await rm(target, { recursive: true, force: true });
23073
23836
  } catch (e) {
@@ -23098,7 +23861,7 @@ async function ok(p, what) {
23098
23861
 
23099
23862
  // ../daemon/src/repos/watcher.ts
23100
23863
  import { existsSync as existsSync19, watch } from "fs";
23101
- import { basename as basename4, join as join16 } from "path";
23864
+ import { basename as basename4, join as join17 } from "path";
23102
23865
  function watchDefaultBranch(repoPath, branch, onMove) {
23103
23866
  const last = { local: null, upstream: null };
23104
23867
  const read = async () => {
@@ -23151,13 +23914,13 @@ function watchDefaultBranch(repoPath, branch, onMove) {
23151
23914
  ]);
23152
23915
  if (stopped)
23153
23916
  return;
23154
- const gitDir = common.ok && common.out ? common.out : join16(repoPath, ".git");
23917
+ const gitDir = common.ok && common.out ? common.out : join17(repoPath, ".git");
23155
23918
  tryWatch(gitDir);
23156
- tryWatch(join16(gitDir, "refs", "heads"));
23919
+ tryWatch(join17(gitDir, "refs", "heads"));
23157
23920
  if (!remote.ok || !remote.out)
23158
23921
  return;
23159
- const remotes = join16(gitDir, "refs", "remotes");
23160
- const mine = join16(remotes, remote.out);
23922
+ const remotes = join17(gitDir, "refs", "remotes");
23923
+ const mine = join17(remotes, remote.out);
23161
23924
  let inner = null;
23162
23925
  let mid = null;
23163
23926
  const armInner = () => {
@@ -23173,7 +23936,7 @@ function watchDefaultBranch(repoPath, branch, onMove) {
23173
23936
  }) : null;
23174
23937
  armInner();
23175
23938
  };
23176
- tryWatch(join16(gitDir, "refs"), (_event, filename) => {
23939
+ tryWatch(join17(gitDir, "refs"), (_event, filename) => {
23177
23940
  if (!filename || filename === "remotes")
23178
23941
  armMid();
23179
23942
  schedule();
@@ -23202,7 +23965,7 @@ function watchWorktreeDir(repoPath, onChange) {
23202
23965
  }, 300);
23203
23966
  };
23204
23967
  const arm = (commonDir) => {
23205
- const dir = join16(commonDir, "worktrees");
23968
+ const dir = join17(commonDir, "worktrees");
23206
23969
  const armInner = () => {
23207
23970
  inner?.close();
23208
23971
  inner = null;
@@ -23245,7 +24008,7 @@ function watchWorktreeDir(repoPath, onChange) {
23245
24008
  function watchConfigFile(repoPath, onChange) {
23246
24009
  const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
23247
24010
  const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename4(rel)));
23248
- const dir = join16(repoPath, CONFIG_DIR);
24011
+ const dir = join17(repoPath, CONFIG_DIR);
23249
24012
  let timer = null;
23250
24013
  let outer = null;
23251
24014
  let inner = null;
@@ -23382,7 +24145,7 @@ class RepoRegistry {
23382
24145
  return `${repo.name} is set up now`;
23383
24146
  if (main.promptedAt || main.lastTurn)
23384
24147
  return `${repo.name} has a chat now`;
23385
- if (existsSync20(join17(repo.path, ".claude")))
24148
+ if (existsSync20(join18(repo.path, ".claude")))
23386
24149
  return `an agent has already run in ${repo.name}`;
23387
24150
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
23388
24151
  return `${repo.name} has files in it now`;
@@ -23533,10 +24296,10 @@ class RepoRegistry {
23533
24296
  throw new UserError(current.reason);
23534
24297
  const was = configTarget(repo.path, !!repo.made);
23535
24298
  const rel = configSibling(was, kind);
23536
- const moved = rel !== was && existsSync20(join17(repo.path, was));
24299
+ const moved = rel !== was && existsSync20(join18(repo.path, was));
23537
24300
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
23538
24301
  try {
23539
- rmSync2(join17(repo.path, was));
24302
+ rmSync3(join18(repo.path, was));
23540
24303
  } catch (e) {
23541
24304
  log.warn(repoId, `could not remove ${was}`, e);
23542
24305
  }
@@ -23545,9 +24308,9 @@ class RepoRegistry {
23545
24308
  }
23546
24309
  if (isLocalConfigFile(rel))
23547
24310
  await excludeFromGit(repo.path, rel);
23548
- const file = join17(repo.path, rel);
24311
+ const file = join18(repo.path, rel);
23549
24312
  try {
23550
- mkdirSync6(dirname7(file), { recursive: true });
24313
+ mkdirSync7(dirname7(file), { recursive: true });
23551
24314
  writeFileSync7(file, configText(repo.path, rel, config));
23552
24315
  } catch (e) {
23553
24316
  log.warn(repoId, `could not write ${rel}`, e);
@@ -23649,7 +24412,7 @@ class RepoRegistry {
23649
24412
  }
23650
24413
  placeConfig(repo) {
23651
24414
  const rel = configTarget(repo.path, !!repo.made);
23652
- if (isLocalConfigFile(rel) && existsSync20(join17(repo.path, rel))) {
24415
+ if (isLocalConfigFile(rel) && existsSync20(join18(repo.path, rel))) {
23653
24416
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
23654
24417
  }
23655
24418
  if (repo.configFile === rel)
@@ -23741,7 +24504,7 @@ class RepoRegistry {
23741
24504
  }
23742
24505
 
23743
24506
  // ../daemon/src/routes/service.ts
23744
- import { join as join18 } from "path";
24507
+ import { join as join19 } from "path";
23745
24508
 
23746
24509
  // ../daemon/src/routes/fileRouters.ts
23747
24510
  var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
@@ -24941,7 +25704,7 @@ class RouteService {
24941
25704
  return mergeRoutes(fileRoutes(paths, roots), code);
24942
25705
  }
24943
25706
  async cachedRead(root, path, had, kept, decode) {
24944
- const abs = join18(root, path);
25707
+ const abs = join19(root, path);
24945
25708
  const stat = await this.fs.stat(abs);
24946
25709
  if (!stat)
24947
25710
  return null;
@@ -24966,7 +25729,7 @@ class RouteService {
24966
25729
  }
24967
25730
  }
24968
25731
  async hash(root, file) {
24969
- const abs = join18(root, file);
25732
+ const abs = join19(root, file);
24970
25733
  const stat = await this.fs.stat(abs);
24971
25734
  if (!stat)
24972
25735
  return null;
@@ -25288,7 +26051,7 @@ function servedLocally(origin) {
25288
26051
  import { homedir as homedir11 } from "os";
25289
26052
 
25290
26053
  // ../daemon/src/core/pair.ts
25291
- import { randomBytes as randomBytes3 } from "crypto";
26054
+ import { randomBytes as randomBytes4 } from "crypto";
25292
26055
  class PairCodes {
25293
26056
  now;
25294
26057
  live = null;
@@ -25296,7 +26059,7 @@ class PairCodes {
25296
26059
  this.now = now;
25297
26060
  }
25298
26061
  mint() {
25299
- const code = randomBytes3(9).toString("base64url");
26062
+ const code = randomBytes4(9).toString("base64url");
25300
26063
  this.live = { code, until: this.now() + PAIR_TTL_MS };
25301
26064
  return { code, ms: PAIR_TTL_MS };
25302
26065
  }
@@ -25307,10 +26070,56 @@ class PairCodes {
25307
26070
  }
25308
26071
  }
25309
26072
 
26073
+ // ../daemon/src/core/tailnet.ts
26074
+ var PHONE_OS = new Set(["android", "ios"]);
26075
+ function tailnetPhones(json) {
26076
+ let st;
26077
+ try {
26078
+ st = JSON.parse(json);
26079
+ } catch {
26080
+ return null;
26081
+ }
26082
+ if (st === null || typeof st !== "object")
26083
+ return null;
26084
+ const phones = [];
26085
+ for (const p of Object.values(st.Peer ?? {})) {
26086
+ if (!p || !PHONE_OS.has((p.OS ?? "").toLowerCase()))
26087
+ continue;
26088
+ const name = p.HostName || (p.DNSName ?? "").split(".")[0] || "a phone";
26089
+ phones.push({ name, ip: p.TailscaleIPs?.[0] ?? null });
26090
+ }
26091
+ return phones;
26092
+ }
26093
+ var PING_TIMEOUT = "2s";
26094
+ function cli(bin) {
26095
+ return async (args) => {
26096
+ try {
26097
+ const p = Bun.spawn([bin, ...args], { stdin: "ignore", stdout: "pipe", stderr: "ignore", timeout: 5000 });
26098
+ const out = await new Response(p.stdout).text();
26099
+ return { ok: await p.exited === 0, out };
26100
+ } catch {
26101
+ return { ok: false, out: "" };
26102
+ }
26103
+ };
26104
+ }
26105
+ var ping = (ip) => ["ping", "--until-direct=false", "--c", "1", "--timeout", PING_TIMEOUT, ip];
26106
+ async function readTailnetPhones(run = ((bin) => bin ? cli(bin) : null)(Bun.which("tailscale"))) {
26107
+ if (run === null)
26108
+ return null;
26109
+ const st = await run(["status", "--json"]);
26110
+ const listed = st.ok ? tailnetPhones(st.out) : null;
26111
+ if (listed === null)
26112
+ return null;
26113
+ return Promise.all(listed.map(async ({ name, ip }) => ({ name, online: ip !== null && (await run(ping(ip))).ok })));
26114
+ }
26115
+
26116
+ // ../daemon/src/server/handlers.ts
26117
+ import { randomBytes as randomBytes5 } from "crypto";
26118
+
25310
26119
  // ../daemon/src/agent/config.ts
25311
26120
  import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
25312
26121
  import { homedir as homedir10 } from "os";
25313
- import { join as join19 } from "path";
26122
+ import { join as join20 } from "path";
25314
26123
  function agentConfigFiles(spec, repoPath, home = homedir10()) {
25315
26124
  const file = (id, label, path) => ({
25316
26125
  id,
@@ -25320,30 +26129,30 @@ function agentConfigFiles(spec, repoPath, home = homedir10()) {
25320
26129
  });
25321
26130
  if (spec.id === "claude") {
25322
26131
  return [
25323
- file("user-config", "user config (MCP servers, projects)", join19(home, ".claude.json")),
25324
- file("user-settings", "user settings (permissions, hooks)", join19(home, ".claude", "settings.json")),
25325
- ...repoPath ? [file("project-mcp", "project MCP servers", join19(repoPath, ".mcp.json"))] : [],
25326
- ...repoPath ? [file("project-settings", "project settings", join19(repoPath, ".claude", "settings.json"))] : []
26132
+ file("user-config", "user config (MCP servers, projects)", join20(home, ".claude.json")),
26133
+ file("user-settings", "user settings (permissions, hooks)", join20(home, ".claude", "settings.json")),
26134
+ ...repoPath ? [file("project-mcp", "project MCP servers", join20(repoPath, ".mcp.json"))] : [],
26135
+ ...repoPath ? [file("project-settings", "project settings", join20(repoPath, ".claude", "settings.json"))] : []
25327
26136
  ];
25328
26137
  }
25329
26138
  if (spec.id === "codex")
25330
- return [file("user-config", "config", join19(home, ".codex", "config.toml"))];
26139
+ return [file("user-config", "config", join20(home, ".codex", "config.toml"))];
25331
26140
  return [];
25332
26141
  }
25333
26142
  function describeAgentConfig(spec, repoPath, home = homedir10()) {
25334
26143
  const files = agentConfigFiles(spec, repoPath, home);
25335
26144
  const servers = [];
25336
26145
  if (spec.id === "claude") {
25337
- const user = readJson(join19(home, ".claude.json"));
26146
+ const user = readJson(join20(home, ".claude.json"));
25338
26147
  servers.push(...serversOf(user?.mcpServers, "user"));
25339
26148
  if (repoPath) {
25340
26149
  const projects = user?.projects;
25341
26150
  const mine = projects?.[repoPath];
25342
26151
  servers.push(...serversOf(mine?.mcpServers, "local"));
25343
- servers.push(...serversOf(readJson(join19(repoPath, ".mcp.json"))?.mcpServers, "project"));
26152
+ servers.push(...serversOf(readJson(join20(repoPath, ".mcp.json"))?.mcpServers, "project"));
25344
26153
  }
25345
26154
  } else if (spec.id === "codex") {
25346
- servers.push(...codexServers(join19(home, ".codex", "config.toml")));
26155
+ servers.push(...codexServers(join20(home, ".codex", "config.toml")));
25347
26156
  }
25348
26157
  return { agent: spec.id, files, servers };
25349
26158
  }
@@ -25981,6 +26790,9 @@ var handlers = {
25981
26790
  s.drafts.set(msg.boxId, msg.text, msg.clientId);
25982
26791
  s.worktrees.spare.typed(msg.boxId, msg.text);
25983
26792
  },
26793
+ "set-attachments"(msg, _ctx, s) {
26794
+ s.drafts.setAttachments(msg.boxId, msg.items, msg.clientId);
26795
+ },
25984
26796
  "refresh-git"(msg, _ctx, s) {
25985
26797
  s.worktrees.recount(msg.repoId);
25986
26798
  },
@@ -25993,19 +26805,17 @@ var handlers = {
25993
26805
  "forget-visit"(msg, _ctx, s) {
25994
26806
  s.routes.forget(msg.repoId, msg.path);
25995
26807
  },
25996
- async chat(msg, _ctx, s) {
25997
- await s.worktrees.send(msg.worktreeId, {
26808
+ async chat(msg, ctx, s) {
26809
+ await sending(s, ctx, msg, msg.attachments, () => s.worktrees.send(msg.worktreeId, {
25998
26810
  text: msg.text,
25999
26811
  clientId: msg.clientId,
26000
26812
  context: msg.context,
26001
26813
  attachments: msg.attachments
26002
- });
26814
+ }));
26003
26815
  },
26004
- async "create-worktree"(msg, _ctx, s) {
26005
- await s.worktrees.create(msg.repoId, msg.prompt, {
26816
+ async "create-worktree"(msg, ctx, s) {
26817
+ const opts = {
26006
26818
  createdBy: msg.clientId,
26007
- worktreeId: msg.worktreeId,
26008
- variant: msg.variant,
26009
26819
  context: msg.context,
26010
26820
  attachments: msg.attachments,
26011
26821
  agent: msg.agent,
@@ -26014,6 +26824,30 @@ var handlers = {
26014
26824
  model: msg.model,
26015
26825
  effort: msg.effort,
26016
26826
  carry: msg.carry
26827
+ };
26828
+ const of = msg.variants ?? 1;
26829
+ await sending(s, ctx, msg, msg.attachments, async () => {
26830
+ if (of === 1) {
26831
+ await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
26832
+ return;
26833
+ }
26834
+ const group = randomBytes5(4).toString("hex");
26835
+ const made = await Promise.allSettled(Array.from({ length: of }, (_, i) => s.worktrees.create(msg.repoId, msg.prompt, {
26836
+ ...opts,
26837
+ ...i === 0 ? { worktreeId: msg.worktreeId } : {},
26838
+ variant: { group, index: i + 1, of }
26839
+ })));
26840
+ const failed = made.flatMap((m) => m.status === "rejected" ? [m.reason] : []);
26841
+ if (failed.length === 0)
26842
+ return;
26843
+ const went = failed.find((e) => e instanceof UserError && e.delivered);
26844
+ if (went)
26845
+ throw went;
26846
+ if (failed.length < of) {
26847
+ const why = failed[0] instanceof Error ? failed[0].message : String(failed[0]);
26848
+ throw new UserError(`${failed.length} of ${of} attempts did not start: ${why}`, { delivered: true });
26849
+ }
26850
+ throw failed[0];
26017
26851
  });
26018
26852
  },
26019
26853
  "set-worktree-profile"(msg, _ctx, s) {
@@ -26036,6 +26870,8 @@ var handlers = {
26036
26870
  },
26037
26871
  "batch-worktrees"(msg, ctx, s) {
26038
26872
  const repo = s.state.requireRepo(msg.repoId);
26873
+ if (msg.boxId)
26874
+ s.drafts.take(msg.boxId);
26039
26875
  fireAndForget(msg.repoId, (async () => {
26040
26876
  const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
26041
26877
  const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
@@ -26059,23 +26895,34 @@ var handlers = {
26059
26895
  "list-archived"(msg, ctx, s) {
26060
26896
  ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
26061
26897
  },
26062
- async "restore-worktree"(msg, _ctx, s) {
26063
- await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
26898
+ async "restore-worktree"(msg, ctx, s) {
26899
+ await sending(s, ctx, msg, msg.message?.attachments, async () => {
26900
+ await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
26901
+ });
26064
26902
  },
26065
26903
  async "delete-archived"(msg, _ctx, s) {
26066
26904
  await s.worktrees.deleteArchived(msg.archiveId);
26067
26905
  },
26068
- async "adopt-worktree"(msg, _ctx, s) {
26069
- await s.worktrees.adopt(msg.worktreeId, msg.clientId);
26906
+ async "adopt-worktree"(msg, ctx, s) {
26907
+ await sending(s, ctx, msg, msg.message?.attachments, async () => {
26908
+ await s.worktrees.adopt(msg.worktreeId, msg.clientId, msg.message);
26909
+ });
26070
26910
  },
26071
26911
  async "git-status"(msg, ctx, s) {
26072
26912
  await gitStatus(s, ctx, msg.worktreeId);
26073
26913
  },
26074
26914
  async "read-file"(msg, ctx, s) {
26075
- const head = { t: "file-read", worktreeId: msg.worktreeId, path: msg.path, ref: msg.ref, seq: msg.seq };
26915
+ const head = {
26916
+ t: "file-read",
26917
+ worktreeId: msg.worktreeId,
26918
+ path: msg.path,
26919
+ ref: msg.ref,
26920
+ since: msg.since,
26921
+ seq: msg.seq
26922
+ };
26076
26923
  try {
26077
26924
  const kept = s.worktrees.readable(msg.worktreeId) ? null : await s.worktrees.archivedFile(msg.worktreeId, msg.path, msg.ref);
26078
- const read = kept ? keptRead(kept.before, kept.after) : await s.files.read(msg.worktreeId, msg.path, msg.ref);
26925
+ const read = kept ? keptRead(kept.before, kept.after) : await s.files.read(msg.worktreeId, msg.path, msg.ref, msg.since);
26079
26926
  ctx.reply({ ...head, ...read });
26080
26927
  } catch (e) {
26081
26928
  if (!(e instanceof UserError))
@@ -26105,21 +26952,17 @@ var handlers = {
26105
26952
  },
26106
26953
  async land(msg, ctx, s) {
26107
26954
  const { result } = await s.worktrees.land(msg.worktreeId, msg.message);
26108
- const suggestion = !result.ok && result.conflict ? "Bring main into this branch and resolve the conflicts, then verify the app still works." : undefined;
26109
26955
  if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
26110
26956
  fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
26111
26957
  }
26112
26958
  await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
26113
26959
  url: result.url,
26114
- ...suggestion ? { suggestion } : {}
26960
+ ...result.asked ? { asked: true } : {}
26115
26961
  }));
26116
26962
  },
26117
26963
  async "sync-main"(msg, ctx, s) {
26118
- const { result, base } = await s.worktrees.sync(msg.worktreeId);
26119
- const prompt = !result.ok && result.conflict && s.state.worktree(msg.worktreeId);
26120
- await notify(s, ctx, msg.worktreeId, prompt ? shipped(msg.worktreeId, false, `sync conflicts with ${base}: prompt prefilled in chat`, {
26121
- suggestion: `Merge ${base} into this branch and resolve the conflicts, then verify the app still works.`
26122
- }) : shipped(msg.worktreeId, result.ok, result.message));
26964
+ const { result } = await s.worktrees.sync(msg.worktreeId);
26965
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, result.asked ? { asked: true } : {}));
26123
26966
  },
26124
26967
  async "pull-main"(msg, ctx, s) {
26125
26968
  const result = await s.worktrees.pull(msg.worktreeId);
@@ -26148,7 +26991,7 @@ var handlers = {
26148
26991
  },
26149
26992
  async commit(msg, ctx, s) {
26150
26993
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
26151
- await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
26994
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, result.asked ? { asked: true } : {}));
26152
26995
  },
26153
26996
  async graft(msg, ctx, s) {
26154
26997
  const { target } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
@@ -26188,7 +27031,21 @@ var handlers = {
26188
27031
  },
26189
27032
  unqueue(msg, _ctx, s) {
26190
27033
  requireRun(s, msg.worktreeId);
26191
- s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index);
27034
+ s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index, msg.edit ? (m) => s.drafts.putBack(msg.worktreeId, { text: m.text, items: m.attachments ?? [] }) : undefined);
27035
+ },
27036
+ async redraft(msg, _ctx, s) {
27037
+ const r = s.worktrees.readable(msg.worktreeId);
27038
+ const events = r?.wt ? s.runtime.ensureAgent(r.wt).agent.transcript() : s.worktrees.archivedTranscript(msg.worktreeId) ?? [];
27039
+ const event = events.find((e) => e.seq === msg.seq)?.event;
27040
+ if (event?.type !== "user-message")
27041
+ throw new UserError("that message is no longer in this chat");
27042
+ const items = [];
27043
+ for (const ref of event.attachments ?? []) {
27044
+ const item = await s.attachments.reattach(msg.worktreeId, ref, (file) => s.worktrees.archivedAttachment(msg.worktreeId, file));
27045
+ if (item)
27046
+ items.push(item);
27047
+ }
27048
+ s.drafts.putBack(msg.worktreeId, { text: event.text, items });
26192
27049
  },
26193
27050
  async "changed-ranges"(msg, ctx, s) {
26194
27051
  const { ranges, lineOffset } = await s.files.changedRanges(msg.worktreeId, msg.path);
@@ -26209,6 +27066,12 @@ var handlers = {
26209
27066
  async judge(msg, _ctx, s) {
26210
27067
  await s.landing.judge(msg.worktreeId, msg.note);
26211
27068
  },
27069
+ async "fix-preview"(msg, _ctx, s) {
27070
+ await s.worktrees.fixPreview(msg.worktreeId, msg.clientId);
27071
+ },
27072
+ fix(msg, _ctx, s) {
27073
+ s.fix.press(msg.worktreeId, msg.toolId);
27074
+ },
26212
27075
  async "write-file"(msg, ctx, s) {
26213
27076
  const head = { t: "file-written", worktreeId: msg.worktreeId, path: msg.path, seq: msg.seq };
26214
27077
  try {
@@ -26339,6 +27202,10 @@ var handlers = {
26339
27202
  s.state.setDefaultAgent(msg.agent);
26340
27203
  s.hub.emit("agentsChanged");
26341
27204
  },
27205
+ "set-keep-awake"(msg, _ctx, s) {
27206
+ s.state.setKeepAwake(msg.mode);
27207
+ s.hub.emit("keepAwakeChanged");
27208
+ },
26342
27209
  "rescan-themes"(_msg, _ctx, s) {
26343
27210
  s.themes.load();
26344
27211
  s.hub.emit("themesChanged");
@@ -26388,6 +27255,10 @@ var handlers = {
26388
27255
  if (!s.runs.stop(msg.worktreeId, msg.kind))
26389
27256
  throw new UserError(`no ${msg.kind} is running here`);
26390
27257
  },
27258
+ "preview-start"(msg, _ctx, s) {
27259
+ requireRun(s, msg.worktreeId);
27260
+ s.worktrees.startPreview(msg.worktreeId);
27261
+ },
26391
27262
  async "search-refs"(msg, ctx, s) {
26392
27263
  const refs = await s.refs.search(msg.repoId, msg.query);
26393
27264
  ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
@@ -26400,6 +27271,30 @@ var handlers = {
26400
27271
  await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
26401
27272
  }
26402
27273
  };
27274
+ async function sending(s, ctx, from, attachments, deliver) {
27275
+ const uploads = s.attachments.uploads;
27276
+ const { boxId, clientId } = from;
27277
+ const named = uploadIds(attachments);
27278
+ const boxed = boxId ? uploadIds(s.drafts.attachments(boxId)) : [];
27279
+ const ids = [...new Set([...named, ...boxed])].filter((id) => uploads.has(id));
27280
+ uploads.hold(ids);
27281
+ const was = boxId ? s.drafts.take(boxId, clientId) : null;
27282
+ try {
27283
+ if (named.some((id) => !uploads.has(id)))
27284
+ throw new UserError(GONE);
27285
+ await deliver();
27286
+ } catch (e) {
27287
+ if (!boxId || !was || e instanceof UserError && e.delivered)
27288
+ throw e;
27289
+ if (!(e instanceof UserError))
27290
+ log.error("ws", "a message reached no agent", e);
27291
+ const items = was.items.filter((a) => !("upload" in a) || uploads.has(a.upload));
27292
+ s.drafts.putBack(boxId, { text: was.text, items }, clientId);
27293
+ ctx.reply({ t: "unsent", boxId, text: was.text, items, message: e instanceof Error ? e.message : String(e) });
27294
+ } finally {
27295
+ uploads.release(ids);
27296
+ }
27297
+ }
26403
27298
  function looseCwd(s, id, stream) {
26404
27299
  const disc = s.worktrees.discoveredById(id);
26405
27300
  if (!disc)
@@ -26425,9 +27320,16 @@ async function dispatch(msg, ctx, s) {
26425
27320
 
26426
27321
  // ../daemon/src/server/http.ts
26427
27322
  import { existsSync as existsSync23 } from "fs";
26428
- import { join as join20 } from "path";
27323
+ import { join as join21 } from "path";
26429
27324
  var IMMUTABLE = "private, max-age=31536000, immutable";
26430
27325
  var NO_STORE = "no-store";
27326
+ function attachedHeaders(imageType) {
27327
+ return {
27328
+ "content-type": imageType ?? "text/plain; charset=utf-8",
27329
+ "x-content-type-options": "nosniff",
27330
+ ...imageType ? {} : { "content-security-policy": "sandbox" }
27331
+ };
27332
+ }
26431
27333
  function createFetch(opts) {
26432
27334
  const grant = previewGrant(opts.token);
26433
27335
  const remote = opts.remote;
@@ -26479,7 +27381,8 @@ function createFetch(opts) {
26479
27381
  dropped: new Set,
26480
27382
  sent: 0,
26481
27383
  bytes: 0,
26482
- view: null
27384
+ view: null,
27385
+ ...remoteShell ? { remote: true } : {}
26483
27386
  };
26484
27387
  const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
26485
27388
  if (srv.upgrade(req, { data, headers }))
@@ -26515,6 +27418,13 @@ function createFetch(opts) {
26515
27418
  const body = { code, url: pairLink(remote.host, code), ms };
26516
27419
  return Response.json(body, { headers: { "cache-control": NO_STORE } });
26517
27420
  }
27421
+ if (url.pathname === "/pair/phones" && req.method === "GET") {
27422
+ if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
27423
+ return new Response("unauthorized", { status: 401 });
27424
+ }
27425
+ const phones = remote && isTailnetName(remote.host) ? await opts.phones() : null;
27426
+ return Response.json(phones, { headers: { "cache-control": NO_STORE } });
27427
+ }
26518
27428
  if (url.pathname === "/pair/redeem" && req.method === "POST") {
26519
27429
  if (!remote || !remoteShell)
26520
27430
  return new Response("not found", { status: 404 });
@@ -26561,6 +27471,37 @@ function createFetch(opts) {
26561
27471
  return new Response("open failed; see daemon log", { status: 500 });
26562
27472
  }
26563
27473
  }
27474
+ if (url.pathname === "/uploads" && req.method === "POST") {
27475
+ if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
27476
+ return new Response("unauthorized", { status: 401 });
27477
+ }
27478
+ const kind = url.searchParams.get("kind");
27479
+ if (!isUploadKind(kind))
27480
+ return new Response("missing kind", { status: 400 });
27481
+ const mime = (req.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
27482
+ try {
27483
+ const body = req.body;
27484
+ return Response.json(await opts.attachments.uploads.put(kind, mime, body), {
27485
+ headers: { "cache-control": NO_STORE }
27486
+ });
27487
+ } catch (e) {
27488
+ if (e instanceof UserError)
27489
+ return new Response(e.message, { status: 400 });
27490
+ log.error("http", "upload failed", e);
27491
+ return new Response("upload failed; see daemon log", { status: 500 });
27492
+ }
27493
+ }
27494
+ if (url.pathname.startsWith("/uploads/")) {
27495
+ if (!sameSecret(url.searchParams.get("token"), opts.token))
27496
+ return new Response("unauthorized", { status: 401 });
27497
+ const id = url.pathname.slice("/uploads/".length);
27498
+ const path = opts.attachments.uploads.path(id);
27499
+ if (!path || !existsSync23(path))
27500
+ return new Response("not found", { status: 404 });
27501
+ return new Response(Bun.file(path), {
27502
+ headers: { "cache-control": IMMUTABLE, ...attachedHeaders(opts.attachments.uploads.imageType(id)) }
27503
+ });
27504
+ }
26564
27505
  if (url.pathname.startsWith("/attachments/")) {
26565
27506
  if (!sameSecret(url.searchParams.get("token"), opts.token))
26566
27507
  return new Response("unauthorized", { status: 401 });
@@ -26571,7 +27512,9 @@ function createFetch(opts) {
26571
27512
  const path = places.find((p) => !!p && existsSync23(p));
26572
27513
  if (!path)
26573
27514
  return new Response("not found", { status: 404 });
26574
- return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
27515
+ return new Response(Bun.file(path), {
27516
+ headers: { "cache-control": IMMUTABLE, ...attachedHeaders(drawnType(file ?? "")) }
27517
+ });
26575
27518
  }
26576
27519
  if (url.pathname.startsWith("/files/")) {
26577
27520
  if (!sameSecret(url.searchParams.get("token"), opts.token))
@@ -26599,14 +27542,21 @@ function createFetch(opts) {
26599
27542
  });
26600
27543
  }
26601
27544
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
26602
- const file = join20(opts.shellDist, rel.replaceAll("..", ""));
27545
+ const file = join21(opts.shellDist, rel.replaceAll("..", ""));
26603
27546
  const hashed = rel.startsWith("/assets/");
26604
27547
  if (existsSync23(file) && Bun.file(file).size > 0) {
27548
+ if (rel === "/manifest.json") {
27549
+ const { bar, ground } = opts.manifestColors();
27550
+ const manifest = { ...await Bun.file(file).json(), theme_color: bar, background_color: ground };
27551
+ return Response.json(manifest, {
27552
+ headers: { "content-type": "application/manifest+json", "cache-control": NO_STORE }
27553
+ });
27554
+ }
26605
27555
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
26606
27556
  }
26607
27557
  if (hashed)
26608
27558
  return new Response("not found", { status: 404 });
26609
- const index = join20(opts.shellDist, "index.html");
27559
+ const index = join21(opts.shellDist, "index.html");
26610
27560
  if (existsSync23(index))
26611
27561
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
26612
27562
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
@@ -26618,17 +27568,37 @@ var DROP_ABOVE_BYTES = 4 * 1024 * 1024;
26618
27568
  var DROPPED_NOTICE = `\r
26619
27569
  [toyon] output dropped: this pane was too far behind\r
26620
27570
  `;
27571
+ var SENDS = new Set(["chat", "create-worktree", "restore-worktree", "batch-worktrees"]);
27572
+ function sentFrom(json) {
27573
+ if (!json || typeof json !== "object")
27574
+ return null;
27575
+ const { t, boxId } = json;
27576
+ if (typeof t !== "string" || !SENDS.has(t))
27577
+ return null;
27578
+ return typeof boxId === "string" && boxId.length > 0 && boxId.length <= 200 ? boxId : null;
27579
+ }
27580
+ function unparsedReply(json, reason, drafts) {
27581
+ const message = `invalid message: ${reason}`;
27582
+ const boxId = sentFrom(json);
27583
+ if (boxId === null)
27584
+ return { t: "error", message };
27585
+ return { t: "unsent", boxId, text: drafts.text(boxId), items: drafts.attachments(boxId), message };
27586
+ }
26621
27587
  function startServer(opts) {
26622
27588
  const { services: s, token, version } = opts;
26623
27589
  const sockets = new Set;
26624
27590
  const linked = new Set;
27591
+ const counted = () => {
27592
+ s.idleExit.clients(linked.size);
27593
+ s.keepAwake.remoteShells([...linked].filter((ws) => ws.data.remote || ws.data.preview).length);
27594
+ };
26625
27595
  const link = (ws) => {
26626
27596
  linked.add(ws);
26627
- s.idleExit.clients(linked.size);
27597
+ counted();
26628
27598
  };
26629
27599
  const unlink = (ws) => {
26630
27600
  if (linked.delete(ws))
26631
- s.idleExit.clients(linked.size);
27601
+ counted();
26632
27602
  };
26633
27603
  const raw = (ws, json) => {
26634
27604
  try {
@@ -26837,12 +27807,14 @@ function startServer(opts) {
26837
27807
  agentChosen: s.state.defaultAgent !== undefined
26838
27808
  });
26839
27809
  s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
27810
+ s.hub.on("keepAwakeChanged", () => broadcast({ t: "keep-awake", mode: s.keepAwake.setting() }));
26840
27811
  s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
26841
27812
  s.hub.on("failed", (worktreeId, message) => broadcast({ t: "error", message, worktreeId }));
26842
27813
  s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
26843
27814
  s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
26844
27815
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
26845
27816
  s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
27817
+ s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
26846
27818
  const helloFrame = async () => {
26847
27819
  await s.update.refresh();
26848
27820
  return {
@@ -26861,6 +27833,7 @@ function startServer(opts) {
26861
27833
  agentChosen: s.state.defaultAgent !== undefined,
26862
27834
  home: homedir11(),
26863
27835
  folderDialog: process.platform === "darwin" && !cloud.enabled,
27836
+ keepAwake: s.keepAwake.setting(),
26864
27837
  remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
26865
27838
  paired: s.state.paired,
26866
27839
  gitIdentity: await s.repos.gitIdentity(),
@@ -26869,11 +27842,13 @@ function startServer(opts) {
26869
27842
  self: s.self.get(),
26870
27843
  update: s.update.get(),
26871
27844
  drafts: s.drafts.all(),
27845
+ attachments: s.drafts.allAttachments(),
26872
27846
  managed: managedView(opts.managed)
26873
27847
  };
26874
27848
  };
26875
27849
  let branded = false;
26876
27850
  const serverConfig = {
27851
+ maxRequestBodySize: UPLOAD_MAX_BYTES,
26877
27852
  hostname: cloud.bindHost,
26878
27853
  fetch: createFetch({
26879
27854
  token,
@@ -26891,6 +27866,10 @@ function startServer(opts) {
26891
27866
  managed: { source: opts.managed.source, hash: opts.managed.hash },
26892
27867
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
26893
27868
  bootstrap: helloFrame,
27869
+ manifestColors: () => {
27870
+ const { surface0, surface1 } = s.themes.current().colors;
27871
+ return { bar: surface1, ground: surface0 };
27872
+ },
26894
27873
  open: (path) => s.opens.open(path),
26895
27874
  restart: (now) => s.restarter.request({ now }),
26896
27875
  restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
@@ -26898,10 +27877,10 @@ function startServer(opts) {
26898
27877
  onPaired: () => {
26899
27878
  s.state.notePaired();
26900
27879
  broadcast({ t: "paired" });
26901
- }
27880
+ },
27881
+ phones: () => readTailnetPhones()
26902
27882
  }),
26903
27883
  websocket: {
26904
- maxPayloadLength: 64 * 1024 * 1024,
26905
27884
  async open(ws) {
26906
27885
  const preview = ws.data.preview;
26907
27886
  if (preview) {
@@ -26943,7 +27922,7 @@ function startServer(opts) {
26943
27922
  }
26944
27923
  const parsed = parseClientMsg(json);
26945
27924
  if (!parsed.ok) {
26946
- send(ws, { t: "error", message: `invalid message: ${parsed.reason}` });
27925
+ send(ws, unparsedReply(json, parsed.reason, s.drafts));
26947
27926
  return;
26948
27927
  }
26949
27928
  const ctx = {
@@ -27010,16 +27989,16 @@ function startServer(opts) {
27010
27989
  }
27011
27990
 
27012
27991
  // ../daemon/src/themes/store.ts
27013
- import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync9, writeFileSync as writeFileSync8 } from "fs";
27992
+ import { existsSync as existsSync24, readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync9, writeFileSync as writeFileSync8 } from "fs";
27014
27993
  import { homedir as homedir12 } from "os";
27015
- import { basename as basename6, dirname as dirname8, join as join21, resolve as resolve9 } from "path";
27994
+ import { basename as basename6, dirname as dirname8, join as join22, resolve as resolve9 } from "path";
27016
27995
  var home = homedir12();
27017
27996
  var defaultExtensionDirs = [
27018
- join21(home, ".vscode", "extensions"),
27019
- join21(home, ".vscode-insiders", "extensions"),
27020
- join21(home, ".vscode-oss", "extensions"),
27021
- join21(home, ".cursor", "extensions"),
27022
- join21(home, ".windsurf", "extensions"),
27997
+ join22(home, ".vscode", "extensions"),
27998
+ join22(home, ".vscode-insiders", "extensions"),
27999
+ join22(home, ".vscode-oss", "extensions"),
28000
+ join22(home, ".cursor", "extensions"),
28001
+ join22(home, ".windsurf", "extensions"),
27023
28002
  "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
27024
28003
  "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
27025
28004
  "/Applications/Cursor.app/Contents/Resources/app/extensions",
@@ -27103,7 +28082,7 @@ class ThemeStore {
27103
28082
  name: json?.name ?? base,
27104
28083
  source: "file"
27105
28084
  });
27106
- writeFileSync8(join21(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
28085
+ writeFileSync8(join22(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
27107
28086
  `);
27108
28087
  this.load();
27109
28088
  return theme;
@@ -27135,10 +28114,10 @@ function loadDir(dir) {
27135
28114
  if (!existsSync24(dir))
27136
28115
  return [];
27137
28116
  const out = [];
27138
- for (const f of readdirSync6(dir)) {
28117
+ for (const f of readdirSync7(dir)) {
27139
28118
  if (!/\.jsonc?$/i.test(f))
27140
28119
  continue;
27141
- const file = join21(dir, f);
28120
+ const file = join22(dir, f);
27142
28121
  const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
27143
28122
  try {
27144
28123
  const json = readJsonc(file);
@@ -27159,13 +28138,13 @@ function discover(dir) {
27159
28138
  const out = [];
27160
28139
  let entries;
27161
28140
  try {
27162
- entries = readdirSync6(dir);
28141
+ entries = readdirSync7(dir);
27163
28142
  } catch {
27164
28143
  return out;
27165
28144
  }
27166
28145
  for (const name of entries) {
27167
- const extDir = join21(dir, name);
27168
- const pkgFile = join21(extDir, "package.json");
28146
+ const extDir = join22(dir, name);
28147
+ const pkgFile = join22(extDir, "package.json");
27169
28148
  try {
27170
28149
  if (!statSync9(extDir).isDirectory() || !existsSync24(pkgFile))
27171
28150
  continue;
@@ -27175,7 +28154,7 @@ function discover(dir) {
27175
28154
  continue;
27176
28155
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
27177
28156
  let nls = {};
27178
- const nlsFile = join21(extDir, "package.nls.json");
28157
+ const nlsFile = join22(extDir, "package.nls.json");
27179
28158
  if (existsSync24(nlsFile)) {
27180
28159
  try {
27181
28160
  nls = JSON.parse(readFileSync15(nlsFile, "utf8"));
@@ -27591,8 +28570,8 @@ class BackendShare {
27591
28570
  // ../daemon/src/worktrees/cache.ts
27592
28571
  import { createHash as createHash3 } from "crypto";
27593
28572
  import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
27594
- import { mkdir as mkdir4, readdir as readdir2, rename, rm as rm3, stat as stat3, utimes, writeFile as writeFile2 } from "fs/promises";
27595
- import { join as join22 } from "path";
28573
+ import { mkdir as mkdir4, readdir as readdir2, rename, rm as rm3, stat as stat4, utimes, writeFile as writeFile2 } from "fs/promises";
28574
+ import { join as join23 } from "path";
27596
28575
 
27597
28576
  // ../daemon/src/core/clone.ts
27598
28577
  import { mkdir as mkdir3, rm as rm2 } from "fs/promises";
@@ -27623,7 +28602,7 @@ var STALE_MS = 30 * 24 * 3600000;
27623
28602
  var TMP = ".tmp-";
27624
28603
  var MANIFEST = "manifest.json";
27625
28604
  function defaultTools(dir) {
27626
- const has = (...files) => files.some((f) => existsSync25(join22(dir, f)));
28605
+ const has = (...files) => files.some((f) => existsSync25(join23(dir, f)));
27627
28606
  const out = [];
27628
28607
  if (has("bun.lock", "bun.lockb"))
27629
28608
  out.push("bun --version");
@@ -27636,7 +28615,7 @@ function defaultTools(dir) {
27636
28615
  return out;
27637
28616
  }
27638
28617
  function withSidecars(root, rel) {
27639
- return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync25(join22(root, n)));
28618
+ return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync25(join23(root, n)));
27640
28619
  }
27641
28620
  var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
27642
28621
  async function listEntries(familyDir) {
@@ -27651,8 +28630,8 @@ async function listEntries(familyDir) {
27651
28630
  const m = /^([0-9a-f]{12})-([0-9a-z]+)$/.exec(name);
27652
28631
  if (!m?.[1] || !m[2])
27653
28632
  continue;
27654
- const dir = join22(familyDir, name);
27655
- if (!existsSync25(join22(dir, MANIFEST)))
28633
+ const dir = join23(familyDir, name);
28634
+ if (!existsSync25(join23(dir, MANIFEST)))
27656
28635
  continue;
27657
28636
  out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
27658
28637
  }
@@ -27682,7 +28661,7 @@ class ArtifactCache {
27682
28661
  try {
27683
28662
  const family = await this.family(wt, repo, policy);
27684
28663
  const base = await this.baseCommit(wt, repo);
27685
- const entries = await listEntries(join22(this.d.paths.cacheDir, repo.id, family.key));
28664
+ const entries = await listEntries(join23(this.d.paths.cacheDir, repo.id, family.key));
27686
28665
  const entry = entries.find((e) => e.base === base.slice(0, 12)) ?? entries[0];
27687
28666
  if (!entry) {
27688
28667
  say(`cache: nothing kept for these lockfiles and tools on ${family.platform}`);
@@ -27690,14 +28669,14 @@ class ArtifactCache {
27690
28669
  }
27691
28670
  const restored = [];
27692
28671
  for (const rel of policy.paths) {
27693
- if (existsSync25(join22(wt.path, rel)))
28672
+ if (existsSync25(join23(wt.path, rel)))
27694
28673
  continue;
27695
28674
  const names = withSidecars(entry.dir, rel);
27696
28675
  if (names.length === 0)
27697
28676
  continue;
27698
28677
  let whole = true;
27699
28678
  for (const name of names) {
27700
- const r = await (this.d.clone ?? cloneTree)(join22(entry.dir, name), join22(wt.path, name));
28679
+ const r = await (this.d.clone ?? cloneTree)(join23(entry.dir, name), join23(wt.path, name));
27701
28680
  if (r.ok)
27702
28681
  continue;
27703
28682
  say(`cache: could not restore ${name}: ${r.error}`);
@@ -27731,12 +28710,12 @@ class ArtifactCache {
27731
28710
  let tmp;
27732
28711
  let slot;
27733
28712
  try {
27734
- const present = policy.paths.filter((rel) => existsSync25(join22(wt.path, rel)));
28713
+ const present = policy.paths.filter((rel) => existsSync25(join23(wt.path, rel)));
27735
28714
  if (present.length === 0)
27736
28715
  return;
27737
28716
  const family = await this.family(wt, repo, policy);
27738
28717
  const base = await this.baseCommit(wt, repo);
27739
- const familyDir = join22(this.d.paths.cacheDir, repo.id, family.key);
28718
+ const familyDir = join23(this.d.paths.cacheDir, repo.id, family.key);
27740
28719
  const key = `${familyDir}/${base.slice(0, 12)}`;
27741
28720
  const kept = (await listEntries(familyDir)).find((e) => e.base === base.slice(0, 12));
27742
28721
  if (kept || this.making.has(key)) {
@@ -27747,21 +28726,21 @@ class ArtifactCache {
27747
28726
  this.making.add(key);
27748
28727
  const at = Date.now();
27749
28728
  const name = entryName(base, at);
27750
- tmp = join22(familyDir, `${TMP}${name}`);
28729
+ tmp = join23(familyDir, `${TMP}${name}`);
27751
28730
  await mkdir4(tmp, { recursive: true });
27752
28731
  let how = "clonefile";
27753
28732
  for (const rel of present) {
27754
28733
  for (const n of withSidecars(wt.path, rel)) {
27755
- const r = await (this.d.clone ?? cloneTree)(join22(wt.path, n), join22(tmp, n));
28734
+ const r = await (this.d.clone ?? cloneTree)(join23(wt.path, n), join23(tmp, n));
27756
28735
  if (!r.ok)
27757
28736
  throw new Error(`could not keep ${n}: ${r.error}`);
27758
28737
  how = r.how;
27759
28738
  }
27760
28739
  }
27761
28740
  const manifest = { at, base, platform: family.platform, tools: family.tools, paths: present };
27762
- await writeFile2(join22(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
28741
+ await writeFile2(join23(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
27763
28742
  `);
27764
- await rename(tmp, join22(familyDir, name));
28743
+ await rename(tmp, join23(familyDir, name));
27765
28744
  tmp = undefined;
27766
28745
  log.info(worktreeId, `cache: kept ${list(present)} for ${base.slice(0, 7)} via ${how} in ${Date.now() - started}ms`);
27767
28746
  await this.prune(familyDir);
@@ -27789,7 +28768,7 @@ class ArtifactCache {
27789
28768
  return;
27790
28769
  }
27791
28770
  for (const repoId of repoIds) {
27792
- const repoDir = join22(root, repoId);
28771
+ const repoDir = join23(root, repoId);
27793
28772
  if (!this.d.state.repo(repoId)) {
27794
28773
  await rm3(repoDir, { recursive: true, force: true });
27795
28774
  continue;
@@ -27801,7 +28780,7 @@ class ArtifactCache {
27801
28780
  continue;
27802
28781
  }
27803
28782
  for (const family of families) {
27804
- const familyDir = join22(repoDir, family);
28783
+ const familyDir = join23(repoDir, family);
27805
28784
  let names;
27806
28785
  try {
27807
28786
  names = await readdir2(familyDir);
@@ -27810,11 +28789,11 @@ class ArtifactCache {
27810
28789
  }
27811
28790
  for (const n of names)
27812
28791
  if (n.startsWith(TMP))
27813
- await rm3(join22(familyDir, n), { recursive: true, force: true });
28792
+ await rm3(join23(familyDir, n), { recursive: true, force: true });
27814
28793
  const entries = await listEntries(familyDir);
27815
28794
  let newest = 0;
27816
28795
  for (const e of entries) {
27817
- const s = await stat3(e.dir).catch(() => null);
28796
+ const s = await stat4(e.dir).catch(() => null);
27818
28797
  if (s)
27819
28798
  newest = Math.max(newest, s.mtimeMs);
27820
28799
  }
@@ -27830,7 +28809,7 @@ class ArtifactCache {
27830
28809
  for (const f of policy.key) {
27831
28810
  h.update(`\x00${f}\x00`);
27832
28811
  try {
27833
- h.update(readFileSync16(join22(wt.path, f)));
28812
+ h.update(readFileSync16(join23(wt.path, f)));
27834
28813
  } catch {
27835
28814
  h.update("(absent)");
27836
28815
  }
@@ -27995,6 +28974,92 @@ class ChatSearch {
27995
28974
  }
27996
28975
  }
27997
28976
 
28977
+ // ../daemon/src/worktrees/fix.ts
28978
+ var RESPONSE = {
28979
+ hook: { does: "ask" },
28980
+ conflict: { does: "ask" },
28981
+ check: { does: "ask", once: true },
28982
+ command: { does: "offer" },
28983
+ preview: { does: "offer" }
28984
+ };
28985
+ function lastAsked(entries) {
28986
+ for (let i = entries.length - 1;i >= 0; i--) {
28987
+ const e = entries[i].event;
28988
+ if (e.type === "fix-asked")
28989
+ return e.kind;
28990
+ if (e.type === "user-message")
28991
+ return;
28992
+ }
28993
+ return;
28994
+ }
28995
+ function askedFor(entries, toolId) {
28996
+ return entries.some(({ event }) => event.type === "fix-asked" && event.toolId === toolId);
28997
+ }
28998
+ function failureOf(entries, toolId) {
28999
+ let command;
29000
+ for (const { event } of entries) {
29001
+ if (event.type === "tool-start" && event.toolId === toolId) {
29002
+ const input = event.input;
29003
+ command = typeof input?.command === "string" ? input.command : undefined;
29004
+ } else if (event.type === "tool-end" && event.toolId === toolId) {
29005
+ if (!event.fixable || command === undefined)
29006
+ return null;
29007
+ const run = { toolId, command, text: unformatOutput(event.output ?? "") };
29008
+ if (event.fixable.kind === "hook")
29009
+ return { kind: "hook", hook: event.fixable.hook ?? "git", ...run };
29010
+ return { kind: event.fixable.kind, ...run };
29011
+ }
29012
+ }
29013
+ return null;
29014
+ }
29015
+
29016
+ class FixService {
29017
+ d;
29018
+ constructor(d) {
29019
+ this.d = d;
29020
+ d.hub.on("checkFailed", (id, run) => {
29021
+ this.report(id, { kind: "check", ...run });
29022
+ });
29023
+ }
29024
+ report(worktreeId, failure, opts = {}) {
29025
+ const response = RESPONSE[failure.kind];
29026
+ if (response.does === "offer" && !opts.pressed)
29027
+ return false;
29028
+ const wt = this.d.state.worktree(worktreeId);
29029
+ if (!wt || isMain(wt))
29030
+ return false;
29031
+ const agent = this.d.runtime.agentFor(worktreeId);
29032
+ if (!agent)
29033
+ return false;
29034
+ const entries = agent.transcript();
29035
+ if (response.once && !opts.pressed && lastAsked(entries) === failure.kind)
29036
+ return false;
29037
+ const toolId = failedRun(failure)?.toolId;
29038
+ if (toolId && askedFor(entries, toolId))
29039
+ return false;
29040
+ const context = failureContext(failure);
29041
+ agent.send(fixPrompt(failure), {
29042
+ asked: { kind: failure.kind, why: fixWhy(failure), ...toolId ? { toolId } : {} },
29043
+ ...context ? { context: [context] } : {}
29044
+ });
29045
+ return true;
29046
+ }
29047
+ press(worktreeId, toolId) {
29048
+ this.d.state.requireWorktree(worktreeId);
29049
+ const agent = this.d.runtime.agentFor(worktreeId);
29050
+ if (!agent)
29051
+ throw new UserError("worktree still starting; try again in a moment");
29052
+ const entries = agent.transcript();
29053
+ const failure = failureOf(entries, toolId);
29054
+ if (!failure)
29055
+ throw new UserError("nothing to fix on that row");
29056
+ if (askedFor(entries, toolId))
29057
+ return;
29058
+ if (!this.report(worktreeId, failure, { pressed: true }))
29059
+ throw new UserError("nothing runs on main");
29060
+ }
29061
+ }
29062
+
27998
29063
  // ../daemon/src/worktrees/landing.ts
27999
29064
  var TAIL_CHARS = 400;
28000
29065
  var DIFF_CHARS = 2000;
@@ -28008,6 +29073,7 @@ class LandingService {
28008
29073
  constructor(d) {
28009
29074
  this.d = d;
28010
29075
  d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
29076
+ d.hub.on("messageRefused", (id, said) => fireAndForget(id, this.judge(id, `The commit-msg hook refused the last message, saying: ${said}`), "landing message after a refusal"));
28011
29077
  d.hub.on("agentStatus", (id, status) => {
28012
29078
  this.note(id, status);
28013
29079
  if (status === "working")
@@ -28178,6 +29244,7 @@ class LandingService {
28178
29244
  let check = "none";
28179
29245
  let checkTail;
28180
29246
  let waited = 0;
29247
+ let ran;
28181
29248
  const command = repo.config.check?.trim();
28182
29249
  if (command) {
28183
29250
  const ceiling = timeoutFor(repo.config, "check");
@@ -28201,6 +29268,7 @@ class LandingService {
28201
29268
  run.finish(r.exit);
28202
29269
  if (!live())
28203
29270
  return;
29271
+ ran = r;
28204
29272
  check = r.exit === 0 ? "pass" : "fail";
28205
29273
  if (r.exit === "timeout") {
28206
29274
  checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
@@ -28256,6 +29324,8 @@ class LandingService {
28256
29324
  record.recap = { at: Date.now(), text: verdict.recap };
28257
29325
  log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms${waited > 100 ? ` (waited ${waited}ms for a check slot)` : ""}`);
28258
29326
  this.d.worktrees.setLanding(worktreeId, landing);
29327
+ if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
29328
+ this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
28259
29329
  }
28260
29330
  async sharedMigration(wt, repo) {
28261
29331
  if (!repo.services || keepsCopiesApart(repo.config))
@@ -28660,12 +29730,12 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
28660
29730
  }
28661
29731
 
28662
29732
  // ../daemon/src/worktrees/service.ts
28663
- import { cpSync, existsSync as existsSync28, readFileSync as readFileSync18, rmSync as rmSync5 } from "fs";
29733
+ import { cpSync, existsSync as existsSync30, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
28664
29734
  import { rm as rm4 } from "fs/promises";
28665
- import { dirname as dirname11, join as join25 } from "path";
29735
+ import { dirname as dirname11, join as join27 } from "path";
28666
29736
 
28667
29737
  // ../daemon/src/git/archive.ts
28668
- import { rmSync as rmSync3 } from "fs";
29738
+ import { rmSync as rmSync4 } from "fs";
28669
29739
  var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
28670
29740
  var SNAPSHOT_SUBJECT = "toyon: uncommitted work";
28671
29741
  var SNAPSHOT_AUTHOR = "toyon";
@@ -28708,7 +29778,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
28708
29778
  ], wtPath);
28709
29779
  return commit.ok ? commit.out : null;
28710
29780
  } finally {
28711
- rmSync3(indexFile, { force: true });
29781
+ rmSync4(indexFile, { force: true });
28712
29782
  }
28713
29783
  }
28714
29784
  async function keptAt(repoPath, ref) {
@@ -28786,7 +29856,11 @@ async function dropLandRefs(repoPath, worktreeId) {
28786
29856
  }
28787
29857
 
28788
29858
  // ../daemon/src/git/land.ts
29859
+ import { existsSync as existsSync26 } from "fs";
28789
29860
  var STEP_TIMEOUT_MS = 10 * 60000;
29861
+ function rowOf(r) {
29862
+ return r.shown && r.toolId && r.command ? { toolId: r.toolId, command: r.command, text: r.text } : undefined;
29863
+ }
28790
29864
  var stepRun = (cwd, args, onText, signal, opts = {}) => runWatched(GIT, args, cwd, { env: NO_PROMPT, onText, signal, ...opts, timeoutMs: opts.timeoutMs ?? STEP_TIMEOUT_MS });
28791
29865
  var UNWATCHED = {
28792
29866
  step() {},
@@ -28799,6 +29873,14 @@ function refused(what, r, n = 200) {
28799
29873
  }
28800
29874
  return `${what}: ${r.shown ? "what git and its hooks printed is on the chat" : r.err.slice(0, n)}`;
28801
29875
  }
29876
+ function stepRefused(what, r, n = 200) {
29877
+ const row = typeof r.exit === "number" && r.hook ? rowOf(r) : undefined;
29878
+ return {
29879
+ ok: false,
29880
+ message: refused(what, r, n),
29881
+ ...row && r.hook ? { hook: { ...row, name: r.hook } } : {}
29882
+ };
29883
+ }
28802
29884
  async function commitWorktree(worktreePath, message, w = UNWATCHED) {
28803
29885
  if ((await statusFiles(worktreePath)).length === 0)
28804
29886
  return { ok: false, message: "nothing to commit" };
@@ -28806,7 +29888,7 @@ async function commitWorktree(worktreePath, message, w = UNWATCHED) {
28806
29888
  w.step("committing");
28807
29889
  const c = await w.git(worktreePath, ["commit", "-m", message]);
28808
29890
  if (!c.ok)
28809
- return { ok: false, message: refused("commit refused", c) };
29891
+ return stepRefused("commit refused", c);
28810
29892
  return { ok: true, message: `committed: ${message}` };
28811
29893
  }
28812
29894
  async function requireClean(worktreePath) {
@@ -28892,7 +29974,27 @@ async function putBack(worktreePath, defaultBr, aside, taken) {
28892
29974
  message: `the uncommitted changes here conflict with ${defaultBr}${named}: commit them first, then sync`
28893
29975
  };
28894
29976
  }
29977
+ var MIDWAY = [
29978
+ ["MERGE_HEAD", "merge"],
29979
+ ["rebase-merge", "rebase"],
29980
+ ["rebase-apply", "rebase"],
29981
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
29982
+ ["REVERT_HEAD", "revert"]
29983
+ ];
29984
+ async function midway(worktreePath) {
29985
+ const args = MIDWAY.flatMap(([file]) => ["--git-path", file]);
29986
+ const r = await git(worktreePath, "rev-parse", "--path-format=absolute", ...args);
29987
+ if (!r.ok)
29988
+ return null;
29989
+ const at = r.out.split(`
29990
+ `).findIndex((file) => file && existsSync26(file));
29991
+ return MIDWAY[at]?.[1] ?? null;
29992
+ }
28895
29993
  async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
29994
+ const unfinished = await midway(worktreePath);
29995
+ if (unfinished) {
29996
+ return { ok: false, message: `a ${unfinished} is unfinished here: finish or abort it first` };
29997
+ }
28896
29998
  if (!carry) {
28897
29999
  const cErr = await requireClean(worktreePath);
28898
30000
  if (cErr)
@@ -28907,8 +30009,19 @@ async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = f
28907
30009
  const taken = await bringIn(worktreePath, defaultBr, own, behind, w);
28908
30010
  return aside ? putBack(worktreePath, defaultBr, aside, taken) : taken;
28909
30011
  }
30012
+ async function requireLinear(worktreePath, base) {
30013
+ const merges = await git(worktreePath, "rev-list", "--merges", "--count", `${base}..HEAD`);
30014
+ if (!merges.ok || merges.out === "0")
30015
+ return null;
30016
+ return {
30017
+ ok: false,
30018
+ conflict: "rebase",
30019
+ message: `the branch holds a merge commit, which landing by rebase would put on ${base}: ask the agent to rebase onto ${base} first`
30020
+ };
30021
+ }
28910
30022
  async function bringIn(worktreePath, defaultBr, own, behind, w) {
28911
- if (own) {
30023
+ const merges = own ? await git(worktreePath, "rev-list", "--merges", "--count", `${defaultBr}..HEAD`) : null;
30024
+ if (own && merges?.out === "0") {
28912
30025
  w.step(`rebasing onto ${defaultBr}`);
28913
30026
  const r = await w.git(worktreePath, ["rebase", defaultBr]);
28914
30027
  if (!r.ok) {
@@ -28916,10 +30029,12 @@ async function bringIn(worktreePath, defaultBr, own, behind, w) {
28916
30029
  const files = overwritten(r.text);
28917
30030
  if (files.length)
28918
30031
  return { ok: false, message: untrackedInTheWay(defaultBr, files) };
28919
- const ask = `rebase onto ${defaultBr} conflicts: ask the agent to bring ${defaultBr} in and resolve them`;
30032
+ const ask = `rebase onto ${defaultBr} conflicts: ask the agent to rebase onto ${defaultBr} and resolve them`;
30033
+ const step = rowOf(r);
28920
30034
  return {
28921
30035
  ok: false,
28922
- conflict: true,
30036
+ conflict: "rebase",
30037
+ ...step ? { step } : {},
28923
30038
  message: r.shown ? `${ask}; the conflicts are on the chat` : `${ask} (${r.text.slice(0, 200)})`
28924
30039
  };
28925
30040
  }
@@ -28966,7 +30081,7 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
28966
30081
  const c = await w.git(repoPath, ["commit", "-m", message]);
28967
30082
  if (!c.ok) {
28968
30083
  await git(repoPath, "reset", "--hard", "HEAD");
28969
- return { ok: false, message: refused("squash commit refused", c) };
30084
+ return stepRefused("squash commit refused", c);
28970
30085
  }
28971
30086
  return { ok: true, message: `squashed ${branch} onto ${defaultBr}` };
28972
30087
  }
@@ -29025,7 +30140,7 @@ async function landingCommit(worktreePath, branch, base, method, message, own, w
29025
30140
  const c = await w.git(worktreePath, ["commit", "-m", message]);
29026
30141
  if (!c.ok) {
29027
30142
  await git(worktreePath, "reset", "--soft", head.out);
29028
- return { ok: false, message: refused("squash commit refused", c) };
30143
+ return stepRefused("squash commit refused", c);
29029
30144
  }
29030
30145
  const sha = await git(worktreePath, "rev-parse", "HEAD");
29031
30146
  return { ok: true, sha: sha.out, message: `squashed ${branch} onto ${base}` };
@@ -29038,15 +30153,15 @@ async function landingCommit(worktreePath, branch, base, method, message, own, w
29038
30153
  w.step(`squashing onto ${base}`);
29039
30154
  const s = await w.git(worktreePath, ["merge", "--squash", branch]);
29040
30155
  if (!s.ok)
29041
- return { ok: false, message: refused("squash refused", s) };
30156
+ return stepRefused("squash refused", s);
29042
30157
  const c = await w.git(worktreePath, ["commit", "-m", message]);
29043
30158
  if (!c.ok)
29044
- return { ok: false, message: refused("squash commit refused", c) };
30159
+ return stepRefused("squash commit refused", c);
29045
30160
  } else {
29046
30161
  w.step(`merging into ${base}`);
29047
30162
  const m = await w.git(worktreePath, ["merge", "--no-ff", "-m", `Merge branch '${branch}'`, branch]);
29048
30163
  if (!m.ok)
29049
- return { ok: false, message: refused("merge refused", m) };
30164
+ return stepRefused("merge refused", m);
29050
30165
  }
29051
30166
  const sha = await git(worktreePath, "rev-parse", "HEAD");
29052
30167
  return { ok: true, sha: sha.out, message: `${method === "squash" ? "squashed" : "merged"} ${branch} onto ${base}` };
@@ -29073,15 +30188,17 @@ async function pushLanding(worktreePath, sha, defaultBr, w = UNWATCHED) {
29073
30188
  if (p.ok)
29074
30189
  return { ok: true, message: `pushed ${defaultBr} to ${tracked.remote}` };
29075
30190
  const moved = /\[rejected\]|fetch first|non-fast-forward|cannot lock ref/.test(p.err);
29076
- return moved ? { ok: false, moved: true, message: `${defaultBr} on ${tracked.remote} moved while landing` } : { ok: false, message: refused("push failed", p, 300) };
30191
+ return moved ? { ok: false, moved: true, message: `${defaultBr} on ${tracked.remote} moved while landing` } : stepRefused("push failed", p, 300);
29077
30192
  }
29078
30193
  async function mergeFailure(cwd, m, conflictMessage) {
29079
30194
  const conflict = /CONFLICT|Automatic merge failed/.test(m.text);
29080
30195
  if (conflict) {
29081
30196
  await git(cwd, "merge", "--abort");
30197
+ const step = rowOf(m);
29082
30198
  return {
29083
30199
  ok: false,
29084
- conflict: true,
30200
+ conflict: "merge",
30201
+ ...step ? { step } : {},
29085
30202
  message: m.shown ? `${conflictMessage}; the conflicts are on the chat` : `${conflictMessage} (${m.text.slice(0, 200)})`
29086
30203
  };
29087
30204
  }
@@ -29112,7 +30229,7 @@ async function pushBranch(worktreePath, branch, w = UNWATCHED) {
29112
30229
  push = await w.git(worktreePath, args);
29113
30230
  }
29114
30231
  if (!push.ok)
29115
- return { ok: false, message: refused("push failed", push, 300) };
30232
+ return stepRefused("push failed", push, 300);
29116
30233
  return { ok: true, message: `pushed ${branch}` };
29117
30234
  }
29118
30235
  async function goneFromOrigin(worktreePath, branch) {
@@ -29183,18 +30300,142 @@ function compareUrl(remoteUrl, base, branch) {
29183
30300
  return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
29184
30301
  }
29185
30302
 
30303
+ // ../daemon/src/git/turn.ts
30304
+ import { copyFileSync, existsSync as existsSync27, rmSync as rmSync5 } from "fs";
30305
+ import { join as join24 } from "path";
30306
+ var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
30307
+ var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
30308
+ var queue = new Map;
30309
+ function inTurn(wtPath, job) {
30310
+ const next = (queue.get(wtPath) ?? Promise.resolve()).then(job, job);
30311
+ queue.set(wtPath, next.catch(() => {}));
30312
+ return next;
30313
+ }
30314
+ var synced = new Map;
30315
+ var MAX_PATHS = 400;
30316
+ async function treeNow(wtPath, head, dirty) {
30317
+ let was = synced.get(wtPath);
30318
+ let index = was?.index;
30319
+ if (!index) {
30320
+ const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
30321
+ if (!dir.ok || !dir.out)
30322
+ return null;
30323
+ index = join24(dir.out, "toyon-turn-index");
30324
+ }
30325
+ const env = { GIT_INDEX_FILE: index };
30326
+ const writeTree = async () => {
30327
+ const tree = await run(GIT, ["write-tree"], wtPath, env);
30328
+ return tree.ok && tree.out ? tree.out : null;
30329
+ };
30330
+ if (!existsSync27(index))
30331
+ was = undefined;
30332
+ const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
30333
+ if (was && paths && was.head === head) {
30334
+ const again = [...new Set([...was.dirty, ...paths])];
30335
+ if (again.length <= MAX_PATHS) {
30336
+ const added = again.length === 0 || (await run(GIT, ["--literal-pathspecs", "add", "-A", "--", ...again], wtPath, env)).ok;
30337
+ const tree = added ? await writeTree() : null;
30338
+ if (tree) {
30339
+ synced.set(wtPath, { index, head, dirty: new Set(paths) });
30340
+ return tree;
30341
+ }
30342
+ }
30343
+ }
30344
+ synced.delete(wtPath);
30345
+ if (!existsSync27(index)) {
30346
+ const own = join24(index, "..", "index");
30347
+ if (existsSync27(own))
30348
+ copyFileSync(own, index);
30349
+ else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
30350
+ return null;
30351
+ }
30352
+ const full = async () => (await run(GIT, ["add", "-A"], wtPath, env)).ok ? writeTree() : null;
30353
+ let tree = await full();
30354
+ if (!tree) {
30355
+ rmSync5(index, { force: true });
30356
+ if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
30357
+ return null;
30358
+ tree = await full();
30359
+ if (!tree)
30360
+ return null;
30361
+ }
30362
+ const differs = await git(wtPath, "diff", "--name-only", "--no-renames", "-z", head, tree);
30363
+ if (differs.ok)
30364
+ synced.set(wtPath, { index, head, dirty: new Set(differs.out.split("\x00").filter(Boolean)) });
30365
+ return tree;
30366
+ }
30367
+ async function snapshotAt(wtPath, ref) {
30368
+ const r = await git(wtPath, "rev-parse", "--quiet", `${ref}^{commit}`, `${ref}^{tree}`, `${ref}^`);
30369
+ const [commit, tree, parent] = r.ok ? r.out.split(`
30370
+ `) : [];
30371
+ return commit && tree && parent ? { commit, tree, parent } : null;
30372
+ }
30373
+ function markTurn(wtPath, worktreeId) {
30374
+ return inTurn(wtPath, async () => {
30375
+ const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
30376
+ if (!head.ok)
30377
+ return;
30378
+ const tree = await treeNow(wtPath, head.out);
30379
+ if (!tree)
30380
+ return;
30381
+ const at = await snapshotAt(wtPath, turnRef(worktreeId, "at"));
30382
+ if (at?.tree === tree && at.parent === head.out)
30383
+ return;
30384
+ const commit = await git(wtPath, "-c", "user.name=toyon", "-c", "user.email=toyon@localhost", "commit-tree", tree, "-p", head.out, "-m", SNAPSHOT_SUBJECT2);
30385
+ if (!commit.ok)
30386
+ return;
30387
+ if (at && at.tree !== tree)
30388
+ await git(wtPath, "update-ref", turnRef(worktreeId, "before"), at.commit);
30389
+ await git(wtPath, "update-ref", turnRef(worktreeId, "at"), commit.out);
30390
+ });
30391
+ }
30392
+ function turnChanges(wtPath, worktreeId, defaultBr, own, dirty) {
30393
+ return inTurn(wtPath, async () => {
30394
+ const at = await snapshotAt(wtPath, turnRef(worktreeId, "at"));
30395
+ if (!at)
30396
+ return null;
30397
+ const head = await git(wtPath, "rev-parse", "HEAD", "HEAD^{tree}");
30398
+ const [headCommit, headTree] = head.ok ? head.out.split(`
30399
+ `) : [];
30400
+ if (!headCommit)
30401
+ return null;
30402
+ const tree = await treeNow(wtPath, headCommit, dirty);
30403
+ if (!tree)
30404
+ return null;
30405
+ const from = at.tree !== tree ? at : await snapshotAt(wtPath, turnRef(worktreeId, "before"));
30406
+ if (!from || from.tree === tree || from.tree === headTree)
30407
+ return null;
30408
+ if (from.parent !== headCommit) {
30409
+ if (!own)
30410
+ return null;
30411
+ const [then, now] = await Promise.all([
30412
+ git(wtPath, "merge-base", from.parent, defaultBr),
30413
+ git(wtPath, "merge-base", headCommit, defaultBr)
30414
+ ]);
30415
+ if (!then.ok || !now.ok || then.out !== now.out)
30416
+ return null;
30417
+ }
30418
+ const files = await filesBetween(wtPath, from.commit, tree);
30419
+ return files.length > 0 ? { base: from.commit, files } : null;
30420
+ });
30421
+ }
30422
+ async function dropTurnRefs(repoPath, worktreeId) {
30423
+ for (const which of ["at", "before"])
30424
+ await git(repoPath, "update-ref", "-d", turnRef(worktreeId, which));
30425
+ }
30426
+
29186
30427
  // ../daemon/src/worktrees/archive.ts
29187
- import { existsSync as existsSync26, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
29188
- import { basename as basename8, dirname as dirname10, join as join23 } from "path";
30428
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
30429
+ import { basename as basename8, dirname as dirname10, join as join25 } from "path";
29189
30430
  class WorktreeArchive {
29190
30431
  dir;
29191
30432
  records = new Map;
29192
30433
  constructor(dir) {
29193
30434
  this.dir = dir;
29194
- mkdirSync7(dir, { recursive: true });
29195
- for (const id of readdirSync7(dir)) {
30435
+ mkdirSync8(dir, { recursive: true });
30436
+ for (const id of readdirSync8(dir)) {
29196
30437
  const file = this.filesOf(id).record;
29197
- if (!existsSync26(file))
30438
+ if (!existsSync28(file))
29198
30439
  continue;
29199
30440
  try {
29200
30441
  this.records.set(id, JSON.parse(readFileSync17(file, "utf8")));
@@ -29208,7 +30449,7 @@ class WorktreeArchive {
29208
30449
  }
29209
30450
  put(rec, from) {
29210
30451
  const at = this.filesOf(rec.worktree.id);
29211
- mkdirSync7(at.dir, { recursive: true });
30452
+ mkdirSync8(at.dir, { recursive: true });
29212
30453
  move(from.transcript, at.transcript);
29213
30454
  move(from.attachments, at.attachments);
29214
30455
  move(from.plans, at.plans);
@@ -29228,7 +30469,7 @@ class WorktreeArchive {
29228
30469
  delete(id) {
29229
30470
  if (!this.records.has(id))
29230
30471
  return;
29231
- rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
30472
+ rmSync6(this.filesOf(id).dir, { recursive: true, force: true });
29232
30473
  this.records.delete(id);
29233
30474
  }
29234
30475
  list(repo) {
@@ -29243,8 +30484,8 @@ class WorktreeArchive {
29243
30484
  planFile(id, rel) {
29244
30485
  if (!this.records.has(id) || !isPlanPath(rel))
29245
30486
  return null;
29246
- const file = join23(this.filesOf(id).plans, basename8(rel));
29247
- if (!existsSync26(file))
30487
+ const file = join25(this.filesOf(id).plans, basename8(rel));
30488
+ if (!existsSync28(file))
29248
30489
  return null;
29249
30490
  try {
29250
30491
  return readFileSync17(file, "utf8");
@@ -29254,13 +30495,13 @@ class WorktreeArchive {
29254
30495
  }
29255
30496
  }
29256
30497
  filesOf(id) {
29257
- const dir = join23(this.dir, id);
30498
+ const dir = join25(this.dir, id);
29258
30499
  return {
29259
30500
  dir,
29260
- record: join23(dir, "record.json"),
29261
- transcript: join23(dir, "transcript.jsonl"),
29262
- attachments: join23(dir, "attachments"),
29263
- plans: join23(dir, "plans")
30501
+ record: join25(dir, "record.json"),
30502
+ transcript: join25(dir, "transcript.jsonl"),
30503
+ attachments: join25(dir, "attachments"),
30504
+ plans: join25(dir, "plans")
29264
30505
  };
29265
30506
  }
29266
30507
  }
@@ -29285,7 +30526,7 @@ function summarize(r, repoId, transcript) {
29285
30526
  };
29286
30527
  }
29287
30528
  function lastUsage(transcript) {
29288
- if (!existsSync26(transcript))
30529
+ if (!existsSync28(transcript))
29289
30530
  return null;
29290
30531
  const lines = readFileSync17(transcript, "utf8").split(`
29291
30532
  `);
@@ -29301,7 +30542,7 @@ function lastUsage(transcript) {
29301
30542
  return null;
29302
30543
  }
29303
30544
  function firstPrompt(transcript) {
29304
- if (!existsSync26(transcript))
30545
+ if (!existsSync28(transcript))
29305
30546
  return;
29306
30547
  for (const line of readFileSync17(transcript, "utf8").split(`
29307
30548
  `)) {
@@ -29316,10 +30557,10 @@ function firstPrompt(transcript) {
29316
30557
  return;
29317
30558
  }
29318
30559
  function move(from, to) {
29319
- if (!existsSync26(from))
30560
+ if (!existsSync28(from))
29320
30561
  return;
29321
- mkdirSync7(dirname10(to), { recursive: true });
29322
- rmSync4(to, { recursive: true, force: true });
30562
+ mkdirSync8(dirname10(to), { recursive: true });
30563
+ rmSync6(to, { recursive: true, force: true });
29323
30564
  renameSync4(from, to);
29324
30565
  }
29325
30566
 
@@ -29381,8 +30622,8 @@ class ArchivedGit {
29381
30622
  }
29382
30623
 
29383
30624
  // ../daemon/src/worktrees/spare.ts
29384
- import { existsSync as existsSync27 } from "fs";
29385
- import { basename as basename9, join as join24 } from "path";
30625
+ import { existsSync as existsSync29 } from "fs";
30626
+ import { basename as basename9, join as join26 } from "path";
29386
30627
  class SparePool {
29387
30628
  d;
29388
30629
  inflight = new Map;
@@ -29401,7 +30642,7 @@ class SparePool {
29401
30642
  if (!text.trim())
29402
30643
  return;
29403
30644
  const wt = this.d.state.worktree(boxId);
29404
- if (wt?.kind !== "spare" || !existsSync27(wt.path))
30645
+ if (wt?.kind !== "spare" || !existsSync29(wt.path))
29405
30646
  return;
29406
30647
  const agent = this.d.runtime.ensureAgent(wt).agent;
29407
30648
  if (agent.runningAgent === null)
@@ -29446,11 +30687,11 @@ class SparePool {
29446
30687
  return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
29447
30688
  }
29448
30689
  async record(repoId, repo) {
29449
- const { id, dir } = freeSlot(join24(this.d.paths.worktreesDir, repo.name));
30690
+ const { id, dir } = freeSlot(join26(this.d.paths.worktreesDir, repo.name));
29450
30691
  const wt = {
29451
30692
  id,
29452
30693
  repoId,
29453
- path: join24(this.d.paths.worktreesDir, repo.name, dir),
30694
+ path: join26(this.d.paths.worktreesDir, repo.name, dir),
29454
30695
  branch: repo.defaultBranch,
29455
30696
  kind: "spare",
29456
30697
  phase: "reserved",
@@ -29465,7 +30706,7 @@ class SparePool {
29465
30706
  const repoId = repo.id;
29466
30707
  let cut = () => {};
29467
30708
  const added = (async () => {
29468
- if (existsSync27(wt.path))
30709
+ if (existsSync29(wt.path))
29469
30710
  return;
29470
30711
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
29471
30712
  })();
@@ -29513,7 +30754,7 @@ class SparePool {
29513
30754
  }
29514
30755
  log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
29515
30756
  this.d.carryDraft?.(wt.id, wt.repoId);
29516
- if (existsSync27(wt.path)) {
30757
+ if (existsSync29(wt.path)) {
29517
30758
  await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
29518
30759
  return;
29519
30760
  }
@@ -29570,7 +30811,7 @@ class SparePool {
29570
30811
  const running = this.inflight.get(repoId);
29571
30812
  await running?.added.catch(() => {});
29572
30813
  running?.cut();
29573
- if (wt.kind !== "spare" || !existsSync27(wt.path))
30814
+ if (wt.kind !== "spare" || !existsSync29(wt.path))
29574
30815
  return null;
29575
30816
  }
29576
30817
  await this.refreshing.get(repoId);
@@ -29616,7 +30857,7 @@ class Trunk {
29616
30857
  const last = this.lastFetch.get(path) ?? 0;
29617
30858
  if (Date.now() - last > FETCH_EVERY_MS) {
29618
30859
  this.lastFetch.set(path, Date.now());
29619
- fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then(async (r) => {
30860
+ fireAndForget("fetch", withFetchLock(path, () => run(GIT, ["fetch", "--quiet"], path, NO_PROMPT)).then(async (r) => {
29620
30861
  const main = this.d.state.worktree(mainId);
29621
30862
  if (main)
29622
30863
  this.noteFetch(main.repoId, r);
@@ -29641,7 +30882,7 @@ class Trunk {
29641
30882
  if (Date.now() - last < OPEN_FETCH_MIN_MS)
29642
30883
  return;
29643
30884
  this.lastFetch.set(repo.path, Date.now());
29644
- const f = await run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT);
30885
+ const f = await withFetchLock(repo.path, () => run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT));
29645
30886
  this.noteFetch(repo.id, f);
29646
30887
  if (!f.ok) {
29647
30888
  log.warn("fetch", `could not fetch ${repo.path}: ${f.err.slice(0, 200)}`);
@@ -29665,7 +30906,7 @@ class Trunk {
29665
30906
  return { ok: false, message: "no main checkout to pull" };
29666
30907
  this.lastFetch.set(repo.path, Date.now());
29667
30908
  w.step(`pulling ${repo.defaultBranch} from origin`);
29668
- const f = await w.git(repo.path, ["fetch", "--quiet"]);
30909
+ const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
29669
30910
  this.noteFetch(repo.id, f);
29670
30911
  if (!f.ok)
29671
30912
  return { ok: false, message: refused("fetch failed", f) };
@@ -29807,6 +31048,7 @@ function stopOf(prev, next) {
29807
31048
  // ../daemon/src/worktrees/service.ts
29808
31049
  var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
29809
31050
  var BASE_IGNORE = "base .gitignore";
31051
+ var REFUSAL_CHARS = 400;
29810
31052
  async function deleteTree(path) {
29811
31053
  try {
29812
31054
  await rm4(path, { recursive: true, force: true });
@@ -29867,10 +31109,9 @@ class WorktreeService {
29867
31109
  await this.discardWorktree(id);
29868
31110
  },
29869
31111
  carryDraft: (fromId, repoId) => {
29870
- const text = this.d.drafts?.text(fromId);
29871
31112
  const main = this.mainOf(repoId);
29872
- if (text && main)
29873
- this.d.drafts?.set(main.id, text);
31113
+ if (main)
31114
+ this.d.drafts?.move(fromId, main.id, true);
29874
31115
  },
29875
31116
  ready: (repoId) => {
29876
31117
  const main = this.mainOf(repoId);
@@ -29879,15 +31120,15 @@ class WorktreeService {
29879
31120
  if (this.d.runtime.get(main.id)?.procs) {
29880
31121
  fireAndForget(main.id, this.d.runtime.stopPage(main.id), "main's page stops for the spare");
29881
31122
  }
29882
- const text = this.d.drafts?.text(main.id);
29883
31123
  const spare = this.leadSpareOf(repoId);
29884
- if (text && spare && !this.d.drafts?.text(spare)) {
29885
- this.d.drafts?.set(spare, text);
29886
- this.d.drafts?.drop(main.id);
29887
- }
31124
+ if (spare)
31125
+ this.d.drafts?.move(main.id, spare);
29888
31126
  }
29889
31127
  });
29890
31128
  d.hub.on("agent", (worktreeId, _seq, event) => {
31129
+ if ((event.type === "tool-start" || event.type === "tool-update" && event.kind) && isWriteTool({ name: event.name ?? "", kind: event.kind })) {
31130
+ this.startPreview(worktreeId);
31131
+ }
29891
31132
  if (event.type !== "usage")
29892
31133
  return;
29893
31134
  const { used, size, cost } = event;
@@ -29952,14 +31193,14 @@ class WorktreeService {
29952
31193
  rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
29953
31194
  this.scheduleNaming(claimed, task, variant);
29954
31195
  if (carried.unmoved)
29955
- throw new UserError(carried.unmoved);
31196
+ throw new UserError(carried.unmoved, { delivered: true });
29956
31197
  return claimed;
29957
31198
  }
29958
31199
  if (!await isGitRepo(repo.path)) {
29959
31200
  throw new UserError(`git no longer reads ${repo.name} as a checkout, so there is nothing to start a worktree from. If \`git config core.bare\` says true there, \`git config --unset core.bare\` puts it back.`);
29960
31201
  }
29961
- const { id, dir } = freeSlot(join25(this.d.paths.worktreesDir, repo.name));
29962
- const wtPath = join25(this.d.paths.worktreesDir, repo.name, dir);
31202
+ const { id, dir } = freeSlot(join27(this.d.paths.worktreesDir, repo.name));
31203
+ const wtPath = join27(this.d.paths.worktreesDir, repo.name, dir);
29963
31204
  const branch = `toyon/${dir}`;
29964
31205
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
29965
31206
  const wt = {
@@ -29986,7 +31227,7 @@ class WorktreeService {
29986
31227
  this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
29987
31228
  this.scheduleNaming(wt, task, variant);
29988
31229
  if (carried.unmoved)
29989
- throw new UserError(carried.unmoved);
31230
+ throw new UserError(carried.unmoved, { delivered: true });
29990
31231
  return wt;
29991
31232
  }
29992
31233
  async carryMain(repo, wt, move) {
@@ -30025,7 +31266,7 @@ class WorktreeService {
30025
31266
  };
30026
31267
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
30027
31268
  const restored = (carried.ok ? carried.out.split(`
30028
- `).filter(Boolean) : []).filter((p) => !existsSync28(join25(wt.path, p)));
31269
+ `).filter(Boolean) : []).filter((p) => !existsSync30(join27(wt.path, p)));
30029
31270
  const applied = await git(wt.path, "stash", "apply", sha);
30030
31271
  if (applied.ok) {
30031
31272
  const ref = await entry();
@@ -30061,9 +31302,9 @@ class WorktreeService {
30061
31302
  const branch = kind === "pr" ? `pr/${number}` : ref;
30062
31303
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
30063
31304
  let slug = branchSlug(title) || "branch";
30064
- if (existsSync28(join25(this.d.paths.worktreesDir, repo.name, slug)))
31305
+ if (existsSync30(join27(this.d.paths.worktreesDir, repo.name, slug)))
30065
31306
  slug = `${slug}-${shortId().slice(0, 4)}`;
30066
- const wtPath = join25(this.d.paths.worktreesDir, repo.name, slug);
31307
+ const wtPath = join27(this.d.paths.worktreesDir, repo.name, slug);
30067
31308
  await withRepoLock(repo.path, async () => {
30068
31309
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
30069
31310
  if (out)
@@ -30106,7 +31347,7 @@ class WorktreeService {
30106
31347
  throw new UserError("that worktree is gone, or Toyon already has it");
30107
31348
  return found;
30108
31349
  }
30109
- async adopt(worktreeId, createdBy) {
31350
+ async adopt(worktreeId, createdBy, message) {
30110
31351
  const r = this.readable(worktreeId);
30111
31352
  if (!r)
30112
31353
  throw new UserError("that worktree is gone");
@@ -30135,12 +31376,18 @@ class WorktreeService {
30135
31376
  title: found.name,
30136
31377
  createdAt: Date.now(),
30137
31378
  agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
31379
+ parked: true,
31380
+ ...message ? { promptedAt: Date.now() } : {},
30138
31381
  ...createdBy ? { createdBy } : {}
30139
31382
  };
30140
31383
  return rec;
30141
31384
  });
30142
31385
  this.invalidateDiscovered();
30143
31386
  this.launch(wt, repo, repo.path, { setupCommands: false });
31387
+ const { agent } = this.d.runtime.ensureAgent(wt);
31388
+ agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
31389
+ if (message)
31390
+ agent.send(message.text, { attachments: message.attachments });
30144
31391
  return wt;
30145
31392
  }
30146
31393
  async ownProcs(worktreeId, names) {
@@ -30283,7 +31530,7 @@ class WorktreeService {
30283
31530
  try {
30284
31531
  await this.removeDirectory(repo, wt);
30285
31532
  } catch (e) {
30286
- rmSync5(this.plansScratch(wt.id), { recursive: true, force: true });
31533
+ rmSync7(this.plansScratch(wt.id), { recursive: true, force: true });
30287
31534
  throw e;
30288
31535
  }
30289
31536
  if (hasOwnBranch(wt)) {
@@ -30297,6 +31544,7 @@ class WorktreeService {
30297
31544
  this.d.state.removeWorktree(worktreeId);
30298
31545
  releasePort(wt.proxyPort);
30299
31546
  let archived = null;
31547
+ await dropTurnRefs(repo.path, worktreeId);
30300
31548
  if (archive)
30301
31549
  archived = this.archiveChat(wt, repo, kept, sessionId, reason);
30302
31550
  else {
@@ -30309,7 +31557,7 @@ class WorktreeService {
30309
31557
  }
30310
31558
  async teardown(wt, repo) {
30311
31559
  const cmds = repo.config.teardown ?? [];
30312
- if (cmds.length === 0 || !existsSync28(wt.path))
31560
+ if (cmds.length === 0 || !existsSync30(wt.path))
30313
31561
  return;
30314
31562
  for (const cmd of cmds) {
30315
31563
  const code = await runSetup(cmd, wt.path, (line) => this.d.hub.emit("log", wt.id, "setup", line), worktreeEnv(wt, repo), { timeoutMs: timeoutFor(repo.config, "setup") });
@@ -30318,7 +31566,7 @@ class WorktreeService {
30318
31566
  }
30319
31567
  }
30320
31568
  async keep(repo, wt) {
30321
- const index = join25(this.d.paths.archiveDir, `${wt.id}.index`);
31569
+ const index = join27(this.d.paths.archiveDir, `${wt.id}.index`);
30322
31570
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
30323
31571
  if (!kept)
30324
31572
  log.warn(wt.id, "could not keep its git state");
@@ -30355,11 +31603,11 @@ class WorktreeService {
30355
31603
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
30356
31604
  }
30357
31605
  plansScratch(worktreeId) {
30358
- return join25(this.d.paths.archiveDir, `${worktreeId}.plans`);
31606
+ return join27(this.d.paths.archiveDir, `${worktreeId}.plans`);
30359
31607
  }
30360
31608
  keepPlans(wt) {
30361
- const from = join25(wt.path, PLANS_DIR);
30362
- if (!existsSync28(from))
31609
+ const from = join27(wt.path, PLANS_DIR);
31610
+ if (!existsSync30(from))
30363
31611
  return;
30364
31612
  try {
30365
31613
  cpSync(from, this.plansScratch(wt.id), { recursive: true });
@@ -30400,8 +31648,8 @@ class WorktreeService {
30400
31648
  deleteChat(worktreeId) {
30401
31649
  const files = this.chatFiles(worktreeId);
30402
31650
  try {
30403
- rmSync5(files.transcript, { force: true });
30404
- rmSync5(files.attachments, { recursive: true, force: true });
31651
+ rmSync7(files.transcript, { force: true });
31652
+ rmSync7(files.attachments, { recursive: true, force: true });
30405
31653
  } catch (e) {
30406
31654
  log.warn(worktreeId, "could not delete transcript or attachments", e);
30407
31655
  }
@@ -30447,7 +31695,7 @@ class WorktreeService {
30447
31695
  }
30448
31696
  archivedAttachment(archiveId, file) {
30449
31697
  const files = this.archive.chatFiles(archiveId);
30450
- return files && isAttachmentFile(file) ? join25(files.attachments, file) : null;
31698
+ return files && isAttachmentFile(file) ? join27(files.attachments, file) : null;
30451
31699
  }
30452
31700
  archivedGit(archiveId) {
30453
31701
  const rec = this.archive.get(archiveId);
@@ -30490,7 +31738,7 @@ class WorktreeService {
30490
31738
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
30491
31739
  if (rec.auto)
30492
31740
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
30493
- const path = existsSync28(old.path) ? join25(dirname11(old.path), freeSlot(dirname11(old.path)).dir) : old.path;
31741
+ const path = existsSync30(old.path) ? join27(dirname11(old.path), freeSlot(dirname11(old.path)).dir) : old.path;
30494
31742
  const branch = await withRepoLock(repo.path, async () => {
30495
31743
  if (!await commitOf(repo.path, archiveRef(old.id))) {
30496
31744
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -30524,10 +31772,11 @@ class WorktreeService {
30524
31772
  branch,
30525
31773
  proxyPort: await allocateProxyPort(),
30526
31774
  promptedAt: Date.now(),
31775
+ parked: true,
30527
31776
  ...createdBy ? { createdBy } : {}
30528
31777
  };
30529
31778
  try {
30530
- this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join25(path, PLANS_DIR) });
31779
+ this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join27(path, PLANS_DIR) });
30531
31780
  } catch (e) {
30532
31781
  log.warn(old.id, "could not move its chat back out of the archive", e);
30533
31782
  }
@@ -30556,12 +31805,12 @@ class WorktreeService {
30556
31805
  return;
30557
31806
  const repo = this.repoOf(rec);
30558
31807
  const repoPath = repo?.path ?? rec.repoPath;
30559
- if (rec.kept && existsSync28(repoPath)) {
31808
+ if (rec.kept && existsSync30(repoPath)) {
30560
31809
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
30561
31810
  if (!r.ok)
30562
31811
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
30563
31812
  }
30564
- if (rec.worktree.lands?.length && existsSync28(repoPath))
31813
+ if (rec.worktree.lands?.length && existsSync30(repoPath))
30565
31814
  await dropLandRefs(repoPath, archiveId);
30566
31815
  this.archive.delete(archiveId);
30567
31816
  this.d.drafts?.drop(archiveId);
@@ -30652,6 +31901,15 @@ class WorktreeService {
30652
31901
  this.d.hub.emit("worktreesChanged");
30653
31902
  return { target, grafted: sources.map((w) => w.title) };
30654
31903
  }
31904
+ startPreview(worktreeId) {
31905
+ const wt = this.d.state.worktree(worktreeId);
31906
+ if (!wt?.parked)
31907
+ return;
31908
+ delete wt.parked;
31909
+ this.d.state.save();
31910
+ this.d.hub.emit("worktreesChanged");
31911
+ fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start a parked preview");
31912
+ }
30655
31913
  async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
30656
31914
  this.d.runtime.markSetup(wt.id, true);
30657
31915
  try {
@@ -30661,9 +31919,9 @@ class WorktreeService {
30661
31919
  }
30662
31920
  }
30663
31921
  async mirrorBaseIgnore(repo) {
30664
- const src = join25(repo.path, ".gitignore");
30665
- const tracked = existsSync28(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
30666
- const mirror = existsSync28(src) && !tracked;
31922
+ const src = join27(repo.path, ".gitignore");
31923
+ const tracked = existsSync30(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
31924
+ const mirror = existsSync30(src) && !tracked;
30667
31925
  const lines = mirror ? readFileSync18(src, "utf8").split(`
30668
31926
  `).filter((l) => l.trim() !== "") : [];
30669
31927
  await excludeBlock(repo.path, BASE_IGNORE, lines);
@@ -30676,9 +31934,9 @@ class WorktreeService {
30676
31934
  await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
30677
31935
  run.finish(0);
30678
31936
  }
30679
- const srcNm = join25(depsSource, "node_modules");
30680
- const dstNm = join25(wt.path, "node_modules");
30681
- if (existsSync28(srcNm) && !existsSync28(dstNm)) {
31937
+ const srcNm = join27(depsSource, "node_modules");
31938
+ const dstNm = join27(wt.path, "node_modules");
31939
+ if (existsSync30(srcNm) && !existsSync30(dstNm)) {
30682
31940
  const r = await cloneTree(srcNm, dstNm);
30683
31941
  if (r.ok)
30684
31942
  this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
@@ -30686,10 +31944,10 @@ class WorktreeService {
30686
31944
  log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
30687
31945
  }
30688
31946
  for (const f of LOCAL_CONFIG_FILES) {
30689
- const src = join25(depsSource, f);
30690
- if (!existsSync28(src) || existsSync28(join25(wt.path, f)))
31947
+ const src = join27(depsSource, f);
31948
+ if (!existsSync30(src) || existsSync30(join27(wt.path, f)))
30691
31949
  continue;
30692
- const r = await run("cp", [src, join25(wt.path, f)], wt.path);
31950
+ const r = await run("cp", [src, join27(wt.path, f)], wt.path);
30693
31951
  if (r.ok)
30694
31952
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
30695
31953
  else
@@ -30820,13 +32078,15 @@ ${wt.landing.body}` : s;
30820
32078
  const handle = commit ? this.runs.begin(wt.id, "commit", { timeoutMs: ceiling }) : null;
30821
32079
  const run = (onText, signal) => stepRun(cwd, args, onText, signal, {
30822
32080
  timeoutMs: ceiling,
30823
- ...handle ? { onSpawn: handle.spawned, onHook: (hook) => handle.stage(hook ? `${hook} hook` : undefined) } : {}
32081
+ ...handle ? { onSpawn: handle.spawned } : {},
32082
+ onHook: (hook) => handle?.stage(hook ? `${hook} hook` : undefined)
30824
32083
  });
30825
32084
  const watched = async () => {
30826
32085
  if (!this.d.watch)
30827
32086
  return run(() => {});
32087
+ const command = `${where} ${commandLine(args)}`;
30828
32088
  try {
30829
- return await this.d.watch(wt.id, `${where} ${commandLine(args)}`, run);
32089
+ return { ...await this.d.watch(wt.id, command, run), command };
30830
32090
  } catch (e) {
30831
32091
  log.warn(wt.id, `landing step runs unwatched: ${e instanceof Error ? e.message : e}`);
30832
32092
  return run(() => {});
@@ -30864,7 +32124,35 @@ ${wt.landing.body}` : s;
30864
32124
  });
30865
32125
  if (out.result.ok && out.archiveIds)
30866
32126
  this.noteLanded(wt, out.result.message, out.archiveIds);
30867
- return out;
32127
+ return { ...out, result: this.fixShip(wt, out.result, baseOf(repo)) };
32128
+ }
32129
+ fixShip(wt, result, base) {
32130
+ if (result.ok)
32131
+ return result;
32132
+ if (result.hook?.name === "commit-msg") {
32133
+ this.d.hub.emit("messageRefused", wt.id, result.hook.text.trim().slice(-REFUSAL_CHARS));
32134
+ return { ...result, message: "the commit-msg hook refused the message: another is being written" };
32135
+ }
32136
+ const { hook } = result;
32137
+ const failure = hook ? { kind: "hook", hook: hook.name, toolId: hook.toolId, command: hook.command, text: hook.text } : result.conflict ? { kind: "conflict", base, how: result.conflict, ...result.step ? { step: result.step } : {} } : null;
32138
+ return failure && this.d.fix.report(wt.id, failure) ? { ...result, asked: true } : result;
32139
+ }
32140
+ async fixPreview(worktreeId, clientId) {
32141
+ const wt = this.d.state.requireWorktree(worktreeId);
32142
+ const failure = {
32143
+ kind: "preview",
32144
+ procs: this.d.runtime.get(worktreeId)?.procs?.states() ?? [],
32145
+ log: this.d.runtime.recentLogs(worktreeId)
32146
+ };
32147
+ if (!isLead(wt)) {
32148
+ if (!this.d.fix.report(worktreeId, failure, { pressed: true }))
32149
+ throw new UserError("worktree still starting; try again in a moment");
32150
+ return;
32151
+ }
32152
+ await this.create(wt.repoId, fixPrompt(failure), {
32153
+ ...clientId ? { createdBy: clientId } : {},
32154
+ ...isProvisional(wt) ? { worktreeId } : {}
32155
+ });
30868
32156
  }
30869
32157
  async movedMidTurn(wt) {
30870
32158
  const status = this.d.runtime.agentFor(wt.id)?.status;
@@ -30993,11 +32281,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
30993
32281
  if (committed && !committed.ok)
30994
32282
  return committed;
30995
32283
  committedHere = committed !== null;
32284
+ const method = policy.merge ?? DEFAULT_MERGE_METHOD;
32285
+ const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
32286
+ if (merged)
32287
+ return merged;
30996
32288
  const taken = await takeMainIn(wt.path, base, own, w);
30997
32289
  if (!taken.ok)
30998
32290
  return taken;
30999
32291
  const mark = await landingMark(wt.path, base);
31000
- const method = policy.merge ?? DEFAULT_MERGE_METHOD;
31001
32292
  const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
31002
32293
  const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
31003
32294
  if (!landed.ok)
@@ -31028,6 +32319,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31028
32319
  return { result: { ok: false, message: `nothing to land: no commits ahead of ${base}` } };
31029
32320
  let sha = "";
31030
32321
  let already = false;
32322
+ const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
32323
+ if (merged)
32324
+ return { result: merged };
31031
32325
  for (let attempt = 0;; attempt++) {
31032
32326
  const fetched = await this.fetchBase(wt, repo, w);
31033
32327
  if (!fetched.ok)
@@ -31097,7 +32391,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31097
32391
  async fetchBase(wt, repo, w) {
31098
32392
  if (!baseIsRemote(repo))
31099
32393
  return { ok: true, message: `${baseOf(repo)} here is the base` };
31100
- const r = await fetchBase(wt.path, repo, w);
32394
+ const r = await withFetchLock(repo.path, () => fetchBase(wt.path, repo, w));
31101
32395
  this.trunk.noteFetch(repo.id, { ok: r.ok, err: r.message });
31102
32396
  return r;
31103
32397
  }
@@ -31165,7 +32459,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31165
32459
  }
31166
32460
  return taken;
31167
32461
  });
31168
- return { result, base: r.base };
32462
+ return { result: r.wt ? this.fixShip(r.wt, result, r.base) : result, base: r.base };
31169
32463
  }
31170
32464
  async pull(worktreeId) {
31171
32465
  const wt = this.d.state.requireWorktree(worktreeId);
@@ -31187,7 +32481,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31187
32481
  const m = message.trim();
31188
32482
  if (!m)
31189
32483
  throw new UserError("commit message required");
31190
- return this.ship(wt.id, "commit", () => this.commitRecorded(wt, m));
32484
+ const base = baseOf(this.d.state.requireRepo(wt.repoId));
32485
+ return this.fixShip(wt, await this.ship(wt.id, "commit", () => this.commitRecorded(wt, m)), base);
31191
32486
  }
31192
32487
  setEmpty(wt, empty) {
31193
32488
  if (wt.empty === empty)
@@ -31333,6 +32628,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31333
32628
  base: baseOf(repo)
31334
32629
  };
31335
32630
  }
32631
+ async turnStarted(worktreeId) {
32632
+ const wt = this.d.state.worktree(worktreeId);
32633
+ if (wt && !this.going.has(worktreeId))
32634
+ await markTurn(wt.path, wt.id);
32635
+ }
31336
32636
  async gitStatus(worktreeId) {
31337
32637
  if (this.going.has(worktreeId))
31338
32638
  return null;
@@ -31353,6 +32653,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31353
32653
  const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
31354
32654
  const ahead = counts.ahead ?? 0;
31355
32655
  const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.base) : undefined;
32656
+ const turn = r.wt && files.length > 0 ? await turnChanges(r.path, r.wt.id, r.base, !isMain, files) : null;
31356
32657
  if (r.wt && !isMain && hasOwnBranch(r.wt) && files.length === 0 && ahead === 0 && head.ok && this.handChecked.get(r.wt.id) !== head.out) {
31357
32658
  this.handChecked.set(r.wt.id, head.out);
31358
32659
  const mark = await handLanding(r.path, r.wt.branch, r.base, r.wt.lands?.at(-1)?.tip);
@@ -31379,7 +32680,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31379
32680
  if (this.going.has(worktreeId))
31380
32681
  return null;
31381
32682
  this.noteCounts(worktreeId, isMain, files.length, counts);
31382
- return { files, committed, head: head.ok ? head.out : undefined, ...counts };
32683
+ return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
31383
32684
  } catch (e) {
31384
32685
  log.warn(worktreeId, "git status failed", e);
31385
32686
  return null;
@@ -31648,7 +32949,8 @@ var accounts = new AgentAccounts({
31648
32949
  connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `auth:${spec.id}`)
31649
32950
  });
31650
32951
  accounts.onChange = () => hub.emit("agentsChanged");
31651
- var attachments2 = new AttachmentStore(paths.attachmentsDir);
32952
+ var uploads = new UploadStore(paths.uploadsDir);
32953
+ var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
31652
32954
  var runtime = new RuntimeRegistry({
31653
32955
  hub,
31654
32956
  state,
@@ -31658,16 +32960,23 @@ var runtime = new RuntimeRegistry({
31658
32960
  accounts,
31659
32961
  attachments: attachments2,
31660
32962
  bridgeScript: () => bridge.get(),
32963
+ turnStarting: (id) => worktrees.turnStarted(id),
31661
32964
  remote,
31662
32965
  grant: previewGrant(token),
31663
32966
  viewed: (id) => idle.isViewed(id),
31664
32967
  shown: (id) => idle.isShown(id),
31665
32968
  mainLeads: (repoId) => worktrees.spare.current(repoId) === null
31666
32969
  });
31667
- var drafts = new DraftStore({ file: paths.draftsFile, hub });
32970
+ var drafts = new DraftStore({
32971
+ file: paths.draftsFile,
32972
+ attachmentsFile: paths.draftAttachmentsFile,
32973
+ hub,
32974
+ uploads
32975
+ });
31668
32976
  var runs = new RunService({ state, hub });
31669
32977
  var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
31670
32978
  var cache = new ArtifactCache({ paths, state, hub });
32979
+ var fix = new FixService({ state, hub, runtime });
31671
32980
  var worktrees = new WorktreeService({
31672
32981
  state,
31673
32982
  hub,
@@ -31677,7 +32986,8 @@ var worktrees = new WorktreeService({
31677
32986
  drafts,
31678
32987
  cache,
31679
32988
  watch: (id, command, run) => exec.watch(id, command, run),
31680
- runs
32989
+ runs,
32990
+ fix
31681
32991
  });
31682
32992
  new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
31683
32993
  var turns = new TurnService({
@@ -31777,6 +33087,13 @@ var idleExit = new IdleExit({
31777
33087
  },
31778
33088
  afterMs: stopAfter
31779
33089
  });
33090
+ var keepAwake = new KeepAwake({
33091
+ hub,
33092
+ demand: () => runtime.demand(),
33093
+ assert: idleSleepAssertion(process.env.TOYON_KEEP_AWAKE),
33094
+ mode: () => state.keepAwake,
33095
+ answerable: remote !== null
33096
+ });
31780
33097
  var { branded, stop: stopServer } = startServer({
31781
33098
  port,
31782
33099
  token,
@@ -31798,6 +33115,7 @@ var { branded, stop: stopServer } = startServer({
31798
33115
  runtime,
31799
33116
  idle,
31800
33117
  idleExit,
33118
+ keepAwake,
31801
33119
  exec,
31802
33120
  runs,
31803
33121
  refs,
@@ -31805,6 +33123,7 @@ var { branded, stop: stopServer } = startServer({
31805
33123
  drafts,
31806
33124
  prs,
31807
33125
  landing,
33126
+ fix,
31808
33127
  themes,
31809
33128
  agents,
31810
33129
  accounts,
@@ -31834,6 +33153,7 @@ var stopLagSampler = startLagSampler();
31834
33153
  await runtime.reclaimOrphans();
31835
33154
  await repos.boot();
31836
33155
  drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
33156
+ uploads.sweep(drafts.uploadIds());
31837
33157
  idle.boot();
31838
33158
  runs.boot();
31839
33159
  landing.boot();
@@ -31876,6 +33196,7 @@ async function shutdown(signal, opts = {}) {
31876
33196
  shuttingDown = true;
31877
33197
  log.info("daemon", `${signal}: stopping dev servers`);
31878
33198
  idleExit.stop();
33199
+ keepAwake.stop();
31879
33200
  idle.shutdown();
31880
33201
  landing.stop();
31881
33202
  runs.stopWatching();
@@ -31889,7 +33210,7 @@ async function shutdown(signal, opts = {}) {
31889
33210
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
31890
33211
  await Promise.race([Promise.all([runtime.shutdown(), exec.stopAll()]), deadline]);
31891
33212
  state.flushGroups();
31892
- rmSync6(paths.pidFile, { force: true });
33213
+ rmSync8(paths.pidFile, { force: true });
31893
33214
  if (opts.respawn) {
31894
33215
  await Bun.sleep(250);
31895
33216
  log.info("daemon", "starting the replacement");