toyon 0.1.0 → 0.2.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 (29) hide show
  1. package/README.md +3 -3
  2. package/dist/bridge.js +1 -1
  3. package/dist/cli.js +26 -2
  4. package/dist/daemon.js +1326 -295
  5. package/dist/shell/assets/{Editor-Cn-KsoFr.js → Editor-D-PlbMSY.js} +148 -148
  6. package/dist/shell/assets/{XTerm-DSSGYyFZ.js → XTerm-CM_FJEwN.js} +13 -13
  7. package/dist/shell/assets/{cssMode-BU_otbjh.js → cssMode-Bkqyr9Lr.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-C-NGmKSC.js → freemarker2-DVAK5zoJ.js} +1 -1
  9. package/dist/shell/assets/{handlebars-BWvVdY_e.js → handlebars-Bdtj7uMl.js} +1 -1
  10. package/dist/shell/assets/{html-CBO-8nU2.js → html-B1pqTvyA.js} +1 -1
  11. package/dist/shell/assets/{htmlMode-Dwz6npQ-.js → htmlMode-CBtvzewS.js} +1 -1
  12. package/dist/shell/assets/index-GBQLHG7z.css +1 -0
  13. package/dist/shell/assets/index-jy_zeFGs.js +230 -0
  14. package/dist/shell/assets/{javascript-__evPE7m.js → javascript-FNavUzsm.js} +1 -1
  15. package/dist/shell/assets/{jsonMode-DpbQTbP6.js → jsonMode-HgW1GXQH.js} +1 -1
  16. package/dist/shell/assets/{liquid-CgOzfjEh.js → liquid-6wkVlojW.js} +1 -1
  17. package/dist/shell/assets/{lspLanguageFeatures-DM57zeAM.js → lspLanguageFeatures-CvNG6b_1.js} +1 -1
  18. package/dist/shell/assets/{mdx-DxN7Bq5u.js → mdx-C-byKky3.js} +1 -1
  19. package/dist/shell/assets/{python-C3rGYQJF.js → python-BEJMbP4i.js} +1 -1
  20. package/dist/shell/assets/{razor-C5rkTP_6.js → razor-DeXWW25R.js} +1 -1
  21. package/dist/shell/assets/{tsMode-DDCDv-RH.js → tsMode-DMwJnvD-.js} +1 -1
  22. package/dist/shell/assets/{typescript-CyHgeKIZ.js → typescript-Cj57ccSp.js} +1 -1
  23. package/dist/shell/assets/{workers-C7XbwGBM.js → workers-BZi9jqBp.js} +1 -1
  24. package/dist/shell/assets/{xml-Dmw92JAf.js → xml-DUDeQI0I.js} +1 -1
  25. package/dist/shell/assets/{yaml-Bj_aHuSs.js → yaml-BCD8Shg5.js} +1 -1
  26. package/dist/shell/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/shell/assets/index-B61fMQI2.css +0 -1
  29. package/dist/shell/assets/index-Mb4zTZaD.js +0 -230
package/dist/daemon.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // ../daemon/src/index.ts
3
- import { rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
3
+ import { rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
4
4
  import { dirname as dirname9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
@@ -40,6 +40,10 @@ var CONFIG_DIR = ".toyon";
40
40
  function isLocalConfigFile(rel) {
41
41
  return rel === CONFIG_FILES.folder.local || rel === CONFIG_FILES.root.local;
42
42
  }
43
+ function configSibling(rel, kind) {
44
+ const place = Object.values(CONFIG_FILES).find((p) => p.shared === rel || p.local === rel) ?? CONFIG_FILES.folder;
45
+ return place[kind];
46
+ }
43
47
 
44
48
  // ../shared/src/daemon.ts
45
49
  var DAEMON_FILES = {
@@ -183,7 +187,7 @@ function streamKey(worktreeId, stream) {
183
187
  }
184
188
 
185
189
  // ../shared/src/protocol/limits.ts
186
- var PROTOCOL_VERSION = 43;
190
+ var PROTOCOL_VERSION = 50;
187
191
  var FILE_MAX_CHARS = 5000000;
188
192
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
189
193
  var IMAGE_MAX_EDGE = 2576;
@@ -6221,6 +6225,7 @@ var toyonConfigSchema = object({
6221
6225
  setup: array(shellCommand).max(50).optional(),
6222
6226
  run: record(declaredProcName, shellCommand),
6223
6227
  check: shellCommand.optional(),
6228
+ afterLand: array(shellCommand).max(50).optional(),
6224
6229
  land: landConfigSchema.optional(),
6225
6230
  preview: procName.optional(),
6226
6231
  profiles: record(procName, runProfileSchema).optional(),
@@ -6255,6 +6260,7 @@ var variantSchema = object({ group: string2(), index: number2().int().min(1), of
6255
6260
  var clientMsgSchema = discriminatedUnion("t", [
6256
6261
  object({ t: literal("subscribe"), worktreeId: id }),
6257
6262
  object({ t: literal("unsubscribe"), worktreeId: id }),
6263
+ object({ t: literal("view"), worktreeId: id.nullable() }),
6258
6264
  object({
6259
6265
  t: literal("chat"),
6260
6266
  worktreeId: id,
@@ -6267,7 +6273,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6267
6273
  clientId: string2().max(64).optional(),
6268
6274
  repoId: id,
6269
6275
  prompt,
6270
- baseWorktreeId: id.optional(),
6271
6276
  variant: variantSchema.optional(),
6272
6277
  context: prose.optional(),
6273
6278
  attachments,
@@ -6275,7 +6280,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6275
6280
  profile: string2().max(100).optional(),
6276
6281
  mode: permissionModeSchema.optional(),
6277
6282
  model: string2().max(200).optional(),
6278
- effort: string2().max(100).optional()
6283
+ effort: string2().max(100).optional(),
6284
+ carry: boolean2().optional()
6279
6285
  }),
6280
6286
  object({
6281
6287
  t: literal("batch-worktrees"),
@@ -6294,7 +6300,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6294
6300
  object({
6295
6301
  t: literal("restore-worktree"),
6296
6302
  archiveId: id,
6297
- clientId: string2().max(64).optional()
6303
+ clientId: string2().max(64).optional(),
6304
+ message: object({ text: prose, attachments }).optional()
6298
6305
  }),
6299
6306
  object({ t: literal("delete-archived"), archiveId: id }),
6300
6307
  object({
@@ -6310,6 +6317,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6310
6317
  object({ t: literal("commit"), worktreeId: id, message: string2().max(5000) }),
6311
6318
  object({ t: literal("graft"), targetId: id, sourceIds: array(id).min(1).max(20) }),
6312
6319
  object({ t: literal("sync-main"), worktreeId: id }),
6320
+ object({ t: literal("run-after-land"), repoId: id }),
6321
+ object({ t: literal("restart-daemon") }),
6313
6322
  object({ t: literal("pull-main"), worktreeId: id }),
6314
6323
  object({
6315
6324
  t: literal("write-file"),
@@ -6337,7 +6346,12 @@ var clientMsgSchema = discriminatedUnion("t", [
6337
6346
  object({ t: literal("unqueue"), worktreeId: id, index: number2().int().min(0) }),
6338
6347
  object({ t: literal("changed-ranges"), worktreeId: id, path: relPath }),
6339
6348
  object({ t: literal("rename-worktree"), worktreeId: id, title: string2().min(1).max(200) }),
6340
- object({ t: literal("confirm-config"), repoId: id, config: toyonConfigSchema }),
6349
+ object({
6350
+ t: literal("confirm-config"),
6351
+ repoId: id,
6352
+ config: toyonConfigSchema,
6353
+ kind: _enum(["shared", "local"])
6354
+ }),
6341
6355
  object({ t: literal("register-repo"), path: string2().min(1).max(4000) }),
6342
6356
  object({
6343
6357
  t: literal("create-repo"),
@@ -6393,6 +6407,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6393
6407
  object({ t: literal("exec"), worktreeId: id, command: shellCommand.min(1) }),
6394
6408
  object({ t: literal("exec-stop"), worktreeId: id }),
6395
6409
  object({ t: literal("search-refs"), repoId: id, query: string2().max(200) }),
6410
+ object({ t: literal("search-chats"), repoId: id, query: string2().max(200) }),
6396
6411
  object({
6397
6412
  t: literal("open-ref"),
6398
6413
  clientId: string2().max(64).optional(),
@@ -7558,7 +7573,7 @@ function canGraft(wt) {
7558
7573
  // ../daemon/package.json
7559
7574
  var package_default = {
7560
7575
  name: "@toyon/daemon",
7561
- version: "0.1.0",
7576
+ version: "0.2.0",
7562
7577
  private: true,
7563
7578
  type: "module",
7564
7579
  main: "src/index.ts",
@@ -11885,6 +11900,7 @@ var EXT = {
11885
11900
  };
11886
11901
  var ID = /^[A-Za-z0-9_-]{1,64}$/;
11887
11902
  var FILE = /^\d{1,6}\.(png|jpg|gif|webp|txt)$/;
11903
+ var isAttachmentFile = (file) => FILE.test(file);
11888
11904
  function attachmentsDirFor(attachmentsDir, worktreeId) {
11889
11905
  return join(attachmentsDir, worktreeId);
11890
11906
  }
@@ -12480,6 +12496,7 @@ function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon"))
12480
12496
  stateFile: join3(home, DAEMON_FILES.state),
12481
12497
  tokenFile: join3(home, DAEMON_FILES.token),
12482
12498
  pidFile: join3(home, DAEMON_FILES.pid),
12499
+ logFile: join3(home, DAEMON_FILES.log),
12483
12500
  remoteFile: join3(home, DAEMON_FILES.remote),
12484
12501
  transcriptsDir: join3(home, "transcripts"),
12485
12502
  attachmentsDir: join3(home, "attachments"),
@@ -12506,11 +12523,17 @@ function ensureDirs(p) {
12506
12523
  // ../daemon/src/git/exclude.ts
12507
12524
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
12508
12525
  import { dirname } from "path";
12509
- async function excludeFromGit(cwd, pattern) {
12526
+ async function excludeFile(cwd) {
12510
12527
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
12511
- if (!r.ok || !r.out)
12512
- return log.warn("git", `cannot locate info/exclude for ${cwd}: ${r.err}`);
12513
- const file = r.out;
12528
+ if (r.ok && r.out)
12529
+ return r.out;
12530
+ log.warn("git", `cannot locate info/exclude for ${cwd}: ${r.err}`);
12531
+ return null;
12532
+ }
12533
+ async function excludeFromGit(cwd, pattern) {
12534
+ const file = await excludeFile(cwd);
12535
+ if (!file)
12536
+ return;
12514
12537
  const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
12515
12538
  if (current.split(`
12516
12539
  `).some((l) => l.trim() === pattern))
@@ -12521,6 +12544,18 @@ async function excludeFromGit(cwd, pattern) {
12521
12544
  ` : ""}${pattern}
12522
12545
  `);
12523
12546
  }
12547
+ async function unexcludeFromGit(cwd, pattern) {
12548
+ const file = await excludeFile(cwd);
12549
+ if (!file || !existsSync3(file))
12550
+ return;
12551
+ const lines = readFileSync3(file, "utf8").split(`
12552
+ `);
12553
+ const kept = lines.filter((l) => l.trim() !== pattern);
12554
+ if (kept.length === lines.length)
12555
+ return;
12556
+ writeFileSync(file, kept.join(`
12557
+ `));
12558
+ }
12524
12559
 
12525
12560
  // ../daemon/src/agent/bounds.ts
12526
12561
  import { existsSync as existsSync4, realpathSync } from "fs";
@@ -13013,7 +13048,7 @@ var SYSTEM_APPEND = [
13013
13048
  "Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
13014
13049
  "Keep the scope tight: do the asked task well, then stop. Suggest follow-ups in chat instead of expanding scope.",
13015
13050
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
13016
- `Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can be merged, and land.route (optional: "merge", "push" or "pr") says how the user lands work on main. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed.`,
13051
+ `Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed.`,
13017
13052
  "When you scaffold a project, write its .gitignore (dependencies, build output, local env files) before installing anything, so an install never leaves thousands of untracked files for the user to wade through or commit.",
13018
13053
  "When you scaffold a project or change how it installs or starts, finish by updating the settings file Toyon already reads, or writing .toyon/settings.json when there is none, so the preview can run it."
13019
13054
  ].join(" ");
@@ -13050,10 +13085,11 @@ function buildPrompt(text, context, prefix, attachments = []) {
13050
13085
  }
13051
13086
  for (const a of attachments)
13052
13087
  blocks.push(...attachmentBlocks(a));
13053
- if (!leads)
13054
- blocks.push(textBlock(prefix ? `${prefix}
13088
+ const body = [prefix, text].filter(Boolean).join(`
13055
13089
 
13056
- ${text}` : text));
13090
+ `);
13091
+ if (!leads && body)
13092
+ blocks.push(textBlock(body));
13057
13093
  if (context && !leads)
13058
13094
  blocks.push(textBlock(context));
13059
13095
  return blocks;
@@ -13077,26 +13113,41 @@ ${a.text}
13077
13113
  }
13078
13114
 
13079
13115
  // ../daemon/src/agent/transcript.ts
13080
- import { existsSync as existsSync7, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
13081
- import { appendFile, rename, writeFile as writeFile2 } from "fs/promises";
13116
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
13117
+ import { appendFile } from "fs/promises";
13082
13118
  import { join as join6 } from "path";
13083
- var MAX_ENTRIES = 4000;
13084
- var KEEP_ENTRIES = 2000;
13085
13119
  function transcriptPathFor(transcriptsDir, worktreeId) {
13086
13120
  return join6(transcriptsDir, `${worktreeId}.jsonl`);
13087
13121
  }
13088
- function cutPoint(entries) {
13089
- const from = Math.max(0, entries.length - KEEP_ENTRIES);
13090
- for (let i = from;i < entries.length; i++) {
13091
- const t = entries[i].event.type;
13092
- if (t === "user-message" || t === "turn-start")
13093
- return i;
13122
+ function coalesce(entries) {
13123
+ const out = [];
13124
+ for (const entry of entries) {
13125
+ const last = out.at(-1);
13126
+ const { event } = entry;
13127
+ if ((event.type === "text-delta" || event.type === "thinking-delta") && last?.event.type === event.type) {
13128
+ out[out.length - 1] = { seq: last.seq, event: { type: event.type, text: last.event.text + event.text } };
13129
+ } else {
13130
+ out.push(entry);
13131
+ }
13094
13132
  }
13095
- return from;
13133
+ return out;
13096
13134
  }
13097
- function serialize(entries) {
13098
- return entries.map((e) => `${JSON.stringify(e)}
13099
- `).join("");
13135
+ function parseTranscript(text, tag) {
13136
+ const out = [];
13137
+ let torn = 0;
13138
+ for (const line of text.split(`
13139
+ `)) {
13140
+ if (!line)
13141
+ continue;
13142
+ try {
13143
+ out.push(JSON.parse(line));
13144
+ } catch {
13145
+ torn++;
13146
+ }
13147
+ }
13148
+ if (torn)
13149
+ log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
13150
+ return out;
13100
13151
  }
13101
13152
 
13102
13153
  class Transcript {
@@ -13112,31 +13163,7 @@ class Transcript {
13112
13163
  this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
13113
13164
  }
13114
13165
  read() {
13115
- if (!existsSync7(this.path))
13116
- return [];
13117
- const out = [];
13118
- let torn = 0;
13119
- for (const line of readFileSync6(this.path, "utf8").split(`
13120
- `)) {
13121
- if (!line)
13122
- continue;
13123
- try {
13124
- out.push(JSON.parse(line));
13125
- } catch {
13126
- torn++;
13127
- }
13128
- }
13129
- if (torn)
13130
- log.warn(this.tag, `transcript: skipped ${torn} unparsable line(s)`);
13131
- if (out.length > MAX_ENTRIES) {
13132
- const kept = out.slice(cutPoint(out));
13133
- const tmp = `${this.path}.tmp`;
13134
- writeFileSync4(tmp, serialize(kept));
13135
- renameSync2(tmp, this.path);
13136
- log.info(this.tag, `transcript: compacted ${out.length} entries to ${kept.length}`);
13137
- return kept;
13138
- }
13139
- return out;
13166
+ return existsSync7(this.path) ? parseTranscript(readFileSync6(this.path, "utf8"), this.tag) : [];
13140
13167
  }
13141
13168
  append(event) {
13142
13169
  const entry = { seq: this.seq++, event };
@@ -13144,19 +13171,8 @@ class Transcript {
13144
13171
  const line = `${JSON.stringify(entry)}
13145
13172
  `;
13146
13173
  this.writes = this.writes.then(() => appendFile(this.path, line)).catch((e) => log.warn(this.tag, "transcript append failed", e));
13147
- if (this.entries.length > MAX_ENTRIES)
13148
- this.compact();
13149
13174
  return entry;
13150
13175
  }
13151
- compact() {
13152
- this.entries.splice(0, cutPoint(this.entries));
13153
- const snapshot = serialize(this.entries);
13154
- const tmp = `${this.path}.tmp`;
13155
- this.writes = this.writes.then(async () => {
13156
- await writeFile2(tmp, snapshot);
13157
- await rename(tmp, this.path);
13158
- }).catch((e) => log.warn(this.tag, "transcript compaction failed", e));
13159
- }
13160
13176
  flush() {
13161
13177
  return this.writes;
13162
13178
  }
@@ -13929,7 +13945,7 @@ class AcpSession {
13929
13945
  describe(e) {
13930
13946
  if (isAuthRequired(e))
13931
13947
  return this.conn?.spec.loginHint ?? this.d.spec().loginHint;
13932
- const message = e instanceof Error ? e.message : String(e);
13948
+ const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
13933
13949
  return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
13934
13950
  }
13935
13951
  async record({ text, attachments }) {
@@ -14611,24 +14627,31 @@ function setsGrant(line) {
14611
14627
  // ../daemon/src/runtime/proxy.ts
14612
14628
  var DIAL_TIMEOUT_MS = 5000;
14613
14629
  var frame = (ev) => typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
14630
+ var VITE_PING = "text/x-vite-ping";
14614
14631
  function previewHandler(opts) {
14615
14632
  const hostPart = (t) => t.host.includes(":") ? `[${t.host}]` : t.host;
14616
14633
  return {
14617
14634
  async fetch(req, upgrade) {
14618
14635
  const url = new URL(req.url);
14619
- const target = opts.getTarget();
14620
14636
  if (url.pathname === "/__toyon/bridge.js") {
14621
14637
  return new Response(opts.bridgeScript(), {
14622
14638
  headers: { "content-type": "text/javascript", "cache-control": "no-store" }
14623
14639
  });
14624
14640
  }
14641
+ const isUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
14642
+ const machine = isUpgrade || req.headers.get("accept") === VITE_PING;
14643
+ if (!machine)
14644
+ opts.onRequest?.();
14645
+ let target = opts.getTarget();
14646
+ if (target == null && !machine && opts.ready)
14647
+ target = await opts.ready();
14625
14648
  if (target == null) {
14626
14649
  return new Response(waitingPage(), {
14627
14650
  status: 503,
14628
14651
  headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
14629
14652
  });
14630
14653
  }
14631
- if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
14654
+ if (isUpgrade) {
14632
14655
  const proto = req.headers.get("sec-websocket-protocol") ?? undefined;
14633
14656
  const upstreamUrl = `ws://${hostPart(target)}:${target.port}${url.pathname}${url.search}`;
14634
14657
  const pending = [];
@@ -14982,6 +15005,8 @@ async function reachableHost(port) {
14982
15005
  var LOG_RING_SIZE = 500;
14983
15006
  var POLL_INTERVAL_MS = 500;
14984
15007
  var POLL_ATTEMPTS = 120;
15008
+ var FAST_POLL_MS = 100;
15009
+ var FAST_POLL_WINDOW_MS = 3000;
14985
15010
  var PROC_RING = 128 * 1024;
14986
15011
  var PROC_COLS = 120;
14987
15012
  var PROC_ROWS = 30;
@@ -14995,6 +15020,7 @@ class WorktreeProcs {
14995
15020
  opts;
14996
15021
  procs = new Map;
14997
15022
  stopped = false;
15023
+ asleep = false;
14998
15024
  lines = [];
14999
15025
  constructor(worktreePath, onProc, onLog, onData = () => {}, onStreamExit = () => {}, opts = {}) {
15000
15026
  this.worktreePath = worktreePath;
@@ -15041,7 +15067,7 @@ class WorktreeProcs {
15041
15067
  cols: mp.pty?.cols ?? PROC_COLS,
15042
15068
  rows: mp.pty?.rows ?? PROC_ROWS,
15043
15069
  file: "sh",
15044
- args: ["-c", command],
15070
+ args: ["-c", execForm(command)],
15045
15071
  ring: PROC_RING
15046
15072
  }, (data) => {
15047
15073
  this.onData(name, data);
@@ -15062,7 +15088,7 @@ class WorktreeProcs {
15062
15088
  const { name } = mp.state;
15063
15089
  mp.state.exitCode = code;
15064
15090
  this.onStreamExit(name, code);
15065
- if (this.stopped || mp.state.status === "stopped")
15091
+ if (this.stopped || mp.state.status === "stopped" || mp.state.status === "asleep")
15066
15092
  return;
15067
15093
  if (mp.handInput) {
15068
15094
  mp.state.status = "stopped";
@@ -15088,8 +15114,9 @@ class WorktreeProcs {
15088
15114
  }
15089
15115
  }
15090
15116
  async pollPort(mp) {
15091
- const attempts = this.opts.pollAttempts ?? POLL_ATTEMPTS;
15092
- for (let i = 0;i < attempts; i++) {
15117
+ const deadline = Date.now() + (this.opts.pollAttempts ?? POLL_ATTEMPTS) * POLL_INTERVAL_MS;
15118
+ const started = Date.now();
15119
+ while (Date.now() < deadline) {
15093
15120
  if (this.stopped || mp.state.status !== "starting")
15094
15121
  return;
15095
15122
  const host = await reachableHost(mp.state.port);
@@ -15099,7 +15126,7 @@ class WorktreeProcs {
15099
15126
  this.onProc({ ...mp.state });
15100
15127
  return;
15101
15128
  }
15102
- await Bun.sleep(POLL_INTERVAL_MS);
15129
+ await Bun.sleep(Date.now() - started < FAST_POLL_WINDOW_MS ? FAST_POLL_MS : POLL_INTERVAL_MS);
15103
15130
  }
15104
15131
  await this.diagnose(mp);
15105
15132
  }
@@ -15146,7 +15173,7 @@ class WorktreeProcs {
15146
15173
  }
15147
15174
  restart(name) {
15148
15175
  const mp = this.procs.get(name);
15149
- if (!mp)
15176
+ if (!mp || this.asleep)
15150
15177
  return;
15151
15178
  mp.restarts = 0;
15152
15179
  if (mp.state.status === "crashed") {
@@ -15162,6 +15189,35 @@ class WorktreeProcs {
15162
15189
  async killProc(mp) {
15163
15190
  await mp.pty?.kill();
15164
15191
  }
15192
+ async sleep() {
15193
+ if (this.asleep || this.stopped)
15194
+ return;
15195
+ this.asleep = true;
15196
+ const exits = [];
15197
+ for (const mp of this.procs.values()) {
15198
+ mp.state.status = "asleep";
15199
+ mp.state.pid = undefined;
15200
+ this.onProc({ ...mp.state });
15201
+ exits.push(this.killProc(mp));
15202
+ }
15203
+ await Promise.all(exits);
15204
+ }
15205
+ wake() {
15206
+ if (!this.asleep || this.stopped)
15207
+ return;
15208
+ this.asleep = false;
15209
+ for (const mp of this.procs.values()) {
15210
+ mp.restarts = 0;
15211
+ this.spawnProc(mp);
15212
+ }
15213
+ }
15214
+ pgids() {
15215
+ const out = [];
15216
+ for (const mp of this.procs.values())
15217
+ if (mp.pty?.alive)
15218
+ out.push(mp.pty.pid);
15219
+ return out;
15220
+ }
15165
15221
  async stopAll() {
15166
15222
  this.stopped = true;
15167
15223
  const exits = [];
@@ -15185,6 +15241,12 @@ class WorktreeProcs {
15185
15241
  this.onLog(proc, line);
15186
15242
  }
15187
15243
  }
15244
+ function execForm(command) {
15245
+ const trimmed = command.trim();
15246
+ if (/[;&|<>()`\n]|\$\(/.test(trimmed) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed))
15247
+ return command;
15248
+ return `exec ${trimmed}`;
15249
+ }
15188
15250
  function envStrings(base) {
15189
15251
  const env = {};
15190
15252
  for (const [k, v] of Object.entries(base))
@@ -15222,6 +15284,8 @@ function terminalEnv(base, own, urls) {
15222
15284
  return { ...env, ...urls, TERM: "xterm-256color", COLORTERM: "truecolor", ...own };
15223
15285
  }
15224
15286
  var DEFAULT_AGENT_ID = "claude";
15287
+ var PREVIEW_WAKE_MS = 8000;
15288
+ var PREVIEW_POLL_MS = 100;
15225
15289
  function defaultAgent(wt, d) {
15226
15290
  const agent = new AcpSession({
15227
15291
  worktreeId: wt.id,
@@ -15272,19 +15336,24 @@ function defaultProcs(wt, d) {
15272
15336
  function defaultTerminal(_wt, opts, onData, onExit) {
15273
15337
  return new PtyStream(opts, onData, onExit);
15274
15338
  }
15275
- function defaultProxy(wt, previewName, procs, d) {
15339
+ function defaultProxy(wt, previewName, procs, d, wake) {
15276
15340
  return startProxy({
15277
15341
  port: wt.proxyPort,
15278
15342
  hostname: cloud.bindHost,
15279
15343
  remote: d.remote ?? null,
15280
15344
  grant: d.grant ?? "",
15281
15345
  bridgeScript: d.bridgeScript,
15282
- getTarget: () => previewTargetOf(procs, previewName)
15346
+ getTarget: () => previewTargetOf(procs, previewName),
15347
+ onRequest: wake.onRequest,
15348
+ ready: wake.ready
15283
15349
  });
15284
15350
  }
15351
+ function previewProcOf(procs, previewName) {
15352
+ return procs.states().find((p) => p.name === previewName) ?? procs.states()[0];
15353
+ }
15285
15354
  function previewTargetOf(procs, previewName) {
15286
- const st = procs.states().find((p) => p.name === previewName) ?? procs.states()[0];
15287
- if (!st || st.status === "crashed" || st.status === "unreachable")
15355
+ const st = previewProcOf(procs, previewName);
15356
+ if (st?.status !== "running")
15288
15357
  return null;
15289
15358
  return { port: st.boundPort ?? st.port, host: st.host ?? "127.0.0.1" };
15290
15359
  }
@@ -15293,9 +15362,103 @@ class RuntimeRegistry {
15293
15362
  deps;
15294
15363
  runtimes = new Map;
15295
15364
  looseShells = new Map;
15365
+ holds = new Map;
15366
+ settingUp = new Set;
15367
+ starting = new Set;
15296
15368
  constructor(deps) {
15297
15369
  this.deps = deps;
15298
15370
  }
15371
+ hold(id, tag) {
15372
+ let tags = this.holds.get(id);
15373
+ if (!tags) {
15374
+ tags = new Set;
15375
+ this.holds.set(id, tags);
15376
+ }
15377
+ tags.add(tag);
15378
+ this.deps.hub.emit("holdsChanged", id, tags.size);
15379
+ }
15380
+ release(id, tag) {
15381
+ const tags = this.holds.get(id);
15382
+ if (!tags?.delete(tag))
15383
+ return;
15384
+ if (tags.size === 0)
15385
+ this.holds.delete(id);
15386
+ this.deps.hub.emit("holdsChanged", id, tags.size);
15387
+ }
15388
+ holdCount(id) {
15389
+ return this.holds.get(id)?.size ?? 0;
15390
+ }
15391
+ markSetup(id, on) {
15392
+ if (on)
15393
+ this.settingUp.add(id);
15394
+ else
15395
+ this.settingUp.delete(id);
15396
+ }
15397
+ async wake(id) {
15398
+ const wt = this.deps.state.worktree(id);
15399
+ if (!wt)
15400
+ return;
15401
+ const rt = this.runtimes.get(id);
15402
+ if (rt?.procs) {
15403
+ if (!rt.procs.asleep)
15404
+ return;
15405
+ rt.procs.wake();
15406
+ log.info(id, "awake");
15407
+ this.deps.hub.emit("worktreesChanged");
15408
+ return;
15409
+ }
15410
+ if (this.settingUp.has(id))
15411
+ return;
15412
+ const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
15413
+ if (repo)
15414
+ await this.start(wt, repo);
15415
+ }
15416
+ async sleep(id) {
15417
+ const rt = this.runtimes.get(id);
15418
+ if (!rt?.procs || rt.procs.asleep)
15419
+ return;
15420
+ await rt.procs.sleep();
15421
+ this.deps.hub.emit("worktreesChanged");
15422
+ }
15423
+ async awaitPreview(id, ms) {
15424
+ const deadline = Date.now() + ms;
15425
+ for (;; ) {
15426
+ const rt = this.runtimes.get(id);
15427
+ if (!rt?.procs)
15428
+ return null;
15429
+ const st = previewProcOf(rt.procs, rt.previewName);
15430
+ if (st?.status === "running")
15431
+ return previewTargetOf(rt.procs, rt.previewName);
15432
+ const coming = st ? st.status === "starting" : this.starting.has(id);
15433
+ if (!coming || Date.now() >= deadline)
15434
+ return null;
15435
+ await Bun.sleep(PREVIEW_POLL_MS);
15436
+ }
15437
+ }
15438
+ tiers() {
15439
+ let awake = 0;
15440
+ let asleep = 0;
15441
+ for (const rt of this.runtimes.values()) {
15442
+ if (!rt.procs)
15443
+ continue;
15444
+ if (rt.procs.asleep)
15445
+ asleep++;
15446
+ else
15447
+ awake++;
15448
+ }
15449
+ return { awake, asleep };
15450
+ }
15451
+ awake() {
15452
+ const out = [];
15453
+ for (const [id, rt] of this.runtimes) {
15454
+ if (rt.procs && !rt.procs.asleep)
15455
+ out.push({ id, pgids: rt.procs.pgids() });
15456
+ }
15457
+ return out;
15458
+ }
15459
+ isAsleep(id) {
15460
+ return this.runtimes.get(id)?.procs?.asleep ?? false;
15461
+ }
15299
15462
  openLooseShell(id, cwd, cols, rows) {
15300
15463
  let term = this.looseShells.get(id);
15301
15464
  if (!term?.alive) {
@@ -15380,19 +15543,30 @@ class RuntimeRegistry {
15380
15543
  const urls = { ...procUrlEnv(procs.states(), previewName), ...worktreeEnv(wt, repo) };
15381
15544
  return { ...urls, ...expandEnv(run.env, urls) };
15382
15545
  };
15383
- for (const [name, cmd] of Object.entries(run.procs)) {
15384
- if (name !== previewName)
15385
- await procs.start(name, cmd, envFor());
15386
- }
15387
- if (previewName && run.procs[previewName]) {
15388
- await procs.start(previewName, run.procs[previewName], envFor());
15546
+ this.starting.add(wt.id);
15547
+ try {
15548
+ for (const [name, cmd] of Object.entries(run.procs)) {
15549
+ if (name !== previewName)
15550
+ await procs.start(name, cmd, envFor());
15551
+ }
15552
+ if (previewName && run.procs[previewName]) {
15553
+ await procs.start(previewName, run.procs[previewName], envFor());
15554
+ }
15555
+ } finally {
15556
+ this.starting.delete(wt.id);
15389
15557
  }
15390
15558
  if (!this.deps.state.worktree(wt.id) || this.runtimes.get(wt.id) !== rt) {
15391
15559
  this.returnLease(wt.id);
15392
15560
  await procs.stopAll();
15393
15561
  return;
15394
15562
  }
15395
- rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps);
15563
+ rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps, {
15564
+ onRequest: () => {
15565
+ this.deps.hub.emit("previewRequest", wt.id);
15566
+ fireAndForget(wt.id, this.wake(wt.id), "wake on request");
15567
+ },
15568
+ ready: () => this.awaitPreview(wt.id, PREVIEW_WAKE_MS)
15569
+ });
15396
15570
  this.deps.hub.emit("worktreesChanged");
15397
15571
  }
15398
15572
  leased = new Map;
@@ -15415,6 +15589,8 @@ class RuntimeRegistry {
15415
15589
  await procs?.stopAll();
15416
15590
  }
15417
15591
  async stop(id) {
15592
+ this.holds.delete(id);
15593
+ this.settingUp.delete(id);
15418
15594
  const rt = this.runtimes.get(id);
15419
15595
  if (!rt)
15420
15596
  return;
@@ -15537,6 +15713,8 @@ class RuntimeRegistry {
15537
15713
  this.startLogin(id, rt.login.run);
15538
15714
  } else if (stream === SHELL_STREAM)
15539
15715
  await rt?.shell?.kill();
15716
+ else if (rt?.procs?.asleep)
15717
+ await this.wake(id);
15540
15718
  else
15541
15719
  rt?.procs?.restart(stream);
15542
15720
  }
@@ -15545,11 +15723,7 @@ class RuntimeRegistry {
15545
15723
  }
15546
15724
  previewTarget(id) {
15547
15725
  const rt = this.runtimes.get(id);
15548
- if (!rt?.procs)
15549
- return null;
15550
- const t = previewTargetOf(rt.procs, rt.previewName);
15551
- const st = rt.procs.states().find((p) => p.port === t?.port);
15552
- return st?.status === "running" ? t : null;
15726
+ return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
15553
15727
  }
15554
15728
  async shutdown() {
15555
15729
  this.pruneLooseShells(new Set);
@@ -15859,6 +16033,20 @@ var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested nam
15859
16033
  var namePrompt = (task) => `Name this coding task in 2 to 4 lowercase kebab-case words (like "sticky-header" or "dark-mode-toggle"). Reply with ONLY the name, nothing else.
15860
16034
 
15861
16035
  Task: ${task.slice(0, 500)}`;
16036
+ function taskText(text, attachments = []) {
16037
+ if (text.trim())
16038
+ return text;
16039
+ return attachments.map(attachmentText).join(`
16040
+
16041
+ `);
16042
+ }
16043
+ function attachmentText(a) {
16044
+ if (a.kind === "paste")
16045
+ return a.text;
16046
+ if (a.kind === "image")
16047
+ return a.name ? `image ${a.name}` : "an image";
16048
+ return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
16049
+ }
15862
16050
  function parseName(text) {
15863
16051
  if (!text)
15864
16052
  return null;
@@ -15932,15 +16120,46 @@ function makePlanner(agents) {
15932
16120
  }
15933
16121
 
15934
16122
  // ../daemon/src/core/assets.ts
15935
- import { existsSync as existsSync9 } from "fs";
15936
- import { join as join7 } from "path";
16123
+ import { existsSync as existsSync9, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
16124
+ import { join as join7, resolve as resolve5 } from "path";
15937
16125
  function locateAssets(here, env = process.env) {
15938
16126
  const packaged = existsSync9(join7(here, "shell", "index.html"));
15939
16127
  return {
15940
16128
  shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join7(here, "shell") : join7(here, "../../shell/dist")),
15941
- bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js"))
16129
+ bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js")),
16130
+ sourceRoot: packaged ? null : resolve5(here, "../../..")
15942
16131
  };
15943
16132
  }
16133
+ var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
16134
+ function pruneAssets(shellDist) {
16135
+ const dir = join7(shellDist, "assets");
16136
+ let names;
16137
+ try {
16138
+ names = readdirSync2(dir);
16139
+ } catch {
16140
+ return 0;
16141
+ }
16142
+ const found = [];
16143
+ for (const name of names) {
16144
+ const path = join7(dir, name);
16145
+ try {
16146
+ found.push({ path, mtime: statSync2(path).mtimeMs });
16147
+ } catch {}
16148
+ }
16149
+ if (found.length === 0)
16150
+ return 0;
16151
+ const newest = Math.max(...found.map((f) => f.mtime));
16152
+ let gone = 0;
16153
+ for (const f of found) {
16154
+ if (newest - f.mtime <= KEEP_MS)
16155
+ continue;
16156
+ try {
16157
+ rmSync(f.path, { recursive: true, force: true });
16158
+ gone++;
16159
+ } catch {}
16160
+ }
16161
+ return gone;
16162
+ }
15944
16163
 
15945
16164
  // ../daemon/src/core/dialog.ts
15946
16165
  import { existsSync as existsSync10 } from "fs";
@@ -16030,9 +16249,104 @@ class Hub {
16030
16249
  }
16031
16250
  }
16032
16251
 
16252
+ // ../daemon/src/core/restart.ts
16253
+ import { spawn as spawn2 } from "child_process";
16254
+ import { openSync } from "fs";
16255
+ function restartable(env = process.env) {
16256
+ if (env.TOYON_WORKTREE !== undefined) {
16257
+ return { ok: false, reason: "this daemon runs as a toyon process; restart it from its terminal tab" };
16258
+ }
16259
+ return { ok: true };
16260
+ }
16261
+ function respawn(logFile) {
16262
+ const logFd = openSync(logFile, "a");
16263
+ const child = spawn2(process.execPath, process.argv.slice(1), {
16264
+ detached: true,
16265
+ stdio: ["ignore", logFd, logFd],
16266
+ env: { ...process.env }
16267
+ });
16268
+ child.unref();
16269
+ }
16270
+
16271
+ // ../daemon/src/core/self.ts
16272
+ function needsRestart(path) {
16273
+ return path.startsWith("packages/daemon/") || path.startsWith("packages/shared/") || path === "package.json" || path === "bun.lock";
16274
+ }
16275
+ function needsRebuild(path) {
16276
+ return path.startsWith("packages/shell/") || path.startsWith("packages/bridge/") || path.startsWith("packages/shared/");
16277
+ }
16278
+ function classify(paths) {
16279
+ let rebuild = false;
16280
+ let restart = false;
16281
+ for (const path of paths) {
16282
+ if (needsRebuild(path))
16283
+ rebuild = true;
16284
+ if (needsRestart(path))
16285
+ restart = true;
16286
+ }
16287
+ return { rebuild, restart };
16288
+ }
16289
+
16290
+ class SelfWatch {
16291
+ root;
16292
+ booted = null;
16293
+ state = null;
16294
+ constructor(root) {
16295
+ this.root = root;
16296
+ }
16297
+ async start() {
16298
+ if (!this.root)
16299
+ return;
16300
+ const head = await git(this.root, "rev-parse", "HEAD");
16301
+ if (!head.ok) {
16302
+ log.debug("self", `no HEAD in ${this.root}; not watching it`);
16303
+ return;
16304
+ }
16305
+ this.booted = head.out.trim();
16306
+ }
16307
+ own(repo) {
16308
+ return this.root !== null && repo.path === this.root;
16309
+ }
16310
+ async check(repo) {
16311
+ if (this.booted === null || !this.own(repo))
16312
+ return false;
16313
+ const names = await git(this.root ?? "", "diff", "--name-only", `${this.booted}..${repo.defaultBranch}`);
16314
+ if (!names.ok) {
16315
+ log.debug("self", `could not diff ${this.booted}..${repo.defaultBranch}: ${names.err}`);
16316
+ return false;
16317
+ }
16318
+ const paths = names.out.split(`
16319
+ `).filter((l) => l !== "");
16320
+ const { rebuild, restart } = classify(paths);
16321
+ return this.set({ repoId: repo.id, rebuild, restart, building: this.state?.building ?? false });
16322
+ }
16323
+ building(on, failure) {
16324
+ if (!this.state)
16325
+ return false;
16326
+ const done = !on && failure === undefined;
16327
+ return this.set({
16328
+ ...this.state,
16329
+ building: on,
16330
+ rebuild: done ? false : this.state.rebuild,
16331
+ buildFailed: on ? undefined : failure
16332
+ });
16333
+ }
16334
+ get() {
16335
+ return this.state;
16336
+ }
16337
+ set(next) {
16338
+ const quiet = !next.rebuild && !next.restart && !next.building && next.buildFailed === undefined;
16339
+ const value = quiet ? null : next;
16340
+ if (JSON.stringify(value) === JSON.stringify(this.state))
16341
+ return false;
16342
+ this.state = value;
16343
+ return true;
16344
+ }
16345
+ }
16346
+
16033
16347
  // ../daemon/src/core/state.ts
16034
16348
  import { randomBytes } from "crypto";
16035
- import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
16349
+ import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
16036
16350
  var SEEN_WORKTREES = 50;
16037
16351
  var empty = { repos: [], worktrees: [], sessions: {} };
16038
16352
  function loadState(paths) {
@@ -16053,7 +16367,7 @@ function loadState(paths) {
16053
16367
  } catch (e) {
16054
16368
  const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
16055
16369
  try {
16056
- writeFileSync5(backup, raw);
16370
+ writeFileSync4(backup, raw);
16057
16371
  } catch (be) {
16058
16372
  log.warn("state", "could not write the corrupt-state backup", be);
16059
16373
  }
@@ -16080,8 +16394,8 @@ function loadState(paths) {
16080
16394
  }
16081
16395
  function saveState(paths, state) {
16082
16396
  const tmp = `${paths.stateFile}.tmp`;
16083
- writeFileSync5(tmp, JSON.stringify(state, null, 2));
16084
- renameSync3(tmp, paths.stateFile);
16397
+ writeFileSync4(tmp, JSON.stringify(state, null, 2));
16398
+ renameSync2(tmp, paths.stateFile);
16085
16399
  }
16086
16400
  function loadOrCreateToken(paths) {
16087
16401
  ensureDirs(paths);
@@ -16089,7 +16403,7 @@ function loadOrCreateToken(paths) {
16089
16403
  if (existsSync11(TOKEN_FILE))
16090
16404
  return readFileSync8(TOKEN_FILE, "utf8").trim();
16091
16405
  const token = randomBytes(32).toString("hex");
16092
- writeFileSync5(TOKEN_FILE, token, { mode: 384 });
16406
+ writeFileSync4(TOKEN_FILE, token, { mode: 384 });
16093
16407
  return token;
16094
16408
  }
16095
16409
 
@@ -16841,6 +17155,7 @@ class ExecService {
16841
17155
  }
16842
17156
  const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
16843
17157
  this.track(worktreeId, toolId, { proc, timer });
17158
+ this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
16844
17159
  return this.collect(worktreeId, toolId, proc, agent);
16845
17160
  }
16846
17161
  stop(worktreeId) {
@@ -16906,6 +17221,7 @@ class ExecService {
16906
17221
  const r = this.untrack(worktreeId, toolId);
16907
17222
  if (r)
16908
17223
  clearTimeout(r.timer);
17224
+ this.deps.runtime.release(worktreeId, `exec:${toolId}`);
16909
17225
  }
16910
17226
  agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
16911
17227
  return { exit, text };
@@ -16931,7 +17247,7 @@ ${body}
16931
17247
  }
16932
17248
 
16933
17249
  // ../daemon/src/files/service.ts
16934
- import { spawn as spawn2 } from "child_process";
17250
+ import { spawn as spawn3 } from "child_process";
16935
17251
  import { stat, unlink } from "fs/promises";
16936
17252
 
16937
17253
  // ../daemon/src/git/lock.ts
@@ -17080,6 +17396,9 @@ async function treeEmpty(worktreePath) {
17080
17396
  const r = await git(worktreePath, "ls-tree", "--name-only", "HEAD");
17081
17397
  return !r.ok || r.out === "";
17082
17398
  }
17399
+ async function isTracked(worktreePath, rel) {
17400
+ return (await git(worktreePath, "ls-files", "--error-unmatch", "--", rel)).ok;
17401
+ }
17083
17402
  async function behindUpstream(worktreePath) {
17084
17403
  const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
17085
17404
  return r.ok ? Number(r.out) || 0 : null;
@@ -17149,12 +17468,12 @@ async function fileAtCommit(worktreePath, sha, file) {
17149
17468
  }
17150
17469
 
17151
17470
  // ../daemon/src/worktrees/paths.ts
17152
- import { resolve as resolve5 } from "path";
17471
+ import { resolve as resolve6 } from "path";
17153
17472
  function resolveInside(worktreePath, rel, opts = {}) {
17154
17473
  if (typeof rel !== "string" || rel.includes("\x00"))
17155
17474
  throw new UserError("invalid path");
17156
- const root = canonical(resolve5(worktreePath));
17157
- const target = canonical(resolve5(worktreePath, rel));
17475
+ const root = canonical(resolve6(worktreePath));
17476
+ const target = canonical(resolve6(worktreePath, rel));
17158
17477
  if (target === root) {
17159
17478
  if (opts.allowRoot)
17160
17479
  return target;
@@ -17521,7 +17840,7 @@ class FileService {
17521
17840
  revealPath(target) {
17522
17841
  if (process.platform !== "darwin")
17523
17842
  throw new UserError("reveal is only available on macOS");
17524
- const child = spawn2("open", ["-R", target], { stdio: "ignore" });
17843
+ const child = spawn3("open", ["-R", target], { stdio: "ignore" });
17525
17844
  child.on("error", () => {});
17526
17845
  child.unref();
17527
17846
  }
@@ -17614,8 +17933,89 @@ function prNumberOf(url) {
17614
17933
  return m ? Number(m[1]) : null;
17615
17934
  }
17616
17935
 
17936
+ // ../daemon/src/runtime/setup.ts
17937
+ var SETUP_RING = 64 * 1024;
17938
+ var SETUP_COLS = 120;
17939
+ var SETUP_ROWS = 30;
17940
+ function runSetup(cmd, cwd, onLine, extra = {}) {
17941
+ return new Promise((resolve) => {
17942
+ const lines = new LineSplitter;
17943
+ try {
17944
+ new PtyStream({
17945
+ cwd,
17946
+ env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
17947
+ cols: SETUP_COLS,
17948
+ rows: SETUP_ROWS,
17949
+ file: "sh",
17950
+ args: ["-c", cmd],
17951
+ ring: SETUP_RING
17952
+ }, (data) => {
17953
+ for (const line of lines.feed(data))
17954
+ onLine(line);
17955
+ }, resolve);
17956
+ } catch (e) {
17957
+ onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
17958
+ resolve(1);
17959
+ }
17960
+ });
17961
+ }
17962
+ function envStrings2(base) {
17963
+ const env = {};
17964
+ for (const [k, v] of Object.entries(base))
17965
+ if (typeof v === "string")
17966
+ env[k] = v;
17967
+ return env;
17968
+ }
17969
+
17970
+ // ../daemon/src/repos/afterLand.ts
17971
+ class AfterLand {
17972
+ d;
17973
+ running = new Set;
17974
+ constructor(d) {
17975
+ this.d = d;
17976
+ }
17977
+ busy(repoId) {
17978
+ return this.running.has(repoId);
17979
+ }
17980
+ run(repoId) {
17981
+ const repo = this.d.state.repo(repoId);
17982
+ const commands = repo?.config.afterLand ?? [];
17983
+ if (!repo || commands.length === 0 || this.running.has(repoId))
17984
+ return;
17985
+ this.running.add(repoId);
17986
+ const done = this.exec(repo, commands).finally(() => this.running.delete(repoId));
17987
+ fireAndForget(repo.id, done, "afterLand");
17988
+ }
17989
+ async exec(repo, commands) {
17990
+ if (this.d.self.building(true))
17991
+ this.d.hub.emit("selfChanged");
17992
+ let failure;
17993
+ for (const command of commands) {
17994
+ this.tellMain(repo, `afterLand: ${command}`);
17995
+ let last = "";
17996
+ const code = await runSetup(command, repo.path, (line) => {
17997
+ last = line;
17998
+ this.tellMain(repo, line);
17999
+ });
18000
+ if (code === 0)
18001
+ continue;
18002
+ failure = last.trim() === "" ? `${command} (exit ${code})` : last.trim();
18003
+ this.tellMain(repo, `afterLand stopped: ${command} exited ${code}`);
18004
+ log.warn(repo.id, `afterLand stopped: ${command} exited ${code}`);
18005
+ break;
18006
+ }
18007
+ if (this.d.self.building(false, failure))
18008
+ this.d.hub.emit("selfChanged");
18009
+ }
18010
+ tellMain(repo, line) {
18011
+ const main = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
18012
+ if (main)
18013
+ this.d.hub.emit("log", main.id, "land", line);
18014
+ }
18015
+ }
18016
+
17617
18017
  // ../daemon/src/repos/registry.ts
17618
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
18018
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
17619
18019
  import { homedir as homedir7 } from "os";
17620
18020
  import { basename as basename3, dirname as dirname6, join as join14 } from "path";
17621
18021
 
@@ -17643,11 +18043,11 @@ function variantLens(index) {
17643
18043
  }
17644
18044
 
17645
18045
  // ../daemon/src/repos/browse.ts
17646
- import { existsSync as existsSync12, statSync as statSync2 } from "fs";
18046
+ import { existsSync as existsSync12, statSync as statSync3 } from "fs";
17647
18047
  import { readdir } from "fs/promises";
17648
18048
  import { homedir as homedir5 } from "os";
17649
18049
  import { dirname as dirname4, join as join10 } from "path";
17650
- var MAX_ENTRIES2 = 40;
18050
+ var MAX_ENTRIES = 40;
17651
18051
  function expandTilde(raw) {
17652
18052
  return raw === "~" || raw.startsWith("~/") ? join10(homedir5(), raw.slice(1)) : raw;
17653
18053
  }
@@ -17672,7 +18072,7 @@ var NOWHERE = { exists: false, isDir: false, isRepo: false, parentExists: false
17672
18072
  function describe2(typed, parent) {
17673
18073
  let isDir = false;
17674
18074
  try {
17675
- isDir = statSync2(typed).isDirectory();
18075
+ isDir = statSync3(typed).isDirectory();
17676
18076
  } catch {}
17677
18077
  return {
17678
18078
  exists: existsSync12(typed),
@@ -17706,11 +18106,11 @@ async function browsePath(raw) {
17706
18106
  entries.push({ path: collapseTilde(abs), name, isRepo: existsSync12(join10(abs, ".git")) });
17707
18107
  }
17708
18108
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
17709
- return { entries: entries.slice(0, MAX_ENTRIES2), target };
18109
+ return { entries: entries.slice(0, MAX_ENTRIES), target };
17710
18110
  }
17711
18111
 
17712
18112
  // ../daemon/src/repos/config.ts
17713
- import { closeSync, existsSync as existsSync13, openSync, readdirSync as readdirSync2, readFileSync as readFileSync9, readSync } from "fs";
18113
+ import { closeSync, existsSync as existsSync13, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync9, readSync } from "fs";
17714
18114
  import { join as join11 } from "path";
17715
18115
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
17716
18116
  var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync13(join11(repoPath, rel)));
@@ -17873,7 +18273,7 @@ function markerIn(repoPath) {
17873
18273
  return named;
17874
18274
  let entries;
17875
18275
  try {
17876
- entries = readdirSync2(repoPath);
18276
+ entries = readdirSync3(repoPath);
17877
18277
  } catch {
17878
18278
  return;
17879
18279
  }
@@ -17895,7 +18295,7 @@ function servesPage(repoPath) {
17895
18295
  function frontEndBeside(repoPath) {
17896
18296
  let dirs;
17897
18297
  try {
17898
- dirs = readdirSync2(repoPath, { withFileTypes: true });
18298
+ dirs = readdirSync3(repoPath, { withFileTypes: true });
17899
18299
  } catch {
17900
18300
  return false;
17901
18301
  }
@@ -17920,7 +18320,7 @@ function head(path) {
17920
18320
  if (!existsSync13(path))
17921
18321
  return "";
17922
18322
  try {
17923
- const fd = openSync(path, "r");
18323
+ const fd = openSync2(path, "r");
17924
18324
  try {
17925
18325
  const buf = Buffer.alloc(READ_CAP);
17926
18326
  return buf.toString("utf8", 0, readSync(fd, buf, 0, READ_CAP, 0));
@@ -17966,10 +18366,10 @@ function detectRunner(repoPath, packageManager) {
17966
18366
  }
17967
18367
 
17968
18368
  // ../daemon/src/repos/create.ts
17969
- import { existsSync as existsSync14, readdirSync as readdirSync3, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
18369
+ import { existsSync as existsSync14, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
17970
18370
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
17971
18371
  import { homedir as homedir6 } from "os";
17972
- import { dirname as dirname5, join as join12, resolve as resolve6 } from "path";
18372
+ import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
17973
18373
  function isInside(dir, parent) {
17974
18374
  const p = parent.replace(/\/+$/, "");
17975
18375
  return dir === p || dir.startsWith(`${p}/`);
@@ -17997,12 +18397,12 @@ function planInPlace(opts) {
17997
18397
  throw new UserError(`${opts.name} is not a folder name`);
17998
18398
  }
17999
18399
  const typed = join12(resolveParent(opts.parent), name);
18000
- if (!existsSync14(typed) || !statSync3(typed).isDirectory())
18400
+ if (!existsSync14(typed) || !statSync4(typed).isDirectory())
18001
18401
  throw new UserError(`${typed} is not a folder`);
18002
18402
  const dir = realpathSync2(typed);
18003
18403
  if (existsSync14(join12(dir, ".git")))
18004
18404
  throw new UserError(`${dir} is already a project; open it instead`);
18005
- if (!holdsNothing(readdirSync3(dir))) {
18405
+ if (!holdsNothing(readdirSync4(dir))) {
18006
18406
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
18007
18407
  }
18008
18408
  return { parent: dirname5(dir), dir, makeParent: false };
@@ -18013,7 +18413,7 @@ function resolveParent(raw) {
18013
18413
  throw new UserError(`${raw} is not a folder path`);
18014
18414
  if (!existsSync14(typed))
18015
18415
  throw new UserError(`${raw} does not exist; make it first`);
18016
- if (!statSync3(typed).isDirectory())
18416
+ if (!statSync4(typed).isDirectory())
18017
18417
  throw new UserError(`${raw} is not a folder`);
18018
18418
  return realpathSync2(typed);
18019
18419
  }
@@ -18051,7 +18451,7 @@ async function initWithEmptyCommit(dir) {
18051
18451
  const exclude = await git(dir, "rev-parse", "--git-path", "info/exclude");
18052
18452
  if (!exclude.ok)
18053
18453
  throw new UserError(`git rev-parse failed: ${exclude.err}`);
18054
- const excludePath = resolve6(dir, exclude.out);
18454
+ const excludePath = resolve7(dir, exclude.out);
18055
18455
  await mkdir2(dirname5(excludePath), { recursive: true });
18056
18456
  await appendFile2(excludePath, `
18057
18457
  ${LOCAL_EXCLUDES.join(`
@@ -18313,25 +18713,15 @@ class RepoRegistry {
18313
18713
  log.info("daemon", `${cold} worktrees across ${state.repos.length} repos start when opened`);
18314
18714
  }
18315
18715
  warm(repoId) {
18316
- if (this.warmed.has(repoId))
18716
+ if (this.warmed.has(repoId) || !this.d.state.repo(repoId))
18317
18717
  return;
18318
18718
  this.warmed.add(repoId);
18319
- const repo = this.d.state.requireRepo(repoId);
18320
- const mine = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind !== "spare");
18321
- fireAndForget(repoId, (async () => {
18322
- for (const wt of mine)
18323
- await this.d.runtime.start(wt, repo);
18324
- })(), "warm");
18325
18719
  this.d.worktrees.spare.warm(repoId);
18326
18720
  }
18327
18721
  touch(worktreeId) {
18328
- const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
18329
- if (!this.warmed.has(repo.id)) {
18330
- this.warm(repo.id);
18722
+ if (!this.d.state.worktree(worktreeId))
18331
18723
  return;
18332
- }
18333
- if (!this.d.runtime.get(wt.id)?.procs)
18334
- fireAndForget(wt.id, this.d.runtime.start(wt, repo), "start on open");
18724
+ fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start on open");
18335
18725
  }
18336
18726
  async create(opts) {
18337
18727
  const inPlace = opts.mode === "init";
@@ -18512,23 +18902,34 @@ class RepoRegistry {
18512
18902
  this.d.hub.emit("reposChanged");
18513
18903
  this.d.hub.emit("worktreesChanged");
18514
18904
  }
18515
- async confirmConfig(repoId, config) {
18905
+ async confirmConfig(repoId, config, kind) {
18516
18906
  const repo = this.d.state.requireRepo(repoId);
18517
18907
  const current = readConfigFile(repo.path);
18518
18908
  if (current && !current.ok && current.conflict)
18519
18909
  throw new UserError(current.reason);
18520
- this.placeConfig(repo);
18521
- const rel = repo.configFile;
18910
+ const was = configTarget(repo.path, !!repo.made);
18911
+ const rel = configSibling(was, kind);
18912
+ const moved = rel !== was && existsSync16(join14(repo.path, was));
18913
+ if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
18914
+ try {
18915
+ rmSync2(join14(repo.path, was));
18916
+ } catch (e) {
18917
+ log.warn(repoId, `could not remove ${was}`, e);
18918
+ }
18919
+ if (isLocalConfigFile(was))
18920
+ await unexcludeFromGit(repo.path, was);
18921
+ }
18522
18922
  if (isLocalConfigFile(rel))
18523
18923
  await excludeFromGit(repo.path, rel);
18524
18924
  const file = join14(repo.path, rel);
18525
18925
  try {
18526
18926
  mkdirSync5(dirname6(file), { recursive: true });
18527
- writeFileSync6(file, `${JSON.stringify(configBody(repo.path, rel, config), null, 2)}
18927
+ writeFileSync5(file, `${JSON.stringify(configBody(repo.path, rel, config), null, 2)}
18528
18928
  `);
18529
18929
  } catch (e) {
18530
18930
  log.warn(repoId, `could not write ${rel}`, e);
18531
18931
  }
18932
+ this.placeConfig(repo);
18532
18933
  const back = readConfigFile(repo.path);
18533
18934
  repo.config = back?.ok ? back.config : config;
18534
18935
  repo.needsSetup = false;
@@ -18637,6 +19038,10 @@ class RepoRegistry {
18637
19038
  if (main)
18638
19039
  this.d.hub.emit("log", main.id, "config", line);
18639
19040
  }
19041
+ async checkSelf(repo) {
19042
+ if (await this.d.self.check(repo))
19043
+ this.d.hub.emit("selfChanged");
19044
+ }
18640
19045
  startWatcher(repo) {
18641
19046
  if (this.watchers.has(repo.id))
18642
19047
  return;
@@ -18644,6 +19049,7 @@ class RepoRegistry {
18644
19049
  this.d.worktrees.invalidateCounts();
18645
19050
  this.d.hub.emit("repoTick", repo.id);
18646
19051
  fireAndForget(repo.id, this.d.worktrees.spare.refresh(repo.id), "spare refresh");
19052
+ fireAndForget(repo.id, this.checkSelf(repo).then(() => this.d.afterLand.run(repo.id)), "self check");
18647
19053
  });
18648
19054
  const stopCfg = watchConfigFile(repo.path, () => this.reloadConfig(repo.id));
18649
19055
  const stopWt = watchWorktreeDir(repo.path, () => {
@@ -19937,7 +20343,7 @@ class RouteService {
19937
20343
  }
19938
20344
 
19939
20345
  // ../daemon/src/runtime/bridge-script.ts
19940
- import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
20346
+ import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
19941
20347
  var LEARNED_MAX = 8;
19942
20348
 
19943
20349
  class BridgeScript {
@@ -19969,7 +20375,7 @@ class BridgeScript {
19969
20375
  get() {
19970
20376
  if (!existsSync17(this.path))
19971
20377
  return "// toyon bridge not built";
19972
- const s = statSync4(this.path);
20378
+ const s = statSync5(this.path);
19973
20379
  const stamp = `${s.mtimeMs}:${s.size}`;
19974
20380
  if (this.cache && stamp === this.stamp)
19975
20381
  return this.cache;
@@ -19993,6 +20399,343 @@ function servedLocally(origin) {
19993
20399
  return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
19994
20400
  }
19995
20401
 
20402
+ // ../daemon/src/runtime/memory.ts
20403
+ var MAC_PRESSURE_NORMAL = 1;
20404
+ var AVAILABLE_FLOOR = 0.15;
20405
+ var unsupportedLogged = false;
20406
+ async function memoryTight() {
20407
+ const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
20408
+ if (signal === null && !unsupportedLogged) {
20409
+ unsupportedLogged = true;
20410
+ log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
20411
+ }
20412
+ return signal;
20413
+ }
20414
+ async function darwin() {
20415
+ const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
20416
+ if (!r.ok)
20417
+ return null;
20418
+ const [level, available] = r.out.split(/\s+/).map(Number);
20419
+ if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
20420
+ return null;
20421
+ const pressure = level === MAC_PRESSURE_NORMAL ? "normal" : level === 2 ? "warn" : level === 4 ? "critical" : `level ${level}`;
20422
+ return {
20423
+ tight: level !== MAC_PRESSURE_NORMAL,
20424
+ backstop: level === MAC_PRESSURE_NORMAL && available < AVAILABLE_FLOOR * 100,
20425
+ why: `memory pressure ${pressure} with ${available}% available`
20426
+ };
20427
+ }
20428
+ async function linux() {
20429
+ let text;
20430
+ try {
20431
+ text = await Bun.file("/proc/meminfo").text();
20432
+ } catch {
20433
+ return null;
20434
+ }
20435
+ const field = (name) => {
20436
+ const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
20437
+ return m ? Number(m[1]) : null;
20438
+ };
20439
+ const total = field("MemTotal");
20440
+ const available = field("MemAvailable");
20441
+ if (!total || available === null)
20442
+ return null;
20443
+ const percent = Math.round(available / total * 100);
20444
+ return { tight: available / total < AVAILABLE_FLOOR, backstop: false, why: `${percent}% of memory available` };
20445
+ }
20446
+ async function sampleCosts(pgids) {
20447
+ const costs = new Map;
20448
+ if (pgids.length === 0)
20449
+ return costs;
20450
+ const wanted = new Set(pgids);
20451
+ const r = await run("ps", ["-axo", "pgid=,rss="], "/");
20452
+ if (!r.ok)
20453
+ return costs;
20454
+ for (const line of r.out.split(`
20455
+ `)) {
20456
+ const [pgid, rss] = line.trim().split(/\s+/).map(Number);
20457
+ if (pgid === undefined || rss === undefined || !wanted.has(pgid))
20458
+ continue;
20459
+ costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
20460
+ }
20461
+ return costs;
20462
+ }
20463
+
20464
+ // ../daemon/src/runtime/idle.ts
20465
+ var LOCAL_SLEEP_MS = 2 * 60 * 60000;
20466
+ var CLOUD_SLEEP_MS = 5 * 60000;
20467
+ function sleepMsFrom(raw, inCloud) {
20468
+ if (raw === "off")
20469
+ return null;
20470
+ const n = Number(raw);
20471
+ if (raw && Number.isFinite(n) && n > 0)
20472
+ return n;
20473
+ return inCloud ? CLOUD_SLEEP_MS : LOCAL_SLEEP_MS;
20474
+ }
20475
+ var DEFAULT_SLEEP_MS = sleepMsFrom(process.env.TOYON_PROC_SLEEP_MS, cloud.enabled);
20476
+ var PRESSURE_EVERY_MS = 15000;
20477
+ var COSTS_EVERY_MS = 60000;
20478
+ var STAGGER_MS = 3000;
20479
+
20480
+ class IdlePolicy {
20481
+ d;
20482
+ entries = new Map;
20483
+ viewing = new Map;
20484
+ queue = [];
20485
+ draining = false;
20486
+ sleepMs;
20487
+ now;
20488
+ constructor(d) {
20489
+ this.d = d;
20490
+ this.sleepMs = d.sleepMs === undefined ? DEFAULT_SLEEP_MS : d.sleepMs;
20491
+ this.now = d.now ?? Date.now;
20492
+ d.hub.on("previewRequest", (id) => this.touch(id));
20493
+ d.hub.on("agentStatus", (id) => this.stamp(id));
20494
+ d.hub.on("holdsChanged", (id) => this.stamp(id));
20495
+ d.hub.on("turnSettled", (id) => {
20496
+ if (this.recentlyViewed(id))
20497
+ d.wake(id);
20498
+ });
20499
+ d.hub.on("proc", (id, p) => {
20500
+ if (p.status !== "starting")
20501
+ this.reconsider(id);
20502
+ });
20503
+ d.hub.on("worktreesChanged", () => this.reconsiderAll());
20504
+ const every = d.setInterval ?? ((fn, ms) => {
20505
+ setInterval(fn, ms).unref?.();
20506
+ });
20507
+ every(() => fireAndForget("idle", this.checkPressure(), "memory check"), PRESSURE_EVERY_MS);
20508
+ every(() => fireAndForget("idle", this.sampleCosts(), "cost sample"), COSTS_EVERY_MS);
20509
+ }
20510
+ view(key, id) {
20511
+ const prev = this.viewing.get(key) ?? null;
20512
+ if (prev === id)
20513
+ return;
20514
+ if (prev !== null) {
20515
+ this.viewing.delete(key);
20516
+ this.entry(prev).viewers.delete(key);
20517
+ this.record(prev);
20518
+ this.stamp(prev);
20519
+ }
20520
+ if (id === null)
20521
+ return;
20522
+ this.viewing.set(key, id);
20523
+ this.entry(id).viewers.add(key);
20524
+ const wt = this.record(id);
20525
+ this.stamp(id);
20526
+ this.d.wake(id);
20527
+ if (wt && wt.kind !== "spare") {
20528
+ this.enqueue(`spare:${wt.repoId}`, async () => {
20529
+ await this.d.runtime.awaitPreview(id, STAGGER_MS);
20530
+ this.d.warmSpare(wt.repoId);
20531
+ const spare = this.spareOf(wt.repoId);
20532
+ if (spare)
20533
+ this.d.wake(spare.id);
20534
+ });
20535
+ }
20536
+ }
20537
+ drop(key) {
20538
+ this.view(key, null);
20539
+ }
20540
+ shutdown() {
20541
+ this.recordViewed();
20542
+ }
20543
+ record(id) {
20544
+ const wt = this.d.state.worktree(id);
20545
+ if (!wt)
20546
+ return;
20547
+ wt.viewedAt = this.now();
20548
+ this.d.state.save();
20549
+ return wt;
20550
+ }
20551
+ recordViewed() {
20552
+ let changed = false;
20553
+ for (const id of new Set(this.viewing.values())) {
20554
+ const wt = this.d.state.worktree(id);
20555
+ if (!wt)
20556
+ continue;
20557
+ wt.viewedAt = this.now();
20558
+ changed = true;
20559
+ }
20560
+ if (changed)
20561
+ this.d.state.save();
20562
+ }
20563
+ touch(id) {
20564
+ this.stamp(id);
20565
+ }
20566
+ boot() {
20567
+ const recent = this.d.state.worktrees.filter((w) => w.kind !== "spare" && this.recentlyViewed(w.id)).sort((a, b) => (b.viewedAt ?? 0) - (a.viewedAt ?? 0));
20568
+ for (const wt of recent) {
20569
+ this.enqueue(`boot:${wt.id}`, async () => {
20570
+ this.d.wake(wt.id);
20571
+ await this.d.runtime.awaitPreview(wt.id, STAGGER_MS);
20572
+ });
20573
+ }
20574
+ }
20575
+ recentlyViewed(id) {
20576
+ const wt = this.d.state.worktree(id);
20577
+ return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
20578
+ }
20579
+ isViewed(id) {
20580
+ if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
20581
+ return true;
20582
+ const wt = this.d.state.worktree(id);
20583
+ if (wt?.kind !== "spare")
20584
+ return false;
20585
+ return this.d.state.worktrees.some((w) => w.repoId === wt.repoId && (this.entries.get(w.id)?.viewers.size ?? 0) > 0);
20586
+ }
20587
+ costs() {
20588
+ const out = {};
20589
+ for (const [id, e] of this.entries)
20590
+ if (e.costKb !== undefined)
20591
+ out[id] = e.costKb;
20592
+ return out;
20593
+ }
20594
+ async checkPressure() {
20595
+ const signal = await (this.d.memory ?? memoryTight)();
20596
+ if (!signal || !signal.tight && !signal.backstop)
20597
+ return;
20598
+ const pick = this.d.state.worktrees.filter((w) => this.candidate(w.id)).sort((a, b) => this.entry(a.id).activeAt - this.entry(b.id).activeAt)[0];
20599
+ if (!pick)
20600
+ return;
20601
+ const why = `${signal.why}; least recently used`;
20602
+ if (signal.tight)
20603
+ this.sleep(pick.id, why);
20604
+ else
20605
+ log.info(pick.id, `would sleep on the backstop: ${why}`);
20606
+ }
20607
+ async sampleCosts() {
20608
+ this.recordViewed();
20609
+ const awake = this.d.runtime.awake();
20610
+ const pgids = awake.flatMap((a) => a.pgids);
20611
+ if (pgids.length === 0)
20612
+ return;
20613
+ const rss = await (this.d.costs ?? sampleCosts)(pgids);
20614
+ for (const a of awake)
20615
+ this.entry(a.id).costKb = a.pgids.reduce((sum, g) => sum + (rss.get(g) ?? 0), 0);
20616
+ }
20617
+ entry(id) {
20618
+ let e = this.entries.get(id);
20619
+ if (!e) {
20620
+ e = { viewers: new Set, activeAt: this.now(), timer: null };
20621
+ this.entries.set(id, e);
20622
+ }
20623
+ return e;
20624
+ }
20625
+ spareOf(repoId) {
20626
+ return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
20627
+ }
20628
+ stamp(id) {
20629
+ this.entry(id).activeAt = this.now();
20630
+ this.reconsider(id);
20631
+ const wt = this.d.state.worktree(id);
20632
+ if (!wt || wt.kind === "spare")
20633
+ return;
20634
+ const spare = this.spareOf(wt.repoId);
20635
+ if (!spare)
20636
+ return;
20637
+ this.entry(spare.id).activeAt = this.now();
20638
+ this.reconsider(spare.id);
20639
+ }
20640
+ candidate(id) {
20641
+ const wt = this.d.state.worktree(id);
20642
+ if (!wt || this.isViewed(id) || this.busy(id))
20643
+ return false;
20644
+ const procs = this.d.runtime.get(id)?.procs;
20645
+ if (!procs || procs.asleep)
20646
+ return false;
20647
+ const states = procs.states();
20648
+ if (states.length === 0 || states.some((s) => s.status === "starting"))
20649
+ return false;
20650
+ if (wt.kind === "spare") {
20651
+ return !this.d.state.worktrees.some((w) => w.repoId === wt.repoId && w.id !== id && this.busy(w.id));
20652
+ }
20653
+ return true;
20654
+ }
20655
+ busy(id) {
20656
+ const agent = this.d.runtime.agentFor(id);
20657
+ if (agent && (agent.status !== "idle" || agent.queueItems.length > 0))
20658
+ return true;
20659
+ return this.d.runtime.holdCount(id) > 0;
20660
+ }
20661
+ reconsider(id) {
20662
+ if (!this.candidate(id)) {
20663
+ this.disarm(id);
20664
+ return;
20665
+ }
20666
+ if (this.sleepMs === null)
20667
+ return;
20668
+ this.arm(id, Math.max(0, this.entry(id).activeAt + this.sleepMs - this.now()));
20669
+ }
20670
+ reconsiderAll() {
20671
+ for (const id of this.entries.keys()) {
20672
+ if (this.d.state.worktree(id))
20673
+ continue;
20674
+ this.disarm(id);
20675
+ this.entries.delete(id);
20676
+ }
20677
+ for (const wt of this.d.state.worktrees)
20678
+ this.reconsider(wt.id);
20679
+ }
20680
+ arm(id, ms) {
20681
+ this.disarm(id);
20682
+ const set = this.d.setTimer ?? ((fn, delay) => {
20683
+ const t = setTimeout(fn, delay);
20684
+ t.unref?.();
20685
+ return t;
20686
+ });
20687
+ this.entry(id).timer = set(() => this.fire(id), ms);
20688
+ }
20689
+ disarm(id) {
20690
+ const e = this.entries.get(id);
20691
+ if (!e?.timer)
20692
+ return;
20693
+ (this.d.clearTimer ?? ((t) => clearTimeout(t)))(e.timer);
20694
+ e.timer = null;
20695
+ }
20696
+ fire(id) {
20697
+ const e = this.entry(id);
20698
+ e.timer = null;
20699
+ if (this.sleepMs === null || !this.candidate(id))
20700
+ return;
20701
+ const idle = this.now() - e.activeAt;
20702
+ if (idle < this.sleepMs) {
20703
+ this.arm(id, this.sleepMs - idle);
20704
+ return;
20705
+ }
20706
+ this.sleep(id, `idle ${humanMs(this.sleepMs)}`);
20707
+ }
20708
+ sleep(id, why) {
20709
+ this.disarm(id);
20710
+ this.entry(id).costKb = undefined;
20711
+ log.info(id, `asleep: ${why}`);
20712
+ fireAndForget(id, this.d.runtime.sleep(id), "sleep");
20713
+ }
20714
+ enqueue(key, run) {
20715
+ if (this.queue.some((q) => q.key === key))
20716
+ return;
20717
+ this.queue.push({ key, run });
20718
+ if (!this.draining)
20719
+ fireAndForget("idle", this.drain(), "wake queue");
20720
+ }
20721
+ async drain() {
20722
+ this.draining = true;
20723
+ try {
20724
+ for (let next = this.queue.shift();next; next = this.queue.shift())
20725
+ await next.run();
20726
+ } finally {
20727
+ this.draining = false;
20728
+ }
20729
+ }
20730
+ }
20731
+ function humanMs(ms) {
20732
+ if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
20733
+ return `${ms / (60 * 60000)}h`;
20734
+ if (ms >= 60000)
20735
+ return `${Math.round(ms / 60000)} min`;
20736
+ return `${Math.round(ms / 1000)}s`;
20737
+ }
20738
+
19996
20739
  // ../daemon/src/server/ws.ts
19997
20740
  import { homedir as homedir9 } from "os";
19998
20741
 
@@ -20597,7 +21340,7 @@ function daylightNow(tz, at = new Date) {
20597
21340
  }
20598
21341
 
20599
21342
  // ../daemon/src/server/handlers.ts
20600
- var toast = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
21343
+ var shipped = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
20601
21344
  var errorText = (e) => e instanceof Error ? e.message : String(e);
20602
21345
  var gitStatus = async (s, ctx, worktreeId) => {
20603
21346
  const info = await s.worktrees.gitStatus(worktreeId);
@@ -20611,6 +21354,8 @@ var gitAndPages = async (s, ctx, worktreeId) => {
20611
21354
  };
20612
21355
  var notify = async (s, ctx, worktreeId, msg) => {
20613
21356
  ctx.reply(msg);
21357
+ if (!msg.ok)
21358
+ s.turns.markUnread(worktreeId);
20614
21359
  await gitStatus(s, ctx, worktreeId);
20615
21360
  };
20616
21361
  var requireRun = (s, id) => {
@@ -20622,8 +21367,17 @@ var requireRun = (s, id) => {
20622
21367
  var handlers = {
20623
21368
  async subscribe(msg, ctx, s) {
20624
21369
  const r = s.worktrees.readable(msg.worktreeId);
20625
- if (!r)
20626
- throw new UserError("unknown worktree");
21370
+ if (!r) {
21371
+ const kept = s.worktrees.archivedTranscript(msg.worktreeId);
21372
+ if (!kept)
21373
+ throw new UserError("unknown worktree");
21374
+ if (!ctx.subscribe(msg.worktreeId))
21375
+ return;
21376
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
21377
+ ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
21378
+ ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
21379
+ return;
21380
+ }
20627
21381
  if (!ctx.subscribe(msg.worktreeId))
20628
21382
  return;
20629
21383
  if (!r.wt) {
@@ -20633,13 +21387,11 @@ var handlers = {
20633
21387
  await gitAndPages(s, ctx, msg.worktreeId);
20634
21388
  return;
20635
21389
  }
20636
- s.repos.touch(msg.worktreeId);
20637
21390
  const agent = s.runtime.ensureAgent(r.wt).agent;
20638
- const events = agent.transcript();
20639
21391
  ctx.reply({
20640
21392
  t: "backfill",
20641
21393
  worktreeId: msg.worktreeId,
20642
- events: events.slice(-1000),
21394
+ events: coalesce(agent.transcript()),
20643
21395
  log: s.runtime.recentLogs(msg.worktreeId)
20644
21396
  });
20645
21397
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
@@ -20649,6 +21401,9 @@ var handlers = {
20649
21401
  unsubscribe(msg, ctx) {
20650
21402
  ctx.unsubscribe(msg.worktreeId);
20651
21403
  },
21404
+ view(msg, ctx) {
21405
+ ctx.view(msg.worktreeId);
21406
+ },
20652
21407
  seen(msg, _ctx, s) {
20653
21408
  s.turns.markSeen(msg.worktreeId);
20654
21409
  },
@@ -20678,7 +21433,6 @@ var handlers = {
20678
21433
  async "create-worktree"(msg, _ctx, s) {
20679
21434
  await s.worktrees.create(msg.repoId, msg.prompt, {
20680
21435
  createdBy: msg.clientId,
20681
- baseWorktreeId: msg.baseWorktreeId,
20682
21436
  variant: msg.variant,
20683
21437
  context: msg.context,
20684
21438
  attachments: msg.attachments,
@@ -20686,7 +21440,8 @@ var handlers = {
20686
21440
  profile: msg.profile,
20687
21441
  mode: msg.mode,
20688
21442
  model: msg.model,
20689
- effort: msg.effort
21443
+ effort: msg.effort,
21444
+ carry: msg.carry
20690
21445
  });
20691
21446
  },
20692
21447
  "set-worktree-profile"(msg, _ctx, s) {
@@ -20703,7 +21458,6 @@ var handlers = {
20703
21458
  },
20704
21459
  "batch-worktrees"(msg, ctx, s) {
20705
21460
  const repo = s.state.requireRepo(msg.repoId);
20706
- ctx.reply(toast("", true, "batch: planning tasks\u2026"));
20707
21461
  fireAndForget(msg.repoId, (async () => {
20708
21462
  const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
20709
21463
  const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
@@ -20717,22 +21471,18 @@ var handlers = {
20717
21471
  }
20718
21472
  }
20719
21473
  const started = tasks.length - failed;
20720
- ctx.reply(toast("", failed === 0, failed ? `batch: ${started} started, ${failed} failed (see daemon log)` : `batch: ${started} worktree(s) started`));
21474
+ if (failed)
21475
+ ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
20721
21476
  })(), "batch");
20722
21477
  },
20723
- async "remove-worktree"(msg, ctx, s) {
20724
- const archived = await s.worktrees.remove(msg.worktreeId);
20725
- if (archived) {
20726
- ctx.reply(toast(msg.worktreeId, true, `removed ${archived.title}: archived`, {
20727
- ...archived.restorable ? { restoreId: archived.id } : {}
20728
- }));
20729
- }
21478
+ async "remove-worktree"(msg, _ctx, s) {
21479
+ await s.worktrees.remove(msg.worktreeId);
20730
21480
  },
20731
21481
  "list-archived"(msg, ctx, s) {
20732
21482
  ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
20733
21483
  },
20734
21484
  async "restore-worktree"(msg, _ctx, s) {
20735
- await s.worktrees.restore(msg.archiveId, msg.clientId);
21485
+ await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
20736
21486
  },
20737
21487
  async "delete-archived"(msg, _ctx, s) {
20738
21488
  await s.worktrees.deleteArchived(msg.archiveId);
@@ -20768,7 +21518,7 @@ var handlers = {
20768
21518
  if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
20769
21519
  fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
20770
21520
  }
20771
- await notify(s, ctx, msg.worktreeId, toast(msg.worktreeId, result.ok, result.message, {
21521
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
20772
21522
  merged: result.ok,
20773
21523
  url: result.url,
20774
21524
  removeIds: removeIds ?? [],
@@ -20778,21 +21528,41 @@ var handlers = {
20778
21528
  async "sync-main"(msg, ctx, s) {
20779
21529
  const { result, defaultBranch } = await s.worktrees.sync(msg.worktreeId);
20780
21530
  const prompt = !result.ok && result.conflict && s.state.worktree(msg.worktreeId);
20781
- await notify(s, ctx, msg.worktreeId, prompt ? toast(msg.worktreeId, false, `sync conflicts with ${defaultBranch}: prompt prefilled in chat`, {
21531
+ await notify(s, ctx, msg.worktreeId, prompt ? shipped(msg.worktreeId, false, `sync conflicts with ${defaultBranch}: prompt prefilled in chat`, {
20782
21532
  suggestion: `Merge ${defaultBranch} into this branch and resolve the conflicts, then verify the app still works.`
20783
- }) : toast(msg.worktreeId, result.ok, result.message));
21533
+ }) : shipped(msg.worktreeId, result.ok, result.message));
20784
21534
  },
20785
21535
  async "pull-main"(msg, ctx, s) {
20786
21536
  const result = await s.worktrees.pull(msg.worktreeId);
20787
- await notify(s, ctx, msg.worktreeId, toast(msg.worktreeId, result.ok, result.message));
21537
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
21538
+ },
21539
+ "run-after-land"(msg, ctx, s) {
21540
+ const repo = s.state.repo(msg.repoId);
21541
+ if (!repo)
21542
+ throw new UserError("no such project");
21543
+ if ((repo.config.afterLand ?? []).length === 0) {
21544
+ throw new UserError(`${repo.name} has no afterLand commands in ${repo.configFile}`);
21545
+ }
21546
+ s.afterLand.run(msg.repoId);
21547
+ ctx.reply({ t: "self", self: s.self.get() });
21548
+ },
21549
+ "restart-daemon"(_msg, _ctx, s) {
21550
+ const busy = s.state.worktrees.filter((w) => s.runtime.agentFor(w.id)?.status === "working");
21551
+ if (busy.length > 0) {
21552
+ const names = busy.map((w) => w.title).join(", ");
21553
+ throw new UserError(`still working on ${names}; restart once that settles`);
21554
+ }
21555
+ const refused = s.restart();
21556
+ if (refused)
21557
+ throw new UserError(refused);
20788
21558
  },
20789
21559
  async commit(msg, ctx, s) {
20790
21560
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
20791
- await notify(s, ctx, msg.worktreeId, toast(msg.worktreeId, result.ok, result.message));
21561
+ await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
20792
21562
  },
20793
21563
  async graft(msg, ctx, s) {
20794
- const { target, grafted } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
20795
- await notify(s, ctx, target.id, toast(target.id, true, `grafted ${grafted.join(", ")} into ${target.title}; merged locally, nothing pushed`));
21564
+ const { target } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
21565
+ await gitStatus(s, ctx, target.id);
20796
21566
  },
20797
21567
  async "rename-worktree"(msg, _ctx, s) {
20798
21568
  await s.worktrees.rename(msg.worktreeId, msg.title);
@@ -20837,9 +21607,8 @@ var handlers = {
20837
21607
  reveal(msg, _ctx, s) {
20838
21608
  s.files.reveal(msg.worktreeId, msg.path);
20839
21609
  },
20840
- async "discard-file"(msg, ctx, s) {
21610
+ async "discard-file"(msg, _ctx, s) {
20841
21611
  await s.files.discard(msg.worktreeId, msg.path);
20842
- ctx.reply(toast(msg.worktreeId, true, `discarded ${msg.path}`));
20843
21612
  s.hub.emit("filesChanged", msg.worktreeId);
20844
21613
  },
20845
21614
  async "write-file"(msg, ctx, s) {
@@ -20856,19 +21625,17 @@ var handlers = {
20856
21625
  }
20857
21626
  },
20858
21627
  async "confirm-config"(msg, _ctx, s) {
20859
- await s.repos.confirmConfig(msg.repoId, msg.config);
21628
+ await s.repos.confirmConfig(msg.repoId, msg.config, msg.kind);
20860
21629
  },
20861
- async "register-repo"(msg, ctx, s) {
20862
- const repo = await s.repos.register(msg.path);
20863
- ctx.reply(toast("", true, `opened ${repo.name}`));
21630
+ async "register-repo"(msg, _ctx, s) {
21631
+ await s.repos.register(msg.path);
20864
21632
  },
20865
- async "create-repo"(msg, ctx, s) {
21633
+ async "create-repo"(msg, _ctx, s) {
20866
21634
  if (msg.mode === "clone") {
20867
21635
  s.repos.startImport({ parent: msg.parent, name: msg.name, url: msg.url ?? "" });
20868
21636
  return;
20869
21637
  }
20870
- const repo = await s.repos.create(msg);
20871
- ctx.reply(toast("", true, `created ${repo.name}`));
21638
+ await s.repos.create(msg);
20872
21639
  },
20873
21640
  async "unmake-repo"(msg, _ctx, s) {
20874
21641
  await s.repos.unmake(msg.repoId);
@@ -20892,10 +21659,8 @@ var handlers = {
20892
21659
  "cancel-folder"(_msg, _ctx, s) {
20893
21660
  s.folderDialog.cancel();
20894
21661
  },
20895
- async "forget-repo"(msg, ctx, s) {
20896
- const name = s.state.requireRepo(msg.repoId).name;
21662
+ async "forget-repo"(msg, _ctx, s) {
20897
21663
  await s.repos.forget(msg.repoId);
20898
- ctx.reply(toast("", true, `forgot ${name}`));
20899
21664
  },
20900
21665
  "set-theme"(msg, _ctx, s) {
20901
21666
  s.themes.setPrefs(msg.prefs);
@@ -21005,6 +21770,10 @@ var handlers = {
21005
21770
  const refs = await s.refs.search(msg.repoId, msg.query);
21006
21771
  ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
21007
21772
  },
21773
+ async "search-chats"(msg, ctx, s) {
21774
+ const { hits, truncated } = await s.chats.search(msg.repoId, msg.query);
21775
+ ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated });
21776
+ },
21008
21777
  async "open-ref"(msg, _ctx, s) {
21009
21778
  await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
21010
21779
  }
@@ -21061,6 +21830,7 @@ function createFetch(opts) {
21061
21830
  dropped: new Set,
21062
21831
  sent: 0,
21063
21832
  bytes: 0,
21833
+ view: null,
21064
21834
  preview: { handler, data }
21065
21835
  }
21066
21836
  }));
@@ -21069,7 +21839,15 @@ function createFetch(opts) {
21069
21839
  const authed = sameSecret(url.searchParams.get("token"), opts.token);
21070
21840
  if (authed)
21071
21841
  opts.noteShellOrigin(req.headers.get("origin"));
21072
- const data = { authed, subs: new Set, terms: new Set, dropped: new Set, sent: 0, bytes: 0 };
21842
+ const data = {
21843
+ authed,
21844
+ subs: new Set,
21845
+ terms: new Set,
21846
+ dropped: new Set,
21847
+ sent: 0,
21848
+ bytes: 0,
21849
+ view: null
21850
+ };
21073
21851
  const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
21074
21852
  if (srv.upgrade(req, { data, headers }))
21075
21853
  return;
@@ -21106,8 +21884,9 @@ function createFetch(opts) {
21106
21884
  if (!sameSecret(url.searchParams.get("token"), opts.token))
21107
21885
  return new Response("unauthorized", { status: 401 });
21108
21886
  const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
21109
- const path = worktreeId && file && !extra ? opts.attachments.fileFor(worktreeId, file) : null;
21110
- if (!path || !existsSync19(path))
21887
+ const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
21888
+ const path = places.find((p) => !!p && existsSync19(p));
21889
+ if (!path)
21111
21890
  return new Response("not found", { status: 404 });
21112
21891
  return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
21113
21892
  }
@@ -21214,8 +21993,10 @@ function startServer(opts) {
21214
21993
  lag,
21215
21994
  worktrees: {
21216
21995
  total: s.state.worktrees.filter((w) => w.kind !== "spare").length,
21217
- running: s.runtime.runningCount()
21996
+ running: s.runtime.runningCount(),
21997
+ ...s.runtime.tiers()
21218
21998
  },
21999
+ costs: s.idle.costs(),
21219
22000
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
21220
22001
  });
21221
22002
  let statusesInFlight = false;
@@ -21320,9 +22101,11 @@ function startServer(opts) {
21320
22101
  const agentsMsg = () => ({
21321
22102
  t: "agents",
21322
22103
  agents: agentInfos(),
21323
- defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID
22104
+ defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
22105
+ agentChosen: s.state.defaultAgent !== undefined
21324
22106
  });
21325
22107
  s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
22108
+ s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
21326
22109
  s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
21327
22110
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
21328
22111
  const helloFrame = async () => ({
@@ -21336,12 +22119,14 @@ function startServer(opts) {
21336
22119
  themePrefs: s.themes.prefs,
21337
22120
  agents: agentInfos(),
21338
22121
  defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
22122
+ agentChosen: s.state.defaultAgent !== undefined,
21339
22123
  home: homedir9(),
21340
22124
  folderDialog: process.platform === "darwin" && !cloud.enabled,
21341
22125
  remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
21342
22126
  gitIdentity: await s.repos.gitIdentity(),
21343
22127
  pending: s.repos.pending,
21344
- visits: s.routes.historyAll()
22128
+ visits: s.routes.historyAll(),
22129
+ self: s.self.get()
21345
22130
  });
21346
22131
  let branded = false;
21347
22132
  const serverConfig = {
@@ -21352,6 +22137,7 @@ function startServer(opts) {
21352
22137
  version,
21353
22138
  repos: s.repos,
21354
22139
  attachments: s.attachments,
22140
+ archivedAttachment: (id, file) => s.worktrees.archivedAttachment(id, file),
21355
22141
  branded: () => branded,
21356
22142
  metrics,
21357
22143
  noteShellOrigin: opts.noteShellOrigin,
@@ -21377,6 +22163,8 @@ function startServer(opts) {
21377
22163
  close(ws) {
21378
22164
  if (ws.data.preview)
21379
22165
  ws.data.preview.handler.close(ws.data.preview.data);
22166
+ else
22167
+ s.idle.drop(ws.data);
21380
22168
  sockets.delete(ws);
21381
22169
  },
21382
22170
  async message(ws, raw) {
@@ -21409,6 +22197,14 @@ function startServer(opts) {
21409
22197
  },
21410
22198
  unsubscribe: (id) => {
21411
22199
  ws.data.subs.delete(id);
22200
+ if (ws.data.view === id) {
22201
+ ws.data.view = null;
22202
+ s.idle.view(ws.data, null);
22203
+ }
22204
+ },
22205
+ view: (id) => {
22206
+ ws.data.view = id;
22207
+ s.idle.view(ws.data, id);
21412
22208
  },
21413
22209
  watchTerminal: (id, stream) => {
21414
22210
  ws.data.terms.add(streamKey(id, stream));
@@ -21424,7 +22220,12 @@ function startServer(opts) {
21424
22220
  } catch (e) {
21425
22221
  if (!(e instanceof UserError || e instanceof ThemeImportError))
21426
22222
  log.error("ws", `${parsed.msg.t} failed`, e);
21427
- send(ws, { t: "error", message: e instanceof Error ? e.message : String(e) });
22223
+ const worktreeId = "worktreeId" in parsed.msg ? parsed.msg.worktreeId : undefined;
22224
+ send(ws, {
22225
+ t: "error",
22226
+ message: e instanceof Error ? e.message : String(e),
22227
+ ...worktreeId ? { worktreeId } : {}
22228
+ });
21428
22229
  }
21429
22230
  }
21430
22231
  }
@@ -21448,9 +22249,9 @@ function startServer(opts) {
21448
22249
  }
21449
22250
 
21450
22251
  // ../daemon/src/themes/store.ts
21451
- import { existsSync as existsSync20, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync5, writeFileSync as writeFileSync7 } from "fs";
22252
+ import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
21452
22253
  import { homedir as homedir10 } from "os";
21453
- import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve7 } from "path";
22254
+ import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve8 } from "path";
21454
22255
 
21455
22256
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
21456
22257
  function createScanner(text, ignoreTrivia = false) {
@@ -22347,7 +23148,7 @@ class ThemeStore {
22347
23148
  name: json?.name ?? base,
22348
23149
  source: "file"
22349
23150
  });
22350
- writeFileSync7(join18(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
23151
+ writeFileSync6(join18(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
22351
23152
  `);
22352
23153
  this.load();
22353
23154
  return theme;
@@ -22362,7 +23163,7 @@ function readVscodeTheme(file, depth = 0) {
22362
23163
  throw new ThemeImportError(`${file}: not a JSON object`);
22363
23164
  const t = json;
22364
23165
  if (typeof t.include === "string" && depth < 5) {
22365
- const parent = readVscodeTheme(resolve7(dirname7(file), t.include), depth + 1);
23166
+ const parent = readVscodeTheme(resolve8(dirname7(file), t.include), depth + 1);
22366
23167
  return {
22367
23168
  ...parent,
22368
23169
  ...t,
@@ -22379,7 +23180,7 @@ function loadDir(dir) {
22379
23180
  if (!existsSync20(dir))
22380
23181
  return [];
22381
23182
  const out = [];
22382
- for (const f of readdirSync4(dir)) {
23183
+ for (const f of readdirSync5(dir)) {
22383
23184
  if (!/\.jsonc?$/i.test(f))
22384
23185
  continue;
22385
23186
  const file = join18(dir, f);
@@ -22403,7 +23204,7 @@ function discover(dir) {
22403
23204
  const out = [];
22404
23205
  let entries;
22405
23206
  try {
22406
- entries = readdirSync4(dir);
23207
+ entries = readdirSync5(dir);
22407
23208
  } catch {
22408
23209
  return out;
22409
23210
  }
@@ -22411,7 +23212,7 @@ function discover(dir) {
22411
23212
  const extDir = join18(dir, name);
22412
23213
  const pkgFile = join18(extDir, "package.json");
22413
23214
  try {
22414
- if (!statSync5(extDir).isDirectory() || !existsSync20(pkgFile))
23215
+ if (!statSync6(extDir).isDirectory() || !existsSync20(pkgFile))
22415
23216
  continue;
22416
23217
  const pkg = JSON.parse(readFileSync12(pkgFile, "utf8"));
22417
23218
  const themes = pkg.contributes?.themes;
@@ -22431,7 +23232,7 @@ function discover(dir) {
22431
23232
  const m = /^%(.+)%$/.exec(c.label);
22432
23233
  if (m)
22433
23234
  c.label = nls[m[1]] ?? m[1];
22434
- const file = resolve7(extDir, c.path);
23235
+ const file = resolve8(extDir, c.path);
22435
23236
  try {
22436
23237
  const json = readVscodeTheme(file);
22437
23238
  out.push(vscodeToTheme(json, { id: `vscode:${ext}:${slug(c.label)}`, name: c.label, source: "vscode" }));
@@ -22446,6 +23247,129 @@ function discover(dir) {
22446
23247
  return out;
22447
23248
  }
22448
23249
 
23250
+ // ../daemon/src/worktrees/chats.ts
23251
+ var CHAT_HITS_PER_WORKTREE = 20;
23252
+ var CHAT_HITS_MAX = 200;
23253
+ var MIN_QUERY = 2;
23254
+ var SNIPPET = 160;
23255
+ var BEFORE = 50;
23256
+ var bunFs2 = {
23257
+ async stat(path) {
23258
+ try {
23259
+ const s = await Bun.file(path).stat();
23260
+ return { size: s.size, mtimeMs: s.mtimeMs };
23261
+ } catch {
23262
+ return null;
23263
+ }
23264
+ },
23265
+ async text(path) {
23266
+ try {
23267
+ return await Bun.file(path).text();
23268
+ } catch {
23269
+ return null;
23270
+ }
23271
+ }
23272
+ };
23273
+ var flat = (text) => text.replace(/\s+/g, " ").trim();
23274
+ function saidRows(entries) {
23275
+ const out = [];
23276
+ let turnTs = 0;
23277
+ const push = (seq, role, said, ts) => {
23278
+ const text = flat(said);
23279
+ if (text)
23280
+ out.push({ seq, role, text, lower: text.toLowerCase(), ts });
23281
+ };
23282
+ for (const { seq, event } of coalesce(entries)) {
23283
+ if (event.type === "user-message") {
23284
+ turnTs = event.ts;
23285
+ push(seq, "user", event.text, event.ts);
23286
+ } else if (event.type === "turn-start") {
23287
+ turnTs = event.ts;
23288
+ } else if (event.type === "text-delta") {
23289
+ push(seq, "assistant", event.text, turnTs);
23290
+ }
23291
+ }
23292
+ return out;
23293
+ }
23294
+ function snippet(text, at, length) {
23295
+ const start = Math.max(0, Math.min(at - BEFORE, text.length - SNIPPET));
23296
+ const end = Math.min(text.length, start + SNIPPET);
23297
+ const head = start > 0 ? "\u2026" : "";
23298
+ const tail = end < text.length ? "\u2026" : "";
23299
+ return {
23300
+ text: `${head}${text.slice(start, end)}${tail}`,
23301
+ match: [at - start + head.length, Math.min(length, end - at)]
23302
+ };
23303
+ }
23304
+ function searchRows(rows, needle, chat, limit = CHAT_HITS_PER_WORKTREE) {
23305
+ const hits = [];
23306
+ for (let i = rows.length - 1;i >= 0; i--) {
23307
+ const row = rows[i];
23308
+ const at = row.lower.indexOf(needle);
23309
+ if (at < 0)
23310
+ continue;
23311
+ if (hits.length === limit)
23312
+ return { hits, more: true };
23313
+ hits.push({ ...chat, seq: row.seq, role: row.role, ts: row.ts, ...snippet(row.text, at, needle.length) });
23314
+ }
23315
+ return { hits, more: false };
23316
+ }
23317
+
23318
+ class ChatSearch {
23319
+ d;
23320
+ said = new Map;
23321
+ constructor(d) {
23322
+ this.d = d;
23323
+ }
23324
+ async search(repoId, query) {
23325
+ this.d.state.requireRepo(repoId);
23326
+ const needle = flat(query).toLowerCase();
23327
+ if (needle.length < MIN_QUERY)
23328
+ return { hits: [], truncated: false };
23329
+ const chats = [
23330
+ ...this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree").map((w) => ({ id: w.id, archived: false, path: this.d.transcriptPath(w.id) })),
23331
+ ...this.d.archivedChats(repoId).map((a) => ({ id: a.id, archived: true, path: a.transcript }))
23332
+ ];
23333
+ const read = new Set;
23334
+ const found = await Promise.all(chats.map(async (c) => {
23335
+ const rows = await this.rowsOf(c.id, c.archived, c.path, read);
23336
+ return searchRows(rows, needle, { worktreeId: c.id, archived: c.archived });
23337
+ }));
23338
+ for (const key of this.said.keys())
23339
+ if (!read.has(key))
23340
+ this.said.delete(key);
23341
+ const all = found.filter((f) => f.hits.length > 0).sort((a, b) => b.hits[0].ts - a.hits[0].ts).flatMap((f) => f.hits);
23342
+ return { hits: all.slice(0, CHAT_HITS_MAX), truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more) };
23343
+ }
23344
+ async rowsOf(id, archived, path, read) {
23345
+ const live = archived ? undefined : this.d.live(id);
23346
+ if (live) {
23347
+ const key = `live:${id}`;
23348
+ read.add(key);
23349
+ return this.remember(key, `n:${live.length}`, () => saidRows(live));
23350
+ }
23351
+ read.add(path);
23352
+ const fs = this.d.fs ?? bunFs2;
23353
+ const at = await fs.stat(path);
23354
+ if (!at)
23355
+ return [];
23356
+ const sig = `${at.mtimeMs}:${at.size}`;
23357
+ const known = this.said.get(path);
23358
+ if (known?.sig === sig)
23359
+ return known.rows;
23360
+ const text = await fs.text(path);
23361
+ return text === null ? [] : this.remember(path, sig, () => saidRows(parseTranscript(text, id)));
23362
+ }
23363
+ remember(key, sig, rows) {
23364
+ const known = this.said.get(key);
23365
+ if (known?.sig === sig)
23366
+ return known.rows;
23367
+ const fresh = rows();
23368
+ this.said.set(key, { sig, rows: fresh });
23369
+ return fresh;
23370
+ }
23371
+ }
23372
+
22449
23373
  // ../daemon/src/worktrees/landing.ts
22450
23374
  var TAIL_CHARS = 400;
22451
23375
  var DIFF_CHARS = 2000;
@@ -22918,11 +23842,11 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
22918
23842
  }
22919
23843
 
22920
23844
  // ../daemon/src/worktrees/service.ts
22921
- import { existsSync as existsSync22, lstatSync, readFileSync as readFileSync14, readlinkSync, rmSync as rmSync3, symlinkSync, unlinkSync } from "fs";
23845
+ import { existsSync as existsSync22, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
22922
23846
  import { basename as basename6, dirname as dirname8, join as join21 } from "path";
22923
23847
 
22924
23848
  // ../daemon/src/git/archive.ts
22925
- import { rmSync } from "fs";
23849
+ import { rmSync as rmSync3 } from "fs";
22926
23850
  var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
22927
23851
  async function keepState(repoPath, wtPath, ref, indexFile) {
22928
23852
  const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
@@ -22959,7 +23883,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
22959
23883
  ], wtPath);
22960
23884
  return commit.ok ? commit.out : null;
22961
23885
  } finally {
22962
- rmSync(indexFile, { force: true });
23886
+ rmSync3(indexFile, { force: true });
22963
23887
  }
22964
23888
  }
22965
23889
  async function checkOutKept(repoPath, wtPath, kept, branch) {
@@ -23186,42 +24110,8 @@ function compareUrl(remoteUrl, base, branch) {
23186
24110
  return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
23187
24111
  }
23188
24112
 
23189
- // ../daemon/src/runtime/setup.ts
23190
- var SETUP_RING = 64 * 1024;
23191
- var SETUP_COLS = 120;
23192
- var SETUP_ROWS = 30;
23193
- function runSetup(cmd, cwd, onLine, extra = {}) {
23194
- return new Promise((resolve) => {
23195
- const lines = new LineSplitter;
23196
- try {
23197
- new PtyStream({
23198
- cwd,
23199
- env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
23200
- cols: SETUP_COLS,
23201
- rows: SETUP_ROWS,
23202
- file: "sh",
23203
- args: ["-c", cmd],
23204
- ring: SETUP_RING
23205
- }, (data) => {
23206
- for (const line of lines.feed(data))
23207
- onLine(line);
23208
- }, resolve);
23209
- } catch (e) {
23210
- onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
23211
- resolve(1);
23212
- }
23213
- });
23214
- }
23215
- function envStrings2(base) {
23216
- const env = {};
23217
- for (const [k, v] of Object.entries(base))
23218
- if (typeof v === "string")
23219
- env[k] = v;
23220
- return env;
23221
- }
23222
-
23223
24113
  // ../daemon/src/worktrees/archive.ts
23224
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync13, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
24114
+ import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
23225
24115
  import { join as join19 } from "path";
23226
24116
  class WorktreeArchive {
23227
24117
  dir;
@@ -23229,7 +24119,7 @@ class WorktreeArchive {
23229
24119
  constructor(dir) {
23230
24120
  this.dir = dir;
23231
24121
  mkdirSync6(dir, { recursive: true });
23232
- for (const id of readdirSync5(dir)) {
24122
+ for (const id of readdirSync6(dir)) {
23233
24123
  const file = this.filesOf(id).record;
23234
24124
  if (!existsSync21(file))
23235
24125
  continue;
@@ -23248,7 +24138,7 @@ class WorktreeArchive {
23248
24138
  mkdirSync6(at.dir, { recursive: true });
23249
24139
  move(from.transcript, at.transcript);
23250
24140
  move(from.attachments, at.attachments);
23251
- writeFileSync8(at.record, JSON.stringify(rec, null, 2));
24141
+ writeFileSync7(at.record, JSON.stringify(rec, null, 2));
23252
24142
  this.records.set(rec.worktree.id, rec);
23253
24143
  }
23254
24144
  take(id, to) {
@@ -23262,12 +24152,18 @@ class WorktreeArchive {
23262
24152
  delete(id) {
23263
24153
  if (!this.records.has(id))
23264
24154
  return;
23265
- rmSync2(this.filesOf(id).dir, { recursive: true, force: true });
24155
+ rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
23266
24156
  this.records.delete(id);
23267
24157
  }
23268
24158
  list(repo) {
23269
24159
  return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id));
23270
24160
  }
24161
+ chatFiles(id) {
24162
+ if (!this.records.has(id))
24163
+ return;
24164
+ const at = this.filesOf(id);
24165
+ return { transcript: at.transcript, attachments: at.attachments };
24166
+ }
23271
24167
  filesOf(id) {
23272
24168
  const dir = join19(this.dir, id);
23273
24169
  return {
@@ -23284,14 +24180,32 @@ function summarize(r, repoId) {
23284
24180
  repoId,
23285
24181
  title: r.worktree.title,
23286
24182
  branch: r.worktree.branch,
24183
+ path: r.worktree.path,
23287
24184
  createdAt: r.worktree.createdAt,
23288
24185
  archivedAt: r.archivedAt,
23289
24186
  ...r.prompt ? { prompt: r.prompt } : {},
23290
24187
  restorable: !!r.kept,
23291
24188
  ...r.kept?.snapshot ? { uncommitted: true } : {},
23292
- ...r.worktree.landed ? { landed: true } : {}
24189
+ ...r.worktree.landed ? { landed: true } : {},
24190
+ ...r.cost !== undefined ? { cost: r.cost } : {}
23293
24191
  };
23294
24192
  }
24193
+ function lastUsage(transcript) {
24194
+ if (!existsSync21(transcript))
24195
+ return null;
24196
+ const lines = readFileSync13(transcript, "utf8").split(`
24197
+ `);
24198
+ for (let i = lines.length - 1;i >= 0; i--) {
24199
+ if (!lines[i]?.includes('"usage"'))
24200
+ continue;
24201
+ try {
24202
+ const e = JSON.parse(lines[i]).event;
24203
+ if (e?.type === "usage")
24204
+ return { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
24205
+ } catch {}
24206
+ }
24207
+ return null;
24208
+ }
23295
24209
  function firstPrompt(transcript) {
23296
24210
  if (!existsSync21(transcript))
23297
24211
  return;
@@ -23310,8 +24224,8 @@ function firstPrompt(transcript) {
23310
24224
  function move(from, to) {
23311
24225
  if (!existsSync21(from))
23312
24226
  return;
23313
- rmSync2(to, { recursive: true, force: true });
23314
- renameSync4(from, to);
24227
+ rmSync4(to, { recursive: true, force: true });
24228
+ renameSync3(from, to);
23315
24229
  }
23316
24230
 
23317
24231
  // ../daemon/src/worktrees/spare.ts
@@ -23442,12 +24356,14 @@ class SparePool {
23442
24356
  wt.title = slug;
23443
24357
  wt.createdAt = Date.now();
23444
24358
  this.d.state.save();
24359
+ fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
23445
24360
  fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
23446
24361
  return wt;
23447
24362
  }
23448
24363
  }
23449
24364
 
23450
24365
  // ../daemon/src/worktrees/turns.ts
24366
+ var TURN_HOLD = "turn";
23451
24367
  var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
23452
24368
 
23453
24369
  class TurnService {
@@ -23485,11 +24401,14 @@ class TurnService {
23485
24401
  onStatus(worktreeId, status) {
23486
24402
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
23487
24403
  this.lastStatus.set(worktreeId, status);
23488
- if (status === "working")
24404
+ if (status === "working") {
23489
24405
  this.disarm(worktreeId);
24406
+ this.d.hold?.(worktreeId, TURN_HOLD);
24407
+ }
23490
24408
  const edge = stopOf(prev, status);
23491
24409
  if (!edge)
23492
24410
  return;
24411
+ this.d.release?.(worktreeId, TURN_HOLD);
23493
24412
  const wt = this.d.state.worktree(worktreeId);
23494
24413
  if (!wt || wt.kind === "spare")
23495
24414
  return;
@@ -23599,6 +24518,15 @@ function withoutAttachments(event) {
23599
24518
  const { attachments: _attachments, ...rest } = event;
23600
24519
  return rest;
23601
24520
  }
24521
+ function withCarry(context, c) {
24522
+ if (c.count === 0)
24523
+ return context;
24524
+ const files = `${c.count} uncommitted ${c.count === 1 ? "file" : "files"}`;
24525
+ const line = c.moved ? `[This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.]` : `[${c.branch} has ${files} the person chose to leave there. This worktree does not have them.]`;
24526
+ return context ? `${context}
24527
+
24528
+ ${line}` : line;
24529
+ }
23602
24530
  var FETCH_EVERY_MS = 5 * 60000;
23603
24531
 
23604
24532
  class WorktreeService {
@@ -23647,46 +24575,48 @@ class WorktreeService {
23647
24575
  const repo = this.d.state.requireRepo(repoId);
23648
24576
  const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
23649
24577
  const profile = this.checkProfile(repo, opts.profile);
23650
- let slug = variant ? `${slugify2(prompt, false)}-v${variant.index}` : slugify2(prompt);
24578
+ const task = taskText(prompt, attachments);
24579
+ let slug = variant ? `${slugify2(task, false)}-v${variant.index}` : slugify2(task);
23651
24580
  if (variant && (await git(repo.path, "show-ref", "--verify", `refs/heads/toyon/${slug}`)).ok) {
23652
24581
  slug = `${slug}-${shortId().slice(0, 3)}`;
23653
24582
  }
23654
24583
  const branch = `toyon/${slug}`;
23655
- const base = opts.baseWorktreeId ? this.d.state.worktree(opts.baseWorktreeId) : undefined;
23656
- const fromMain = !base || base.kind === "main";
23657
- const agentPrompt = variant && variant.of >= 2 ? `${prompt}
24584
+ if (opts.carry && variant && variant.of > 1) {
24585
+ throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
24586
+ }
24587
+ const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
23658
24588
 
23659
- ${variantLens(variant.index)}` : prompt;
23660
- if (fromMain) {
23661
- const claimed = await this.spare.claim(repoId, branch, slug);
23662
- if (claimed) {
23663
- if (variant)
23664
- claimed.variant = variant;
23665
- if (opts.createdBy)
23666
- claimed.createdBy = opts.createdBy;
23667
- claimed.promptedAt = claimed.createdAt;
23668
- claimed.agent = agent;
23669
- if (opts.mode)
23670
- claimed.mode = opts.mode;
23671
- if (opts.model)
23672
- claimed.model = opts.model;
23673
- if (opts.effort)
23674
- claimed.effort = opts.effort;
23675
- this.refreshLink(claimed);
23676
- if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
23677
- claimed.profile = profile;
23678
- this.restartProcs(claimed, repo);
23679
- }
23680
- this.d.state.save();
23681
- this.d.hub.emit("worktreesChanged");
23682
- this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context, attachments });
23683
- this.scheduleNaming(claimed, prompt, variant);
23684
- return claimed;
24589
+ `) : prompt;
24590
+ const claimed = await this.spare.claim(repoId, branch, slug);
24591
+ if (claimed) {
24592
+ if (variant)
24593
+ claimed.variant = variant;
24594
+ if (opts.createdBy)
24595
+ claimed.createdBy = opts.createdBy;
24596
+ claimed.promptedAt = claimed.createdAt;
24597
+ claimed.agent = agent;
24598
+ if (opts.mode)
24599
+ claimed.mode = opts.mode;
24600
+ if (opts.model)
24601
+ claimed.model = opts.model;
24602
+ if (opts.effort)
24603
+ claimed.effort = opts.effort;
24604
+ this.refreshLink(claimed);
24605
+ if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
24606
+ claimed.profile = profile;
24607
+ this.restartProcs(claimed, repo);
23685
24608
  }
24609
+ const carried = await this.carryMain(repo, claimed, !!opts.carry);
24610
+ this.d.state.save();
24611
+ this.d.hub.emit("worktreesChanged");
24612
+ this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
24613
+ this.scheduleNaming(claimed, task, variant);
24614
+ if (carried.unmoved)
24615
+ throw new UserError(carried.unmoved);
24616
+ return claimed;
23686
24617
  }
23687
24618
  const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
23688
- const baseBranch = fromMain ? repo.defaultBranch : base.branch;
23689
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, baseBranch));
24619
+ await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
23690
24620
  const wt = {
23691
24621
  id: shortId(),
23692
24622
  repoId,
@@ -23705,11 +24635,56 @@ ${variantLens(variant.index)}` : prompt;
23705
24635
  ...opts.model ? { model: opts.model } : {},
23706
24636
  ...opts.effort ? { effort: opts.effort } : {}
23707
24637
  };
23708
- this.launch(wt, repo, base?.path ?? repo.path);
23709
- this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context, attachments });
23710
- this.scheduleNaming(wt, prompt, variant);
24638
+ const carried = await this.carryMain(repo, wt, !!opts.carry);
24639
+ this.launch(wt, repo, repo.path);
24640
+ this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
24641
+ this.scheduleNaming(wt, task, variant);
24642
+ if (carried.unmoved)
24643
+ throw new UserError(carried.unmoved);
23711
24644
  return wt;
23712
24645
  }
24646
+ async carryMain(repo, wt, move) {
24647
+ const main = repo.defaultBranch;
24648
+ if (!move)
24649
+ return { branch: main, moved: false, count: (await statusFiles(repo.path)).length };
24650
+ let count = 0;
24651
+ const unmoved = await withRepoLock(repo.path, async () => {
24652
+ count = (await statusFiles(repo.path)).length;
24653
+ if (count === 0)
24654
+ return;
24655
+ const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
24656
+ const pushed = await git(repo.path, "stash", "push", "--include-untracked", "-m", `toyon: into ${wt.title}`);
24657
+ if (!pushed.ok)
24658
+ return `the changes on ${main} could not be moved: ${pushed.err.trim()}`;
24659
+ const after = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
24660
+ const sha = after.out.trim();
24661
+ if (!after.ok || before.ok && before.out.trim() === sha)
24662
+ return;
24663
+ const entry = async () => {
24664
+ const list = await git(repo.path, "stash", "list", "--format=%H");
24665
+ const i = list.out.split(`
24666
+ `).indexOf(sha);
24667
+ return i < 0 ? null : `stash@{${i}}`;
24668
+ };
24669
+ const applied = await git(wt.path, "stash", "apply", sha);
24670
+ if (applied.ok) {
24671
+ const ref = await entry();
24672
+ if (ref)
24673
+ await git(repo.path, "stash", "drop", ref);
24674
+ return;
24675
+ }
24676
+ log.warn(wt.id, `moving ${main}'s changes failed`, applied.err);
24677
+ await git(wt.path, "reset", "--hard");
24678
+ await git(wt.path, "clean", "-fd");
24679
+ const ref = await entry();
24680
+ const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
24681
+ return back?.ok ? `the changes on ${main} did not apply in ${wt.title}, so they stayed on ${main}` : `the changes on ${main} did not apply in ${wt.title}; they are kept in git stash as "toyon: into ${wt.title}"`;
24682
+ });
24683
+ const mainWt = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
24684
+ if (mainWt)
24685
+ this.countsCache.delete(mainWt.id);
24686
+ return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
24687
+ }
23713
24688
  launch(wt, repo, depsSource, opts = {}) {
23714
24689
  this.d.state.addWorktree(wt);
23715
24690
  this.d.hub.emit("worktreesChanged");
@@ -23859,6 +24834,8 @@ ${variantLens(variant.index)}` : prompt;
23859
24834
  this.d.hub.emit("worktreesChanged");
23860
24835
  }
23861
24836
  scheduleNaming(wt, prompt, variant) {
24837
+ if (!prompt.trim())
24838
+ return;
23862
24839
  if (!variant) {
23863
24840
  fireAndForget(wt.id, (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt).then((name) => {
23864
24841
  if (name)
@@ -23919,12 +24896,14 @@ ${variantLens(variant.index)}` : prompt;
23919
24896
  archiveChat(wt, repo, kept, sessionId) {
23920
24897
  const files = this.chatFiles(wt.id);
23921
24898
  const prompt = firstPrompt(files.transcript);
24899
+ const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
23922
24900
  const rec = {
23923
24901
  worktree: wt,
23924
24902
  repoPath: repo.path,
23925
24903
  archivedAt: Date.now(),
23926
24904
  ...sessionId ? { sessionId } : {},
23927
24905
  ...prompt ? { prompt } : {},
24906
+ ...cost !== undefined ? { cost } : {},
23928
24907
  ...kept ? { kept } : {}
23929
24908
  };
23930
24909
  try {
@@ -23945,8 +24924,8 @@ ${variantLens(variant.index)}` : prompt;
23945
24924
  deleteChat(worktreeId) {
23946
24925
  const files = this.chatFiles(worktreeId);
23947
24926
  try {
23948
- rmSync3(files.transcript, { force: true });
23949
- rmSync3(files.attachments, { recursive: true, force: true });
24927
+ rmSync5(files.transcript, { force: true });
24928
+ rmSync5(files.attachments, { recursive: true, force: true });
23950
24929
  } catch (e) {
23951
24930
  log.warn(worktreeId, "could not delete transcript or attachments", e);
23952
24931
  }
@@ -23975,10 +24954,27 @@ ${variantLens(variant.index)}` : prompt;
23975
24954
  const repo = this.d.state.repo(repoId);
23976
24955
  return repo ? this.archive.list(repo) : [];
23977
24956
  }
24957
+ archivedChats(repoId) {
24958
+ const repo = this.d.state.repo(repoId);
24959
+ if (!repo)
24960
+ return [];
24961
+ return this.archive.list(repo).flatMap((a) => {
24962
+ const files = this.archive.chatFiles(a.id);
24963
+ return files ? [{ id: a.id, transcript: files.transcript }] : [];
24964
+ });
24965
+ }
24966
+ archivedTranscript(archiveId) {
24967
+ const files = this.archive.chatFiles(archiveId);
24968
+ return files ? new Transcript(files.transcript, archiveId).entries : null;
24969
+ }
24970
+ archivedAttachment(archiveId, file) {
24971
+ const files = this.archive.chatFiles(archiveId);
24972
+ return files && isAttachmentFile(file) ? join21(files.attachments, file) : null;
24973
+ }
23978
24974
  repoOf(rec) {
23979
24975
  return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
23980
24976
  }
23981
- async restore(archiveId, createdBy) {
24977
+ async restore(archiveId, createdBy, message) {
23982
24978
  const rec = this.archive.get(archiveId);
23983
24979
  if (!rec)
23984
24980
  throw new UserError("that archived worktree is gone");
@@ -24038,6 +25034,8 @@ ${variantLens(variant.index)}` : prompt;
24038
25034
  this.countsCache.delete(wt.id);
24039
25035
  this.refreshLink(wt);
24040
25036
  this.launch(wt, repo, repo.path);
25037
+ if (message)
25038
+ this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
24041
25039
  this.d.hub.emit("archiveChanged", repo.id);
24042
25040
  return wt;
24043
25041
  }
@@ -24163,7 +25161,7 @@ ${variantLens(variant.index)}` : prompt;
24163
25161
  for (const w of sources) {
24164
25162
  const entries = this.d.runtime.ensureAgent(w).agent.transcript();
24165
25163
  agent.note({ type: "grafted", title: w.title, branch: w.branch, ts: Date.now() });
24166
- for (const { event } of entries.slice(cutPoint(entries)))
25164
+ for (const { event } of coalesce(entries))
24167
25165
  agent.note(withoutAttachments(event));
24168
25166
  }
24169
25167
  for (const w of sources)
@@ -24173,7 +25171,15 @@ ${variantLens(variant.index)}` : prompt;
24173
25171
  this.d.hub.emit("worktreesChanged");
24174
25172
  return { target, grafted: sources.map((w) => w.title) };
24175
25173
  }
24176
- async setupAndStart(wt, repo, depsSource = repo.path, { setupCommands = true } = {}) {
25174
+ async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
25175
+ this.d.runtime.markSetup(wt.id, true);
25176
+ try {
25177
+ await this.setupThenStart(wt, repo, depsSource, opts);
25178
+ } finally {
25179
+ this.d.runtime.markSetup(wt.id, false);
25180
+ }
25181
+ }
25182
+ async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
24177
25183
  const srcNm = join21(depsSource, "node_modules");
24178
25184
  const dstNm = join21(wt.path, "node_modules");
24179
25185
  if (existsSync22(srcNm) && !existsSync22(dstNm)) {
@@ -24430,21 +25436,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24430
25436
  const known = this.usage.get(worktreeId);
24431
25437
  if (known !== undefined)
24432
25438
  return known ?? undefined;
24433
- let found = null;
24434
- const file = transcriptPathFor(this.d.paths.transcriptsDir, worktreeId);
24435
- if (existsSync22(file)) {
24436
- const lines = readFileSync14(file, "utf8").split(`
24437
- `);
24438
- for (let i = lines.length - 1;i >= 0 && !found; i--) {
24439
- if (!lines[i]?.includes('"usage"'))
24440
- continue;
24441
- try {
24442
- const e = JSON.parse(lines[i]).event;
24443
- if (e?.type === "usage")
24444
- found = { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
24445
- } catch {}
24446
- }
24447
- }
25439
+ const found = lastUsage(transcriptPathFor(this.d.paths.transcriptsDir, worktreeId));
24448
25440
  this.usage.set(worktreeId, found);
24449
25441
  return found ?? undefined;
24450
25442
  }
@@ -24651,7 +25643,10 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
24651
25643
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
24652
25644
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
24653
25645
  var here = dirname9(fileURLToPath(import.meta.url));
24654
- var { shellDist: SHELL_DIST, bridgeJs: BRIDGE_JS } = locateAssets(here);
25646
+ var { shellDist: SHELL_DIST, bridgeJs: BRIDGE_JS, sourceRoot: SOURCE_ROOT } = locateAssets(here);
25647
+ var pruned = pruneAssets(SHELL_DIST);
25648
+ if (pruned > 0)
25649
+ log.debug("daemon", `pruned ${pruned} assets from builds this one has outlived`);
24655
25650
  var paths = makePaths();
24656
25651
  ensureDirs(paths);
24657
25652
  var token = loadOrCreateToken(paths);
@@ -24702,7 +25697,9 @@ var turns = new TurnService({
24702
25697
  state,
24703
25698
  hub,
24704
25699
  transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
24705
- summarize: makeRecapper(runtime, agents, state)
25700
+ summarize: makeRecapper(runtime, agents, state),
25701
+ hold: (id, tag) => runtime.hold(id, tag),
25702
+ release: (id, tag) => runtime.release(id, tag)
24706
25703
  });
24707
25704
  var files = new FileService(state, runtime, (id) => worktrees.readable(id));
24708
25705
  var design = new DesignService((id) => worktrees.readable(id));
@@ -24717,8 +25714,24 @@ new LandingService({
24717
25714
  judge: makeLander(runtime, agents, state)
24718
25715
  });
24719
25716
  var refs = new RefSearch({ state });
25717
+ var chats = new ChatSearch({
25718
+ state,
25719
+ live: (id) => runtime.agentFor(id)?.transcript(),
25720
+ transcriptPath: (id) => transcriptPathFor(paths.transcriptsDir, id),
25721
+ archivedChats: (repoId) => worktrees.archivedChats(repoId)
25722
+ });
24720
25723
  var prs = new PrService({ state, hub, worktrees, view: viewPr });
24721
- var repos = new RepoRegistry({ state, hub, runtime, worktrees });
25724
+ var self = new SelfWatch(SOURCE_ROOT);
25725
+ await self.start();
25726
+ var afterLand = new AfterLand({ state, hub, self });
25727
+ var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self });
25728
+ var idle = new IdlePolicy({
25729
+ runtime,
25730
+ state,
25731
+ hub,
25732
+ wake: (id) => repos.touch(id),
25733
+ warmSpare: (repoId) => repos.warm(repoId)
25734
+ });
24722
25735
  var themes = new ThemeStore({ get: () => state.theme, set: (p) => state.setTheme(p) }, paths.themesDir);
24723
25736
  themes.load();
24724
25737
  var { branded, stop: stopServer } = startServer({
@@ -24738,13 +25751,24 @@ var { branded, stop: stopServer } = startServer({
24738
25751
  design,
24739
25752
  routes,
24740
25753
  runtime,
25754
+ idle,
24741
25755
  exec,
24742
25756
  refs,
25757
+ chats,
24743
25758
  prs,
24744
25759
  themes,
24745
25760
  agents,
24746
25761
  accounts,
24747
25762
  attachments: attachments2,
25763
+ self,
25764
+ afterLand,
25765
+ restart: () => {
25766
+ const can = restartable();
25767
+ if (!can.ok)
25768
+ return can.reason;
25769
+ fireAndForget("daemon", shutdown("restart", { respawn: true }), "restart");
25770
+ return null;
25771
+ },
24748
25772
  planTasks: makePlanner(agents),
24749
25773
  folderDialog: folderDialog()
24750
25774
  }
@@ -24760,10 +25784,11 @@ bridge.setShellOrigins([
24760
25784
  ],
24761
25785
  ...remote ? [`https://${remote.host}`] : []
24762
25786
  ]);
24763
- writeFileSync9(paths.pidFile, `${process.pid}
25787
+ writeFileSync8(paths.pidFile, `${process.pid}
24764
25788
  `);
24765
25789
  var stopLagSampler = startLagSampler();
24766
25790
  await repos.boot();
25791
+ idle.boot();
24767
25792
  fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
24768
25793
  var repoArg = process.argv[2];
24769
25794
  if (repoArg) {
@@ -24788,11 +25813,12 @@ if (remote?.front === "edge") {
24788
25813
  }
24789
25814
  }
24790
25815
  var shuttingDown = false;
24791
- async function shutdown(signal) {
25816
+ async function shutdown(signal, opts = {}) {
24792
25817
  if (shuttingDown)
24793
25818
  return;
24794
25819
  shuttingDown = true;
24795
25820
  log.info("daemon", `${signal}: stopping dev servers`);
25821
+ idle.shutdown();
24796
25822
  repos.stopWatchers();
24797
25823
  prs.stop();
24798
25824
  stopLagSampler();
@@ -24800,7 +25826,12 @@ async function shutdown(signal) {
24800
25826
  routes.flush();
24801
25827
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
24802
25828
  await Promise.race([runtime.shutdown(), deadline]);
24803
- rmSync4(paths.pidFile, { force: true });
25829
+ rmSync6(paths.pidFile, { force: true });
25830
+ if (opts.respawn) {
25831
+ await Bun.sleep(250);
25832
+ log.info("daemon", "starting the replacement");
25833
+ respawn(paths.logFile);
25834
+ }
24804
25835
  process.exit(0);
24805
25836
  }
24806
25837
  process.on("SIGINT", () => fireAndForget("daemon", shutdown("SIGINT"), "shutdown"));