toyon 0.16.0 → 0.17.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 (28) hide show
  1. package/README.md +1 -1
  2. package/dist/CHANGELOG.md +35 -0
  3. package/dist/cli.js +114 -47
  4. package/dist/daemon.js +1350 -502
  5. package/dist/shell/assets/{Editor-BATywBIx.js → Editor-BMBnZV2V.js} +175 -175
  6. package/dist/shell/assets/{XTerm-So8-Vafj.js → XTerm-xxm4nCyN.js} +1 -1
  7. package/dist/shell/assets/{cssMode-DlHcrkXj.js → cssMode-DRRmIKjD.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-D3xOnVxc.js → freemarker2-BWXbPnFF.js} +1 -1
  9. package/dist/shell/assets/{grammar-RZJ5oG33.js → grammar-BlgE3B1J.js} +1 -1
  10. package/dist/shell/assets/{handlebars-UbIG7080.js → handlebars-swdzvdWB.js} +1 -1
  11. package/dist/shell/assets/{html-D92eLK6U.js → html-GPVhANjL.js} +1 -1
  12. package/dist/shell/assets/{htmlMode-DsZIJzoj.js → htmlMode-BTtuR8kD.js} +1 -1
  13. package/dist/shell/assets/index-9aG0ri4Y.css +1 -0
  14. package/dist/shell/assets/index-miqqiN7Z.js +269 -0
  15. package/dist/shell/assets/{javascript-DUkgFtV2.js → javascript-CFXn2iKE.js} +1 -1
  16. package/dist/shell/assets/{jsonMode-BiIooPg9.js → jsonMode-wmrRCrSu.js} +1 -1
  17. package/dist/shell/assets/{liquid-DFFjZ5TH.js → liquid-Bz7Svg4V.js} +1 -1
  18. package/dist/shell/assets/{lspLanguageFeatures-C7Ug4qkg.js → lspLanguageFeatures-BDP4BowE.js} +1 -1
  19. package/dist/shell/assets/{mdx-DdjSgAuG.js → mdx-CzERTPdi.js} +1 -1
  20. package/dist/shell/assets/{python-DPUF4OK9.js → python-3OsSAVmo.js} +1 -1
  21. package/dist/shell/assets/{razor-IRYJ0pOA.js → razor-BKY4SJuy.js} +1 -1
  22. package/dist/shell/assets/{tsMode-CljE7MPO.js → tsMode-BESW693Z.js} +1 -1
  23. package/dist/shell/assets/{xml-D8Zv3Gd0.js → xml-SaTEQQ9V.js} +1 -1
  24. package/dist/shell/assets/{yaml-Dxx9PL0B.js → yaml-gHi84-nk.js} +1 -1
  25. package/dist/shell/index.html +2 -2
  26. package/package.json +1 -1
  27. package/dist/shell/assets/index-BvYzBRq_.css +0 -1
  28. package/dist/shell/assets/index-CHQwm2uN.js +0 -268
package/dist/daemon.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // @bun
2
2
  // ../daemon/src/index.ts
3
3
  import { rmSync as rmSync9, writeFileSync as writeFileSync13 } from "fs";
4
- import { dirname as dirname13, join as join31 } from "path";
4
+ import { homedir as homedir15 } from "os";
5
+ import { dirname as dirname13, join as join32 } from "path";
5
6
  import { fileURLToPath } from "url";
6
7
 
7
8
  // ../shared/src/protocol/events.ts
@@ -342,6 +343,8 @@ var APP_SCHEME = "toyon";
342
343
  var START_LINK = `${APP_SCHEME}://start`;
343
344
  var OFFLINE_SLOTS = { start: "data-start", port: "data-port" };
344
345
  var LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
346
+ var FD_LIMIT_LOW = 4096;
347
+ var fdLimitLine = (n) => `open-file limit is ${n}; raise it with ulimit -n 8192 before toyon starts`;
345
348
  var DAEMON_FILES = {
346
349
  token: "token",
347
350
  helper: "Toyon.app",
@@ -6598,7 +6601,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6598
6601
  }
6599
6602
 
6600
6603
  // ../shared/src/protocol/limits.ts
6601
- var PROTOCOL_VERSION = 112;
6604
+ var PROTOCOL_VERSION = 115;
6602
6605
  var PAGE_ERROR_MAX_CHARS = 2000;
6603
6606
  var PAGE_TITLE_MAX_CHARS = 1000;
6604
6607
  var FILE_MAX_CHARS = 5000000;
@@ -6947,6 +6950,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6947
6950
  object({ t: literal("list-files"), worktreeId: id }),
6948
6951
  object({ t: literal("list-commands"), worktreeId: id }),
6949
6952
  object({ t: literal("search"), worktreeId: id, query: string2().max(500) }),
6953
+ object({ t: literal("search-stop") }),
6950
6954
  object({ t: literal("find-element"), worktreeId: id, seq, element: elementTraitsSchema }),
6951
6955
  object({ t: literal("design-scan"), worktreeId: id }),
6952
6956
  object({ t: literal("discard-files"), worktreeId: id, paths: array(relPath).min(1).max(500) }),
@@ -7011,6 +7015,7 @@ var clientMsgSchema = discriminatedUnion("t", [
7011
7015
  start: string2().max(4000),
7012
7016
  purpose: _enum(["location", "open"])
7013
7017
  }),
7018
+ object({ t: literal("choose-file"), start: string2().max(4000) }),
7014
7019
  object({ t: literal("cancel-folder") }),
7015
7020
  object({ t: literal("forget-repo"), repoId: id }),
7016
7021
  object({ t: literal("set-theme"), prefs: themePrefsSchema }),
@@ -8514,7 +8519,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8514
8519
  // ../daemon/package.json
8515
8520
  var package_default = {
8516
8521
  name: "@toyon/daemon",
8517
- version: "0.16.0",
8522
+ version: "0.17.0",
8518
8523
  private: true,
8519
8524
  type: "module",
8520
8525
  main: "src/index.ts",
@@ -12617,17 +12622,26 @@ function envLevel() {
12617
12622
  return v && v in LEVELS ? v : "info";
12618
12623
  }
12619
12624
  var threshold = LEVELS[envLevel()];
12620
- function fmtExtra(extra) {
12625
+ var BUGS = [TypeError, RangeError, ReferenceError, SyntaxError];
12626
+ function fmtError(level, e) {
12627
+ const line = `${e.name}: ${e.message}`;
12628
+ const whole = level === "error" || threshold <= LEVELS.debug || BUGS.some((c) => e instanceof c);
12629
+ return whole ? e.stack ?? line : line;
12630
+ }
12631
+ function fmtExtra(level, extra) {
12621
12632
  if (extra === undefined)
12622
12633
  return "";
12623
12634
  if (extra instanceof Error)
12624
- return ` ${extra.stack ?? extra.message}`;
12635
+ return ` ${fmtError(level, extra)}`;
12625
12636
  try {
12626
12637
  return ` ${JSON.stringify(extra)}`;
12627
12638
  } catch {
12628
12639
  return ` ${String(extra)}`;
12629
12640
  }
12630
12641
  }
12642
+ function formatLine(level, tag, message, extra, iso = new Date().toISOString()) {
12643
+ return `${iso.slice(11, 19)} ${level.padEnd(5)} [${tag}] ${message}${fmtExtra(level, extra)}`;
12644
+ }
12631
12645
  var lastDay = "";
12632
12646
  function write(level, tag, message, extra) {
12633
12647
  if (LEVELS[level] < threshold)
@@ -12639,7 +12653,7 @@ function write(level, tag, message, extra) {
12639
12653
  process.stderr.write(`-------- ${day} --------
12640
12654
  `);
12641
12655
  }
12642
- process.stderr.write(`${iso.slice(11, 19)} ${level.padEnd(5)} [${tag}] ${message}${fmtExtra(extra)}
12656
+ process.stderr.write(`${formatLine(level, tag, message, extra, iso)}
12643
12657
  `);
12644
12658
  }
12645
12659
  var log = {
@@ -13794,23 +13808,90 @@ import { join as join3 } from "path";
13794
13808
  // ../daemon/src/core/metrics.ts
13795
13809
  var WARN_MS = 50;
13796
13810
  var TICK_MS = 250;
13811
+ var SLEEP_MS = 30000;
13797
13812
  var lastGit = { cmd: "", ms: 0, at: 0 };
13813
+ var gitSpawns = {};
13814
+ function noteSpawn(args) {
13815
+ const key = args.slice(0, 2).join(" ");
13816
+ gitSpawns[key] = (gitSpawns[key] ?? 0) + 1;
13817
+ }
13818
+ var treeReads = { core: 0, extras: 0, superseded: 0, failed: 0 };
13819
+ var syncHot = {};
13820
+ function noteSync(name, ms) {
13821
+ if (ms > (syncHot[name] ?? 0))
13822
+ syncHot[name] = ms;
13823
+ }
13824
+ var limits = { fdLimit: 0 };
13825
+ var lagHist = { over50: 0, over100: 0, over250: 0, over1000: 0 };
13798
13826
  var lag = { last: 0, max: 0, maxCause: null, over: 0 };
13827
+
13828
+ class LagMinute {
13829
+ minute = -1;
13830
+ ticks = 0;
13831
+ worst = 0;
13832
+ git = null;
13833
+ gitMs = null;
13834
+ note(now, late, git) {
13835
+ const minute = Math.floor(now / 60000);
13836
+ let out = null;
13837
+ if (minute !== this.minute) {
13838
+ if (this.ticks > 0)
13839
+ out = { ticks: this.ticks, worst: this.worst, git: this.git, gitMs: this.gitMs };
13840
+ this.minute = minute;
13841
+ this.ticks = 0;
13842
+ this.worst = 0;
13843
+ this.git = null;
13844
+ this.gitMs = null;
13845
+ }
13846
+ if (late > WARN_MS && late <= SLEEP_MS) {
13847
+ this.ticks++;
13848
+ if (late > this.worst) {
13849
+ this.worst = late;
13850
+ this.git = git?.cmd ?? null;
13851
+ this.gitMs = git?.ms ?? null;
13852
+ }
13853
+ }
13854
+ return out;
13855
+ }
13856
+ }
13857
+ function describeLag(l) {
13858
+ const times = l.ticks === 1 ? "once" : `${l.ticks} times`;
13859
+ const cause = l.git ? ` after ${l.git} (${l.gitMs}ms)` : "";
13860
+ return `event loop late ${times} in the last minute, worst ${l.worst}ms${cause}`;
13861
+ }
13799
13862
  function startLagSampler() {
13800
13863
  let expected = Date.now() + TICK_MS;
13864
+ const minute = new LagMinute;
13801
13865
  const t = setInterval(() => {
13802
13866
  const now = Date.now();
13803
13867
  const late = Math.max(0, now - expected);
13804
13868
  expected = now + TICK_MS;
13805
13869
  lag.last = late;
13870
+ const git = lastGit.cmd && now - lastGit.at < TICK_MS + late ? { cmd: lastGit.cmd, ms: lastGit.ms } : null;
13871
+ const line = minute.note(now, late, git);
13872
+ if (line) {
13873
+ log.warn("lag", describeLag(line), {
13874
+ ticks: line.ticks,
13875
+ worst: line.worst,
13876
+ ...line.git ? { git: line.git, gitMs: line.gitMs } : {}
13877
+ });
13878
+ }
13879
+ if (late > SLEEP_MS)
13880
+ return;
13806
13881
  if (late > lag.max) {
13807
13882
  lag.max = late;
13808
- lag.maxCause = now - lastGit.at < TICK_MS + late ? `${lastGit.cmd} (${lastGit.ms}ms)` : null;
13883
+ lag.maxCause = git ? `${git.cmd} (${git.ms}ms)` : null;
13809
13884
  }
13810
- if (late > WARN_MS) {
13885
+ if (late > WARN_MS)
13811
13886
  lag.over++;
13812
- log.warn("lag", `event loop late by ${late}ms`, lastGit.cmd ? { lastGit: lastGit.cmd, ms: lastGit.ms } : undefined);
13813
- }
13887
+ if (late > 50)
13888
+ lagHist.over50++;
13889
+ if (late > 100)
13890
+ lagHist.over100++;
13891
+ if (late > 250)
13892
+ lagHist.over250++;
13893
+ if (late > 1000)
13894
+ lagHist.over1000++;
13814
13895
  }, TICK_MS);
13815
13896
  return () => clearInterval(t);
13816
13897
  }
@@ -13860,8 +13941,13 @@ var NO_PROMPT = {
13860
13941
  GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
13861
13942
  };
13862
13943
  var SPAWN_ENV = { GIT_OPTIONAL_LOCKS: "0" };
13863
- async function run(cmd, args, cwd, env) {
13944
+ var RUN_CEILING_MS = 10 * 60000;
13945
+ async function run(cmd, args, cwd, env, opts = {}) {
13864
13946
  const started = Date.now();
13947
+ const ceiling = opts.timeoutMs ?? RUN_CEILING_MS;
13948
+ let timer;
13949
+ if (cmd === GIT)
13950
+ noteSpawn(args);
13865
13951
  try {
13866
13952
  const p = Bun.spawn([cmd, ...args], {
13867
13953
  cwd,
@@ -13870,17 +13956,31 @@ async function run(cmd, args, cwd, env) {
13870
13956
  stdin: "ignore",
13871
13957
  env: { ...process.env, ...SPAWN_ENV, ...env }
13872
13958
  });
13873
- const [rawOut, err, status] = await Promise.all([
13874
- new Response(p.stdout).text(),
13875
- new Response(p.stderr).text(),
13876
- p.exited
13877
- ]);
13878
- const exit = p.signalCode ?? status;
13879
- return { ok: status === 0, out: rawOut.trim(), rawOut, err: err.trim(), exit };
13959
+ const done = (async () => {
13960
+ const [rawOut, err, status] = await Promise.all([
13961
+ new Response(p.stdout).text(),
13962
+ new Response(p.stderr).text(),
13963
+ p.exited
13964
+ ]);
13965
+ const exit = p.signalCode ?? status;
13966
+ return { ok: status === 0, out: rawOut.trim(), rawOut, err: err.trim(), exit };
13967
+ })();
13968
+ const gaveUp = new Promise((resolve) => {
13969
+ timer = setTimeout(() => {
13970
+ const what = `${cmd === GIT ? "git" : cmd} ${args.slice(0, 3).join(" ")}`;
13971
+ const msg = `gave up after ${describeDuration(ceiling)}`;
13972
+ log.warn("git", `${what} ${msg} in ${cwd}`);
13973
+ p.kill("SIGKILL");
13974
+ resolve({ ok: false, out: "", rawOut: "", err: msg, exit: "timeout" });
13975
+ }, ceiling);
13976
+ });
13977
+ return await Promise.race([done, gaveUp]);
13880
13978
  } catch (e) {
13881
13979
  const msg = e instanceof Error ? e.message : String(e);
13882
13980
  return { ok: false, out: "", rawOut: "", err: msg, exit: msg };
13883
13981
  } finally {
13982
+ if (timer)
13983
+ clearTimeout(timer);
13884
13984
  if (cmd === GIT) {
13885
13985
  lastGit.cmd = `git ${args.slice(0, 3).join(" ")}`;
13886
13986
  lastGit.ms = Date.now() - started;
@@ -13942,6 +14042,80 @@ async function runLive(cmd, args, cwd, opts = {}) {
13942
14042
  }
13943
14043
  }
13944
14044
  }
14045
+ async function runRows(cmd, args, cwd, opts) {
14046
+ const started = Date.now();
14047
+ const rowMax = opts.rowMax ?? 8192;
14048
+ let stopped = false;
14049
+ try {
14050
+ const p = Bun.spawn([cmd, ...args], {
14051
+ cwd,
14052
+ stdout: "pipe",
14053
+ stderr: "pipe",
14054
+ stdin: "ignore",
14055
+ env: { ...process.env, ...SPAWN_ENV }
14056
+ });
14057
+ const reader = p.stdout.getReader();
14058
+ const stop = () => {
14059
+ stopped = true;
14060
+ p.kill();
14061
+ reader.cancel().catch(() => {});
14062
+ };
14063
+ if (opts.signal?.aborted)
14064
+ stop();
14065
+ else
14066
+ opts.signal?.addEventListener("abort", stop, { once: true });
14067
+ const pump = async () => {
14068
+ const dec = new TextDecoder;
14069
+ let buf = "";
14070
+ let skipping = false;
14071
+ while (!stopped) {
14072
+ const { done, value } = await reader.read();
14073
+ if (done)
14074
+ break;
14075
+ buf += dec.decode(value, { stream: true });
14076
+ while (!stopped) {
14077
+ const nl = buf.indexOf(`
14078
+ `);
14079
+ if (nl < 0) {
14080
+ if (skipping)
14081
+ buf = "";
14082
+ else if (buf.length > rowMax) {
14083
+ const keep = buf.slice(0, rowMax);
14084
+ buf = "";
14085
+ skipping = true;
14086
+ if (opts.onRow(keep) === false)
14087
+ stop();
14088
+ }
14089
+ break;
14090
+ }
14091
+ const row = buf.slice(0, nl);
14092
+ buf = buf.slice(nl + 1);
14093
+ if (skipping) {
14094
+ skipping = false;
14095
+ continue;
14096
+ }
14097
+ if (row && opts.onRow(row.length > rowMax ? row.slice(0, rowMax) : row) === false)
14098
+ stop();
14099
+ }
14100
+ }
14101
+ if (!stopped && buf && !skipping)
14102
+ opts.onRow(buf);
14103
+ };
14104
+ const [, err, status] = await Promise.all([pump(), new Response(p.stderr).text(), p.exited]);
14105
+ opts.signal?.removeEventListener("abort", stop);
14106
+ const exit = p.signalCode ?? status;
14107
+ return { ok: status === 0 || stopped, out: "", err: err.trim(), exit, stopped };
14108
+ } catch (e) {
14109
+ const msg = e instanceof Error ? e.message : String(e);
14110
+ return { ok: false, out: "", err: msg, exit: msg, stopped };
14111
+ } finally {
14112
+ if (cmd === GIT) {
14113
+ lastGit.cmd = `git ${args.slice(0, 3).join(" ")}`;
14114
+ lastGit.ms = Date.now() - started;
14115
+ lastGit.at = Date.now();
14116
+ }
14117
+ }
14118
+ }
13945
14119
  function watchHooks(stream, onHook) {
13946
14120
  let failed;
13947
14121
  if (!stream)
@@ -14443,7 +14617,26 @@ function planEdited(cwd, rel, proposed) {
14443
14617
 
14444
14618
  // ../daemon/src/agent/sandbox.ts
14445
14619
  var CACHE_DIRS = [".bun", ".npm", ".cache", ".yarn", ".pnpm-store", "Library/Caches"].map((d) => resolve2(homedir2(), d));
14446
- var AGENT_SETTINGS = [".claude", ".codex", ".opencode", "opencode.json", "opencode.jsonc"];
14620
+ var AGENT_SETTINGS = [
14621
+ ".claude/settings.json",
14622
+ ".claude/settings.local.json",
14623
+ ".claude/hooks",
14624
+ ".claude/agents",
14625
+ ".codex/config.toml",
14626
+ ".codex/hooks.json",
14627
+ ".codex/hooks",
14628
+ ".codex/rules",
14629
+ "opencode.json",
14630
+ "opencode.jsonc",
14631
+ ".opencode/opencode.json",
14632
+ ".opencode/opencode.jsonc",
14633
+ ".opencode/plugin",
14634
+ ".opencode/plugins",
14635
+ ".opencode/tool",
14636
+ ".opencode/tools",
14637
+ ".opencode/agent",
14638
+ ".opencode/agents"
14639
+ ];
14447
14640
  function uniq2(xs) {
14448
14641
  return [...new Set(xs)];
14449
14642
  }
@@ -14588,10 +14781,7 @@ function installErrorLine(stderr) {
14588
14781
  }
14589
14782
  var UNTRUSTED_CA = /UNKNOWN_CERTIFICATE_VERIFICATION_ERROR|SELF_SIGNED_CERT|UNABLE_TO_GET_ISSUER_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/;
14590
14783
  var bunInstall = async (dir) => {
14591
- const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir, {
14592
- NO_COLOR: "1",
14593
- FORCE_COLOR: "0"
14594
- });
14784
+ const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir, { NO_COLOR: "1", FORCE_COLOR: "0" }, { timeoutMs: 30 * 60000 });
14595
14785
  return { ok: r.ok, err: r.ok ? "" : installErrorLine(r.err) };
14596
14786
  };
14597
14787
  var RETRY_AFTER_MS = 30000;
@@ -15676,7 +15866,7 @@ function decide(req, bounds, cwd, mode = "auto", sandboxed = true) {
15676
15866
  kind: "reject",
15677
15867
  tool,
15678
15868
  path: raw,
15679
- reason: "Agent settings are managed by Toyon, not written by the agent."
15869
+ reason: "Agent settings, hooks and plugin code are yours to change, never the agent's."
15680
15870
  };
15681
15871
  }
15682
15872
  if (kind && NON_WRITE_KINDS.has(kind))
@@ -15718,14 +15908,14 @@ var SYSTEM_APPEND = [
15718
15908
  "You are working inside a dedicated git worktree managed by Toyon.",
15719
15909
  "Make every change inside the current working directory and never modify files outside it. Reading files outside it is fine when the user points you there.",
15720
15910
  "When a change belongs in another project Toyon has open rather than in this worktree, do not edit outside it and do not stop at saying so: propose it with the `handoff` tool when you have one, naming the project and writing the message its agent will start from, with what is needed and why, how to reproduce it, and the paths here that depend on it; the user decides on a card, and once it is proposed you finish your turn.",
15721
- "A write Toyon refuses (outside the worktree, or to an agent's own settings such as .claude/ or opencode.json) was refused by Toyon and not by the user, who was never asked. Report it as the boundary that stopped it, never as the user declining.",
15911
+ "A write Toyon refuses (outside the worktree, or to an agent's settings, hooks or plugin code such as .claude/settings.json, .claude/hooks or opencode.json) was refused by Toyon and not by the user, who was never asked. Report it as the boundary that stopped it, never as the user declining.",
15722
15912
  "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
15723
15913
  "Never run `git commit` unless the user explicitly asks you to, and never offer to, through the question tool or otherwise; leave changes uncommitted for the user to review and commit themselves.",
15724
15914
  "Keep the scope tight: do the asked task well, then stop. A follow-up the user might want later is a sentence in your reply, not a question.",
15725
15915
  "When the work is done and the only thing left is whether to do something you recommend, do not end your turn on a prose offer waiting for a yes. Ask it with your question tool when you have one: one question, two options with the recommended one first, each saying what it changes, and typed text allowed so the answer can carry instructions. The card is the first thing the user reads, before any prose of yours, so the question itself states what you propose and why in a sentence or two; a question that is only the yes-or-no leaves them guessing what it is. Ask this way only where you would otherwise have stopped and waited; never to confirm work already asked for.",
15726
15916
  "An answer to a question of yours can carry typed text beside the option picked, and the text outranks the pick. Text that doubts the pick, asks you something or says the user cannot decide is not a yes: answer it in prose and change nothing until they have decided. Text that only adds detail to the pick is an instruction for carrying it out.",
15727
15917
  "When referencing a file in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor, whether it is in this worktree or anywhere else on this machine. A folder, a URL route or a command is not a file: name it in backticks, never as a link.",
15728
- "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
15918
+ "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists; `@<branch>` names another worktree of this project, and the context attached to the message says where its chat is on disk.",
15729
15919
  'Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, teardown (optional) runs once when a worktree is removed, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself; timeouts (optional) gives setup, check and commit each a ceiling as a duration like "30m" (ten minutes each when unset), for a suite that runs longer. A settings.local.json beside it (toyon.local.json at the root) holds one person\'s overrides and is never committed. Both files may carry comments and trailing commas; leave any you find.',
15730
15920
  "Toyon runs those commands itself in every worktree, and the user watches the result live in a preview beside this chat, so never start a dev server or open a browser of your own to check your work. Each message says where this worktree's preview answers; fetch a page from there when you want to see it.",
15731
15921
  "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.",
@@ -15748,7 +15938,7 @@ function fixPrompt(f) {
15748
15938
  case "hook":
15749
15939
  return hookFixPrompt(f.hook);
15750
15940
  case "conflict":
15751
- return conflictFixPrompt(f.base, f.how, !!f.step);
15941
+ return conflictFixPrompt(f.base, f.how, !!f.step, f.carried);
15752
15942
  case "check":
15753
15943
  return checkFixPrompt(f.command);
15754
15944
  case "command":
@@ -15763,6 +15953,8 @@ function fixWhy(f) {
15763
15953
  case "hook":
15764
15954
  return `the ${f.hook} hook refused the ${f.hook.includes("push") ? "push" : "commit"}`;
15765
15955
  case "conflict":
15956
+ if (f.carried)
15957
+ return `the uncommitted changes conflict with ${f.base}`;
15766
15958
  return f.how === "rebase" ? `the branch needs a rebase onto ${f.base}` : `the branch needs ${f.base} merged in`;
15767
15959
  case "check":
15768
15960
  return "the check failed";
@@ -15793,8 +15985,12 @@ function hookFixPrompt(hook) {
15793
15985
  "Do not skip the hook, do not edit it or its configuration to get past it, and do not commit: Toyon runs the check when your turn ends, and the user lands from there."
15794
15986
  ].join(" ");
15795
15987
  }
15796
- function conflictFixPrompt(base, how, shown) {
15988
+ function conflictFixPrompt(base, how, shown, carried) {
15797
15989
  const ask = how === "rebase" ? `Rebase this branch onto ${base} and resolve the conflicts, keeping any uncommitted changes, then verify the app still works.` : `Merge ${base} into this branch, resolve the conflicts and commit the merge, then verify the app still works.`;
15990
+ if (carried) {
15991
+ const where = carried.length ? `, in ${carried.join(", ")}` : "";
15992
+ return `${ask} The branch's own commits take ${base} cleanly; the conflicts are in the uncommitted changes${where}.`;
15993
+ }
15798
15994
  return shown ? `${ask} What git printed when Toyon tried is attached below.` : ask;
15799
15995
  }
15800
15996
  function checkFixPrompt(command) {
@@ -15873,6 +16069,17 @@ function handoffAskPrompt(repo, text) {
15873
16069
  function handoffLandedContext(title, project, url) {
15874
16070
  return `The work this worktree handed off to ${project}, "${title}", has landed there${url ? ` (${url})` : ""}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
15875
16071
  }
16072
+ var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16073
+ function worktreeContext(text, siblings) {
16074
+ const out = [];
16075
+ for (const s of siblings) {
16076
+ const named = new RegExp(`(^|\\s)@${escapeRegExp(s.branch)}(?=$|[\\s.,;:!?)\\]}'"])`);
16077
+ if (!named.test(text))
16078
+ continue;
16079
+ out.push(`@${s.branch} is the worktree "${s.title}" in this project. Its chat is at ${s.transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`);
16080
+ }
16081
+ return out;
16082
+ }
15876
16083
  function ambientBlock(paragraphs) {
15877
16084
  const said = paragraphs.filter((p) => !!p);
15878
16085
  if (said.length === 0)
@@ -16811,7 +17018,13 @@ class AcpSession {
16811
17018
  this.emit({ type: "turn-end", stopReason: mapStopReason(res.stopReason), ts: Date.now() });
16812
17019
  }
16813
17020
  contextFor(item) {
16814
- return ambientBlock([...item.context ?? [], this.d.preview?.(), this.d.pageErrors?.(), ...this.landedSince()]);
17021
+ return ambientBlock([
17022
+ ...item.context ?? [],
17023
+ ...this.d.mentioned?.(item.text) ?? [],
17024
+ this.d.preview?.(),
17025
+ this.d.pageErrors?.(),
17026
+ ...this.landedSince()
17027
+ ]);
16815
17028
  }
16816
17029
  landedSince() {
16817
17030
  const entries = this.log.entries;
@@ -18574,6 +18787,11 @@ function defaultAgent(wt, d, preview, onProcess) {
18574
18787
  cwd: wt.path,
18575
18788
  preview: () => previewContext(preview()),
18576
18789
  pageErrors: () => d.pageErrors?.(wt.id),
18790
+ mentioned: (text) => worktreeContext(text, d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree").map((w) => ({
18791
+ branch: w.branch,
18792
+ title: w.title,
18793
+ transcript: transcriptPathFor(d.paths.transcriptsDir, w.id)
18794
+ }))),
18577
18795
  onProcess,
18578
18796
  spec,
18579
18797
  connect: (app, spec, prepared) => spawnAcp(app, d.agents.launch(spec, prepared), wt.path, wt.id),
@@ -20447,13 +20665,14 @@ class KeepAwake {
20447
20665
  import { existsSync as existsSync16 } from "fs";
20448
20666
  import { homedir as homedir6 } from "os";
20449
20667
  var PROMPTS = {
20450
- location: "Choose where the new project goes",
20451
- open: "Choose a project folder to open"
20668
+ location: { prompt: "Choose where the new project goes", pick: "folder" },
20669
+ open: { prompt: "Choose a project folder to open", pick: "folder" },
20670
+ file: { prompt: "Choose a file to open", pick: "file" }
20452
20671
  };
20453
- var SCRIPT = [
20672
+ var script = (pick) => [
20454
20673
  "on run argv",
20455
20674
  "activate",
20456
- "POSIX path of (choose folder with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))",
20675
+ `POSIX path of (choose ${pick} with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))`,
20457
20676
  "end run"
20458
20677
  ];
20459
20678
  function folderDialog() {
@@ -20464,7 +20683,8 @@ function folderDialog() {
20464
20683
  const typed = start === "~" || start.startsWith("~/") ? `${homedir6()}${start.slice(1)}` : start;
20465
20684
  const from = typed.startsWith("/") && existsSync16(typed) ? typed : homedir6();
20466
20685
  cancelled = false;
20467
- const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
20686
+ const { prompt, pick } = PROMPTS[purpose];
20687
+ const p = Bun.spawn(["osascript", ...script(pick).flatMap((line) => ["-e", line]), prompt, from], {
20468
20688
  stdout: "pipe",
20469
20689
  stderr: "pipe",
20470
20690
  stdin: "ignore"
@@ -20485,7 +20705,7 @@ function folderDialog() {
20485
20705
  return {
20486
20706
  choose(start, purpose) {
20487
20707
  if (process.platform !== "darwin") {
20488
- return Promise.reject(new UserError("choosing a folder in Finder is only available on macOS"));
20708
+ return Promise.reject(new UserError(`choosing a ${PROMPTS[purpose].pick} in Finder is only available on macOS`));
20489
20709
  }
20490
20710
  if (!open) {
20491
20711
  open = ask(start, purpose).finally(() => {
@@ -20933,8 +21153,21 @@ class RemoteSwitch {
20933
21153
 
20934
21154
  // ../daemon/src/core/restart.ts
20935
21155
  import { spawn as spawn4 } from "child_process";
20936
- import { openSync } from "fs";
20937
21156
  import { homedir as homedir7 } from "os";
21157
+
21158
+ // ../shared/src/logfile.ts
21159
+ import { chmodSync, existsSync as existsSync18, openSync, renameSync as renameSync3, statSync as statSync6 } from "fs";
21160
+ var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
21161
+ var previousLog = (path) => `${path}.1`;
21162
+ function openLog(path, maxBytes = LOG_ROTATE_BYTES) {
21163
+ if (existsSync18(path) && statSync6(path).size > maxBytes)
21164
+ renameSync3(path, previousLog(path));
21165
+ const fd = openSync(path, "a", 384);
21166
+ chmodSync(path, 384);
21167
+ return fd;
21168
+ }
21169
+
21170
+ // ../daemon/src/core/restart.ts
20938
21171
  function restartable(env = process.env) {
20939
21172
  if (env.TOYON_WORKTREE !== undefined) {
20940
21173
  return { ok: false, reason: "this daemon runs as a toyon process; restart it from its terminal tab" };
@@ -20952,7 +21185,7 @@ async function replacementRuns() {
20952
21185
  };
20953
21186
  }
20954
21187
  function respawn(logFile) {
20955
- const logFd = openSync(logFile, "a");
21188
+ const logFd = openLog(logFile);
20956
21189
  const child = spawn4(process.execPath, process.argv.slice(1), {
20957
21190
  detached: true,
20958
21191
  stdio: ["ignore", logFd, logFd],
@@ -21095,7 +21328,7 @@ class SelfWatch {
21095
21328
 
21096
21329
  // ../daemon/src/core/state.ts
21097
21330
  import { randomBytes as randomBytes6 } from "crypto";
21098
- import { existsSync as existsSync18, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "fs";
21331
+ import { existsSync as existsSync19, readFileSync as readFileSync12, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "fs";
21099
21332
  var TRUSTED_MAX = 16;
21100
21333
  var DAEMON_GROUPS = "daemon";
21101
21334
  var GROUPS_SAVE_MS = 250;
@@ -21104,7 +21337,7 @@ var empty = { repos: [], worktrees: [], sessions: {} };
21104
21337
  function loadState(paths) {
21105
21338
  ensureDirs(paths);
21106
21339
  const STATE_FILE = paths.stateFile;
21107
- if (!existsSync18(STATE_FILE))
21340
+ if (!existsSync19(STATE_FILE))
21108
21341
  return structuredClone(empty);
21109
21342
  let raw;
21110
21343
  try {
@@ -21151,12 +21384,12 @@ function loadState(paths) {
21151
21384
  function saveState(paths, state) {
21152
21385
  const tmp = `${paths.stateFile}.tmp`;
21153
21386
  writeFileSync8(tmp, JSON.stringify(state, null, 2));
21154
- renameSync3(tmp, paths.stateFile);
21387
+ renameSync4(tmp, paths.stateFile);
21155
21388
  }
21156
21389
  function loadOrCreateToken(paths) {
21157
21390
  ensureDirs(paths);
21158
21391
  const TOKEN_FILE = paths.tokenFile;
21159
- if (existsSync18(TOKEN_FILE))
21392
+ if (existsSync19(TOKEN_FILE))
21160
21393
  return readFileSync12(TOKEN_FILE, "utf8").trim();
21161
21394
  const token = randomBytes6(32).toString("hex");
21162
21395
  writeFileSync8(TOKEN_FILE, token, { mode: 384 });
@@ -21505,7 +21738,7 @@ class Tailnet {
21505
21738
  }
21506
21739
  async start() {
21507
21740
  await this.refresh();
21508
- this.timer = setInterval(() => void this.refresh(), READINESS_EVERY_MS);
21741
+ this.timer = setInterval(() => fireAndForget("tailscale", this.refresh(), "tailscale readiness"), READINESS_EVERY_MS);
21509
21742
  this.timer.unref?.();
21510
21743
  }
21511
21744
  stop() {
@@ -21564,7 +21797,10 @@ class Tailnet {
21564
21797
  const now = (this.d.now ?? Date.now)();
21565
21798
  if (this.status === null || fresh || now - this.statusAt > STATUS_FRESH_MS) {
21566
21799
  this.statusAt = now;
21567
- this.status = ts(["status", "--json"]).then((r) => r.ok ? r.out : r.out || r.err);
21800
+ this.status = ts(["status", "--json"]).then((r) => r.ok ? r.out : r.out || r.err, (e) => {
21801
+ log.warn("tailscale", "could not run tailscale status", e);
21802
+ return "";
21803
+ });
21568
21804
  }
21569
21805
  return this.status;
21570
21806
  }
@@ -22191,7 +22427,7 @@ async function loadProjectTypescript(root) {
22191
22427
  }
22192
22428
 
22193
22429
  // ../daemon/src/drafts/store.ts
22194
- import { existsSync as existsSync19, readFileSync as readFileSync13, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "fs";
22430
+ import { existsSync as existsSync20, readFileSync as readFileSync13, renameSync as renameSync5, writeFileSync as writeFileSync9 } from "fs";
22195
22431
  var SAVE_DELAY_MS = 1000;
22196
22432
 
22197
22433
  class DraftStore {
@@ -22328,7 +22564,7 @@ class DraftStore {
22328
22564
  }
22329
22565
  }
22330
22566
  function readRecord(file) {
22331
- if (!existsSync19(file))
22567
+ if (!existsSync20(file))
22332
22568
  return [];
22333
22569
  try {
22334
22570
  const raw = JSON.parse(readFileSync13(file, "utf8"));
@@ -22342,7 +22578,7 @@ function writeAtomic(file, value) {
22342
22578
  const tmp = `${file}.tmp`;
22343
22579
  try {
22344
22580
  writeFileSync9(tmp, JSON.stringify(value));
22345
- renameSync4(tmp, file);
22581
+ renameSync5(tmp, file);
22346
22582
  } catch (e) {
22347
22583
  log.error("drafts", `could not write ${file}`, e);
22348
22584
  }
@@ -22588,7 +22824,7 @@ import { stat as stat3 } from "fs/promises";
22588
22824
  import { basename as basename6, resolve as resolve7 } from "path";
22589
22825
 
22590
22826
  // ../daemon/src/repos/browse.ts
22591
- import { existsSync as existsSync20, statSync as statSync6 } from "fs";
22827
+ import { existsSync as existsSync21, statSync as statSync7 } from "fs";
22592
22828
  import { readdir } from "fs/promises";
22593
22829
  import { homedir as homedir8 } from "os";
22594
22830
  import { dirname as dirname6, join as join16 } from "path";
@@ -22605,7 +22841,7 @@ function holdsNothing(names) {
22605
22841
  }
22606
22842
  async function describeFolder(abs) {
22607
22843
  const path = collapseTilde(abs);
22608
- if (existsSync20(join16(abs, ".git")))
22844
+ if (existsSync21(join16(abs, ".git")))
22609
22845
  return { path, kind: "project" };
22610
22846
  try {
22611
22847
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -22613,17 +22849,21 @@ async function describeFolder(abs) {
22613
22849
  return { path, kind: "folder" };
22614
22850
  }
22615
22851
  }
22616
- var NOWHERE = { exists: false, isDir: false, isRepo: false, parentExists: false };
22852
+ var NOWHERE = { exists: false, isDir: false, isRepo: false, isFile: false, parentExists: false };
22617
22853
  function describe2(typed, parent) {
22618
22854
  let isDir = false;
22855
+ let isFile = false;
22619
22856
  try {
22620
- isDir = statSync6(typed).isDirectory();
22857
+ const st = statSync7(typed);
22858
+ isDir = st.isDirectory();
22859
+ isFile = st.isFile();
22621
22860
  } catch {}
22622
22861
  return {
22623
- exists: existsSync20(typed),
22862
+ exists: existsSync21(typed),
22624
22863
  isDir,
22625
- isRepo: isDir && existsSync20(join16(typed, ".git")),
22626
- parentExists: existsSync20(parent)
22864
+ isRepo: isDir && existsSync21(join16(typed, ".git")),
22865
+ isFile,
22866
+ parentExists: existsSync21(parent)
22627
22867
  };
22628
22868
  }
22629
22869
  async function browsePath(raw) {
@@ -22634,25 +22874,37 @@ async function browsePath(raw) {
22634
22874
  const dir = listing ? typed : dirname6(typed);
22635
22875
  const prefix = listing ? "" : typed.slice(dir.length).replace(/^\//, "").toLowerCase();
22636
22876
  const target = describe2(listing ? typed.replace(/\/+$/, "") : typed, dir);
22637
- let names;
22877
+ let found;
22638
22878
  try {
22639
- const found = await readdir(dir, { withFileTypes: true });
22640
- names = found.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
22879
+ const listed = await readdir(dir, { withFileTypes: true });
22880
+ found = listed.map((e) => ({
22881
+ name: e.name,
22882
+ isFile: e.isSymbolicLink() ? isFileAt(join16(dir, e.name)) : e.isFile() ? true : e.isDirectory() ? false : null
22883
+ })).filter((e) => e.isFile !== null);
22641
22884
  } catch {
22642
22885
  return { entries: [], target };
22643
22886
  }
22644
22887
  const entries = [];
22645
- for (const name of names) {
22888
+ for (const { name, isFile } of found) {
22646
22889
  if (name.startsWith(".") && !prefix.startsWith("."))
22647
22890
  continue;
22648
22891
  if (!name.toLowerCase().startsWith(prefix))
22649
22892
  continue;
22650
22893
  const abs = join16(dir, name);
22651
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync20(join16(abs, ".git")) });
22894
+ entries.push({ path: collapseTilde(abs), name, isRepo: !isFile && existsSync21(join16(abs, ".git")), isFile });
22652
22895
  }
22653
- entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
22896
+ const rank = (e) => e.isRepo ? 0 : e.isFile ? 2 : 1;
22897
+ entries.sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
22654
22898
  return { entries: entries.slice(0, MAX_ENTRIES), target };
22655
22899
  }
22900
+ function isFileAt(abs) {
22901
+ try {
22902
+ const st = statSync7(abs);
22903
+ return st.isFile() ? true : st.isDirectory() ? false : null;
22904
+ } catch {
22905
+ return null;
22906
+ }
22907
+ }
22656
22908
 
22657
22909
  // ../daemon/src/files/content.ts
22658
22910
  function versionOf(bytes) {
@@ -22855,20 +23107,20 @@ async function numstat(worktreePath, ...range) {
22855
23107
  const r = await git(worktreePath, "diff", "--numstat", "--no-renames", ...range);
22856
23108
  return r.ok ? parseNumstat(r.out) : new Map;
22857
23109
  }
22858
- async function untrackedLines(worktreePath, file) {
23110
+ async function untrackedLines(worktreePath, file, budget) {
22859
23111
  try {
22860
23112
  const f = Bun.file(join17(worktreePath, file));
22861
- if (f.size > 2000000)
23113
+ if (f.size > 2000000 || f.size > budget.left)
22862
23114
  return {};
23115
+ budget.left -= f.size;
22863
23116
  const buf = new Uint8Array(await f.arrayBuffer());
22864
23117
  if (buf.subarray(0, 8000).includes(0))
22865
23118
  return {};
22866
23119
  if (buf.length === 0)
22867
23120
  return { add: 0, del: 0 };
22868
23121
  let n = 0;
22869
- for (const b of buf)
22870
- if (b === 10)
22871
- n++;
23122
+ for (let i = buf.indexOf(10);i !== -1; i = buf.indexOf(10, i + 1))
23123
+ n++;
22872
23124
  if (buf[buf.length - 1] !== 10)
22873
23125
  n++;
22874
23126
  return { add: n, del: 0 };
@@ -22877,16 +23129,38 @@ async function untrackedLines(worktreePath, file) {
22877
23129
  }
22878
23130
  }
22879
23131
  var MAX_UNTRACKED_COUNTED = 500;
22880
- async function statusFilesWithCounts(worktreePath) {
22881
- const { files, folded } = await readStatus(worktreePath, []);
23132
+ var UNTRACKED_BYTES_MAX = 16 * 1024 * 1024;
23133
+ function statusEntries(worktreePath) {
23134
+ return readStatus(worktreePath, []);
23135
+ }
23136
+ async function countEntries(worktreePath, files, folded, bytesMax = UNTRACKED_BYTES_MAX) {
23137
+ const out = new Map;
22882
23138
  if (files.length === 0)
22883
- return files;
22884
- const counts = files.some((f) => f.xy !== "??") ? await numstat(worktreePath, "HEAD") : new Map;
22885
- const countUntracked = files.filter((f) => f.xy === "??").length <= MAX_UNTRACKED_COUNTED;
22886
- return Promise.all(files.map(async (f) => ({
22887
- ...f,
22888
- ...folded.has(f.path) ? {} : f.xy === "??" ? countUntracked ? await untrackedLines(worktreePath, f.path) : {} : counts.get(f.path) ?? {}
22889
- })));
23139
+ return out;
23140
+ const tracked = files.some((f) => f.xy !== "??" && !folded.has(f.path));
23141
+ const counts = tracked ? await numstat(worktreePath, "HEAD") : new Map;
23142
+ const untracked = files.filter((f) => f.xy === "??");
23143
+ const countUntracked = untracked.length <= MAX_UNTRACKED_COUNTED;
23144
+ const budget = { left: bytesMax };
23145
+ for (const f of files) {
23146
+ if (folded.has(f.path)) {
23147
+ if (f.add !== undefined || f.del !== undefined)
23148
+ out.set(f.path, { add: f.add, del: f.del });
23149
+ continue;
23150
+ }
23151
+ if (f.xy !== "??") {
23152
+ const c = counts.get(f.path);
23153
+ if (c)
23154
+ out.set(f.path, c);
23155
+ continue;
23156
+ }
23157
+ if (!countUntracked)
23158
+ continue;
23159
+ const c = await untrackedLines(worktreePath, f.path, budget);
23160
+ if (c.add !== undefined)
23161
+ out.set(f.path, c);
23162
+ }
23163
+ return out;
22890
23164
  }
22891
23165
  async function committedFiles(worktreePath, defaultBr) {
22892
23166
  const base = await git(worktreePath, "merge-base", "HEAD", defaultBr);
@@ -23209,6 +23483,20 @@ async function viteLineOffset(worktreePath, path, target) {
23209
23483
 
23210
23484
  // ../daemon/src/files/service.ts
23211
23485
  var SEARCH_MAX = 300;
23486
+ var SEARCH_PER_FILE = 20;
23487
+ var HIT_TEXT_MAX = 200;
23488
+ function parseGrepRow(row) {
23489
+ const a = row.indexOf("\x00");
23490
+ if (a < 0)
23491
+ return null;
23492
+ const b = row.indexOf("\x00", a + 1);
23493
+ if (b < 0)
23494
+ return null;
23495
+ const line = Number(row.slice(a + 1, b));
23496
+ if (!Number.isInteger(line) || line < 1)
23497
+ return null;
23498
+ return { path: row.slice(0, a), line, text: row.slice(b + 1) };
23499
+ }
23212
23500
  var FIND_ROWS_MAX = 5000;
23213
23501
  var NOT_WHERE_WRITTEN = [
23214
23502
  ":!*.css",
@@ -23282,6 +23570,7 @@ class FileService {
23282
23570
  this.runtime = runtime;
23283
23571
  this.readable = readable;
23284
23572
  }
23573
+ searches = new Map;
23285
23574
  require(worktreeId) {
23286
23575
  const r = this.readable(worktreeId);
23287
23576
  if (!r)
@@ -23361,37 +23650,73 @@ class FileService {
23361
23650
  ]);
23362
23651
  return parseListing(staged.out, untracked.out, deleted.out, ignored.out);
23363
23652
  }
23364
- async search(worktreeId, query) {
23653
+ async search(worktreeId, query, asker) {
23365
23654
  const { path: cwd } = this.require(worktreeId);
23366
23655
  const q = query.trim();
23656
+ if (q.length < 2)
23657
+ return { hits: [], truncated: false };
23658
+ this.searches.get(asker)?.abort();
23659
+ const ctl = new AbortController;
23660
+ this.searches.set(asker, ctl);
23367
23661
  const hits = [];
23368
23662
  let truncated = false;
23369
- if (q.length < 2)
23370
- return { hits, truncated };
23371
- const r = await run(GIT, ["grep", "-n", "-I", "-i", "-F", "--untracked", "--no-color", `--max-count=${SEARCH_MAX}`, "-e", q, "--"], cwd);
23372
- for (const row of r.rawOut.split(`
23373
- `)) {
23374
- if (!row)
23375
- continue;
23376
- const m = /^(.+?):(\d+):(.*)$/.exec(row);
23377
- if (!m)
23378
- continue;
23379
- if (hits.length >= SEARCH_MAX) {
23380
- truncated = true;
23381
- break;
23382
- }
23383
- hits.push({ path: m[1], line: Number(m[2]), text: m[3].trim().slice(0, 200) });
23663
+ let inFile = "";
23664
+ let inFileRows = 0;
23665
+ try {
23666
+ await runRows(GIT, [
23667
+ "grep",
23668
+ "-n",
23669
+ "-z",
23670
+ "-I",
23671
+ "-i",
23672
+ "-F",
23673
+ "--untracked",
23674
+ "--no-color",
23675
+ `--max-count=${SEARCH_PER_FILE + 1}`,
23676
+ "-e",
23677
+ q,
23678
+ "--"
23679
+ ], cwd, {
23680
+ signal: ctl.signal,
23681
+ onRow: (row) => {
23682
+ const hit = parseGrepRow(row);
23683
+ if (!hit)
23684
+ return;
23685
+ if (hit.path !== inFile) {
23686
+ inFile = hit.path;
23687
+ inFileRows = 0;
23688
+ }
23689
+ if (++inFileRows > SEARCH_PER_FILE) {
23690
+ truncated = true;
23691
+ return;
23692
+ }
23693
+ if (hits.length >= SEARCH_MAX) {
23694
+ truncated = true;
23695
+ return false;
23696
+ }
23697
+ hits.push({ ...hit, text: hit.text.trim().slice(0, HIT_TEXT_MAX) });
23698
+ }
23699
+ });
23700
+ } finally {
23701
+ if (this.searches.get(asker) === ctl)
23702
+ this.searches.delete(asker);
23384
23703
  }
23385
- return { hits, truncated };
23704
+ return ctl.signal.aborted ? null : { hits, truncated };
23705
+ }
23706
+ stopSearch(asker) {
23707
+ this.searches.get(asker)?.abort();
23708
+ this.searches.delete(asker);
23386
23709
  }
23387
23710
  async findElement(worktreeId, element) {
23388
23711
  const { path: cwd } = this.require(worktreeId);
23389
23712
  const needles = needlesOf(element);
23390
23713
  if (!needles.length)
23391
23714
  return { hits: [], sure: false };
23392
- const r = await run(GIT, [
23715
+ const rows = [];
23716
+ await runRows(GIT, [
23393
23717
  "grep",
23394
23718
  "-n",
23719
+ "-z",
23395
23720
  "-I",
23396
23721
  "-F",
23397
23722
  "--untracked",
@@ -23400,17 +23725,14 @@ class FileService {
23400
23725
  ...needles.flatMap((n) => ["-e", n.text]),
23401
23726
  "--",
23402
23727
  ...NOT_WHERE_WRITTEN
23403
- ], cwd);
23404
- const rows = [];
23405
- for (const row of r.rawOut.split(`
23406
- `)) {
23407
- const m = /^(.+?):(\d+):(.*)$/.exec(row);
23408
- if (!m)
23409
- continue;
23410
- rows.push({ path: m[1], line: Number(m[2]), text: m[3] });
23411
- if (rows.length >= FIND_ROWS_MAX)
23412
- break;
23413
- }
23728
+ ], cwd, {
23729
+ onRow: (row) => {
23730
+ const hit = parseGrepRow(row);
23731
+ if (hit)
23732
+ rows.push(hit);
23733
+ return rows.length < FIND_ROWS_MAX;
23734
+ }
23735
+ });
23414
23736
  return rankElementSources(element, rows);
23415
23737
  }
23416
23738
  async changedRanges(worktreeId, path) {
@@ -23459,12 +23781,13 @@ class OpenService {
23459
23781
  if (st.isDirectory()) {
23460
23782
  if (!await isGitRepo(path))
23461
23783
  throw new UserError(`${raw} is not a git repository: open one, or a file`);
23462
- return { kind: "repo", repoId: (await this.d.register(path)).id };
23784
+ return { kind: "repo", repoId: (await this.d.register(path)).id, delivered: false };
23463
23785
  }
23464
23786
  const opened = await this.openFile(raw, path, st);
23465
- this.pending.push({ opened, at: this.now() });
23787
+ const entry = { opened, at: this.now() };
23788
+ this.pending.push(entry);
23466
23789
  this.d.hub.emit("opened", opened);
23467
- return opened;
23790
+ return { ...opened, delivered: !this.pending.includes(entry) };
23468
23791
  }
23469
23792
  async file(raw) {
23470
23793
  const { path, st } = await this.locate(raw);
@@ -23892,7 +24215,7 @@ class AfterLand {
23892
24215
  }
23893
24216
 
23894
24217
  // ../daemon/src/repos/registry.ts
23895
- import { existsSync as existsSync24, mkdirSync as mkdirSync7, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
24218
+ import { existsSync as existsSync25, mkdirSync as mkdirSync7, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
23896
24219
  import { homedir as homedir10 } from "os";
23897
24220
  import { basename as basename8, dirname as dirname8, join as join21 } from "path";
23898
24221
 
@@ -23905,7 +24228,7 @@ async function resolveBase(repoPath, defaultBranch, route) {
23905
24228
  }
23906
24229
 
23907
24230
  // ../daemon/src/repos/config.ts
23908
- import { closeSync, existsSync as existsSync21, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync14, readSync, statSync as statSync7 } from "fs";
24231
+ import { closeSync, existsSync as existsSync22, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync14, readSync, statSync as statSync8 } from "fs";
23909
24232
  import { join as join18 } from "path";
23910
24233
 
23911
24234
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -25270,7 +25593,7 @@ function applyEdits(text, edits) {
25270
25593
 
25271
25594
  // ../daemon/src/repos/config.ts
25272
25595
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
25273
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync21(join18(repoPath, rel)));
25596
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync22(join18(repoPath, rel)));
25274
25597
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25275
25598
  function parseSettings(text) {
25276
25599
  const errors = [];
@@ -25355,7 +25678,7 @@ function configTarget(repoPath, made) {
25355
25678
  function configBody(repoPath, rel, config) {
25356
25679
  const place = PLACES.find((p) => p.local === rel);
25357
25680
  const abs = place && join18(repoPath, place.shared);
25358
- if (!abs || !existsSync21(abs))
25681
+ if (!abs || !existsSync22(abs))
25359
25682
  return config;
25360
25683
  let shared;
25361
25684
  try {
@@ -25403,7 +25726,7 @@ function detectConfig(repoPath) {
25403
25726
  if (explicit)
25404
25727
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
25405
25728
  const pkgPath = join18(repoPath, "package.json");
25406
- if (existsSync21(pkgPath)) {
25729
+ if (existsSync22(pkgPath)) {
25407
25730
  const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
25408
25731
  const scripts = pkg.scripts ?? {};
25409
25732
  const runner = detectRunner(repoPath, pkg.packageManager);
@@ -25425,7 +25748,7 @@ function detectConfig(repoPath) {
25425
25748
  };
25426
25749
  }
25427
25750
  }
25428
- if (existsSync21(join18(repoPath, "start.sh"))) {
25751
+ if (existsSync22(join18(repoPath, "start.sh"))) {
25429
25752
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
25430
25753
  }
25431
25754
  const assumed = assumeNothingToRun(repoPath);
@@ -25468,7 +25791,7 @@ function assumeNothingToRun(repoPath) {
25468
25791
  return marker && !servesPage(repoPath) ? marker : undefined;
25469
25792
  }
25470
25793
  function markerIn(repoPath) {
25471
- const named = NO_PAGE_MARKERS.find((f) => existsSync21(join18(repoPath, f)));
25794
+ const named = NO_PAGE_MARKERS.find((f) => existsSync22(join18(repoPath, f)));
25472
25795
  if (named)
25473
25796
  return named;
25474
25797
  let entries;
@@ -25480,7 +25803,7 @@ function markerIn(repoPath) {
25480
25803
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
25481
25804
  }
25482
25805
  function servesPage(repoPath) {
25483
- if (WEB_FILES.some((f) => existsSync21(join18(repoPath, f))))
25806
+ if (WEB_FILES.some((f) => existsSync22(join18(repoPath, f))))
25484
25807
  return true;
25485
25808
  if (/^web:/m.test(head(join18(repoPath, "Procfile"))))
25486
25809
  return true;
@@ -25517,7 +25840,7 @@ function mentions(text, name) {
25517
25840
  return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
25518
25841
  }
25519
25842
  function head(path) {
25520
- if (!existsSync21(path))
25843
+ if (!existsSync22(path))
25521
25844
  return "";
25522
25845
  try {
25523
25846
  const fd = openSync2(path, "r");
@@ -25558,7 +25881,7 @@ function isLocalDatabase(url) {
25558
25881
  }
25559
25882
  function detectServices(repoPath) {
25560
25883
  const out = {};
25561
- const compose = COMPOSE_FILES.find((f) => existsSync21(join18(repoPath, f)));
25884
+ const compose = COMPOSE_FILES.find((f) => existsSync22(join18(repoPath, f)));
25562
25885
  if (compose)
25563
25886
  out.compose = compose;
25564
25887
  for (const file of ENV_FILES) {
@@ -25586,7 +25909,7 @@ function inferPaths(cmd, repoPath, depth = 1) {
25586
25909
  return;
25587
25910
  let dir = false;
25588
25911
  try {
25589
- dir = statSync7(join18(repoPath, clean)).isDirectory();
25912
+ dir = statSync8(join18(repoPath, clean)).isDirectory();
25590
25913
  } catch {
25591
25914
  return;
25592
25915
  }
@@ -25597,21 +25920,21 @@ function inferPaths(cmd, repoPath, depth = 1) {
25597
25920
  const t = tokens[i];
25598
25921
  if (t === "cd") {
25599
25922
  const dir = tokens[i + 1];
25600
- if (dir && !dir.startsWith("-") && existsSync21(join18(repoPath, dir)))
25923
+ if (dir && !dir.startsWith("-") && existsSync22(join18(repoPath, dir)))
25601
25924
  add(dir);
25602
25925
  continue;
25603
25926
  }
25604
25927
  if (t.startsWith("-") || t.startsWith("$"))
25605
25928
  continue;
25606
25929
  const mod = /^([A-Za-z_]\w*)(?:\.\w+)*:\w+$/.exec(t);
25607
- if (mod?.[1] && existsSync21(join18(repoPath, mod[1]))) {
25930
+ if (mod?.[1] && existsSync22(join18(repoPath, mod[1]))) {
25608
25931
  add(mod[1]);
25609
25932
  continue;
25610
25933
  }
25611
25934
  const rel = t.replace(/^\.\//, "");
25612
25935
  if (rel.startsWith("/") || rel.startsWith("..") || rel.includes("="))
25613
25936
  continue;
25614
- if (!existsSync21(join18(repoPath, rel)))
25937
+ if (!existsSync22(join18(repoPath, rel)))
25615
25938
  continue;
25616
25939
  add(rel);
25617
25940
  if (depth > 0 && rel.endsWith(".sh")) {
@@ -25638,9 +25961,9 @@ function procCommand(runner, script, body) {
25638
25961
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
25639
25962
  }
25640
25963
  function detectPages(repoPath, scripts, runner) {
25641
- if (!scripts.build || !existsSync21(join18(repoPath, "functions")))
25964
+ if (!scripts.build || !existsSync22(join18(repoPath, "functions")))
25642
25965
  return null;
25643
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join18(repoPath, f)).find((f) => existsSync21(f));
25966
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join18(repoPath, f)).find((f) => existsSync22(f));
25644
25967
  if (!cfg)
25645
25968
  return null;
25646
25969
  const out = readFileSync14(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
@@ -25649,17 +25972,17 @@ function detectPages(repoPath, scripts, runner) {
25649
25972
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
25650
25973
  }
25651
25974
  function detectRunner(repoPath, packageManager) {
25652
- if (existsSync21(join18(repoPath, "bun.lock")) || existsSync21(join18(repoPath, "bun.lockb")))
25975
+ if (existsSync22(join18(repoPath, "bun.lock")) || existsSync22(join18(repoPath, "bun.lockb")))
25653
25976
  return "bun";
25654
- if (existsSync21(join18(repoPath, "pnpm-lock.yaml")))
25977
+ if (existsSync22(join18(repoPath, "pnpm-lock.yaml")))
25655
25978
  return "pnpm";
25656
- if (existsSync21(join18(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
25979
+ if (existsSync22(join18(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
25657
25980
  return "yarn";
25658
25981
  return "npm";
25659
25982
  }
25660
25983
 
25661
25984
  // ../daemon/src/repos/create.ts
25662
- import { existsSync as existsSync22, realpathSync as realpathSync2, statSync as statSync8 } from "fs";
25985
+ import { existsSync as existsSync23, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
25663
25986
  import { appendFile as appendFile2, mkdir as mkdir3, rm as rm2 } from "fs/promises";
25664
25987
  import { homedir as homedir9 } from "os";
25665
25988
  import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
@@ -25674,13 +25997,13 @@ function planProject(opts, home = homedir9()) {
25674
25997
  const first = firstProjectsFolder(opts.parent, home);
25675
25998
  const parent = first ?? resolveParent(opts.parent);
25676
25999
  const dir = join19(parent, opts.name.trim());
25677
- if (existsSync22(dir))
26000
+ if (existsSync23(dir))
25678
26001
  throw new UserError(`${dir} already exists`);
25679
26002
  return { parent, dir, makeParent: first !== null };
25680
26003
  }
25681
26004
  function firstProjectsFolder(raw, home) {
25682
26005
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
25683
- if (typed !== join19(home, PROJECTS_FOLDER) || existsSync22(typed) || !existsSync22(home))
26006
+ if (typed !== join19(home, PROJECTS_FOLDER) || existsSync23(typed) || !existsSync23(home))
25684
26007
  return null;
25685
26008
  return join19(realpathSync2(home), PROJECTS_FOLDER);
25686
26009
  }
@@ -25690,10 +26013,10 @@ function planInPlace(opts) {
25690
26013
  throw new UserError(`${opts.name} is not a folder name`);
25691
26014
  }
25692
26015
  const typed = join19(resolveParent(opts.parent), name);
25693
- if (!existsSync22(typed) || !statSync8(typed).isDirectory())
26016
+ if (!existsSync23(typed) || !statSync9(typed).isDirectory())
25694
26017
  throw new UserError(`${typed} is not a folder`);
25695
26018
  const dir = realpathSync2(typed);
25696
- if (existsSync22(join19(dir, ".git")))
26019
+ if (existsSync23(join19(dir, ".git")))
25697
26020
  throw new UserError(`${dir} is already a project; open it instead`);
25698
26021
  return { parent: dirname7(dir), dir, makeParent: false };
25699
26022
  }
@@ -25701,9 +26024,9 @@ function resolveParent(raw) {
25701
26024
  const typed = expandTilde(raw.trim());
25702
26025
  if (!typed.startsWith("/"))
25703
26026
  throw new UserError(`${raw} is not a folder path`);
25704
- if (!existsSync22(typed))
26027
+ if (!existsSync23(typed))
25705
26028
  throw new UserError(`${raw} does not exist; make it first`);
25706
- if (!statSync8(typed).isDirectory())
26029
+ if (!statSync9(typed).isDirectory())
25707
26030
  throw new UserError(`${raw} is not a folder`);
25708
26031
  return realpathSync2(typed);
25709
26032
  }
@@ -25818,7 +26141,7 @@ async function ok(p, what) {
25818
26141
  }
25819
26142
 
25820
26143
  // ../daemon/src/repos/watcher.ts
25821
- import { existsSync as existsSync23, watch } from "fs";
26144
+ import { existsSync as existsSync24, watch } from "fs";
25822
26145
  import { basename as basename7, join as join20 } from "path";
25823
26146
  function watchDefaultBranch(repoPath, branch, onMove) {
25824
26147
  const last = { local: null, upstream: null };
@@ -25883,11 +26206,11 @@ function watchDefaultBranch(repoPath, branch, onMove) {
25883
26206
  let mid = null;
25884
26207
  const armInner = () => {
25885
26208
  inner?.close();
25886
- inner = existsSync23(mine) ? tryWatch(mine) : null;
26209
+ inner = existsSync24(mine) ? tryWatch(mine) : null;
25887
26210
  };
25888
26211
  const armMid = () => {
25889
26212
  mid?.close();
25890
- mid = existsSync23(remotes) ? tryWatch(remotes, (_event, filename) => {
26213
+ mid = existsSync24(remotes) ? tryWatch(remotes, (_event, filename) => {
25891
26214
  if (!filename || filename === remote.out)
25892
26215
  armInner();
25893
26216
  schedule();
@@ -25981,7 +26304,7 @@ function watchConfigFile(repoPath, onChange) {
25981
26304
  const armInner = () => {
25982
26305
  inner?.close();
25983
26306
  inner = null;
25984
- if (!existsSync23(dir))
26307
+ if (!existsSync24(dir))
25985
26308
  return;
25986
26309
  try {
25987
26310
  inner = watch(dir, (_event, filename) => {
@@ -26035,7 +26358,7 @@ class RepoRegistry {
26035
26358
  }
26036
26359
  async boot() {
26037
26360
  const { state } = this.d;
26038
- const gone = state.worktrees.filter((wt) => !existsSync24(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
26361
+ const gone = state.worktrees.filter((wt) => !existsSync25(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
26039
26362
  for (const wt of gone)
26040
26363
  await this.d.worktrees.forgetGone(wt);
26041
26364
  for (const repo of state.repos) {
@@ -26103,7 +26426,7 @@ class RepoRegistry {
26103
26426
  return `${repo.name} is set up now`;
26104
26427
  if (main.promptedAt || main.lastTurn)
26105
26428
  return `${repo.name} has a chat now`;
26106
- if (existsSync24(join21(repo.path, ".claude")))
26429
+ if (existsSync25(join21(repo.path, ".claude")))
26107
26430
  return `an agent has already run in ${repo.name}`;
26108
26431
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
26109
26432
  return `${repo.name} has files in it now`;
@@ -26184,7 +26507,7 @@ class RepoRegistry {
26184
26507
  }
26185
26508
  async register(rawPath, made) {
26186
26509
  const path = expandTilde(rawPath);
26187
- if (!existsSync24(path))
26510
+ if (!existsSync25(path))
26188
26511
  throw new UserError(`${path} does not exist`);
26189
26512
  if (!await isGitRepo(path))
26190
26513
  throw new UserError(`${path} is not a git repository`);
@@ -26258,7 +26581,7 @@ class RepoRegistry {
26258
26581
  throw new UserError(current.reason);
26259
26582
  const was = configTarget(repo.path, !!repo.made);
26260
26583
  const rel = configSibling(was, kind);
26261
- const moved = rel !== was && existsSync24(join21(repo.path, was));
26584
+ const moved = rel !== was && existsSync25(join21(repo.path, was));
26262
26585
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
26263
26586
  try {
26264
26587
  rmSync4(join21(repo.path, was));
@@ -26374,7 +26697,7 @@ class RepoRegistry {
26374
26697
  }
26375
26698
  placeConfig(repo) {
26376
26699
  const rel = configTarget(repo.path, !!repo.made);
26377
- if (isLocalConfigFile(rel) && existsSync24(join21(repo.path, rel))) {
26700
+ if (isLocalConfigFile(rel) && existsSync25(join21(repo.path, rel))) {
26378
26701
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
26379
26702
  }
26380
26703
  if (repo.configFile === rel)
@@ -27450,6 +27773,14 @@ var bunFs = {
27450
27773
  }
27451
27774
  }
27452
27775
  };
27776
+ function scanKey(git) {
27777
+ if (git.version !== undefined)
27778
+ return `v${git.version}`;
27779
+ const entries = git.files.filter((f) => isManifest(f.path) || CODE_FILE.test(f.path)).map((f) => `${f.xy}${f.path}`).sort();
27780
+ return `${git.head ?? ""}
27781
+ ${entries.join(`
27782
+ `)}`;
27783
+ }
27453
27784
  var signatureOf = (list) => list.map((e) => `${e.path} ${e.title ?? ""}`).join(`
27454
27785
  `);
27455
27786
 
@@ -27514,7 +27845,11 @@ class RouteService {
27514
27845
  if (!wt)
27515
27846
  return { routes: [], unseen: {} };
27516
27847
  const scan = this.scanFor(worktreeId, wt.path);
27517
- await this.routesOf(scan);
27848
+ const key = scanKey(git);
27849
+ if (!scan.routes || scan.key !== key) {
27850
+ scan.key = key;
27851
+ await this.routesOf(scan);
27852
+ }
27518
27853
  scan.git = git;
27519
27854
  await this.refreshUnseen(worktreeId, scan);
27520
27855
  return scan.pages ?? { routes: [], unseen: {} };
@@ -27565,7 +27900,7 @@ class RouteService {
27565
27900
  async refreshUnseen(worktreeId, scan) {
27566
27901
  if (!scan.routes)
27567
27902
  return false;
27568
- const changed = scan.git ? [...scan.git.files, ...scan.git.committed ?? []] : [];
27903
+ const changed = scan.git ? [...scan.git.files, ...scan.git.extras?.committed ?? []] : [];
27569
27904
  const seen = this.d.state.seenOf(worktreeId);
27570
27905
  const candidates = badgeCandidates(scan.routes, changed, seen?.here).slice(0, MAX_HASHED);
27571
27906
  const hashes = {};
@@ -27590,7 +27925,8 @@ class RouteService {
27590
27925
  manifests: new Map,
27591
27926
  code: new Map,
27592
27927
  git: null,
27593
- pages: null
27928
+ pages: null,
27929
+ key: null
27594
27930
  };
27595
27931
  scan.root = root;
27596
27932
  this.scans.delete(worktreeId);
@@ -27953,7 +28289,7 @@ function isAlive(pid) {
27953
28289
  }
27954
28290
 
27955
28291
  // ../daemon/src/runtime/bridge-script.ts
27956
- import { existsSync as existsSync25, readFileSync as readFileSync15, statSync as statSync9 } from "fs";
28292
+ import { existsSync as existsSync26, readFileSync as readFileSync15, statSync as statSync10 } from "fs";
27957
28293
  var LEARNED_MAX = 8;
27958
28294
 
27959
28295
  class BridgeScript {
@@ -27983,9 +28319,9 @@ class BridgeScript {
27983
28319
  this.cache = null;
27984
28320
  }
27985
28321
  get() {
27986
- if (!existsSync25(this.path))
28322
+ if (!existsSync26(this.path))
27987
28323
  return "// toyon bridge not built";
27988
- const s = statSync9(this.path);
28324
+ const s = statSync10(this.path);
27989
28325
  const stamp = `${s.mtimeMs}:${s.size}`;
27990
28326
  if (this.cache && stamp === this.stamp)
27991
28327
  return this.cache;
@@ -28009,6 +28345,51 @@ function servedLocally(origin) {
28009
28345
  return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
28010
28346
  }
28011
28347
 
28348
+ // ../daemon/src/server/interest.ts
28349
+ class Interest {
28350
+ keys = new Map;
28351
+ first = [];
28352
+ last = [];
28353
+ subscribe(key, id) {
28354
+ let set = this.keys.get(id);
28355
+ if (!set) {
28356
+ set = new Set;
28357
+ this.keys.set(id, set);
28358
+ }
28359
+ const was = set.size;
28360
+ set.add(key);
28361
+ if (was === 0)
28362
+ for (const fn of this.first)
28363
+ fn(id);
28364
+ }
28365
+ unsubscribe(key, id) {
28366
+ const set = this.keys.get(id);
28367
+ if (!set?.delete(key))
28368
+ return;
28369
+ if (set.size === 0) {
28370
+ this.keys.delete(id);
28371
+ for (const fn of this.last)
28372
+ fn(id);
28373
+ }
28374
+ }
28375
+ drop(key) {
28376
+ for (const id of [...this.keys.keys()])
28377
+ this.unsubscribe(key, id);
28378
+ }
28379
+ isSubscribed(id) {
28380
+ return (this.keys.get(id)?.size ?? 0) > 0;
28381
+ }
28382
+ subscribed() {
28383
+ return [...this.keys.keys()];
28384
+ }
28385
+ onFirst(fn) {
28386
+ this.first.push(fn);
28387
+ }
28388
+ onLast(fn) {
28389
+ this.last.push(fn);
28390
+ }
28391
+ }
28392
+
28012
28393
  // ../daemon/src/server/ws.ts
28013
28394
  import { homedir as homedir12, hostname as hostname2 } from "os";
28014
28395
 
@@ -28016,7 +28397,7 @@ import { homedir as homedir12, hostname as hostname2 } from "os";
28016
28397
  import { randomBytes as randomBytes7 } from "crypto";
28017
28398
 
28018
28399
  // ../daemon/src/agent/config.ts
28019
- import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
28400
+ import { existsSync as existsSync27, readFileSync as readFileSync16 } from "fs";
28020
28401
  import { homedir as homedir11 } from "os";
28021
28402
  import { join as join23 } from "path";
28022
28403
  function agentConfigFiles(spec, repoPath, home = homedir11()) {
@@ -28024,7 +28405,7 @@ function agentConfigFiles(spec, repoPath, home = homedir11()) {
28024
28405
  id,
28025
28406
  label,
28026
28407
  path,
28027
- exists: existsSync26(path)
28408
+ exists: existsSync27(path)
28028
28409
  });
28029
28410
  if (spec.id === "claude") {
28030
28411
  return [
@@ -28056,7 +28437,7 @@ function describeAgentConfig(spec, repoPath, home = homedir11()) {
28056
28437
  return { agent: spec.id, files, servers };
28057
28438
  }
28058
28439
  function readJson(path) {
28059
- if (!existsSync26(path))
28440
+ if (!existsSync27(path))
28060
28441
  return null;
28061
28442
  try {
28062
28443
  const v = JSON.parse(readFileSync16(path, "utf8"));
@@ -28077,7 +28458,7 @@ function serversOf(raw, scope) {
28077
28458
  });
28078
28459
  }
28079
28460
  function codexServers(path) {
28080
- if (!existsSync26(path))
28461
+ if (!existsSync27(path))
28081
28462
  return [];
28082
28463
  const out = [];
28083
28464
  let current = null;
@@ -28619,11 +29000,12 @@ function daylightNow(tz, at = new Date) {
28619
29000
  var shipped = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
28620
29001
  var errorText = (e) => e instanceof Error ? e.message : String(e);
28621
29002
  var gitStatus = async (s, ctx, worktreeId) => {
28622
- const info = await s.worktrees.gitStatus(worktreeId);
29003
+ const info = s.worktrees.readable(worktreeId) ? await s.worktrees.tree.current(worktreeId, FIRST_FRAME_WAIT_MS) : await s.worktrees.archivedStatus(worktreeId);
28623
29004
  if (info)
28624
29005
  ctx.reply({ t: "git-status", worktreeId, ...info });
28625
29006
  return info;
28626
29007
  };
29008
+ var FIRST_FRAME_WAIT_MS = 250;
28627
29009
  var gitAndPages = async (s, ctx, worktreeId) => {
28628
29010
  const info = await gitStatus(s, ctx, worktreeId);
28629
29011
  ctx.reply({ t: "routes", worktreeId, ...await s.routes.pages(worktreeId, info ?? { files: [] }) });
@@ -28922,8 +29304,12 @@ var handlers = {
28922
29304
  ctx.reply({ t: "files", worktreeId: msg.worktreeId, ...await s.files.list(msg.worktreeId) });
28923
29305
  },
28924
29306
  async search(msg, ctx, s) {
28925
- const { hits, truncated } = await s.files.search(msg.worktreeId, msg.query);
28926
- ctx.reply({ t: "search-results", worktreeId: msg.worktreeId, query: msg.query, hits, truncated });
29307
+ const page = await s.files.search(msg.worktreeId, msg.query, ctx.socket);
29308
+ if (page)
29309
+ ctx.reply({ t: "search-results", worktreeId: msg.worktreeId, query: msg.query, ...page });
29310
+ },
29311
+ "search-stop"(_msg, ctx, s) {
29312
+ s.files.stopSearch(ctx.socket);
28927
29313
  },
28928
29314
  async "find-element"(msg, ctx, s) {
28929
29315
  const { hits, sure } = await s.files.findElement(msg.worktreeId, msg.element);
@@ -29060,6 +29446,11 @@ var handlers = {
29060
29446
  }
29061
29447
  ctx.reply({ t: "folder-chosen", folder: path ? await describeFolder(path) : null });
29062
29448
  },
29449
+ async "choose-file"(msg, ctx, s) {
29450
+ const path = await s.folderDialog.choose(msg.start, "file");
29451
+ if (path)
29452
+ ctx.reply(openedFrame(await s.opens.file(path)));
29453
+ },
29063
29454
  "cancel-folder"(_msg, _ctx, s) {
29064
29455
  s.folderDialog.cancel();
29065
29456
  },
@@ -29259,7 +29650,7 @@ async function dispatch(msg, ctx, s) {
29259
29650
  }
29260
29651
 
29261
29652
  // ../daemon/src/server/http.ts
29262
- import { existsSync as existsSync27 } from "fs";
29653
+ import { existsSync as existsSync28 } from "fs";
29263
29654
  import { hostname } from "os";
29264
29655
  import { join as join24 } from "path";
29265
29656
 
@@ -29564,7 +29955,7 @@ function createFetch(opts) {
29564
29955
  return new Response("unauthorized", { status: 401 });
29565
29956
  const id = url.pathname.slice("/uploads/".length);
29566
29957
  const path = opts.attachments.uploads.path(id);
29567
- if (!path || !existsSync27(path))
29958
+ if (!path || !existsSync28(path))
29568
29959
  return new Response("not found", { status: 404 });
29569
29960
  return new Response(Bun.file(path), {
29570
29961
  headers: { "cache-control": IMMUTABLE, ...attachedHeaders(opts.attachments.uploads.imageType(id)) }
@@ -29577,7 +29968,7 @@ function createFetch(opts) {
29577
29968
  const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
29578
29969
  if (worktreeId && file)
29579
29970
  await opts.attachments.whenWritten(worktreeId, file);
29580
- const path = places.find((p) => !!p && existsSync27(p));
29971
+ const path = places.find((p) => !!p && existsSync28(p));
29581
29972
  if (!path)
29582
29973
  return new Response("not found", { status: 404 });
29583
29974
  if (url.searchParams.has("where")) {
@@ -29617,7 +30008,7 @@ function createFetch(opts) {
29617
30008
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
29618
30009
  const file = join24(opts.shellDist, rel.replaceAll("..", ""));
29619
30010
  const hashed = rel.startsWith("/assets/");
29620
- if (existsSync27(file) && Bun.file(file).size > 0) {
30011
+ if (existsSync28(file) && Bun.file(file).size > 0) {
29621
30012
  if (rel === "/manifest.json") {
29622
30013
  const { bar, ground } = opts.manifestColors();
29623
30014
  const manifest = { ...await Bun.file(file).json(), theme_color: bar, background_color: ground };
@@ -29637,7 +30028,7 @@ function createFetch(opts) {
29637
30028
  if (hashed)
29638
30029
  return new Response("not found", { status: 404 });
29639
30030
  const index = join24(opts.shellDist, "index.html");
29640
- if (existsSync27(index))
30031
+ if (existsSync28(index))
29641
30032
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE2 } });
29642
30033
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
29643
30034
  }
@@ -29780,6 +30171,11 @@ function startServer(opts) {
29780
30171
  };
29781
30172
  const metrics = () => ({
29782
30173
  lag,
30174
+ lagHist,
30175
+ gitSpawns,
30176
+ treeReads,
30177
+ syncHot,
30178
+ fdLimit: limits.fdLimit || undefined,
29783
30179
  worktrees: {
29784
30180
  total: s.state.worktrees.filter((w) => w.kind !== "spare").length,
29785
30181
  running: s.runtime.runningCount(),
@@ -29811,24 +30207,19 @@ function startServer(opts) {
29811
30207
  s.hub.on("pendingChanged", () => broadcast({ t: "pending-repos", pending: s.repos.pending }));
29812
30208
  s.hub.on("proc", (worktreeId, proc) => broadcast({ t: "proc", worktreeId, proc }));
29813
30209
  s.hub.on("log", (worktreeId, proc, line, retract) => sendTo(worktreeId, retract ? { t: "log", worktreeId, proc, line, retract } : { t: "log", worktreeId, proc, line }));
30210
+ s.hub.on("agent", (worktreeId, seq, event) => sendTo(worktreeId, { t: "agent", worktreeId, seq, event }));
29814
30211
  s.hub.on("queue", (worktreeId, items) => sendTo(worktreeId, { t: "queue", worktreeId, items }));
29815
30212
  s.hub.on("agentCommands", (worktreeId, commands) => sendTo(worktreeId, { t: "agent-commands", worktreeId, commands }));
29816
30213
  s.hub.on("termData", (worktreeId, stream, data) => queueTerm(worktreeId, stream, data));
29817
30214
  s.hub.on("termExit", (worktreeId, stream, exitCode) => sendTerm(worktreeId, stream, { t: "term-exit", worktreeId, stream, exitCode }));
29818
- const gitStatusTimers = new Map;
29819
- const refreshGitStatus = (worktreeId) => {
29820
- clearTimeout(gitStatusTimers.get(worktreeId));
29821
- gitStatusTimers.set(worktreeId, setTimeout(() => {
29822
- gitStatusTimers.delete(worktreeId);
29823
- fireAndForget(worktreeId, pushGitStatus(worktreeId), "git status after agent edit");
29824
- }, 150));
29825
- };
29826
- s.hub.on("agent", (worktreeId, seq, event) => {
29827
- sendTo(worktreeId, { t: "agent", worktreeId, seq, event });
29828
- if (event.type === "tool-end" || event.type === "turn-end")
29829
- refreshGitStatus(worktreeId);
30215
+ s.hub.on("treeChanged", (worktreeId) => {
30216
+ const frame = s.worktrees.tree.frame(worktreeId);
30217
+ if (!frame)
30218
+ return;
30219
+ sendTo(worktreeId, { t: "git-status", worktreeId, ...frame });
30220
+ if (frame.extras)
30221
+ fireAndForget(worktreeId, s.routes.pages(worktreeId, frame).then((p) => sendPages(worktreeId, p)), "pages after git status");
29830
30222
  });
29831
- s.hub.on("filesChanged", refreshGitStatus);
29832
30223
  s.hub.on("opened", (o) => {
29833
30224
  if (sockets.size === 0)
29834
30225
  return;
@@ -29836,18 +30227,6 @@ function startServer(opts) {
29836
30227
  s.opens.delivered();
29837
30228
  });
29838
30229
  s.hub.on("openRefused", (message) => broadcast({ t: "error", message }));
29839
- const shellQuiet = new Map;
29840
- s.hub.on("termData", (worktreeId, stream) => {
29841
- if (stream !== SHELL_STREAM)
29842
- return;
29843
- clearTimeout(shellQuiet.get(worktreeId));
29844
- shellQuiet.set(worktreeId, setTimeout(() => {
29845
- shellQuiet.delete(worktreeId);
29846
- s.worktrees.invalidateCounts(worktreeId);
29847
- worktreesChanged();
29848
- fireAndForget(worktreeId, pushGitStatus(worktreeId), "git status after the shell went quiet");
29849
- }, 1500));
29850
- });
29851
30230
  const sentPages = new Map;
29852
30231
  const sendPages = (worktreeId, pages) => {
29853
30232
  const json = JSON.stringify(pages);
@@ -29856,32 +30235,12 @@ function startServer(opts) {
29856
30235
  sentPages.set(worktreeId, json);
29857
30236
  sendTo(worktreeId, { t: "routes", worktreeId, ...pages });
29858
30237
  };
29859
- const pushGitStatus = async (worktreeId) => {
29860
- if (![...sockets].some((ws) => ws.data.subs.has(worktreeId)))
29861
- return;
29862
- const info = await s.worktrees.gitStatus(worktreeId);
29863
- if (!info)
29864
- return;
29865
- sendTo(worktreeId, { t: "git-status", worktreeId, ...info });
29866
- sendPages(worktreeId, await s.routes.pages(worktreeId, info));
29867
- };
29868
30238
  s.hub.on("pagesChanged", (worktreeId) => {
29869
30239
  const pages = s.routes.cached(worktreeId);
29870
30240
  if (pages)
29871
30241
  sendPages(worktreeId, pages);
29872
30242
  });
29873
- s.hub.on("repoTick", (repoId) => {
29874
- worktreesChanged();
29875
- const subscribed = new Set;
29876
- for (const ws of sockets)
29877
- for (const id of ws.data.subs)
29878
- subscribed.add(id);
29879
- for (const id of subscribed) {
29880
- if (s.worktrees.readable(id)?.repoId !== repoId)
29881
- continue;
29882
- fireAndForget(id, pushGitStatus(id), "git status on ref tick");
29883
- }
29884
- });
30243
+ s.hub.on("repoTick", () => worktreesChanged());
29885
30244
  const themesChanged = () => {
29886
30245
  const cur = s.themes.current();
29887
30246
  setWaitingColors({ bg: cur.colors.surface0, fg: cur.colors.text1 });
@@ -30024,8 +30383,11 @@ function startServer(opts) {
30024
30383
  close(ws) {
30025
30384
  if (ws.data.preview)
30026
30385
  ws.data.preview.handler.close(ws.data.preview.data);
30027
- else
30386
+ else {
30028
30387
  s.idle.drop(ws.data);
30388
+ s.interest.drop(ws.data);
30389
+ }
30390
+ s.files.stopSearch(ws.data);
30029
30391
  sockets.delete(ws);
30030
30392
  unlink(ws);
30031
30393
  },
@@ -30055,10 +30417,12 @@ function startServer(opts) {
30055
30417
  if (ws.data.subs.has(id))
30056
30418
  return false;
30057
30419
  ws.data.subs.add(id);
30420
+ s.interest.subscribe(ws.data, id);
30058
30421
  return true;
30059
30422
  },
30060
30423
  unsubscribe: (id) => {
30061
- ws.data.subs.delete(id);
30424
+ if (ws.data.subs.delete(id))
30425
+ s.interest.unsubscribe(ws.data, id);
30062
30426
  if (ws.data.view === id) {
30063
30427
  ws.data.view = null;
30064
30428
  s.idle.view(ws.data, null);
@@ -30075,7 +30439,8 @@ function startServer(opts) {
30075
30439
  const key = streamKey(id, stream);
30076
30440
  ws.data.terms.delete(key);
30077
30441
  ws.data.dropped.delete(key);
30078
- }
30442
+ },
30443
+ socket: ws.data
30079
30444
  };
30080
30445
  try {
30081
30446
  await dispatch(parsed.msg, ctx, s);
@@ -30112,7 +30477,7 @@ function startServer(opts) {
30112
30477
  }
30113
30478
 
30114
30479
  // ../daemon/src/themes/store.ts
30115
- import { existsSync as existsSync28, readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync10, writeFileSync as writeFileSync11 } from "fs";
30480
+ import { existsSync as existsSync29, readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync11, writeFileSync as writeFileSync11 } from "fs";
30116
30481
  import { homedir as homedir13 } from "os";
30117
30482
  import { basename as basename9, dirname as dirname9, join as join25, resolve as resolve9 } from "path";
30118
30483
  var home = homedir13();
@@ -30234,7 +30599,7 @@ function readVscodeTheme(file, depth = 0) {
30234
30599
  return t;
30235
30600
  }
30236
30601
  function loadDir(dir) {
30237
- if (!existsSync28(dir))
30602
+ if (!existsSync29(dir))
30238
30603
  return [];
30239
30604
  const out = [];
30240
30605
  for (const f of readdirSync8(dir)) {
@@ -30256,7 +30621,7 @@ function loadDir(dir) {
30256
30621
  return out;
30257
30622
  }
30258
30623
  function discover(dir) {
30259
- if (!existsSync28(dir))
30624
+ if (!existsSync29(dir))
30260
30625
  return [];
30261
30626
  const out = [];
30262
30627
  let entries;
@@ -30269,7 +30634,7 @@ function discover(dir) {
30269
30634
  const extDir = join25(dir, name);
30270
30635
  const pkgFile = join25(extDir, "package.json");
30271
30636
  try {
30272
- if (!statSync10(extDir).isDirectory() || !existsSync28(pkgFile))
30637
+ if (!statSync11(extDir).isDirectory() || !existsSync29(pkgFile))
30273
30638
  continue;
30274
30639
  const pkg = JSON.parse(readFileSync17(pkgFile, "utf8"));
30275
30640
  const themes = pkg.contributes?.themes;
@@ -30278,7 +30643,7 @@ function discover(dir) {
30278
30643
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
30279
30644
  let nls = {};
30280
30645
  const nlsFile = join25(extDir, "package.nls.json");
30281
- if (existsSync28(nlsFile)) {
30646
+ if (existsSync29(nlsFile)) {
30282
30647
  try {
30283
30648
  nls = JSON.parse(readFileSync17(nlsFile, "utf8"));
30284
30649
  } catch {}
@@ -30692,7 +31057,7 @@ class BackendShare {
30692
31057
 
30693
31058
  // ../daemon/src/worktrees/cache.ts
30694
31059
  import { createHash as createHash4 } from "crypto";
30695
- import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
31060
+ import { existsSync as existsSync30, readFileSync as readFileSync18 } from "fs";
30696
31061
  import { mkdir as mkdir5, readdir as readdir2, rename, rm as rm4, stat as stat4, utimes, writeFile as writeFile3 } from "fs/promises";
30697
31062
  import { join as join26 } from "path";
30698
31063
 
@@ -30725,7 +31090,7 @@ var STALE_MS = 30 * 24 * 3600000;
30725
31090
  var TMP = ".tmp-";
30726
31091
  var MANIFEST = "manifest.json";
30727
31092
  function defaultTools(dir) {
30728
- const has = (...files) => files.some((f) => existsSync29(join26(dir, f)));
31093
+ const has = (...files) => files.some((f) => existsSync30(join26(dir, f)));
30729
31094
  const out = [];
30730
31095
  if (has("bun.lock", "bun.lockb"))
30731
31096
  out.push("bun --version");
@@ -30738,7 +31103,7 @@ function defaultTools(dir) {
30738
31103
  return out;
30739
31104
  }
30740
31105
  function withSidecars(root, rel) {
30741
- return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync29(join26(root, n)));
31106
+ return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync30(join26(root, n)));
30742
31107
  }
30743
31108
  var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
30744
31109
  async function listEntries(familyDir) {
@@ -30754,7 +31119,7 @@ async function listEntries(familyDir) {
30754
31119
  if (!m?.[1] || !m[2])
30755
31120
  continue;
30756
31121
  const dir = join26(familyDir, name);
30757
- if (!existsSync29(join26(dir, MANIFEST)))
31122
+ if (!existsSync30(join26(dir, MANIFEST)))
30758
31123
  continue;
30759
31124
  out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
30760
31125
  }
@@ -30792,7 +31157,7 @@ class ArtifactCache {
30792
31157
  }
30793
31158
  const restored = [];
30794
31159
  for (const rel of policy.paths) {
30795
- if (existsSync29(join26(wt.path, rel)))
31160
+ if (existsSync30(join26(wt.path, rel)))
30796
31161
  continue;
30797
31162
  const names = withSidecars(entry.dir, rel);
30798
31163
  if (names.length === 0)
@@ -30833,7 +31198,7 @@ class ArtifactCache {
30833
31198
  let tmp;
30834
31199
  let slot;
30835
31200
  try {
30836
- const present = policy.paths.filter((rel) => existsSync29(join26(wt.path, rel)));
31201
+ const present = policy.paths.filter((rel) => existsSync30(join26(wt.path, rel)));
30837
31202
  if (present.length === 0)
30838
31203
  return;
30839
31204
  const family = await this.family(wt, repo, policy);
@@ -31633,6 +31998,9 @@ class LandingService {
31633
31998
  };
31634
31999
  }
31635
32000
  async hasWork(wt) {
32001
+ const rec = await this.d.worktrees.tree?.refresh(wt.id);
32002
+ if (rec && rec.failedAt === undefined)
32003
+ return rec.core.files.length > 0 || (rec.core.ahead ?? 0) > 0;
31636
32004
  const repo = this.d.state.requireRepo(wt.repoId);
31637
32005
  const files = await statusFiles(wt.path);
31638
32006
  if (files.length > 0)
@@ -31857,11 +32225,8 @@ class PrService {
31857
32225
  if (!fresh || !now || now.pr?.number !== pr.number)
31858
32226
  return fresh;
31859
32227
  this.d.worktrees.setPr(worktreeId, fresh);
31860
- if (fresh.state === "merged" && !prTaken(now)) {
31861
- const r = await this.d.worktrees.prMerged(worktreeId);
31862
- if (!r.ok)
31863
- log.info(worktreeId, `PR #${pr.number} merged; main here did not follow: ${r.message}`);
31864
- }
32228
+ if (fresh.state === "merged" && !prTaken(now))
32229
+ await this.d.worktrees.prMerged(worktreeId);
31865
32230
  return fresh;
31866
32231
  } finally {
31867
32232
  this.reading.delete(worktreeId);
@@ -32172,9 +32537,9 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
32172
32537
  }
32173
32538
 
32174
32539
  // ../daemon/src/worktrees/service.ts
32175
- import { cpSync, existsSync as existsSync34, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
32540
+ import { cpSync, existsSync as existsSync35, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
32176
32541
  import { rm as rm5 } from "fs/promises";
32177
- import { dirname as dirname12, join as join30 } from "path";
32542
+ import { dirname as dirname12, join as join31 } from "path";
32178
32543
 
32179
32544
  // ../daemon/src/git/archive.ts
32180
32545
  import { rmSync as rmSync5 } from "fs";
@@ -32298,8 +32663,8 @@ async function dropLandRefs(repoPath, worktreeId) {
32298
32663
  }
32299
32664
 
32300
32665
  // ../daemon/src/git/land.ts
32301
- import { existsSync as existsSync30 } from "fs";
32302
- var STEP_TIMEOUT_MS = 10 * 60000;
32666
+ import { existsSync as existsSync31 } from "fs";
32667
+ var STEP_TIMEOUT_MS = RUN_CEILING_MS;
32303
32668
  function rowOf(r) {
32304
32669
  return r.shown && r.toolId && r.command ? { toolId: r.toolId, command: r.command, text: r.text } : undefined;
32305
32670
  }
@@ -32387,7 +32752,7 @@ async function setAside(worktreePath) {
32387
32752
  }
32388
32753
  return { sha: made.out, head: head.out };
32389
32754
  }
32390
- async function putBack(worktreePath, defaultBr, aside, taken) {
32755
+ async function putBack(worktreePath, defaultBr, how, aside, taken) {
32391
32756
  let clash = null;
32392
32757
  if (taken.ok) {
32393
32758
  const applied = await git(worktreePath, "stash", "apply", aside.sha);
@@ -32411,9 +32776,12 @@ async function putBack(worktreePath, defaultBr, aside, taken) {
32411
32776
  if (!clash)
32412
32777
  return taken;
32413
32778
  const named = clash.length ? ` (${listed(clash)})` : "";
32779
+ const ask = how === "rebase" ? `rebase onto ${defaultBr}` : `merge ${defaultBr}`;
32414
32780
  return {
32415
32781
  ok: false,
32416
- message: `the uncommitted changes here conflict with ${defaultBr}${named}: commit them first, then sync`
32782
+ conflict: how,
32783
+ carried: clash,
32784
+ message: `the uncommitted changes here conflict with ${defaultBr}${named}: ask the agent to ${ask} and resolve them`
32417
32785
  };
32418
32786
  }
32419
32787
  var MIDWAY = [
@@ -32429,7 +32797,7 @@ async function midway(worktreePath) {
32429
32797
  if (!r.ok)
32430
32798
  return null;
32431
32799
  const at = r.out.split(`
32432
- `).findIndex((file) => file && existsSync30(file));
32800
+ `).findIndex((file) => file && existsSync31(file));
32433
32801
  return MIDWAY[at]?.[1] ?? null;
32434
32802
  }
32435
32803
  async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
@@ -32445,11 +32813,12 @@ async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = f
32445
32813
  const { behind } = await aheadBehind(worktreePath, defaultBr);
32446
32814
  if (behind === 0)
32447
32815
  return { ok: true, message: `already up to date with ${defaultBr}` };
32816
+ const how = await howToBring(worktreePath, defaultBr, own);
32448
32817
  const aside = carry ? await setAside(worktreePath) : null;
32449
32818
  if (aside && "message" in aside)
32450
32819
  return aside;
32451
- const taken = await bringIn(worktreePath, defaultBr, own, behind, w);
32452
- return aside ? putBack(worktreePath, defaultBr, aside, taken) : taken;
32820
+ const taken = await bringIn(worktreePath, defaultBr, how, behind, w);
32821
+ return aside ? putBack(worktreePath, defaultBr, how, aside, taken) : taken;
32453
32822
  }
32454
32823
  async function requireLinear(worktreePath, base) {
32455
32824
  const merges = await git(worktreePath, "rev-list", "--merges", "--count", `${base}..HEAD`);
@@ -32461,9 +32830,14 @@ async function requireLinear(worktreePath, base) {
32461
32830
  message: `the branch holds a merge commit, which landing by rebase would put on ${base}: ask the agent to rebase onto ${base} first`
32462
32831
  };
32463
32832
  }
32464
- async function bringIn(worktreePath, defaultBr, own, behind, w) {
32465
- const merges = own ? await git(worktreePath, "rev-list", "--merges", "--count", `${defaultBr}..HEAD`) : null;
32466
- if (own && merges?.out === "0") {
32833
+ async function howToBring(worktreePath, defaultBr, own) {
32834
+ if (!own)
32835
+ return "merge";
32836
+ const merges = await git(worktreePath, "rev-list", "--merges", "--count", `${defaultBr}..HEAD`);
32837
+ return merges.out === "0" ? "rebase" : "merge";
32838
+ }
32839
+ async function bringIn(worktreePath, defaultBr, how, behind, w) {
32840
+ if (how === "rebase") {
32467
32841
  w.step(`rebasing onto ${defaultBr}`);
32468
32842
  const r = await w.git(worktreePath, ["rebase", defaultBr]);
32469
32843
  if (!r.ok) {
@@ -32716,7 +33090,7 @@ async function openPr(o, w = UNWATCHED) {
32716
33090
  const push = await pushBranch(o.worktreePath, o.branch, w);
32717
33091
  if (!push.ok)
32718
33092
  return push;
32719
- w.step("opening the PR");
33093
+ w.step("creating the PR");
32720
33094
  const titled = o.subject ? ["--title", o.subject, "--body", o.body ?? ""] : ["--fill"];
32721
33095
  const created = await gh(["pr", "create", "--head", o.branch, "--base", o.defaultBr, ...titled], o.worktreePath);
32722
33096
  let url = null;
@@ -32735,7 +33109,7 @@ ${created.exit}`)) {
32735
33109
  return { ok: false, message: `gh pr create failed: ${created.err.slice(0, 300)}` };
32736
33110
  const number = url ? prNumberOf(url) : null;
32737
33111
  if (!url || number === null)
32738
- return { ok: true, message: `pushed ${o.branch}; PR opened but its URL was not read` };
33112
+ return { ok: true, message: `pushed ${o.branch}; PR created but its URL was not read` };
32739
33113
  let note = "";
32740
33114
  if (o.automerge) {
32741
33115
  const method = await ghMethod(o.worktreePath, o.method);
@@ -32773,7 +33147,7 @@ function compareUrl(remoteUrl, base, branch) {
32773
33147
  }
32774
33148
 
32775
33149
  // ../daemon/src/git/turn.ts
32776
- import { copyFileSync, existsSync as existsSync31, rmSync as rmSync6 } from "fs";
33150
+ import { copyFileSync, existsSync as existsSync32, rmSync as rmSync6 } from "fs";
32777
33151
  import { join as join27 } from "path";
32778
33152
  var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
32779
33153
  var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
@@ -32799,7 +33173,7 @@ async function treeNow(wtPath, head, dirty) {
32799
33173
  const tree = await run(GIT, ["write-tree"], wtPath, env);
32800
33174
  return tree.ok && tree.out ? tree.out : null;
32801
33175
  };
32802
- if (!existsSync31(index))
33176
+ if (!existsSync32(index))
32803
33177
  was = undefined;
32804
33178
  const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
32805
33179
  if (was && paths && was.head === head) {
@@ -32814,9 +33188,9 @@ async function treeNow(wtPath, head, dirty) {
32814
33188
  }
32815
33189
  }
32816
33190
  synced.delete(wtPath);
32817
- if (!existsSync31(index)) {
33191
+ if (!existsSync32(index)) {
32818
33192
  const own = join27(index, "..", "index");
32819
- if (existsSync31(own))
33193
+ if (existsSync32(own))
32820
33194
  copyFileSync(own, index);
32821
33195
  else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
32822
33196
  return null;
@@ -32897,7 +33271,7 @@ async function dropTurnRefs(repoPath, worktreeId) {
32897
33271
  }
32898
33272
 
32899
33273
  // ../daemon/src/worktrees/archive.ts
32900
- import { existsSync as existsSync32, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync19, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
33274
+ import { existsSync as existsSync33, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync19, renameSync as renameSync6, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
32901
33275
  import { basename as basename11, dirname as dirname11, join as join28 } from "path";
32902
33276
  class WorktreeArchive {
32903
33277
  dir;
@@ -32907,7 +33281,7 @@ class WorktreeArchive {
32907
33281
  mkdirSync8(dir, { recursive: true });
32908
33282
  for (const id of readdirSync9(dir)) {
32909
33283
  const file = this.filesOf(id).record;
32910
- if (!existsSync32(file))
33284
+ if (!existsSync33(file))
32911
33285
  continue;
32912
33286
  try {
32913
33287
  this.records.set(id, JSON.parse(readFileSync19(file, "utf8")));
@@ -32966,7 +33340,7 @@ class WorktreeArchive {
32966
33340
  if (!this.records.has(id) || !isPlanPath(rel))
32967
33341
  return null;
32968
33342
  const file = join28(this.filesOf(id).plans, basename11(rel));
32969
- if (!existsSync32(file))
33343
+ if (!existsSync33(file))
32970
33344
  return null;
32971
33345
  try {
32972
33346
  return readFileSync19(file, "utf8");
@@ -33007,7 +33381,7 @@ function summarize(r, repoId, transcript) {
33007
33381
  };
33008
33382
  }
33009
33383
  function lastUsage(transcript) {
33010
- if (!existsSync32(transcript))
33384
+ if (!existsSync33(transcript))
33011
33385
  return null;
33012
33386
  const lines = readFileSync19(transcript, "utf8").split(`
33013
33387
  `);
@@ -33023,7 +33397,7 @@ function lastUsage(transcript) {
33023
33397
  return null;
33024
33398
  }
33025
33399
  function firstPrompt(transcript) {
33026
- if (!existsSync32(transcript))
33400
+ if (!existsSync33(transcript))
33027
33401
  return;
33028
33402
  for (const line of readFileSync19(transcript, "utf8").split(`
33029
33403
  `)) {
@@ -33038,11 +33412,11 @@ function firstPrompt(transcript) {
33038
33412
  return;
33039
33413
  }
33040
33414
  function move(from, to) {
33041
- if (!existsSync32(from))
33415
+ if (!existsSync33(from))
33042
33416
  return;
33043
33417
  mkdirSync8(dirname11(to), { recursive: true });
33044
33418
  rmSync7(to, { recursive: true, force: true });
33045
- renameSync5(from, to);
33419
+ renameSync6(from, to);
33046
33420
  }
33047
33421
 
33048
33422
  // ../daemon/src/worktrees/archivedGit.ts
@@ -33102,95 +33476,6 @@ class ArchivedGit {
33102
33476
  }
33103
33477
  }
33104
33478
 
33105
- // ../daemon/src/worktrees/counts.ts
33106
- var COUNTS_FLOOR_MS = 1e4;
33107
- var same2 = (a, b) => a.ahead === b.ahead && a.behind === b.behind && a.unpushed === b.unpushed && a.dirty === b.dirty;
33108
-
33109
- class Counts {
33110
- d;
33111
- cache = new Map;
33112
- reading = new Map;
33113
- marks = new Map;
33114
- markAll = 0;
33115
- constructor(d) {
33116
- this.d = d;
33117
- }
33118
- get(id) {
33119
- return this.cache.get(id)?.value;
33120
- }
33121
- stale(id) {
33122
- if (id === undefined) {
33123
- this.markAll++;
33124
- for (const c of this.cache.values())
33125
- c.at = 0;
33126
- return;
33127
- }
33128
- this.marks.set(id, (this.marks.get(id) ?? 0) + 1);
33129
- const c = this.cache.get(id);
33130
- if (c)
33131
- c.at = 0;
33132
- }
33133
- put(id, value) {
33134
- return this.store(id, value);
33135
- }
33136
- refresh(ids) {
33137
- const reads = [];
33138
- for (const id of new Set(ids)) {
33139
- const running = this.reading.get(id);
33140
- if (running) {
33141
- reads.push(running);
33142
- continue;
33143
- }
33144
- const cached = this.cache.get(id);
33145
- if (cached && Date.now() - cached.at < COUNTS_FLOOR_MS)
33146
- continue;
33147
- if (this.d.going(id))
33148
- continue;
33149
- reads.push(this.readOne(id));
33150
- }
33151
- if (!reads.length)
33152
- return null;
33153
- return Promise.all(reads).then((moved) => moved.some(Boolean));
33154
- }
33155
- async read(id) {
33156
- this.stale(id);
33157
- await this.refresh([id]);
33158
- const c = this.cache.get(id);
33159
- return c?.failed || !c?.value ? null : c.value;
33160
- }
33161
- markOf(id) {
33162
- return (this.marks.get(id) ?? 0) + this.markAll;
33163
- }
33164
- readOne(id) {
33165
- const read = (async () => {
33166
- for (;; ) {
33167
- const mark = this.markOf(id);
33168
- let value = null;
33169
- try {
33170
- value = await this.d.read(id);
33171
- } catch {}
33172
- if (this.d.going(id))
33173
- return false;
33174
- if (this.markOf(id) !== mark)
33175
- continue;
33176
- if (value)
33177
- return this.store(id, value);
33178
- this.cache.set(id, { value: this.get(id), at: Date.now(), failed: true });
33179
- return false;
33180
- }
33181
- })().finally(() => {
33182
- this.reading.delete(id);
33183
- });
33184
- this.reading.set(id, read);
33185
- return read;
33186
- }
33187
- store(id, value) {
33188
- const prev = this.cache.get(id)?.value;
33189
- this.cache.set(id, { value, at: Date.now() });
33190
- return !prev || !same2(prev, value);
33191
- }
33192
- }
33193
-
33194
33479
  // ../daemon/src/worktrees/landSubjects.ts
33195
33480
  function unfilled(mark) {
33196
33481
  return mark.subjects === undefined;
@@ -33209,15 +33494,19 @@ async function fillLandMarks(jobs, read, width = 8) {
33209
33494
  }
33210
33495
 
33211
33496
  // ../daemon/src/worktrees/spare.ts
33212
- import { existsSync as existsSync33 } from "fs";
33497
+ import { existsSync as existsSync34 } from "fs";
33213
33498
  import { basename as basename12, join as join29 } from "path";
33214
33499
  class SparePool {
33215
33500
  d;
33216
33501
  inflight = new Map;
33217
33502
  refreshing = new Map;
33503
+ writing = new Set;
33218
33504
  constructor(d) {
33219
33505
  this.d = d;
33220
33506
  }
33507
+ checkingOut(id) {
33508
+ return this.writing.has(id);
33509
+ }
33221
33510
  spareOf(repoId) {
33222
33511
  return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
33223
33512
  }
@@ -33229,7 +33518,7 @@ class SparePool {
33229
33518
  if (!text.trim())
33230
33519
  return;
33231
33520
  const wt = this.d.state.worktree(boxId);
33232
- if (wt?.kind !== "spare" || !existsSync33(wt.path))
33521
+ if (wt?.kind !== "spare" || !existsSync34(wt.path))
33233
33522
  return;
33234
33523
  const agent = this.d.runtime.ensureAgent(wt).agent;
33235
33524
  if (agent.runningAgent === null)
@@ -33293,16 +33582,18 @@ class SparePool {
33293
33582
  const repoId = repo.id;
33294
33583
  let cut = () => {};
33295
33584
  const added = (async () => {
33296
- if (existsSync33(wt.path))
33585
+ if (existsSync34(wt.path))
33297
33586
  return;
33298
33587
  const run = this.d.runs.begin(wt.id, "setup", {
33299
33588
  timeoutMs: timeoutFor(repo.config, "setup"),
33300
33589
  stage: "checking out"
33301
33590
  });
33591
+ this.writing.add(wt.id);
33302
33592
  try {
33303
33593
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", "--no-checkout", wt.path, baseOf(repo)));
33304
33594
  await withLock(`checkout:${repo.path}`, () => gitOrThrow(wt.path, "reset", "--hard"));
33305
33595
  } finally {
33596
+ this.writing.delete(wt.id);
33306
33597
  run.finish(0);
33307
33598
  }
33308
33599
  })();
@@ -33352,7 +33643,7 @@ class SparePool {
33352
33643
  }
33353
33644
  log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
33354
33645
  this.d.carryDraft?.(wt.id, wt.repoId);
33355
- if (existsSync33(wt.path)) {
33646
+ if (existsSync34(wt.path)) {
33356
33647
  await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
33357
33648
  return;
33358
33649
  }
@@ -33424,7 +33715,7 @@ class SparePool {
33424
33715
  try {
33425
33716
  await running?.added;
33426
33717
  running?.cut();
33427
- if (!existsSync33(wt.path))
33718
+ if (!existsSync34(wt.path))
33428
33719
  throw new Error("the worktree's directory is gone");
33429
33720
  await this.refreshing.get(repoId);
33430
33721
  await prepare(wt);
@@ -33452,7 +33743,7 @@ class SparePool {
33452
33743
  for (const k of Object.keys(stamps))
33453
33744
  delete wt[k];
33454
33745
  delete wt.promptedAt;
33455
- if (!existsSync33(wt.path)) {
33746
+ if (!existsSync34(wt.path)) {
33456
33747
  await this.rollBack(wt, e);
33457
33748
  return;
33458
33749
  }
@@ -33463,6 +33754,518 @@ class SparePool {
33463
33754
  }
33464
33755
  }
33465
33756
 
33757
+ // ../daemon/src/worktrees/treeState.ts
33758
+ import { watch as watch2 } from "fs";
33759
+ import { stat as stat5 } from "fs/promises";
33760
+ import { join as join30 } from "path";
33761
+ var EDIT_DELAY_MS = 150;
33762
+ var SHELL_QUIET_MS2 = 1500;
33763
+ var WATCH_DELAY_MS = 300;
33764
+ var FLOOR_MS = 5 * 60000;
33765
+ var FLOOR_BATCH = 3;
33766
+ var SLOTS = { fg: 6, bg: 3, extras: 2 };
33767
+ var RETRY_MS = [2000, 1e4, 60000];
33768
+ var SIGNATURE_STATS = 2000;
33769
+ var sameCounts = (a, b) => !!a && a.ahead === b.ahead && a.behind === b.behind && a.unpushed === b.unpushed && a.dirty === b.dirty;
33770
+
33771
+ class TreeState {
33772
+ d;
33773
+ records = new Map;
33774
+ reading = new Map;
33775
+ marks = new Map;
33776
+ markAll = 0;
33777
+ extrasRunning = new Map;
33778
+ inSideEffects = new Set;
33779
+ extrasAgain = new Set;
33780
+ timers = new Map;
33781
+ lastFailure = new Map;
33782
+ retries = new Map;
33783
+ watchers = new Map;
33784
+ pending = new Set;
33785
+ offs = [];
33786
+ stopped = false;
33787
+ slots;
33788
+ constructor(d) {
33789
+ this.d = d;
33790
+ this.slots = d.slots ?? SLOTS;
33791
+ const { hub } = d;
33792
+ this.offs.push(hub.on("agent", (id, _seq, event) => {
33793
+ if (event.type === "tool-end" || event.type === "turn-end")
33794
+ this.signal(id, "edit");
33795
+ }), hub.on("filesChanged", (id) => this.signal(id, "edit")), hub.on("termData", (id, stream) => {
33796
+ if (stream === SHELL_STREAM)
33797
+ this.signal(id, "shell");
33798
+ }), hub.on("turnSettled", (id) => this.signal(id, "now")), hub.on("repoTick", (repoId) => {
33799
+ for (const id of this.d.rowsOf(repoId)) {
33800
+ this.invalidate(id);
33801
+ if (this.hot(id))
33802
+ this.signal(id, "now");
33803
+ }
33804
+ }));
33805
+ }
33806
+ now() {
33807
+ return (this.d.now ?? Date.now)();
33808
+ }
33809
+ get(id) {
33810
+ return this.records.get(id);
33811
+ }
33812
+ counts(id) {
33813
+ const rec = this.records.get(id);
33814
+ if (!rec)
33815
+ return;
33816
+ const { ahead, behind, unpushed } = rec.core;
33817
+ return {
33818
+ ...ahead === undefined ? {} : { ahead },
33819
+ ...behind === undefined ? {} : { behind },
33820
+ ...unpushed === undefined ? {} : { unpushed },
33821
+ dirty: rec.core.files.length
33822
+ };
33823
+ }
33824
+ frame(id) {
33825
+ const rec = this.records.get(id);
33826
+ if (!rec)
33827
+ return null;
33828
+ const { ahead, behind, unpushed, head } = rec.core;
33829
+ const x = rec.extras;
33830
+ const files = x ? rec.core.files.map((f) => ({ ...f, ...x.counts.get(f.path) ?? {} })) : rec.core.files;
33831
+ return {
33832
+ version: rec.version,
33833
+ files,
33834
+ ...ahead === undefined ? {} : { ahead },
33835
+ ...behind === undefined ? {} : { behind },
33836
+ ...unpushed === undefined ? {} : { unpushed },
33837
+ ...head === undefined ? {} : { head },
33838
+ ...x ? { extras: { committed: x.committed ?? [], ...x.turn ? { turn: x.turn } : {} } } : {}
33839
+ };
33840
+ }
33841
+ async current(id, waitExtrasMs = 0) {
33842
+ if (!this.records.has(id))
33843
+ await this.refresh(id);
33844
+ const rec = this.records.get(id);
33845
+ if (!rec)
33846
+ return null;
33847
+ if (!rec.extras && this.d.interested(id)) {
33848
+ this.scheduleExtras(id);
33849
+ const run = this.extrasRunning.get(id);
33850
+ if (run && waitExtrasMs > 0) {
33851
+ await Promise.race([run, new Promise((r) => setTimeout(r, waitExtrasMs))]);
33852
+ }
33853
+ }
33854
+ return this.frame(id);
33855
+ }
33856
+ async full(id) {
33857
+ const rec = await this.refresh(id);
33858
+ if (!rec)
33859
+ return { frame: null, moved: false };
33860
+ if (!rec.extras) {
33861
+ this.scheduleExtras(id, true);
33862
+ await this.extrasRunning.get(id);
33863
+ }
33864
+ return { frame: this.frame(id), moved: rec.moved };
33865
+ }
33866
+ invalidate(id) {
33867
+ this.marks.set(id, (this.marks.get(id) ?? 0) + 1);
33868
+ const rec = this.records.get(id);
33869
+ if (rec)
33870
+ rec.staleSince ??= this.now();
33871
+ }
33872
+ invalidateAll() {
33873
+ this.markAll++;
33874
+ const now = this.now();
33875
+ for (const [id, rec] of this.records) {
33876
+ rec.staleSince ??= now;
33877
+ if (this.hot(id))
33878
+ this.kick(id);
33879
+ }
33880
+ }
33881
+ forget(id) {
33882
+ this.records.delete(id);
33883
+ this.marks.delete(id);
33884
+ this.lastFailure.delete(id);
33885
+ this.unwatch(id);
33886
+ const retry = this.retries.get(id);
33887
+ if (retry) {
33888
+ clearTimeout(retry);
33889
+ this.retries.delete(id);
33890
+ }
33891
+ const t = this.timers.get(id);
33892
+ if (t) {
33893
+ clearTimeout(t.t);
33894
+ this.timers.delete(id);
33895
+ t.done();
33896
+ }
33897
+ }
33898
+ refresh(id) {
33899
+ this.invalidate(id);
33900
+ if (this.d.going(id))
33901
+ return Promise.resolve(null);
33902
+ if (this.inSideEffects.has(id)) {
33903
+ const rec = this.records.get(id);
33904
+ return Promise.resolve(rec ? { ...rec, moved: false } : null);
33905
+ }
33906
+ return this.reading.get(id) ?? this.readOne(id);
33907
+ }
33908
+ refreshStale(ids) {
33909
+ const reads = [];
33910
+ const floor = [];
33911
+ const now = this.now();
33912
+ for (const id of new Set(ids)) {
33913
+ if (this.d.going(id))
33914
+ continue;
33915
+ const running = this.reading.get(id);
33916
+ if (running) {
33917
+ reads.push(running.then(() => false));
33918
+ continue;
33919
+ }
33920
+ const rec = this.records.get(id);
33921
+ if (!rec) {
33922
+ const failed = this.lastFailure.get(id);
33923
+ if (failed === undefined || now - failed > (this.d.floorMs ?? FLOOR_MS))
33924
+ reads.push(this.readMoved(id));
33925
+ continue;
33926
+ }
33927
+ if (rec.failedAt !== undefined && this.retries.has(id))
33928
+ continue;
33929
+ if (rec.staleSince !== undefined && this.hot(id)) {
33930
+ reads.push(this.readMoved(id));
33931
+ continue;
33932
+ }
33933
+ if (rec.staleSince !== undefined || now - rec.readAt > (this.d.floorMs ?? FLOOR_MS)) {
33934
+ floor.push({ id, at: rec.staleSince ?? rec.readAt });
33935
+ }
33936
+ }
33937
+ floor.sort((a, b) => a.at - b.at);
33938
+ for (const { id } of floor.slice(0, this.d.floorBatch ?? FLOOR_BATCH))
33939
+ reads.push(this.readMoved(id));
33940
+ if (!reads.length)
33941
+ return null;
33942
+ const all = Promise.all(reads).then((moved) => moved.some(Boolean));
33943
+ this.track(all);
33944
+ return all;
33945
+ }
33946
+ signal(id, kind) {
33947
+ if (this.stopped)
33948
+ return;
33949
+ const prev = this.timers.get(id);
33950
+ const delay = kind === "now" ? 0 : kind === "edit" ? this.d.editDelayMs ?? EDIT_DELAY_MS : this.d.shellQuietMs ?? SHELL_QUIET_MS2;
33951
+ const at = Date.now() + delay;
33952
+ if (prev) {
33953
+ if (kind !== "now" && prev.at <= at)
33954
+ return;
33955
+ clearTimeout(prev.t);
33956
+ this.timers.delete(id);
33957
+ prev.done();
33958
+ }
33959
+ if (kind === "now") {
33960
+ this.kick(id);
33961
+ return;
33962
+ }
33963
+ let done = () => {};
33964
+ const waited = new Promise((r) => {
33965
+ done = r;
33966
+ });
33967
+ this.track(waited);
33968
+ const t = setTimeout(() => {
33969
+ this.timers.delete(id);
33970
+ done();
33971
+ this.kick(id);
33972
+ }, delay);
33973
+ t.unref?.();
33974
+ this.timers.set(id, { at, t, done });
33975
+ }
33976
+ kick(id) {
33977
+ this.invalidate(id);
33978
+ if (this.d.going(id))
33979
+ return;
33980
+ const read = this.reading.get(id) ?? this.readOne(id);
33981
+ this.track(read.then((rec) => {
33982
+ if (rec?.moved)
33983
+ this.d.hub.emit("worktreesChanged");
33984
+ }));
33985
+ }
33986
+ watch(id) {
33987
+ const r = this.d.readable(id);
33988
+ if (!r || this.watchers.has(id) || this.stopped)
33989
+ return;
33990
+ if (this.records.has(id))
33991
+ this.scheduleExtras(id);
33992
+ let timer = null;
33993
+ const ignore = new Set([".git"]);
33994
+ let w;
33995
+ try {
33996
+ w = watch2(r.path, { recursive: true }, (_event, filename) => {
33997
+ const name = filename ? String(filename) : "";
33998
+ if (!name)
33999
+ return;
34000
+ for (const dir of ignore)
34001
+ if (name === dir || name.startsWith(`${dir}/`))
34002
+ return;
34003
+ if (timer)
34004
+ clearTimeout(timer);
34005
+ timer = setTimeout(() => {
34006
+ timer = null;
34007
+ this.signal(id, "edit");
34008
+ }, this.d.watchDelayMs ?? WATCH_DELAY_MS);
34009
+ timer.unref?.();
34010
+ });
34011
+ } catch (e) {
34012
+ log.warn(id, "could not watch the worktree", e);
34013
+ return;
34014
+ }
34015
+ w.on("error", (e) => {
34016
+ log.warn(id, "the worktree watcher stopped", e);
34017
+ this.unwatch(id);
34018
+ });
34019
+ w.unref?.();
34020
+ this.watchers.set(id, { w, ignore });
34021
+ fireAndForget(id, git(r.path, "ls-files", "-o", "-i", "--exclude-standard", "--directory").then((res) => {
34022
+ if (!res.ok)
34023
+ return;
34024
+ for (const line of res.out.split(`
34025
+ `)) {
34026
+ const dir = line.replace(/\/$/, "");
34027
+ if (dir)
34028
+ ignore.add(dir);
34029
+ }
34030
+ }), "ignored folders for the watcher");
34031
+ }
34032
+ unwatch(id) {
34033
+ const w = this.watchers.get(id);
34034
+ if (!w)
34035
+ return;
34036
+ this.watchers.delete(id);
34037
+ w.w.close();
34038
+ }
34039
+ async settled() {
34040
+ let waited = false;
34041
+ while (this.pending.size) {
34042
+ waited = true;
34043
+ await Promise.all(this.pending);
34044
+ }
34045
+ return waited;
34046
+ }
34047
+ stop() {
34048
+ this.stopped = true;
34049
+ for (const off of this.offs)
34050
+ off();
34051
+ for (const [id, t] of this.timers) {
34052
+ clearTimeout(t.t);
34053
+ this.timers.delete(id);
34054
+ t.done();
34055
+ }
34056
+ for (const t of this.retries.values())
34057
+ clearTimeout(t);
34058
+ this.retries.clear();
34059
+ for (const id of [...this.watchers.keys()])
34060
+ this.unwatch(id);
34061
+ }
34062
+ hot(id) {
34063
+ return this.d.interested(id) || this.d.shown(id) || this.d.live(id);
34064
+ }
34065
+ markOf(id) {
34066
+ return (this.marks.get(id) ?? 0) + this.markAll;
34067
+ }
34068
+ track(p) {
34069
+ this.pending.add(p);
34070
+ const done = () => this.pending.delete(p);
34071
+ p.then(done, done);
34072
+ return p;
34073
+ }
34074
+ readMoved(id) {
34075
+ return this.readOne(id).then((rec) => !!rec?.moved);
34076
+ }
34077
+ readOne(id) {
34078
+ const read = (async () => {
34079
+ for (let reruns = 0;; ) {
34080
+ const mark = this.markOf(id);
34081
+ const r = this.d.readable(id);
34082
+ if (!r) {
34083
+ this.records.delete(id);
34084
+ return null;
34085
+ }
34086
+ let core = null;
34087
+ const key = this.hot(id) ? "tree:fg" : "tree:bg";
34088
+ const cap = key === "tree:fg" ? this.slots.fg : this.slots.bg;
34089
+ try {
34090
+ core = await withSlot(key, cap, () => (this.d.readCore ?? this.readCore)(r));
34091
+ treeReads.core++;
34092
+ } catch (e) {
34093
+ log.warn(id, "git state read failed", e);
34094
+ }
34095
+ if (this.d.going(id))
34096
+ return null;
34097
+ if (this.markOf(id) !== mark) {
34098
+ treeReads.superseded++;
34099
+ continue;
34100
+ }
34101
+ if (!core) {
34102
+ this.noteFailure(id);
34103
+ return null;
34104
+ }
34105
+ let again = false;
34106
+ if (r.wt) {
34107
+ this.inSideEffects.add(id);
34108
+ try {
34109
+ again = await this.d.afterCore(r, core);
34110
+ } catch (e) {
34111
+ log.warn(id, "git state side effects failed", e);
34112
+ } finally {
34113
+ this.inSideEffects.delete(id);
34114
+ }
34115
+ }
34116
+ if (again && reruns++ === 0)
34117
+ continue;
34118
+ if (this.markOf(id) !== mark) {
34119
+ treeReads.superseded++;
34120
+ continue;
34121
+ }
34122
+ return this.store(id, core);
34123
+ }
34124
+ })().finally(() => this.reading.delete(id));
34125
+ this.reading.set(id, read);
34126
+ return read;
34127
+ }
34128
+ readCore = async (r) => {
34129
+ const isMain = r.wt?.kind === "main";
34130
+ const countable = !isMain && (!!r.wt || !!r.branch);
34131
+ const [entries, ab, unpushed, head, branch] = await Promise.all([
34132
+ statusEntries(r.path),
34133
+ isMain ? this.d.trunkBehind(r.id, r.path) : countable ? aheadBehind(r.path, r.base) : Promise.resolve({}),
34134
+ this.d.prOpen(r.id) ? aheadUpstream(r.path, r.branch) : Promise.resolve(null),
34135
+ git(r.path, "rev-parse", "HEAD"),
34136
+ git(r.path, "branch", "--show-current")
34137
+ ]);
34138
+ const files = entries.files.map((f) => ({
34139
+ path: f.path,
34140
+ xy: f.xy,
34141
+ ...entries.folded.has(f.path) ? { add: f.add, del: f.del } : {}
34142
+ }));
34143
+ const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat5(join30(r.path, f.path)).then((s) => `${s.mtimeMs}:${s.size}`, () => "-")));
34144
+ const t0 = performance.now();
34145
+ const counts = { ...ab, ...unpushed === null ? {} : { unpushed } };
34146
+ const signature = String(Bun.hash([
34147
+ head.ok ? head.out : "",
34148
+ counts.ahead,
34149
+ counts.behind,
34150
+ counts.unpushed,
34151
+ ...files.map((f) => `${f.xy}${f.path}`),
34152
+ ...stats
34153
+ ].join("\x00")));
34154
+ noteSync("tree.signature", performance.now() - t0);
34155
+ return {
34156
+ files,
34157
+ ...counts,
34158
+ ...head.ok ? { head: head.out } : {},
34159
+ ...branch.ok && branch.out ? { branch: branch.out } : {},
34160
+ folded: entries.folded,
34161
+ signature
34162
+ };
34163
+ };
34164
+ readExtras = async (r, core, folded) => {
34165
+ const isMain = r.wt?.kind === "main";
34166
+ const counts = await countEntries(r.path, core.files, folded);
34167
+ const committed = !isMain && (core.ahead ?? 0) > 0 ? await committedFiles(r.path, r.base) : undefined;
34168
+ const turn = r.wt && core.files.length > 0 ? await turnChanges(r.path, r.wt.id, r.base, !isMain, core.files) : null;
34169
+ return { counts, ...committed ? { committed } : {}, ...turn ? { turn } : {} };
34170
+ };
34171
+ store(id, read) {
34172
+ const { folded, signature, ...core } = read;
34173
+ const prev = this.records.get(id);
34174
+ const now = this.now();
34175
+ const retry = this.retries.get(id);
34176
+ if (retry) {
34177
+ clearTimeout(retry);
34178
+ this.retries.delete(id);
34179
+ }
34180
+ this.lastFailure.delete(id);
34181
+ if (prev && prev.signature === signature) {
34182
+ prev.readAt = now;
34183
+ prev.staleSince = undefined;
34184
+ prev.failedAt = undefined;
34185
+ prev.failures = 0;
34186
+ return { ...prev, moved: false };
34187
+ }
34188
+ const before = prev && this.counts(id);
34189
+ const rec = {
34190
+ version: (prev?.version ?? 0) + 1,
34191
+ core,
34192
+ signature,
34193
+ folded,
34194
+ readAt: now,
34195
+ failures: 0
34196
+ };
34197
+ this.records.set(id, rec);
34198
+ const moved = !sameCounts(before, this.counts(id));
34199
+ if (this.d.interested(id)) {
34200
+ this.d.hub.emit("treeChanged", id);
34201
+ this.scheduleExtras(id);
34202
+ }
34203
+ return { ...rec, moved };
34204
+ }
34205
+ noteFailure(id) {
34206
+ treeReads.failed++;
34207
+ const now = this.now();
34208
+ const rec = this.records.get(id);
34209
+ this.lastFailure.set(id, now);
34210
+ if (rec) {
34211
+ if (rec.failedAt === undefined)
34212
+ log.warn(id, "git could not read the tree; the last numbers stand");
34213
+ rec.failedAt = now;
34214
+ rec.failures++;
34215
+ rec.readAt = now;
34216
+ rec.staleSince = undefined;
34217
+ }
34218
+ if (!this.d.interested(id) || this.retries.has(id) || this.stopped)
34219
+ return;
34220
+ const delay = RETRY_MS[Math.min((rec?.failures ?? 1) - 1, RETRY_MS.length - 1)] ?? RETRY_MS[0];
34221
+ const t = setTimeout(() => {
34222
+ this.retries.delete(id);
34223
+ if (this.d.interested(id))
34224
+ this.kick(id);
34225
+ }, delay);
34226
+ t.unref?.();
34227
+ this.retries.set(id, t);
34228
+ }
34229
+ scheduleExtras(id, asked = false) {
34230
+ if (this.extrasRunning.has(id)) {
34231
+ this.extrasAgain.add(id);
34232
+ return;
34233
+ }
34234
+ const rec = this.records.get(id);
34235
+ if (!rec || rec.extras || !(asked || this.d.interested(id)) || this.stopped)
34236
+ return;
34237
+ const version = rec.version;
34238
+ const run = (async () => {
34239
+ const r = this.d.readable(id);
34240
+ if (!r)
34241
+ return;
34242
+ let extras;
34243
+ try {
34244
+ extras = await withSlot("tree:extras", this.slots.extras, () => (this.d.readExtras ?? this.readExtras)(r, rec.core, rec.folded));
34245
+ treeReads.extras++;
34246
+ } catch (e) {
34247
+ log.warn(id, "git state extras failed", e);
34248
+ return;
34249
+ }
34250
+ const now = this.records.get(id);
34251
+ if (!now || now.version !== version || this.d.going(id)) {
34252
+ treeReads.superseded++;
34253
+ return;
34254
+ }
34255
+ now.extras = extras;
34256
+ now.version++;
34257
+ if (this.d.interested(id))
34258
+ this.d.hub.emit("treeChanged", id);
34259
+ })().finally(() => {
34260
+ this.extrasRunning.delete(id);
34261
+ if (this.extrasAgain.delete(id))
34262
+ this.scheduleExtras(id);
34263
+ });
34264
+ this.extrasRunning.set(id, run);
34265
+ this.track(run);
34266
+ }
34267
+ }
34268
+
33466
34269
  // ../daemon/src/worktrees/trunk.ts
33467
34270
  var FETCH_EVERY_MS = 5 * 60000;
33468
34271
  var OPEN_FETCH_MIN_MS = 60000;
@@ -33489,11 +34292,9 @@ class Trunk {
33489
34292
  const main = this.d.state.worktree(mainId);
33490
34293
  if (main)
33491
34294
  this.noteFetch(main.repoId, r);
33492
- if (!r.ok) {
33493
- log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
34295
+ if (!r.ok)
33494
34296
  return;
33495
- }
33496
- this.d.invalidateCounts();
34297
+ this.d.invalidateCounts(mainId);
33497
34298
  this.d.hub.emit("worktreesChanged");
33498
34299
  if (await behindUpstream(path) ?? 0)
33499
34300
  await this.follow(mainId);
@@ -33512,11 +34313,9 @@ class Trunk {
33512
34313
  this.lastFetch.set(repo.path, Date.now());
33513
34314
  const f = await withFetchLock(repo.path, () => run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT));
33514
34315
  this.noteFetch(repo.id, f);
33515
- if (!f.ok) {
33516
- log.warn("fetch", `could not fetch ${repo.path}: ${f.err.slice(0, 200)}`);
34316
+ if (!f.ok)
33517
34317
  return;
33518
- }
33519
- this.d.invalidateCounts();
34318
+ this.d.invalidateCounts(main.id);
33520
34319
  await this.follow(main.id);
33521
34320
  }
33522
34321
  noteFetch(repoId, r) {
@@ -33524,6 +34323,11 @@ class Trunk {
33524
34323
  const next = r.ok ? { at: Date.now() } : { ...was.at ? { at: was.at } : {}, failed: r.err.split(`
33525
34324
  `).find((l) => l.trim()) ?? "fetch failed" };
33526
34325
  this.fetches.set(repoId, next);
34326
+ const path = this.d.state.repo(repoId)?.path ?? repoId;
34327
+ if (next.failed !== undefined && next.failed !== was.failed)
34328
+ log.warn("fetch", `could not fetch ${path}: ${next.failed}`);
34329
+ if (r.ok && was.failed !== undefined)
34330
+ log.info("fetch", `origin answers again for ${path}`);
33527
34331
  if (was.failed !== next.failed || was.at !== next.at)
33528
34332
  this.d.hub.emit("worktreesChanged");
33529
34333
  }
@@ -33538,7 +34342,7 @@ class Trunk {
33538
34342
  this.noteFetch(repo.id, f);
33539
34343
  if (!f.ok)
33540
34344
  return { ok: false, message: refused2("fetch failed", f) };
33541
- this.d.invalidateCounts();
34345
+ this.d.invalidateCounts(main.id);
33542
34346
  return this.follow(main.id);
33543
34347
  }
33544
34348
  async catchUp(repoId) {
@@ -33559,7 +34363,7 @@ class Trunk {
33559
34363
  else
33560
34364
  this.stale.delete(repo.id);
33561
34365
  if (ff.moved) {
33562
- this.d.invalidateCounts();
34366
+ this.d.invalidateCounts(mainId);
33563
34367
  this.d.hub.emit("worktreesChanged");
33564
34368
  } else if (was !== ff.stale)
33565
34369
  this.d.hub.emit("worktreesChanged");
@@ -33656,6 +34460,7 @@ function stopOf(prev, next) {
33656
34460
  }
33657
34461
 
33658
34462
  // ../daemon/src/worktrees/service.ts
34463
+ var LIST_FLOOR_MS = 1e4;
33659
34464
  var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
33660
34465
  var BASE_IGNORE = "base .gitignore";
33661
34466
  var REFUSAL_CHARS = 400;
@@ -33702,7 +34507,7 @@ class WorktreeService {
33702
34507
  going = new Set;
33703
34508
  arriving = new Set;
33704
34509
  firstPrompt = new Map;
33705
- counts;
34510
+ tree;
33706
34511
  handChecked = new Map;
33707
34512
  naming = new Set;
33708
34513
  discoverCache = new Map;
@@ -33714,15 +34519,23 @@ class WorktreeService {
33714
34519
  this.d = d;
33715
34520
  this.runs = d.runs ?? new RunService({ state: d.state, hub: d.hub });
33716
34521
  this.archive = new WorktreeArchive(d.paths.archiveDir);
33717
- this.counts = new Counts({
33718
- read: (id) => this.readCounts(id),
33719
- going: (id) => this.going.has(id) || this.arriving.has(id)
33720
- });
33721
34522
  this.trunk = new Trunk({
33722
34523
  state: d.state,
33723
34524
  hub: d.hub,
33724
- counts: (main) => this.counts.get(main.id),
33725
- invalidateCounts: () => this.counts.stale()
34525
+ counts: (main) => this.tree.counts(main.id),
34526
+ invalidateCounts: (mainId) => this.tree.invalidate(mainId)
34527
+ });
34528
+ this.tree = new TreeState({
34529
+ hub: d.hub,
34530
+ readable: (id) => this.readable(id),
34531
+ going: (id) => this.going.has(id) || this.arriving.has(id) || this.spare.checkingOut(id),
34532
+ interested: (id) => d.interested?.(id) ?? false,
34533
+ shown: (id) => d.shown?.(id) ?? false,
34534
+ live: (id) => d.runtime.busy(id),
34535
+ trunkBehind: (mainId, path) => this.trunk.behind(mainId, path),
34536
+ prOpen: (id) => d.state.worktree(id)?.pr?.state === "open",
34537
+ afterCore: (r, core) => this.afterCore(r, core),
34538
+ rowsOf: (repoId) => this.rowsOf(repoId)
33726
34539
  });
33727
34540
  this.spare = new SparePool({
33728
34541
  state: d.state,
@@ -33762,7 +34575,6 @@ class WorktreeService {
33762
34575
  this.usage.set(worktreeId, { used, size, ...cost !== undefined ? { cost } : {} });
33763
34576
  });
33764
34577
  d.hub.on("turnSettled", (worktreeId, turn) => {
33765
- this.counts.stale(worktreeId);
33766
34578
  if (turn.end === "done") {
33767
34579
  const wt = d.state.worktree(worktreeId);
33768
34580
  const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
@@ -33854,8 +34666,8 @@ class WorktreeService {
33854
34666
  if (!await isGitRepo(repo.path)) {
33855
34667
  throw new UserError(`git no longer reads ${repo.name} as a checkout, so there is nothing to start a worktree from. If \`git config core.bare\` says true there, \`git config --unset core.bare\` puts it back.`);
33856
34668
  }
33857
- const { id, dir } = freeSlot(join30(this.d.paths.worktreesDir, repo.name));
33858
- const wtPath = join30(this.d.paths.worktreesDir, repo.name, dir);
34669
+ const { id, dir } = freeSlot(join31(this.d.paths.worktreesDir, repo.name));
34670
+ const wtPath = join31(this.d.paths.worktreesDir, repo.name, dir);
33859
34671
  const branch = `toyon/${dir}`;
33860
34672
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
33861
34673
  const wt = {
@@ -33905,14 +34717,12 @@ class WorktreeService {
33905
34717
  async carryMainOrThrow(repo, wt, move) {
33906
34718
  const main = repo.defaultBranch;
33907
34719
  if (!move) {
33908
- const count = this.counts.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
34720
+ const count = this.tree.counts(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
33909
34721
  return { branch: main, moved: false, count };
33910
34722
  }
33911
34723
  let count = 0;
33912
- let left;
33913
34724
  const unmoved = await withRepoLock(repo.path, async () => {
33914
34725
  count = (await statusFiles(repo.path)).length;
33915
- left = count;
33916
34726
  if (count === 0)
33917
34727
  return;
33918
34728
  const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
@@ -33923,7 +34733,6 @@ class WorktreeService {
33923
34733
  const sha = after.out.trim();
33924
34734
  if (!after.ok || before.ok && before.out.trim() === sha)
33925
34735
  return;
33926
- left = 0;
33927
34736
  const entry = async () => {
33928
34737
  const list = await git(repo.path, "stash", "list", "--format=%H");
33929
34738
  const i = list.out.split(`
@@ -33932,7 +34741,7 @@ class WorktreeService {
33932
34741
  };
33933
34742
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
33934
34743
  const restored = (carried.ok ? carried.out.split(`
33935
- `).filter(Boolean) : []).filter((p) => !existsSync34(join30(wt.path, p)));
34744
+ `).filter(Boolean) : []).filter((p) => !existsSync35(join31(wt.path, p)));
33936
34745
  const applied = await git(wt.path, "stash", "apply", sha);
33937
34746
  if (applied.ok) {
33938
34747
  const ref = await entry();
@@ -33947,14 +34756,11 @@ class WorktreeService {
33947
34756
  }
33948
34757
  const ref = await entry();
33949
34758
  const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
33950
- left = back?.ok ? count : undefined;
33951
34759
  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}"`;
33952
34760
  });
33953
34761
  const mainWt = this.mainOf(repo.id);
33954
34762
  if (mainWt) {
33955
- if (left !== undefined)
33956
- this.counts.put(mainWt.id, { ...this.counts.get(mainWt.id), dirty: left });
33957
- this.counts.stale(mainWt.id);
34763
+ await this.tree.refresh(mainWt.id);
33958
34764
  }
33959
34765
  return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
33960
34766
  }
@@ -33972,9 +34778,9 @@ class WorktreeService {
33972
34778
  const branch = kind === "pr" ? `pr/${number}` : ref;
33973
34779
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
33974
34780
  let slug = branchSlug(title) || "branch";
33975
- if (existsSync34(join30(this.d.paths.worktreesDir, repo.name, slug)))
34781
+ if (existsSync35(join31(this.d.paths.worktreesDir, repo.name, slug)))
33976
34782
  slug = `${slug}-${shortId().slice(0, 4)}`;
33977
- const wtPath = join30(this.d.paths.worktreesDir, repo.name, slug);
34783
+ const wtPath = join31(this.d.paths.worktreesDir, repo.name, slug);
33978
34784
  await withRepoLock(repo.path, async () => {
33979
34785
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
33980
34786
  if (out)
@@ -34223,6 +35029,7 @@ class WorktreeService {
34223
35029
  const sessionId = this.d.state.session(worktreeId);
34224
35030
  const placed = repo.promptedAt;
34225
35031
  this.d.state.removeWorktree(worktreeId);
35032
+ this.tree.forget(worktreeId);
34226
35033
  if (repo.promptedAt !== placed)
34227
35034
  this.d.hub.emit("reposChanged");
34228
35035
  releasePort(wt.proxyPort);
@@ -34240,7 +35047,7 @@ class WorktreeService {
34240
35047
  }
34241
35048
  async teardown(wt, repo) {
34242
35049
  const cmds = repo.config.teardown ?? [];
34243
- if (cmds.length === 0 || !existsSync34(wt.path))
35050
+ if (cmds.length === 0 || !existsSync35(wt.path))
34244
35051
  return;
34245
35052
  for (const cmd of cmds) {
34246
35053
  const code = await runSetup(cmd, wt.path, (line) => this.d.hub.emit("log", wt.id, "setup", line), worktreeEnv(wt, repo), { timeoutMs: timeoutFor(repo.config, "setup") });
@@ -34249,7 +35056,7 @@ class WorktreeService {
34249
35056
  }
34250
35057
  }
34251
35058
  async keep(repo, wt) {
34252
- const index = join30(this.d.paths.archiveDir, `${wt.id}.index`);
35059
+ const index = join31(this.d.paths.archiveDir, `${wt.id}.index`);
34253
35060
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
34254
35061
  if (!kept)
34255
35062
  log.warn(wt.id, "could not keep its git state");
@@ -34286,11 +35093,11 @@ class WorktreeService {
34286
35093
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
34287
35094
  }
34288
35095
  plansScratch(worktreeId) {
34289
- return join30(this.d.paths.archiveDir, `${worktreeId}.plans`);
35096
+ return join31(this.d.paths.archiveDir, `${worktreeId}.plans`);
34290
35097
  }
34291
35098
  keepPlans(wt) {
34292
- const from = join30(wt.path, PLANS_DIR);
34293
- if (!existsSync34(from))
35099
+ const from = join31(wt.path, PLANS_DIR);
35100
+ if (!existsSync35(from))
34294
35101
  return;
34295
35102
  try {
34296
35103
  cpSync(from, this.plansScratch(wt.id), { recursive: true });
@@ -34341,6 +35148,7 @@ class WorktreeService {
34341
35148
  const repo = this.d.state.repo(wt.repoId);
34342
35149
  const sessionId = this.d.state.session(wt.id);
34343
35150
  this.d.state.removeWorktree(wt.id);
35151
+ this.tree.forget(wt.id);
34344
35152
  if (!repo || wt.kind !== "worktree") {
34345
35153
  this.deleteChat(wt.id);
34346
35154
  return;
@@ -34429,7 +35237,7 @@ class WorktreeService {
34429
35237
  }
34430
35238
  archivedAttachment(archiveId, file) {
34431
35239
  const files = this.archive.chatFiles(archiveId);
34432
- return files && isAttachmentFile(file) ? join30(files.attachments, file) : null;
35240
+ return files && isAttachmentFile(file) ? join31(files.attachments, file) : null;
34433
35241
  }
34434
35242
  archivedGit(archiveId) {
34435
35243
  const rec = this.archive.get(archiveId);
@@ -34443,7 +35251,8 @@ class WorktreeService {
34443
35251
  if (!kept)
34444
35252
  return null;
34445
35253
  try {
34446
- return await kept.status();
35254
+ const { files, committed, head } = await kept.status();
35255
+ return { version: 0, files, head, extras: { committed } };
34447
35256
  } catch (e) {
34448
35257
  log.warn(archiveId, "reading its kept changes failed", e);
34449
35258
  return null;
@@ -34484,7 +35293,7 @@ class WorktreeService {
34484
35293
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
34485
35294
  if (rec.auto)
34486
35295
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
34487
- const path = existsSync34(old.path) ? join30(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
35296
+ const path = existsSync35(old.path) ? join31(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
34488
35297
  const { variant: _variant, ...rest } = old;
34489
35298
  const wt = {
34490
35299
  ...rest,
@@ -34523,11 +35332,11 @@ class WorktreeService {
34523
35332
  run.finish(0);
34524
35333
  this.arriving.delete(wt.id);
34525
35334
  wt.branch = branch;
34526
- move(chat.plans, join30(path, PLANS_DIR));
35335
+ move(chat.plans, join31(path, PLANS_DIR));
34527
35336
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
34528
35337
  if (!dropped.ok)
34529
35338
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
34530
- this.counts.stale(wt.id);
35339
+ this.tree.invalidate(wt.id);
34531
35340
  this.d.state.save();
34532
35341
  this.d.hub.emit("worktreesChanged");
34533
35342
  fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path), "setup + start");
@@ -34590,6 +35399,7 @@ class WorktreeService {
34590
35399
  agent.unqueue(0);
34591
35400
  await this.d.runtime.stop(wt.id);
34592
35401
  this.d.state.removeWorktree(wt.id, { refused: true });
35402
+ this.tree.forget(wt.id);
34593
35403
  releasePort(wt.proxyPort);
34594
35404
  }
34595
35405
  async deleteArchived(archiveId) {
@@ -34598,12 +35408,12 @@ class WorktreeService {
34598
35408
  return;
34599
35409
  const repo = this.repoOf(rec);
34600
35410
  const repoPath = repo?.path ?? rec.repoPath;
34601
- if (rec.kept && existsSync34(repoPath)) {
35411
+ if (rec.kept && existsSync35(repoPath)) {
34602
35412
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
34603
35413
  if (!r.ok)
34604
35414
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
34605
35415
  }
34606
- if (rec.worktree.lands?.length && existsSync34(repoPath))
35416
+ if (rec.worktree.lands?.length && existsSync35(repoPath))
34607
35417
  await dropLandRefs(repoPath, archiveId);
34608
35418
  this.archive.delete(archiveId);
34609
35419
  this.d.drafts?.drop(archiveId);
@@ -34711,9 +35521,9 @@ class WorktreeService {
34711
35521
  }
34712
35522
  }
34713
35523
  async mirrorBaseIgnore(repo) {
34714
- const src = join30(repo.path, ".gitignore");
34715
- const tracked = existsSync34(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
34716
- const mirror = existsSync34(src) && !tracked;
35524
+ const src = join31(repo.path, ".gitignore");
35525
+ const tracked = existsSync35(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
35526
+ const mirror = existsSync35(src) && !tracked;
34717
35527
  const lines = mirror ? readFileSync20(src, "utf8").split(`
34718
35528
  `).filter((l) => l.trim() !== "") : [];
34719
35529
  await excludeBlock(repo.path, BASE_IGNORE, lines);
@@ -34726,9 +35536,9 @@ class WorktreeService {
34726
35536
  await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
34727
35537
  run.finish(0);
34728
35538
  }
34729
- const srcNm = join30(depsSource, "node_modules");
34730
- const dstNm = join30(wt.path, "node_modules");
34731
- if (existsSync34(srcNm) && !existsSync34(dstNm)) {
35539
+ const srcNm = join31(depsSource, "node_modules");
35540
+ const dstNm = join31(wt.path, "node_modules");
35541
+ if (existsSync35(srcNm) && !existsSync35(dstNm)) {
34732
35542
  const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
34733
35543
  const r = await cloneTree(srcNm, dstNm);
34734
35544
  run.finish(0);
@@ -34738,10 +35548,10 @@ class WorktreeService {
34738
35548
  log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
34739
35549
  }
34740
35550
  for (const f of LOCAL_CONFIG_FILES) {
34741
- const src = join30(depsSource, f);
34742
- if (!existsSync34(src) || existsSync34(join30(wt.path, f)))
35551
+ const src = join31(depsSource, f);
35552
+ if (!existsSync35(src) || existsSync35(join31(wt.path, f)))
34743
35553
  continue;
34744
- const r = await run("cp", [src, join30(wt.path, f)], wt.path);
35554
+ const r = await run("cp", [src, join31(wt.path, f)], wt.path);
34745
35555
  if (r.ok)
34746
35556
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
34747
35557
  else
@@ -34818,6 +35628,9 @@ class WorktreeService {
34818
35628
  this.d.state.save();
34819
35629
  this.d.hub.emit("worktreesChanged");
34820
35630
  }
35631
+ fingerprint(path) {
35632
+ return (this.d.fingerprint ?? treeFingerprint)(path);
35633
+ }
34821
35634
  setLanding(worktreeId, landing) {
34822
35635
  const wt = this.d.state.worktree(worktreeId);
34823
35636
  if (!wt || wt.landing === undefined && landing === undefined)
@@ -34898,7 +35711,7 @@ ${wt.landing.body}` : s;
34898
35711
  return;
34899
35712
  const { subject, body, unanswered, ...kept } = l;
34900
35713
  const message = opts.dropMessage ? {} : { ...subject ? { subject } : {}, ...body ? { body } : {}, ...unanswered ? { unanswered } : {} };
34901
- this.setLanding(wt.id, { ...kept, ...message, fingerprint: await treeFingerprint(wt.path) });
35714
+ this.setLanding(wt.id, { ...kept, ...message, fingerprint: await this.fingerprint(wt.path) });
34902
35715
  }
34903
35716
  async landedByHand(wt, mark) {
34904
35717
  const repo = this.d.state.requireRepo(wt.repoId);
@@ -34931,7 +35744,13 @@ ${wt.landing.body}` : s;
34931
35744
  return { ...result, message: "the commit-msg hook refused the message: another is being written" };
34932
35745
  }
34933
35746
  const { hook } = result;
34934
- const failure = hook ? { kind: "hook", hook: hook.name, toolId: hook.toolId, command: hook.command, text: hook.text } : result.conflict ? { kind: "conflict", base, how: result.conflict, ...result.step ? { step: result.step } : {} } : null;
35747
+ const failure = hook ? { kind: "hook", hook: hook.name, toolId: hook.toolId, command: hook.command, text: hook.text } : result.conflict ? {
35748
+ kind: "conflict",
35749
+ base,
35750
+ how: result.conflict,
35751
+ ...result.step ? { step: result.step } : {},
35752
+ ...result.carried ? { carried: result.carried } : {}
35753
+ } : null;
34935
35754
  return failure && this.d.fix.report(wt.id, failure) ? { ...result, asked: true } : result;
34936
35755
  }
34937
35756
  async fixPreview(worktreeId, clientId) {
@@ -34956,7 +35775,7 @@ ${wt.landing.body}` : s;
34956
35775
  if (status !== "working" && status !== "waiting")
34957
35776
  return null;
34958
35777
  const l = wt.landing;
34959
- if (!l || l.fingerprint === await treeFingerprint(wt.path))
35778
+ if (!l || l.fingerprint === await this.fingerprint(wt.path))
34960
35779
  return null;
34961
35780
  return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
34962
35781
  }
@@ -34988,9 +35807,17 @@ ${wt.landing.body}` : s;
34988
35807
  const release = this.d.runtime.agentFor(id)?.hold();
34989
35808
  this.ops.set(id, { op });
34990
35809
  this.d.hub.emit("worktreesChanged");
35810
+ const started = Date.now();
35811
+ const watchdog = setInterval(() => {
35812
+ const step = this.ops.get(id)?.step;
35813
+ const at = step ? ` at "${step}"` : "";
35814
+ log.warn(id, `${shipNoun(op)} still running after ${describeDuration(Date.now() - started)}${at}`);
35815
+ }, RUN_CEILING_MS);
35816
+ watchdog.unref?.();
34991
35817
  try {
34992
35818
  return await run();
34993
35819
  } finally {
35820
+ clearInterval(watchdog);
34994
35821
  this.ops.delete(id);
34995
35822
  release?.();
34996
35823
  this.d.hub.emit("worktreesChanged");
@@ -35031,10 +35858,12 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35031
35858
  const committed = await this.commitIfDirty(wt, message);
35032
35859
  if (committed && !committed.ok)
35033
35860
  return { result: committed };
35861
+ this.tree.invalidate(wt.id);
35034
35862
  const taken = await takeMainIn(wt.path, base, own, w);
35035
35863
  if (!taken.ok)
35036
35864
  return { result: taken };
35037
35865
  await this.headMoved(wt.id);
35866
+ this.tree.invalidate(wt.id);
35038
35867
  const pushed = await pushBranch(wt.path, wt.branch, w);
35039
35868
  if (!pushed.ok)
35040
35869
  return { result: pushed };
@@ -35047,6 +35876,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35047
35876
  if (committed && !committed.ok)
35048
35877
  return { result: committed };
35049
35878
  const mark = await landingMark(wt.path, base);
35879
+ this.tree.invalidate(wt.id);
35050
35880
  const taken = await takeMainIn(wt.path, base, own, w);
35051
35881
  if (!taken.ok)
35052
35882
  return { result: taken };
@@ -35135,6 +35965,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35135
35965
  const fetched = await this.fetchBase(wt, repo, w);
35136
35966
  if (!fetched.ok)
35137
35967
  return { result: fetched };
35968
+ this.tree.invalidate(wt.id);
35138
35969
  const taken = await takeMainIn(wt.path, base, own, w);
35139
35970
  if (!taken.ok)
35140
35971
  return { result: taken };
@@ -35145,6 +35976,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35145
35976
  break;
35146
35977
  }
35147
35978
  squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
35979
+ this.tree.invalidate(wt.id);
35148
35980
  const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
35149
35981
  await this.headMoved(wt.id);
35150
35982
  if (!built.ok || !built.sha)
@@ -35211,11 +36043,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35211
36043
  async restartFromMain(wt, base) {
35212
36044
  if (!hasOwnBranch(wt))
35213
36045
  return;
36046
+ this.tree.invalidate(wt.id);
35214
36047
  const r = await git(wt.path, "reset", "--keep", base);
35215
36048
  if (!r.ok)
35216
36049
  log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
35217
- this.counts.stale(wt.id);
35218
- await this.counts.refresh([wt.id]);
36050
+ await this.tree.refresh(wt.id);
35219
36051
  }
35220
36052
  async fetchBase(wt, repo, w) {
35221
36053
  if (!baseIsRemote(repo))
@@ -35225,13 +36057,22 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35225
36057
  return r;
35226
36058
  }
35227
36059
  async prMerged(worktreeId) {
35228
- const out = this.ops.get(worktreeId);
35229
- if (out)
35230
- return { ok: false, message: `${shipNoun(out.op)} is already running here` };
35231
36060
  const wt = this.d.state.worktree(worktreeId);
36061
+ const pr = wt?.pr ? `PR #${wt.pr.number}` : "the PR";
36062
+ const out = this.ops.get(worktreeId);
36063
+ if (out) {
36064
+ const message = `${shipNoun(out.op)} is already running here`;
36065
+ log.info(worktreeId, `${pr} merged on GitHub; ${message}, landing waits`);
36066
+ return { ok: false, message };
36067
+ }
35232
36068
  if (wt && prTaken(wt))
35233
- return { ok: true, message: `PR #${wt.pr?.number} merged; landed already` };
36069
+ return { ok: true, message: `${pr} merged; landed already` };
36070
+ log.info(worktreeId, `${pr} merged on GitHub; landing it`);
35234
36071
  const { mark, ...result } = await this.ship(worktreeId, "land", () => this.prMergedOp(worktreeId));
36072
+ if (result.ok)
36073
+ log.info(worktreeId, `landed: ${result.message}`);
36074
+ else
36075
+ log.warn(worktreeId, `landing failed: ${result.message}`);
35235
36076
  if (result.ok && wt)
35236
36077
  this.noteLanded(wt, result.message, this.siblingIds(wt), mark);
35237
36078
  return result;
@@ -35284,6 +36125,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35284
36125
  if (!fetched.ok)
35285
36126
  return fetched;
35286
36127
  }
36128
+ this.tree.invalidate(worktreeId);
35287
36129
  const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w, carry));
35288
36130
  if (taken.ok) {
35289
36131
  await this.headMoved(worktreeId);
@@ -35368,19 +36210,26 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35368
36210
  return found ?? undefined;
35369
36211
  }
35370
36212
  async headMoved(worktreeId) {
35371
- this.counts.stale(worktreeId);
35372
- await this.counts.refresh([worktreeId]);
36213
+ await this.tree.refresh(worktreeId);
35373
36214
  this.d.hub.emit("worktreesChanged");
35374
36215
  }
35375
36216
  async recountAll() {
35376
- this.counts.stale();
35377
- await this.counts.refresh(this.countedIds());
36217
+ this.tree.invalidateAll();
36218
+ for (;; ) {
36219
+ const reading = this.tree.refreshStale(this.countedIds());
36220
+ if (!reading)
36221
+ return;
36222
+ await reading;
36223
+ }
35378
36224
  }
35379
36225
  invalidateCounts(worktreeId) {
35380
- this.counts.stale(worktreeId);
36226
+ if (worktreeId)
36227
+ this.tree.invalidate(worktreeId);
36228
+ else
36229
+ this.tree.invalidateAll();
35381
36230
  }
35382
36231
  recount(repoId) {
35383
- this.counts.stale();
36232
+ this.tree.invalidateAll();
35384
36233
  this.d.hub.emit("repoTick", repoId);
35385
36234
  const repo = this.d.state.repo(repoId);
35386
36235
  if (repo && baseIsRemote(repo))
@@ -35410,7 +36259,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35410
36259
  }
35411
36260
  listDue(repoId) {
35412
36261
  const cached = this.discoverCache.get(repoId);
35413
- return !cached || Date.now() - cached.at >= COUNTS_FLOOR_MS;
36262
+ return !cached || Date.now() - cached.at >= LIST_FLOOR_MS;
35414
36263
  }
35415
36264
  async relist(repos) {
35416
36265
  const moved = await Promise.all(repos.map((repo) => {
@@ -35444,29 +36293,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35444
36293
  this.d.hub.emit("worktreesChanged");
35445
36294
  return any;
35446
36295
  }
35447
- async readCounts(id) {
35448
- const wt = this.d.state.worktree(id);
35449
- const found = wt ? null : this.discoveredById(id);
35450
- const row = wt ?? found;
35451
- const repo = row && this.d.state.repo(row.repoId);
35452
- if (!row || !repo)
35453
- return null;
35454
- const countable = wt ? !isMain(wt) : !!found?.branch;
35455
- const ab = countable ? await aheadBehind(row.path, baseOf(repo)) : await this.trunk.behind(id, row.path);
35456
- const dirty = (await statusFiles(row.path)).length;
35457
- const unpushed = await this.unpushed(id, row.path);
35458
- return { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty };
35459
- }
35460
- async unpushed(id, path) {
35461
- const wt = this.d.state.worktree(id);
35462
- if (wt?.pr?.state !== "open")
35463
- return;
35464
- return await aheadUpstream(path, wt.branch) ?? undefined;
35465
- }
35466
36296
  async freshCounts(worktreeId) {
35467
36297
  if (this.going.has(worktreeId))
35468
36298
  return {};
35469
- return await this.counts.read(worktreeId) ?? {};
36299
+ const rec = await this.tree.refresh(worktreeId);
36300
+ if (!rec || rec.failedAt !== undefined)
36301
+ return {};
36302
+ return { ...rec.core.ahead === undefined ? {} : { ahead: rec.core.ahead }, dirty: rec.core.files.length };
35470
36303
  }
35471
36304
  readable(id) {
35472
36305
  const wt = this.d.state.worktree(id);
@@ -35503,68 +36336,60 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35503
36336
  if (wt && !this.going.has(worktreeId))
35504
36337
  await markTurn(wt.path, wt.id);
35505
36338
  }
35506
- async gitStatus(worktreeId) {
35507
- if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35508
- return null;
35509
- const r = this.readable(worktreeId);
35510
- if (!r)
35511
- return this.archivedStatus(worktreeId);
35512
- try {
35513
- const isMain = r.wt?.kind === "main";
35514
- const [files, ab, unpushed, head, current] = await Promise.all([
35515
- statusFilesWithCounts(r.path),
35516
- isMain ? Promise.resolve({}) : aheadBehind(r.path, r.base),
35517
- this.unpushed(worktreeId, r.path),
35518
- git(r.path, "rev-parse", "HEAD"),
35519
- git(r.path, "branch", "--show-current")
35520
- ]);
35521
- if (r.wt && current.ok)
35522
- await this.followBranch(r.wt, current.out);
35523
- const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
35524
- const ahead = counts.ahead ?? 0;
35525
- const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.base) : undefined;
35526
- const turn = r.wt && files.length > 0 ? await turnChanges(r.path, r.wt.id, r.base, !isMain, files) : null;
35527
- if (r.wt && !isMain && hasOwnBranch(r.wt) && files.length === 0 && ahead === 0 && head.ok && this.handChecked.get(r.wt.id) !== head.out) {
35528
- this.handChecked.set(r.wt.id, head.out);
35529
- const mark = await handLanding(r.path, r.wt.branch, r.base, r.wt.lands?.at(-1)?.tip);
35530
- if (mark) {
35531
- await this.landedByHand(r.wt, mark);
35532
- return this.gitStatus(worktreeId);
35533
- }
36339
+ async afterCore(r, core) {
36340
+ const wt = r.wt;
36341
+ if (!wt)
36342
+ return false;
36343
+ if (this.ops.has(wt.id))
36344
+ return false;
36345
+ const isMain = wt.kind === "main";
36346
+ if (core.branch)
36347
+ await this.followBranch(wt, core.branch);
36348
+ const { files, head } = core;
36349
+ const ahead = core.ahead ?? 0;
36350
+ if (!isMain && hasOwnBranch(wt) && files.length === 0 && ahead === 0 && head && this.handChecked.get(wt.id) !== head) {
36351
+ this.handChecked.set(wt.id, head);
36352
+ const mark = await handLanding(r.path, wt.branch, r.base, wt.lands?.at(-1)?.tip);
36353
+ if (mark) {
36354
+ await this.landedByHand(wt, mark);
36355
+ return true;
35534
36356
  }
35535
- if (r.wt && !isMain && head.ok) {
35536
- const facts = { clean: files.length === 0, head: head.out, ahead };
35537
- if (prTaken(r.wt) && movedPastLand(r.wt, facts))
35538
- this.setPr(r.wt.id, undefined);
35539
- await this.refreshLanded(r.wt, facts);
35540
- }
35541
- if (r.wt?.landing && r.wt.landing.check !== "pending") {
35542
- const moved = r.wt.landing.fingerprint !== await treeFingerprint(r.path);
35543
- if (moved !== !!r.wt.landing.stale) {
35544
- const { stale: _was, ...rest } = r.wt.landing;
35545
- this.setLanding(r.wt.id, moved ? { ...rest, stale: true } : rest);
35546
- }
36357
+ }
36358
+ if (!isMain && head) {
36359
+ const facts = { clean: files.length === 0, head, ahead };
36360
+ if (prTaken(wt) && movedPastLand(wt, facts))
36361
+ this.setPr(wt.id, undefined);
36362
+ await this.refreshLanded(wt, facts);
36363
+ }
36364
+ const landing = wt.landing;
36365
+ if (landing && landing.check !== "pending") {
36366
+ const moved = landing.fingerprint !== await this.fingerprint(r.path);
36367
+ if (wt.landing === landing && moved !== !!landing.stale) {
36368
+ const { stale: _was, ...rest } = landing;
36369
+ this.setLanding(wt.id, moved ? { ...rest, stale: true } : rest);
35547
36370
  }
35548
- if (r.wt && isMain)
35549
- this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
35550
- if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35551
- return null;
35552
- this.noteCounts(worktreeId, isMain, files.length, counts);
35553
- return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
35554
- } catch (e) {
35555
- log.warn(worktreeId, "git status failed", e);
35556
- return null;
35557
36371
  }
36372
+ if (isMain)
36373
+ this.setEmpty(wt, files.length === 0 ? await treeEmpty(r.path) : false);
36374
+ return false;
35558
36375
  }
35559
- noteCounts(id, main, dirty, ab) {
35560
- const prev = this.counts.get(id);
35561
- if (main && !prev)
35562
- return;
35563
- const ahead = main ? prev?.ahead : ab.ahead;
35564
- const behind = main ? prev?.behind : ab.behind;
35565
- const unpushed = main ? prev?.unpushed : ab.unpushed;
35566
- if (this.counts.put(id, { ahead, behind, unpushed, dirty }))
36376
+ async gitStatus(worktreeId) {
36377
+ if (!this.readable(worktreeId))
36378
+ return this.archivedStatus(worktreeId);
36379
+ const { frame, moved } = await this.tree.full(worktreeId);
36380
+ if (moved)
35567
36381
  this.d.hub.emit("worktreesChanged");
36382
+ return frame;
36383
+ }
36384
+ rowsOf(repoId) {
36385
+ const ids = this.d.state.worktrees.filter((wt) => wt.repoId === repoId && this.isRow(wt)).map((wt) => wt.id);
36386
+ for (const f of this.foundCached())
36387
+ if (f.repoId === repoId)
36388
+ ids.push(f.id);
36389
+ const main = this.mainOf(repoId);
36390
+ if (main && !ids.includes(main.id))
36391
+ ids.push(main.id);
36392
+ return ids;
35568
36393
  }
35569
36394
  async gitLog(worktreeId) {
35570
36395
  const r = this.readable(worktreeId);
@@ -35611,9 +36436,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35611
36436
  snapshot() {
35612
36437
  const rows = [...this.ownedRows(), ...this.foundRows()];
35613
36438
  const trunks = this.trunksNow();
35614
- const reading = this.counts.refresh(idsOf(rows, trunks));
36439
+ const reading = this.tree.refreshStale(idsOf(rows, trunks));
35615
36440
  if (reading) {
35616
- this.behind("counts", reading.then((moved) => {
36441
+ this.behind("tree", reading.then((moved) => {
35617
36442
  if (moved)
35618
36443
  this.d.hub.emit("worktreesChanged");
35619
36444
  }), "badge counts");
@@ -35637,11 +36462,18 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35637
36462
  }
35638
36463
  async settled() {
35639
36464
  let waited = false;
35640
- while (this.pending.size) {
35641
- waited = true;
35642
- await Promise.all(this.pending);
36465
+ for (;; ) {
36466
+ if (this.pending.size) {
36467
+ waited = true;
36468
+ await Promise.all(this.pending);
36469
+ continue;
36470
+ }
36471
+ if (await this.tree.settled()) {
36472
+ waited = true;
36473
+ continue;
36474
+ }
36475
+ return waited;
35643
36476
  }
35644
- return waited;
35645
36477
  }
35646
36478
  mainOf(repoId) {
35647
36479
  return mainOf(this.d.state, repoId);
@@ -35658,7 +36490,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35658
36490
  ownedRows() {
35659
36491
  return this.d.state.worktrees.filter((wt) => this.isRow(wt)).map((wt) => {
35660
36492
  const rt = this.d.runtime.get(wt.id);
35661
- const { ahead, behind, unpushed, dirty } = this.counts.get(wt.id) ?? {};
36493
+ const { ahead, behind, unpushed, dirty } = this.tree.counts(wt.id) ?? {};
35662
36494
  return {
35663
36495
  id: wt.id,
35664
36496
  repoId: wt.repoId,
@@ -35686,7 +36518,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
35686
36518
  }
35687
36519
  foundRows() {
35688
36520
  return this.foundNow().map((f) => {
35689
- const { ahead, behind, dirty } = this.counts.get(f.id) ?? {};
36521
+ const { ahead, behind, dirty } = this.tree.counts(f.id) ?? {};
35690
36522
  return {
35691
36523
  ...f,
35692
36524
  procs: [],
@@ -35792,6 +36624,7 @@ class ArchiveSweep {
35792
36624
  // ../daemon/src/index.ts
35793
36625
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
35794
36626
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
36627
+ log.info("daemon", `toyon ${package_default.version} on bun ${Bun.version}, pid ${process.pid}, ${process.argv[1]}`);
35795
36628
  var here = dirname13(fileURLToPath(import.meta.url));
35796
36629
  var {
35797
36630
  shellDist: SHELL_DIST,
@@ -35808,17 +36641,17 @@ if (pruned > 0)
35808
36641
  var managed = await loadManaged();
35809
36642
  var paths = makePaths();
35810
36643
  ensureDirs(paths);
35811
- forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join31(paths.home, "transcripts-forwarded"));
36644
+ forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join32(paths.home, "transcripts-forwarded"));
35812
36645
  var token = loadOrCreateToken(paths);
35813
36646
  var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
35814
- var helperApp = join31(paths.home, DAEMON_FILES.helper);
36647
+ var helperApp = join32(paths.home, DAEMON_FILES.helper);
35815
36648
  var helper = helperWanted({ platform: process.platform, cloud: cloud.enabled, port, home: process.env.TOYON_HOME }) ? new Helper({
35816
36649
  dir: helperApp,
35817
36650
  stub: HELPER_STUB,
35818
36651
  bun: process.execPath,
35819
36652
  cli: CLI_ENTRY,
35820
- log: join31(paths.home, DAEMON_FILES.launcherLog),
35821
- icon: join31(SHELL_DIST, "icon.svg"),
36653
+ log: join32(paths.home, DAEMON_FILES.launcherLog),
36654
+ icon: join32(SHELL_DIST, "icon.svg"),
35822
36655
  version: package_default.version
35823
36656
  }) : null;
35824
36657
  if (helper) {
@@ -35903,6 +36736,7 @@ var runs = new RunService({ state, hub });
35903
36736
  var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
35904
36737
  var cache = new ArtifactCache({ paths, state, hub });
35905
36738
  var fix = new FixService({ state, hub, runtime });
36739
+ var interest = new Interest;
35906
36740
  var worktrees = new WorktreeService({
35907
36741
  state,
35908
36742
  hub,
@@ -35914,8 +36748,12 @@ var worktrees = new WorktreeService({
35914
36748
  watch: (id, command, run) => exec.watch(id, command, run),
35915
36749
  runs,
35916
36750
  fix,
35917
- pageErrors: (id) => pageErrors.ambient(id)
36751
+ pageErrors: (id) => pageErrors.ambient(id),
36752
+ interested: (id) => interest.isSubscribed(id),
36753
+ shown: (id) => idle.isShown(id)
35918
36754
  });
36755
+ interest.onFirst((id) => worktrees.tree.watch(id));
36756
+ interest.onLast((id) => worktrees.tree.unwatch(id));
35919
36757
  new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
35920
36758
  var handoff2 = new HandoffService({
35921
36759
  state,
@@ -36064,6 +36902,7 @@ var {
36064
36902
  hub,
36065
36903
  repos,
36066
36904
  worktrees,
36905
+ interest,
36067
36906
  turns,
36068
36907
  files,
36069
36908
  opens,
@@ -36109,6 +36948,14 @@ fireAndForget("remote", tailnet.start(), "ask Tailscale");
36109
36948
  writeFileSync13(paths.pidFile, `${process.pid}
36110
36949
  `);
36111
36950
  var stopLagSampler = startLagSampler();
36951
+ fireAndForget("daemon", run("sh", ["-c", "ulimit -n"], homedir15()).then((r) => {
36952
+ const n = Number.parseInt(r.out, 10);
36953
+ if (!r.ok || !Number.isFinite(n))
36954
+ return;
36955
+ limits.fdLimit = n;
36956
+ if (n < FD_LIMIT_LOW)
36957
+ log.warn("daemon", fdLimitLine(n));
36958
+ }), "open-file limit");
36112
36959
  await runtime.reclaimOrphans();
36113
36960
  await repos.boot();
36114
36961
  drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
@@ -36131,7 +36978,7 @@ if (repoArg) {
36131
36978
  if (r?.front === "edge") {
36132
36979
  console.log(`Toyon daemon on https://${r.host}/, ${previewsLine(r)}; token read from ${paths.tokenFile}`);
36133
36980
  } else {
36134
- console.log(`Toyon daemon on http://toyon.localhost${branded ? "" : `:${port}`}/#token=${token}`);
36981
+ console.log(`Toyon daemon on http://toyon.localhost${branded ? "" : `:${port}`}/; token read from ${paths.tokenFile}`);
36135
36982
  if (r)
36136
36983
  console.log(` remote: https://${r.host}/, ${previewsLine(r)}`);
36137
36984
  }
@@ -36157,6 +37004,7 @@ async function shutdown(signal, opts = {}) {
36157
37004
  repos.stopWatchers();
36158
37005
  prs.stop();
36159
37006
  sweep.stop();
37007
+ worktrees.tree.stop();
36160
37008
  stopLagSampler();
36161
37009
  stopServer();
36162
37010
  routes.flush();