toyon 0.8.0 → 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 +19 -1
  3. package/dist/daemon.js +1323 -426
  4. package/dist/shell/assets/{Editor-CFH2QbSg.js → Editor-DAJni9OF.js} +137 -137
  5. package/dist/shell/assets/{XTerm-C3aMkE_w.js → XTerm-dug6M6yd.js} +16 -16
  6. package/dist/shell/assets/{cssMode-dc6AdHsZ.js → cssMode-B5iiwdXn.js} +1 -1
  7. package/dist/shell/assets/{freemarker2-CxtmNsLB.js → freemarker2-CdVcBNE9.js} +1 -1
  8. package/dist/shell/assets/{grammar-B-X0FPRk.js → grammar-Cfxx1yRn.js} +5 -5
  9. package/dist/shell/assets/{handlebars-DVzmQ0KM.js → handlebars-CW5EFZC2.js} +1 -1
  10. package/dist/shell/assets/{html-CqqXL9ke.js → html-B8gP1xu-.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-Bg-Em0rL.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-W-C4G7BB.js → javascript-C_3_LFln.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-De4ajMpV.js → jsonMode-CD5xJEUk.js} +1 -1
  16. package/dist/shell/assets/{liquid-BSUg_7LQ.js → liquid-CJw93Zxn.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-BImXYajo.js → lspLanguageFeatures-wMR368iN.js} +1 -1
  18. package/dist/shell/assets/{mdx-B9A7RorP.js → mdx-BJ6yMgc3.js} +1 -1
  19. package/dist/shell/assets/{python-BD9sq3DF.js → python-BWGkxFUr.js} +1 -1
  20. package/dist/shell/assets/{razor-DGQVRh5-.js → razor-BV9pdU1A.js} +1 -1
  21. package/dist/shell/assets/{tsMode-DudQlgN_.js → tsMode-CN8OfYAX.js} +1 -1
  22. package/dist/shell/assets/{xml-BI6-GyBn.js → xml-DBGLqvw7.js} +1 -1
  23. package/dist/shell/assets/{yaml-BEUvyYtP.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-BskWflhx.css +0 -1
  27. package/dist/shell/assets/index-C8QP4sgP.js +0 -255
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 = 82;
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({
@@ -7882,15 +7929,6 @@ function installCommand(method, version) {
7882
7929
  return ["bun", "add", "-g", `toyon@${version}`];
7883
7930
  return null;
7884
7931
  }
7885
- function registryHost(registry) {
7886
- if (registry === null || registry === "")
7887
- return "the npm registry";
7888
- try {
7889
- return new URL(registry).host;
7890
- } catch {
7891
- return registry;
7892
- }
7893
- }
7894
7932
  function newer(a, b) {
7895
7933
  const pa = parseVersion(a);
7896
7934
  const pb = parseVersion(b);
@@ -8163,7 +8201,7 @@ function safeUser() {
8163
8201
  // ../daemon/package.json
8164
8202
  var package_default = {
8165
8203
  name: "@toyon/daemon",
8166
- version: "0.8.0",
8204
+ version: "0.9.0",
8167
8205
  private: true,
8168
8206
  type: "module",
8169
8207
  main: "src/index.ts",
@@ -12257,9 +12295,11 @@ var legacyClientNotificationMethods = new Set([
12257
12295
 
12258
12296
  // ../daemon/src/core/errors.ts
12259
12297
  class UserError extends Error {
12260
- constructor(message) {
12298
+ delivered;
12299
+ constructor(message, about = {}) {
12261
12300
  super(message);
12262
12301
  this.name = "UserError";
12302
+ this.delivered = about.delivered ?? false;
12263
12303
  }
12264
12304
  }
12265
12305
 
@@ -12512,18 +12552,177 @@ function spawnAcp(app, launch, cwd, tag) {
12512
12552
 
12513
12553
  // ../daemon/src/agent/attachments.ts
12514
12554
  import { createHash } from "crypto";
12515
- 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";
12516
12562
  import { join } from "path";
12517
- var EXT = {
12563
+ var IMAGE_EXT = {
12518
12564
  "image/png": "png",
12519
12565
  "image/jpeg": "jpg",
12520
12566
  "image/gif": "gif",
12521
12567
  "image/webp": "webp"
12522
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
12523
12707
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
12524
- 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
+ }
12525
12724
  function toolImage(data, mimeType) {
12526
- const ext = EXT[mimeType];
12725
+ const ext = IMAGE_EXT[mimeType];
12527
12726
  if (!ext)
12528
12727
  return null;
12529
12728
  const bytes = Buffer.from(data, "base64");
@@ -12532,27 +12731,40 @@ function toolImage(data, mimeType) {
12532
12731
  const file = `${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}.${ext}`;
12533
12732
  return { ref: { file, mimeType, bytes: bytes.length }, bytes };
12534
12733
  }
12535
- var isAttachmentFile = (file) => FILE.test(file);
12734
+ var isAttachmentFile = (file) => isFile(file);
12536
12735
  function attachmentsDirFor(attachmentsDir, worktreeId) {
12537
- 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
+ }
12538
12748
  }
12539
12749
 
12540
12750
  class AttachmentStore {
12541
12751
  dir;
12752
+ uploads;
12542
12753
  pending = new Map;
12543
- constructor(dir) {
12754
+ constructor(dir, uploads) {
12544
12755
  this.dir = dir;
12756
+ this.uploads = uploads;
12545
12757
  }
12546
12758
  async putToolImage(worktreeId, file, bytes) {
12547
- if (!ID.test(worktreeId) || !FILE.test(file))
12759
+ if (!ID.test(worktreeId) || !DRAWN.test(file))
12548
12760
  throw new UserError("bad tool image");
12549
- const path = join(attachmentsDirFor(this.dir, worktreeId), file);
12761
+ const path = join2(attachmentsDirFor(this.dir, worktreeId), file);
12550
12762
  if (this.pending.has(path) || await Bun.file(path).exists())
12551
12763
  return;
12552
- await this.write(worktreeId, file, bytes);
12764
+ await this.write(worktreeId, file, (path) => writeFile(path, bytes));
12553
12765
  }
12554
12766
  whenWritten(worktreeId, file) {
12555
- 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();
12556
12768
  }
12557
12769
  async put(worktreeId, n, input) {
12558
12770
  if (!ID.test(worktreeId))
@@ -12562,19 +12774,23 @@ class AttachmentStore {
12562
12774
  return this.putImage(worktreeId, n, input);
12563
12775
  case "paste":
12564
12776
  return this.putText(worktreeId, n, input);
12777
+ case "file":
12778
+ return this.putFile(worktreeId, n, input);
12565
12779
  case "pick":
12566
12780
  return { kind: "pick", ref: { ...input, n } };
12567
12781
  }
12568
12782
  }
12569
12783
  async putImage(worktreeId, n, img) {
12570
- const bytes = Buffer.from(img.data, "base64");
12571
- if (bytes.length === 0)
12572
- throw new UserError(`${img.name}: empty image`);
12573
- const file = `${n}.${EXT[img.mimeType]}`;
12574
- 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));
12575
12790
  return {
12576
12791
  kind: "image",
12577
12792
  bytes,
12793
+ path,
12578
12794
  ref: {
12579
12795
  kind: "image",
12580
12796
  n,
@@ -12587,28 +12803,78 @@ class AttachmentStore {
12587
12803
  }
12588
12804
  };
12589
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
+ }
12590
12821
  async putText(worktreeId, n, { text, name, source }) {
12591
12822
  if (text.length === 0)
12592
12823
  throw new UserError("empty paste");
12593
12824
  const file = `${n}.txt`;
12594
- await this.write(worktreeId, file, text);
12825
+ await this.write(worktreeId, file, (to) => writeFile(to, text));
12595
12826
  return {
12596
12827
  kind: "paste",
12597
12828
  text,
12598
12829
  ref: { kind: "paste", n, ...name ? { name } : {}, ...source ? { source } : {}, ...pasteSummary(text), file }
12599
12830
  };
12600
12831
  }
12601
- 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) {
12602
12867
  const wtDir = attachmentsDirFor(this.dir, worktreeId);
12603
- const path = join(wtDir, file);
12604
- 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));
12605
12870
  this.pending.set(path, done);
12606
- return done;
12871
+ await done;
12872
+ return path;
12607
12873
  }
12608
12874
  fileFor(worktreeId, file) {
12609
- if (!ID.test(worktreeId) || !FILE.test(file))
12875
+ if (!ID.test(worktreeId) || !isFile(file))
12610
12876
  return null;
12611
- return join(attachmentsDirFor(this.dir, worktreeId), file);
12877
+ return join2(attachmentsDirFor(this.dir, worktreeId), file);
12612
12878
  }
12613
12879
  }
12614
12880
 
@@ -12765,9 +13031,9 @@ class OptionProbe {
12765
13031
  }
12766
13032
 
12767
13033
  // ../daemon/src/agent/registry.ts
12768
- 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";
12769
13035
  import { homedir as homedir3 } from "os";
12770
- 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";
12771
13037
 
12772
13038
  // ../daemon/src/core/cloud.ts
12773
13039
  var enabled = process.env.TOYON_CLOUD === "1";
@@ -12794,7 +13060,7 @@ var cloud = {
12794
13060
  import { spawn as spawn2 } from "child_process";
12795
13061
  import { createHash as createHash2 } from "crypto";
12796
13062
  import { existsSync, readFileSync as readFileSync2 } from "fs";
12797
- import { join as join2 } from "path";
13063
+ import { join as join3 } from "path";
12798
13064
 
12799
13065
  // ../daemon/src/core/metrics.ts
12800
13066
  var WARN_MS = 50;
@@ -12836,7 +13102,7 @@ function lockfileHash(dir) {
12836
13102
  const h = createHash2("sha1");
12837
13103
  for (const f of LOCKFILES) {
12838
13104
  try {
12839
- h.update(readFileSync2(join2(dir, f)));
13105
+ h.update(readFileSync2(join3(dir, f)));
12840
13106
  } catch {}
12841
13107
  }
12842
13108
  return h.digest("hex");
@@ -13151,7 +13417,7 @@ function confine(launch, b, extraWrite = [], platform = process.platform, which
13151
13417
  }
13152
13418
 
13153
13419
  // ../daemon/src/agent/native.ts
13154
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "fs";
13420
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
13155
13421
  var OS = { darwin: "darwin", linux: "linux", win32: "windows" };
13156
13422
  function nativePackage(pkg, host) {
13157
13423
  const os = OS[host.platform];
@@ -13174,7 +13440,7 @@ function hostTraits() {
13174
13440
  }
13175
13441
  function muslLoader() {
13176
13442
  try {
13177
- return existsSync2("/lib") && readdirSync("/lib").some((f) => f.startsWith("ld-musl-"));
13443
+ return existsSync2("/lib") && readdirSync2("/lib").some((f) => f.startsWith("ld-musl-"));
13178
13444
  } catch {
13179
13445
  return false;
13180
13446
  }
@@ -13244,49 +13510,52 @@ function opencodeEnv() {
13244
13510
  }
13245
13511
 
13246
13512
  // ../daemon/src/agent/sandbox.ts
13247
- 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";
13248
13514
  import { homedir as homedir2, tmpdir } from "os";
13249
- import { join as join4, resolve as resolve2 } from "path";
13515
+ import { join as join5, resolve as resolve2 } from "path";
13250
13516
 
13251
13517
  // ../daemon/src/core/paths.ts
13252
- import { mkdirSync } from "fs";
13518
+ import { mkdirSync as mkdirSync2 } from "fs";
13253
13519
  import { homedir } from "os";
13254
- import { join as join3 } from "path";
13255
- 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")) {
13256
13522
  return {
13257
13523
  home,
13258
- stateFile: join3(home, DAEMON_FILES.state),
13259
- draftsFile: join3(home, DAEMON_FILES.drafts),
13260
- tokenFile: join3(home, DAEMON_FILES.token),
13261
- pidFile: join3(home, DAEMON_FILES.pid),
13262
- logFile: join3(home, DAEMON_FILES.log),
13263
- remoteFile: join3(home, DAEMON_FILES.remote),
13264
- transcriptsDir: join3(home, "transcripts"),
13265
- attachmentsDir: join3(home, "attachments"),
13266
- archiveDir: join3(home, "archive"),
13267
- worktreesDir: join3(home, "worktrees.noindex"),
13268
- cacheDir: join3(home, "cache.noindex"),
13269
- themesDir: join3(home, "themes"),
13270
- agentsDir: process.env.TOYON_AGENTS_DIR ?? join3(home, "agents"),
13271
- agentsFile: join3(home, "agents.json"),
13272
- scratchDir: join3(home, "scratch"),
13273
- 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")
13274
13542
  };
13275
13543
  }
13276
13544
  function ensureDirs(p) {
13277
- mkdirSync(p.home, { recursive: true, mode: 448 });
13278
- mkdirSync(p.transcriptsDir, { recursive: true });
13279
- mkdirSync(p.attachmentsDir, { recursive: true });
13280
- mkdirSync(p.archiveDir, { recursive: true });
13281
- mkdirSync(p.worktreesDir, { recursive: true });
13282
- mkdirSync(p.cacheDir, { recursive: true });
13283
- mkdirSync(p.themesDir, { recursive: true });
13284
- mkdirSync(p.agentsDir, { recursive: true });
13285
- 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 });
13286
13555
  }
13287
13556
 
13288
13557
  // ../daemon/src/git/exclude.ts
13289
- 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";
13290
13559
  import { dirname } from "path";
13291
13560
  async function excludeFile(cwd) {
13292
13561
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
@@ -13303,7 +13572,7 @@ async function excludeFromGit(cwd, pattern) {
13303
13572
  if (current.split(`
13304
13573
  `).some((l) => l.trim() === pattern))
13305
13574
  return;
13306
- mkdirSync2(dirname(file), { recursive: true });
13575
+ mkdirSync3(dirname(file), { recursive: true });
13307
13576
  writeFileSync(file, `${current}${current && !current.endsWith(`
13308
13577
  `) ? `
13309
13578
  ` : ""}${pattern}
@@ -13329,7 +13598,7 @@ async function excludeBlock(cwd, name, patterns) {
13329
13598
  ` : "";
13330
13599
  if (text === current)
13331
13600
  return;
13332
- mkdirSync2(dirname(file), { recursive: true });
13601
+ mkdirSync3(dirname(file), { recursive: true });
13333
13602
  writeFileSync(file, text);
13334
13603
  }
13335
13604
  async function unexcludeFromGit(cwd, pattern) {
@@ -13396,7 +13665,7 @@ async function prepareLaunch(cwd, spec) {
13396
13665
  log.warn(cwd, `agent ${spec.id} runs without an OS sandbox`);
13397
13666
  return { bounds, env: setup?.env ?? {} };
13398
13667
  }
13399
- var SETTINGS_REL = join4(".claude", "settings.local.json");
13668
+ var SETTINGS_REL = join5(".claude", "settings.local.json");
13400
13669
  function claudeLocalSettings(b) {
13401
13670
  return {
13402
13671
  sandbox: {
@@ -13410,8 +13679,8 @@ function claudeLocalSettings(b) {
13410
13679
  };
13411
13680
  }
13412
13681
  async function writeClaudeLocalSettings(cwd, b) {
13413
- const file = join4(cwd, SETTINGS_REL);
13414
- mkdirSync3(join4(cwd, ".claude"), { recursive: true });
13682
+ const file = join5(cwd, SETTINGS_REL);
13683
+ mkdirSync4(join5(cwd, ".claude"), { recursive: true });
13415
13684
  let existing = {};
13416
13685
  if (existsSync5(file)) {
13417
13686
  try {
@@ -13433,7 +13702,7 @@ async function writeClaudeLocalSettings(cwd, b) {
13433
13702
  writeFileSync2(tmp, `${JSON.stringify(next, null, 2)}
13434
13703
  `);
13435
13704
  renameSync(tmp, file);
13436
- if (existsSync5(join4(cwd, ".git")))
13705
+ if (existsSync5(join5(cwd, ".git")))
13437
13706
  await excludeFromGit(cwd, SETTINGS_REL);
13438
13707
  }
13439
13708
 
@@ -13558,12 +13827,12 @@ class AgentRegistry {
13558
13827
  const name = this.packageName(spec);
13559
13828
  if (!name || spec.run.kind === "command")
13560
13829
  return null;
13561
- const pkgDir = join5(this.dir, spec.id, "node_modules", name);
13830
+ const pkgDir = join6(this.dir, spec.id, "node_modules", name);
13562
13831
  if (spec.run.kind === "npm-native") {
13563
- const bin = join5(pkgDir, spec.run.bin);
13832
+ const bin = join6(pkgDir, spec.run.bin);
13564
13833
  return existsSync6(bin) ? bin : null;
13565
13834
  }
13566
- const pkgJson = join5(pkgDir, "package.json");
13835
+ const pkgJson = join6(pkgDir, "package.json");
13567
13836
  if (!existsSync6(pkgJson))
13568
13837
  return null;
13569
13838
  try {
@@ -13571,7 +13840,7 @@ class AgentRegistry {
13571
13840
  const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
13572
13841
  if (!rel)
13573
13842
  return null;
13574
- const script = join5(dirname3(pkgJson), rel);
13843
+ const script = join6(dirname3(pkgJson), rel);
13575
13844
  return existsSync6(script) ? script : null;
13576
13845
  } catch (e) {
13577
13846
  log.warn("agents", `cannot read ${pkgJson}`, e);
@@ -13625,10 +13894,10 @@ class AgentRegistry {
13625
13894
  if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
13626
13895
  return Promise.resolve();
13627
13896
  const { version } = spec.run;
13628
- const dir = join5(this.dir, id);
13897
+ const dir = join6(this.dir, id);
13629
13898
  const p = (async () => {
13630
- mkdirSync4(dir, { recursive: true });
13631
- 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)}
13632
13901
  `);
13633
13902
  log.info("agents", `installing ${pkg}@${version} for ${id}`);
13634
13903
  const r = await this.installer(dir, pkg, version);
@@ -13652,7 +13921,7 @@ class AgentRegistry {
13652
13921
  const name = this.packageName(spec);
13653
13922
  if (!name)
13654
13923
  return null;
13655
- 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");
13656
13925
  try {
13657
13926
  return JSON.parse(readFileSync6(pkgJson, "utf8")).version ?? null;
13658
13927
  } catch {
@@ -13667,6 +13936,19 @@ class AgentRegistry {
13667
13936
  await this.install(id);
13668
13937
  }
13669
13938
  }
13939
+ adapters() {
13940
+ const out = [];
13941
+ for (const spec of this.list()) {
13942
+ if (spec.run.kind === "command" || !this.packageName(spec))
13943
+ continue;
13944
+ const installed = this.installed(spec) ? this.installedVersion(spec) : null;
13945
+ const error = this.installErrors.get(spec.id);
13946
+ if (spec.onDemand && installed === null && !error)
13947
+ continue;
13948
+ out.push({ id: spec.id, installed, pinned: spec.run.version, ...error ? { error } : {} });
13949
+ }
13950
+ return out;
13951
+ }
13670
13952
  infos() {
13671
13953
  return this.list().map((spec) => {
13672
13954
  const reason = this.unavailable(spec);
@@ -13902,6 +14184,9 @@ function previewContext(p) {
13902
14184
  case "setup":
13903
14185
  line = "Toyon is setting this worktree up and starts the preview when that is done; the next message will say where it answers.";
13904
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;
13905
14190
  case "starting":
13906
14191
  line = `Toyon is starting the preview; it answers${at} once it is up.`;
13907
14192
  break;
@@ -13916,8 +14201,12 @@ function previewContext(p) {
13916
14201
  }
13917
14202
  return line;
13918
14203
  }
13919
- function imageCaption(ref) {
13920
- 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.`;
13921
14210
  }
13922
14211
  function pasteCaption(ref) {
13923
14212
  const label = attachmentLabel("paste", ref.n);
@@ -13963,7 +14252,7 @@ function attachmentBlocks(a) {
13963
14252
  switch (a.kind) {
13964
14253
  case "image":
13965
14254
  return [
13966
- textBlock(imageCaption(a.ref)),
14255
+ textBlock(imageCaption(a.ref, a.path)),
13967
14256
  { type: "image", mimeType: a.ref.mimeType, data: a.bytes.toString("base64") }
13968
14257
  ];
13969
14258
  case "paste":
@@ -13971,6 +14260,13 @@ function attachmentBlocks(a) {
13971
14260
  <pasted-text ${a.ref.n}>
13972
14261
  ${a.text}
13973
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
+ ];
13974
14270
  case "pick":
13975
14271
  return [textBlock(pickCaption(a.ref))];
13976
14272
  }
@@ -13979,9 +14275,9 @@ ${a.text}
13979
14275
  // ../daemon/src/agent/transcript.ts
13980
14276
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
13981
14277
  import { appendFile } from "fs/promises";
13982
- import { join as join6 } from "path";
14278
+ import { join as join7 } from "path";
13983
14279
  function transcriptPathFor(transcriptsDir, worktreeId) {
13984
- return join6(transcriptsDir, `${worktreeId}.jsonl`);
14280
+ return join7(transcriptsDir, `${worktreeId}.jsonl`);
13985
14281
  }
13986
14282
  function coalesce(entries) {
13987
14283
  const out = [];
@@ -14136,7 +14432,7 @@ async function cleanUp(ctx, a, sessionId) {
14136
14432
  import { existsSync as existsSync8, statSync as statSync3 } from "fs";
14137
14433
  import { open as open2 } from "fs/promises";
14138
14434
  import { homedir as homedir4 } from "os";
14139
- 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";
14140
14436
  var STARTED = /Command running in background with ID: (\S+)\. Output is being written to: (\S+)\./;
14141
14437
  function backgroundStart(input, output) {
14142
14438
  const flag = input && typeof input === "object" ? input.run_in_background : undefined;
@@ -14145,15 +14441,18 @@ function backgroundStart(input, output) {
14145
14441
  const m = STARTED.exec(output);
14146
14442
  return m ? { taskId: m[1], file: m[2] } : null;
14147
14443
  }
14444
+ function spawnDetached(output) {
14445
+ return output.includes("Async agent launched successfully");
14446
+ }
14148
14447
  function claudeConfigDir() {
14149
- return process.env.CLAUDE_CONFIG_DIR || join7(homedir4(), ".claude");
14448
+ return process.env.CLAUDE_CONFIG_DIR || join8(homedir4(), ".claude");
14150
14449
  }
14151
14450
  function sessionLogFor(file, configDir = claudeConfigDir()) {
14152
14451
  const tasks = dirname4(file);
14153
14452
  if (basename2(tasks) !== "tasks")
14154
14453
  return null;
14155
14454
  const session = dirname4(tasks);
14156
- const path = join7(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14455
+ const path = join8(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14157
14456
  return existsSync8(path) ? path : null;
14158
14457
  }
14159
14458
  function taskEnd(text, toolId) {
@@ -14466,7 +14765,8 @@ function spawnOf(meta) {
14466
14765
  const claude = asRecord(asRecord(meta).claudeCode);
14467
14766
  const parentToolId = typeof claude.parentToolUseId === "string" ? claude.parentToolUseId : undefined;
14468
14767
  const codex = asRecord(asRecord(meta).codex).subagent;
14469
- const subagent = claude.subagent === true || Object.keys(asRecord(codex)).length > 0;
14768
+ const spawns = claude.toolName === "Agent" || claude.toolName === "Task";
14769
+ const subagent = spawns || claude.subagent === true || Object.keys(asRecord(codex)).length > 0;
14470
14770
  return { ...parentToolId ? { parentToolId } : {}, ...subagent ? { subagent: true } : {} };
14471
14771
  }
14472
14772
  function asRecord(v) {
@@ -14513,11 +14813,26 @@ function endOfAsk(req, res, memos) {
14513
14813
  isError: !allowed
14514
14814
  };
14515
14815
  }
14516
- 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) {
14517
14826
  const out = [];
14518
14827
  for (const [toolId, memo] of memos) {
14519
14828
  if (memo.ended || !memo.writing || memo.parent !== parent)
14520
14829
  continue;
14830
+ if (opening && memo.held) {
14831
+ const sent = release(toolId, memo);
14832
+ if (sent)
14833
+ out.push(sent);
14834
+ continue;
14835
+ }
14521
14836
  memo.ended = true;
14522
14837
  out.push({ type: "tool-end", toolId });
14523
14838
  }
@@ -14546,7 +14861,7 @@ function mapUpdate(update, memos, tag, sink, background) {
14546
14861
  }
14547
14862
  case "tool_call": {
14548
14863
  const spawn = spawnOf(update._meta);
14549
- const out = abandoned(memos, spawn.parentToolId);
14864
+ const out = abandoned(memos, spawn.parentToolId, true);
14550
14865
  const input = update.rawInput ?? { locations: update.locations ?? [] };
14551
14866
  const head = heading(update);
14552
14867
  const memo = {
@@ -14556,7 +14871,8 @@ function mapUpdate(update, memos, tag, sink, background) {
14556
14871
  rawOutput: update.rawOutput,
14557
14872
  ended: false,
14558
14873
  writing: isWrittenKind(head.kind) && emptyInput(input),
14559
- ...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
14874
+ ...spawn.parentToolId ? { parent: spawn.parentToolId } : {},
14875
+ ...spawn.subagent ? { spawns: true } : {}
14560
14876
  };
14561
14877
  memos.set(update.toolCallId, memo);
14562
14878
  out.push({
@@ -14586,7 +14902,8 @@ function mapUpdate(update, memos, tag, sink, background) {
14586
14902
  content: [],
14587
14903
  ended: false,
14588
14904
  writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
14589
- ...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
14905
+ ...spawn.parentToolId ? { parent: spawn.parentToolId } : {},
14906
+ ...spawn.subagent ? { spawns: true } : {}
14590
14907
  };
14591
14908
  memos.set(update.toolCallId, memo);
14592
14909
  out.push({
@@ -14599,26 +14916,27 @@ function mapUpdate(update, memos, tag, sink, background) {
14599
14916
  ...spawn
14600
14917
  });
14601
14918
  }
14602
- if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
14603
- return out;
14604
- const refined = { type: "tool-update", toolId: update.toolCallId };
14919
+ const refined = { ...memo.held };
14605
14920
  const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : update.title;
14606
- if (title && title !== memo.title) {
14607
- memo.title = title;
14608
- refined.title = title;
14609
- }
14610
- 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)
14611
14924
  refined.name = memo.name = update.name;
14612
- }
14613
14925
  if (update.kind && update.kind !== memo.kind)
14614
14926
  refined.kind = memo.kind = update.kind;
14615
- if (update.rawInput !== undefined) {
14927
+ if (update.rawInput !== undefined)
14616
14928
  refined.input = memo.input = update.rawInput;
14617
- if (!emptyInput(update.rawInput))
14618
- 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 });
14619
14939
  }
14620
- if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
14621
- out.push(refined);
14622
14940
  if (update.content)
14623
14941
  memo.content = [...memo.content, ...update.content];
14624
14942
  if (update.rawOutput !== undefined)
@@ -14664,14 +14982,23 @@ function endOf(toolId, memo, status, sink, background) {
14664
14982
  return { type: "tool-update", toolId, background: true };
14665
14983
  }
14666
14984
  const images = sink ? toolImages(memo.content, sink) : [];
14985
+ const detached = status === "completed" && !!memo.spawns && spawnDetached(wholeText(memo.content, memo.rawOutput));
14667
14986
  return {
14668
14987
  type: "tool-end",
14669
14988
  toolId,
14670
14989
  output,
14671
14990
  isError: status === "failed" || exitCodeOf(memo.rawOutput) > 0,
14991
+ ...detached ? { detached: true } : {},
14672
14992
  ...images.length ? { images } : {}
14673
14993
  };
14674
14994
  }
14995
+ function wholeText(content, rawOutput) {
14996
+ const parts = content.flatMap((c) => c.type === "content" && c.content.type === "text" ? [c.content.text] : []);
14997
+ if (typeof rawOutput === "string")
14998
+ parts.push(rawOutput);
14999
+ return parts.join(`
15000
+ `);
15001
+ }
14675
15002
  function toolImages(content, sink) {
14676
15003
  const out = [];
14677
15004
  for (const c of content) {
@@ -14757,6 +15084,11 @@ function steerOutcome(value) {
14757
15084
  // ../daemon/src/agent/acp/session.ts
14758
15085
  var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
14759
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
+ });
14760
15092
  function recorded(reply) {
14761
15093
  if (reply?.kind === "choice")
14762
15094
  return { choiceId: reply.choiceId };
@@ -14872,7 +15204,7 @@ class AcpSession {
14872
15204
  return this.queue.length > 0 || this.steered.length > 0 || this.refused !== null || this.asks.size > 0;
14873
15205
  }
14874
15206
  get queueItems() {
14875
- return this.waiting().map((q) => q.text);
15207
+ return this.waiting().map(queued);
14876
15208
  }
14877
15209
  waiting() {
14878
15210
  return this.queue.filter((q) => !q.recorded);
@@ -14881,13 +15213,19 @@ class AcpSession {
14881
15213
  queueChanged() {
14882
15214
  this.onQueueChange?.();
14883
15215
  }
14884
- unqueue(index) {
15216
+ unqueue(index, taken) {
14885
15217
  const item = this.waiting()[index];
14886
15218
  if (!item)
14887
15219
  return;
14888
15220
  this.queue.splice(this.queue.indexOf(item), 1);
15221
+ taken?.(queued(item));
15222
+ this.letGo(item);
14889
15223
  this.queueChanged();
14890
15224
  }
15225
+ letGo(item) {
15226
+ if (!item.recorded)
15227
+ this.d.attachments.uploads.release(uploadIds(item.attachments));
15228
+ }
14891
15229
  transcript() {
14892
15230
  return this.log.entries;
14893
15231
  }
@@ -14905,8 +15243,14 @@ class AcpSession {
14905
15243
  send(text, opts = {}) {
14906
15244
  if (this.stopped)
14907
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 = {}) {
14908
15251
  const { context, attachments } = opts;
14909
15252
  const item = { text, context, ...attachments?.length ? { attachments } : {} };
15253
+ this.d.attachments.uploads.hold(uploadIds(attachments));
14910
15254
  if ((this.running || this.own) && !this.interrupted && !this.held && this.live?.conn.steering) {
14911
15255
  return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
14912
15256
  }
@@ -14993,6 +15337,8 @@ class AcpSession {
14993
15337
  async close() {
14994
15338
  this.stopped = true;
14995
15339
  this.clearReaper();
15340
+ for (const item of this.queue)
15341
+ this.letGo(item);
14996
15342
  this.queue = [];
14997
15343
  this.steered = [];
14998
15344
  this.queueChanged();
@@ -15116,8 +15462,8 @@ class AcpSession {
15116
15462
  retry() {
15117
15463
  const item = this.refused;
15118
15464
  this.refused = null;
15119
- if (item)
15120
- this.send(item.text, item);
15465
+ if (item && !this.stopped)
15466
+ this.enqueue(item);
15121
15467
  }
15122
15468
  planWithheld(conn, methodId) {
15123
15469
  return this.d.planSignIn?.() === false && conn.spec.terminalLogins?.[methodId]?.plan === true;
@@ -15140,8 +15486,12 @@ class AcpSession {
15140
15486
  }
15141
15487
  async record({ text, attachments }) {
15142
15488
  const stored = [];
15143
- for (const a of attachments ?? [])
15144
- 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
+ }
15145
15495
  this.emit({
15146
15496
  type: "user-message",
15147
15497
  text,
@@ -15163,6 +15513,11 @@ class AcpSession {
15163
15513
  }
15164
15514
  async runTurn(item) {
15165
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
+ }
15166
15521
  this.emit({ type: "turn-start", ts: Date.now() });
15167
15522
  const live = await this.ensureLive("a turn");
15168
15523
  await this.applyMode(live);
@@ -15267,7 +15622,8 @@ class AcpSession {
15267
15622
  sessionId,
15268
15623
  cwd: this.d.cwd,
15269
15624
  mcpServers: [],
15270
- additionalDirectories
15625
+ additionalDirectories,
15626
+ ...conn.spec.systemPrompt === "meta-append" ? { _meta: { systemPrompt: { append: SYSTEM_APPEND } } } : {}
15271
15627
  });
15272
15628
  modes = r.modes;
15273
15629
  configOptions = r.configOptions;
@@ -15390,6 +15746,10 @@ class AcpSession {
15390
15746
  if (params.update.sessionUpdate === "config_option_update")
15391
15747
  this.absorb(live, params.update.configOptions);
15392
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) {
15393
15753
  if (!this.running)
15394
15754
  this.trackOwn(events);
15395
15755
  for (const ev of events) {
@@ -15398,6 +15758,23 @@ class AcpSession {
15398
15758
  this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
15399
15759
  }
15400
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
+ }
15401
15778
  onPermission(params, bounds, sandboxed) {
15402
15779
  const tools = this.live?.tools;
15403
15780
  return Promise.resolve(this.answerPermission(params, bounds, sandboxed)).then((res) => {
@@ -15456,7 +15833,7 @@ class AcpSession {
15456
15833
  async sayPlanEdited(proposed, path) {
15457
15834
  if (!await this.d.planEdited?.(path, proposed))
15458
15835
  return;
15459
- 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.`);
15460
15837
  }
15461
15838
  onElicit(params, signal) {
15462
15839
  const live = this.live;
@@ -15651,9 +16028,9 @@ function sameCommands(a, b) {
15651
16028
  }
15652
16029
 
15653
16030
  // ../daemon/src/agent/planDoc.ts
15654
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
15655
- import { join as join8 } from "path";
15656
- 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");
15657
16034
  var PLAN_NAME = /^(\d+)\.md$/;
15658
16035
  function isPlanPath(rel) {
15659
16036
  return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
@@ -15662,7 +16039,7 @@ function nextNumber(dir) {
15662
16039
  if (!existsSync9(dir))
15663
16040
  return 1;
15664
16041
  let max = 0;
15665
- for (const name of readdirSync2(dir)) {
16042
+ for (const name of readdirSync3(dir)) {
15666
16043
  const n = Number(PLAN_NAME.exec(name)?.[1]);
15667
16044
  if (n > max)
15668
16045
  max = n;
@@ -15670,11 +16047,11 @@ function nextNumber(dir) {
15670
16047
  return max + 1;
15671
16048
  }
15672
16049
  async function writePlanDoc(cwd, markdown) {
15673
- const dir = join8(cwd, PLANS_DIR);
15674
- const rel = join8(PLANS_DIR, `${nextNumber(dir)}.md`);
16050
+ const dir = join9(cwd, PLANS_DIR);
16051
+ const rel = join9(PLANS_DIR, `${nextNumber(dir)}.md`);
15675
16052
  try {
15676
- mkdirSync5(dir, { recursive: true });
15677
- writeFileSync4(join8(cwd, rel), markdown.endsWith(`
16053
+ mkdirSync6(dir, { recursive: true });
16054
+ writeFileSync4(join9(cwd, rel), markdown.endsWith(`
15678
16055
  `) ? markdown : `${markdown}
15679
16056
  `);
15680
16057
  } catch (e) {
@@ -15685,7 +16062,7 @@ async function writePlanDoc(cwd, markdown) {
15685
16062
  return rel;
15686
16063
  }
15687
16064
  function planEdited(cwd, rel, proposed) {
15688
- const file = join8(cwd, rel);
16065
+ const file = join9(cwd, rel);
15689
16066
  if (!existsSync9(file))
15690
16067
  return false;
15691
16068
  try {
@@ -16990,11 +17367,12 @@ function defaultAgent(wt, d, preview, onProcess) {
16990
17367
  launch: (spec) => d.agents.command(spec),
16991
17368
  planSignIn: () => d.managed?.planSignIn ?? true,
16992
17369
  transcriptsDir: d.paths.transcriptsDir,
16993
- attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir),
17370
+ attachments: d.attachments ?? new AttachmentStore(d.paths.attachmentsDir, new UploadStore(d.paths.uploadsDir)),
16994
17371
  getSessionId: () => d.state.session(wt.id),
16995
17372
  setSessionId: (id) => d.state.setSession(wt.id, id),
16996
17373
  onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
16997
17374
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
17375
+ beforeTurn: async () => d.turnStarting?.(wt.id),
16998
17376
  onAuth: (agentId, o) => d.accounts?.observe(agentId, o),
16999
17377
  seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
17000
17378
  onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
@@ -17298,9 +17676,12 @@ class RuntimeRegistry {
17298
17676
  return rt;
17299
17677
  }
17300
17678
  async start(wt, repo) {
17301
- if (!this.deps.state.worktree(wt.id))
17679
+ const record = this.deps.state.worktree(wt.id);
17680
+ if (!record)
17302
17681
  return;
17303
17682
  const rt = this.ensureAgent(wt);
17683
+ if (record.parked)
17684
+ return;
17304
17685
  const run = repo.needsSetup ? EMPTY_RUN : resolveRun(repo, wt);
17305
17686
  const leads = wt.kind !== "main" || !this.deps.mainLeads || this.deps.mainLeads(wt.repoId);
17306
17687
  if (rt.procs) {
@@ -17699,6 +18080,11 @@ class RuntimeRegistry {
17699
18080
  return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
17700
18081
  }
17701
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
+ }
17702
18088
  if (this.settingUp.has(id))
17703
18089
  return { status: "setup" };
17704
18090
  const rt = this.runtimes.get(id);
@@ -17763,17 +18149,17 @@ class RuntimeRegistry {
17763
18149
  }
17764
18150
 
17765
18151
  // ../daemon/src/worktrees/naming.ts
17766
- import { randomBytes } from "crypto";
18152
+ import { randomBytes as randomBytes2 } from "crypto";
17767
18153
  import { existsSync as existsSync11 } from "fs";
17768
- import { join as join9 } from "path";
18154
+ import { join as join10 } from "path";
17769
18155
  function shortId() {
17770
- return randomBytes(5).toString("hex");
18156
+ return randomBytes2(5).toString("hex");
17771
18157
  }
17772
18158
  function freeSlot(parent) {
17773
18159
  for (;; ) {
17774
18160
  const id = shortId();
17775
18161
  const dir = `wt-${id.slice(0, 4)}`;
17776
- if (!existsSync11(join9(parent, dir)))
18162
+ if (!existsSync11(join10(parent, dir)))
17777
18163
  return { id, dir };
17778
18164
  }
17779
18165
  }
@@ -18113,6 +18499,8 @@ function attachmentText(a) {
18113
18499
  return a.text;
18114
18500
  if (a.kind === "image")
18115
18501
  return a.name ? `image ${a.name}` : "an image";
18502
+ if (a.kind === "file")
18503
+ return `file ${a.name}`;
18116
18504
  return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
18117
18505
  }
18118
18506
  function parseName(text) {
@@ -18187,29 +18575,29 @@ function makePlanner(agents) {
18187
18575
  }
18188
18576
 
18189
18577
  // ../daemon/src/core/assets.ts
18190
- import { existsSync as existsSync12, readdirSync as readdirSync3, rmSync, statSync as statSync4 } from "fs";
18191
- 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";
18192
18580
  function locateAssets(here, env = process.env) {
18193
- const packaged = existsSync12(join10(here, "shell", "index.html"));
18581
+ const packaged = existsSync12(join11(here, "shell", "index.html"));
18194
18582
  return {
18195
- shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join10(here, "shell") : join10(here, "../../shell/dist")),
18196
- 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")),
18197
18585
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
18198
- packageJson: packaged ? join10(here, "..", "package.json") : null
18586
+ packageJson: packaged ? join11(here, "..", "package.json") : null
18199
18587
  };
18200
18588
  }
18201
18589
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
18202
18590
  function pruneAssets(shellDist) {
18203
- const dir = join10(shellDist, "assets");
18591
+ const dir = join11(shellDist, "assets");
18204
18592
  let names;
18205
18593
  try {
18206
- names = readdirSync3(dir);
18594
+ names = readdirSync4(dir);
18207
18595
  } catch {
18208
18596
  return 0;
18209
18597
  }
18210
18598
  const found = [];
18211
18599
  for (const name of names) {
18212
- const path = join10(dir, name);
18600
+ const path = join11(dir, name);
18213
18601
  try {
18214
18602
  found.push({ path, mtime: statSync4(path).mtimeMs });
18215
18603
  } catch {}
@@ -18222,7 +18610,7 @@ function pruneAssets(shellDist) {
18222
18610
  if (newest - f.mtime <= KEEP_MS)
18223
18611
  continue;
18224
18612
  try {
18225
- rmSync(f.path, { recursive: true, force: true });
18613
+ rmSync2(f.path, { recursive: true, force: true });
18226
18614
  gone++;
18227
18615
  } catch {}
18228
18616
  }
@@ -18874,7 +19262,7 @@ class SelfWatch {
18874
19262
  }
18875
19263
 
18876
19264
  // ../daemon/src/core/state.ts
18877
- import { randomBytes as randomBytes2 } from "crypto";
19265
+ import { randomBytes as randomBytes3 } from "crypto";
18878
19266
  import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
18879
19267
  var GROUPS_SAVE_MS = 250;
18880
19268
  var SEEN_WORKTREES = 50;
@@ -18936,7 +19324,7 @@ function loadOrCreateToken(paths) {
18936
19324
  const TOKEN_FILE = paths.tokenFile;
18937
19325
  if (existsSync14(TOKEN_FILE))
18938
19326
  return readFileSync10(TOKEN_FILE, "utf8").trim();
18939
- const token = randomBytes2(32).toString("hex");
19327
+ const token = randomBytes3(32).toString("hex");
18940
19328
  writeFileSync5(TOKEN_FILE, token, { mode: 384 });
18941
19329
  return token;
18942
19330
  }
@@ -19150,7 +19538,7 @@ class StateStore {
19150
19538
  }
19151
19539
 
19152
19540
  // ../daemon/src/design/service.ts
19153
- import { join as join11 } from "path";
19541
+ import { join as join12 } from "path";
19154
19542
 
19155
19543
  // ../daemon/src/design/scan.ts
19156
19544
  var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
@@ -19600,7 +19988,7 @@ class DesignService {
19600
19988
  async function readAll(root, paths) {
19601
19989
  const out = [];
19602
19990
  for (const path of paths) {
19603
- const file = Bun.file(join11(root, path));
19991
+ const file = Bun.file(join12(root, path));
19604
19992
  if (file.size === 0 || file.size > MAX_FILE_BYTES)
19605
19993
  continue;
19606
19994
  try {
@@ -19715,7 +20103,7 @@ function typicalUse(classes) {
19715
20103
  var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
19716
20104
  async function loadProjectTypescript(root) {
19717
20105
  try {
19718
- return await import(join11(root, "node_modules", "typescript", "lib", "typescript.js"));
20106
+ return await import(join12(root, "node_modules", "typescript", "lib", "typescript.js"));
19719
20107
  } catch {
19720
20108
  return null;
19721
20109
  }
@@ -19728,28 +20116,32 @@ var SAVE_DELAY_MS = 1000;
19728
20116
  class DraftStore {
19729
20117
  d;
19730
20118
  drafts = {};
20119
+ lists = {};
20120
+ dirty = { text: false, lists: false };
19731
20121
  saveTimer = null;
19732
20122
  constructor(d) {
19733
20123
  this.d = d;
19734
- if (!existsSync15(d.file))
19735
- return;
19736
- try {
19737
- const raw = JSON.parse(readFileSync11(d.file, "utf8"));
19738
- if (!raw || typeof raw !== "object")
19739
- return;
19740
- for (const [id, text] of Object.entries(raw)) {
19741
- if (typeof text === "string" && text)
19742
- this.drafts[id] = text;
19743
- }
19744
- } catch (e) {
19745
- 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;
19746
20135
  }
19747
20136
  }
19748
20137
  all() {
19749
20138
  return { ...this.drafts };
19750
20139
  }
20140
+ allAttachments() {
20141
+ return { ...this.lists };
20142
+ }
19751
20143
  has(boxId) {
19752
- return !!this.drafts[boxId]?.trim();
20144
+ return !!this.drafts[boxId]?.trim() || !!this.lists[boxId]?.length;
19753
20145
  }
19754
20146
  text(boxId) {
19755
20147
  return this.drafts[boxId] ?? "";
@@ -19761,22 +20153,67 @@ class DraftStore {
19761
20153
  this.drafts[boxId] = text;
19762
20154
  else
19763
20155
  delete this.drafts[boxId];
19764
- this.scheduleSave();
20156
+ this.scheduleSave("text");
19765
20157
  this.d.hub.emit("draftChanged", boxId, text, clientId);
19766
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
+ }
19767
20188
  drop(boxId) {
19768
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;
19769
20203
  }
19770
20204
  prune(known) {
19771
- let changed = false;
19772
20205
  for (const id of Object.keys(this.drafts)) {
19773
20206
  if (known(id))
19774
20207
  continue;
19775
20208
  delete this.drafts[id];
19776
- 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");
19777
20216
  }
19778
- if (changed)
19779
- this.scheduleSave();
19780
20217
  }
19781
20218
  flush() {
19782
20219
  if (!this.saveTimer)
@@ -19785,7 +20222,8 @@ class DraftStore {
19785
20222
  this.saveTimer = null;
19786
20223
  this.write();
19787
20224
  }
19788
- scheduleSave() {
20225
+ scheduleSave(which) {
20226
+ this.dirty[which] = true;
19789
20227
  if (this.saveTimer)
19790
20228
  return;
19791
20229
  this.saveTimer = setTimeout(() => {
@@ -19795,13 +20233,31 @@ class DraftStore {
19795
20233
  this.saveTimer.unref?.();
19796
20234
  }
19797
20235
  write() {
19798
- const tmp = `${this.d.file}.tmp`;
19799
- try {
19800
- writeFileSync6(tmp, JSON.stringify(this.drafts));
19801
- renameSync3(tmp, this.d.file);
19802
- } catch (e) {
19803
- log.error("drafts", "could not write the drafts", e);
19804
- }
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);
19805
20261
  }
19806
20262
  }
19807
20263
 
@@ -20030,17 +20486,17 @@ class ExecService {
20030
20486
  }
20031
20487
 
20032
20488
  // ../daemon/src/files/open.ts
20033
- import { stat as stat2 } from "fs/promises";
20489
+ import { stat as stat3 } from "fs/promises";
20034
20490
  import { basename as basename3, resolve as resolve7 } from "path";
20035
20491
 
20036
20492
  // ../daemon/src/repos/browse.ts
20037
20493
  import { existsSync as existsSync16, statSync as statSync5 } from "fs";
20038
20494
  import { readdir } from "fs/promises";
20039
20495
  import { homedir as homedir7 } from "os";
20040
- import { dirname as dirname5, join as join12 } from "path";
20496
+ import { dirname as dirname5, join as join13 } from "path";
20041
20497
  var MAX_ENTRIES = 40;
20042
20498
  function expandTilde(raw) {
20043
- return raw === "~" || raw.startsWith("~/") ? join12(homedir7(), raw.slice(1)) : raw;
20499
+ return raw === "~" || raw.startsWith("~/") ? join13(homedir7(), raw.slice(1)) : raw;
20044
20500
  }
20045
20501
  function collapseTilde(abs) {
20046
20502
  const home = homedir7();
@@ -20051,7 +20507,7 @@ function holdsNothing(names) {
20051
20507
  }
20052
20508
  async function describeFolder(abs) {
20053
20509
  const path = collapseTilde(abs);
20054
- if (existsSync16(join12(abs, ".git")))
20510
+ if (existsSync16(join13(abs, ".git")))
20055
20511
  return { path, kind: "project" };
20056
20512
  try {
20057
20513
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -20068,7 +20524,7 @@ function describe2(typed, parent) {
20068
20524
  return {
20069
20525
  exists: existsSync16(typed),
20070
20526
  isDir,
20071
- isRepo: isDir && existsSync16(join12(typed, ".git")),
20527
+ isRepo: isDir && existsSync16(join13(typed, ".git")),
20072
20528
  parentExists: existsSync16(parent)
20073
20529
  };
20074
20530
  }
@@ -20093,8 +20549,8 @@ async function browsePath(raw) {
20093
20549
  continue;
20094
20550
  if (!name.toLowerCase().startsWith(prefix))
20095
20551
  continue;
20096
- const abs = join12(dir, name);
20097
- 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")) });
20098
20554
  }
20099
20555
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
20100
20556
  return { entries: entries.slice(0, MAX_ENTRIES), target };
@@ -20131,7 +20587,7 @@ function looksBinary(text) {
20131
20587
 
20132
20588
  // ../daemon/src/files/service.ts
20133
20589
  import { spawn as spawn4 } from "child_process";
20134
- import { stat, unlink } from "fs/promises";
20590
+ import { stat as stat2, unlink } from "fs/promises";
20135
20591
 
20136
20592
  // ../daemon/src/git/blame.ts
20137
20593
  var NONE = { commits: [], lines: [] };
@@ -20202,10 +20658,13 @@ async function withLock(key, fn) {
20202
20658
  function withRepoLock(repoPath, fn) {
20203
20659
  return withLock(repoPath, fn);
20204
20660
  }
20661
+ function withFetchLock(repoPath, fn) {
20662
+ return withLock(`fetch:${repoPath}`, fn);
20663
+ }
20205
20664
  var fileLockKey = (absPath) => `file:${absPath}`;
20206
20665
 
20207
20666
  // ../daemon/src/git/status.ts
20208
- import { join as join13 } from "path";
20667
+ import { join as join14 } from "path";
20209
20668
  async function treeFingerprint(worktreePath) {
20210
20669
  const [head, diff, untracked] = await Promise.all([
20211
20670
  git(worktreePath, "rev-parse", "HEAD"),
@@ -20215,7 +20674,7 @@ async function treeFingerprint(worktreePath) {
20215
20674
  const others = await Promise.all(untracked.out.split(`
20216
20675
  `).filter(Boolean).map(async (p) => {
20217
20676
  try {
20218
- const f = Bun.file(join13(worktreePath, p));
20677
+ const f = Bun.file(join14(worktreePath, p));
20219
20678
  const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
20220
20679
  return `${p}:${body}`;
20221
20680
  } catch {
@@ -20248,6 +20707,10 @@ async function fileBefore(worktreePath, defaultBr, file) {
20248
20707
  const r = await gitRaw(worktreePath, "show", `${ref}:${file}`);
20249
20708
  return r.ok ? r.out : "";
20250
20709
  }
20710
+ async function fileAt(worktreePath, commit, file) {
20711
+ const r = await gitRaw(worktreePath, "show", `${commit}:${file}`);
20712
+ return r.ok ? r.out : "";
20713
+ }
20251
20714
  function parseNumstat(out) {
20252
20715
  const counts = new Map;
20253
20716
  for (const line of out.split(`
@@ -20266,7 +20729,7 @@ async function numstat(worktreePath, ...range) {
20266
20729
  }
20267
20730
  async function untrackedLines(worktreePath, file) {
20268
20731
  try {
20269
- const f = Bun.file(join13(worktreePath, file));
20732
+ const f = Bun.file(join14(worktreePath, file));
20270
20733
  if (f.size > 2000000)
20271
20734
  return {};
20272
20735
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -20439,7 +20902,7 @@ var COMMON = 40;
20439
20902
  var NEAR = 3;
20440
20903
  var CLEAR_LEAD = 1.5;
20441
20904
  var ELEMENT_SOURCES_MAX = 8;
20442
- var NAME = /^[\w-]+$/;
20905
+ var NAME2 = /^[\w-]+$/;
20443
20906
  var TAG = /^[a-z][a-z0-9-]*$/;
20444
20907
  var NAME_CHAR = /[\w-]/;
20445
20908
  function needlesOf(el) {
@@ -20452,12 +20915,12 @@ function needlesOf(el) {
20452
20915
  out.set(text, { text, weight, word });
20453
20916
  };
20454
20917
  if (el.id)
20455
- add(el.id, 8, NAME.test(el.id));
20918
+ add(el.id, 8, NAME2.test(el.id));
20456
20919
  const words = (el.text.split(/[&<>"'`${}]/)[0] ?? "").trim().split(/\s+/).slice(0, 6).join(" ");
20457
20920
  if (words.length >= 3 && /\p{L}/u.test(words))
20458
20921
  add(words, 6, false);
20459
20922
  for (const [, value] of el.attrs)
20460
- add(value, 5, NAME.test(value));
20923
+ add(value, 5, NAME2.test(value));
20461
20924
  for (const c of el.classes)
20462
20925
  add(c, 3, true);
20463
20926
  return [...out.values()];
@@ -20623,7 +21086,7 @@ var NOT_WHERE_WRITTEN = [
20623
21086
  ];
20624
21087
  var NO_TEXT = { before: "", after: "" };
20625
21088
  async function statFile(target) {
20626
- const st = await stat(target).catch((e) => {
21089
+ const st = await stat2(target).catch((e) => {
20627
21090
  if (e.code === "ENOENT")
20628
21091
  return null;
20629
21092
  throw e;
@@ -20684,14 +21147,14 @@ class FileService {
20684
21147
  throw new UserError("unknown worktree");
20685
21148
  return r;
20686
21149
  }
20687
- async read(worktreeId, path, ref) {
21150
+ async read(worktreeId, path, ref, since) {
20688
21151
  const r = this.require(worktreeId);
20689
21152
  const target = resolveInside(r.path, path);
20690
21153
  if (ref) {
20691
21154
  const { before, after } = await fileAtCommit(r.path, ref, path);
20692
21155
  return { ...sides(before, after), version: null, writable: false };
20693
21156
  }
20694
- 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);
20695
21158
  const st = await statFile(target);
20696
21159
  if (st && st.size > FILE_MAX_CHARS) {
20697
21160
  return { ...NO_TEXT, version: `${st.size}-${st.mtimeMs}`, writable: false, binary: false, tooLarge: true };
@@ -20870,7 +21333,7 @@ class OpenService {
20870
21333
  }
20871
21334
  async locate(raw) {
20872
21335
  const path = canonical(resolve7(expandTilde(raw)));
20873
- const st = await stat2(path).catch((e) => {
21336
+ const st = await stat3(path).catch((e) => {
20874
21337
  if (e.code === "ENOENT")
20875
21338
  return null;
20876
21339
  throw e;
@@ -21149,9 +21612,9 @@ class AfterLand {
21149
21612
  }
21150
21613
 
21151
21614
  // ../daemon/src/repos/registry.ts
21152
- 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";
21153
21616
  import { homedir as homedir9 } from "os";
21154
- 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";
21155
21618
 
21156
21619
  // ../daemon/src/git/base.ts
21157
21620
  async function resolveBase(repoPath, defaultBranch, route) {
@@ -21162,8 +21625,8 @@ async function resolveBase(repoPath, defaultBranch, route) {
21162
21625
  }
21163
21626
 
21164
21627
  // ../daemon/src/repos/config.ts
21165
- import { closeSync, existsSync as existsSync17, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync12, readSync, statSync as statSync6 } from "fs";
21166
- 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";
21167
21630
 
21168
21631
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
21169
21632
  function createScanner(text, ignoreTrivia = false) {
@@ -22527,7 +22990,7 @@ function applyEdits(text, edits) {
22527
22990
 
22528
22991
  // ../daemon/src/repos/config.ts
22529
22992
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
22530
- 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)));
22531
22994
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
22532
22995
  function parseSettings(text) {
22533
22996
  const errors = [];
@@ -22552,7 +23015,7 @@ function readConfigFile(repoPath) {
22552
23015
  for (const rel of files) {
22553
23016
  let raw;
22554
23017
  try {
22555
- raw = parseSettings(readFileSync12(join14(repoPath, rel), "utf8"));
23018
+ raw = parseSettings(readFileSync12(join15(repoPath, rel), "utf8"));
22556
23019
  } catch (e) {
22557
23020
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
22558
23021
  }
@@ -22611,7 +23074,7 @@ function configTarget(repoPath, made) {
22611
23074
  }
22612
23075
  function configBody(repoPath, rel, config) {
22613
23076
  const place = PLACES.find((p) => p.local === rel);
22614
- const abs = place && join14(repoPath, place.shared);
23077
+ const abs = place && join15(repoPath, place.shared);
22615
23078
  if (!abs || !existsSync17(abs))
22616
23079
  return config;
22617
23080
  let shared;
@@ -22629,7 +23092,7 @@ function configText(repoPath, rel, config) {
22629
23092
  let text;
22630
23093
  let was;
22631
23094
  try {
22632
- text = readFileSync12(join14(repoPath, rel), "utf8");
23095
+ text = readFileSync12(join15(repoPath, rel), "utf8");
22633
23096
  was = parseSettings(text);
22634
23097
  } catch {
22635
23098
  return fresh;
@@ -22659,7 +23122,7 @@ function detectConfig(repoPath) {
22659
23122
  return { config: explicit.config, needsSetup: false };
22660
23123
  if (explicit)
22661
23124
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
22662
- const pkgPath = join14(repoPath, "package.json");
23125
+ const pkgPath = join15(repoPath, "package.json");
22663
23126
  if (existsSync17(pkgPath)) {
22664
23127
  const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
22665
23128
  const scripts = pkg.scripts ?? {};
@@ -22682,7 +23145,7 @@ function detectConfig(repoPath) {
22682
23145
  };
22683
23146
  }
22684
23147
  }
22685
- if (existsSync17(join14(repoPath, "start.sh"))) {
23148
+ if (existsSync17(join15(repoPath, "start.sh"))) {
22686
23149
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
22687
23150
  }
22688
23151
  const assumed = assumeNothingToRun(repoPath);
@@ -22725,24 +23188,24 @@ function assumeNothingToRun(repoPath) {
22725
23188
  return marker && !servesPage(repoPath) ? marker : undefined;
22726
23189
  }
22727
23190
  function markerIn(repoPath) {
22728
- const named = NO_PAGE_MARKERS.find((f) => existsSync17(join14(repoPath, f)));
23191
+ const named = NO_PAGE_MARKERS.find((f) => existsSync17(join15(repoPath, f)));
22729
23192
  if (named)
22730
23193
  return named;
22731
23194
  let entries;
22732
23195
  try {
22733
- entries = readdirSync4(repoPath);
23196
+ entries = readdirSync5(repoPath);
22734
23197
  } catch {
22735
23198
  return;
22736
23199
  }
22737
23200
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
22738
23201
  }
22739
23202
  function servesPage(repoPath) {
22740
- if (WEB_FILES.some((f) => existsSync17(join14(repoPath, f))))
23203
+ if (WEB_FILES.some((f) => existsSync17(join15(repoPath, f))))
22741
23204
  return true;
22742
- if (/^web:/m.test(head(join14(repoPath, "Procfile"))))
23205
+ if (/^web:/m.test(head(join15(repoPath, "Procfile"))))
22743
23206
  return true;
22744
23207
  for (const [files, names] of WEB_DEPENDENCIES) {
22745
- const text = files.map((f) => head(join14(repoPath, f))).join(`
23208
+ const text = files.map((f) => head(join15(repoPath, f))).join(`
22746
23209
  `);
22747
23210
  if (text.trim() && names.some((n) => mentions(text, n)))
22748
23211
  return true;
@@ -22752,14 +23215,14 @@ function servesPage(repoPath) {
22752
23215
  function frontEndBeside(repoPath) {
22753
23216
  let dirs;
22754
23217
  try {
22755
- dirs = readdirSync4(repoPath, { withFileTypes: true });
23218
+ dirs = readdirSync5(repoPath, { withFileTypes: true });
22756
23219
  } catch {
22757
23220
  return false;
22758
23221
  }
22759
23222
  return dirs.some((d) => {
22760
23223
  if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
22761
23224
  return false;
22762
- const pkg = head(join14(repoPath, d.name, "package.json"));
23225
+ const pkg = head(join15(repoPath, d.name, "package.json"));
22763
23226
  if (!pkg)
22764
23227
  return false;
22765
23228
  try {
@@ -22815,11 +23278,11 @@ function isLocalDatabase(url) {
22815
23278
  }
22816
23279
  function detectServices(repoPath) {
22817
23280
  const out = {};
22818
- const compose = COMPOSE_FILES.find((f) => existsSync17(join14(repoPath, f)));
23281
+ const compose = COMPOSE_FILES.find((f) => existsSync17(join15(repoPath, f)));
22819
23282
  if (compose)
22820
23283
  out.compose = compose;
22821
23284
  for (const file of ENV_FILES) {
22822
- const text = head(join14(repoPath, file));
23285
+ const text = head(join15(repoPath, file));
22823
23286
  if (!text)
22824
23287
  continue;
22825
23288
  for (const name of DB_URL_NAMES) {
@@ -22843,7 +23306,7 @@ function inferPaths(cmd, repoPath, depth = 1) {
22843
23306
  return;
22844
23307
  let dir = false;
22845
23308
  try {
22846
- dir = statSync6(join14(repoPath, clean)).isDirectory();
23309
+ dir = statSync6(join15(repoPath, clean)).isDirectory();
22847
23310
  } catch {
22848
23311
  return;
22849
23312
  }
@@ -22854,25 +23317,25 @@ function inferPaths(cmd, repoPath, depth = 1) {
22854
23317
  const t = tokens[i];
22855
23318
  if (t === "cd") {
22856
23319
  const dir = tokens[i + 1];
22857
- if (dir && !dir.startsWith("-") && existsSync17(join14(repoPath, dir)))
23320
+ if (dir && !dir.startsWith("-") && existsSync17(join15(repoPath, dir)))
22858
23321
  add(dir);
22859
23322
  continue;
22860
23323
  }
22861
23324
  if (t.startsWith("-") || t.startsWith("$"))
22862
23325
  continue;
22863
23326
  const mod = /^([A-Za-z_]\w*)(?:\.\w+)*:\w+$/.exec(t);
22864
- if (mod?.[1] && existsSync17(join14(repoPath, mod[1]))) {
23327
+ if (mod?.[1] && existsSync17(join15(repoPath, mod[1]))) {
22865
23328
  add(mod[1]);
22866
23329
  continue;
22867
23330
  }
22868
23331
  const rel = t.replace(/^\.\//, "");
22869
23332
  if (rel.startsWith("/") || rel.startsWith("..") || rel.includes("="))
22870
23333
  continue;
22871
- if (!existsSync17(join14(repoPath, rel)))
23334
+ if (!existsSync17(join15(repoPath, rel)))
22872
23335
  continue;
22873
23336
  add(rel);
22874
23337
  if (depth > 0 && rel.endsWith(".sh")) {
22875
- 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))
22876
23339
  out.add(p);
22877
23340
  }
22878
23341
  }
@@ -22895,9 +23358,9 @@ function procCommand(runner, script, body) {
22895
23358
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
22896
23359
  }
22897
23360
  function detectPages(repoPath, scripts, runner) {
22898
- if (!scripts.build || !existsSync17(join14(repoPath, "functions")))
23361
+ if (!scripts.build || !existsSync17(join15(repoPath, "functions")))
22899
23362
  return null;
22900
- 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));
22901
23364
  if (!cfg)
22902
23365
  return null;
22903
23366
  const out = readFileSync12(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
@@ -22906,20 +23369,20 @@ function detectPages(repoPath, scripts, runner) {
22906
23369
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
22907
23370
  }
22908
23371
  function detectRunner(repoPath, packageManager) {
22909
- if (existsSync17(join14(repoPath, "bun.lock")) || existsSync17(join14(repoPath, "bun.lockb")))
23372
+ if (existsSync17(join15(repoPath, "bun.lock")) || existsSync17(join15(repoPath, "bun.lockb")))
22910
23373
  return "bun";
22911
- if (existsSync17(join14(repoPath, "pnpm-lock.yaml")))
23374
+ if (existsSync17(join15(repoPath, "pnpm-lock.yaml")))
22912
23375
  return "pnpm";
22913
- 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"))
22914
23377
  return "yarn";
22915
23378
  return "npm";
22916
23379
  }
22917
23380
 
22918
23381
  // ../daemon/src/repos/create.ts
22919
- 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";
22920
23383
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
22921
23384
  import { homedir as homedir8 } from "os";
22922
- 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";
22923
23386
  function isInside(dir, parent) {
22924
23387
  const p = parent.replace(/\/+$/, "");
22925
23388
  return dir === p || dir.startsWith(`${p}/`);
@@ -22930,29 +23393,29 @@ function planProject(opts, home = homedir8()) {
22930
23393
  throw new UserError(nameError);
22931
23394
  const first = firstProjectsFolder(opts.parent, home);
22932
23395
  const parent = first ?? resolveParent(opts.parent);
22933
- const dir = join15(parent, opts.name.trim());
23396
+ const dir = join16(parent, opts.name.trim());
22934
23397
  if (existsSync18(dir))
22935
23398
  throw new UserError(`${dir} already exists`);
22936
23399
  return { parent, dir, makeParent: first !== null };
22937
23400
  }
22938
23401
  function firstProjectsFolder(raw, home) {
22939
23402
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
22940
- if (typed !== join15(home, PROJECTS_FOLDER) || existsSync18(typed) || !existsSync18(home))
23403
+ if (typed !== join16(home, PROJECTS_FOLDER) || existsSync18(typed) || !existsSync18(home))
22941
23404
  return null;
22942
- return join15(realpathSync2(home), PROJECTS_FOLDER);
23405
+ return join16(realpathSync2(home), PROJECTS_FOLDER);
22943
23406
  }
22944
23407
  function planInPlace(opts) {
22945
23408
  const name = opts.name.trim();
22946
23409
  if (!name || name === "." || name === ".." || name.includes("/")) {
22947
23410
  throw new UserError(`${opts.name} is not a folder name`);
22948
23411
  }
22949
- const typed = join15(resolveParent(opts.parent), name);
23412
+ const typed = join16(resolveParent(opts.parent), name);
22950
23413
  if (!existsSync18(typed) || !statSync7(typed).isDirectory())
22951
23414
  throw new UserError(`${typed} is not a folder`);
22952
23415
  const dir = realpathSync2(typed);
22953
- if (existsSync18(join15(dir, ".git")))
23416
+ if (existsSync18(join16(dir, ".git")))
22954
23417
  throw new UserError(`${dir} is already a project; open it instead`);
22955
- if (!holdsNothing(readdirSync5(dir))) {
23418
+ if (!holdsNothing(readdirSync6(dir))) {
22956
23419
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
22957
23420
  }
22958
23421
  return { parent: dirname6(dir), dir, makeParent: false };
@@ -23041,13 +23504,13 @@ async function undoCreate(dir) {
23041
23504
  }
23042
23505
  async function undoInit(dir) {
23043
23506
  try {
23044
- await rm(join15(dir, ".git"), { recursive: true, force: true });
23507
+ await rm(join16(dir, ".git"), { recursive: true, force: true });
23045
23508
  } catch (e) {
23046
23509
  log.warn(dir, "could not take back a half-made .git", e);
23047
23510
  }
23048
23511
  }
23049
23512
  async function unmakeProject(dir, made) {
23050
- const target = made === "folder" ? dir : join15(dir, ".git");
23513
+ const target = made === "folder" ? dir : join16(dir, ".git");
23051
23514
  try {
23052
23515
  await rm(target, { recursive: true, force: true });
23053
23516
  } catch (e) {
@@ -23078,7 +23541,7 @@ async function ok(p, what) {
23078
23541
 
23079
23542
  // ../daemon/src/repos/watcher.ts
23080
23543
  import { existsSync as existsSync19, watch } from "fs";
23081
- import { basename as basename4, join as join16 } from "path";
23544
+ import { basename as basename4, join as join17 } from "path";
23082
23545
  function watchDefaultBranch(repoPath, branch, onMove) {
23083
23546
  const last = { local: null, upstream: null };
23084
23547
  const read = async () => {
@@ -23131,13 +23594,13 @@ function watchDefaultBranch(repoPath, branch, onMove) {
23131
23594
  ]);
23132
23595
  if (stopped)
23133
23596
  return;
23134
- const gitDir = common.ok && common.out ? common.out : join16(repoPath, ".git");
23597
+ const gitDir = common.ok && common.out ? common.out : join17(repoPath, ".git");
23135
23598
  tryWatch(gitDir);
23136
- tryWatch(join16(gitDir, "refs", "heads"));
23599
+ tryWatch(join17(gitDir, "refs", "heads"));
23137
23600
  if (!remote.ok || !remote.out)
23138
23601
  return;
23139
- const remotes = join16(gitDir, "refs", "remotes");
23140
- const mine = join16(remotes, remote.out);
23602
+ const remotes = join17(gitDir, "refs", "remotes");
23603
+ const mine = join17(remotes, remote.out);
23141
23604
  let inner = null;
23142
23605
  let mid = null;
23143
23606
  const armInner = () => {
@@ -23153,7 +23616,7 @@ function watchDefaultBranch(repoPath, branch, onMove) {
23153
23616
  }) : null;
23154
23617
  armInner();
23155
23618
  };
23156
- tryWatch(join16(gitDir, "refs"), (_event, filename) => {
23619
+ tryWatch(join17(gitDir, "refs"), (_event, filename) => {
23157
23620
  if (!filename || filename === "remotes")
23158
23621
  armMid();
23159
23622
  schedule();
@@ -23182,7 +23645,7 @@ function watchWorktreeDir(repoPath, onChange) {
23182
23645
  }, 300);
23183
23646
  };
23184
23647
  const arm = (commonDir) => {
23185
- const dir = join16(commonDir, "worktrees");
23648
+ const dir = join17(commonDir, "worktrees");
23186
23649
  const armInner = () => {
23187
23650
  inner?.close();
23188
23651
  inner = null;
@@ -23225,7 +23688,7 @@ function watchWorktreeDir(repoPath, onChange) {
23225
23688
  function watchConfigFile(repoPath, onChange) {
23226
23689
  const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
23227
23690
  const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename4(rel)));
23228
- const dir = join16(repoPath, CONFIG_DIR);
23691
+ const dir = join17(repoPath, CONFIG_DIR);
23229
23692
  let timer = null;
23230
23693
  let outer = null;
23231
23694
  let inner = null;
@@ -23362,7 +23825,7 @@ class RepoRegistry {
23362
23825
  return `${repo.name} is set up now`;
23363
23826
  if (main.promptedAt || main.lastTurn)
23364
23827
  return `${repo.name} has a chat now`;
23365
- if (existsSync20(join17(repo.path, ".claude")))
23828
+ if (existsSync20(join18(repo.path, ".claude")))
23366
23829
  return `an agent has already run in ${repo.name}`;
23367
23830
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
23368
23831
  return `${repo.name} has files in it now`;
@@ -23513,10 +23976,10 @@ class RepoRegistry {
23513
23976
  throw new UserError(current.reason);
23514
23977
  const was = configTarget(repo.path, !!repo.made);
23515
23978
  const rel = configSibling(was, kind);
23516
- const moved = rel !== was && existsSync20(join17(repo.path, was));
23979
+ const moved = rel !== was && existsSync20(join18(repo.path, was));
23517
23980
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
23518
23981
  try {
23519
- rmSync2(join17(repo.path, was));
23982
+ rmSync3(join18(repo.path, was));
23520
23983
  } catch (e) {
23521
23984
  log.warn(repoId, `could not remove ${was}`, e);
23522
23985
  }
@@ -23525,9 +23988,9 @@ class RepoRegistry {
23525
23988
  }
23526
23989
  if (isLocalConfigFile(rel))
23527
23990
  await excludeFromGit(repo.path, rel);
23528
- const file = join17(repo.path, rel);
23991
+ const file = join18(repo.path, rel);
23529
23992
  try {
23530
- mkdirSync6(dirname7(file), { recursive: true });
23993
+ mkdirSync7(dirname7(file), { recursive: true });
23531
23994
  writeFileSync7(file, configText(repo.path, rel, config));
23532
23995
  } catch (e) {
23533
23996
  log.warn(repoId, `could not write ${rel}`, e);
@@ -23629,7 +24092,7 @@ class RepoRegistry {
23629
24092
  }
23630
24093
  placeConfig(repo) {
23631
24094
  const rel = configTarget(repo.path, !!repo.made);
23632
- if (isLocalConfigFile(rel) && existsSync20(join17(repo.path, rel))) {
24095
+ if (isLocalConfigFile(rel) && existsSync20(join18(repo.path, rel))) {
23633
24096
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
23634
24097
  }
23635
24098
  if (repo.configFile === rel)
@@ -23721,7 +24184,7 @@ class RepoRegistry {
23721
24184
  }
23722
24185
 
23723
24186
  // ../daemon/src/routes/service.ts
23724
- import { join as join18 } from "path";
24187
+ import { join as join19 } from "path";
23725
24188
 
23726
24189
  // ../daemon/src/routes/fileRouters.ts
23727
24190
  var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
@@ -24921,7 +25384,7 @@ class RouteService {
24921
25384
  return mergeRoutes(fileRoutes(paths, roots), code);
24922
25385
  }
24923
25386
  async cachedRead(root, path, had, kept, decode) {
24924
- const abs = join18(root, path);
25387
+ const abs = join19(root, path);
24925
25388
  const stat = await this.fs.stat(abs);
24926
25389
  if (!stat)
24927
25390
  return null;
@@ -24946,7 +25409,7 @@ class RouteService {
24946
25409
  }
24947
25410
  }
24948
25411
  async hash(root, file) {
24949
- const abs = join18(root, file);
25412
+ const abs = join19(root, file);
24950
25413
  const stat = await this.fs.stat(abs);
24951
25414
  if (!stat)
24952
25415
  return null;
@@ -25268,7 +25731,7 @@ function servedLocally(origin) {
25268
25731
  import { homedir as homedir11 } from "os";
25269
25732
 
25270
25733
  // ../daemon/src/core/pair.ts
25271
- import { randomBytes as randomBytes3 } from "crypto";
25734
+ import { randomBytes as randomBytes4 } from "crypto";
25272
25735
  class PairCodes {
25273
25736
  now;
25274
25737
  live = null;
@@ -25276,7 +25739,7 @@ class PairCodes {
25276
25739
  this.now = now;
25277
25740
  }
25278
25741
  mint() {
25279
- const code = randomBytes3(9).toString("base64url");
25742
+ const code = randomBytes4(9).toString("base64url");
25280
25743
  this.live = { code, until: this.now() + PAIR_TTL_MS };
25281
25744
  return { code, ms: PAIR_TTL_MS };
25282
25745
  }
@@ -25287,10 +25750,13 @@ class PairCodes {
25287
25750
  }
25288
25751
  }
25289
25752
 
25753
+ // ../daemon/src/server/handlers.ts
25754
+ import { randomBytes as randomBytes5 } from "crypto";
25755
+
25290
25756
  // ../daemon/src/agent/config.ts
25291
25757
  import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
25292
25758
  import { homedir as homedir10 } from "os";
25293
- import { join as join19 } from "path";
25759
+ import { join as join20 } from "path";
25294
25760
  function agentConfigFiles(spec, repoPath, home = homedir10()) {
25295
25761
  const file = (id, label, path) => ({
25296
25762
  id,
@@ -25300,30 +25766,30 @@ function agentConfigFiles(spec, repoPath, home = homedir10()) {
25300
25766
  });
25301
25767
  if (spec.id === "claude") {
25302
25768
  return [
25303
- file("user-config", "user config (MCP servers, projects)", join19(home, ".claude.json")),
25304
- file("user-settings", "user settings (permissions, hooks)", join19(home, ".claude", "settings.json")),
25305
- ...repoPath ? [file("project-mcp", "project MCP servers", join19(repoPath, ".mcp.json"))] : [],
25306
- ...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"))] : []
25307
25773
  ];
25308
25774
  }
25309
25775
  if (spec.id === "codex")
25310
- return [file("user-config", "config", join19(home, ".codex", "config.toml"))];
25776
+ return [file("user-config", "config", join20(home, ".codex", "config.toml"))];
25311
25777
  return [];
25312
25778
  }
25313
25779
  function describeAgentConfig(spec, repoPath, home = homedir10()) {
25314
25780
  const files = agentConfigFiles(spec, repoPath, home);
25315
25781
  const servers = [];
25316
25782
  if (spec.id === "claude") {
25317
- const user = readJson(join19(home, ".claude.json"));
25783
+ const user = readJson(join20(home, ".claude.json"));
25318
25784
  servers.push(...serversOf(user?.mcpServers, "user"));
25319
25785
  if (repoPath) {
25320
25786
  const projects = user?.projects;
25321
25787
  const mine = projects?.[repoPath];
25322
25788
  servers.push(...serversOf(mine?.mcpServers, "local"));
25323
- servers.push(...serversOf(readJson(join19(repoPath, ".mcp.json"))?.mcpServers, "project"));
25789
+ servers.push(...serversOf(readJson(join20(repoPath, ".mcp.json"))?.mcpServers, "project"));
25324
25790
  }
25325
25791
  } else if (spec.id === "codex") {
25326
- servers.push(...codexServers(join19(home, ".codex", "config.toml")));
25792
+ servers.push(...codexServers(join20(home, ".codex", "config.toml")));
25327
25793
  }
25328
25794
  return { agent: spec.id, files, servers };
25329
25795
  }
@@ -25961,6 +26427,9 @@ var handlers = {
25961
26427
  s.drafts.set(msg.boxId, msg.text, msg.clientId);
25962
26428
  s.worktrees.spare.typed(msg.boxId, msg.text);
25963
26429
  },
26430
+ "set-attachments"(msg, _ctx, s) {
26431
+ s.drafts.setAttachments(msg.boxId, msg.items, msg.clientId);
26432
+ },
25964
26433
  "refresh-git"(msg, _ctx, s) {
25965
26434
  s.worktrees.recount(msg.repoId);
25966
26435
  },
@@ -25973,19 +26442,17 @@ var handlers = {
25973
26442
  "forget-visit"(msg, _ctx, s) {
25974
26443
  s.routes.forget(msg.repoId, msg.path);
25975
26444
  },
25976
- async chat(msg, _ctx, s) {
25977
- 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, {
25978
26447
  text: msg.text,
25979
26448
  clientId: msg.clientId,
25980
26449
  context: msg.context,
25981
26450
  attachments: msg.attachments
25982
- });
26451
+ }));
25983
26452
  },
25984
- async "create-worktree"(msg, _ctx, s) {
25985
- await s.worktrees.create(msg.repoId, msg.prompt, {
26453
+ async "create-worktree"(msg, ctx, s) {
26454
+ const opts = {
25986
26455
  createdBy: msg.clientId,
25987
- worktreeId: msg.worktreeId,
25988
- variant: msg.variant,
25989
26456
  context: msg.context,
25990
26457
  attachments: msg.attachments,
25991
26458
  agent: msg.agent,
@@ -25994,6 +26461,30 @@ var handlers = {
25994
26461
  model: msg.model,
25995
26462
  effort: msg.effort,
25996
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];
25997
26488
  });
25998
26489
  },
25999
26490
  "set-worktree-profile"(msg, _ctx, s) {
@@ -26016,6 +26507,8 @@ var handlers = {
26016
26507
  },
26017
26508
  "batch-worktrees"(msg, ctx, s) {
26018
26509
  const repo = s.state.requireRepo(msg.repoId);
26510
+ if (msg.boxId)
26511
+ s.drafts.take(msg.boxId);
26019
26512
  fireAndForget(msg.repoId, (async () => {
26020
26513
  const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
26021
26514
  const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
@@ -26039,8 +26532,10 @@ var handlers = {
26039
26532
  "list-archived"(msg, ctx, s) {
26040
26533
  ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
26041
26534
  },
26042
- async "restore-worktree"(msg, _ctx, s) {
26043
- 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
+ });
26044
26539
  },
26045
26540
  async "delete-archived"(msg, _ctx, s) {
26046
26541
  await s.worktrees.deleteArchived(msg.archiveId);
@@ -26052,10 +26547,17 @@ var handlers = {
26052
26547
  await gitStatus(s, ctx, msg.worktreeId);
26053
26548
  },
26054
26549
  async "read-file"(msg, ctx, s) {
26055
- 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
+ };
26056
26558
  try {
26057
26559
  const kept = s.worktrees.readable(msg.worktreeId) ? null : await s.worktrees.archivedFile(msg.worktreeId, msg.path, msg.ref);
26058
- 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);
26059
26561
  ctx.reply({ ...head, ...read });
26060
26562
  } catch (e) {
26061
26563
  if (!(e instanceof UserError))
@@ -26123,8 +26625,8 @@ var handlers = {
26123
26625
  "update-now"(_msg, _ctx, s) {
26124
26626
  return s.update.updateNow();
26125
26627
  },
26126
- "check-update"(_msg, _ctx, s) {
26127
- return s.update.checkNow();
26628
+ async "check-update"(_msg, ctx, s) {
26629
+ ctx.reply({ t: "update-checked", check: await s.update.checkNow() });
26128
26630
  },
26129
26631
  async commit(msg, ctx, s) {
26130
26632
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
@@ -26168,7 +26670,21 @@ var handlers = {
26168
26670
  },
26169
26671
  unqueue(msg, _ctx, s) {
26170
26672
  requireRun(s, msg.worktreeId);
26171
- 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 });
26172
26688
  },
26173
26689
  async "changed-ranges"(msg, ctx, s) {
26174
26690
  const { ranges, lineOffset } = await s.files.changedRanges(msg.worktreeId, msg.path);
@@ -26368,6 +26884,10 @@ var handlers = {
26368
26884
  if (!s.runs.stop(msg.worktreeId, msg.kind))
26369
26885
  throw new UserError(`no ${msg.kind} is running here`);
26370
26886
  },
26887
+ "preview-start"(msg, _ctx, s) {
26888
+ requireRun(s, msg.worktreeId);
26889
+ s.worktrees.startPreview(msg.worktreeId);
26890
+ },
26371
26891
  async "search-refs"(msg, ctx, s) {
26372
26892
  const refs = await s.refs.search(msg.repoId, msg.query);
26373
26893
  ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
@@ -26380,6 +26900,30 @@ var handlers = {
26380
26900
  await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
26381
26901
  }
26382
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
+ }
26383
26927
  function looseCwd(s, id, stream) {
26384
26928
  const disc = s.worktrees.discoveredById(id);
26385
26929
  if (!disc)
@@ -26405,9 +26949,16 @@ async function dispatch(msg, ctx, s) {
26405
26949
 
26406
26950
  // ../daemon/src/server/http.ts
26407
26951
  import { existsSync as existsSync23 } from "fs";
26408
- import { join as join20 } from "path";
26952
+ import { join as join21 } from "path";
26409
26953
  var IMMUTABLE = "private, max-age=31536000, immutable";
26410
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
+ }
26411
26962
  function createFetch(opts) {
26412
26963
  const grant = previewGrant(opts.token);
26413
26964
  const remote = opts.remote;
@@ -26541,6 +27092,37 @@ function createFetch(opts) {
26541
27092
  return new Response("open failed; see daemon log", { status: 500 });
26542
27093
  }
26543
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
+ }
26544
27126
  if (url.pathname.startsWith("/attachments/")) {
26545
27127
  if (!sameSecret(url.searchParams.get("token"), opts.token))
26546
27128
  return new Response("unauthorized", { status: 401 });
@@ -26551,7 +27133,9 @@ function createFetch(opts) {
26551
27133
  const path = places.find((p) => !!p && existsSync23(p));
26552
27134
  if (!path)
26553
27135
  return new Response("not found", { status: 404 });
26554
- 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
+ });
26555
27139
  }
26556
27140
  if (url.pathname.startsWith("/files/")) {
26557
27141
  if (!sameSecret(url.searchParams.get("token"), opts.token))
@@ -26579,14 +27163,14 @@ function createFetch(opts) {
26579
27163
  });
26580
27164
  }
26581
27165
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
26582
- const file = join20(opts.shellDist, rel.replaceAll("..", ""));
27166
+ const file = join21(opts.shellDist, rel.replaceAll("..", ""));
26583
27167
  const hashed = rel.startsWith("/assets/");
26584
27168
  if (existsSync23(file) && Bun.file(file).size > 0) {
26585
27169
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
26586
27170
  }
26587
27171
  if (hashed)
26588
27172
  return new Response("not found", { status: 404 });
26589
- const index = join20(opts.shellDist, "index.html");
27173
+ const index = join21(opts.shellDist, "index.html");
26590
27174
  if (existsSync23(index))
26591
27175
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
26592
27176
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
@@ -26598,6 +27182,22 @@ var DROP_ABOVE_BYTES = 4 * 1024 * 1024;
26598
27182
  var DROPPED_NOTICE = `\r
26599
27183
  [toyon] output dropped: this pane was too far behind\r
26600
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
+ }
26601
27201
  function startServer(opts) {
26602
27202
  const { services: s, token, version } = opts;
26603
27203
  const sockets = new Set;
@@ -26695,6 +27295,7 @@ function startServer(opts) {
26695
27295
  },
26696
27296
  costs: s.idle.costs(),
26697
27297
  updates: s.update.status(),
27298
+ agents: s.agents.adapters(),
26698
27299
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
26699
27300
  });
26700
27301
  let statusesInFlight = false;
@@ -26822,6 +27423,7 @@ function startServer(opts) {
26822
27423
  s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
26823
27424
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
26824
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 } : {} }));
26825
27427
  const helloFrame = async () => {
26826
27428
  await s.update.refresh();
26827
27429
  return {
@@ -26848,11 +27450,13 @@ function startServer(opts) {
26848
27450
  self: s.self.get(),
26849
27451
  update: s.update.get(),
26850
27452
  drafts: s.drafts.all(),
27453
+ attachments: s.drafts.allAttachments(),
26851
27454
  managed: managedView(opts.managed)
26852
27455
  };
26853
27456
  };
26854
27457
  let branded = false;
26855
27458
  const serverConfig = {
27459
+ maxRequestBodySize: UPLOAD_MAX_BYTES,
26856
27460
  hostname: cloud.bindHost,
26857
27461
  fetch: createFetch({
26858
27462
  token,
@@ -26880,7 +27484,6 @@ function startServer(opts) {
26880
27484
  }
26881
27485
  }),
26882
27486
  websocket: {
26883
- maxPayloadLength: 64 * 1024 * 1024,
26884
27487
  async open(ws) {
26885
27488
  const preview = ws.data.preview;
26886
27489
  if (preview) {
@@ -26922,7 +27525,7 @@ function startServer(opts) {
26922
27525
  }
26923
27526
  const parsed = parseClientMsg(json);
26924
27527
  if (!parsed.ok) {
26925
- send(ws, { t: "error", message: `invalid message: ${parsed.reason}` });
27528
+ send(ws, unparsedReply(json, parsed.reason, s.drafts));
26926
27529
  return;
26927
27530
  }
26928
27531
  const ctx = {
@@ -26989,16 +27592,16 @@ function startServer(opts) {
26989
27592
  }
26990
27593
 
26991
27594
  // ../daemon/src/themes/store.ts
26992
- 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";
26993
27596
  import { homedir as homedir12 } from "os";
26994
- 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";
26995
27598
  var home = homedir12();
26996
27599
  var defaultExtensionDirs = [
26997
- join21(home, ".vscode", "extensions"),
26998
- join21(home, ".vscode-insiders", "extensions"),
26999
- join21(home, ".vscode-oss", "extensions"),
27000
- join21(home, ".cursor", "extensions"),
27001
- 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"),
27002
27605
  "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
27003
27606
  "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
27004
27607
  "/Applications/Cursor.app/Contents/Resources/app/extensions",
@@ -27082,7 +27685,7 @@ class ThemeStore {
27082
27685
  name: json?.name ?? base,
27083
27686
  source: "file"
27084
27687
  });
27085
- writeFileSync8(join21(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
27688
+ writeFileSync8(join22(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
27086
27689
  `);
27087
27690
  this.load();
27088
27691
  return theme;
@@ -27114,10 +27717,10 @@ function loadDir(dir) {
27114
27717
  if (!existsSync24(dir))
27115
27718
  return [];
27116
27719
  const out = [];
27117
- for (const f of readdirSync6(dir)) {
27720
+ for (const f of readdirSync7(dir)) {
27118
27721
  if (!/\.jsonc?$/i.test(f))
27119
27722
  continue;
27120
- const file = join21(dir, f);
27723
+ const file = join22(dir, f);
27121
27724
  const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
27122
27725
  try {
27123
27726
  const json = readJsonc(file);
@@ -27138,13 +27741,13 @@ function discover(dir) {
27138
27741
  const out = [];
27139
27742
  let entries;
27140
27743
  try {
27141
- entries = readdirSync6(dir);
27744
+ entries = readdirSync7(dir);
27142
27745
  } catch {
27143
27746
  return out;
27144
27747
  }
27145
27748
  for (const name of entries) {
27146
- const extDir = join21(dir, name);
27147
- const pkgFile = join21(extDir, "package.json");
27749
+ const extDir = join22(dir, name);
27750
+ const pkgFile = join22(extDir, "package.json");
27148
27751
  try {
27149
27752
  if (!statSync9(extDir).isDirectory() || !existsSync24(pkgFile))
27150
27753
  continue;
@@ -27154,7 +27757,7 @@ function discover(dir) {
27154
27757
  continue;
27155
27758
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
27156
27759
  let nls = {};
27157
- const nlsFile = join21(extDir, "package.nls.json");
27760
+ const nlsFile = join22(extDir, "package.nls.json");
27158
27761
  if (existsSync24(nlsFile)) {
27159
27762
  try {
27160
27763
  nls = JSON.parse(readFileSync15(nlsFile, "utf8"));
@@ -27295,16 +27898,20 @@ class UpdateService {
27295
27898
  async check() {
27296
27899
  if (this.d.method === "none" || this.d.managedBy)
27297
27900
  return;
27901
+ await this.ask();
27902
+ }
27903
+ async ask() {
27298
27904
  const answer = await this.d.latest();
27299
27905
  this.registryName = answer.registry;
27300
27906
  if (answer.version === null) {
27301
27907
  this.unreachable = answer.registry;
27302
- return;
27908
+ return { registry: answer.registry, latest: null };
27303
27909
  }
27304
27910
  this.unreachable = null;
27305
27911
  this.latest = newer(answer.version, this.d.running) ? answer.version : null;
27306
27912
  this.announce();
27307
27913
  await this.auto();
27914
+ return { registry: answer.registry, latest: answer.version };
27308
27915
  }
27309
27916
  async updateNow() {
27310
27917
  this.refuseManaged();
@@ -27321,14 +27928,17 @@ class UpdateService {
27321
27928
  async checkNow() {
27322
27929
  this.refuseManaged();
27323
27930
  if (this.d.method === "none") {
27324
- throw new UserError(`Toyon ${this.d.running} runs from a checkout, which does not update itself`);
27931
+ const answer = await this.d.latest();
27932
+ return { registry: answer.registry, latest: answer.version };
27325
27933
  }
27326
- await this.check();
27327
- if (this.unreachable)
27328
- throw new UserError(`Could not reach ${this.unreachable}`);
27329
- if (this.target() === null) {
27330
- throw new UserError(`Toyon ${this.d.running} is the newest version ${registryHost(this.registryName)} lists`);
27934
+ const answer = await this.ask();
27935
+ const target = this.target();
27936
+ if (target !== null && !this.wanted && !(this.needsInstall() && this.d.command(target) === null)) {
27937
+ this.failed = null;
27938
+ this.wanted = true;
27939
+ fireAndForget("update", this.proceed(), "update");
27331
27940
  }
27941
+ return answer;
27332
27942
  }
27333
27943
  install() {
27334
27944
  return this.d.method;
@@ -27563,8 +28173,8 @@ class BackendShare {
27563
28173
  // ../daemon/src/worktrees/cache.ts
27564
28174
  import { createHash as createHash3 } from "crypto";
27565
28175
  import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
27566
- import { mkdir as mkdir4, readdir as readdir2, rename, rm as rm3, stat as stat3, utimes, writeFile as writeFile2 } from "fs/promises";
27567
- 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";
27568
28178
 
27569
28179
  // ../daemon/src/core/clone.ts
27570
28180
  import { mkdir as mkdir3, rm as rm2 } from "fs/promises";
@@ -27595,7 +28205,7 @@ var STALE_MS = 30 * 24 * 3600000;
27595
28205
  var TMP = ".tmp-";
27596
28206
  var MANIFEST = "manifest.json";
27597
28207
  function defaultTools(dir) {
27598
- const has = (...files) => files.some((f) => existsSync25(join22(dir, f)));
28208
+ const has = (...files) => files.some((f) => existsSync25(join23(dir, f)));
27599
28209
  const out = [];
27600
28210
  if (has("bun.lock", "bun.lockb"))
27601
28211
  out.push("bun --version");
@@ -27608,7 +28218,7 @@ function defaultTools(dir) {
27608
28218
  return out;
27609
28219
  }
27610
28220
  function withSidecars(root, rel) {
27611
- 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)));
27612
28222
  }
27613
28223
  var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
27614
28224
  async function listEntries(familyDir) {
@@ -27623,8 +28233,8 @@ async function listEntries(familyDir) {
27623
28233
  const m = /^([0-9a-f]{12})-([0-9a-z]+)$/.exec(name);
27624
28234
  if (!m?.[1] || !m[2])
27625
28235
  continue;
27626
- const dir = join22(familyDir, name);
27627
- if (!existsSync25(join22(dir, MANIFEST)))
28236
+ const dir = join23(familyDir, name);
28237
+ if (!existsSync25(join23(dir, MANIFEST)))
27628
28238
  continue;
27629
28239
  out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
27630
28240
  }
@@ -27654,7 +28264,7 @@ class ArtifactCache {
27654
28264
  try {
27655
28265
  const family = await this.family(wt, repo, policy);
27656
28266
  const base = await this.baseCommit(wt, repo);
27657
- 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));
27658
28268
  const entry = entries.find((e) => e.base === base.slice(0, 12)) ?? entries[0];
27659
28269
  if (!entry) {
27660
28270
  say(`cache: nothing kept for these lockfiles and tools on ${family.platform}`);
@@ -27662,14 +28272,14 @@ class ArtifactCache {
27662
28272
  }
27663
28273
  const restored = [];
27664
28274
  for (const rel of policy.paths) {
27665
- if (existsSync25(join22(wt.path, rel)))
28275
+ if (existsSync25(join23(wt.path, rel)))
27666
28276
  continue;
27667
28277
  const names = withSidecars(entry.dir, rel);
27668
28278
  if (names.length === 0)
27669
28279
  continue;
27670
28280
  let whole = true;
27671
28281
  for (const name of names) {
27672
- 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));
27673
28283
  if (r.ok)
27674
28284
  continue;
27675
28285
  say(`cache: could not restore ${name}: ${r.error}`);
@@ -27703,12 +28313,12 @@ class ArtifactCache {
27703
28313
  let tmp;
27704
28314
  let slot;
27705
28315
  try {
27706
- const present = policy.paths.filter((rel) => existsSync25(join22(wt.path, rel)));
28316
+ const present = policy.paths.filter((rel) => existsSync25(join23(wt.path, rel)));
27707
28317
  if (present.length === 0)
27708
28318
  return;
27709
28319
  const family = await this.family(wt, repo, policy);
27710
28320
  const base = await this.baseCommit(wt, repo);
27711
- const familyDir = join22(this.d.paths.cacheDir, repo.id, family.key);
28321
+ const familyDir = join23(this.d.paths.cacheDir, repo.id, family.key);
27712
28322
  const key = `${familyDir}/${base.slice(0, 12)}`;
27713
28323
  const kept = (await listEntries(familyDir)).find((e) => e.base === base.slice(0, 12));
27714
28324
  if (kept || this.making.has(key)) {
@@ -27719,21 +28329,21 @@ class ArtifactCache {
27719
28329
  this.making.add(key);
27720
28330
  const at = Date.now();
27721
28331
  const name = entryName(base, at);
27722
- tmp = join22(familyDir, `${TMP}${name}`);
28332
+ tmp = join23(familyDir, `${TMP}${name}`);
27723
28333
  await mkdir4(tmp, { recursive: true });
27724
28334
  let how = "clonefile";
27725
28335
  for (const rel of present) {
27726
28336
  for (const n of withSidecars(wt.path, rel)) {
27727
- 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));
27728
28338
  if (!r.ok)
27729
28339
  throw new Error(`could not keep ${n}: ${r.error}`);
27730
28340
  how = r.how;
27731
28341
  }
27732
28342
  }
27733
28343
  const manifest = { at, base, platform: family.platform, tools: family.tools, paths: present };
27734
- await writeFile2(join22(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
28344
+ await writeFile2(join23(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
27735
28345
  `);
27736
- await rename(tmp, join22(familyDir, name));
28346
+ await rename(tmp, join23(familyDir, name));
27737
28347
  tmp = undefined;
27738
28348
  log.info(worktreeId, `cache: kept ${list(present)} for ${base.slice(0, 7)} via ${how} in ${Date.now() - started}ms`);
27739
28349
  await this.prune(familyDir);
@@ -27761,7 +28371,7 @@ class ArtifactCache {
27761
28371
  return;
27762
28372
  }
27763
28373
  for (const repoId of repoIds) {
27764
- const repoDir = join22(root, repoId);
28374
+ const repoDir = join23(root, repoId);
27765
28375
  if (!this.d.state.repo(repoId)) {
27766
28376
  await rm3(repoDir, { recursive: true, force: true });
27767
28377
  continue;
@@ -27773,7 +28383,7 @@ class ArtifactCache {
27773
28383
  continue;
27774
28384
  }
27775
28385
  for (const family of families) {
27776
- const familyDir = join22(repoDir, family);
28386
+ const familyDir = join23(repoDir, family);
27777
28387
  let names;
27778
28388
  try {
27779
28389
  names = await readdir2(familyDir);
@@ -27782,11 +28392,11 @@ class ArtifactCache {
27782
28392
  }
27783
28393
  for (const n of names)
27784
28394
  if (n.startsWith(TMP))
27785
- await rm3(join22(familyDir, n), { recursive: true, force: true });
28395
+ await rm3(join23(familyDir, n), { recursive: true, force: true });
27786
28396
  const entries = await listEntries(familyDir);
27787
28397
  let newest = 0;
27788
28398
  for (const e of entries) {
27789
- const s = await stat3(e.dir).catch(() => null);
28399
+ const s = await stat4(e.dir).catch(() => null);
27790
28400
  if (s)
27791
28401
  newest = Math.max(newest, s.mtimeMs);
27792
28402
  }
@@ -27802,7 +28412,7 @@ class ArtifactCache {
27802
28412
  for (const f of policy.key) {
27803
28413
  h.update(`\x00${f}\x00`);
27804
28414
  try {
27805
- h.update(readFileSync16(join22(wt.path, f)));
28415
+ h.update(readFileSync16(join23(wt.path, f)));
27806
28416
  } catch {
27807
28417
  h.update("(absent)");
27808
28418
  }
@@ -27983,9 +28593,14 @@ class LandingService {
27983
28593
  d.hub.on("agentStatus", (id, status) => {
27984
28594
  this.note(id, status);
27985
28595
  if (status === "working")
27986
- this.clear(id);
28596
+ this.turnStarts(id);
27987
28597
  });
27988
28598
  }
28599
+ turnStarts(worktreeId) {
28600
+ if (!this.d.state.worktree(worktreeId)?.landing?.ready)
28601
+ return this.clear(worktreeId);
28602
+ this.judging.delete(worktreeId);
28603
+ }
27989
28604
  note(worktreeId, status) {
27990
28605
  if (status === "working" || status === "waiting")
27991
28606
  this.busy.add(worktreeId);
@@ -28011,14 +28626,69 @@ class LandingService {
28011
28626
  const wt = this.d.state.worktree(worktreeId);
28012
28627
  if (!wt || !canLand(wt))
28013
28628
  return;
28014
- if (turn.end !== "done")
28015
- return this.clear(worktreeId);
28629
+ const held = await this.standing(wt);
28630
+ if (turn.end !== "done") {
28631
+ if (!held)
28632
+ this.clear(worktreeId);
28633
+ return;
28634
+ }
28016
28635
  if (!await this.hasWork(wt)) {
28017
28636
  this.clear(worktreeId);
28018
28637
  return this.recapAnswer(wt, turn);
28019
28638
  }
28639
+ if (held)
28640
+ return this.reword(wt, turn, held);
28020
28641
  await this.run(wt, { at: turn.at, ask: true, quiet: false });
28021
28642
  }
28643
+ async standing(wt) {
28644
+ const l = wt.landing;
28645
+ if (!l?.ready)
28646
+ return;
28647
+ return l.fingerprint === await treeFingerprint(wt.path) ? l : undefined;
28648
+ }
28649
+ async reword(wt, turn, held) {
28650
+ if (!this.d.judge)
28651
+ return;
28652
+ const worktreeId = wt.id;
28653
+ const repo = this.d.state.requireRepo(wt.repoId);
28654
+ this.judging.set(worktreeId, turn.at);
28655
+ const started = Date.now();
28656
+ const entries = this.d.transcript(worktreeId);
28657
+ const prompt = landPrompt({
28658
+ title: wt.title,
28659
+ firstAsk: firstAskOf(entries),
28660
+ turns: turnsSince(entries, 0),
28661
+ diffStat: await diffSummary(wt.path, baseOf(repo)),
28662
+ recentSubjects: await recentSubjects(wt.path, baseOf(repo))
28663
+ });
28664
+ let verdict = null;
28665
+ try {
28666
+ verdict = await this.d.judge(wt, prompt);
28667
+ } catch (e) {
28668
+ log.warn(worktreeId, "landing verdict kept its words: the question went unanswered", e);
28669
+ }
28670
+ if (this.stopping || this.judging.get(worktreeId) !== turn.at)
28671
+ return;
28672
+ this.judging.delete(worktreeId);
28673
+ const row = this.d.state.worktree(worktreeId);
28674
+ const now = row?.landing;
28675
+ if (!verdict || !row || !now?.ready || now.fingerprint !== held.fingerprint)
28676
+ return;
28677
+ const { why: _was, stale: _moved, ...rest } = now;
28678
+ const caveat = await this.sharedMigration(wt, repo);
28679
+ const doubt = [!verdict.ready && verdict.why ? verdict.why : "", caveat ?? ""].filter(Boolean).join(". ");
28680
+ const landing = { ...rest, at: turn.at, ...doubt ? { why: doubt } : {} };
28681
+ if (!landing.subject && verdict.subject) {
28682
+ landing.subject = verdict.subject;
28683
+ if (verdict.body)
28684
+ landing.body = verdict.body;
28685
+ delete landing.unanswered;
28686
+ }
28687
+ if (row.lastTurn?.at === turn.at && verdict.recap)
28688
+ row.lastTurn.recap = { at: Date.now(), text: verdict.recap };
28689
+ log.info(worktreeId, `landing: tree unchanged, check not run again${doubt ? ", doubted" : ""}, ${Date.now() - started}ms`);
28690
+ this.d.worktrees.setLanding(worktreeId, landing);
28691
+ }
28022
28692
  async recapAnswer(wt, turn) {
28023
28693
  if (!this.d.recap)
28024
28694
  return;
@@ -28572,12 +29242,12 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
28572
29242
  }
28573
29243
 
28574
29244
  // ../daemon/src/worktrees/service.ts
28575
- 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";
28576
29246
  import { rm as rm4 } from "fs/promises";
28577
- import { dirname as dirname11, join as join25 } from "path";
29247
+ import { dirname as dirname11, join as join27 } from "path";
28578
29248
 
28579
29249
  // ../daemon/src/git/archive.ts
28580
- import { rmSync as rmSync3 } from "fs";
29250
+ import { rmSync as rmSync4 } from "fs";
28581
29251
  var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
28582
29252
  var SNAPSHOT_SUBJECT = "toyon: uncommitted work";
28583
29253
  var SNAPSHOT_AUTHOR = "toyon";
@@ -28620,7 +29290,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
28620
29290
  ], wtPath);
28621
29291
  return commit.ok ? commit.out : null;
28622
29292
  } finally {
28623
- rmSync3(indexFile, { force: true });
29293
+ rmSync4(indexFile, { force: true });
28624
29294
  }
28625
29295
  }
28626
29296
  async function keptAt(repoPath, ref) {
@@ -28742,9 +29412,12 @@ function overwritten(text) {
28742
29412
  }
28743
29413
  return files;
28744
29414
  }
29415
+ var listed = (files) => files.length > 3 ? `${files.slice(0, 3).join(", ")} and ${files.length - 3} more` : files.join(", ");
28745
29416
  function inTheWay(defaultBr, what, files) {
28746
- const shown = files.length > 3 ? `${files.slice(0, 3).join(", ")} and ${files.length - 3} more` : files.join(", ");
28747
- return `${defaultBr} has uncommitted files the ${what} would overwrite (${shown}): commit or stash them there first`;
29417
+ return `${defaultBr} has uncommitted files the ${what} would overwrite (${listed(files)}): commit or stash them there first`;
29418
+ }
29419
+ function untrackedInTheWay(defaultBr, files) {
29420
+ return `${defaultBr} has files that are untracked here (${listed(files)}): move or delete them, then sync`;
28748
29421
  }
28749
29422
  async function requireMainReady(repoPath, defaultBr, clean) {
28750
29423
  const current = await git(repoPath, "branch", "--show-current");
@@ -28756,18 +29429,75 @@ async function requireMainReady(repoPath, defaultBr, clean) {
28756
29429
  }
28757
29430
  return null;
28758
29431
  }
28759
- async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED) {
28760
- const cErr = await requireClean(worktreePath);
28761
- if (cErr)
28762
- return cErr;
29432
+ var CARRIED = "refs/worktree/toyon/carried";
29433
+ async function setAside(worktreePath) {
29434
+ const head = await git(worktreePath, "rev-parse", "HEAD");
29435
+ const made = await git(worktreePath, "stash", "create");
29436
+ if (!head.ok || !made.ok) {
29437
+ return { ok: false, message: `could not set the uncommitted changes aside: ${made.err.slice(0, 200)}` };
29438
+ }
29439
+ if (!made.out)
29440
+ return null;
29441
+ const kept = await git(worktreePath, "update-ref", CARRIED, made.out);
29442
+ const cleared = kept.ok ? await git(worktreePath, "reset", "--hard", "-q") : kept;
29443
+ if (!cleared.ok) {
29444
+ return { ok: false, message: `could not set the uncommitted changes aside: ${cleared.err.slice(0, 200)}` };
29445
+ }
29446
+ return { sha: made.out, head: head.out };
29447
+ }
29448
+ async function putBack(worktreePath, defaultBr, aside, taken) {
29449
+ let clash = null;
29450
+ if (taken.ok) {
29451
+ const applied = await git(worktreePath, "stash", "apply", aside.sha);
29452
+ if (applied.ok) {
29453
+ await git(worktreePath, "update-ref", "-d", CARRIED);
29454
+ return taken;
29455
+ }
29456
+ const unmerged = await git(worktreePath, "diff", "--name-only", "--diff-filter=U");
29457
+ clash = unmerged.out.split(`
29458
+ `).filter(Boolean);
29459
+ await git(worktreePath, "reset", "--hard", "-q", aside.head);
29460
+ }
29461
+ const back = await git(worktreePath, "stash", "apply", "--index", aside.sha);
29462
+ if (!back.ok) {
29463
+ return {
29464
+ ok: false,
29465
+ message: `sync stopped with the uncommitted changes set aside: git stash apply ${CARRIED} in the worktree brings them back`
29466
+ };
29467
+ }
29468
+ await git(worktreePath, "update-ref", "-d", CARRIED);
29469
+ if (!clash)
29470
+ return taken;
29471
+ const named = clash.length ? ` (${listed(clash)})` : "";
29472
+ return {
29473
+ ok: false,
29474
+ message: `the uncommitted changes here conflict with ${defaultBr}${named}: commit them first, then sync`
29475
+ };
29476
+ }
29477
+ async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
29478
+ if (!carry) {
29479
+ const cErr = await requireClean(worktreePath);
29480
+ if (cErr)
29481
+ return cErr;
29482
+ }
28763
29483
  const { behind } = await aheadBehind(worktreePath, defaultBr);
28764
29484
  if (behind === 0)
28765
29485
  return { ok: true, message: `already up to date with ${defaultBr}` };
29486
+ const aside = carry ? await setAside(worktreePath) : null;
29487
+ if (aside && "message" in aside)
29488
+ return aside;
29489
+ const taken = await bringIn(worktreePath, defaultBr, own, behind, w);
29490
+ return aside ? putBack(worktreePath, defaultBr, aside, taken) : taken;
29491
+ }
29492
+ async function bringIn(worktreePath, defaultBr, own, behind, w) {
28766
29493
  if (own) {
28767
29494
  w.step(`rebasing onto ${defaultBr}`);
28768
29495
  const r = await w.git(worktreePath, ["rebase", defaultBr]);
28769
29496
  if (!r.ok) {
28770
29497
  await git(worktreePath, "rebase", "--abort");
29498
+ const files = overwritten(r.text);
29499
+ if (files.length)
29500
+ return { ok: false, message: untrackedInTheWay(defaultBr, files) };
28771
29501
  const ask = `rebase onto ${defaultBr} conflicts: ask the agent to bring ${defaultBr} in and resolve them`;
28772
29502
  return {
28773
29503
  ok: false,
@@ -28780,6 +29510,9 @@ async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED) {
28780
29510
  w.step(`merging ${defaultBr} in`);
28781
29511
  const m = await w.git(worktreePath, ["merge", "--no-edit", defaultBr]);
28782
29512
  if (!m.ok) {
29513
+ const files = overwritten(m.text);
29514
+ if (files.length)
29515
+ return { ok: false, message: untrackedInTheWay(defaultBr, files) };
28783
29516
  return mergeFailure(worktreePath, m, `sync conflicts with ${defaultBr}: ask the agent to merge ${defaultBr} and resolve them`);
28784
29517
  }
28785
29518
  return { ok: true, message: `synced ${behind} commit(s) from ${defaultBr}` };
@@ -29032,18 +29765,142 @@ function compareUrl(remoteUrl, base, branch) {
29032
29765
  return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
29033
29766
  }
29034
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
+
29035
29892
  // ../daemon/src/worktrees/archive.ts
29036
- import { existsSync as existsSync26, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
29037
- 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";
29038
29895
  class WorktreeArchive {
29039
29896
  dir;
29040
29897
  records = new Map;
29041
29898
  constructor(dir) {
29042
29899
  this.dir = dir;
29043
- mkdirSync7(dir, { recursive: true });
29044
- for (const id of readdirSync7(dir)) {
29900
+ mkdirSync8(dir, { recursive: true });
29901
+ for (const id of readdirSync8(dir)) {
29045
29902
  const file = this.filesOf(id).record;
29046
- if (!existsSync26(file))
29903
+ if (!existsSync27(file))
29047
29904
  continue;
29048
29905
  try {
29049
29906
  this.records.set(id, JSON.parse(readFileSync17(file, "utf8")));
@@ -29057,7 +29914,7 @@ class WorktreeArchive {
29057
29914
  }
29058
29915
  put(rec, from) {
29059
29916
  const at = this.filesOf(rec.worktree.id);
29060
- mkdirSync7(at.dir, { recursive: true });
29917
+ mkdirSync8(at.dir, { recursive: true });
29061
29918
  move(from.transcript, at.transcript);
29062
29919
  move(from.attachments, at.attachments);
29063
29920
  move(from.plans, at.plans);
@@ -29077,7 +29934,7 @@ class WorktreeArchive {
29077
29934
  delete(id) {
29078
29935
  if (!this.records.has(id))
29079
29936
  return;
29080
- rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
29937
+ rmSync6(this.filesOf(id).dir, { recursive: true, force: true });
29081
29938
  this.records.delete(id);
29082
29939
  }
29083
29940
  list(repo) {
@@ -29092,8 +29949,8 @@ class WorktreeArchive {
29092
29949
  planFile(id, rel) {
29093
29950
  if (!this.records.has(id) || !isPlanPath(rel))
29094
29951
  return null;
29095
- const file = join23(this.filesOf(id).plans, basename8(rel));
29096
- if (!existsSync26(file))
29952
+ const file = join25(this.filesOf(id).plans, basename8(rel));
29953
+ if (!existsSync27(file))
29097
29954
  return null;
29098
29955
  try {
29099
29956
  return readFileSync17(file, "utf8");
@@ -29103,13 +29960,13 @@ class WorktreeArchive {
29103
29960
  }
29104
29961
  }
29105
29962
  filesOf(id) {
29106
- const dir = join23(this.dir, id);
29963
+ const dir = join25(this.dir, id);
29107
29964
  return {
29108
29965
  dir,
29109
- record: join23(dir, "record.json"),
29110
- transcript: join23(dir, "transcript.jsonl"),
29111
- attachments: join23(dir, "attachments"),
29112
- 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")
29113
29970
  };
29114
29971
  }
29115
29972
  }
@@ -29134,7 +29991,7 @@ function summarize(r, repoId, transcript) {
29134
29991
  };
29135
29992
  }
29136
29993
  function lastUsage(transcript) {
29137
- if (!existsSync26(transcript))
29994
+ if (!existsSync27(transcript))
29138
29995
  return null;
29139
29996
  const lines = readFileSync17(transcript, "utf8").split(`
29140
29997
  `);
@@ -29150,7 +30007,7 @@ function lastUsage(transcript) {
29150
30007
  return null;
29151
30008
  }
29152
30009
  function firstPrompt(transcript) {
29153
- if (!existsSync26(transcript))
30010
+ if (!existsSync27(transcript))
29154
30011
  return;
29155
30012
  for (const line of readFileSync17(transcript, "utf8").split(`
29156
30013
  `)) {
@@ -29165,10 +30022,10 @@ function firstPrompt(transcript) {
29165
30022
  return;
29166
30023
  }
29167
30024
  function move(from, to) {
29168
- if (!existsSync26(from))
30025
+ if (!existsSync27(from))
29169
30026
  return;
29170
- mkdirSync7(dirname10(to), { recursive: true });
29171
- rmSync4(to, { recursive: true, force: true });
30027
+ mkdirSync8(dirname10(to), { recursive: true });
30028
+ rmSync6(to, { recursive: true, force: true });
29172
30029
  renameSync4(from, to);
29173
30030
  }
29174
30031
 
@@ -29230,8 +30087,8 @@ class ArchivedGit {
29230
30087
  }
29231
30088
 
29232
30089
  // ../daemon/src/worktrees/spare.ts
29233
- import { existsSync as existsSync27 } from "fs";
29234
- 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";
29235
30092
  class SparePool {
29236
30093
  d;
29237
30094
  inflight = new Map;
@@ -29250,7 +30107,7 @@ class SparePool {
29250
30107
  if (!text.trim())
29251
30108
  return;
29252
30109
  const wt = this.d.state.worktree(boxId);
29253
- if (wt?.kind !== "spare" || !existsSync27(wt.path))
30110
+ if (wt?.kind !== "spare" || !existsSync28(wt.path))
29254
30111
  return;
29255
30112
  const agent = this.d.runtime.ensureAgent(wt).agent;
29256
30113
  if (agent.runningAgent === null)
@@ -29295,11 +30152,11 @@ class SparePool {
29295
30152
  return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
29296
30153
  }
29297
30154
  async record(repoId, repo) {
29298
- const { id, dir } = freeSlot(join24(this.d.paths.worktreesDir, repo.name));
30155
+ const { id, dir } = freeSlot(join26(this.d.paths.worktreesDir, repo.name));
29299
30156
  const wt = {
29300
30157
  id,
29301
30158
  repoId,
29302
- path: join24(this.d.paths.worktreesDir, repo.name, dir),
30159
+ path: join26(this.d.paths.worktreesDir, repo.name, dir),
29303
30160
  branch: repo.defaultBranch,
29304
30161
  kind: "spare",
29305
30162
  phase: "reserved",
@@ -29314,7 +30171,7 @@ class SparePool {
29314
30171
  const repoId = repo.id;
29315
30172
  let cut = () => {};
29316
30173
  const added = (async () => {
29317
- if (existsSync27(wt.path))
30174
+ if (existsSync28(wt.path))
29318
30175
  return;
29319
30176
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
29320
30177
  })();
@@ -29362,7 +30219,7 @@ class SparePool {
29362
30219
  }
29363
30220
  log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
29364
30221
  this.d.carryDraft?.(wt.id, wt.repoId);
29365
- if (existsSync27(wt.path)) {
30222
+ if (existsSync28(wt.path)) {
29366
30223
  await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
29367
30224
  return;
29368
30225
  }
@@ -29419,7 +30276,7 @@ class SparePool {
29419
30276
  const running = this.inflight.get(repoId);
29420
30277
  await running?.added.catch(() => {});
29421
30278
  running?.cut();
29422
- if (wt.kind !== "spare" || !existsSync27(wt.path))
30279
+ if (wt.kind !== "spare" || !existsSync28(wt.path))
29423
30280
  return null;
29424
30281
  }
29425
30282
  await this.refreshing.get(repoId);
@@ -29465,7 +30322,7 @@ class Trunk {
29465
30322
  const last = this.lastFetch.get(path) ?? 0;
29466
30323
  if (Date.now() - last > FETCH_EVERY_MS) {
29467
30324
  this.lastFetch.set(path, Date.now());
29468
- 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) => {
29469
30326
  const main = this.d.state.worktree(mainId);
29470
30327
  if (main)
29471
30328
  this.noteFetch(main.repoId, r);
@@ -29490,7 +30347,7 @@ class Trunk {
29490
30347
  if (Date.now() - last < OPEN_FETCH_MIN_MS)
29491
30348
  return;
29492
30349
  this.lastFetch.set(repo.path, Date.now());
29493
- 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));
29494
30351
  this.noteFetch(repo.id, f);
29495
30352
  if (!f.ok) {
29496
30353
  log.warn("fetch", `could not fetch ${repo.path}: ${f.err.slice(0, 200)}`);
@@ -29514,7 +30371,7 @@ class Trunk {
29514
30371
  return { ok: false, message: "no main checkout to pull" };
29515
30372
  this.lastFetch.set(repo.path, Date.now());
29516
30373
  w.step(`pulling ${repo.defaultBranch} from origin`);
29517
- const f = await w.git(repo.path, ["fetch", "--quiet"]);
30374
+ const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
29518
30375
  this.noteFetch(repo.id, f);
29519
30376
  if (!f.ok)
29520
30377
  return { ok: false, message: refused("fetch failed", f) };
@@ -29716,10 +30573,9 @@ class WorktreeService {
29716
30573
  await this.discardWorktree(id);
29717
30574
  },
29718
30575
  carryDraft: (fromId, repoId) => {
29719
- const text = this.d.drafts?.text(fromId);
29720
30576
  const main = this.mainOf(repoId);
29721
- if (text && main)
29722
- this.d.drafts?.set(main.id, text);
30577
+ if (main)
30578
+ this.d.drafts?.move(fromId, main.id, true);
29723
30579
  },
29724
30580
  ready: (repoId) => {
29725
30581
  const main = this.mainOf(repoId);
@@ -29728,15 +30584,15 @@ class WorktreeService {
29728
30584
  if (this.d.runtime.get(main.id)?.procs) {
29729
30585
  fireAndForget(main.id, this.d.runtime.stopPage(main.id), "main's page stops for the spare");
29730
30586
  }
29731
- const text = this.d.drafts?.text(main.id);
29732
30587
  const spare = this.leadSpareOf(repoId);
29733
- if (text && spare && !this.d.drafts?.text(spare)) {
29734
- this.d.drafts?.set(spare, text);
29735
- this.d.drafts?.drop(main.id);
29736
- }
30588
+ if (spare)
30589
+ this.d.drafts?.move(main.id, spare);
29737
30590
  }
29738
30591
  });
29739
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
+ }
29740
30596
  if (event.type !== "usage")
29741
30597
  return;
29742
30598
  const { used, size, cost } = event;
@@ -29801,14 +30657,14 @@ class WorktreeService {
29801
30657
  rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
29802
30658
  this.scheduleNaming(claimed, task, variant);
29803
30659
  if (carried.unmoved)
29804
- throw new UserError(carried.unmoved);
30660
+ throw new UserError(carried.unmoved, { delivered: true });
29805
30661
  return claimed;
29806
30662
  }
29807
30663
  if (!await isGitRepo(repo.path)) {
29808
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.`);
29809
30665
  }
29810
- const { id, dir } = freeSlot(join25(this.d.paths.worktreesDir, repo.name));
29811
- 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);
29812
30668
  const branch = `toyon/${dir}`;
29813
30669
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
29814
30670
  const wt = {
@@ -29835,7 +30691,7 @@ class WorktreeService {
29835
30691
  this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
29836
30692
  this.scheduleNaming(wt, task, variant);
29837
30693
  if (carried.unmoved)
29838
- throw new UserError(carried.unmoved);
30694
+ throw new UserError(carried.unmoved, { delivered: true });
29839
30695
  return wt;
29840
30696
  }
29841
30697
  async carryMain(repo, wt, move) {
@@ -29874,7 +30730,7 @@ class WorktreeService {
29874
30730
  };
29875
30731
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
29876
30732
  const restored = (carried.ok ? carried.out.split(`
29877
- `).filter(Boolean) : []).filter((p) => !existsSync28(join25(wt.path, p)));
30733
+ `).filter(Boolean) : []).filter((p) => !existsSync29(join27(wt.path, p)));
29878
30734
  const applied = await git(wt.path, "stash", "apply", sha);
29879
30735
  if (applied.ok) {
29880
30736
  const ref = await entry();
@@ -29910,9 +30766,9 @@ class WorktreeService {
29910
30766
  const branch = kind === "pr" ? `pr/${number}` : ref;
29911
30767
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
29912
30768
  let slug = branchSlug(title) || "branch";
29913
- if (existsSync28(join25(this.d.paths.worktreesDir, repo.name, slug)))
30769
+ if (existsSync29(join27(this.d.paths.worktreesDir, repo.name, slug)))
29914
30770
  slug = `${slug}-${shortId().slice(0, 4)}`;
29915
- const wtPath = join25(this.d.paths.worktreesDir, repo.name, slug);
30771
+ const wtPath = join27(this.d.paths.worktreesDir, repo.name, slug);
29916
30772
  await withRepoLock(repo.path, async () => {
29917
30773
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
29918
30774
  if (out)
@@ -30132,7 +30988,7 @@ class WorktreeService {
30132
30988
  try {
30133
30989
  await this.removeDirectory(repo, wt);
30134
30990
  } catch (e) {
30135
- rmSync5(this.plansScratch(wt.id), { recursive: true, force: true });
30991
+ rmSync7(this.plansScratch(wt.id), { recursive: true, force: true });
30136
30992
  throw e;
30137
30993
  }
30138
30994
  if (hasOwnBranch(wt)) {
@@ -30146,6 +31002,7 @@ class WorktreeService {
30146
31002
  this.d.state.removeWorktree(worktreeId);
30147
31003
  releasePort(wt.proxyPort);
30148
31004
  let archived = null;
31005
+ await dropTurnRefs(repo.path, worktreeId);
30149
31006
  if (archive)
30150
31007
  archived = this.archiveChat(wt, repo, kept, sessionId, reason);
30151
31008
  else {
@@ -30158,7 +31015,7 @@ class WorktreeService {
30158
31015
  }
30159
31016
  async teardown(wt, repo) {
30160
31017
  const cmds = repo.config.teardown ?? [];
30161
- if (cmds.length === 0 || !existsSync28(wt.path))
31018
+ if (cmds.length === 0 || !existsSync29(wt.path))
30162
31019
  return;
30163
31020
  for (const cmd of cmds) {
30164
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") });
@@ -30167,7 +31024,7 @@ class WorktreeService {
30167
31024
  }
30168
31025
  }
30169
31026
  async keep(repo, wt) {
30170
- const index = join25(this.d.paths.archiveDir, `${wt.id}.index`);
31027
+ const index = join27(this.d.paths.archiveDir, `${wt.id}.index`);
30171
31028
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
30172
31029
  if (!kept)
30173
31030
  log.warn(wt.id, "could not keep its git state");
@@ -30204,11 +31061,11 @@ class WorktreeService {
30204
31061
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
30205
31062
  }
30206
31063
  plansScratch(worktreeId) {
30207
- return join25(this.d.paths.archiveDir, `${worktreeId}.plans`);
31064
+ return join27(this.d.paths.archiveDir, `${worktreeId}.plans`);
30208
31065
  }
30209
31066
  keepPlans(wt) {
30210
- const from = join25(wt.path, PLANS_DIR);
30211
- if (!existsSync28(from))
31067
+ const from = join27(wt.path, PLANS_DIR);
31068
+ if (!existsSync29(from))
30212
31069
  return;
30213
31070
  try {
30214
31071
  cpSync(from, this.plansScratch(wt.id), { recursive: true });
@@ -30249,8 +31106,8 @@ class WorktreeService {
30249
31106
  deleteChat(worktreeId) {
30250
31107
  const files = this.chatFiles(worktreeId);
30251
31108
  try {
30252
- rmSync5(files.transcript, { force: true });
30253
- rmSync5(files.attachments, { recursive: true, force: true });
31109
+ rmSync7(files.transcript, { force: true });
31110
+ rmSync7(files.attachments, { recursive: true, force: true });
30254
31111
  } catch (e) {
30255
31112
  log.warn(worktreeId, "could not delete transcript or attachments", e);
30256
31113
  }
@@ -30296,7 +31153,7 @@ class WorktreeService {
30296
31153
  }
30297
31154
  archivedAttachment(archiveId, file) {
30298
31155
  const files = this.archive.chatFiles(archiveId);
30299
- return files && isAttachmentFile(file) ? join25(files.attachments, file) : null;
31156
+ return files && isAttachmentFile(file) ? join27(files.attachments, file) : null;
30300
31157
  }
30301
31158
  archivedGit(archiveId) {
30302
31159
  const rec = this.archive.get(archiveId);
@@ -30339,7 +31196,7 @@ class WorktreeService {
30339
31196
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
30340
31197
  if (rec.auto)
30341
31198
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
30342
- 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;
30343
31200
  const branch = await withRepoLock(repo.path, async () => {
30344
31201
  if (!await commitOf(repo.path, archiveRef(old.id))) {
30345
31202
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -30373,10 +31230,11 @@ class WorktreeService {
30373
31230
  branch,
30374
31231
  proxyPort: await allocateProxyPort(),
30375
31232
  promptedAt: Date.now(),
31233
+ parked: true,
30376
31234
  ...createdBy ? { createdBy } : {}
30377
31235
  };
30378
31236
  try {
30379
- 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) });
30380
31238
  } catch (e) {
30381
31239
  log.warn(old.id, "could not move its chat back out of the archive", e);
30382
31240
  }
@@ -30405,12 +31263,12 @@ class WorktreeService {
30405
31263
  return;
30406
31264
  const repo = this.repoOf(rec);
30407
31265
  const repoPath = repo?.path ?? rec.repoPath;
30408
- if (rec.kept && existsSync28(repoPath)) {
31266
+ if (rec.kept && existsSync29(repoPath)) {
30409
31267
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
30410
31268
  if (!r.ok)
30411
31269
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
30412
31270
  }
30413
- if (rec.worktree.lands?.length && existsSync28(repoPath))
31271
+ if (rec.worktree.lands?.length && existsSync29(repoPath))
30414
31272
  await dropLandRefs(repoPath, archiveId);
30415
31273
  this.archive.delete(archiveId);
30416
31274
  this.d.drafts?.drop(archiveId);
@@ -30501,6 +31359,15 @@ class WorktreeService {
30501
31359
  this.d.hub.emit("worktreesChanged");
30502
31360
  return { target, grafted: sources.map((w) => w.title) };
30503
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
+ }
30504
31371
  async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
30505
31372
  this.d.runtime.markSetup(wt.id, true);
30506
31373
  try {
@@ -30510,9 +31377,9 @@ class WorktreeService {
30510
31377
  }
30511
31378
  }
30512
31379
  async mirrorBaseIgnore(repo) {
30513
- const src = join25(repo.path, ".gitignore");
30514
- const tracked = existsSync28(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
30515
- 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;
30516
31383
  const lines = mirror ? readFileSync18(src, "utf8").split(`
30517
31384
  `).filter((l) => l.trim() !== "") : [];
30518
31385
  await excludeBlock(repo.path, BASE_IGNORE, lines);
@@ -30525,9 +31392,9 @@ class WorktreeService {
30525
31392
  await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
30526
31393
  run.finish(0);
30527
31394
  }
30528
- const srcNm = join25(depsSource, "node_modules");
30529
- const dstNm = join25(wt.path, "node_modules");
30530
- 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)) {
30531
31398
  const r = await cloneTree(srcNm, dstNm);
30532
31399
  if (r.ok)
30533
31400
  this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
@@ -30535,10 +31402,10 @@ class WorktreeService {
30535
31402
  log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
30536
31403
  }
30537
31404
  for (const f of LOCAL_CONFIG_FILES) {
30538
- const src = join25(depsSource, f);
30539
- if (!existsSync28(src) || existsSync28(join25(wt.path, f)))
31405
+ const src = join27(depsSource, f);
31406
+ if (!existsSync29(src) || existsSync29(join27(wt.path, f)))
30540
31407
  continue;
30541
- 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);
30542
31409
  if (r.ok)
30543
31410
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
30544
31411
  else
@@ -30706,12 +31573,24 @@ ${wt.landing.body}` : s;
30706
31573
  const { wt, repo } = this.landable(worktreeId, "land");
30707
31574
  const out = await this.ship(wt.id, "land", async () => {
30708
31575
  await this.followBranch(wt);
31576
+ const moved = await this.movedMidTurn(wt);
31577
+ if (moved)
31578
+ return { result: moved };
30709
31579
  return this.landOp(wt, repo, message);
30710
31580
  });
30711
31581
  if (out.result.ok && out.archiveIds)
30712
31582
  this.noteLanded(wt, out.result.message, out.archiveIds);
30713
31583
  return out;
30714
31584
  }
31585
+ async movedMidTurn(wt) {
31586
+ const status = this.d.runtime.agentFor(wt.id)?.status;
31587
+ if (status !== "working" && status !== "waiting")
31588
+ return null;
31589
+ const l = wt.landing;
31590
+ if (!l || l.fingerprint === await treeFingerprint(wt.path))
31591
+ return null;
31592
+ return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
31593
+ }
30715
31594
  noteLanded(wt, message, archiveIds) {
30716
31595
  this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, archiveIds, ts: Date.now() });
30717
31596
  }
@@ -30934,7 +31813,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
30934
31813
  async fetchBase(wt, repo, w) {
30935
31814
  if (!baseIsRemote(repo))
30936
31815
  return { ok: true, message: `${baseOf(repo)} here is the base` };
30937
- const r = await fetchBase(wt.path, repo, w);
31816
+ const r = await withFetchLock(repo.path, () => fetchBase(wt.path, repo, w));
30938
31817
  this.trunk.noteFetch(repo.id, { ok: r.ok, err: r.message });
30939
31818
  return r;
30940
31819
  }
@@ -30983,6 +31862,10 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
30983
31862
  throw new UserError(`${r.name} is held by another tool`);
30984
31863
  const repo = this.d.state.requireRepo(r.repoId);
30985
31864
  const own = r.wt ? hasOwnBranch(r.wt) : false;
31865
+ const carry = !!r.wt;
31866
+ const status = this.d.runtime.agentFor(worktreeId)?.status;
31867
+ if (carry && (status === "working" || status === "waiting") && (await statusFiles(r.path)).length > 0)
31868
+ throw new UserError(`${r.name}'s agent is mid-turn with uncommitted changes: sync when it is done`);
30986
31869
  const w = r.wt ? this.landWatch(r.wt) : UNWATCHED;
30987
31870
  const result = await this.ship(worktreeId, "sync-main", async () => {
30988
31871
  if (r.wt) {
@@ -30990,7 +31873,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
30990
31873
  if (!fetched.ok)
30991
31874
  return fetched;
30992
31875
  }
30993
- const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w));
31876
+ const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w, carry));
30994
31877
  if (taken.ok) {
30995
31878
  this.headMoved(worktreeId);
30996
31879
  if (r.wt)
@@ -31166,6 +32049,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31166
32049
  base: baseOf(repo)
31167
32050
  };
31168
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
+ }
31169
32057
  async gitStatus(worktreeId) {
31170
32058
  if (this.going.has(worktreeId))
31171
32059
  return null;
@@ -31186,6 +32074,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31186
32074
  const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
31187
32075
  const ahead = counts.ahead ?? 0;
31188
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;
31189
32078
  if (r.wt && !isMain && hasOwnBranch(r.wt) && files.length === 0 && ahead === 0 && head.ok && this.handChecked.get(r.wt.id) !== head.out) {
31190
32079
  this.handChecked.set(r.wt.id, head.out);
31191
32080
  const mark = await handLanding(r.path, r.wt.branch, r.base, r.wt.lands?.at(-1)?.tip);
@@ -31212,7 +32101,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
31212
32101
  if (this.going.has(worktreeId))
31213
32102
  return null;
31214
32103
  this.noteCounts(worktreeId, isMain, files.length, counts);
31215
- return { files, committed, head: head.ok ? head.out : undefined, ...counts };
32104
+ return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
31216
32105
  } catch (e) {
31217
32106
  log.warn(worktreeId, "git status failed", e);
31218
32107
  return null;
@@ -31481,7 +32370,8 @@ var accounts = new AgentAccounts({
31481
32370
  connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `auth:${spec.id}`)
31482
32371
  });
31483
32372
  accounts.onChange = () => hub.emit("agentsChanged");
31484
- var attachments2 = new AttachmentStore(paths.attachmentsDir);
32373
+ var uploads = new UploadStore(paths.uploadsDir);
32374
+ var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
31485
32375
  var runtime = new RuntimeRegistry({
31486
32376
  hub,
31487
32377
  state,
@@ -31491,13 +32381,19 @@ var runtime = new RuntimeRegistry({
31491
32381
  accounts,
31492
32382
  attachments: attachments2,
31493
32383
  bridgeScript: () => bridge.get(),
32384
+ turnStarting: (id) => worktrees.turnStarted(id),
31494
32385
  remote,
31495
32386
  grant: previewGrant(token),
31496
32387
  viewed: (id) => idle.isViewed(id),
31497
32388
  shown: (id) => idle.isShown(id),
31498
32389
  mainLeads: (repoId) => worktrees.spare.current(repoId) === null
31499
32390
  });
31500
- 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
+ });
31501
32397
  var runs = new RunService({ state, hub });
31502
32398
  var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
31503
32399
  var cache = new ArtifactCache({ paths, state, hub });
@@ -31667,6 +32563,7 @@ var stopLagSampler = startLagSampler();
31667
32563
  await runtime.reclaimOrphans();
31668
32564
  await repos.boot();
31669
32565
  drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
32566
+ uploads.sweep(drafts.uploadIds());
31670
32567
  idle.boot();
31671
32568
  runs.boot();
31672
32569
  landing.boot();
@@ -31722,7 +32619,7 @@ async function shutdown(signal, opts = {}) {
31722
32619
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
31723
32620
  await Promise.race([Promise.all([runtime.shutdown(), exec.stopAll()]), deadline]);
31724
32621
  state.flushGroups();
31725
- rmSync6(paths.pidFile, { force: true });
32622
+ rmSync8(paths.pidFile, { force: true });
31726
32623
  if (opts.respawn) {
31727
32624
  await Bun.sleep(250);
31728
32625
  log.info("daemon", "starting the replacement");