toyon 0.8.1 → 0.9.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 +6 -3
  3. package/dist/daemon.js +1124 -394
  4. package/dist/shell/assets/{Editor-CrfFlcLA.js → Editor-DAJni9OF.js} +137 -137
  5. package/dist/shell/assets/{XTerm-CsuyFSMK.js → XTerm-dug6M6yd.js} +16 -16
  6. package/dist/shell/assets/{cssMode-CWkICf31.js → cssMode-B5iiwdXn.js} +1 -1
  7. package/dist/shell/assets/{freemarker2-C2fP12sE.js → freemarker2-CdVcBNE9.js} +1 -1
  8. package/dist/shell/assets/{grammar-5S_Kl1e8.js → grammar-Cfxx1yRn.js} +5 -5
  9. package/dist/shell/assets/{handlebars-CkRyqZGC.js → handlebars-CW5EFZC2.js} +1 -1
  10. package/dist/shell/assets/{html-C9iJKWKA.js → html-B8gP1xu-.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-Dps-6NJd.js → htmlMode-C6I99y8u.js} +1 -1
  12. package/dist/shell/assets/index-BRSL9cyD.css +1 -0
  13. package/dist/shell/assets/index-D_PsyNmp.js +266 -0
  14. package/dist/shell/assets/{javascript-DM5EiBY7.js → javascript-C_3_LFln.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-ampRCTsW.js → jsonMode-CD5xJEUk.js} +1 -1
  16. package/dist/shell/assets/{liquid-CEUat1ZS.js → liquid-CJw93Zxn.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-ChupARNW.js → lspLanguageFeatures-wMR368iN.js} +1 -1
  18. package/dist/shell/assets/{mdx-DAglTq4w.js → mdx-BJ6yMgc3.js} +1 -1
  19. package/dist/shell/assets/{python-DOPtAG-1.js → python-BWGkxFUr.js} +1 -1
  20. package/dist/shell/assets/{razor-qRDOBjWc.js → razor-BV9pdU1A.js} +1 -1
  21. package/dist/shell/assets/{tsMode-BD49NPBt.js → tsMode-CN8OfYAX.js} +1 -1
  22. package/dist/shell/assets/{xml-p5Js1H7t.js → xml-DBGLqvw7.js} +1 -1
  23. package/dist/shell/assets/{yaml-CctH9qfh.js → yaml-dH0C6Dth.js} +1 -1
  24. package/dist/shell/index.html +21 -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
 
@@ -120,8 +120,9 @@ function archiveReason(wt, f) {
120
120
  if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
121
121
  return null;
122
122
  const what = lost ? "a sibling landed" : wt.landed ? "landed" : "no changes";
123
- return `${what}, not opened in ${duration(f.afterMs)}`;
123
+ return `${what}, ${UNOPENED}${duration(f.afterMs)}`;
124
124
  }
125
+ var UNOPENED = "not opened in ";
125
126
  function nothingToKeep(w, f, lost) {
126
127
  if (w.kind !== "worktree" || !hasOwnBranch(w))
127
128
  return false;
@@ -157,22 +158,30 @@ function duration(ms) {
157
158
  }
158
159
 
159
160
  // ../shared/src/attachment.ts
160
- var ATTACHMENT_KINDS = ["image", "paste", "pick"];
161
- var ATTACHMENT_LIMITS = { image: 6, paste: 4, pick: 8 };
161
+ var ATTACHMENT_KINDS = ["image", "paste", "file", "pick"];
162
+ var ATTACHMENT_LIMITS = { image: 6, paste: 4, file: 8, pick: 8 };
162
163
  var ATTACHMENTS_PER_MESSAGE = ATTACHMENT_KINDS.reduce((sum, k) => sum + ATTACHMENT_LIMITS[k], 0);
163
- var KIND_NOUN = { image: "image", paste: "paste", pick: "element" };
164
+ var KIND_NOUN = {
165
+ image: "image",
166
+ paste: "paste",
167
+ file: "file",
168
+ pick: "element"
169
+ };
164
170
  var KIND_LABEL = { image: "Image", paste: "Pasted text" };
165
171
  var attachmentLabel = (kind, n) => `${KIND_LABEL[kind]} ${n}`;
166
172
  var limitMessage = (kind) => `at most ${ATTACHMENT_LIMITS[kind]} ${KIND_NOUN[kind]}s per message`;
167
173
  var countOf = (list, kind) => list.filter((a) => a.kind === kind).length;
168
174
  var overLimit = (list) => ATTACHMENT_KINDS.find((k) => countOf(list, k) > ATTACHMENT_LIMITS[k]) ?? null;
169
175
  function nextNumbers(sent) {
170
- const next = { image: 1, paste: 1, pick: 1 };
176
+ const next = { image: 1, paste: 1, file: 1, pick: 1 };
171
177
  for (const list of sent)
172
178
  for (const a of list ?? [])
173
179
  next[a.kind] = Math.max(next[a.kind], a.n + 1);
174
180
  return next;
175
181
  }
182
+ function fmtBytes(n) {
183
+ return n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`;
184
+ }
176
185
 
177
186
  // ../shared/src/cache.ts
178
187
  function cachePolicy(config) {
@@ -6469,12 +6478,15 @@ var PAIR_TTL_MS = 120000;
6469
6478
  var pairLink = (host, code) => `https://${host}/#pair=${code}`;
6470
6479
 
6471
6480
  // ../shared/src/protocol/limits.ts
6472
- var PROTOCOL_VERSION = 83;
6481
+ var PROTOCOL_VERSION = 85;
6473
6482
  var FILE_MAX_CHARS = 5000000;
6474
6483
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
6475
6484
  var IMAGE_MAX_EDGE = 2576;
6476
6485
  var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
6477
6486
  var PASTE_MAX_CHARS = 1e5;
6487
+ var FILE_INLINE_CHARS = 20000;
6488
+ var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
6489
+ var VARIANTS_MAX = 3;
6478
6490
 
6479
6491
  // ../shared/src/paste.ts
6480
6492
  var PREVIEW_CHARS = 80;
@@ -6559,11 +6571,13 @@ var termInput = string2().max(65536);
6559
6571
  var streamName = string2().min(1).max(100);
6560
6572
  var routePath = string2().min(1).max(2000);
6561
6573
  var pageTitle = string2().max(1000);
6574
+ var uploadId = string2().regex(/^[A-Za-z0-9_-]{1,64}$/);
6562
6575
  var imageInputSchema = object({
6563
6576
  kind: literal("image"),
6564
6577
  name: string2().max(200),
6565
6578
  mimeType: _enum(IMAGE_MIME_TYPES),
6566
- data: string2().max(Math.ceil(IMAGE_MAX_BYTES * 4 / 3 / 4) * 4),
6579
+ upload: uploadId,
6580
+ bytes: number2().int().min(1).max(IMAGE_MAX_BYTES),
6567
6581
  width: number2().int().min(1).max(IMAGE_MAX_EDGE),
6568
6582
  height: number2().int().min(1).max(IMAGE_MAX_EDGE)
6569
6583
  });
@@ -6579,6 +6593,13 @@ var pasteInputSchema = object({
6579
6593
  name: string2().max(200).optional(),
6580
6594
  source: pasteSourceSchema.optional()
6581
6595
  });
6596
+ var fileInputSchema = object({
6597
+ kind: literal("file"),
6598
+ upload: uploadId,
6599
+ name: string2().min(1).max(200),
6600
+ bytes: number2().int().min(1).max(UPLOAD_MAX_BYTES),
6601
+ text: boolean2()
6602
+ });
6582
6603
  var pickInputSchema = pickMetaSchema.extend({
6583
6604
  kind: literal("pick"),
6584
6605
  text: string2().max(120),
@@ -6587,13 +6608,16 @@ var pickInputSchema = pickMetaSchema.extend({
6587
6608
  var attachmentInputSchema = discriminatedUnion("kind", [
6588
6609
  imageInputSchema,
6589
6610
  pasteInputSchema,
6611
+ fileInputSchema,
6590
6612
  pickInputSchema
6591
6613
  ]);
6592
- var attachments = array(attachmentInputSchema).max(ATTACHMENTS_PER_MESSAGE).superRefine((list, ctx) => {
6614
+ var attachmentList = array(attachmentInputSchema).max(ATTACHMENTS_PER_MESSAGE).superRefine((list, ctx) => {
6593
6615
  const over = overLimit(list);
6594
6616
  if (over)
6595
6617
  ctx.addIssue({ code: "custom", message: limitMessage(over) });
6596
- }).optional();
6618
+ });
6619
+ var attachments = attachmentList.optional();
6620
+ var boxId = id.optional();
6597
6621
  var askAnswerSchema = object({
6598
6622
  selected: array(string2().max(2000)).max(32),
6599
6623
  note: string2().max(1e4).optional()
@@ -6702,7 +6726,6 @@ var themePrefsSchema = object({
6702
6726
  light: string2(),
6703
6727
  dark: string2()
6704
6728
  });
6705
- var variantSchema = object({ group: string2(), index: number2().int().min(1), of: number2().int().min(1) });
6706
6729
  var clientMsgSchema = discriminatedUnion("t", [
6707
6730
  object({ t: literal("subscribe"), worktreeId: id }),
6708
6731
  object({ t: literal("unsubscribe"), worktreeId: id }),
@@ -6713,7 +6736,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6713
6736
  clientId: string2().max(64).optional(),
6714
6737
  text: prose,
6715
6738
  context: ambient.optional(),
6716
- attachments
6739
+ attachments,
6740
+ boxId
6717
6741
  }),
6718
6742
  object({
6719
6743
  t: literal("create-worktree"),
@@ -6721,7 +6745,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6721
6745
  repoId: id,
6722
6746
  worktreeId: id.optional(),
6723
6747
  prompt,
6724
- variant: variantSchema.optional(),
6748
+ variants: number2().int().min(1).max(VARIANTS_MAX).optional(),
6725
6749
  context: ambient.optional(),
6726
6750
  attachments,
6727
6751
  agent: id.optional(),
@@ -6729,7 +6753,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6729
6753
  mode: permissionModeSchema.optional(),
6730
6754
  model: string2().max(200).optional(),
6731
6755
  effort: string2().max(100).optional(),
6732
- carry: boolean2().optional()
6756
+ carry: boolean2().optional(),
6757
+ boxId
6733
6758
  }),
6734
6759
  object({
6735
6760
  t: literal("batch-worktrees"),
@@ -6737,7 +6762,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6737
6762
  prompt,
6738
6763
  agent: id.optional(),
6739
6764
  model: string2().max(200).optional(),
6740
- effort: string2().max(100).optional()
6765
+ effort: string2().max(100).optional(),
6766
+ boxId
6741
6767
  }),
6742
6768
  object({ t: literal("set-worktree-mode"), worktreeId: id, mode: permissionModeSchema }),
6743
6769
  object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
@@ -6751,7 +6777,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6751
6777
  t: literal("restore-worktree"),
6752
6778
  archiveId: id,
6753
6779
  clientId: string2().max(64).optional(),
6754
- message: object({ text: prose, attachments }).optional()
6780
+ message: object({ text: prose, attachments }).optional(),
6781
+ boxId
6755
6782
  }),
6756
6783
  object({ t: literal("delete-archived"), archiveId: id }),
6757
6784
  object({
@@ -6760,7 +6787,14 @@ var clientMsgSchema = discriminatedUnion("t", [
6760
6787
  worktreeId: id
6761
6788
  }),
6762
6789
  object({ t: literal("git-status"), worktreeId: id }),
6763
- object({ t: literal("read-file"), worktreeId: id, path: relPath, ref: sha.optional(), seq }),
6790
+ object({
6791
+ t: literal("read-file"),
6792
+ worktreeId: id,
6793
+ path: relPath,
6794
+ ref: sha.optional(),
6795
+ since: sha.optional(),
6796
+ seq
6797
+ }),
6764
6798
  object({ t: literal("blame-file"), worktreeId: id, path: relPath, ref: sha.optional(), seq }),
6765
6799
  object({ t: literal("git-log"), worktreeId: id }),
6766
6800
  object({ t: literal("git-commit"), worktreeId: id, sha }),
@@ -6801,12 +6835,24 @@ var clientMsgSchema = discriminatedUnion("t", [
6801
6835
  object({ t: literal("seen"), worktreeId: id }),
6802
6836
  object({ t: literal("mark-unread"), worktreeId: id }),
6803
6837
  object({ t: literal("set-draft"), boxId: id, text: prose, clientId: string2().max(64).optional() }),
6838
+ object({
6839
+ t: literal("set-attachments"),
6840
+ boxId: id,
6841
+ items: attachmentList,
6842
+ clientId: string2().max(64).optional()
6843
+ }),
6804
6844
  object({ t: literal("refresh-git"), repoId: id }),
6805
6845
  object({ t: literal("visit"), worktreeId: id, path: routePath, title: pageTitle.optional() }),
6806
6846
  object({ t: literal("page-title"), worktreeId: id, path: routePath, title: pageTitle }),
6807
6847
  object({ t: literal("forget-visit"), repoId: id, path: routePath }),
6808
6848
  object({ t: literal("pick-variant"), worktreeId: id }),
6809
- object({ t: literal("unqueue"), worktreeId: id, index: number2().int().min(0) }),
6849
+ object({
6850
+ t: literal("unqueue"),
6851
+ worktreeId: id,
6852
+ index: number2().int().min(0),
6853
+ edit: boolean2().optional()
6854
+ }),
6855
+ object({ t: literal("redraft"), worktreeId: id, seq: number2().int().min(0) }),
6810
6856
  object({ t: literal("changed-ranges"), worktreeId: id, path: relPath }),
6811
6857
  object({ t: literal("rename-worktree"), worktreeId: id, title: string2().min(1).max(200) }),
6812
6858
  object({
@@ -6870,6 +6916,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6870
6916
  object({ t: literal("exec"), worktreeId: id, command: shellCommand.min(1) }),
6871
6917
  object({ t: literal("exec-stop"), worktreeId: id, toolId: id }),
6872
6918
  object({ t: literal("run-stop"), worktreeId: id, kind: _enum(["setup", "check", "commit"]) }),
6919
+ object({ t: literal("preview-start"), worktreeId: id }),
6873
6920
  object({ t: literal("search-refs"), repoId: id, query: string2().max(200) }),
6874
6921
  object({ t: literal("search-chats"), repoId: id, query: string2().max(200) }),
6875
6922
  object({
@@ -8154,7 +8201,7 @@ function safeUser() {
8154
8201
  // ../daemon/package.json
8155
8202
  var package_default = {
8156
8203
  name: "@toyon/daemon",
8157
- version: "0.8.1",
8204
+ version: "0.9.0",
8158
8205
  private: true,
8159
8206
  type: "module",
8160
8207
  main: "src/index.ts",
@@ -12248,9 +12295,11 @@ var legacyClientNotificationMethods = new Set([
12248
12295
 
12249
12296
  // ../daemon/src/core/errors.ts
12250
12297
  class UserError extends Error {
12251
- constructor(message) {
12298
+ delivered;
12299
+ constructor(message, about = {}) {
12252
12300
  super(message);
12253
12301
  this.name = "UserError";
12302
+ this.delivered = about.delivered ?? false;
12254
12303
  }
12255
12304
  }
12256
12305
 
@@ -12503,18 +12552,177 @@ function spawnAcp(app, launch, cwd, tag) {
12503
12552
 
12504
12553
  // ../daemon/src/agent/attachments.ts
12505
12554
  import { createHash } from "crypto";
12506
- import { mkdir, writeFile } from "fs/promises";
12555
+ import { copyFile as copyFile2, mkdir, readFile, stat, writeFile } from "fs/promises";
12556
+ import { join as join2 } from "path";
12557
+
12558
+ // ../daemon/src/agent/uploads.ts
12559
+ import { randomBytes } from "crypto";
12560
+ import { mkdirSync, readdirSync, rmSync } from "fs";
12561
+ import { copyFile } from "fs/promises";
12507
12562
  import { join } from "path";
12508
- var EXT = {
12563
+ var IMAGE_EXT = {
12509
12564
  "image/png": "png",
12510
12565
  "image/jpeg": "jpg",
12511
12566
  "image/gif": "gif",
12512
12567
  "image/webp": "webp"
12513
12568
  };
12569
+ var MIME_OF_EXT = new Map(IMAGE_MIME_TYPES.map((mime) => [IMAGE_EXT[mime], mime]));
12570
+ var OTHER_EXT = "bin";
12571
+ var NAME = /^([A-Za-z0-9_-]{1,64})\.(png|jpg|gif|webp|bin)$/;
12572
+ var UNNAMED_MS = 10 * 60000;
12573
+ var GONE = "an attachment is no longer on this machine; attach it again";
12574
+ var isUploadKind = (kind) => kind === "image" || kind === "file";
12575
+ function uploadIds(items) {
12576
+ return (items ?? []).flatMap((a) => a.kind === "image" || a.kind === "file" ? [a.upload] : []);
12577
+ }
12578
+
12579
+ class UploadStore {
12580
+ dir;
12581
+ unnamedMs;
12582
+ files = new Map;
12583
+ holds = new Map;
12584
+ named = new Set;
12585
+ constructor(dir, unnamedMs = UNNAMED_MS) {
12586
+ this.dir = dir;
12587
+ this.unnamedMs = unnamedMs;
12588
+ mkdirSync(dir, { recursive: true });
12589
+ for (const file of readdirSync(dir)) {
12590
+ const id = NAME.exec(file)?.[1];
12591
+ if (id)
12592
+ this.files.set(id, file);
12593
+ }
12594
+ }
12595
+ async put(kind, mime, body) {
12596
+ const image = IMAGE_EXT[mime];
12597
+ if (kind === "image" && !image)
12598
+ throw new UserError("not an image format the models accept");
12599
+ const cap = kind === "image" ? IMAGE_MAX_BYTES : UPLOAD_MAX_BYTES;
12600
+ const upload = randomBytes(12).toString("base64url");
12601
+ const file = `${upload}.${kind === "image" ? image : OTHER_EXT}`;
12602
+ const path = join(this.dir, file);
12603
+ const writer = Bun.file(path).writer();
12604
+ let decoder = kind === "file" ? new TextDecoder("utf-8", { fatal: true }) : null;
12605
+ let bytes = 0;
12606
+ try {
12607
+ for await (const chunk of body ?? []) {
12608
+ bytes += chunk.byteLength;
12609
+ if (bytes > cap)
12610
+ throw new UserError(`larger than ${fmtBytes(cap)}`);
12611
+ writer.write(chunk);
12612
+ await writer.flush();
12613
+ if (decoder && (bytes > FILE_MAX_CHARS || !decodes(decoder, chunk)))
12614
+ decoder = null;
12615
+ }
12616
+ if (bytes === 0)
12617
+ throw new UserError("empty file");
12618
+ await writer.end();
12619
+ } catch (e) {
12620
+ await Promise.resolve(writer.end()).catch(() => {});
12621
+ rmSync(path, { force: true });
12622
+ throw e;
12623
+ }
12624
+ this.files.set(upload, file);
12625
+ this.expireUnnamed(upload);
12626
+ return { upload, bytes, text: decoder !== null };
12627
+ }
12628
+ async adopt(from, image) {
12629
+ const upload = randomBytes(12).toString("base64url");
12630
+ const file = `${upload}.${image ? IMAGE_EXT[image] : OTHER_EXT}`;
12631
+ await copyFile(from, join(this.dir, file));
12632
+ this.files.set(upload, file);
12633
+ this.expireUnnamed(upload);
12634
+ return upload;
12635
+ }
12636
+ expireUnnamed(id) {
12637
+ const timer = setTimeout(() => {
12638
+ if (!this.named.has(id) && !this.holds.has(id))
12639
+ this.remove(id);
12640
+ }, this.unnamedMs);
12641
+ timer.unref?.();
12642
+ }
12643
+ path(id) {
12644
+ const file = this.files.get(id);
12645
+ return file ? join(this.dir, file) : null;
12646
+ }
12647
+ imageType(id) {
12648
+ const file = this.files.get(id);
12649
+ return file && MIME_OF_EXT.get(file.slice(file.lastIndexOf(".") + 1)) || null;
12650
+ }
12651
+ has(id) {
12652
+ return this.files.has(id);
12653
+ }
12654
+ hold(ids) {
12655
+ if (ids.some((id) => !this.files.has(id)))
12656
+ throw new UserError(GONE);
12657
+ for (const id of ids)
12658
+ this.holds.set(id, (this.holds.get(id) ?? 0) + 1);
12659
+ }
12660
+ release(ids) {
12661
+ for (const id of ids) {
12662
+ const left = (this.holds.get(id) ?? 0) - 1;
12663
+ if (left > 0) {
12664
+ this.holds.set(id, left);
12665
+ continue;
12666
+ }
12667
+ this.holds.delete(id);
12668
+ if (!this.named.has(id))
12669
+ this.remove(id);
12670
+ }
12671
+ }
12672
+ keep(named) {
12673
+ const before = this.named;
12674
+ this.named = new Set(named);
12675
+ for (const id of before)
12676
+ if (!this.named.has(id) && !this.holds.has(id))
12677
+ this.remove(id);
12678
+ }
12679
+ sweep(named) {
12680
+ this.named = new Set(named);
12681
+ for (const id of [...this.files.keys()])
12682
+ if (!this.named.has(id))
12683
+ this.remove(id);
12684
+ }
12685
+ remove(id) {
12686
+ const file = this.files.get(id);
12687
+ if (!file)
12688
+ return;
12689
+ this.files.delete(id);
12690
+ try {
12691
+ rmSync(join(this.dir, file), { force: true });
12692
+ } catch (e) {
12693
+ log.warn("uploads", `could not delete ${file}`, e);
12694
+ }
12695
+ }
12696
+ }
12697
+ function decodes(decoder, chunk) {
12698
+ try {
12699
+ decoder.decode(chunk, { stream: true });
12700
+ return !chunk.includes(0);
12701
+ } catch {
12702
+ return false;
12703
+ }
12704
+ }
12705
+
12706
+ // ../daemon/src/agent/attachments.ts
12514
12707
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
12515
- var FILE = /^(\d{1,6}|[0-9a-f]{16})\.(png|jpg|gif|webp|txt)$/;
12708
+ var NAME_MAX = 80;
12709
+ var DRAWN = /^(?:\d{1,6}|[0-9a-f]{16})\.(png|jpg|gif|webp|txt)$/;
12710
+ var NAMED = new RegExp(`^\\d{1,6}-[A-Za-z0-9._-]{1,${NAME_MAX}}$`);
12711
+ var isFile = (file) => DRAWN.test(file) || NAMED.test(file);
12712
+ var MIME = { png: "image/png", jpg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
12713
+ function drawnType(file) {
12714
+ return MIME[DRAWN.exec(file)?.[1] ?? ""] ?? null;
12715
+ }
12716
+ function storedName(name) {
12717
+ const safe = name.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "") || "file";
12718
+ if (safe.length <= NAME_MAX)
12719
+ return safe;
12720
+ const dot = safe.lastIndexOf(".");
12721
+ const ext = dot > 0 && safe.length - dot <= 16 ? safe.slice(dot) : "";
12722
+ return safe.slice(0, NAME_MAX - ext.length) + ext;
12723
+ }
12516
12724
  function toolImage(data, mimeType) {
12517
- const ext = EXT[mimeType];
12725
+ const ext = IMAGE_EXT[mimeType];
12518
12726
  if (!ext)
12519
12727
  return null;
12520
12728
  const bytes = Buffer.from(data, "base64");
@@ -12523,27 +12731,40 @@ function toolImage(data, mimeType) {
12523
12731
  const file = `${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}.${ext}`;
12524
12732
  return { ref: { file, mimeType, bytes: bytes.length }, bytes };
12525
12733
  }
12526
- var isAttachmentFile = (file) => FILE.test(file);
12734
+ var isAttachmentFile = (file) => isFile(file);
12527
12735
  function attachmentsDirFor(attachmentsDir, worktreeId) {
12528
- return join(attachmentsDir, worktreeId);
12736
+ return join2(attachmentsDir, worktreeId);
12737
+ }
12738
+ var GONE2 = "the upload is gone; attach it again";
12739
+ async function shortText(path, bytes) {
12740
+ if (bytes > FILE_INLINE_CHARS * 4)
12741
+ return null;
12742
+ try {
12743
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(await readFile(path));
12744
+ return text.length <= FILE_INLINE_CHARS ? text : null;
12745
+ } catch {
12746
+ return null;
12747
+ }
12529
12748
  }
12530
12749
 
12531
12750
  class AttachmentStore {
12532
12751
  dir;
12752
+ uploads;
12533
12753
  pending = new Map;
12534
- constructor(dir) {
12754
+ constructor(dir, uploads) {
12535
12755
  this.dir = dir;
12756
+ this.uploads = uploads;
12536
12757
  }
12537
12758
  async putToolImage(worktreeId, file, bytes) {
12538
- if (!ID.test(worktreeId) || !FILE.test(file))
12759
+ if (!ID.test(worktreeId) || !DRAWN.test(file))
12539
12760
  throw new UserError("bad tool image");
12540
- const path = join(attachmentsDirFor(this.dir, worktreeId), file);
12761
+ const path = join2(attachmentsDirFor(this.dir, worktreeId), file);
12541
12762
  if (this.pending.has(path) || await Bun.file(path).exists())
12542
12763
  return;
12543
- await this.write(worktreeId, file, bytes);
12764
+ await this.write(worktreeId, file, (path) => writeFile(path, bytes));
12544
12765
  }
12545
12766
  whenWritten(worktreeId, file) {
12546
- return this.pending.get(join(attachmentsDirFor(this.dir, worktreeId), file)) ?? Promise.resolve();
12767
+ return this.pending.get(join2(attachmentsDirFor(this.dir, worktreeId), file)) ?? Promise.resolve();
12547
12768
  }
12548
12769
  async put(worktreeId, n, input) {
12549
12770
  if (!ID.test(worktreeId))
@@ -12553,19 +12774,23 @@ class AttachmentStore {
12553
12774
  return this.putImage(worktreeId, n, input);
12554
12775
  case "paste":
12555
12776
  return this.putText(worktreeId, n, input);
12777
+ case "file":
12778
+ return this.putFile(worktreeId, n, input);
12556
12779
  case "pick":
12557
12780
  return { kind: "pick", ref: { ...input, n } };
12558
12781
  }
12559
12782
  }
12560
12783
  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);
12784
+ const from = this.uploads.path(img.upload);
12785
+ if (!from || this.uploads.imageType(img.upload) !== img.mimeType)
12786
+ throw new UserError(`${img.name}: ${GONE2}`);
12787
+ const bytes = await readFile(from);
12788
+ const file = `${n}.${IMAGE_EXT[img.mimeType]}`;
12789
+ const path = await this.write(worktreeId, file, (to) => writeFile(to, bytes));
12566
12790
  return {
12567
12791
  kind: "image",
12568
12792
  bytes,
12793
+ path,
12569
12794
  ref: {
12570
12795
  kind: "image",
12571
12796
  n,
@@ -12578,28 +12803,78 @@ class AttachmentStore {
12578
12803
  }
12579
12804
  };
12580
12805
  }
12806
+ async putFile(worktreeId, n, input) {
12807
+ const from = this.uploads.path(input.upload);
12808
+ if (!from)
12809
+ throw new UserError(`${input.name}: ${GONE2}`);
12810
+ const file = `${n}-${storedName(input.name)}`;
12811
+ const path = await this.write(worktreeId, file, (to) => copyFile2(from, to));
12812
+ const bytes = (await stat(path)).size;
12813
+ const text = input.text ? await shortText(path, bytes) : null;
12814
+ return {
12815
+ kind: "file",
12816
+ path,
12817
+ ...text === null ? {} : { text },
12818
+ ref: { kind: "file", n, name: input.name, bytes, text: input.text, file }
12819
+ };
12820
+ }
12581
12821
  async putText(worktreeId, n, { text, name, source }) {
12582
12822
  if (text.length === 0)
12583
12823
  throw new UserError("empty paste");
12584
12824
  const file = `${n}.txt`;
12585
- await this.write(worktreeId, file, text);
12825
+ await this.write(worktreeId, file, (to) => writeFile(to, text));
12586
12826
  return {
12587
12827
  kind: "paste",
12588
12828
  text,
12589
12829
  ref: { kind: "paste", n, ...name ? { name } : {}, ...source ? { source } : {}, ...pasteSummary(text), file }
12590
12830
  };
12591
12831
  }
12592
- write(worktreeId, file, data) {
12832
+ async reattach(worktreeId, ref, archived) {
12833
+ const again = await this.inputOf(worktreeId, ref, archived);
12834
+ const parsed = attachmentInputSchema.safeParse(again);
12835
+ return parsed.success ? parsed.data : null;
12836
+ }
12837
+ async inputOf(worktreeId, ref, archived) {
12838
+ if (ref.kind === "pick") {
12839
+ const { n: _n, ...pick } = ref;
12840
+ return pick;
12841
+ }
12842
+ let path = null;
12843
+ for (const at of [this.fileFor(worktreeId, ref.file), archived?.(ref.file) ?? null]) {
12844
+ if (at && await Bun.file(at).exists()) {
12845
+ path = at;
12846
+ break;
12847
+ }
12848
+ }
12849
+ if (!path)
12850
+ return null;
12851
+ if (ref.kind === "paste") {
12852
+ const { name, source } = ref;
12853
+ return { kind: "paste", text: await readFile(path, "utf8"), name, source };
12854
+ }
12855
+ if (ref.kind === "file") {
12856
+ const upload = await this.uploads.adopt(path, null);
12857
+ return { kind: "file", upload, name: ref.name, bytes: ref.bytes, text: ref.text };
12858
+ }
12859
+ const mimeType = IMAGE_MIME_TYPES.find((m) => m === ref.mimeType);
12860
+ if (!mimeType)
12861
+ return null;
12862
+ const upload = await this.uploads.adopt(path, mimeType);
12863
+ const { name, bytes, width, height } = ref;
12864
+ return { kind: "image", upload, name, mimeType, bytes, width, height };
12865
+ }
12866
+ async write(worktreeId, file, fill) {
12593
12867
  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));
12868
+ const path = join2(wtDir, file);
12869
+ const done = mkdir(wtDir, { recursive: true }).then(() => fill(path)).finally(() => this.pending.delete(path));
12596
12870
  this.pending.set(path, done);
12597
- return done;
12871
+ await done;
12872
+ return path;
12598
12873
  }
12599
12874
  fileFor(worktreeId, file) {
12600
- if (!ID.test(worktreeId) || !FILE.test(file))
12875
+ if (!ID.test(worktreeId) || !isFile(file))
12601
12876
  return null;
12602
- return join(attachmentsDirFor(this.dir, worktreeId), file);
12877
+ return join2(attachmentsDirFor(this.dir, worktreeId), file);
12603
12878
  }
12604
12879
  }
12605
12880
 
@@ -12756,9 +13031,9 @@ class OptionProbe {
12756
13031
  }
12757
13032
 
12758
13033
  // ../daemon/src/agent/registry.ts
12759
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
13034
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
12760
13035
  import { homedir as homedir3 } from "os";
12761
- import { dirname as dirname3, isAbsolute, join as join5, resolve as resolve3 } from "path";
13036
+ import { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "path";
12762
13037
 
12763
13038
  // ../daemon/src/core/cloud.ts
12764
13039
  var enabled = process.env.TOYON_CLOUD === "1";
@@ -12785,7 +13060,7 @@ var cloud = {
12785
13060
  import { spawn as spawn2 } from "child_process";
12786
13061
  import { createHash as createHash2 } from "crypto";
12787
13062
  import { existsSync, readFileSync as readFileSync2 } from "fs";
12788
- import { join as join2 } from "path";
13063
+ import { join as join3 } from "path";
12789
13064
 
12790
13065
  // ../daemon/src/core/metrics.ts
12791
13066
  var WARN_MS = 50;
@@ -12827,7 +13102,7 @@ function lockfileHash(dir) {
12827
13102
  const h = createHash2("sha1");
12828
13103
  for (const f of LOCKFILES) {
12829
13104
  try {
12830
- h.update(readFileSync2(join2(dir, f)));
13105
+ h.update(readFileSync2(join3(dir, f)));
12831
13106
  } catch {}
12832
13107
  }
12833
13108
  return h.digest("hex");
@@ -13142,7 +13417,7 @@ function confine(launch, b, extraWrite = [], platform = process.platform, which
13142
13417
  }
13143
13418
 
13144
13419
  // ../daemon/src/agent/native.ts
13145
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
13420
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
13146
13421
  var OS = { darwin: "darwin", linux: "linux", win32: "windows" };
13147
13422
  function nativePackage(pkg, host) {
13148
13423
  const os = OS[host.platform];
@@ -13165,7 +13440,7 @@ function hostTraits() {
13165
13440
  }
13166
13441
  function muslLoader() {
13167
13442
  try {
13168
- return existsSync2("/lib") && readdirSync("/lib").some((f) => f.startsWith("ld-musl-"));
13443
+ return existsSync2("/lib") && readdirSync2("/lib").some((f) => f.startsWith("ld-musl-"));
13169
13444
  } catch {
13170
13445
  return false;
13171
13446
  }
@@ -13235,49 +13510,52 @@ function opencodeEnv() {
13235
13510
  }
13236
13511
 
13237
13512
  // ../daemon/src/agent/sandbox.ts
13238
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "fs";
13513
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "fs";
13239
13514
  import { homedir as homedir2, tmpdir } from "os";
13240
- import { join as join4, resolve as resolve2 } from "path";
13515
+ import { join as join5, resolve as resolve2 } from "path";
13241
13516
 
13242
13517
  // ../daemon/src/core/paths.ts
13243
- import { mkdirSync } from "fs";
13518
+ import { mkdirSync as mkdirSync2 } from "fs";
13244
13519
  import { homedir } from "os";
13245
- import { join as join3 } from "path";
13246
- function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon")) {
13520
+ import { join as join4 } from "path";
13521
+ function makePaths(home = process.env.TOYON_HOME ?? join4(homedir(), ".toyon")) {
13247
13522
  return {
13248
13523
  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")
13524
+ stateFile: join4(home, DAEMON_FILES.state),
13525
+ draftsFile: join4(home, DAEMON_FILES.drafts),
13526
+ draftAttachmentsFile: join4(home, "draft-attachments.json"),
13527
+ tokenFile: join4(home, DAEMON_FILES.token),
13528
+ pidFile: join4(home, DAEMON_FILES.pid),
13529
+ logFile: join4(home, DAEMON_FILES.log),
13530
+ remoteFile: join4(home, DAEMON_FILES.remote),
13531
+ transcriptsDir: join4(home, "transcripts"),
13532
+ attachmentsDir: join4(home, "attachments"),
13533
+ uploadsDir: join4(home, "uploads"),
13534
+ archiveDir: join4(home, "archive"),
13535
+ worktreesDir: join4(home, "worktrees.noindex"),
13536
+ cacheDir: join4(home, "cache.noindex"),
13537
+ themesDir: join4(home, "themes"),
13538
+ agentsDir: process.env.TOYON_AGENTS_DIR ?? join4(home, "agents"),
13539
+ agentsFile: join4(home, "agents.json"),
13540
+ scratchDir: join4(home, "scratch"),
13541
+ gitCredentialsFile: join4(home, "git-credentials")
13265
13542
  };
13266
13543
  }
13267
13544
  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 });
13545
+ mkdirSync2(p.home, { recursive: true, mode: 448 });
13546
+ mkdirSync2(p.transcriptsDir, { recursive: true });
13547
+ mkdirSync2(p.attachmentsDir, { recursive: true });
13548
+ mkdirSync2(p.uploadsDir, { recursive: true });
13549
+ mkdirSync2(p.archiveDir, { recursive: true });
13550
+ mkdirSync2(p.worktreesDir, { recursive: true });
13551
+ mkdirSync2(p.cacheDir, { recursive: true });
13552
+ mkdirSync2(p.themesDir, { recursive: true });
13553
+ mkdirSync2(p.agentsDir, { recursive: true });
13554
+ mkdirSync2(p.scratchDir, { recursive: true });
13277
13555
  }
13278
13556
 
13279
13557
  // ../daemon/src/git/exclude.ts
13280
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
13558
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "fs";
13281
13559
  import { dirname } from "path";
13282
13560
  async function excludeFile(cwd) {
13283
13561
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
@@ -13294,7 +13572,7 @@ async function excludeFromGit(cwd, pattern) {
13294
13572
  if (current.split(`
13295
13573
  `).some((l) => l.trim() === pattern))
13296
13574
  return;
13297
- mkdirSync2(dirname(file), { recursive: true });
13575
+ mkdirSync3(dirname(file), { recursive: true });
13298
13576
  writeFileSync(file, `${current}${current && !current.endsWith(`
13299
13577
  `) ? `
13300
13578
  ` : ""}${pattern}
@@ -13320,7 +13598,7 @@ async function excludeBlock(cwd, name, patterns) {
13320
13598
  ` : "";
13321
13599
  if (text === current)
13322
13600
  return;
13323
- mkdirSync2(dirname(file), { recursive: true });
13601
+ mkdirSync3(dirname(file), { recursive: true });
13324
13602
  writeFileSync(file, text);
13325
13603
  }
13326
13604
  async function unexcludeFromGit(cwd, pattern) {
@@ -13387,7 +13665,7 @@ async function prepareLaunch(cwd, spec) {
13387
13665
  log.warn(cwd, `agent ${spec.id} runs without an OS sandbox`);
13388
13666
  return { bounds, env: setup?.env ?? {} };
13389
13667
  }
13390
- var SETTINGS_REL = join4(".claude", "settings.local.json");
13668
+ var SETTINGS_REL = join5(".claude", "settings.local.json");
13391
13669
  function claudeLocalSettings(b) {
13392
13670
  return {
13393
13671
  sandbox: {
@@ -13401,8 +13679,8 @@ function claudeLocalSettings(b) {
13401
13679
  };
13402
13680
  }
13403
13681
  async function writeClaudeLocalSettings(cwd, b) {
13404
- const file = join4(cwd, SETTINGS_REL);
13405
- mkdirSync3(join4(cwd, ".claude"), { recursive: true });
13682
+ const file = join5(cwd, SETTINGS_REL);
13683
+ mkdirSync4(join5(cwd, ".claude"), { recursive: true });
13406
13684
  let existing = {};
13407
13685
  if (existsSync5(file)) {
13408
13686
  try {
@@ -13424,7 +13702,7 @@ async function writeClaudeLocalSettings(cwd, b) {
13424
13702
  writeFileSync2(tmp, `${JSON.stringify(next, null, 2)}
13425
13703
  `);
13426
13704
  renameSync(tmp, file);
13427
- if (existsSync5(join4(cwd, ".git")))
13705
+ if (existsSync5(join5(cwd, ".git")))
13428
13706
  await excludeFromGit(cwd, SETTINGS_REL);
13429
13707
  }
13430
13708
 
@@ -13549,12 +13827,12 @@ class AgentRegistry {
13549
13827
  const name = this.packageName(spec);
13550
13828
  if (!name || spec.run.kind === "command")
13551
13829
  return null;
13552
- const pkgDir = join5(this.dir, spec.id, "node_modules", name);
13830
+ const pkgDir = join6(this.dir, spec.id, "node_modules", name);
13553
13831
  if (spec.run.kind === "npm-native") {
13554
- const bin = join5(pkgDir, spec.run.bin);
13832
+ const bin = join6(pkgDir, spec.run.bin);
13555
13833
  return existsSync6(bin) ? bin : null;
13556
13834
  }
13557
- const pkgJson = join5(pkgDir, "package.json");
13835
+ const pkgJson = join6(pkgDir, "package.json");
13558
13836
  if (!existsSync6(pkgJson))
13559
13837
  return null;
13560
13838
  try {
@@ -13562,7 +13840,7 @@ class AgentRegistry {
13562
13840
  const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
13563
13841
  if (!rel)
13564
13842
  return null;
13565
- const script = join5(dirname3(pkgJson), rel);
13843
+ const script = join6(dirname3(pkgJson), rel);
13566
13844
  return existsSync6(script) ? script : null;
13567
13845
  } catch (e) {
13568
13846
  log.warn("agents", `cannot read ${pkgJson}`, e);
@@ -13616,10 +13894,10 @@ class AgentRegistry {
13616
13894
  if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
13617
13895
  return Promise.resolve();
13618
13896
  const { version } = spec.run;
13619
- const dir = join5(this.dir, id);
13897
+ const dir = join6(this.dir, id);
13620
13898
  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)}
13899
+ mkdirSync5(dir, { recursive: true });
13900
+ writeFileSync3(join6(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
13623
13901
  `);
13624
13902
  log.info("agents", `installing ${pkg}@${version} for ${id}`);
13625
13903
  const r = await this.installer(dir, pkg, version);
@@ -13643,7 +13921,7 @@ class AgentRegistry {
13643
13921
  const name = this.packageName(spec);
13644
13922
  if (!name)
13645
13923
  return null;
13646
- const pkgJson = join5(this.dir, spec.id, "node_modules", name, "package.json");
13924
+ const pkgJson = join6(this.dir, spec.id, "node_modules", name, "package.json");
13647
13925
  try {
13648
13926
  return JSON.parse(readFileSync6(pkgJson, "utf8")).version ?? null;
13649
13927
  } catch {
@@ -13906,6 +14184,9 @@ function previewContext(p) {
13906
14184
  case "setup":
13907
14185
  line = "Toyon is setting this worktree up and starts the preview when that is done; the next message will say where it answers.";
13908
14186
  break;
14187
+ case "parked":
14188
+ line = "The preview is off: this worktree came back from the archive, and Toyon starts its dev servers when you first write a file or when the user asks for them. Nothing answers until then.";
14189
+ break;
13909
14190
  case "starting":
13910
14191
  line = `Toyon is starting the preview; it answers${at} once it is up.`;
13911
14192
  break;
@@ -13920,8 +14201,12 @@ function previewContext(p) {
13920
14201
  }
13921
14202
  return line;
13922
14203
  }
13923
- function imageCaption(ref) {
13924
- return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height})`;
14204
+ function imageCaption(ref, path) {
14205
+ return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height}), saved at ${path}`;
14206
+ }
14207
+ function fileCaption(ref, path, inline = false) {
14208
+ const at = `A file the user attached: ${ref.name} (${fmtBytes(ref.bytes)}), saved at ${path}.`;
14209
+ return inline ? at : `${at} Read or search it there.`;
13925
14210
  }
13926
14211
  function pasteCaption(ref) {
13927
14212
  const label = attachmentLabel("paste", ref.n);
@@ -13967,7 +14252,7 @@ function attachmentBlocks(a) {
13967
14252
  switch (a.kind) {
13968
14253
  case "image":
13969
14254
  return [
13970
- textBlock(imageCaption(a.ref)),
14255
+ textBlock(imageCaption(a.ref, a.path)),
13971
14256
  { type: "image", mimeType: a.ref.mimeType, data: a.bytes.toString("base64") }
13972
14257
  ];
13973
14258
  case "paste":
@@ -13975,6 +14260,13 @@ function attachmentBlocks(a) {
13975
14260
  <pasted-text ${a.ref.n}>
13976
14261
  ${a.text}
13977
14262
  </pasted-text>`)];
14263
+ case "file":
14264
+ return [
14265
+ textBlock(a.text === undefined ? fileCaption(a.ref, a.path) : `${fileCaption(a.ref, a.path, true)}
14266
+ <attached-file ${a.ref.n}>
14267
+ ${a.text}
14268
+ </attached-file>`)
14269
+ ];
13978
14270
  case "pick":
13979
14271
  return [textBlock(pickCaption(a.ref))];
13980
14272
  }
@@ -13983,9 +14275,9 @@ ${a.text}
13983
14275
  // ../daemon/src/agent/transcript.ts
13984
14276
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
13985
14277
  import { appendFile } from "fs/promises";
13986
- import { join as join6 } from "path";
14278
+ import { join as join7 } from "path";
13987
14279
  function transcriptPathFor(transcriptsDir, worktreeId) {
13988
- return join6(transcriptsDir, `${worktreeId}.jsonl`);
14280
+ return join7(transcriptsDir, `${worktreeId}.jsonl`);
13989
14281
  }
13990
14282
  function coalesce(entries) {
13991
14283
  const out = [];
@@ -14140,7 +14432,7 @@ async function cleanUp(ctx, a, sessionId) {
14140
14432
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
14141
14433
  import { open as open2 } from "fs/promises";
14142
14434
  import { homedir as homedir4 } from "os";
14143
- import { basename as basename2, dirname as dirname4, join as join7 } from "path";
14435
+ import { basename as basename2, dirname as dirname4, join as join8 } from "path";
14144
14436
  var STARTED = /Command running in background with ID: (\S+)\. Output is being written to: (\S+)\./;
14145
14437
  function backgroundStart(input, output) {
14146
14438
  const flag = input && typeof input === "object" ? input.run_in_background : undefined;
@@ -14153,14 +14445,14 @@ function spawnDetached(output) {
14153
14445
  return output.includes("Async agent launched successfully");
14154
14446
  }
14155
14447
  function claudeConfigDir() {
14156
- return process.env.CLAUDE_CONFIG_DIR || join7(homedir4(), ".claude");
14448
+ return process.env.CLAUDE_CONFIG_DIR || join8(homedir4(), ".claude");
14157
14449
  }
14158
14450
  function sessionLogFor(file, configDir = claudeConfigDir()) {
14159
14451
  const tasks = dirname4(file);
14160
14452
  if (basename2(tasks) !== "tasks")
14161
14453
  return null;
14162
14454
  const session = dirname4(tasks);
14163
- const path = join7(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14455
+ const path = join8(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14164
14456
  return existsSync8(path) ? path : null;
14165
14457
  }
14166
14458
  function taskEnd(text, toolId) {
@@ -14521,11 +14813,26 @@ function endOfAsk(req, res, memos) {
14521
14813
  isError: !allowed
14522
14814
  };
14523
14815
  }
14524
- function abandoned(memos, parent) {
14816
+ function release(toolId, memo) {
14817
+ const held = memo.held;
14818
+ if (!held)
14819
+ return null;
14820
+ delete memo.held;
14821
+ if (!emptyInput(memo.input))
14822
+ memo.writing = false;
14823
+ return memo.ended ? null : { type: "tool-update", toolId, ...held };
14824
+ }
14825
+ function abandoned(memos, parent, opening = false) {
14525
14826
  const out = [];
14526
14827
  for (const [toolId, memo] of memos) {
14527
14828
  if (memo.ended || !memo.writing || memo.parent !== parent)
14528
14829
  continue;
14830
+ if (opening && memo.held) {
14831
+ const sent = release(toolId, memo);
14832
+ if (sent)
14833
+ out.push(sent);
14834
+ continue;
14835
+ }
14529
14836
  memo.ended = true;
14530
14837
  out.push({ type: "tool-end", toolId });
14531
14838
  }
@@ -14554,7 +14861,7 @@ function mapUpdate(update, memos, tag, sink, background) {
14554
14861
  }
14555
14862
  case "tool_call": {
14556
14863
  const spawn = spawnOf(update._meta);
14557
- const out = abandoned(memos, spawn.parentToolId);
14864
+ const out = abandoned(memos, spawn.parentToolId, true);
14558
14865
  const input = update.rawInput ?? { locations: update.locations ?? [] };
14559
14866
  const head = heading(update);
14560
14867
  const memo = {
@@ -14609,26 +14916,27 @@ function mapUpdate(update, memos, tag, sink, background) {
14609
14916
  ...spawn
14610
14917
  });
14611
14918
  }
14612
- if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
14613
- return out;
14614
- const refined = { type: "tool-update", toolId: update.toolCallId };
14919
+ const refined = { ...memo.held };
14615
14920
  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) {
14921
+ if (title && title !== memo.title)
14922
+ refined.title = memo.title = title;
14923
+ if (update.name && update.name !== memo.name)
14621
14924
  refined.name = memo.name = update.name;
14622
- }
14623
14925
  if (update.kind && update.kind !== memo.kind)
14624
14926
  refined.kind = memo.kind = update.kind;
14625
- if (update.rawInput !== undefined) {
14927
+ if (update.rawInput !== undefined)
14626
14928
  refined.input = memo.input = update.rawInput;
14627
- if (!emptyInput(update.rawInput))
14628
- memo.writing = false;
14929
+ if (update.rawInput !== undefined && update.content === undefined && update.status === undefined) {
14930
+ if (out.length === 0)
14931
+ memo.held = refined;
14932
+ return out;
14933
+ }
14934
+ delete memo.held;
14935
+ if (!emptyInput(memo.input))
14936
+ memo.writing = false;
14937
+ if (Object.keys(refined).length > 0 && !memo.ended && out.length === 0) {
14938
+ out.push({ type: "tool-update", toolId: update.toolCallId, ...refined });
14629
14939
  }
14630
- if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
14631
- out.push(refined);
14632
14940
  if (update.content)
14633
14941
  memo.content = [...memo.content, ...update.content];
14634
14942
  if (update.rawOutput !== undefined)
@@ -14776,6 +15084,11 @@ function steerOutcome(value) {
14776
15084
  // ../daemon/src/agent/acp/session.ts
14777
15085
  var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
14778
15086
  var OWN_SETTLE_MS = 1e4;
15087
+ var INPUT_HOLD_MS = 1000;
15088
+ var queued = (q) => ({
15089
+ text: q.text,
15090
+ ...q.attachments ? { attachments: q.attachments } : {}
15091
+ });
14779
15092
  function recorded(reply) {
14780
15093
  if (reply?.kind === "choice")
14781
15094
  return { choiceId: reply.choiceId };
@@ -14891,7 +15204,7 @@ class AcpSession {
14891
15204
  return this.queue.length > 0 || this.steered.length > 0 || this.refused !== null || this.asks.size > 0;
14892
15205
  }
14893
15206
  get queueItems() {
14894
- return this.waiting().map((q) => q.text);
15207
+ return this.waiting().map(queued);
14895
15208
  }
14896
15209
  waiting() {
14897
15210
  return this.queue.filter((q) => !q.recorded);
@@ -14900,13 +15213,19 @@ class AcpSession {
14900
15213
  queueChanged() {
14901
15214
  this.onQueueChange?.();
14902
15215
  }
14903
- unqueue(index) {
15216
+ unqueue(index, taken) {
14904
15217
  const item = this.waiting()[index];
14905
15218
  if (!item)
14906
15219
  return;
14907
15220
  this.queue.splice(this.queue.indexOf(item), 1);
15221
+ taken?.(queued(item));
15222
+ this.letGo(item);
14908
15223
  this.queueChanged();
14909
15224
  }
15225
+ letGo(item) {
15226
+ if (!item.recorded)
15227
+ this.d.attachments.uploads.release(uploadIds(item.attachments));
15228
+ }
14910
15229
  transcript() {
14911
15230
  return this.log.entries;
14912
15231
  }
@@ -14924,8 +15243,14 @@ class AcpSession {
14924
15243
  send(text, opts = {}) {
14925
15244
  if (this.stopped)
14926
15245
  return log.warn(this.d.worktreeId, "send after close dropped");
15246
+ for (const ask of [...this.asks.values()])
15247
+ ask.settle("skipped");
15248
+ this.post(text, opts);
15249
+ }
15250
+ post(text, opts = {}) {
14927
15251
  const { context, attachments } = opts;
14928
15252
  const item = { text, context, ...attachments?.length ? { attachments } : {} };
15253
+ this.d.attachments.uploads.hold(uploadIds(attachments));
14929
15254
  if ((this.running || this.own) && !this.interrupted && !this.held && this.live?.conn.steering) {
14930
15255
  return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
14931
15256
  }
@@ -15012,6 +15337,8 @@ class AcpSession {
15012
15337
  async close() {
15013
15338
  this.stopped = true;
15014
15339
  this.clearReaper();
15340
+ for (const item of this.queue)
15341
+ this.letGo(item);
15015
15342
  this.queue = [];
15016
15343
  this.steered = [];
15017
15344
  this.queueChanged();
@@ -15135,8 +15462,8 @@ class AcpSession {
15135
15462
  retry() {
15136
15463
  const item = this.refused;
15137
15464
  this.refused = null;
15138
- if (item)
15139
- this.send(item.text, item);
15465
+ if (item && !this.stopped)
15466
+ this.enqueue(item);
15140
15467
  }
15141
15468
  planWithheld(conn, methodId) {
15142
15469
  return this.d.planSignIn?.() === false && conn.spec.terminalLogins?.[methodId]?.plan === true;
@@ -15159,8 +15486,12 @@ class AcpSession {
15159
15486
  }
15160
15487
  async record({ text, attachments }) {
15161
15488
  const stored = [];
15162
- for (const a of attachments ?? [])
15163
- stored.push(await this.d.attachments.put(this.d.worktreeId, this.seq[a.kind]++, a));
15489
+ try {
15490
+ for (const a of attachments ?? [])
15491
+ stored.push(await this.d.attachments.put(this.d.worktreeId, this.seq[a.kind]++, a));
15492
+ } finally {
15493
+ this.d.attachments.uploads.release(uploadIds(attachments));
15494
+ }
15164
15495
  this.emit({
15165
15496
  type: "user-message",
15166
15497
  text,
@@ -15182,6 +15513,11 @@ class AcpSession {
15182
15513
  }
15183
15514
  async runTurn(item) {
15184
15515
  item.recorded ??= await this.record(item);
15516
+ try {
15517
+ await this.d.beforeTurn?.();
15518
+ } catch (e) {
15519
+ log.warn(this.d.worktreeId, "before-turn hook failed", e);
15520
+ }
15185
15521
  this.emit({ type: "turn-start", ts: Date.now() });
15186
15522
  const live = await this.ensureLive("a turn");
15187
15523
  await this.applyMode(live);
@@ -15410,6 +15746,10 @@ class AcpSession {
15410
15746
  if (params.update.sessionUpdate === "config_option_update")
15411
15747
  this.absorb(live, params.update.configOptions);
15412
15748
  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));
15749
+ this.forward(live, events);
15750
+ this.holdInput(live, params.update);
15751
+ }
15752
+ forward(live, events) {
15413
15753
  if (!this.running)
15414
15754
  this.trackOwn(events);
15415
15755
  for (const ev of events) {
@@ -15418,6 +15758,23 @@ class AcpSession {
15418
15758
  this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
15419
15759
  }
15420
15760
  }
15761
+ holdInput(live, update) {
15762
+ if (update.sessionUpdate !== "tool_call_update")
15763
+ return;
15764
+ const toolId = update.toolCallId;
15765
+ const memo = live.tools.get(toolId);
15766
+ const held = memo?.held;
15767
+ if (!memo || !held)
15768
+ return;
15769
+ const t = setTimeout(() => {
15770
+ if (this.live !== live || memo.held !== held || !(this.running || this.own))
15771
+ return;
15772
+ const sent = release(toolId, memo);
15773
+ if (sent)
15774
+ this.forward(live, [sent]);
15775
+ }, this.d.inputHoldMs ?? INPUT_HOLD_MS);
15776
+ t.unref?.();
15777
+ }
15421
15778
  onPermission(params, bounds, sandboxed) {
15422
15779
  const tools = this.live?.tools;
15423
15780
  return Promise.resolve(this.answerPermission(params, bounds, sandboxed)).then((res) => {
@@ -15476,7 +15833,7 @@ class AcpSession {
15476
15833
  async sayPlanEdited(proposed, path) {
15477
15834
  if (!await this.d.planEdited?.(path, proposed))
15478
15835
  return;
15479
- this.send(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
15836
+ this.post(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
15480
15837
  }
15481
15838
  onElicit(params, signal) {
15482
15839
  const live = this.live;
@@ -15671,9 +16028,9 @@ function sameCommands(a, b) {
15671
16028
  }
15672
16029
 
15673
16030
  // ../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");
16031
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
16032
+ import { join as join9 } from "path";
16033
+ var PLANS_DIR = join9(".toyon", "plans");
15677
16034
  var PLAN_NAME = /^(\d+)\.md$/;
15678
16035
  function isPlanPath(rel) {
15679
16036
  return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
@@ -15682,7 +16039,7 @@ function nextNumber(dir) {
15682
16039
  if (!existsSync9(dir))
15683
16040
  return 1;
15684
16041
  let max = 0;
15685
- for (const name of readdirSync2(dir)) {
16042
+ for (const name of readdirSync3(dir)) {
15686
16043
  const n = Number(PLAN_NAME.exec(name)?.[1]);
15687
16044
  if (n > max)
15688
16045
  max = n;
@@ -15690,11 +16047,11 @@ function nextNumber(dir) {
15690
16047
  return max + 1;
15691
16048
  }
15692
16049
  async function writePlanDoc(cwd, markdown) {
15693
- const dir = join8(cwd, PLANS_DIR);
15694
- const rel = join8(PLANS_DIR, `${nextNumber(dir)}.md`);
16050
+ const dir = join9(cwd, PLANS_DIR);
16051
+ const rel = join9(PLANS_DIR, `${nextNumber(dir)}.md`);
15695
16052
  try {
15696
- mkdirSync5(dir, { recursive: true });
15697
- writeFileSync4(join8(cwd, rel), markdown.endsWith(`
16053
+ mkdirSync6(dir, { recursive: true });
16054
+ writeFileSync4(join9(cwd, rel), markdown.endsWith(`
15698
16055
  `) ? markdown : `${markdown}
15699
16056
  `);
15700
16057
  } catch (e) {
@@ -15705,7 +16062,7 @@ async function writePlanDoc(cwd, markdown) {
15705
16062
  return rel;
15706
16063
  }
15707
16064
  function planEdited(cwd, rel, proposed) {
15708
- const file = join8(cwd, rel);
16065
+ const file = join9(cwd, rel);
15709
16066
  if (!existsSync9(file))
15710
16067
  return false;
15711
16068
  try {
@@ -17010,11 +17367,12 @@ function defaultAgent(wt, d, preview, onProcess) {
17010
17367
  launch: (spec) => d.agents.command(spec),
17011
17368
  planSignIn: () => d.managed?.planSignIn ?? true,
17012
17369
  transcriptsDir: d.paths.transcriptsDir,
17013
- attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir),
17370
+ attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir, new UploadStore(d.paths.uploadsDir)),
17014
17371
  getSessionId: () => d.state.session(wt.id),
17015
17372
  setSessionId: (id) => d.state.setSession(wt.id, id),
17016
17373
  onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
17017
17374
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
17375
+ beforeTurn: async () => d.turnStarting?.(wt.id),
17018
17376
  onAuth: (agentId, o) => d.accounts?.observe(agentId, o),
17019
17377
  seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
17020
17378
  onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
@@ -17318,9 +17676,12 @@ class RuntimeRegistry {
17318
17676
  return rt;
17319
17677
  }
17320
17678
  async start(wt, repo) {
17321
- if (!this.deps.state.worktree(wt.id))
17679
+ const record = this.deps.state.worktree(wt.id);
17680
+ if (!record)
17322
17681
  return;
17323
17682
  const rt = this.ensureAgent(wt);
17683
+ if (record.parked)
17684
+ return;
17324
17685
  const run = repo.needsSetup ? EMPTY_RUN : resolveRun(repo, wt);
17325
17686
  const leads = wt.kind !== "main" || !this.deps.mainLeads || this.deps.mainLeads(wt.repoId);
17326
17687
  if (rt.procs) {
@@ -17719,6 +18080,11 @@ class RuntimeRegistry {
17719
18080
  return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
17720
18081
  }
17721
18082
  previewStanding(id) {
18083
+ const wt = this.deps.state.worktree(id);
18084
+ if (wt?.parked) {
18085
+ const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
18086
+ return repo && !repo.needsSetup && resolveRun(repo, wt).preview ? { status: "parked" } : null;
18087
+ }
17722
18088
  if (this.settingUp.has(id))
17723
18089
  return { status: "setup" };
17724
18090
  const rt = this.runtimes.get(id);
@@ -17783,17 +18149,17 @@ class RuntimeRegistry {
17783
18149
  }
17784
18150
 
17785
18151
  // ../daemon/src/worktrees/naming.ts
17786
- import { randomBytes } from "crypto";
18152
+ import { randomBytes as randomBytes2 } from "crypto";
17787
18153
  import { existsSync as existsSync11 } from "fs";
17788
- import { join as join9 } from "path";
18154
+ import { join as join10 } from "path";
17789
18155
  function shortId() {
17790
- return randomBytes(5).toString("hex");
18156
+ return randomBytes2(5).toString("hex");
17791
18157
  }
17792
18158
  function freeSlot(parent) {
17793
18159
  for (;; ) {
17794
18160
  const id = shortId();
17795
18161
  const dir = `wt-${id.slice(0, 4)}`;
17796
- if (!existsSync11(join9(parent, dir)))
18162
+ if (!existsSync11(join10(parent, dir)))
17797
18163
  return { id, dir };
17798
18164
  }
17799
18165
  }
@@ -18133,6 +18499,8 @@ function attachmentText(a) {
18133
18499
  return a.text;
18134
18500
  if (a.kind === "image")
18135
18501
  return a.name ? `image ${a.name}` : "an image";
18502
+ if (a.kind === "file")
18503
+ return `file ${a.name}`;
18136
18504
  return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
18137
18505
  }
18138
18506
  function parseName(text) {
@@ -18207,29 +18575,29 @@ function makePlanner(agents) {
18207
18575
  }
18208
18576
 
18209
18577
  // ../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";
18578
+ import { existsSync as existsSync12, readdirSync as readdirSync4, rmSync as rmSync2, statSync as statSync4 } from "fs";
18579
+ import { join as join11, resolve as resolve5 } from "path";
18212
18580
  function locateAssets(here, env = process.env) {
18213
- const packaged = existsSync12(join10(here, "shell", "index.html"));
18581
+ const packaged = existsSync12(join11(here, "shell", "index.html"));
18214
18582
  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")),
18583
+ shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join11(here, "shell") : join11(here, "../../shell/dist")),
18584
+ bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join11(here, "bridge.js") : join11(here, "../../bridge/dist/bridge.js")),
18217
18585
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
18218
- packageJson: packaged ? join10(here, "..", "package.json") : null
18586
+ packageJson: packaged ? join11(here, "..", "package.json") : null
18219
18587
  };
18220
18588
  }
18221
18589
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
18222
18590
  function pruneAssets(shellDist) {
18223
- const dir = join10(shellDist, "assets");
18591
+ const dir = join11(shellDist, "assets");
18224
18592
  let names;
18225
18593
  try {
18226
- names = readdirSync3(dir);
18594
+ names = readdirSync4(dir);
18227
18595
  } catch {
18228
18596
  return 0;
18229
18597
  }
18230
18598
  const found = [];
18231
18599
  for (const name of names) {
18232
- const path = join10(dir, name);
18600
+ const path = join11(dir, name);
18233
18601
  try {
18234
18602
  found.push({ path, mtime: statSync4(path).mtimeMs });
18235
18603
  } catch {}
@@ -18242,7 +18610,7 @@ function pruneAssets(shellDist) {
18242
18610
  if (newest - f.mtime <= KEEP_MS)
18243
18611
  continue;
18244
18612
  try {
18245
- rmSync(f.path, { recursive: true, force: true });
18613
+ rmSync2(f.path, { recursive: true, force: true });
18246
18614
  gone++;
18247
18615
  } catch {}
18248
18616
  }
@@ -18894,7 +19262,7 @@ class SelfWatch {
18894
19262
  }
18895
19263
 
18896
19264
  // ../daemon/src/core/state.ts
18897
- import { randomBytes as randomBytes2 } from "crypto";
19265
+ import { randomBytes as randomBytes3 } from "crypto";
18898
19266
  import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
18899
19267
  var GROUPS_SAVE_MS = 250;
18900
19268
  var SEEN_WORKTREES = 50;
@@ -18956,7 +19324,7 @@ function loadOrCreateToken(paths) {
18956
19324
  const TOKEN_FILE = paths.tokenFile;
18957
19325
  if (existsSync14(TOKEN_FILE))
18958
19326
  return readFileSync10(TOKEN_FILE, "utf8").trim();
18959
- const token = randomBytes2(32).toString("hex");
19327
+ const token = randomBytes3(32).toString("hex");
18960
19328
  writeFileSync5(TOKEN_FILE, token, { mode: 384 });
18961
19329
  return token;
18962
19330
  }
@@ -19170,7 +19538,7 @@ class StateStore {
19170
19538
  }
19171
19539
 
19172
19540
  // ../daemon/src/design/service.ts
19173
- import { join as join11 } from "path";
19541
+ import { join as join12 } from "path";
19174
19542
 
19175
19543
  // ../daemon/src/design/scan.ts
19176
19544
  var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
@@ -19620,7 +19988,7 @@ class DesignService {
19620
19988
  async function readAll(root, paths) {
19621
19989
  const out = [];
19622
19990
  for (const path of paths) {
19623
- const file = Bun.file(join11(root, path));
19991
+ const file = Bun.file(join12(root, path));
19624
19992
  if (file.size === 0 || file.size > MAX_FILE_BYTES)
19625
19993
  continue;
19626
19994
  try {
@@ -19735,7 +20103,7 @@ function typicalUse(classes) {
19735
20103
  var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
19736
20104
  async function loadProjectTypescript(root) {
19737
20105
  try {
19738
- return await import(join11(root, "node_modules", "typescript", "lib", "typescript.js"));
20106
+ return await import(join12(root, "node_modules", "typescript", "lib", "typescript.js"));
19739
20107
  } catch {
19740
20108
  return null;
19741
20109
  }
@@ -19748,28 +20116,32 @@ var SAVE_DELAY_MS = 1000;
19748
20116
  class DraftStore {
19749
20117
  d;
19750
20118
  drafts = {};
20119
+ lists = {};
20120
+ dirty = { text: false, lists: false };
19751
20121
  saveTimer = null;
19752
20122
  constructor(d) {
19753
20123
  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);
20124
+ for (const [id, text] of readRecord(d.file)) {
20125
+ if (typeof text === "string" && text)
20126
+ this.drafts[id] = text;
20127
+ }
20128
+ for (const [id, list] of readRecord(d.attachmentsFile)) {
20129
+ const items = (Array.isArray(list) ? list : []).flatMap((item) => {
20130
+ const r = attachmentInputSchema.safeParse(item);
20131
+ return r.success ? [r.data] : [];
20132
+ });
20133
+ if (items.length)
20134
+ this.lists[id] = items;
19766
20135
  }
19767
20136
  }
19768
20137
  all() {
19769
20138
  return { ...this.drafts };
19770
20139
  }
20140
+ allAttachments() {
20141
+ return { ...this.lists };
20142
+ }
19771
20143
  has(boxId) {
19772
- return !!this.drafts[boxId]?.trim();
20144
+ return !!this.drafts[boxId]?.trim() || !!this.lists[boxId]?.length;
19773
20145
  }
19774
20146
  text(boxId) {
19775
20147
  return this.drafts[boxId] ?? "";
@@ -19781,22 +20153,67 @@ class DraftStore {
19781
20153
  this.drafts[boxId] = text;
19782
20154
  else
19783
20155
  delete this.drafts[boxId];
19784
- this.scheduleSave();
20156
+ this.scheduleSave("text");
19785
20157
  this.d.hub.emit("draftChanged", boxId, text, clientId);
19786
20158
  }
20159
+ attachments(boxId) {
20160
+ return this.lists[boxId] ?? [];
20161
+ }
20162
+ setAttachments(boxId, items, clientId) {
20163
+ if (JSON.stringify(this.lists[boxId] ?? []) === JSON.stringify(items))
20164
+ return;
20165
+ if (items.length)
20166
+ this.lists[boxId] = items;
20167
+ else
20168
+ delete this.lists[boxId];
20169
+ this.scheduleSave("lists");
20170
+ this.d.uploads?.keep(this.uploadIds());
20171
+ this.d.hub.emit("attachmentsChanged", boxId, items, clientId);
20172
+ }
20173
+ take(boxId, clientId) {
20174
+ const was = { text: this.text(boxId), items: this.attachments(boxId) };
20175
+ this.set(boxId, "", clientId);
20176
+ this.setAttachments(boxId, [], clientId);
20177
+ return was;
20178
+ }
20179
+ putBack(boxId, was, clientId) {
20180
+ if (was.text && !this.text(boxId))
20181
+ this.set(boxId, was.text, clientId);
20182
+ if (was.items.length)
20183
+ this.setAttachments(boxId, [...was.items, ...this.attachments(boxId)], clientId);
20184
+ }
20185
+ uploadIds() {
20186
+ return Object.values(this.lists).flatMap((items) => uploadIds(items));
20187
+ }
19787
20188
  drop(boxId) {
19788
20189
  this.set(boxId, "");
20190
+ this.setAttachments(boxId, []);
20191
+ }
20192
+ move(fromId, toId, over = false) {
20193
+ const text = this.text(fromId);
20194
+ const items = this.attachments(fromId);
20195
+ if (!text && !items.length)
20196
+ return false;
20197
+ if (!over && (this.text(toId) || this.attachments(toId).length))
20198
+ return false;
20199
+ this.set(toId, text);
20200
+ this.setAttachments(toId, items);
20201
+ this.drop(fromId);
20202
+ return true;
19789
20203
  }
19790
20204
  prune(known) {
19791
- let changed = false;
19792
20205
  for (const id of Object.keys(this.drafts)) {
19793
20206
  if (known(id))
19794
20207
  continue;
19795
20208
  delete this.drafts[id];
19796
- changed = true;
20209
+ this.scheduleSave("text");
20210
+ }
20211
+ for (const id of Object.keys(this.lists)) {
20212
+ if (known(id))
20213
+ continue;
20214
+ delete this.lists[id];
20215
+ this.scheduleSave("lists");
19797
20216
  }
19798
- if (changed)
19799
- this.scheduleSave();
19800
20217
  }
19801
20218
  flush() {
19802
20219
  if (!this.saveTimer)
@@ -19805,7 +20222,8 @@ class DraftStore {
19805
20222
  this.saveTimer = null;
19806
20223
  this.write();
19807
20224
  }
19808
- scheduleSave() {
20225
+ scheduleSave(which) {
20226
+ this.dirty[which] = true;
19809
20227
  if (this.saveTimer)
19810
20228
  return;
19811
20229
  this.saveTimer = setTimeout(() => {
@@ -19815,13 +20233,31 @@ class DraftStore {
19815
20233
  this.saveTimer.unref?.();
19816
20234
  }
19817
20235
  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
- }
20236
+ if (this.dirty.text)
20237
+ writeAtomic(this.d.file, this.drafts);
20238
+ if (this.dirty.lists)
20239
+ writeAtomic(this.d.attachmentsFile, this.lists);
20240
+ this.dirty = { text: false, lists: false };
20241
+ }
20242
+ }
20243
+ function readRecord(file) {
20244
+ if (!existsSync15(file))
20245
+ return [];
20246
+ try {
20247
+ const raw = JSON.parse(readFileSync11(file, "utf8"));
20248
+ return raw && typeof raw === "object" ? Object.entries(raw) : [];
20249
+ } catch (e) {
20250
+ log.warn("drafts", `skipped an unreadable ${file}`, e);
20251
+ return [];
20252
+ }
20253
+ }
20254
+ function writeAtomic(file, value) {
20255
+ const tmp = `${file}.tmp`;
20256
+ try {
20257
+ writeFileSync6(tmp, JSON.stringify(value));
20258
+ renameSync3(tmp, file);
20259
+ } catch (e) {
20260
+ log.error("drafts", `could not write ${file}`, e);
19825
20261
  }
19826
20262
  }
19827
20263
 
@@ -20050,17 +20486,17 @@ class ExecService {
20050
20486
  }
20051
20487
 
20052
20488
  // ../daemon/src/files/open.ts
20053
- import { stat as stat2 } from "fs/promises";
20489
+ import { stat as stat3 } from "fs/promises";
20054
20490
  import { basename as basename3, resolve as resolve7 } from "path";
20055
20491
 
20056
20492
  // ../daemon/src/repos/browse.ts
20057
20493
  import { existsSync as existsSync16, statSync as statSync5 } from "fs";
20058
20494
  import { readdir } from "fs/promises";
20059
20495
  import { homedir as homedir7 } from "os";
20060
- import { dirname as dirname5, join as join12 } from "path";
20496
+ import { dirname as dirname5, join as join13 } from "path";
20061
20497
  var MAX_ENTRIES = 40;
20062
20498
  function expandTilde(raw) {
20063
- return raw === "~" || raw.startsWith("~/") ? join12(homedir7(), raw.slice(1)) : raw;
20499
+ return raw === "~" || raw.startsWith("~/") ? join13(homedir7(), raw.slice(1)) : raw;
20064
20500
  }
20065
20501
  function collapseTilde(abs) {
20066
20502
  const home = homedir7();
@@ -20071,7 +20507,7 @@ function holdsNothing(names) {
20071
20507
  }
20072
20508
  async function describeFolder(abs) {
20073
20509
  const path = collapseTilde(abs);
20074
- if (existsSync16(join12(abs, ".git")))
20510
+ if (existsSync16(join13(abs, ".git")))
20075
20511
  return { path, kind: "project" };
20076
20512
  try {
20077
20513
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -20088,7 +20524,7 @@ function describe2(typed, parent) {
20088
20524
  return {
20089
20525
  exists: existsSync16(typed),
20090
20526
  isDir,
20091
- isRepo: isDir && existsSync16(join12(typed, ".git")),
20527
+ isRepo: isDir && existsSync16(join13(typed, ".git")),
20092
20528
  parentExists: existsSync16(parent)
20093
20529
  };
20094
20530
  }
@@ -20113,8 +20549,8 @@ async function browsePath(raw) {
20113
20549
  continue;
20114
20550
  if (!name.toLowerCase().startsWith(prefix))
20115
20551
  continue;
20116
- const abs = join12(dir, name);
20117
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync16(join12(abs, ".git")) });
20552
+ const abs = join13(dir, name);
20553
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync16(join13(abs, ".git")) });
20118
20554
  }
20119
20555
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
20120
20556
  return { entries: entries.slice(0, MAX_ENTRIES), target };
@@ -20151,7 +20587,7 @@ function looksBinary(text) {
20151
20587
 
20152
20588
  // ../daemon/src/files/service.ts
20153
20589
  import { spawn as spawn4 } from "child_process";
20154
- import { stat, unlink } from "fs/promises";
20590
+ import { stat as stat2, unlink } from "fs/promises";
20155
20591
 
20156
20592
  // ../daemon/src/git/blame.ts
20157
20593
  var NONE = { commits: [], lines: [] };
@@ -20222,10 +20658,13 @@ async function withLock(key, fn) {
20222
20658
  function withRepoLock(repoPath, fn) {
20223
20659
  return withLock(repoPath, fn);
20224
20660
  }
20661
+ function withFetchLock(repoPath, fn) {
20662
+ return withLock(`fetch:${repoPath}`, fn);
20663
+ }
20225
20664
  var fileLockKey = (absPath) => `file:${absPath}`;
20226
20665
 
20227
20666
  // ../daemon/src/git/status.ts
20228
- import { join as join13 } from "path";
20667
+ import { join as join14 } from "path";
20229
20668
  async function treeFingerprint(worktreePath) {
20230
20669
  const [head, diff, untracked] = await Promise.all([
20231
20670
  git(worktreePath, "rev-parse", "HEAD"),
@@ -20235,7 +20674,7 @@ async function treeFingerprint(worktreePath) {
20235
20674
  const others = await Promise.all(untracked.out.split(`
20236
20675
  `).filter(Boolean).map(async (p) => {
20237
20676
  try {
20238
- const f = Bun.file(join13(worktreePath, p));
20677
+ const f = Bun.file(join14(worktreePath, p));
20239
20678
  const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
20240
20679
  return `${p}:${body}`;
20241
20680
  } catch {
@@ -20268,6 +20707,10 @@ async function fileBefore(worktreePath, defaultBr, file) {
20268
20707
  const r = await gitRaw(worktreePath, "show", `${ref}:${file}`);
20269
20708
  return r.ok ? r.out : "";
20270
20709
  }
20710
+ async function fileAt(worktreePath, commit, file) {
20711
+ const r = await gitRaw(worktreePath, "show", `${commit}:${file}`);
20712
+ return r.ok ? r.out : "";
20713
+ }
20271
20714
  function parseNumstat(out) {
20272
20715
  const counts = new Map;
20273
20716
  for (const line of out.split(`
@@ -20286,7 +20729,7 @@ async function numstat(worktreePath, ...range) {
20286
20729
  }
20287
20730
  async function untrackedLines(worktreePath, file) {
20288
20731
  try {
20289
- const f = Bun.file(join13(worktreePath, file));
20732
+ const f = Bun.file(join14(worktreePath, file));
20290
20733
  if (f.size > 2000000)
20291
20734
  return {};
20292
20735
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -20459,7 +20902,7 @@ var COMMON = 40;
20459
20902
  var NEAR = 3;
20460
20903
  var CLEAR_LEAD = 1.5;
20461
20904
  var ELEMENT_SOURCES_MAX = 8;
20462
- var NAME = /^[\w-]+$/;
20905
+ var NAME2 = /^[\w-]+$/;
20463
20906
  var TAG = /^[a-z][a-z0-9-]*$/;
20464
20907
  var NAME_CHAR = /[\w-]/;
20465
20908
  function needlesOf(el) {
@@ -20472,12 +20915,12 @@ function needlesOf(el) {
20472
20915
  out.set(text, { text, weight, word });
20473
20916
  };
20474
20917
  if (el.id)
20475
- add(el.id, 8, NAME.test(el.id));
20918
+ add(el.id, 8, NAME2.test(el.id));
20476
20919
  const words = (el.text.split(/[&<>"'`${}]/)[0] ?? "").trim().split(/\s+/).slice(0, 6).join(" ");
20477
20920
  if (words.length >= 3 && /\p{L}/u.test(words))
20478
20921
  add(words, 6, false);
20479
20922
  for (const [, value] of el.attrs)
20480
- add(value, 5, NAME.test(value));
20923
+ add(value, 5, NAME2.test(value));
20481
20924
  for (const c of el.classes)
20482
20925
  add(c, 3, true);
20483
20926
  return [...out.values()];
@@ -20643,7 +21086,7 @@ var NOT_WHERE_WRITTEN = [
20643
21086
  ];
20644
21087
  var NO_TEXT = { before: "", after: "" };
20645
21088
  async function statFile(target) {
20646
- const st = await stat(target).catch((e) => {
21089
+ const st = await stat2(target).catch((e) => {
20647
21090
  if (e.code === "ENOENT")
20648
21091
  return null;
20649
21092
  throw e;
@@ -20704,14 +21147,14 @@ class FileService {
20704
21147
  throw new UserError("unknown worktree");
20705
21148
  return r;
20706
21149
  }
20707
- async read(worktreeId, path, ref) {
21150
+ async read(worktreeId, path, ref, since) {
20708
21151
  const r = this.require(worktreeId);
20709
21152
  const target = resolveInside(r.path, path);
20710
21153
  if (ref) {
20711
21154
  const { before, after } = await fileAtCommit(r.path, ref, path);
20712
21155
  return { ...sides(before, after), version: null, writable: false };
20713
21156
  }
20714
- const before = await fileBefore(r.path, r.base, path);
21157
+ const before = since ? await fileAt(r.path, since, path) : await fileBefore(r.path, r.base, path);
20715
21158
  const st = await statFile(target);
20716
21159
  if (st && st.size > FILE_MAX_CHARS) {
20717
21160
  return { ...NO_TEXT, version: `${st.size}-${st.mtimeMs}`, writable: false, binary: false, tooLarge: true };
@@ -20890,7 +21333,7 @@ class OpenService {
20890
21333
  }
20891
21334
  async locate(raw) {
20892
21335
  const path = canonical(resolve7(expandTilde(raw)));
20893
- const st = await stat2(path).catch((e) => {
21336
+ const st = await stat3(path).catch((e) => {
20894
21337
  if (e.code === "ENOENT")
20895
21338
  return null;
20896
21339
  throw e;
@@ -21169,9 +21612,9 @@ class AfterLand {
21169
21612
  }
21170
21613
 
21171
21614
  // ../daemon/src/repos/registry.ts
21172
- import { existsSync as existsSync20, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
21615
+ import { existsSync as existsSync20, mkdirSync as mkdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
21173
21616
  import { homedir as homedir9 } from "os";
21174
- import { basename as basename5, dirname as dirname7, join as join17 } from "path";
21617
+ import { basename as basename5, dirname as dirname7, join as join18 } from "path";
21175
21618
 
21176
21619
  // ../daemon/src/git/base.ts
21177
21620
  async function resolveBase(repoPath, defaultBranch, route) {
@@ -21182,8 +21625,8 @@ async function resolveBase(repoPath, defaultBranch, route) {
21182
21625
  }
21183
21626
 
21184
21627
  // ../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";
21628
+ import { closeSync, existsSync as existsSync17, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync12, readSync, statSync as statSync6 } from "fs";
21629
+ import { join as join15 } from "path";
21187
21630
 
21188
21631
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
21189
21632
  function createScanner(text, ignoreTrivia = false) {
@@ -22547,7 +22990,7 @@ function applyEdits(text, edits) {
22547
22990
 
22548
22991
  // ../daemon/src/repos/config.ts
22549
22992
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
22550
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync17(join14(repoPath, rel)));
22993
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync17(join15(repoPath, rel)));
22551
22994
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
22552
22995
  function parseSettings(text) {
22553
22996
  const errors = [];
@@ -22572,7 +23015,7 @@ function readConfigFile(repoPath) {
22572
23015
  for (const rel of files) {
22573
23016
  let raw;
22574
23017
  try {
22575
- raw = parseSettings(readFileSync12(join14(repoPath, rel), "utf8"));
23018
+ raw = parseSettings(readFileSync12(join15(repoPath, rel), "utf8"));
22576
23019
  } catch (e) {
22577
23020
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
22578
23021
  }
@@ -22631,7 +23074,7 @@ function configTarget(repoPath, made) {
22631
23074
  }
22632
23075
  function configBody(repoPath, rel, config) {
22633
23076
  const place = PLACES.find((p) => p.local === rel);
22634
- const abs = place && join14(repoPath, place.shared);
23077
+ const abs = place && join15(repoPath, place.shared);
22635
23078
  if (!abs || !existsSync17(abs))
22636
23079
  return config;
22637
23080
  let shared;
@@ -22649,7 +23092,7 @@ function configText(repoPath, rel, config) {
22649
23092
  let text;
22650
23093
  let was;
22651
23094
  try {
22652
- text = readFileSync12(join14(repoPath, rel), "utf8");
23095
+ text = readFileSync12(join15(repoPath, rel), "utf8");
22653
23096
  was = parseSettings(text);
22654
23097
  } catch {
22655
23098
  return fresh;
@@ -22679,7 +23122,7 @@ function detectConfig(repoPath) {
22679
23122
  return { config: explicit.config, needsSetup: false };
22680
23123
  if (explicit)
22681
23124
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
22682
- const pkgPath = join14(repoPath, "package.json");
23125
+ const pkgPath = join15(repoPath, "package.json");
22683
23126
  if (existsSync17(pkgPath)) {
22684
23127
  const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
22685
23128
  const scripts = pkg.scripts ?? {};
@@ -22702,7 +23145,7 @@ function detectConfig(repoPath) {
22702
23145
  };
22703
23146
  }
22704
23147
  }
22705
- if (existsSync17(join14(repoPath, "start.sh"))) {
23148
+ if (existsSync17(join15(repoPath, "start.sh"))) {
22706
23149
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
22707
23150
  }
22708
23151
  const assumed = assumeNothingToRun(repoPath);
@@ -22745,24 +23188,24 @@ function assumeNothingToRun(repoPath) {
22745
23188
  return marker && !servesPage(repoPath) ? marker : undefined;
22746
23189
  }
22747
23190
  function markerIn(repoPath) {
22748
- const named = NO_PAGE_MARKERS.find((f) => existsSync17(join14(repoPath, f)));
23191
+ const named = NO_PAGE_MARKERS.find((f) => existsSync17(join15(repoPath, f)));
22749
23192
  if (named)
22750
23193
  return named;
22751
23194
  let entries;
22752
23195
  try {
22753
- entries = readdirSync4(repoPath);
23196
+ entries = readdirSync5(repoPath);
22754
23197
  } catch {
22755
23198
  return;
22756
23199
  }
22757
23200
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
22758
23201
  }
22759
23202
  function servesPage(repoPath) {
22760
- if (WEB_FILES.some((f) => existsSync17(join14(repoPath, f))))
23203
+ if (WEB_FILES.some((f) => existsSync17(join15(repoPath, f))))
22761
23204
  return true;
22762
- if (/^web:/m.test(head(join14(repoPath, "Procfile"))))
23205
+ if (/^web:/m.test(head(join15(repoPath, "Procfile"))))
22763
23206
  return true;
22764
23207
  for (const [files, names] of WEB_DEPENDENCIES) {
22765
- const text = files.map((f) => head(join14(repoPath, f))).join(`
23208
+ const text = files.map((f) => head(join15(repoPath, f))).join(`
22766
23209
  `);
22767
23210
  if (text.trim() && names.some((n) => mentions(text, n)))
22768
23211
  return true;
@@ -22772,14 +23215,14 @@ function servesPage(repoPath) {
22772
23215
  function frontEndBeside(repoPath) {
22773
23216
  let dirs;
22774
23217
  try {
22775
- dirs = readdirSync4(repoPath, { withFileTypes: true });
23218
+ dirs = readdirSync5(repoPath, { withFileTypes: true });
22776
23219
  } catch {
22777
23220
  return false;
22778
23221
  }
22779
23222
  return dirs.some((d) => {
22780
23223
  if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
22781
23224
  return false;
22782
- const pkg = head(join14(repoPath, d.name, "package.json"));
23225
+ const pkg = head(join15(repoPath, d.name, "package.json"));
22783
23226
  if (!pkg)
22784
23227
  return false;
22785
23228
  try {
@@ -22835,11 +23278,11 @@ function isLocalDatabase(url) {
22835
23278
  }
22836
23279
  function detectServices(repoPath) {
22837
23280
  const out = {};
22838
- const compose = COMPOSE_FILES.find((f) => existsSync17(join14(repoPath, f)));
23281
+ const compose = COMPOSE_FILES.find((f) => existsSync17(join15(repoPath, f)));
22839
23282
  if (compose)
22840
23283
  out.compose = compose;
22841
23284
  for (const file of ENV_FILES) {
22842
- const text = head(join14(repoPath, file));
23285
+ const text = head(join15(repoPath, file));
22843
23286
  if (!text)
22844
23287
  continue;
22845
23288
  for (const name of DB_URL_NAMES) {
@@ -22863,7 +23306,7 @@ function inferPaths(cmd, repoPath, depth = 1) {
22863
23306
  return;
22864
23307
  let dir = false;
22865
23308
  try {
22866
- dir = statSync6(join14(repoPath, clean)).isDirectory();
23309
+ dir = statSync6(join15(repoPath, clean)).isDirectory();
22867
23310
  } catch {
22868
23311
  return;
22869
23312
  }
@@ -22874,25 +23317,25 @@ function inferPaths(cmd, repoPath, depth = 1) {
22874
23317
  const t = tokens[i];
22875
23318
  if (t === "cd") {
22876
23319
  const dir = tokens[i + 1];
22877
- if (dir && !dir.startsWith("-") && existsSync17(join14(repoPath, dir)))
23320
+ if (dir && !dir.startsWith("-") && existsSync17(join15(repoPath, dir)))
22878
23321
  add(dir);
22879
23322
  continue;
22880
23323
  }
22881
23324
  if (t.startsWith("-") || t.startsWith("$"))
22882
23325
  continue;
22883
23326
  const mod = /^([A-Za-z_]\w*)(?:\.\w+)*:\w+$/.exec(t);
22884
- if (mod?.[1] && existsSync17(join14(repoPath, mod[1]))) {
23327
+ if (mod?.[1] && existsSync17(join15(repoPath, mod[1]))) {
22885
23328
  add(mod[1]);
22886
23329
  continue;
22887
23330
  }
22888
23331
  const rel = t.replace(/^\.\//, "");
22889
23332
  if (rel.startsWith("/") || rel.startsWith("..") || rel.includes("="))
22890
23333
  continue;
22891
- if (!existsSync17(join14(repoPath, rel)))
23334
+ if (!existsSync17(join15(repoPath, rel)))
22892
23335
  continue;
22893
23336
  add(rel);
22894
23337
  if (depth > 0 && rel.endsWith(".sh")) {
22895
- for (const p of inferPaths(head(join14(repoPath, rel)), repoPath, depth - 1))
23338
+ for (const p of inferPaths(head(join15(repoPath, rel)), repoPath, depth - 1))
22896
23339
  out.add(p);
22897
23340
  }
22898
23341
  }
@@ -22915,9 +23358,9 @@ function procCommand(runner, script, body) {
22915
23358
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
22916
23359
  }
22917
23360
  function detectPages(repoPath, scripts, runner) {
22918
- if (!scripts.build || !existsSync17(join14(repoPath, "functions")))
23361
+ if (!scripts.build || !existsSync17(join15(repoPath, "functions")))
22919
23362
  return null;
22920
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join14(repoPath, f)).find((f) => existsSync17(f));
23363
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join15(repoPath, f)).find((f) => existsSync17(f));
22921
23364
  if (!cfg)
22922
23365
  return null;
22923
23366
  const out = readFileSync12(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
@@ -22926,20 +23369,20 @@ function detectPages(repoPath, scripts, runner) {
22926
23369
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
22927
23370
  }
22928
23371
  function detectRunner(repoPath, packageManager) {
22929
- if (existsSync17(join14(repoPath, "bun.lock")) || existsSync17(join14(repoPath, "bun.lockb")))
23372
+ if (existsSync17(join15(repoPath, "bun.lock")) || existsSync17(join15(repoPath, "bun.lockb")))
22930
23373
  return "bun";
22931
- if (existsSync17(join14(repoPath, "pnpm-lock.yaml")))
23374
+ if (existsSync17(join15(repoPath, "pnpm-lock.yaml")))
22932
23375
  return "pnpm";
22933
- if (existsSync17(join14(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
23376
+ if (existsSync17(join15(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
22934
23377
  return "yarn";
22935
23378
  return "npm";
22936
23379
  }
22937
23380
 
22938
23381
  // ../daemon/src/repos/create.ts
22939
- import { existsSync as existsSync18, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
23382
+ import { existsSync as existsSync18, readdirSync as readdirSync6, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
22940
23383
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
22941
23384
  import { homedir as homedir8 } from "os";
22942
- import { dirname as dirname6, join as join15, resolve as resolve8 } from "path";
23385
+ import { dirname as dirname6, join as join16, resolve as resolve8 } from "path";
22943
23386
  function isInside(dir, parent) {
22944
23387
  const p = parent.replace(/\/+$/, "");
22945
23388
  return dir === p || dir.startsWith(`${p}/`);
@@ -22950,29 +23393,29 @@ function planProject(opts, home = homedir8()) {
22950
23393
  throw new UserError(nameError);
22951
23394
  const first = firstProjectsFolder(opts.parent, home);
22952
23395
  const parent = first ?? resolveParent(opts.parent);
22953
- const dir = join15(parent, opts.name.trim());
23396
+ const dir = join16(parent, opts.name.trim());
22954
23397
  if (existsSync18(dir))
22955
23398
  throw new UserError(`${dir} already exists`);
22956
23399
  return { parent, dir, makeParent: first !== null };
22957
23400
  }
22958
23401
  function firstProjectsFolder(raw, home) {
22959
23402
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
22960
- if (typed !== join15(home, PROJECTS_FOLDER) || existsSync18(typed) || !existsSync18(home))
23403
+ if (typed !== join16(home, PROJECTS_FOLDER) || existsSync18(typed) || !existsSync18(home))
22961
23404
  return null;
22962
- return join15(realpathSync2(home), PROJECTS_FOLDER);
23405
+ return join16(realpathSync2(home), PROJECTS_FOLDER);
22963
23406
  }
22964
23407
  function planInPlace(opts) {
22965
23408
  const name = opts.name.trim();
22966
23409
  if (!name || name === "." || name === ".." || name.includes("/")) {
22967
23410
  throw new UserError(`${opts.name} is not a folder name`);
22968
23411
  }
22969
- const typed = join15(resolveParent(opts.parent), name);
23412
+ const typed = join16(resolveParent(opts.parent), name);
22970
23413
  if (!existsSync18(typed) || !statSync7(typed).isDirectory())
22971
23414
  throw new UserError(`${typed} is not a folder`);
22972
23415
  const dir = realpathSync2(typed);
22973
- if (existsSync18(join15(dir, ".git")))
23416
+ if (existsSync18(join16(dir, ".git")))
22974
23417
  throw new UserError(`${dir} is already a project; open it instead`);
22975
- if (!holdsNothing(readdirSync5(dir))) {
23418
+ if (!holdsNothing(readdirSync6(dir))) {
22976
23419
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
22977
23420
  }
22978
23421
  return { parent: dirname6(dir), dir, makeParent: false };
@@ -23061,13 +23504,13 @@ async function undoCreate(dir) {
23061
23504
  }
23062
23505
  async function undoInit(dir) {
23063
23506
  try {
23064
- await rm(join15(dir, ".git"), { recursive: true, force: true });
23507
+ await rm(join16(dir, ".git"), { recursive: true, force: true });
23065
23508
  } catch (e) {
23066
23509
  log.warn(dir, "could not take back a half-made .git", e);
23067
23510
  }
23068
23511
  }
23069
23512
  async function unmakeProject(dir, made) {
23070
- const target = made === "folder" ? dir : join15(dir, ".git");
23513
+ const target = made === "folder" ? dir : join16(dir, ".git");
23071
23514
  try {
23072
23515
  await rm(target, { recursive: true, force: true });
23073
23516
  } catch (e) {
@@ -23098,7 +23541,7 @@ async function ok(p, what) {
23098
23541
 
23099
23542
  // ../daemon/src/repos/watcher.ts
23100
23543
  import { existsSync as existsSync19, watch } from "fs";
23101
- import { basename as basename4, join as join16 } from "path";
23544
+ import { basename as basename4, join as join17 } from "path";
23102
23545
  function watchDefaultBranch(repoPath, branch, onMove) {
23103
23546
  const last = { local: null, upstream: null };
23104
23547
  const read = async () => {
@@ -23151,13 +23594,13 @@ function watchDefaultBranch(repoPath, branch, onMove) {
23151
23594
  ]);
23152
23595
  if (stopped)
23153
23596
  return;
23154
- const gitDir = common.ok && common.out ? common.out : join16(repoPath, ".git");
23597
+ const gitDir = common.ok && common.out ? common.out : join17(repoPath, ".git");
23155
23598
  tryWatch(gitDir);
23156
- tryWatch(join16(gitDir, "refs", "heads"));
23599
+ tryWatch(join17(gitDir, "refs", "heads"));
23157
23600
  if (!remote.ok || !remote.out)
23158
23601
  return;
23159
- const remotes = join16(gitDir, "refs", "remotes");
23160
- const mine = join16(remotes, remote.out);
23602
+ const remotes = join17(gitDir, "refs", "remotes");
23603
+ const mine = join17(remotes, remote.out);
23161
23604
  let inner = null;
23162
23605
  let mid = null;
23163
23606
  const armInner = () => {
@@ -23173,7 +23616,7 @@ function watchDefaultBranch(repoPath, branch, onMove) {
23173
23616
  }) : null;
23174
23617
  armInner();
23175
23618
  };
23176
- tryWatch(join16(gitDir, "refs"), (_event, filename) => {
23619
+ tryWatch(join17(gitDir, "refs"), (_event, filename) => {
23177
23620
  if (!filename || filename === "remotes")
23178
23621
  armMid();
23179
23622
  schedule();
@@ -23202,7 +23645,7 @@ function watchWorktreeDir(repoPath, onChange) {
23202
23645
  }, 300);
23203
23646
  };
23204
23647
  const arm = (commonDir) => {
23205
- const dir = join16(commonDir, "worktrees");
23648
+ const dir = join17(commonDir, "worktrees");
23206
23649
  const armInner = () => {
23207
23650
  inner?.close();
23208
23651
  inner = null;
@@ -23245,7 +23688,7 @@ function watchWorktreeDir(repoPath, onChange) {
23245
23688
  function watchConfigFile(repoPath, onChange) {
23246
23689
  const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
23247
23690
  const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename4(rel)));
23248
- const dir = join16(repoPath, CONFIG_DIR);
23691
+ const dir = join17(repoPath, CONFIG_DIR);
23249
23692
  let timer = null;
23250
23693
  let outer = null;
23251
23694
  let inner = null;
@@ -23382,7 +23825,7 @@ class RepoRegistry {
23382
23825
  return `${repo.name} is set up now`;
23383
23826
  if (main.promptedAt || main.lastTurn)
23384
23827
  return `${repo.name} has a chat now`;
23385
- if (existsSync20(join17(repo.path, ".claude")))
23828
+ if (existsSync20(join18(repo.path, ".claude")))
23386
23829
  return `an agent has already run in ${repo.name}`;
23387
23830
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
23388
23831
  return `${repo.name} has files in it now`;
@@ -23533,10 +23976,10 @@ class RepoRegistry {
23533
23976
  throw new UserError(current.reason);
23534
23977
  const was = configTarget(repo.path, !!repo.made);
23535
23978
  const rel = configSibling(was, kind);
23536
- const moved = rel !== was && existsSync20(join17(repo.path, was));
23979
+ const moved = rel !== was && existsSync20(join18(repo.path, was));
23537
23980
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
23538
23981
  try {
23539
- rmSync2(join17(repo.path, was));
23982
+ rmSync3(join18(repo.path, was));
23540
23983
  } catch (e) {
23541
23984
  log.warn(repoId, `could not remove ${was}`, e);
23542
23985
  }
@@ -23545,9 +23988,9 @@ class RepoRegistry {
23545
23988
  }
23546
23989
  if (isLocalConfigFile(rel))
23547
23990
  await excludeFromGit(repo.path, rel);
23548
- const file = join17(repo.path, rel);
23991
+ const file = join18(repo.path, rel);
23549
23992
  try {
23550
- mkdirSync6(dirname7(file), { recursive: true });
23993
+ mkdirSync7(dirname7(file), { recursive: true });
23551
23994
  writeFileSync7(file, configText(repo.path, rel, config));
23552
23995
  } catch (e) {
23553
23996
  log.warn(repoId, `could not write ${rel}`, e);
@@ -23649,7 +24092,7 @@ class RepoRegistry {
23649
24092
  }
23650
24093
  placeConfig(repo) {
23651
24094
  const rel = configTarget(repo.path, !!repo.made);
23652
- if (isLocalConfigFile(rel) && existsSync20(join17(repo.path, rel))) {
24095
+ if (isLocalConfigFile(rel) && existsSync20(join18(repo.path, rel))) {
23653
24096
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
23654
24097
  }
23655
24098
  if (repo.configFile === rel)
@@ -23741,7 +24184,7 @@ class RepoRegistry {
23741
24184
  }
23742
24185
 
23743
24186
  // ../daemon/src/routes/service.ts
23744
- import { join as join18 } from "path";
24187
+ import { join as join19 } from "path";
23745
24188
 
23746
24189
  // ../daemon/src/routes/fileRouters.ts
23747
24190
  var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
@@ -24941,7 +25384,7 @@ class RouteService {
24941
25384
  return mergeRoutes(fileRoutes(paths, roots), code);
24942
25385
  }
24943
25386
  async cachedRead(root, path, had, kept, decode) {
24944
- const abs = join18(root, path);
25387
+ const abs = join19(root, path);
24945
25388
  const stat = await this.fs.stat(abs);
24946
25389
  if (!stat)
24947
25390
  return null;
@@ -24966,7 +25409,7 @@ class RouteService {
24966
25409
  }
24967
25410
  }
24968
25411
  async hash(root, file) {
24969
- const abs = join18(root, file);
25412
+ const abs = join19(root, file);
24970
25413
  const stat = await this.fs.stat(abs);
24971
25414
  if (!stat)
24972
25415
  return null;
@@ -25288,7 +25731,7 @@ function servedLocally(origin) {
25288
25731
  import { homedir as homedir11 } from "os";
25289
25732
 
25290
25733
  // ../daemon/src/core/pair.ts
25291
- import { randomBytes as randomBytes3 } from "crypto";
25734
+ import { randomBytes as randomBytes4 } from "crypto";
25292
25735
  class PairCodes {
25293
25736
  now;
25294
25737
  live = null;
@@ -25296,7 +25739,7 @@ class PairCodes {
25296
25739
  this.now = now;
25297
25740
  }
25298
25741
  mint() {
25299
- const code = randomBytes3(9).toString("base64url");
25742
+ const code = randomBytes4(9).toString("base64url");
25300
25743
  this.live = { code, until: this.now() + PAIR_TTL_MS };
25301
25744
  return { code, ms: PAIR_TTL_MS };
25302
25745
  }
@@ -25307,10 +25750,13 @@ class PairCodes {
25307
25750
  }
25308
25751
  }
25309
25752
 
25753
+ // ../daemon/src/server/handlers.ts
25754
+ import { randomBytes as randomBytes5 } from "crypto";
25755
+
25310
25756
  // ../daemon/src/agent/config.ts
25311
25757
  import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
25312
25758
  import { homedir as homedir10 } from "os";
25313
- import { join as join19 } from "path";
25759
+ import { join as join20 } from "path";
25314
25760
  function agentConfigFiles(spec, repoPath, home = homedir10()) {
25315
25761
  const file = (id, label, path) => ({
25316
25762
  id,
@@ -25320,30 +25766,30 @@ function agentConfigFiles(spec, repoPath, home = homedir10()) {
25320
25766
  });
25321
25767
  if (spec.id === "claude") {
25322
25768
  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"))] : []
25769
+ file("user-config", "user config (MCP servers, projects)", join20(home, ".claude.json")),
25770
+ file("user-settings", "user settings (permissions, hooks)", join20(home, ".claude", "settings.json")),
25771
+ ...repoPath ? [file("project-mcp", "project MCP servers", join20(repoPath, ".mcp.json"))] : [],
25772
+ ...repoPath ? [file("project-settings", "project settings", join20(repoPath, ".claude", "settings.json"))] : []
25327
25773
  ];
25328
25774
  }
25329
25775
  if (spec.id === "codex")
25330
- return [file("user-config", "config", join19(home, ".codex", "config.toml"))];
25776
+ return [file("user-config", "config", join20(home, ".codex", "config.toml"))];
25331
25777
  return [];
25332
25778
  }
25333
25779
  function describeAgentConfig(spec, repoPath, home = homedir10()) {
25334
25780
  const files = agentConfigFiles(spec, repoPath, home);
25335
25781
  const servers = [];
25336
25782
  if (spec.id === "claude") {
25337
- const user = readJson(join19(home, ".claude.json"));
25783
+ const user = readJson(join20(home, ".claude.json"));
25338
25784
  servers.push(...serversOf(user?.mcpServers, "user"));
25339
25785
  if (repoPath) {
25340
25786
  const projects = user?.projects;
25341
25787
  const mine = projects?.[repoPath];
25342
25788
  servers.push(...serversOf(mine?.mcpServers, "local"));
25343
- servers.push(...serversOf(readJson(join19(repoPath, ".mcp.json"))?.mcpServers, "project"));
25789
+ servers.push(...serversOf(readJson(join20(repoPath, ".mcp.json"))?.mcpServers, "project"));
25344
25790
  }
25345
25791
  } else if (spec.id === "codex") {
25346
- servers.push(...codexServers(join19(home, ".codex", "config.toml")));
25792
+ servers.push(...codexServers(join20(home, ".codex", "config.toml")));
25347
25793
  }
25348
25794
  return { agent: spec.id, files, servers };
25349
25795
  }
@@ -25981,6 +26427,9 @@ var handlers = {
25981
26427
  s.drafts.set(msg.boxId, msg.text, msg.clientId);
25982
26428
  s.worktrees.spare.typed(msg.boxId, msg.text);
25983
26429
  },
26430
+ "set-attachments"(msg, _ctx, s) {
26431
+ s.drafts.setAttachments(msg.boxId, msg.items, msg.clientId);
26432
+ },
25984
26433
  "refresh-git"(msg, _ctx, s) {
25985
26434
  s.worktrees.recount(msg.repoId);
25986
26435
  },
@@ -25993,19 +26442,17 @@ var handlers = {
25993
26442
  "forget-visit"(msg, _ctx, s) {
25994
26443
  s.routes.forget(msg.repoId, msg.path);
25995
26444
  },
25996
- async chat(msg, _ctx, s) {
25997
- await s.worktrees.send(msg.worktreeId, {
26445
+ async chat(msg, ctx, s) {
26446
+ await sending(s, ctx, msg, msg.attachments, () => s.worktrees.send(msg.worktreeId, {
25998
26447
  text: msg.text,
25999
26448
  clientId: msg.clientId,
26000
26449
  context: msg.context,
26001
26450
  attachments: msg.attachments
26002
- });
26451
+ }));
26003
26452
  },
26004
- async "create-worktree"(msg, _ctx, s) {
26005
- await s.worktrees.create(msg.repoId, msg.prompt, {
26453
+ async "create-worktree"(msg, ctx, s) {
26454
+ const opts = {
26006
26455
  createdBy: msg.clientId,
26007
- worktreeId: msg.worktreeId,
26008
- variant: msg.variant,
26009
26456
  context: msg.context,
26010
26457
  attachments: msg.attachments,
26011
26458
  agent: msg.agent,
@@ -26014,6 +26461,30 @@ var handlers = {
26014
26461
  model: msg.model,
26015
26462
  effort: msg.effort,
26016
26463
  carry: msg.carry
26464
+ };
26465
+ const of = msg.variants ?? 1;
26466
+ await sending(s, ctx, msg, msg.attachments, async () => {
26467
+ if (of === 1) {
26468
+ await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
26469
+ return;
26470
+ }
26471
+ const group = randomBytes5(4).toString("hex");
26472
+ const made = await Promise.allSettled(Array.from({ length: of }, (_, i) => s.worktrees.create(msg.repoId, msg.prompt, {
26473
+ ...opts,
26474
+ ...i === 0 ? { worktreeId: msg.worktreeId } : {},
26475
+ variant: { group, index: i + 1, of }
26476
+ })));
26477
+ const failed = made.flatMap((m) => m.status === "rejected" ? [m.reason] : []);
26478
+ if (failed.length === 0)
26479
+ return;
26480
+ const went = failed.find((e) => e instanceof UserError && e.delivered);
26481
+ if (went)
26482
+ throw went;
26483
+ if (failed.length < of) {
26484
+ const why = failed[0] instanceof Error ? failed[0].message : String(failed[0]);
26485
+ throw new UserError(`${failed.length} of ${of} attempts did not start: ${why}`, { delivered: true });
26486
+ }
26487
+ throw failed[0];
26017
26488
  });
26018
26489
  },
26019
26490
  "set-worktree-profile"(msg, _ctx, s) {
@@ -26036,6 +26507,8 @@ var handlers = {
26036
26507
  },
26037
26508
  "batch-worktrees"(msg, ctx, s) {
26038
26509
  const repo = s.state.requireRepo(msg.repoId);
26510
+ if (msg.boxId)
26511
+ s.drafts.take(msg.boxId);
26039
26512
  fireAndForget(msg.repoId, (async () => {
26040
26513
  const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
26041
26514
  const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
@@ -26059,8 +26532,10 @@ var handlers = {
26059
26532
  "list-archived"(msg, ctx, s) {
26060
26533
  ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
26061
26534
  },
26062
- async "restore-worktree"(msg, _ctx, s) {
26063
- await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
26535
+ async "restore-worktree"(msg, ctx, s) {
26536
+ await sending(s, ctx, msg, msg.message?.attachments, async () => {
26537
+ await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
26538
+ });
26064
26539
  },
26065
26540
  async "delete-archived"(msg, _ctx, s) {
26066
26541
  await s.worktrees.deleteArchived(msg.archiveId);
@@ -26072,10 +26547,17 @@ var handlers = {
26072
26547
  await gitStatus(s, ctx, msg.worktreeId);
26073
26548
  },
26074
26549
  async "read-file"(msg, ctx, s) {
26075
- const head = { t: "file-read", worktreeId: msg.worktreeId, path: msg.path, ref: msg.ref, seq: msg.seq };
26550
+ const head = {
26551
+ t: "file-read",
26552
+ worktreeId: msg.worktreeId,
26553
+ path: msg.path,
26554
+ ref: msg.ref,
26555
+ since: msg.since,
26556
+ seq: msg.seq
26557
+ };
26076
26558
  try {
26077
26559
  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);
26560
+ const read = kept ? keptRead(kept.before, kept.after) : await s.files.read(msg.worktreeId, msg.path, msg.ref, msg.since);
26079
26561
  ctx.reply({ ...head, ...read });
26080
26562
  } catch (e) {
26081
26563
  if (!(e instanceof UserError))
@@ -26188,7 +26670,21 @@ var handlers = {
26188
26670
  },
26189
26671
  unqueue(msg, _ctx, s) {
26190
26672
  requireRun(s, msg.worktreeId);
26191
- s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index);
26673
+ s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index, msg.edit ? (m) => s.drafts.putBack(msg.worktreeId, { text: m.text, items: m.attachments ?? [] }) : undefined);
26674
+ },
26675
+ async redraft(msg, _ctx, s) {
26676
+ const r = s.worktrees.readable(msg.worktreeId);
26677
+ const events = r?.wt ? s.runtime.ensureAgent(r.wt).agent.transcript() : s.worktrees.archivedTranscript(msg.worktreeId) ?? [];
26678
+ const event = events.find((e) => e.seq === msg.seq)?.event;
26679
+ if (event?.type !== "user-message")
26680
+ throw new UserError("that message is no longer in this chat");
26681
+ const items = [];
26682
+ for (const ref of event.attachments ?? []) {
26683
+ const item = await s.attachments.reattach(msg.worktreeId, ref, (file) => s.worktrees.archivedAttachment(msg.worktreeId, file));
26684
+ if (item)
26685
+ items.push(item);
26686
+ }
26687
+ s.drafts.putBack(msg.worktreeId, { text: event.text, items });
26192
26688
  },
26193
26689
  async "changed-ranges"(msg, ctx, s) {
26194
26690
  const { ranges, lineOffset } = await s.files.changedRanges(msg.worktreeId, msg.path);
@@ -26388,6 +26884,10 @@ var handlers = {
26388
26884
  if (!s.runs.stop(msg.worktreeId, msg.kind))
26389
26885
  throw new UserError(`no ${msg.kind} is running here`);
26390
26886
  },
26887
+ "preview-start"(msg, _ctx, s) {
26888
+ requireRun(s, msg.worktreeId);
26889
+ s.worktrees.startPreview(msg.worktreeId);
26890
+ },
26391
26891
  async "search-refs"(msg, ctx, s) {
26392
26892
  const refs = await s.refs.search(msg.repoId, msg.query);
26393
26893
  ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
@@ -26400,6 +26900,30 @@ var handlers = {
26400
26900
  await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
26401
26901
  }
26402
26902
  };
26903
+ async function sending(s, ctx, from, attachments, deliver) {
26904
+ const uploads = s.attachments.uploads;
26905
+ const { boxId, clientId } = from;
26906
+ const named = uploadIds(attachments);
26907
+ const boxed = boxId ? uploadIds(s.drafts.attachments(boxId)) : [];
26908
+ const ids = [...new Set([...named, ...boxed])].filter((id) => uploads.has(id));
26909
+ uploads.hold(ids);
26910
+ const was = boxId ? s.drafts.take(boxId, clientId) : null;
26911
+ try {
26912
+ if (named.some((id) => !uploads.has(id)))
26913
+ throw new UserError(GONE);
26914
+ await deliver();
26915
+ } catch (e) {
26916
+ if (!boxId || !was || e instanceof UserError && e.delivered)
26917
+ throw e;
26918
+ if (!(e instanceof UserError))
26919
+ log.error("ws", "a message reached no agent", e);
26920
+ const items = was.items.filter((a) => !("upload" in a) || uploads.has(a.upload));
26921
+ s.drafts.putBack(boxId, { text: was.text, items }, clientId);
26922
+ ctx.reply({ t: "unsent", boxId, text: was.text, items, message: e instanceof Error ? e.message : String(e) });
26923
+ } finally {
26924
+ uploads.release(ids);
26925
+ }
26926
+ }
26403
26927
  function looseCwd(s, id, stream) {
26404
26928
  const disc = s.worktrees.discoveredById(id);
26405
26929
  if (!disc)
@@ -26425,9 +26949,16 @@ async function dispatch(msg, ctx, s) {
26425
26949
 
26426
26950
  // ../daemon/src/server/http.ts
26427
26951
  import { existsSync as existsSync23 } from "fs";
26428
- import { join as join20 } from "path";
26952
+ import { join as join21 } from "path";
26429
26953
  var IMMUTABLE = "private, max-age=31536000, immutable";
26430
26954
  var NO_STORE = "no-store";
26955
+ function attachedHeaders(imageType) {
26956
+ return {
26957
+ "content-type": imageType ?? "text/plain; charset=utf-8",
26958
+ "x-content-type-options": "nosniff",
26959
+ ...imageType ? {} : { "content-security-policy": "sandbox" }
26960
+ };
26961
+ }
26431
26962
  function createFetch(opts) {
26432
26963
  const grant = previewGrant(opts.token);
26433
26964
  const remote = opts.remote;
@@ -26561,6 +27092,37 @@ function createFetch(opts) {
26561
27092
  return new Response("open failed; see daemon log", { status: 500 });
26562
27093
  }
26563
27094
  }
27095
+ if (url.pathname === "/uploads" && req.method === "POST") {
27096
+ if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
27097
+ return new Response("unauthorized", { status: 401 });
27098
+ }
27099
+ const kind = url.searchParams.get("kind");
27100
+ if (!isUploadKind(kind))
27101
+ return new Response("missing kind", { status: 400 });
27102
+ const mime = (req.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
27103
+ try {
27104
+ const body = req.body;
27105
+ return Response.json(await opts.attachments.uploads.put(kind, mime, body), {
27106
+ headers: { "cache-control": NO_STORE }
27107
+ });
27108
+ } catch (e) {
27109
+ if (e instanceof UserError)
27110
+ return new Response(e.message, { status: 400 });
27111
+ log.error("http", "upload failed", e);
27112
+ return new Response("upload failed; see daemon log", { status: 500 });
27113
+ }
27114
+ }
27115
+ if (url.pathname.startsWith("/uploads/")) {
27116
+ if (!sameSecret(url.searchParams.get("token"), opts.token))
27117
+ return new Response("unauthorized", { status: 401 });
27118
+ const id = url.pathname.slice("/uploads/".length);
27119
+ const path = opts.attachments.uploads.path(id);
27120
+ if (!path || !existsSync23(path))
27121
+ return new Response("not found", { status: 404 });
27122
+ return new Response(Bun.file(path), {
27123
+ headers: { "cache-control": IMMUTABLE, ...attachedHeaders(opts.attachments.uploads.imageType(id)) }
27124
+ });
27125
+ }
26564
27126
  if (url.pathname.startsWith("/attachments/")) {
26565
27127
  if (!sameSecret(url.searchParams.get("token"), opts.token))
26566
27128
  return new Response("unauthorized", { status: 401 });
@@ -26571,7 +27133,9 @@ function createFetch(opts) {
26571
27133
  const path = places.find((p) => !!p && existsSync23(p));
26572
27134
  if (!path)
26573
27135
  return new Response("not found", { status: 404 });
26574
- return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
27136
+ return new Response(Bun.file(path), {
27137
+ headers: { "cache-control": IMMUTABLE, ...attachedHeaders(drawnType(file ?? "")) }
27138
+ });
26575
27139
  }
26576
27140
  if (url.pathname.startsWith("/files/")) {
26577
27141
  if (!sameSecret(url.searchParams.get("token"), opts.token))
@@ -26599,14 +27163,14 @@ function createFetch(opts) {
26599
27163
  });
26600
27164
  }
26601
27165
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
26602
- const file = join20(opts.shellDist, rel.replaceAll("..", ""));
27166
+ const file = join21(opts.shellDist, rel.replaceAll("..", ""));
26603
27167
  const hashed = rel.startsWith("/assets/");
26604
27168
  if (existsSync23(file) && Bun.file(file).size > 0) {
26605
27169
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
26606
27170
  }
26607
27171
  if (hashed)
26608
27172
  return new Response("not found", { status: 404 });
26609
- const index = join20(opts.shellDist, "index.html");
27173
+ const index = join21(opts.shellDist, "index.html");
26610
27174
  if (existsSync23(index))
26611
27175
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
26612
27176
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
@@ -26618,6 +27182,22 @@ var DROP_ABOVE_BYTES = 4 * 1024 * 1024;
26618
27182
  var DROPPED_NOTICE = `\r
26619
27183
  [toyon] output dropped: this pane was too far behind\r
26620
27184
  `;
27185
+ var SENDS = new Set(["chat", "create-worktree", "restore-worktree", "batch-worktrees"]);
27186
+ function sentFrom(json) {
27187
+ if (!json || typeof json !== "object")
27188
+ return null;
27189
+ const { t, boxId } = json;
27190
+ if (typeof t !== "string" || !SENDS.has(t))
27191
+ return null;
27192
+ return typeof boxId === "string" && boxId.length > 0 && boxId.length <= 200 ? boxId : null;
27193
+ }
27194
+ function unparsedReply(json, reason, drafts) {
27195
+ const message = `invalid message: ${reason}`;
27196
+ const boxId = sentFrom(json);
27197
+ if (boxId === null)
27198
+ return { t: "error", message };
27199
+ return { t: "unsent", boxId, text: drafts.text(boxId), items: drafts.attachments(boxId), message };
27200
+ }
26621
27201
  function startServer(opts) {
26622
27202
  const { services: s, token, version } = opts;
26623
27203
  const sockets = new Set;
@@ -26843,6 +27423,7 @@ function startServer(opts) {
26843
27423
  s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
26844
27424
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
26845
27425
  s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
27426
+ s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
26846
27427
  const helloFrame = async () => {
26847
27428
  await s.update.refresh();
26848
27429
  return {
@@ -26869,11 +27450,13 @@ function startServer(opts) {
26869
27450
  self: s.self.get(),
26870
27451
  update: s.update.get(),
26871
27452
  drafts: s.drafts.all(),
27453
+ attachments: s.drafts.allAttachments(),
26872
27454
  managed: managedView(opts.managed)
26873
27455
  };
26874
27456
  };
26875
27457
  let branded = false;
26876
27458
  const serverConfig = {
27459
+ maxRequestBodySize: UPLOAD_MAX_BYTES,
26877
27460
  hostname: cloud.bindHost,
26878
27461
  fetch: createFetch({
26879
27462
  token,
@@ -26901,7 +27484,6 @@ function startServer(opts) {
26901
27484
  }
26902
27485
  }),
26903
27486
  websocket: {
26904
- maxPayloadLength: 64 * 1024 * 1024,
26905
27487
  async open(ws) {
26906
27488
  const preview = ws.data.preview;
26907
27489
  if (preview) {
@@ -26943,7 +27525,7 @@ function startServer(opts) {
26943
27525
  }
26944
27526
  const parsed = parseClientMsg(json);
26945
27527
  if (!parsed.ok) {
26946
- send(ws, { t: "error", message: `invalid message: ${parsed.reason}` });
27528
+ send(ws, unparsedReply(json, parsed.reason, s.drafts));
26947
27529
  return;
26948
27530
  }
26949
27531
  const ctx = {
@@ -27010,16 +27592,16 @@ function startServer(opts) {
27010
27592
  }
27011
27593
 
27012
27594
  // ../daemon/src/themes/store.ts
27013
- import { existsSync as existsSync24, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync9, writeFileSync as writeFileSync8 } from "fs";
27595
+ import { existsSync as existsSync24, readdirSync as readdirSync7, readFileSync as readFileSync15, statSync as statSync9, writeFileSync as writeFileSync8 } from "fs";
27014
27596
  import { homedir as homedir12 } from "os";
27015
- import { basename as basename6, dirname as dirname8, join as join21, resolve as resolve9 } from "path";
27597
+ import { basename as basename6, dirname as dirname8, join as join22, resolve as resolve9 } from "path";
27016
27598
  var home = homedir12();
27017
27599
  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"),
27600
+ join22(home, ".vscode", "extensions"),
27601
+ join22(home, ".vscode-insiders", "extensions"),
27602
+ join22(home, ".vscode-oss", "extensions"),
27603
+ join22(home, ".cursor", "extensions"),
27604
+ join22(home, ".windsurf", "extensions"),
27023
27605
  "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
27024
27606
  "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
27025
27607
  "/Applications/Cursor.app/Contents/Resources/app/extensions",
@@ -27103,7 +27685,7 @@ class ThemeStore {
27103
27685
  name: json?.name ?? base,
27104
27686
  source: "file"
27105
27687
  });
27106
- writeFileSync8(join21(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
27688
+ writeFileSync8(join22(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
27107
27689
  `);
27108
27690
  this.load();
27109
27691
  return theme;
@@ -27135,10 +27717,10 @@ function loadDir(dir) {
27135
27717
  if (!existsSync24(dir))
27136
27718
  return [];
27137
27719
  const out = [];
27138
- for (const f of readdirSync6(dir)) {
27720
+ for (const f of readdirSync7(dir)) {
27139
27721
  if (!/\.jsonc?$/i.test(f))
27140
27722
  continue;
27141
- const file = join21(dir, f);
27723
+ const file = join22(dir, f);
27142
27724
  const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
27143
27725
  try {
27144
27726
  const json = readJsonc(file);
@@ -27159,13 +27741,13 @@ function discover(dir) {
27159
27741
  const out = [];
27160
27742
  let entries;
27161
27743
  try {
27162
- entries = readdirSync6(dir);
27744
+ entries = readdirSync7(dir);
27163
27745
  } catch {
27164
27746
  return out;
27165
27747
  }
27166
27748
  for (const name of entries) {
27167
- const extDir = join21(dir, name);
27168
- const pkgFile = join21(extDir, "package.json");
27749
+ const extDir = join22(dir, name);
27750
+ const pkgFile = join22(extDir, "package.json");
27169
27751
  try {
27170
27752
  if (!statSync9(extDir).isDirectory() || !existsSync24(pkgFile))
27171
27753
  continue;
@@ -27175,7 +27757,7 @@ function discover(dir) {
27175
27757
  continue;
27176
27758
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
27177
27759
  let nls = {};
27178
- const nlsFile = join21(extDir, "package.nls.json");
27760
+ const nlsFile = join22(extDir, "package.nls.json");
27179
27761
  if (existsSync24(nlsFile)) {
27180
27762
  try {
27181
27763
  nls = JSON.parse(readFileSync15(nlsFile, "utf8"));
@@ -27591,8 +28173,8 @@ class BackendShare {
27591
28173
  // ../daemon/src/worktrees/cache.ts
27592
28174
  import { createHash as createHash3 } from "crypto";
27593
28175
  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";
28176
+ import { mkdir as mkdir4, readdir as readdir2, rename, rm as rm3, stat as stat4, utimes, writeFile as writeFile2 } from "fs/promises";
28177
+ import { join as join23 } from "path";
27596
28178
 
27597
28179
  // ../daemon/src/core/clone.ts
27598
28180
  import { mkdir as mkdir3, rm as rm2 } from "fs/promises";
@@ -27623,7 +28205,7 @@ var STALE_MS = 30 * 24 * 3600000;
27623
28205
  var TMP = ".tmp-";
27624
28206
  var MANIFEST = "manifest.json";
27625
28207
  function defaultTools(dir) {
27626
- const has = (...files) => files.some((f) => existsSync25(join22(dir, f)));
28208
+ const has = (...files) => files.some((f) => existsSync25(join23(dir, f)));
27627
28209
  const out = [];
27628
28210
  if (has("bun.lock", "bun.lockb"))
27629
28211
  out.push("bun --version");
@@ -27636,7 +28218,7 @@ function defaultTools(dir) {
27636
28218
  return out;
27637
28219
  }
27638
28220
  function withSidecars(root, rel) {
27639
- return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync25(join22(root, n)));
28221
+ return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync25(join23(root, n)));
27640
28222
  }
27641
28223
  var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
27642
28224
  async function listEntries(familyDir) {
@@ -27651,8 +28233,8 @@ async function listEntries(familyDir) {
27651
28233
  const m = /^([0-9a-f]{12})-([0-9a-z]+)$/.exec(name);
27652
28234
  if (!m?.[1] || !m[2])
27653
28235
  continue;
27654
- const dir = join22(familyDir, name);
27655
- if (!existsSync25(join22(dir, MANIFEST)))
28236
+ const dir = join23(familyDir, name);
28237
+ if (!existsSync25(join23(dir, MANIFEST)))
27656
28238
  continue;
27657
28239
  out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
27658
28240
  }
@@ -27682,7 +28264,7 @@ class ArtifactCache {
27682
28264
  try {
27683
28265
  const family = await this.family(wt, repo, policy);
27684
28266
  const base = await this.baseCommit(wt, repo);
27685
- const entries = await listEntries(join22(this.d.paths.cacheDir, repo.id, family.key));
28267
+ const entries = await listEntries(join23(this.d.paths.cacheDir, repo.id, family.key));
27686
28268
  const entry = entries.find((e) => e.base === base.slice(0, 12)) ?? entries[0];
27687
28269
  if (!entry) {
27688
28270
  say(`cache: nothing kept for these lockfiles and tools on ${family.platform}`);
@@ -27690,14 +28272,14 @@ class ArtifactCache {
27690
28272
  }
27691
28273
  const restored = [];
27692
28274
  for (const rel of policy.paths) {
27693
- if (existsSync25(join22(wt.path, rel)))
28275
+ if (existsSync25(join23(wt.path, rel)))
27694
28276
  continue;
27695
28277
  const names = withSidecars(entry.dir, rel);
27696
28278
  if (names.length === 0)
27697
28279
  continue;
27698
28280
  let whole = true;
27699
28281
  for (const name of names) {
27700
- const r = await (this.d.clone ?? cloneTree)(join22(entry.dir, name), join22(wt.path, name));
28282
+ const r = await (this.d.clone ?? cloneTree)(join23(entry.dir, name), join23(wt.path, name));
27701
28283
  if (r.ok)
27702
28284
  continue;
27703
28285
  say(`cache: could not restore ${name}: ${r.error}`);
@@ -27731,12 +28313,12 @@ class ArtifactCache {
27731
28313
  let tmp;
27732
28314
  let slot;
27733
28315
  try {
27734
- const present = policy.paths.filter((rel) => existsSync25(join22(wt.path, rel)));
28316
+ const present = policy.paths.filter((rel) => existsSync25(join23(wt.path, rel)));
27735
28317
  if (present.length === 0)
27736
28318
  return;
27737
28319
  const family = await this.family(wt, repo, policy);
27738
28320
  const base = await this.baseCommit(wt, repo);
27739
- const familyDir = join22(this.d.paths.cacheDir, repo.id, family.key);
28321
+ const familyDir = join23(this.d.paths.cacheDir, repo.id, family.key);
27740
28322
  const key = `${familyDir}/${base.slice(0, 12)}`;
27741
28323
  const kept = (await listEntries(familyDir)).find((e) => e.base === base.slice(0, 12));
27742
28324
  if (kept || this.making.has(key)) {
@@ -27747,21 +28329,21 @@ class ArtifactCache {
27747
28329
  this.making.add(key);
27748
28330
  const at = Date.now();
27749
28331
  const name = entryName(base, at);
27750
- tmp = join22(familyDir, `${TMP}${name}`);
28332
+ tmp = join23(familyDir, `${TMP}${name}`);
27751
28333
  await mkdir4(tmp, { recursive: true });
27752
28334
  let how = "clonefile";
27753
28335
  for (const rel of present) {
27754
28336
  for (const n of withSidecars(wt.path, rel)) {
27755
- const r = await (this.d.clone ?? cloneTree)(join22(wt.path, n), join22(tmp, n));
28337
+ const r = await (this.d.clone ?? cloneTree)(join23(wt.path, n), join23(tmp, n));
27756
28338
  if (!r.ok)
27757
28339
  throw new Error(`could not keep ${n}: ${r.error}`);
27758
28340
  how = r.how;
27759
28341
  }
27760
28342
  }
27761
28343
  const manifest = { at, base, platform: family.platform, tools: family.tools, paths: present };
27762
- await writeFile2(join22(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
28344
+ await writeFile2(join23(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
27763
28345
  `);
27764
- await rename(tmp, join22(familyDir, name));
28346
+ await rename(tmp, join23(familyDir, name));
27765
28347
  tmp = undefined;
27766
28348
  log.info(worktreeId, `cache: kept ${list(present)} for ${base.slice(0, 7)} via ${how} in ${Date.now() - started}ms`);
27767
28349
  await this.prune(familyDir);
@@ -27789,7 +28371,7 @@ class ArtifactCache {
27789
28371
  return;
27790
28372
  }
27791
28373
  for (const repoId of repoIds) {
27792
- const repoDir = join22(root, repoId);
28374
+ const repoDir = join23(root, repoId);
27793
28375
  if (!this.d.state.repo(repoId)) {
27794
28376
  await rm3(repoDir, { recursive: true, force: true });
27795
28377
  continue;
@@ -27801,7 +28383,7 @@ class ArtifactCache {
27801
28383
  continue;
27802
28384
  }
27803
28385
  for (const family of families) {
27804
- const familyDir = join22(repoDir, family);
28386
+ const familyDir = join23(repoDir, family);
27805
28387
  let names;
27806
28388
  try {
27807
28389
  names = await readdir2(familyDir);
@@ -27810,11 +28392,11 @@ class ArtifactCache {
27810
28392
  }
27811
28393
  for (const n of names)
27812
28394
  if (n.startsWith(TMP))
27813
- await rm3(join22(familyDir, n), { recursive: true, force: true });
28395
+ await rm3(join23(familyDir, n), { recursive: true, force: true });
27814
28396
  const entries = await listEntries(familyDir);
27815
28397
  let newest = 0;
27816
28398
  for (const e of entries) {
27817
- const s = await stat3(e.dir).catch(() => null);
28399
+ const s = await stat4(e.dir).catch(() => null);
27818
28400
  if (s)
27819
28401
  newest = Math.max(newest, s.mtimeMs);
27820
28402
  }
@@ -27830,7 +28412,7 @@ class ArtifactCache {
27830
28412
  for (const f of policy.key) {
27831
28413
  h.update(`\x00${f}\x00`);
27832
28414
  try {
27833
- h.update(readFileSync16(join22(wt.path, f)));
28415
+ h.update(readFileSync16(join23(wt.path, f)));
27834
28416
  } catch {
27835
28417
  h.update("(absent)");
27836
28418
  }
@@ -28660,12 +29242,12 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
28660
29242
  }
28661
29243
 
28662
29244
  // ../daemon/src/worktrees/service.ts
28663
- import { cpSync, existsSync as existsSync28, readFileSync as readFileSync18, rmSync as rmSync5 } from "fs";
29245
+ import { cpSync, existsSync as existsSync29, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
28664
29246
  import { rm as rm4 } from "fs/promises";
28665
- import { dirname as dirname11, join as join25 } from "path";
29247
+ import { dirname as dirname11, join as join27 } from "path";
28666
29248
 
28667
29249
  // ../daemon/src/git/archive.ts
28668
- import { rmSync as rmSync3 } from "fs";
29250
+ import { rmSync as rmSync4 } from "fs";
28669
29251
  var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
28670
29252
  var SNAPSHOT_SUBJECT = "toyon: uncommitted work";
28671
29253
  var SNAPSHOT_AUTHOR = "toyon";
@@ -28708,7 +29290,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
28708
29290
  ], wtPath);
28709
29291
  return commit.ok ? commit.out : null;
28710
29292
  } finally {
28711
- rmSync3(indexFile, { force: true });
29293
+ rmSync4(indexFile, { force: true });
28712
29294
  }
28713
29295
  }
28714
29296
  async function keptAt(repoPath, ref) {
@@ -29183,18 +29765,142 @@ function compareUrl(remoteUrl, base, branch) {
29183
29765
  return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
29184
29766
  }
29185
29767
 
29768
+ // ../daemon/src/git/turn.ts
29769
+ import { copyFileSync, existsSync as existsSync26, rmSync as rmSync5 } from "fs";
29770
+ import { join as join24 } from "path";
29771
+ var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
29772
+ var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
29773
+ var queue = new Map;
29774
+ function inTurn(wtPath, job) {
29775
+ const next = (queue.get(wtPath) ?? Promise.resolve()).then(job, job);
29776
+ queue.set(wtPath, next.catch(() => {}));
29777
+ return next;
29778
+ }
29779
+ var synced = new Map;
29780
+ var MAX_PATHS = 400;
29781
+ async function treeNow(wtPath, head, dirty) {
29782
+ let was = synced.get(wtPath);
29783
+ let index = was?.index;
29784
+ if (!index) {
29785
+ const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
29786
+ if (!dir.ok || !dir.out)
29787
+ return null;
29788
+ index = join24(dir.out, "toyon-turn-index");
29789
+ }
29790
+ const env = { GIT_INDEX_FILE: index };
29791
+ const writeTree = async () => {
29792
+ const tree = await run(GIT, ["write-tree"], wtPath, env);
29793
+ return tree.ok && tree.out ? tree.out : null;
29794
+ };
29795
+ if (!existsSync26(index))
29796
+ was = undefined;
29797
+ const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
29798
+ if (was && paths && was.head === head) {
29799
+ const again = [...new Set([...was.dirty, ...paths])];
29800
+ if (again.length <= MAX_PATHS) {
29801
+ const added = again.length === 0 || (await run(GIT, ["--literal-pathspecs", "add", "-A", "--", ...again], wtPath, env)).ok;
29802
+ const tree = added ? await writeTree() : null;
29803
+ if (tree) {
29804
+ synced.set(wtPath, { index, head, dirty: new Set(paths) });
29805
+ return tree;
29806
+ }
29807
+ }
29808
+ }
29809
+ synced.delete(wtPath);
29810
+ if (!existsSync26(index)) {
29811
+ const own = join24(index, "..", "index");
29812
+ if (existsSync26(own))
29813
+ copyFileSync(own, index);
29814
+ else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
29815
+ return null;
29816
+ }
29817
+ const full = async () => (await run(GIT, ["add", "-A"], wtPath, env)).ok ? writeTree() : null;
29818
+ let tree = await full();
29819
+ if (!tree) {
29820
+ rmSync5(index, { force: true });
29821
+ if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
29822
+ return null;
29823
+ tree = await full();
29824
+ if (!tree)
29825
+ return null;
29826
+ }
29827
+ const differs = await git(wtPath, "diff", "--name-only", "--no-renames", "-z", head, tree);
29828
+ if (differs.ok)
29829
+ synced.set(wtPath, { index, head, dirty: new Set(differs.out.split("\x00").filter(Boolean)) });
29830
+ return tree;
29831
+ }
29832
+ async function snapshotAt(wtPath, ref) {
29833
+ const r = await git(wtPath, "rev-parse", "--quiet", `${ref}^{commit}`, `${ref}^{tree}`, `${ref}^`);
29834
+ const [commit, tree, parent] = r.ok ? r.out.split(`
29835
+ `) : [];
29836
+ return commit && tree && parent ? { commit, tree, parent } : null;
29837
+ }
29838
+ function markTurn(wtPath, worktreeId) {
29839
+ return inTurn(wtPath, async () => {
29840
+ const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
29841
+ if (!head.ok)
29842
+ return;
29843
+ const tree = await treeNow(wtPath, head.out);
29844
+ if (!tree)
29845
+ return;
29846
+ const at = await snapshotAt(wtPath, turnRef(worktreeId, "at"));
29847
+ if (at?.tree === tree && at.parent === head.out)
29848
+ return;
29849
+ const commit = await git(wtPath, "-c", "user.name=toyon", "-c", "user.email=toyon@localhost", "commit-tree", tree, "-p", head.out, "-m", SNAPSHOT_SUBJECT2);
29850
+ if (!commit.ok)
29851
+ return;
29852
+ if (at && at.tree !== tree)
29853
+ await git(wtPath, "update-ref", turnRef(worktreeId, "before"), at.commit);
29854
+ await git(wtPath, "update-ref", turnRef(worktreeId, "at"), commit.out);
29855
+ });
29856
+ }
29857
+ function turnChanges(wtPath, worktreeId, defaultBr, own, dirty) {
29858
+ return inTurn(wtPath, async () => {
29859
+ const at = await snapshotAt(wtPath, turnRef(worktreeId, "at"));
29860
+ if (!at)
29861
+ return null;
29862
+ const head = await git(wtPath, "rev-parse", "HEAD", "HEAD^{tree}");
29863
+ const [headCommit, headTree] = head.ok ? head.out.split(`
29864
+ `) : [];
29865
+ if (!headCommit)
29866
+ return null;
29867
+ const tree = await treeNow(wtPath, headCommit, dirty);
29868
+ if (!tree)
29869
+ return null;
29870
+ const from = at.tree !== tree ? at : await snapshotAt(wtPath, turnRef(worktreeId, "before"));
29871
+ if (!from || from.tree === tree || from.tree === headTree)
29872
+ return null;
29873
+ if (from.parent !== headCommit) {
29874
+ if (!own)
29875
+ return null;
29876
+ const [then, now] = await Promise.all([
29877
+ git(wtPath, "merge-base", from.parent, defaultBr),
29878
+ git(wtPath, "merge-base", headCommit, defaultBr)
29879
+ ]);
29880
+ if (!then.ok || !now.ok || then.out !== now.out)
29881
+ return null;
29882
+ }
29883
+ const files = await filesBetween(wtPath, from.commit, tree);
29884
+ return files.length > 0 ? { base: from.commit, files } : null;
29885
+ });
29886
+ }
29887
+ async function dropTurnRefs(repoPath, worktreeId) {
29888
+ for (const which of ["at", "before"])
29889
+ await git(repoPath, "update-ref", "-d", turnRef(worktreeId, which));
29890
+ }
29891
+
29186
29892
  // ../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";
29893
+ import { existsSync as existsSync27, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
29894
+ import { basename as basename8, dirname as dirname10, join as join25 } from "path";
29189
29895
  class WorktreeArchive {
29190
29896
  dir;
29191
29897
  records = new Map;
29192
29898
  constructor(dir) {
29193
29899
  this.dir = dir;
29194
- mkdirSync7(dir, { recursive: true });
29195
- for (const id of readdirSync7(dir)) {
29900
+ mkdirSync8(dir, { recursive: true });
29901
+ for (const id of readdirSync8(dir)) {
29196
29902
  const file = this.filesOf(id).record;
29197
- if (!existsSync26(file))
29903
+ if (!existsSync27(file))
29198
29904
  continue;
29199
29905
  try {
29200
29906
  this.records.set(id, JSON.parse(readFileSync17(file, "utf8")));
@@ -29208,7 +29914,7 @@ class WorktreeArchive {
29208
29914
  }
29209
29915
  put(rec, from) {
29210
29916
  const at = this.filesOf(rec.worktree.id);
29211
- mkdirSync7(at.dir, { recursive: true });
29917
+ mkdirSync8(at.dir, { recursive: true });
29212
29918
  move(from.transcript, at.transcript);
29213
29919
  move(from.attachments, at.attachments);
29214
29920
  move(from.plans, at.plans);
@@ -29228,7 +29934,7 @@ class WorktreeArchive {
29228
29934
  delete(id) {
29229
29935
  if (!this.records.has(id))
29230
29936
  return;
29231
- rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
29937
+ rmSync6(this.filesOf(id).dir, { recursive: true, force: true });
29232
29938
  this.records.delete(id);
29233
29939
  }
29234
29940
  list(repo) {
@@ -29243,8 +29949,8 @@ class WorktreeArchive {
29243
29949
  planFile(id, rel) {
29244
29950
  if (!this.records.has(id) || !isPlanPath(rel))
29245
29951
  return null;
29246
- const file = join23(this.filesOf(id).plans, basename8(rel));
29247
- if (!existsSync26(file))
29952
+ const file = join25(this.filesOf(id).plans, basename8(rel));
29953
+ if (!existsSync27(file))
29248
29954
  return null;
29249
29955
  try {
29250
29956
  return readFileSync17(file, "utf8");
@@ -29254,13 +29960,13 @@ class WorktreeArchive {
29254
29960
  }
29255
29961
  }
29256
29962
  filesOf(id) {
29257
- const dir = join23(this.dir, id);
29963
+ const dir = join25(this.dir, id);
29258
29964
  return {
29259
29965
  dir,
29260
- record: join23(dir, "record.json"),
29261
- transcript: join23(dir, "transcript.jsonl"),
29262
- attachments: join23(dir, "attachments"),
29263
- plans: join23(dir, "plans")
29966
+ record: join25(dir, "record.json"),
29967
+ transcript: join25(dir, "transcript.jsonl"),
29968
+ attachments: join25(dir, "attachments"),
29969
+ plans: join25(dir, "plans")
29264
29970
  };
29265
29971
  }
29266
29972
  }
@@ -29285,7 +29991,7 @@ function summarize(r, repoId, transcript) {
29285
29991
  };
29286
29992
  }
29287
29993
  function lastUsage(transcript) {
29288
- if (!existsSync26(transcript))
29994
+ if (!existsSync27(transcript))
29289
29995
  return null;
29290
29996
  const lines = readFileSync17(transcript, "utf8").split(`
29291
29997
  `);
@@ -29301,7 +30007,7 @@ function lastUsage(transcript) {
29301
30007
  return null;
29302
30008
  }
29303
30009
  function firstPrompt(transcript) {
29304
- if (!existsSync26(transcript))
30010
+ if (!existsSync27(transcript))
29305
30011
  return;
29306
30012
  for (const line of readFileSync17(transcript, "utf8").split(`
29307
30013
  `)) {
@@ -29316,10 +30022,10 @@ function firstPrompt(transcript) {
29316
30022
  return;
29317
30023
  }
29318
30024
  function move(from, to) {
29319
- if (!existsSync26(from))
30025
+ if (!existsSync27(from))
29320
30026
  return;
29321
- mkdirSync7(dirname10(to), { recursive: true });
29322
- rmSync4(to, { recursive: true, force: true });
30027
+ mkdirSync8(dirname10(to), { recursive: true });
30028
+ rmSync6(to, { recursive: true, force: true });
29323
30029
  renameSync4(from, to);
29324
30030
  }
29325
30031
 
@@ -29381,8 +30087,8 @@ class ArchivedGit {
29381
30087
  }
29382
30088
 
29383
30089
  // ../daemon/src/worktrees/spare.ts
29384
- import { existsSync as existsSync27 } from "fs";
29385
- import { basename as basename9, join as join24 } from "path";
30090
+ import { existsSync as existsSync28 } from "fs";
30091
+ import { basename as basename9, join as join26 } from "path";
29386
30092
  class SparePool {
29387
30093
  d;
29388
30094
  inflight = new Map;
@@ -29401,7 +30107,7 @@ class SparePool {
29401
30107
  if (!text.trim())
29402
30108
  return;
29403
30109
  const wt = this.d.state.worktree(boxId);
29404
- if (wt?.kind !== "spare" || !existsSync27(wt.path))
30110
+ if (wt?.kind !== "spare" || !existsSync28(wt.path))
29405
30111
  return;
29406
30112
  const agent = this.d.runtime.ensureAgent(wt).agent;
29407
30113
  if (agent.runningAgent === null)
@@ -29446,11 +30152,11 @@ class SparePool {
29446
30152
  return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
29447
30153
  }
29448
30154
  async record(repoId, repo) {
29449
- const { id, dir } = freeSlot(join24(this.d.paths.worktreesDir, repo.name));
30155
+ const { id, dir } = freeSlot(join26(this.d.paths.worktreesDir, repo.name));
29450
30156
  const wt = {
29451
30157
  id,
29452
30158
  repoId,
29453
- path: join24(this.d.paths.worktreesDir, repo.name, dir),
30159
+ path: join26(this.d.paths.worktreesDir, repo.name, dir),
29454
30160
  branch: repo.defaultBranch,
29455
30161
  kind: "spare",
29456
30162
  phase: "reserved",
@@ -29465,7 +30171,7 @@ class SparePool {
29465
30171
  const repoId = repo.id;
29466
30172
  let cut = () => {};
29467
30173
  const added = (async () => {
29468
- if (existsSync27(wt.path))
30174
+ if (existsSync28(wt.path))
29469
30175
  return;
29470
30176
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
29471
30177
  })();
@@ -29513,7 +30219,7 @@ class SparePool {
29513
30219
  }
29514
30220
  log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
29515
30221
  this.d.carryDraft?.(wt.id, wt.repoId);
29516
- if (existsSync27(wt.path)) {
30222
+ if (existsSync28(wt.path)) {
29517
30223
  await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
29518
30224
  return;
29519
30225
  }
@@ -29570,7 +30276,7 @@ class SparePool {
29570
30276
  const running = this.inflight.get(repoId);
29571
30277
  await running?.added.catch(() => {});
29572
30278
  running?.cut();
29573
- if (wt.kind !== "spare" || !existsSync27(wt.path))
30279
+ if (wt.kind !== "spare" || !existsSync28(wt.path))
29574
30280
  return null;
29575
30281
  }
29576
30282
  await this.refreshing.get(repoId);
@@ -29616,7 +30322,7 @@ class Trunk {
29616
30322
  const last = this.lastFetch.get(path) ?? 0;
29617
30323
  if (Date.now() - last > FETCH_EVERY_MS) {
29618
30324
  this.lastFetch.set(path, Date.now());
29619
- fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then(async (r) => {
30325
+ fireAndForget("fetch", withFetchLock(path, () => run(GIT, ["fetch", "--quiet"], path, NO_PROMPT)).then(async (r) => {
29620
30326
  const main = this.d.state.worktree(mainId);
29621
30327
  if (main)
29622
30328
  this.noteFetch(main.repoId, r);
@@ -29641,7 +30347,7 @@ class Trunk {
29641
30347
  if (Date.now() - last < OPEN_FETCH_MIN_MS)
29642
30348
  return;
29643
30349
  this.lastFetch.set(repo.path, Date.now());
29644
- const f = await run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT);
30350
+ const f = await withFetchLock(repo.path, () => run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT));
29645
30351
  this.noteFetch(repo.id, f);
29646
30352
  if (!f.ok) {
29647
30353
  log.warn("fetch", `could not fetch ${repo.path}: ${f.err.slice(0, 200)}`);
@@ -29665,7 +30371,7 @@ class Trunk {
29665
30371
  return { ok: false, message: "no main checkout to pull" };
29666
30372
  this.lastFetch.set(repo.path, Date.now());
29667
30373
  w.step(`pulling ${repo.defaultBranch} from origin`);
29668
- const f = await w.git(repo.path, ["fetch", "--quiet"]);
30374
+ const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
29669
30375
  this.noteFetch(repo.id, f);
29670
30376
  if (!f.ok)
29671
30377
  return { ok: false, message: refused("fetch failed", f) };
@@ -29867,10 +30573,9 @@ class WorktreeService {
29867
30573
  await this.discardWorktree(id);
29868
30574
  },
29869
30575
  carryDraft: (fromId, repoId) => {
29870
- const text = this.d.drafts?.text(fromId);
29871
30576
  const main = this.mainOf(repoId);
29872
- if (text && main)
29873
- this.d.drafts?.set(main.id, text);
30577
+ if (main)
30578
+ this.d.drafts?.move(fromId, main.id, true);
29874
30579
  },
29875
30580
  ready: (repoId) => {
29876
30581
  const main = this.mainOf(repoId);
@@ -29879,15 +30584,15 @@ class WorktreeService {
29879
30584
  if (this.d.runtime.get(main.id)?.procs) {
29880
30585
  fireAndForget(main.id, this.d.runtime.stopPage(main.id), "main's page stops for the spare");
29881
30586
  }
29882
- const text = this.d.drafts?.text(main.id);
29883
30587
  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
- }
30588
+ if (spare)
30589
+ this.d.drafts?.move(main.id, spare);
29888
30590
  }
29889
30591
  });
29890
30592
  d.hub.on("agent", (worktreeId, _seq, event) => {
30593
+ if ((event.type === "tool-start" || event.type === "tool-update" && event.kind) && isWriteTool({ name: event.name ?? "", kind: event.kind })) {
30594
+ this.startPreview(worktreeId);
30595
+ }
29891
30596
  if (event.type !== "usage")
29892
30597
  return;
29893
30598
  const { used, size, cost } = event;
@@ -29952,14 +30657,14 @@ class WorktreeService {
29952
30657
  rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
29953
30658
  this.scheduleNaming(claimed, task, variant);
29954
30659
  if (carried.unmoved)
29955
- throw new UserError(carried.unmoved);
30660
+ throw new UserError(carried.unmoved, { delivered: true });
29956
30661
  return claimed;
29957
30662
  }
29958
30663
  if (!await isGitRepo(repo.path)) {
29959
30664
  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
30665
  }
29961
- const { id, dir } = freeSlot(join25(this.d.paths.worktreesDir, repo.name));
29962
- const wtPath = join25(this.d.paths.worktreesDir, repo.name, dir);
30666
+ const { id, dir } = freeSlot(join27(this.d.paths.worktreesDir, repo.name));
30667
+ const wtPath = join27(this.d.paths.worktreesDir, repo.name, dir);
29963
30668
  const branch = `toyon/${dir}`;
29964
30669
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
29965
30670
  const wt = {
@@ -29986,7 +30691,7 @@ class WorktreeService {
29986
30691
  this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
29987
30692
  this.scheduleNaming(wt, task, variant);
29988
30693
  if (carried.unmoved)
29989
- throw new UserError(carried.unmoved);
30694
+ throw new UserError(carried.unmoved, { delivered: true });
29990
30695
  return wt;
29991
30696
  }
29992
30697
  async carryMain(repo, wt, move) {
@@ -30025,7 +30730,7 @@ class WorktreeService {
30025
30730
  };
30026
30731
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
30027
30732
  const restored = (carried.ok ? carried.out.split(`
30028
- `).filter(Boolean) : []).filter((p) => !existsSync28(join25(wt.path, p)));
30733
+ `).filter(Boolean) : []).filter((p) => !existsSync29(join27(wt.path, p)));
30029
30734
  const applied = await git(wt.path, "stash", "apply", sha);
30030
30735
  if (applied.ok) {
30031
30736
  const ref = await entry();
@@ -30061,9 +30766,9 @@ class WorktreeService {
30061
30766
  const branch = kind === "pr" ? `pr/${number}` : ref;
30062
30767
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
30063
30768
  let slug = branchSlug(title) || "branch";
30064
- if (existsSync28(join25(this.d.paths.worktreesDir, repo.name, slug)))
30769
+ if (existsSync29(join27(this.d.paths.worktreesDir, repo.name, slug)))
30065
30770
  slug = `${slug}-${shortId().slice(0, 4)}`;
30066
- const wtPath = join25(this.d.paths.worktreesDir, repo.name, slug);
30771
+ const wtPath = join27(this.d.paths.worktreesDir, repo.name, slug);
30067
30772
  await withRepoLock(repo.path, async () => {
30068
30773
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
30069
30774
  if (out)
@@ -30283,7 +30988,7 @@ class WorktreeService {
30283
30988
  try {
30284
30989
  await this.removeDirectory(repo, wt);
30285
30990
  } catch (e) {
30286
- rmSync5(this.plansScratch(wt.id), { recursive: true, force: true });
30991
+ rmSync7(this.plansScratch(wt.id), { recursive: true, force: true });
30287
30992
  throw e;
30288
30993
  }
30289
30994
  if (hasOwnBranch(wt)) {
@@ -30297,6 +31002,7 @@ class WorktreeService {
30297
31002
  this.d.state.removeWorktree(worktreeId);
30298
31003
  releasePort(wt.proxyPort);
30299
31004
  let archived = null;
31005
+ await dropTurnRefs(repo.path, worktreeId);
30300
31006
  if (archive)
30301
31007
  archived = this.archiveChat(wt, repo, kept, sessionId, reason);
30302
31008
  else {
@@ -30309,7 +31015,7 @@ class WorktreeService {
30309
31015
  }
30310
31016
  async teardown(wt, repo) {
30311
31017
  const cmds = repo.config.teardown ?? [];
30312
- if (cmds.length === 0 || !existsSync28(wt.path))
31018
+ if (cmds.length === 0 || !existsSync29(wt.path))
30313
31019
  return;
30314
31020
  for (const cmd of cmds) {
30315
31021
  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 +31024,7 @@ class WorktreeService {
30318
31024
  }
30319
31025
  }
30320
31026
  async keep(repo, wt) {
30321
- const index = join25(this.d.paths.archiveDir, `${wt.id}.index`);
31027
+ const index = join27(this.d.paths.archiveDir, `${wt.id}.index`);
30322
31028
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
30323
31029
  if (!kept)
30324
31030
  log.warn(wt.id, "could not keep its git state");
@@ -30355,11 +31061,11 @@ class WorktreeService {
30355
31061
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
30356
31062
  }
30357
31063
  plansScratch(worktreeId) {
30358
- return join25(this.d.paths.archiveDir, `${worktreeId}.plans`);
31064
+ return join27(this.d.paths.archiveDir, `${worktreeId}.plans`);
30359
31065
  }
30360
31066
  keepPlans(wt) {
30361
- const from = join25(wt.path, PLANS_DIR);
30362
- if (!existsSync28(from))
31067
+ const from = join27(wt.path, PLANS_DIR);
31068
+ if (!existsSync29(from))
30363
31069
  return;
30364
31070
  try {
30365
31071
  cpSync(from, this.plansScratch(wt.id), { recursive: true });
@@ -30400,8 +31106,8 @@ class WorktreeService {
30400
31106
  deleteChat(worktreeId) {
30401
31107
  const files = this.chatFiles(worktreeId);
30402
31108
  try {
30403
- rmSync5(files.transcript, { force: true });
30404
- rmSync5(files.attachments, { recursive: true, force: true });
31109
+ rmSync7(files.transcript, { force: true });
31110
+ rmSync7(files.attachments, { recursive: true, force: true });
30405
31111
  } catch (e) {
30406
31112
  log.warn(worktreeId, "could not delete transcript or attachments", e);
30407
31113
  }
@@ -30447,7 +31153,7 @@ class WorktreeService {
30447
31153
  }
30448
31154
  archivedAttachment(archiveId, file) {
30449
31155
  const files = this.archive.chatFiles(archiveId);
30450
- return files && isAttachmentFile(file) ? join25(files.attachments, file) : null;
31156
+ return files && isAttachmentFile(file) ? join27(files.attachments, file) : null;
30451
31157
  }
30452
31158
  archivedGit(archiveId) {
30453
31159
  const rec = this.archive.get(archiveId);
@@ -30490,7 +31196,7 @@ class WorktreeService {
30490
31196
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
30491
31197
  if (rec.auto)
30492
31198
  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;
31199
+ const path = existsSync29(old.path) ? join27(dirname11(old.path), freeSlot(dirname11(old.path)).dir) : old.path;
30494
31200
  const branch = await withRepoLock(repo.path, async () => {
30495
31201
  if (!await commitOf(repo.path, archiveRef(old.id))) {
30496
31202
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -30524,10 +31230,11 @@ class WorktreeService {
30524
31230
  branch,
30525
31231
  proxyPort: await allocateProxyPort(),
30526
31232
  promptedAt: Date.now(),
31233
+ parked: true,
30527
31234
  ...createdBy ? { createdBy } : {}
30528
31235
  };
30529
31236
  try {
30530
- this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join25(path, PLANS_DIR) });
31237
+ this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join27(path, PLANS_DIR) });
30531
31238
  } catch (e) {
30532
31239
  log.warn(old.id, "could not move its chat back out of the archive", e);
30533
31240
  }
@@ -30556,12 +31263,12 @@ class WorktreeService {
30556
31263
  return;
30557
31264
  const repo = this.repoOf(rec);
30558
31265
  const repoPath = repo?.path ?? rec.repoPath;
30559
- if (rec.kept && existsSync28(repoPath)) {
31266
+ if (rec.kept && existsSync29(repoPath)) {
30560
31267
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
30561
31268
  if (!r.ok)
30562
31269
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
30563
31270
  }
30564
- if (rec.worktree.lands?.length && existsSync28(repoPath))
31271
+ if (rec.worktree.lands?.length && existsSync29(repoPath))
30565
31272
  await dropLandRefs(repoPath, archiveId);
30566
31273
  this.archive.delete(archiveId);
30567
31274
  this.d.drafts?.drop(archiveId);
@@ -30652,6 +31359,15 @@ class WorktreeService {
30652
31359
  this.d.hub.emit("worktreesChanged");
30653
31360
  return { target, grafted: sources.map((w) => w.title) };
30654
31361
  }
31362
+ startPreview(worktreeId) {
31363
+ const wt = this.d.state.worktree(worktreeId);
31364
+ if (!wt?.parked)
31365
+ return;
31366
+ delete wt.parked;
31367
+ this.d.state.save();
31368
+ this.d.hub.emit("worktreesChanged");
31369
+ fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start a parked preview");
31370
+ }
30655
31371
  async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
30656
31372
  this.d.runtime.markSetup(wt.id, true);
30657
31373
  try {
@@ -30661,9 +31377,9 @@ class WorktreeService {
30661
31377
  }
30662
31378
  }
30663
31379
  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;
31380
+ const src = join27(repo.path, ".gitignore");
31381
+ const tracked = existsSync29(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
31382
+ const mirror = existsSync29(src) && !tracked;
30667
31383
  const lines = mirror ? readFileSync18(src, "utf8").split(`
30668
31384
  `).filter((l) => l.trim() !== "") : [];
30669
31385
  await excludeBlock(repo.path, BASE_IGNORE, lines);
@@ -30676,9 +31392,9 @@ class WorktreeService {
30676
31392
  await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
30677
31393
  run.finish(0);
30678
31394
  }
30679
- const srcNm = join25(depsSource, "node_modules");
30680
- const dstNm = join25(wt.path, "node_modules");
30681
- if (existsSync28(srcNm) && !existsSync28(dstNm)) {
31395
+ const srcNm = join27(depsSource, "node_modules");
31396
+ const dstNm = join27(wt.path, "node_modules");
31397
+ if (existsSync29(srcNm) && !existsSync29(dstNm)) {
30682
31398
  const r = await cloneTree(srcNm, dstNm);
30683
31399
  if (r.ok)
30684
31400
  this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
@@ -30686,10 +31402,10 @@ class WorktreeService {
30686
31402
  log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
30687
31403
  }
30688
31404
  for (const f of LOCAL_CONFIG_FILES) {
30689
- const src = join25(depsSource, f);
30690
- if (!existsSync28(src) || existsSync28(join25(wt.path, f)))
31405
+ const src = join27(depsSource, f);
31406
+ if (!existsSync29(src) || existsSync29(join27(wt.path, f)))
30691
31407
  continue;
30692
- const r = await run("cp", [src, join25(wt.path, f)], wt.path);
31408
+ const r = await run("cp", [src, join27(wt.path, f)], wt.path);
30693
31409
  if (r.ok)
30694
31410
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
30695
31411
  else
@@ -31097,7 +31813,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31097
31813
  async fetchBase(wt, repo, w) {
31098
31814
  if (!baseIsRemote(repo))
31099
31815
  return { ok: true, message: `${baseOf(repo)} here is the base` };
31100
- const r = await fetchBase(wt.path, repo, w);
31816
+ const r = await withFetchLock(repo.path, () => fetchBase(wt.path, repo, w));
31101
31817
  this.trunk.noteFetch(repo.id, { ok: r.ok, err: r.message });
31102
31818
  return r;
31103
31819
  }
@@ -31333,6 +32049,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31333
32049
  base: baseOf(repo)
31334
32050
  };
31335
32051
  }
32052
+ async turnStarted(worktreeId) {
32053
+ const wt = this.d.state.worktree(worktreeId);
32054
+ if (wt && !this.going.has(worktreeId))
32055
+ await markTurn(wt.path, wt.id);
32056
+ }
31336
32057
  async gitStatus(worktreeId) {
31337
32058
  if (this.going.has(worktreeId))
31338
32059
  return null;
@@ -31353,6 +32074,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31353
32074
  const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
31354
32075
  const ahead = counts.ahead ?? 0;
31355
32076
  const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.base) : undefined;
32077
+ const turn = r.wt && files.length > 0 ? await turnChanges(r.path, r.wt.id, r.base, !isMain, files) : null;
31356
32078
  if (r.wt && !isMain && hasOwnBranch(r.wt) && files.length === 0 && ahead === 0 && head.ok && this.handChecked.get(r.wt.id) !== head.out) {
31357
32079
  this.handChecked.set(r.wt.id, head.out);
31358
32080
  const mark = await handLanding(r.path, r.wt.branch, r.base, r.wt.lands?.at(-1)?.tip);
@@ -31379,7 +32101,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31379
32101
  if (this.going.has(worktreeId))
31380
32102
  return null;
31381
32103
  this.noteCounts(worktreeId, isMain, files.length, counts);
31382
- return { files, committed, head: head.ok ? head.out : undefined, ...counts };
32104
+ return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
31383
32105
  } catch (e) {
31384
32106
  log.warn(worktreeId, "git status failed", e);
31385
32107
  return null;
@@ -31648,7 +32370,8 @@ var accounts = new AgentAccounts({
31648
32370
  connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `auth:${spec.id}`)
31649
32371
  });
31650
32372
  accounts.onChange = () => hub.emit("agentsChanged");
31651
- var attachments2 = new AttachmentStore(paths.attachmentsDir);
32373
+ var uploads = new UploadStore(paths.uploadsDir);
32374
+ var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
31652
32375
  var runtime = new RuntimeRegistry({
31653
32376
  hub,
31654
32377
  state,
@@ -31658,13 +32381,19 @@ var runtime = new RuntimeRegistry({
31658
32381
  accounts,
31659
32382
  attachments: attachments2,
31660
32383
  bridgeScript: () => bridge.get(),
32384
+ turnStarting: (id) => worktrees.turnStarted(id),
31661
32385
  remote,
31662
32386
  grant: previewGrant(token),
31663
32387
  viewed: (id) => idle.isViewed(id),
31664
32388
  shown: (id) => idle.isShown(id),
31665
32389
  mainLeads: (repoId) => worktrees.spare.current(repoId) === null
31666
32390
  });
31667
- var drafts = new DraftStore({ file: paths.draftsFile, hub });
32391
+ var drafts = new DraftStore({
32392
+ file: paths.draftsFile,
32393
+ attachmentsFile: paths.draftAttachmentsFile,
32394
+ hub,
32395
+ uploads
32396
+ });
31668
32397
  var runs = new RunService({ state, hub });
31669
32398
  var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
31670
32399
  var cache = new ArtifactCache({ paths, state, hub });
@@ -31834,6 +32563,7 @@ var stopLagSampler = startLagSampler();
31834
32563
  await runtime.reclaimOrphans();
31835
32564
  await repos.boot();
31836
32565
  drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
32566
+ uploads.sweep(drafts.uploadIds());
31837
32567
  idle.boot();
31838
32568
  runs.boot();
31839
32569
  landing.boot();
@@ -31889,7 +32619,7 @@ async function shutdown(signal, opts = {}) {
31889
32619
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
31890
32620
  await Promise.race([Promise.all([runtime.shutdown(), exec.stopAll()]), deadline]);
31891
32621
  state.flushGroups();
31892
- rmSync6(paths.pidFile, { force: true });
32622
+ rmSync8(paths.pidFile, { force: true });
31893
32623
  if (opts.respawn) {
31894
32624
  await Bun.sleep(250);
31895
32625
  log.info("daemon", "starting the replacement");