toyon 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/dist/CHANGELOG.md +602 -0
  3. package/dist/bridge.js +1 -1
  4. package/dist/cli.js +41 -22
  5. package/dist/daemon.js +635 -259
  6. package/dist/shell/assets/{Editor-Gzhl3Oqu.js → Editor-jjf6IGvI.js} +3 -3
  7. package/dist/shell/assets/{XTerm-D6TL52yQ.js → XTerm-ClBPMY5e.js} +1 -1
  8. package/dist/shell/assets/{cssMode-BfNm0a39.js → cssMode-CgTk4Slg.js} +1 -1
  9. package/dist/shell/assets/{freemarker2-DhiwR6KJ.js → freemarker2-B4Eirl4Y.js} +1 -1
  10. package/dist/shell/assets/{grammar-thv9InXF.js → grammar-E8nWrhy3.js} +1 -1
  11. package/dist/shell/assets/{handlebars-BbkhCZXb.js → handlebars-C6uEGz8T.js} +1 -1
  12. package/dist/shell/assets/{html-yzhYlR33.js → html-tDAdsEYh.js} +1 -1
  13. package/dist/shell/assets/{htmlMode-DPICAUY8.js → htmlMode-DXGTr81h.js} +1 -1
  14. package/dist/shell/assets/index-1FLxRByi.js +267 -0
  15. package/dist/shell/assets/index-D-4W2x1P.css +1 -0
  16. package/dist/shell/assets/{javascript-D9eBjR4g.js → javascript-BeM3ebwd.js} +1 -1
  17. package/dist/shell/assets/{jsonMode-cNulMi5B.js → jsonMode-CXStIFU4.js} +1 -1
  18. package/dist/shell/assets/{liquid-C_SKIx1n.js → liquid-2UNKoWGQ.js} +1 -1
  19. package/dist/shell/assets/{lspLanguageFeatures-DNU8BRHj.js → lspLanguageFeatures-G4xsCwWV.js} +1 -1
  20. package/dist/shell/assets/{mdx-Bt4opbQO.js → mdx-CoF6St9y.js} +1 -1
  21. package/dist/shell/assets/{python-Crb-EuXe.js → python-rULGuAEe.js} +1 -1
  22. package/dist/shell/assets/{razor-D3BlkbYO.js → razor-0I41g6kD.js} +1 -1
  23. package/dist/shell/assets/{tsMode-BYuAS95U.js → tsMode-CyMsmAXC.js} +1 -1
  24. package/dist/shell/assets/{xml-G5xafDqw.js → xml-BNhTd1tm.js} +1 -1
  25. package/dist/shell/assets/{yaml-CRp1x3nR.js → yaml-Cp69rkq5.js} +1 -1
  26. package/dist/shell/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/shell/assets/index-BYBd8f7P.css +0 -1
  29. package/dist/shell/assets/index-DJBEbneL.js +0 -267
package/dist/daemon.js CHANGED
@@ -29,7 +29,7 @@ function isWrittenKind(kind) {
29
29
  return WRITTEN_KINDS.includes(kind ?? "");
30
30
  }
31
31
  function writingCall(call, input) {
32
- return (isWrittenKind(call.kind) || call.name === ASK_TOOL) && emptyInput(input);
32
+ return (isWrittenKind(call.kind) || call.name === ASK_TOOL || call.name === TOOL_SEARCH) && emptyInput(input);
33
33
  }
34
34
  function emptyInput(input) {
35
35
  if (!input || typeof input !== "object")
@@ -6602,7 +6602,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6602
6602
  }
6603
6603
 
6604
6604
  // ../shared/src/protocol/limits.ts
6605
- var PROTOCOL_VERSION = 107;
6605
+ var PROTOCOL_VERSION = 109;
6606
6606
  var PAGE_ERROR_MAX_CHARS = 2000;
6607
6607
  var PAGE_TITLE_MAX_CHARS = 1000;
6608
6608
  var RENDER_ERRORS_MAX = 10;
@@ -6961,6 +6961,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6961
6961
  object({ t: literal("fix"), worktreeId: id, toolId: string2().min(1).max(200) }),
6962
6962
  object({ t: literal("continue"), worktreeId: id }),
6963
6963
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6964
+ object({ t: literal("reveal-path"), path: relPath }),
6964
6965
  object({ t: literal("stop-agent"), worktreeId: id }),
6965
6966
  object({ t: literal("seen"), worktreeId: id }),
6966
6967
  object({ t: literal("mark-unread"), worktreeId: id }),
@@ -8528,7 +8529,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8528
8529
  // ../daemon/package.json
8529
8530
  var package_default = {
8530
8531
  name: "@toyon/daemon",
8531
- version: "0.14.0",
8532
+ version: "0.15.0",
8532
8533
  private: true,
8533
8534
  type: "module",
8534
8535
  main: "src/index.ts",
@@ -14435,11 +14436,27 @@ var BUILTIN_AGENTS = [
14435
14436
  loginHint: "OpenCode has no provider logged in; run `opencode auth login` in the terminal"
14436
14437
  }
14437
14438
  ];
14439
+ function installErrorLine(stderr) {
14440
+ const lines = stderr.split(`
14441
+ `).map((l) => l.trim()).filter(Boolean);
14442
+ const first = lines.find((l) => l.startsWith("error:"))?.slice("error:".length).trim();
14443
+ const line = first || lines.at(-1) || "no output";
14444
+ if (UNTRUSTED_CA.test(line))
14445
+ return `${line}. A certificate bun does not trust, usually a proxy's: set NODE_EXTRA_CA_CERTS to its file and restart Toyon`;
14446
+ return line;
14447
+ }
14448
+ var UNTRUSTED_CA = /UNKNOWN_CERTIFICATE_VERIFICATION_ERROR|SELF_SIGNED_CERT|UNABLE_TO_GET_ISSUER_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/;
14438
14449
  var bunInstall = async (dir) => {
14439
- const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir);
14440
- return { ok: r.ok, err: r.err.split(`
14441
- `).slice(-3).join(" ") };
14450
+ const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir, {
14451
+ NO_COLOR: "1",
14452
+ FORCE_COLOR: "0"
14453
+ });
14454
+ return { ok: r.ok, err: r.ok ? "" : installErrorLine(r.err) };
14442
14455
  };
14456
+ var RETRY_AFTER_MS = 30000;
14457
+ var RETRY_CAP_MS = 60 * 60000;
14458
+ var retryWindow = (retries) => Math.min(RETRY_AFTER_MS * 2 ** retries, RETRY_CAP_MS);
14459
+ var failedReason = (why) => `install failed: ${why}`;
14443
14460
  function jsRuntime() {
14444
14461
  if (process.env.TOYON_ACP_RUNTIME === "node") {
14445
14462
  const node = Bun.which("node");
@@ -14455,15 +14472,18 @@ class AgentRegistry {
14455
14472
  installer;
14456
14473
  host;
14457
14474
  sandbox;
14475
+ now;
14458
14476
  specs = new Map;
14459
14477
  installing = new Map;
14460
14478
  installErrors = new Map;
14479
+ failures = new Map;
14461
14480
  onChange = null;
14462
- constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }) {
14481
+ constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }, now = Date.now) {
14463
14482
  this.dir = dir;
14464
14483
  this.installer = installer;
14465
14484
  this.host = host;
14466
14485
  this.sandbox = sandbox;
14486
+ this.now = now;
14467
14487
  for (const s of specs)
14468
14488
  this.specs.set(s.id, s);
14469
14489
  }
@@ -14521,22 +14541,26 @@ class AgentRegistry {
14521
14541
  }
14522
14542
  }
14523
14543
  unavailable(spec) {
14524
- return this.notInstalled(spec) ?? (spec.confinement === "none" ? null : this.sandbox.problem());
14525
- }
14526
- notInstalled(spec) {
14527
- if (spec.run.kind !== "command") {
14528
- if (!this.packageName(spec))
14529
- return `no build for ${this.host.platform} ${this.host.arch}`;
14530
- if (this.installed(spec))
14531
- return null;
14532
- if (this.installing.has(spec.id))
14533
- return "installing";
14534
- return this.installErrors.get(spec.id) ?? (spec.onDemand ? "not installed" : "not installed yet");
14535
- }
14536
- const cmd = spec.run.command;
14537
- if (isAbsolute(cmd) ? existsSync8(cmd) : Bun.which(cmd))
14538
- return null;
14539
- return `${cmd} not found on PATH`;
14544
+ return this.status(spec).reason;
14545
+ }
14546
+ status(spec) {
14547
+ const boxed = spec.confinement === "none" ? null : this.sandbox.problem();
14548
+ if (spec.run.kind === "command") {
14549
+ const cmd = spec.run.command;
14550
+ const found = isAbsolute(cmd) ? existsSync8(cmd) : Bun.which(cmd);
14551
+ return { reason: found ? boxed : `${cmd} not found on PATH` };
14552
+ }
14553
+ if (!this.packageName(spec))
14554
+ return { reason: `no build for ${this.host.platform} ${this.host.arch}` };
14555
+ if (this.installed(spec))
14556
+ return { reason: boxed };
14557
+ if (this.installing.has(spec.id))
14558
+ return { reason: "installing" };
14559
+ const failed = this.installErrors.get(spec.id);
14560
+ const missing = failed !== undefined ? failedReason(failed) : spec.onDemand ? "not installed" : "not installed yet";
14561
+ if (boxed)
14562
+ return { reason: failed !== undefined ? `${missing}; ${boxed}` : boxed };
14563
+ return { reason: missing, install: failed !== undefined ? { failed } : "missing" };
14540
14564
  }
14541
14565
  command(spec) {
14542
14566
  const why = this.unavailable(spec);
@@ -14564,23 +14588,29 @@ class AgentRegistry {
14564
14588
  const running = this.installing.get(id);
14565
14589
  if (running)
14566
14590
  return running;
14567
- if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
14591
+ if (this.installed(spec) && this.installedVersion(spec) === spec.run.version) {
14592
+ this.installErrors.delete(id);
14593
+ this.failures.delete(id);
14568
14594
  return Promise.resolve();
14595
+ }
14569
14596
  const { version } = spec.run;
14570
14597
  const dir = join6(this.dir, id);
14571
14598
  const p = (async () => {
14572
- mkdirSync5(dir, { recursive: true });
14573
- writeFileSync4(join6(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
14599
+ try {
14600
+ mkdirSync5(dir, { recursive: true });
14601
+ writeFileSync4(join6(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
14574
14602
  `);
14575
- log.info("agents", `installing ${pkg}@${version} for ${id}`);
14576
- const r = await this.installer(dir, pkg, version);
14577
- if (r.ok && this.installed(spec)) {
14578
- this.installErrors.delete(id);
14579
- log.info("agents", `${id} ready`);
14580
- } else {
14581
- const why = `install failed: ${r.err || "no bin after install"}`;
14582
- this.installErrors.set(id, why);
14583
- log.warn("agents", `${id}: ${why}`);
14603
+ log.info("agents", `installing ${pkg}@${version} for ${id}`);
14604
+ const r = await this.installer(dir, pkg, version);
14605
+ if (r.ok && this.installed(spec)) {
14606
+ this.installErrors.delete(id);
14607
+ this.failures.delete(id);
14608
+ log.info("agents", `${id} ready`);
14609
+ return;
14610
+ }
14611
+ this.fail(id, r.err || "no bin after install");
14612
+ } catch (e) {
14613
+ this.fail(id, e instanceof Error ? e.message : String(e));
14584
14614
  }
14585
14615
  })().finally(() => {
14586
14616
  this.installing.delete(id);
@@ -14590,6 +14620,11 @@ class AgentRegistry {
14590
14620
  this.onChange?.();
14591
14621
  return p;
14592
14622
  }
14623
+ fail(id, why) {
14624
+ this.installErrors.set(id, why);
14625
+ this.failures.set(id, { at: this.now(), retries: this.failures.get(id)?.retries ?? 0 });
14626
+ log.warn("agents", `${id}: ${failedReason(why)}`);
14627
+ }
14593
14628
  installedVersion(spec) {
14594
14629
  const name = this.packageName(spec);
14595
14630
  if (!name)
@@ -14609,22 +14644,37 @@ class AgentRegistry {
14609
14644
  await this.install(id);
14610
14645
  }
14611
14646
  }
14647
+ async retryFailed() {
14648
+ if (this.installing.size > 0)
14649
+ return;
14650
+ for (const [id, entry] of this.failures) {
14651
+ if (this.now() - entry.at < retryWindow(entry.retries))
14652
+ continue;
14653
+ entry.retries += 1;
14654
+ await this.install(id);
14655
+ }
14656
+ }
14612
14657
  adapters() {
14613
14658
  const out = [];
14614
14659
  for (const spec of this.list()) {
14615
14660
  if (spec.run.kind === "command" || !this.packageName(spec))
14616
14661
  continue;
14617
14662
  const installed = this.installed(spec) ? this.installedVersion(spec) : null;
14618
- const error = this.installErrors.get(spec.id);
14619
- if (spec.onDemand && installed === null && !error)
14663
+ const failed = this.installErrors.get(spec.id);
14664
+ if (spec.onDemand && installed === null && failed === undefined)
14620
14665
  continue;
14621
- out.push({ id: spec.id, installed, pinned: spec.run.version, ...error ? { error } : {} });
14666
+ out.push({
14667
+ id: spec.id,
14668
+ installed,
14669
+ pinned: spec.run.version,
14670
+ ...failed !== undefined ? { error: failedReason(failed) } : {}
14671
+ });
14622
14672
  }
14623
14673
  return out;
14624
14674
  }
14625
14675
  infos() {
14626
14676
  return this.list().map((spec) => {
14627
- const reason = this.unavailable(spec);
14677
+ const { reason, install } = this.status(spec);
14628
14678
  return {
14629
14679
  id: spec.id,
14630
14680
  name: spec.name,
@@ -14632,6 +14682,7 @@ class AgentRegistry {
14632
14682
  available: !reason,
14633
14683
  ...reason ? { reason } : {},
14634
14684
  ...this.installing.has(spec.id) ? { installing: true } : {},
14685
+ ...install !== undefined ? { install } : {},
14635
14686
  ...spec.onDemand ? { onDemand: true } : {},
14636
14687
  sandboxed: spec.confinement !== "none"
14637
14688
  };
@@ -14720,6 +14771,18 @@ function agentModeFor(spec, mode, available) {
14720
14771
  function modeAfterPlan(optionName) {
14721
14772
  return /manual/i.test(optionName) ? "ask" : "auto";
14722
14773
  }
14774
+ function orderPlanOptions(options) {
14775
+ const rank = (id) => {
14776
+ if (id === "exit-plan-default")
14777
+ return 1;
14778
+ if (id.startsWith("exit-plan-clear-"))
14779
+ return 2;
14780
+ if (id.startsWith("exit-plan-"))
14781
+ return 0;
14782
+ return 3;
14783
+ };
14784
+ return [...options].sort((a, b) => rank(a.optionId) - rank(b.optionId));
14785
+ }
14723
14786
 
14724
14787
  // ../daemon/src/agent/origins.ts
14725
14788
  import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
@@ -15091,14 +15154,16 @@ function isNetworkAsk(update) {
15091
15154
  function isToolSearch(update) {
15092
15155
  return update.name === TOOL_SEARCH || asRecord2(asRecord2(update._meta).claudeCode).toolName === TOOL_SEARCH;
15093
15156
  }
15094
- function toolSearchQuery(rawInput) {
15157
+ var TOOL_SEARCH_OPENING = "loading a tool";
15158
+ function toolSearchTitle(rawInput) {
15095
15159
  const query = asRecord2(rawInput).query;
15096
15160
  if (typeof query !== "string")
15097
15161
  return "";
15098
15162
  const names = /^select:(.*)$/.exec(query.trim());
15099
15163
  if (!names)
15100
- return query.trim();
15101
- return (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15164
+ return `looked for a tool: ${query.trim()}`;
15165
+ const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15166
+ return loaded ? `loaded ${loaded}` : "";
15102
15167
  }
15103
15168
  var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
15104
15169
  function toyonTool(update) {
@@ -15122,7 +15187,7 @@ function heading(update) {
15122
15187
  if (isNetworkAsk(update) && typeof host === "string")
15123
15188
  return { name: "", title: host, kind: "fetch" };
15124
15189
  if (isToolSearch(update)) {
15125
- return { name: TOOL_SEARCH, title: toolSearchQuery(update.rawInput) || TOOL_SEARCH, kind: "search" };
15190
+ return { name: TOOL_SEARCH, title: toolSearchTitle(update.rawInput) || TOOL_SEARCH_OPENING, kind: "other" };
15126
15191
  }
15127
15192
  const own = toyonTool(update);
15128
15193
  if (own)
@@ -15254,7 +15319,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15254
15319
  });
15255
15320
  }
15256
15321
  const refined = { ...memo.held };
15257
- const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
15322
+ const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
15258
15323
  if (title && title !== memo.title)
15259
15324
  refined.title = memo.title = title;
15260
15325
  if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
@@ -16424,6 +16489,7 @@ class AcpSession {
16424
16489
  this.queue = [];
16425
16490
  this.steered = [];
16426
16491
  this.queueChanged();
16492
+ this.cancelAsks("expired");
16427
16493
  this.stop();
16428
16494
  await this.dropConn("closing");
16429
16495
  await this.log.flush();
@@ -16864,6 +16930,9 @@ class AcpSession {
16864
16930
  }
16865
16931
  if (params.update.sessionUpdate === "current_mode_update") {
16866
16932
  live.modeId = params.update.currentModeId;
16933
+ const planId = agentModeFor(live.conn.spec, "plan", live.modeIds ?? undefined);
16934
+ if (planId && live.modeId === planId && this.mode() !== "plan")
16935
+ this.d.setMode?.("plan");
16867
16936
  return;
16868
16937
  }
16869
16938
  if (params.update.sessionUpdate === "config_option_update") {
@@ -16936,7 +17005,7 @@ class AcpSession {
16936
17005
  const parts = permissionParts(params);
16937
17006
  const detail = parts.detail ?? "";
16938
17007
  const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
16939
- const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
17008
+ const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
16940
17009
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
16941
17010
  return this.openAsk((id) => ({
16942
17011
  type: "agent-permission",
@@ -17022,9 +17091,9 @@ class AcpSession {
17022
17091
  const text = headers.length === 1 ? `with my answer to "${headers[0]}"` : "with my answers";
17023
17092
  this.post(text, { attachments: followUp.attachments });
17024
17093
  }
17025
- cancelAsks() {
17094
+ cancelAsks(outcome = "cancelled") {
17026
17095
  for (const ask of [...this.asks.values()])
17027
- ask.settle("cancelled");
17096
+ ask.settle(outcome);
17028
17097
  }
17029
17098
  syncStatus() {
17030
17099
  if (this.status === "error")
@@ -19504,7 +19573,7 @@ var LAND_ASK = [
19504
19573
  "Line 2: blank",
19505
19574
  "Line 3: Recap: one sentence under 20 words for the user coming back to this task, saying what the task is and then what just happened or what to do next. Skip root-cause narrative, fix internals and secondary to-dos.",
19506
19575
  "Line 4: blank",
19507
- "Line 5: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
19576
+ "Line 5: a commit subject under 60 characters, lowercase, imperative, naming what the diff summary says the message is for, in the style of the recent subjects when given",
19508
19577
  "Line 6: blank",
19509
19578
  "Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
19510
19579
  ].join(`
@@ -19512,17 +19581,25 @@ var LAND_ASK = [
19512
19581
  var LAND_TURNS = 4;
19513
19582
  var LAND_CHARS = 5000;
19514
19583
  function landPrompt(i) {
19515
- const head = [LAND_ASK, "", `Task: ${clip(i.title, 200)}`];
19516
- if (i.firstAsk)
19517
- head.push(`First request: ${clip(i.firstAsk, 300)}`);
19584
+ const own = i.diffStat.trim();
19585
+ const committed = i.committedStat?.trim() ?? "";
19586
+ const head = [LAND_ASK, ""];
19587
+ if (own && i.earlier) {
19588
+ head.push(`Task, as the worktree is titled; background, since the branch committed or landed work before: ${clip(i.title, 200)}`);
19589
+ if (i.firstAsk)
19590
+ head.push(`First request here, which the earlier work may have answered: ${clip(i.firstAsk, 300)}`);
19591
+ } else {
19592
+ head.push(`Task: ${clip(i.title, 200)}`);
19593
+ if (i.firstAsk)
19594
+ head.push(`First request: ${clip(i.firstAsk, 300)}`);
19595
+ }
19518
19596
  if (i.recentSubjects.length)
19519
19597
  head.push(`Recent commit subjects:
19520
19598
  ${i.recentSubjects.map((s) => ` ${s}`).join(`
19521
19599
  `)}`);
19522
19600
  if (i.note?.trim())
19523
19601
  head.push(`Note from the user for the commit message, to be reflected in its wording: ${clip(i.note.trim(), 500)}`);
19524
- const diff = `Diff summary:
19525
- ${i.diffStat.trim() || "(none)"}`;
19602
+ const diff = diffBlock(own, committed);
19526
19603
  let blocks = turnBlocks(i.turns);
19527
19604
  const size = () => [...head, "", ...blocks, "", diff].join(`
19528
19605
 
@@ -19534,6 +19611,22 @@ ${i.diffStat.trim() || "(none)"}`;
19534
19611
 
19535
19612
  `), diff].join(`
19536
19613
 
19614
+ `);
19615
+ }
19616
+ function diffBlock(own, committed) {
19617
+ if (!own && !committed)
19618
+ return `Diff summary:
19619
+ (none)`;
19620
+ const parts = [];
19621
+ if (own)
19622
+ parts.push(`What this commit will contain, which is all the message describes:
19623
+ ${own}`);
19624
+ if (committed)
19625
+ parts.push(own ? `Already committed on this branch, context only, not what the message describes:
19626
+ ${committed}` : `Nothing is uncommitted; the message is for the branch's commits as one:
19627
+ ${committed}`);
19628
+ return parts.join(`
19629
+
19537
19630
  `);
19538
19631
  }
19539
19632
  function turnBlocks(turns) {
@@ -19804,7 +19897,8 @@ function locateAssets(here, env = process.env) {
19804
19897
  helperStub: packaged ? join13(here, "helper-stub") : join13(here, "../dist/helper-stub"),
19805
19898
  cliEntry: packaged ? join13(here, "cli.js") : join13(here, "../../cli/src/cli.ts"),
19806
19899
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
19807
- packageJson: packaged ? join13(here, "..", "package.json") : null
19900
+ packageJson: packaged ? join13(here, "..", "package.json") : null,
19901
+ changelog: packaged ? join13(here, "CHANGELOG.md") : resolve5(here, "../../../CHANGELOG.md")
19808
19902
  };
19809
19903
  }
19810
19904
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
@@ -21063,9 +21157,9 @@ class StateStore {
21063
21157
  this.state.worktrees.push(wt);
21064
21158
  this.save();
21065
21159
  }
21066
- removeWorktree(id) {
21160
+ removeWorktree(id, opts = {}) {
21067
21161
  const wt = this.state.worktrees.find((w) => w.id === id);
21068
- const repo = wt && wt.kind !== "spare" ? this.repo(wt.repoId) : undefined;
21162
+ const repo = wt && wt.kind !== "spare" && !opts.refused ? this.repo(wt.repoId) : undefined;
21069
21163
  if (wt && repo)
21070
21164
  repo.promptedAt = Math.max(repo.promptedAt ?? 0, sentAt(wt));
21071
21165
  this.state.worktrees = this.state.worktrees.filter((w) => w.id !== id);
@@ -23291,6 +23385,9 @@ class OpenService {
23291
23385
  throw new UserError(`${raw} is a folder: Toyon opens files`);
23292
23386
  return this.openFile(raw, path, st);
23293
23387
  }
23388
+ async where(raw) {
23389
+ return (await this.locate(raw)).path;
23390
+ }
23294
23391
  async locate(raw) {
23295
23392
  const path = canonical(resolve7(expandTilde(raw)));
23296
23393
  const st = await stat3(path).catch((e) => {
@@ -23390,13 +23487,70 @@ function parseAllowed(json) {
23390
23487
  return null;
23391
23488
  }
23392
23489
  }
23393
- var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest";
23490
+ var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest,headRefOid";
23394
23491
  async function viewPr(repoPath, number) {
23395
23492
  const r = await gh(["pr", "view", String(number), "--json", PR_VIEW_FIELDS], repoPath, 20000);
23396
23493
  if (!r.ok || !r.out)
23397
23494
  return null;
23398
23495
  const parsed = parsePrView(r.out);
23399
- return parsed ? { ...parsed, at: Date.now() } : null;
23496
+ if (!parsed)
23497
+ return null;
23498
+ if (parsed.state === "open" && !parsed.preview) {
23499
+ const head = headOf(r.out);
23500
+ const preview = head ? await viewDeploymentPreview(repoPath, head) : null;
23501
+ if (preview)
23502
+ parsed.preview = preview;
23503
+ }
23504
+ return { ...parsed, at: Date.now() };
23505
+ }
23506
+ function headOf(json) {
23507
+ try {
23508
+ const o = JSON.parse(json);
23509
+ return typeof o.headRefOid === "string" && o.headRefOid ? o.headRefOid : null;
23510
+ } catch {
23511
+ return null;
23512
+ }
23513
+ }
23514
+ var DEPLOYMENTS_QUERY = `query($owner: String!, $name: String!, $oid: GitObjectID!) {
23515
+ repository(owner: $owner, name: $name) {
23516
+ object(oid: $oid) { ... on Commit {
23517
+ deployments(last: 10) { nodes { environment latestStatus { state environmentUrl } } }
23518
+ } }
23519
+ }
23520
+ }`;
23521
+ async function viewDeploymentPreview(repoPath, head) {
23522
+ const r = await gh([
23523
+ "api",
23524
+ "graphql",
23525
+ "-F",
23526
+ "owner={owner}",
23527
+ "-F",
23528
+ "name={repo}",
23529
+ "-f",
23530
+ `oid=${head}`,
23531
+ "-f",
23532
+ `query=${DEPLOYMENTS_QUERY}`
23533
+ ], repoPath, 20000);
23534
+ if (!r.ok || !r.out)
23535
+ return null;
23536
+ return parseDeployments(r.out);
23537
+ }
23538
+ function parseDeployments(json) {
23539
+ let nodes;
23540
+ try {
23541
+ const o = JSON.parse(json);
23542
+ nodes = o.data?.repository?.object?.deployments?.nodes;
23543
+ } catch {
23544
+ return null;
23545
+ }
23546
+ if (!Array.isArray(nodes))
23547
+ return null;
23548
+ for (const d of [...nodes].reverse()) {
23549
+ const st = d?.latestStatus;
23550
+ if (st?.state === "SUCCESS" && isPreviewUrl(st.environmentUrl))
23551
+ return st.environmentUrl;
23552
+ }
23553
+ return null;
23400
23554
  }
23401
23555
  function parsePrView(json) {
23402
23556
  let o;
@@ -23412,6 +23566,7 @@ function parsePrView(json) {
23412
23566
  const checks = foldChecks(o.statusCheckRollup);
23413
23567
  const mergeable = o.mergeable === "MERGEABLE" ? true : o.mergeable === "CONFLICTING" ? false : undefined;
23414
23568
  const automerge = o.autoMergeRequest !== null && o.autoMergeRequest !== undefined;
23569
+ const preview = state === "open" ? previewOf3(o.statusCheckRollup) : undefined;
23415
23570
  return {
23416
23571
  number: o.number,
23417
23572
  url: o.url,
@@ -23420,9 +23575,37 @@ function parsePrView(json) {
23420
23575
  ...review ? { review } : {},
23421
23576
  ...checks ? { checks } : {},
23422
23577
  ...mergeable !== undefined ? { mergeable } : {},
23423
- ...automerge ? { automerge } : {}
23578
+ ...automerge ? { automerge } : {},
23579
+ ...preview ? { preview } : {}
23424
23580
  };
23425
23581
  }
23582
+ function previewOf3(rollup) {
23583
+ if (!Array.isArray(rollup))
23584
+ return;
23585
+ for (const c of rollup) {
23586
+ const name = String(c.context ?? c.name ?? "");
23587
+ if (!/preview/i.test(name))
23588
+ continue;
23589
+ const word = String(c.conclusion ?? c.state ?? "").toUpperCase();
23590
+ if (word !== "SUCCESS")
23591
+ continue;
23592
+ const url = c.targetUrl ?? c.detailsUrl;
23593
+ if (isPreviewUrl(url))
23594
+ return url;
23595
+ }
23596
+ return;
23597
+ }
23598
+ function isPreviewUrl(url) {
23599
+ if (typeof url !== "string")
23600
+ return false;
23601
+ let host;
23602
+ try {
23603
+ host = new URL(url).hostname;
23604
+ } catch {
23605
+ return false;
23606
+ }
23607
+ return host !== "github.com" && !host.endsWith(".github.com");
23608
+ }
23426
23609
  function foldChecks(rollup) {
23427
23610
  if (!Array.isArray(rollup) || rollup.length === 0)
23428
23611
  return;
@@ -28859,6 +29042,9 @@ var handlers = {
28859
29042
  reveal(msg, _ctx, s) {
28860
29043
  s.files.reveal(msg.worktreeId, msg.path);
28861
29044
  },
29045
+ async "reveal-path"(msg, _ctx, s) {
29046
+ s.files.revealPath(await s.opens.where(msg.path));
29047
+ },
28862
29048
  async "discard-files"(msg, _ctx, s) {
28863
29049
  try {
28864
29050
  for (const path of msg.paths)
@@ -29365,6 +29551,20 @@ function createFetch(opts) {
29365
29551
  const refused = await opts.restart(url.searchParams.has(RESTART_NOW));
29366
29552
  return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
29367
29553
  }
29554
+ if (url.pathname === "/changelog" && req.method === "GET") {
29555
+ if (!authed(req))
29556
+ return unauthorized();
29557
+ const text = await opts.changelog();
29558
+ if (text === null)
29559
+ return new Response("this copy of Toyon carries no release notes", { status: 404 });
29560
+ return new Response(text, {
29561
+ headers: {
29562
+ "content-type": "text/markdown; charset=utf-8",
29563
+ "cache-control": NO_STORE2,
29564
+ "x-content-type-options": "nosniff"
29565
+ }
29566
+ });
29567
+ }
29368
29568
  const paired = await opts.pairing(req, url, d);
29369
29569
  if (paired)
29370
29570
  return paired;
@@ -29880,7 +30080,8 @@ function startServer(opts) {
29880
30080
  restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
29881
30081
  grants: opts.grants,
29882
30082
  trusted: () => s.state.trustedOrigins,
29883
- mcp: (req, id) => s.mcp.fetch(req, id)
30083
+ mcp: (req, id) => s.mcp.fetch(req, id),
30084
+ changelog: () => Bun.file(opts.changelog).text().catch(() => null)
29884
30085
  }),
29885
30086
  websocket: {
29886
30087
  async open(ws) {
@@ -29897,6 +30098,7 @@ function startServer(opts) {
29897
30098
  sockets.add(ws);
29898
30099
  link(ws);
29899
30100
  send(ws, await helloFrame());
30101
+ fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
29900
30102
  for (const o of s.opens.takePending())
29901
30103
  send(ws, openedFrame(o));
29902
30104
  },
@@ -30228,7 +30430,7 @@ var REFRESH_EVERY_MS = 60000;
30228
30430
  var CHECK_EVERY_MS = 6 * 60 * 60000;
30229
30431
  var FIRST_CHECK_MS = 2 * 60000;
30230
30432
  var SETTLED_MS = 2 * 60000;
30231
- var RETRY_AFTER_MS = 24 * 60 * 60000;
30433
+ var RETRY_AFTER_MS2 = 24 * 60 * 60000;
30232
30434
  var INSTALL_SETTLE_MS = 60000;
30233
30435
 
30234
30436
  class UpdateService {
@@ -30380,7 +30582,7 @@ class UpdateService {
30380
30582
  if (this.needsInstall() && this.d.command(target) === null)
30381
30583
  return;
30382
30584
  const failed = this.d.state.updateFailed;
30383
- if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS)
30585
+ if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS2)
30384
30586
  return;
30385
30587
  if (this.d.busy()) {
30386
30588
  this.busyAt = this.now();
@@ -31370,6 +31572,7 @@ function match(repos, q) {
31370
31572
  // ../daemon/src/worktrees/landing.ts
31371
31573
  var TAIL_CHARS = 400;
31372
31574
  var DIFF_CHARS = 2000;
31575
+ var CONTEXT_CHARS2 = 800;
31373
31576
  var CHECK_SLOTS = 2;
31374
31577
 
31375
31578
  class LandingService {
@@ -31445,14 +31648,7 @@ class LandingService {
31445
31648
  const repo = this.d.state.requireRepo(wt.repoId);
31446
31649
  this.judging.set(worktreeId, turn.at);
31447
31650
  const started = Date.now();
31448
- const entries = this.d.transcript(worktreeId);
31449
- const prompt = landPrompt({
31450
- title: wt.title,
31451
- firstAsk: firstAskOf(entries),
31452
- turns: turnsSince(entries, 0),
31453
- diffStat: await diffSummary(wt.path, baseOf(repo)),
31454
- recentSubjects: await recentSubjects(wt.path, baseOf(repo))
31455
- });
31651
+ const prompt = landPrompt(await this.landInput(wt, repo));
31456
31652
  let verdict = null;
31457
31653
  try {
31458
31654
  verdict = await this.d.judge(wt, prompt);
@@ -31525,6 +31721,25 @@ class LandingService {
31525
31721
  return;
31526
31722
  fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
31527
31723
  }
31724
+ async landInput(wt, repo) {
31725
+ const base = baseOf(repo);
31726
+ const entries = this.d.transcript(wt.id);
31727
+ const [diff, subjects, committedAt] = await Promise.all([
31728
+ diffSummary(wt.id, wt.path, base),
31729
+ recentSubjects(wt.path, base),
31730
+ lastOwnCommitAt(wt.id, wt.path, base)
31731
+ ]);
31732
+ const since = Math.max(committedAt, wt.lands?.at(-1)?.at ?? 0);
31733
+ return {
31734
+ title: wt.title,
31735
+ firstAsk: firstAskOf(entries),
31736
+ turns: turnsSince(entries, since),
31737
+ earlier: since > 0,
31738
+ diffStat: diff.own,
31739
+ committedStat: diff.committed,
31740
+ recentSubjects: subjects
31741
+ };
31742
+ }
31528
31743
  async hasWork(wt) {
31529
31744
  const repo = this.d.state.requireRepo(wt.repoId);
31530
31745
  const files = await statusFiles(wt.path);
@@ -31547,14 +31762,14 @@ class LandingService {
31547
31762
  };
31548
31763
  this.judging.set(worktreeId, opts.at);
31549
31764
  const started = Date.now();
31550
- this.d.worktrees.setLanding(worktreeId, { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept });
31765
+ const pending = { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept };
31766
+ this.d.worktrees.setLanding(worktreeId, pending);
31551
31767
  const live = () => !this.stopping && this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
31552
- let check = "none";
31553
- let checkTail;
31554
- let waited = 0;
31555
- let ran;
31556
31768
  const command = repo.config.check?.trim();
31557
- if (command) {
31769
+ let checkDone = false;
31770
+ const checked = async () => {
31771
+ if (!command)
31772
+ return { check: "none" };
31558
31773
  const ceiling = timeoutFor(repo.config, "check");
31559
31774
  this.d.runs.drop(worktreeId, "check");
31560
31775
  const run = this.d.runs.begin(worktreeId, "check", {
@@ -31563,9 +31778,11 @@ class LandingService {
31563
31778
  });
31564
31779
  if (!await run.admitted)
31565
31780
  return;
31566
- if (!live())
31567
- return run.finish(null);
31568
- waited = Date.now() - started;
31781
+ if (!live()) {
31782
+ run.finish(null);
31783
+ return;
31784
+ }
31785
+ const waited = Date.now() - started;
31569
31786
  let r;
31570
31787
  try {
31571
31788
  r = await this.d.check(worktreeId, command, { quiet: opts.quiet, timeoutMs: ceiling, onSpawn: run.spawned });
@@ -31576,8 +31793,8 @@ class LandingService {
31576
31793
  run.finish(r.exit);
31577
31794
  if (!live())
31578
31795
  return;
31579
- ran = r;
31580
- check = r.exit === 0 ? "pass" : "fail";
31796
+ const check = r.exit === 0 ? "pass" : "fail";
31797
+ let checkTail;
31581
31798
  if (r.exit === "timeout") {
31582
31799
  checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
31583
31800
  `);
@@ -31585,19 +31802,14 @@ class LandingService {
31585
31802
  checkTail = tail(r.text) || `exit ${r.exit}`;
31586
31803
  else
31587
31804
  this.d.hub.emit("checkPassed", worktreeId);
31588
- }
31589
- let verdict = null;
31590
- let unanswered = false;
31591
- if (opts.ask && check !== "fail" && this.d.judge) {
31592
- const entries = this.d.transcript(worktreeId);
31593
- const prompt = landPrompt({
31594
- title: wt.title,
31595
- firstAsk: firstAskOf(entries),
31596
- turns: turnsSince(entries, 0),
31597
- diffStat: await diffSummary(wt.path, baseOf(repo)),
31598
- recentSubjects: await recentSubjects(wt.path, baseOf(repo)),
31599
- ...opts.note ? { note: opts.note } : {}
31600
- });
31805
+ return { check, ran: r, waited, ...checkTail ? { checkTail } : {} };
31806
+ };
31807
+ const asked = async () => {
31808
+ if (!opts.ask || !this.d.judge)
31809
+ return { verdict: null, unanswered: false };
31810
+ const prompt = landPrompt({ ...await this.landInput(wt, repo), ...opts.note ? { note: opts.note } : {} });
31811
+ let verdict = null;
31812
+ let unanswered = false;
31601
31813
  try {
31602
31814
  verdict = await this.d.judge(wt, prompt);
31603
31815
  } catch (e) {
@@ -31608,16 +31820,29 @@ class LandingService {
31608
31820
  }
31609
31821
  if (!live())
31610
31822
  return;
31611
- }
31823
+ if (!checkDone && verdict) {
31824
+ this.recapTurn(worktreeId, verdict);
31825
+ this.d.worktrees.setLanding(worktreeId, { ...pending, ...messageOf(verdict) });
31826
+ }
31827
+ return { verdict, unanswered };
31828
+ };
31829
+ const [checkResult, askResult] = await Promise.all([
31830
+ checked().finally(() => {
31831
+ checkDone = true;
31832
+ }),
31833
+ asked()
31834
+ ]);
31835
+ if (!checkResult || !askResult || !live())
31836
+ return;
31837
+ const { check, checkTail, ran, waited = 0 } = checkResult;
31838
+ const { verdict, unanswered } = askResult;
31612
31839
  const landing = {
31613
31840
  at: opts.at,
31614
31841
  check,
31615
31842
  ...checkTail ? { checkTail } : {},
31616
31843
  ready: check !== "fail",
31617
31844
  ...kept,
31618
- ...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
31619
- ...verdict?.subject ? { subject: verdict.subject } : {},
31620
- ...verdict?.body ? { body: verdict.body } : {},
31845
+ ...verdict ? messageOf(verdict) : {},
31621
31846
  ...unanswered ? { unanswered: true } : {},
31622
31847
  fingerprint: await treeFingerprint(wt.path)
31623
31848
  };
@@ -31627,14 +31852,18 @@ class LandingService {
31627
31852
  if (!live())
31628
31853
  return;
31629
31854
  this.judging.delete(worktreeId);
31630
- const record = this.d.state.worktree(worktreeId)?.lastTurn;
31631
- if (record && verdict?.recap)
31632
- record.recap = { at: Date.now(), text: verdict.recap };
31855
+ if (verdict)
31856
+ this.recapTurn(worktreeId, verdict);
31633
31857
  log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms${waited > 100 ? ` (waited ${waited}ms for a check slot)` : ""}`);
31634
31858
  this.d.worktrees.setLanding(worktreeId, landing);
31635
31859
  if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
31636
31860
  this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
31637
31861
  }
31862
+ recapTurn(worktreeId, verdict) {
31863
+ const record = this.d.state.worktree(worktreeId)?.lastTurn;
31864
+ if (record && verdict.recap)
31865
+ record.recap = { at: Date.now(), text: verdict.recap };
31866
+ }
31638
31867
  async sharedMigration(wt, repo) {
31639
31868
  if (!repo.services || keepsCopiesApart(repo.config))
31640
31869
  return;
@@ -31644,31 +31873,44 @@ class LandingService {
31644
31873
  return "this changes migrations, and the worktrees share the database";
31645
31874
  }
31646
31875
  }
31876
+ function messageOf(verdict) {
31877
+ return {
31878
+ ...!verdict.ready && verdict.why ? { why: verdict.why } : {},
31879
+ ...verdict.subject ? { subject: verdict.subject } : {},
31880
+ ...verdict.body ? { body: verdict.body } : {}
31881
+ };
31882
+ }
31647
31883
  function tail(text) {
31648
31884
  const t = text.replace(/\s+$/, "");
31649
31885
  return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
31650
31886
  }
31651
- async function diffSummary(path, defaultBr) {
31887
+ async function diffSummary(id, path, defaultBr) {
31652
31888
  const [base, dirty, untracked] = await Promise.all([
31653
31889
  git(path, "merge-base", "HEAD", defaultBr),
31654
31890
  git(path, "diff", "HEAD", "--stat"),
31655
31891
  git(path, "ls-files", "--others", "--exclude-standard")
31656
31892
  ]);
31893
+ if (!base.ok)
31894
+ log.warn(id, `landing question: no merge base with ${defaultBr}: ${base.err.trim()}`);
31657
31895
  const committed = base.ok && base.out ? await git(path, "diff", "--stat", base.out, "HEAD") : null;
31658
- const parts = [
31659
- committed?.out ? `Committed on the branch:
31660
- ${committed.out}` : "",
31661
- dirty.out ? `Uncommitted:
31662
- ${dirty.out}` : "",
31663
- untracked.out ? `New files:
31664
- ${untracked.out}` : ""
31665
- ].filter(Boolean);
31666
- const text = parts.join(`
31896
+ const own = [dirty.out ? `Uncommitted:
31897
+ ${dirty.out}` : "", untracked.out ? `New files:
31898
+ ${untracked.out}` : ""].filter(Boolean).join(`
31667
31899
 
31668
31900
  `);
31669
- return text.length > DIFF_CHARS ? `${text.slice(0, DIFF_CHARS)}
31901
+ return { own: cap(own, DIFF_CHARS), committed: cap(committed?.out ?? "", CONTEXT_CHARS2) };
31902
+ }
31903
+ function cap(text, max) {
31904
+ return text.length > max ? `${text.slice(0, max)}
31670
31905
  ...` : text;
31671
31906
  }
31907
+ async function lastOwnCommitAt(id, path, defaultBr) {
31908
+ const r = await git(path, "log", "-1", "--no-merges", "--format=%at", `${defaultBr}..HEAD`);
31909
+ if (!r.ok)
31910
+ log.warn(id, `landing question: could not read the branch's commits over ${defaultBr}: ${r.err.trim()}`);
31911
+ const seconds = Number(r.out.trim());
31912
+ return r.ok && Number.isFinite(seconds) ? seconds * 1000 : 0;
31913
+ }
31672
31914
  async function recentSubjects(path, defaultBr) {
31673
31915
  const r = await git(path, "log", "--format=%s", "-n", "5", defaultBr);
31674
31916
  return r.ok ? r.out.split(`
@@ -32403,17 +32645,41 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
32403
32645
  }
32404
32646
  return { ok: true, message: `merged ${branch} into ${defaultBr}` };
32405
32647
  }
32406
- async function squashMessage(worktreePath, defaultBr, suggested) {
32648
+ async function squashMessage(worktreePath, defaultBr, suggested, typed = false) {
32407
32649
  const n = await git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`);
32408
32650
  if (n.out === "1") {
32651
+ if (typed && suggested)
32652
+ return suggested;
32409
32653
  const one = await git(worktreePath, "log", "-1", "--format=%B");
32410
32654
  if (one.ok && one.out.trim())
32411
32655
  return one.out.trim();
32412
32656
  }
32413
- if (suggested)
32414
- return suggested;
32415
- const subjects = await git(worktreePath, "log", "--format=%s", `${defaultBr}..HEAD`);
32416
- return subjects.out.trim() || "land";
32657
+ const log = await git(worktreePath, "log", "--reverse", "--no-merges", "--format=%s", `${defaultBr}..HEAD`);
32658
+ const subjects = log.out.split(`
32659
+ `).filter(Boolean);
32660
+ const [first = "", ...rest] = (suggested ?? subjects.at(-1) ?? "land").split(`
32661
+ `);
32662
+ const subject = first.trim();
32663
+ const others = subjects.at(-1) === subject ? subjects.slice(0, -1) : subjects;
32664
+ const body = [
32665
+ rest.join(`
32666
+ `).trim(),
32667
+ others.length ? `Squashed with the branch's earlier commits:
32668
+ ${others.join(`
32669
+ `)}` : ""
32670
+ ].filter(Boolean).join(`
32671
+
32672
+ `);
32673
+ return body ? `${subject}
32674
+
32675
+ ${body}` : subject;
32676
+ }
32677
+ function splitMessage(text) {
32678
+ const [first = "", ...rest] = text.split(`
32679
+ `);
32680
+ const body = rest.join(`
32681
+ `).trim();
32682
+ return { subject: first.trim(), ...body ? { body } : {} };
32417
32683
  }
32418
32684
  async function fastForwardFetched(repoPath, defaultBr) {
32419
32685
  const current = await git(repoPath, "branch", "--show-current");
@@ -33137,7 +33403,16 @@ class SparePool {
33137
33403
  const added = (async () => {
33138
33404
  if (existsSync32(wt.path))
33139
33405
  return;
33140
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
33406
+ const run = this.d.runs.begin(wt.id, "setup", {
33407
+ timeoutMs: timeoutFor(repo.config, "setup"),
33408
+ stage: "checking out"
33409
+ });
33410
+ try {
33411
+ await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", "--no-checkout", wt.path, baseOf(repo)));
33412
+ await withLock(`checkout:${repo.path}`, () => gitOrThrow(wt.path, "reset", "--hard"));
33413
+ } finally {
33414
+ run.finish(0);
33415
+ }
33141
33416
  })();
33142
33417
  const done = (async () => {
33143
33418
  this.d.runtime.markSetup(wt.id, true);
@@ -33229,40 +33504,70 @@ class SparePool {
33229
33504
  this.refreshing.set(repoId, run);
33230
33505
  await run;
33231
33506
  }
33232
- async claim(repoId, title, worktreeId) {
33507
+ async claim(repoId, title, worktreeId, stamps, prepare) {
33233
33508
  const wt = this.spareOf(repoId);
33234
33509
  if (!wt)
33235
33510
  return null;
33236
33511
  if (worktreeId && wt.id !== worktreeId)
33237
33512
  return null;
33238
33513
  const wasReady = wt.phase === "ready";
33239
- if (!wasReady) {
33240
- if (!worktreeId)
33241
- return null;
33242
- const running = this.inflight.get(repoId);
33243
- await running?.added.catch(() => {});
33244
- running?.cut();
33245
- if (wt.kind !== "spare" || !existsSync32(wt.path))
33246
- return null;
33247
- }
33248
- await this.refreshing.get(repoId);
33514
+ if (!wasReady && !worktreeId)
33515
+ return null;
33249
33516
  const repo = this.d.state.requireRepo(repoId);
33250
- const branch = `toyon/${basename11(wt.path)}`;
33251
- await withRepoLock(repo.path, () => gitOrThrow(wt.path, "switch", "-c", branch));
33517
+ const running = this.inflight.get(repoId);
33518
+ const was = { title: wt.title, branch: wt.branch, phase: wt.phase, lockfile: wt.lockfile };
33252
33519
  wt.kind = "worktree";
33253
33520
  delete wt.phase;
33254
33521
  delete wt.lockfile;
33255
- wt.branch = branch;
33522
+ wt.branch = `toyon/${basename11(wt.path)}`;
33256
33523
  wt.title = title;
33257
- wt.unnamed = true;
33258
33524
  wt.createdAt = Date.now();
33259
- if (wasReady)
33260
- fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
33525
+ wt.promptedAt = wt.createdAt;
33526
+ Object.assign(wt, Object.fromEntries(Object.entries(stamps).filter(([, v]) => v !== undefined)));
33261
33527
  const next = repo.needsSetup ? null : await this.record(repoId, repo);
33262
33528
  this.d.state.save();
33263
33529
  if (next)
33264
33530
  fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
33265
- return wt;
33531
+ const settled = (async () => {
33532
+ try {
33533
+ await running?.added;
33534
+ running?.cut();
33535
+ if (!existsSync32(wt.path))
33536
+ throw new Error("the worktree's directory is gone");
33537
+ await this.refreshing.get(repoId);
33538
+ await prepare(wt);
33539
+ await withRepoLock(repo.path, async () => {
33540
+ await gitOrThrow(wt.path, "branch", wt.branch);
33541
+ await gitOrThrow(wt.path, "symbolic-ref", "HEAD", `refs/heads/${wt.branch}`);
33542
+ });
33543
+ if (wasReady)
33544
+ fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
33545
+ } catch (e) {
33546
+ await this.unclaim(wt, was, stamps, next, e);
33547
+ throw e;
33548
+ }
33549
+ })();
33550
+ return { wt, settled };
33551
+ }
33552
+ async unclaim(wt, was, stamps, next, e) {
33553
+ wt.kind = "spare";
33554
+ wt.title = was.title;
33555
+ wt.branch = was.branch;
33556
+ if (was.phase)
33557
+ wt.phase = was.phase;
33558
+ if (was.lockfile)
33559
+ wt.lockfile = was.lockfile;
33560
+ for (const k of Object.keys(stamps))
33561
+ delete wt[k];
33562
+ delete wt.promptedAt;
33563
+ if (!existsSync32(wt.path)) {
33564
+ await this.rollBack(wt, e);
33565
+ return;
33566
+ }
33567
+ this.d.state.save();
33568
+ this.d.hub.emit("worktreesChanged");
33569
+ if (next)
33570
+ await this.d.discard(next.id).catch((re) => log.warn(wt.repoId, "reserved spare removal failed", re));
33266
33571
  }
33267
33572
  }
33268
33573
 
@@ -33400,23 +33705,8 @@ class TurnService {
33400
33705
  stoppedByHand = new Set;
33401
33706
  constructor(d) {
33402
33707
  this.d = d;
33403
- this.boot();
33404
33708
  d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
33405
33709
  }
33406
- boot() {
33407
- let changed = false;
33408
- for (const wt of this.d.state.worktrees) {
33409
- const turn = wt.lastTurn;
33410
- if (turn?.end !== "asking")
33411
- continue;
33412
- const { ask: _expired, ...facts } = turn.facts;
33413
- turn.end = "stopped";
33414
- turn.facts = facts;
33415
- changed = true;
33416
- }
33417
- if (changed)
33418
- this.d.state.save();
33419
- }
33420
33710
  onStatus(worktreeId, status) {
33421
33711
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
33422
33712
  this.lastStatus.set(worktreeId, status);
@@ -33503,6 +33793,7 @@ var idsOf = (rows, trunks) => [
33503
33793
  ...rows.map((r) => r.id),
33504
33794
  ...Object.values(trunks).map((t) => t.id)
33505
33795
  ];
33796
+ var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
33506
33797
  var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
33507
33798
  `, 1)[0])).join(" ");
33508
33799
 
@@ -33517,6 +33808,8 @@ class WorktreeService {
33517
33808
  archiving = new Map;
33518
33809
  restoring = new Map;
33519
33810
  going = new Set;
33811
+ arriving = new Set;
33812
+ firstPrompt = new Map;
33520
33813
  counts;
33521
33814
  handChecked = new Map;
33522
33815
  naming = new Set;
@@ -33531,7 +33824,7 @@ class WorktreeService {
33531
33824
  this.archive = new WorktreeArchive(d.paths.archiveDir);
33532
33825
  this.counts = new Counts({
33533
33826
  read: (id) => this.readCounts(id),
33534
- going: (id) => this.going.has(id)
33827
+ going: (id) => this.going.has(id) || this.arriving.has(id)
33535
33828
  });
33536
33829
  this.trunk = new Trunk({
33537
33830
  state: d.state,
@@ -33544,6 +33837,7 @@ class WorktreeService {
33544
33837
  hub: d.hub,
33545
33838
  runtime: d.runtime,
33546
33839
  paths: d.paths,
33840
+ runs: this.runs,
33547
33841
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
33548
33842
  teardown: (wt, repo) => this.teardown(wt, repo),
33549
33843
  discard: async (id) => {
@@ -33615,39 +33909,55 @@ class WorktreeService {
33615
33909
  const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
33616
33910
 
33617
33911
  `) : prompt;
33618
- const claimed = await this.spare.claim(repoId, title, opts.worktreeId);
33619
- if (claimed) {
33620
- if (variant)
33621
- claimed.variant = variant;
33622
- if (opts.createdBy)
33623
- claimed.createdBy = opts.createdBy;
33624
- if (given)
33625
- delete claimed.unnamed;
33626
- if (opts.from)
33627
- claimed.from = opts.from;
33628
- claimed.promptedAt = claimed.createdAt;
33629
- claimed.agent = agent;
33630
- if (opts.mode)
33631
- claimed.mode = opts.mode;
33632
- if (opts.model)
33633
- claimed.model = opts.model;
33634
- if (opts.effort)
33635
- claimed.effort = opts.effort;
33636
- if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
33637
- claimed.profile = profile;
33638
- this.restartProcs(claimed);
33639
- }
33640
- const carried = await this.carryMain(repo, claimed, !!opts.carry);
33641
- this.d.state.save();
33642
- this.d.hub.emit("worktreesChanged");
33643
- const rt = this.d.runtime.ensureAgent(claimed);
33912
+ const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
33913
+ const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
33914
+ let carriedIn;
33915
+ let release;
33916
+ const stamps = {
33917
+ createdBy: opts.createdBy,
33918
+ agent,
33919
+ mode: opts.mode,
33920
+ model: opts.model,
33921
+ effort: opts.effort,
33922
+ variant,
33923
+ from: opts.from,
33924
+ profile,
33925
+ unnamed: given ? undefined : true
33926
+ };
33927
+ const claimed = await this.spare.claim(repoId, title, opts.worktreeId, stamps, async (wt) => {
33928
+ const rt = this.d.runtime.ensureAgent(wt);
33929
+ release = rt.agent.hold();
33644
33930
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
33645
33931
  await rt.agent.restart();
33646
- rt.agent.send(agentPrompt, { context: withCarry(context(claimed.id), carried), attachments });
33647
- this.nameOrAsk(claimed, task, given, variant);
33648
- if (carried.unmoved)
33649
- throw new UserError(carried.unmoved, { delivered: true });
33650
- return claimed;
33932
+ carriedIn = await this.carryMain(repo, wt, !!opts.carry);
33933
+ rt.agent.send(agentPrompt, { context: withCarry(context(wt.id), carriedIn), attachments });
33934
+ });
33935
+ if (claimed) {
33936
+ const { wt } = claimed;
33937
+ const rt = this.d.runtime.ensureAgent(wt);
33938
+ this.firstPrompt.set(wt.id, claimed.settled.then(() => {
33939
+ return;
33940
+ }, () => {
33941
+ return;
33942
+ }));
33943
+ this.d.hub.emit("worktreesChanged");
33944
+ try {
33945
+ await claimed.settled;
33946
+ } catch (e) {
33947
+ if (carriedIn && rt.agent.queueLength > 0)
33948
+ rt.agent.unqueue(0);
33949
+ release?.();
33950
+ throw e instanceof UserError ? e : new UserError(e instanceof Error ? e.message : String(e));
33951
+ } finally {
33952
+ this.firstPrompt.delete(wt.id);
33953
+ }
33954
+ release?.();
33955
+ if (restart)
33956
+ this.restartProcs(wt);
33957
+ this.nameOrAsk(wt, task, given, variant);
33958
+ if (carriedIn?.unmoved)
33959
+ throw new UserError(carriedIn.unmoved, { delivered: true });
33960
+ return wt;
33651
33961
  }
33652
33962
  if (!await isGitRepo(repo.path)) {
33653
33963
  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.`);
@@ -33808,8 +34118,9 @@ class WorktreeService {
33808
34118
  this.launch(wt, repo, repo.path);
33809
34119
  return wt;
33810
34120
  }
33811
- async requireDiscovered(repoId, repoPath, target) {
33812
- const rows = await discoverIn(repoId, repoPath, this.d.state.worktrees);
34121
+ async requireDiscovered(repoId, repoPath, target, except) {
34122
+ const known = this.d.state.worktrees.filter((w) => w.id !== except);
34123
+ const rows = await discoverIn(repoId, repoPath, known);
33813
34124
  const found = rows.find((r) => canonical(r.path) === target);
33814
34125
  if (!found)
33815
34126
  throw new UserError("that worktree is gone, or Toyon already has it");
@@ -33824,38 +34135,50 @@ class WorktreeService {
33824
34135
  const repoId = r.repoId;
33825
34136
  const repo = this.d.state.requireRepo(repoId);
33826
34137
  const target = canonical(r.path);
33827
- const wt = await withRepoLock(repo.path, async () => {
33828
- const found = await this.requireDiscovered(repoId, repo.path, target);
33829
- if (found.locked) {
33830
- throw new UserError(`${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`);
33831
- }
33832
- if (!found.branch)
33833
- throw new UserError(`${found.name} is detached: check out a branch in it first`);
33834
- const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((r) => isInside(target, canonical(r.path)));
33835
- if (enclosing)
33836
- throw new UserError(`${found.name} sits inside ${enclosing.path}, which Toyon already manages`);
33837
- const rec = {
33838
- id: shortId(),
33839
- repoId,
33840
- path: found.path,
33841
- branch: found.branch,
33842
- kind: "worktree",
33843
- proxyPort: await allocateProxyPort(),
33844
- title: found.name,
33845
- createdAt: Date.now(),
33846
- agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
33847
- parked: true,
33848
- ...message ? { promptedAt: Date.now() } : {},
33849
- ...createdBy ? { createdBy } : {}
33850
- };
33851
- return rec;
33852
- });
34138
+ const pushed = this.discoveredById(worktreeId);
34139
+ if (pushed?.locked)
34140
+ throw new UserError(heldBy(pushed));
34141
+ if (!r.branch)
34142
+ throw new UserError(`${r.name} is detached: check out a branch in it first`);
34143
+ const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((w) => isInside(target, canonical(w.path)));
34144
+ if (enclosing)
34145
+ throw new UserError(`${r.name} sits inside ${enclosing.path}, which Toyon already manages`);
34146
+ const wt = {
34147
+ id: shortId(),
34148
+ repoId,
34149
+ path: r.path,
34150
+ branch: r.branch,
34151
+ kind: "worktree",
34152
+ proxyPort: await allocateProxyPort(),
34153
+ title: r.name,
34154
+ createdAt: Date.now(),
34155
+ agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
34156
+ parked: true,
34157
+ ...message ? { promptedAt: Date.now() } : {},
34158
+ ...createdBy ? { createdBy } : {}
34159
+ };
34160
+ const { rt, release } = this.listAhead(wt, message);
33853
34161
  this.invalidateDiscovered();
33854
- this.launch(wt, repo, repo.path, { setupCommands: false });
33855
- const { agent } = this.d.runtime.ensureAgent(wt);
33856
- agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
33857
- if (message)
33858
- agent.send(message.text, { attachments: message.attachments });
34162
+ this.d.hub.emit("worktreesChanged");
34163
+ try {
34164
+ await withRepoLock(repo.path, async () => {
34165
+ const found = await this.requireDiscovered(repoId, repo.path, target, wt.id);
34166
+ if (found.locked)
34167
+ throw new UserError(heldBy(found));
34168
+ if (!found.branch)
34169
+ throw new UserError(`${found.name} is detached: check out a branch in it first`);
34170
+ wt.branch = found.branch;
34171
+ });
34172
+ } catch (e) {
34173
+ await this.unlist(wt, !!message);
34174
+ this.invalidateDiscovered();
34175
+ this.d.hub.emit("worktreesChanged");
34176
+ throw e;
34177
+ }
34178
+ this.d.state.save();
34179
+ fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path, { setupCommands: false }), "setup + start");
34180
+ rt.agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
34181
+ release();
33859
34182
  return wt;
33860
34183
  }
33861
34184
  async ownProcs(worktreeId, names) {
@@ -34270,66 +34593,113 @@ class WorktreeService {
34270
34593
  if (rec.auto)
34271
34594
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
34272
34595
  const path = existsSync33(old.path) ? join30(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
34273
- const branch = await withRepoLock(repo.path, async () => {
34274
- if (!await commitOf(repo.path, archiveRef(old.id))) {
34275
- throw new UserError(`${old.title}'s kept commits are gone from git`);
34276
- }
34277
- let name = old.branch;
34278
- let create = true;
34279
- const tip = await commitOf(repo.path, `refs/heads/${name}`);
34280
- if (tip && hasOwnBranch(old)) {
34281
- name = `${name}-${shortId().slice(0, 3)}`;
34282
- } else if (tip) {
34283
- if (tip !== kept.head && kept.snapshot) {
34284
- throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
34285
- }
34286
- create = false;
34287
- }
34288
- try {
34289
- await checkOutKept(repo.path, path, kept, { name, create });
34290
- } catch (e) {
34291
- await git(repo.path, "worktree", "remove", "--force", path);
34292
- if (create)
34293
- await git(repo.path, "branch", "-D", name);
34294
- throw e;
34295
- }
34296
- return name;
34297
- });
34298
34596
  const { variant: _variant, ...rest } = old;
34299
34597
  const wt = {
34300
34598
  ...rest,
34301
34599
  repoId: repo.id,
34302
34600
  path,
34303
- branch,
34601
+ branch: old.branch,
34304
34602
  proxyPort: await allocateProxyPort(),
34305
34603
  promptedAt: Date.now(),
34306
34604
  parked: true,
34307
34605
  ...createdBy ? { createdBy } : {}
34308
34606
  };
34607
+ const chat = { ...this.chatFiles(old.id), plans: this.plansScratch(old.id) };
34309
34608
  try {
34310
- this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join30(path, PLANS_DIR) });
34609
+ this.archive.take(old.id, chat);
34311
34610
  } catch (e) {
34312
34611
  log.warn(old.id, "could not move its chat back out of the archive", e);
34612
+ throw new UserError(`could not move ${old.title}'s chat back out of the archive`);
34313
34613
  }
34314
34614
  if (rec.sessionId && path === old.path)
34315
34615
  this.d.state.setSession(wt.id, rec.sessionId);
34616
+ this.arriving.add(wt.id);
34617
+ const { rt, release } = this.listAhead(wt, message);
34618
+ const run = this.runs.begin(wt.id, "setup", {
34619
+ timeoutMs: timeoutFor(repo.config, "setup"),
34620
+ stage: "checking out the branch"
34621
+ });
34622
+ this.d.hub.emit("worktreesChanged");
34623
+ this.d.hub.emit("archiveChanged", repo.id);
34624
+ let branch;
34625
+ try {
34626
+ branch = await withRepoLock(repo.path, () => this.checkOutArchived(repo, old, kept, path));
34627
+ } catch (e) {
34628
+ await this.takeBack(wt, rec, chat, run, !!message);
34629
+ throw e;
34630
+ }
34631
+ run.finish(0);
34632
+ this.arriving.delete(wt.id);
34633
+ wt.branch = branch;
34634
+ move(chat.plans, join30(path, PLANS_DIR));
34316
34635
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
34317
34636
  if (!dropped.ok)
34318
34637
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
34319
34638
  this.counts.stale(wt.id);
34320
- this.launch(wt, repo, repo.path);
34321
- this.d.runtime.ensureAgent(wt).agent.note({
34639
+ this.d.state.save();
34640
+ this.d.hub.emit("worktreesChanged");
34641
+ fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path), "setup + start");
34642
+ rt.agent.note({
34322
34643
  type: "restored",
34323
34644
  archivedAt: rec.archivedAt,
34324
34645
  branch,
34325
34646
  uncommitted: !!kept.snapshot,
34326
34647
  ts: Date.now()
34327
34648
  });
34328
- if (message)
34329
- this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
34330
- this.d.hub.emit("archiveChanged", repo.id);
34649
+ release();
34650
+ this.d.hub.emit("filesChanged", wt.id);
34331
34651
  return wt;
34332
34652
  }
34653
+ async checkOutArchived(repo, old, kept, path) {
34654
+ if (!await commitOf(repo.path, archiveRef(old.id))) {
34655
+ throw new UserError(`${old.title}'s kept commits are gone from git`);
34656
+ }
34657
+ let name = old.branch;
34658
+ let create = true;
34659
+ const tip = await commitOf(repo.path, `refs/heads/${name}`);
34660
+ if (tip && hasOwnBranch(old)) {
34661
+ name = `${name}-${shortId().slice(0, 3)}`;
34662
+ } else if (tip) {
34663
+ if (tip !== kept.head && kept.snapshot) {
34664
+ throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
34665
+ }
34666
+ create = false;
34667
+ }
34668
+ try {
34669
+ await checkOutKept(repo.path, path, kept, { name, create });
34670
+ } catch (e) {
34671
+ await git(repo.path, "worktree", "remove", "--force", path);
34672
+ if (create)
34673
+ await git(repo.path, "branch", "-D", name);
34674
+ throw e;
34675
+ }
34676
+ return name;
34677
+ }
34678
+ listAhead(wt, message) {
34679
+ this.d.runtime.markSetup(wt.id, true);
34680
+ this.d.state.addWorktree(wt);
34681
+ const rt = this.d.runtime.ensureAgent(wt);
34682
+ const release = rt.agent.hold();
34683
+ if (message)
34684
+ rt.agent.send(message.text, { attachments: message.attachments });
34685
+ return { rt, release };
34686
+ }
34687
+ async takeBack(wt, rec, chat, run, queued) {
34688
+ run.finish(0);
34689
+ await this.unlist(wt, queued);
34690
+ this.arriving.delete(wt.id);
34691
+ this.archive.put(rec, chat);
34692
+ this.d.hub.emit("worktreesChanged");
34693
+ this.d.hub.emit("archiveChanged", wt.repoId);
34694
+ }
34695
+ async unlist(wt, queued) {
34696
+ const agent = this.d.runtime.agentFor(wt.id);
34697
+ if (queued && agent && agent.queueLength > 0)
34698
+ agent.unqueue(0);
34699
+ await this.d.runtime.stop(wt.id);
34700
+ this.d.state.removeWorktree(wt.id, { refused: true });
34701
+ releasePort(wt.proxyPort);
34702
+ }
34333
34703
  async deleteArchived(archiveId) {
34334
34704
  const rec = this.archive.get(archiveId);
34335
34705
  if (!rec)
@@ -34467,7 +34837,9 @@ class WorktreeService {
34467
34837
  const srcNm = join30(depsSource, "node_modules");
34468
34838
  const dstNm = join30(wt.path, "node_modules");
34469
34839
  if (existsSync33(srcNm) && !existsSync33(dstNm)) {
34840
+ const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
34470
34841
  const r = await cloneTree(srcNm, dstNm);
34842
+ run.finish(0);
34471
34843
  if (r.ok)
34472
34844
  this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
34473
34845
  else
@@ -34746,9 +35118,10 @@ ${wt.landing.body}` : s;
34746
35118
  const policy = landPolicy(repo.config);
34747
35119
  const own = hasOwnBranch(wt);
34748
35120
  const w = this.landWatch(wt);
34749
- const suggested = wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
35121
+ const typed = message?.trim();
35122
+ const suggested = typed || (wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
34750
35123
 
34751
- ${wt.landing.body}` : wt.landing.subject : undefined;
35124
+ ${wt.landing.body}` : wt.landing.subject : undefined);
34752
35125
  const base = baseOf(repo);
34753
35126
  if (policy.land === "pr") {
34754
35127
  if (wt.pr?.state === "merged" && !prTaken(wt)) {
@@ -34792,13 +35165,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34792
35165
  const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
34793
35166
  return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
34794
35167
  }
35168
+ const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
34795
35169
  const result = await openPr({
34796
35170
  worktreePath: wt.path,
34797
35171
  branch: wt.branch,
34798
35172
  defaultBr: repo.defaultBranch,
34799
35173
  base,
34800
- subject: wt.landing?.subject,
34801
- body: wt.landing?.body,
35174
+ subject: named?.subject,
35175
+ body: named?.body,
34802
35176
  automerge: policy.automerge,
34803
35177
  method: policy.merge
34804
35178
  }, w);
@@ -34827,7 +35201,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34827
35201
  if (!taken.ok)
34828
35202
  return taken;
34829
35203
  const mark = await landingMark(wt.path, base);
34830
- const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
35204
+ const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
34831
35205
  const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
34832
35206
  if (!landed.ok)
34833
35207
  return landed;
@@ -34878,7 +35252,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34878
35252
  sha = await commitOf(wt.path, base) ?? "";
34879
35253
  break;
34880
35254
  }
34881
- squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
35255
+ squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
34882
35256
  const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
34883
35257
  await this.headMoved(wt.id);
34884
35258
  if (!built.ok || !built.sha)
@@ -34930,8 +35304,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34930
35304
  async landedWhat(repo, wt, mark, opts) {
34931
35305
  if (opts.pr?.title)
34932
35306
  return [opts.pr.title];
34933
- const subject = opts.squash?.split(`
34934
- `)[0]?.trim();
35307
+ const subject = opts.squash ? splitMessage(opts.squash).subject : "";
34935
35308
  if (subject)
34936
35309
  return [subject];
34937
35310
  const read = await rangeSubjects(repo.path, mark.base, mark.tip);
@@ -35238,7 +35611,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35238
35611
  await markTurn(wt.path, wt.id);
35239
35612
  }
35240
35613
  async gitStatus(worktreeId) {
35241
- if (this.going.has(worktreeId))
35614
+ if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35242
35615
  return null;
35243
35616
  const r = this.readable(worktreeId);
35244
35617
  if (!r)
@@ -35281,7 +35654,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35281
35654
  }
35282
35655
  if (r.wt && isMain)
35283
35656
  this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
35284
- if (this.going.has(worktreeId))
35657
+ if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35285
35658
  return null;
35286
35659
  this.noteCounts(worktreeId, isMain, files.length, counts);
35287
35660
  return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
@@ -35327,6 +35700,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35327
35700
  const wt = this.requireRun(worktreeId);
35328
35701
  if (wt.kind === "main")
35329
35702
  throw new UserError("nothing runs on main: the plus starts a worktree for it");
35703
+ await this.firstPrompt.get(worktreeId);
35330
35704
  const agent = this.d.runtime.agentFor(worktreeId);
35331
35705
  if (!agent)
35332
35706
  throw new UserError("worktree still starting; try again in a moment");
@@ -35532,7 +35906,8 @@ var {
35532
35906
  helperStub: HELPER_STUB,
35533
35907
  cliEntry: CLI_ENTRY,
35534
35908
  sourceRoot: SOURCE_ROOT,
35535
- packageJson: PACKAGE_JSON
35909
+ packageJson: PACKAGE_JSON,
35910
+ changelog: CHANGELOG
35536
35911
  } = locateAssets(here);
35537
35912
  var pruned = pruneAssets(SHELL_DIST);
35538
35913
  if (pruned > 0)
@@ -35776,6 +36151,7 @@ var {
35776
36151
  port,
35777
36152
  token,
35778
36153
  shellDist: SHELL_DIST,
36154
+ changelog: CHANGELOG,
35779
36155
  version: package_default.version,
35780
36156
  noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
35781
36157
  startLink: () => helper?.startLink() ?? null,