toyon 0.14.0 → 0.16.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 +629 -0
  3. package/dist/bridge.js +1 -1
  4. package/dist/cli.js +41 -22
  5. package/dist/daemon.js +1233 -971
  6. package/dist/shell/assets/{Editor-Gzhl3Oqu.js → Editor-BATywBIx.js} +3 -3
  7. package/dist/shell/assets/{XTerm-D6TL52yQ.js → XTerm-So8-Vafj.js} +1 -1
  8. package/dist/shell/assets/{cssMode-BfNm0a39.js → cssMode-DlHcrkXj.js} +1 -1
  9. package/dist/shell/assets/{freemarker2-DhiwR6KJ.js → freemarker2-D3xOnVxc.js} +1 -1
  10. package/dist/shell/assets/{grammar-thv9InXF.js → grammar-RZJ5oG33.js} +1 -1
  11. package/dist/shell/assets/{handlebars-BbkhCZXb.js → handlebars-UbIG7080.js} +1 -1
  12. package/dist/shell/assets/{html-yzhYlR33.js → html-D92eLK6U.js} +1 -1
  13. package/dist/shell/assets/{htmlMode-DPICAUY8.js → htmlMode-DsZIJzoj.js} +1 -1
  14. package/dist/shell/assets/index-BvYzBRq_.css +1 -0
  15. package/dist/shell/assets/index-CHQwm2uN.js +268 -0
  16. package/dist/shell/assets/{javascript-D9eBjR4g.js → javascript-DUkgFtV2.js} +1 -1
  17. package/dist/shell/assets/{jsonMode-cNulMi5B.js → jsonMode-BiIooPg9.js} +1 -1
  18. package/dist/shell/assets/{liquid-C_SKIx1n.js → liquid-DFFjZ5TH.js} +1 -1
  19. package/dist/shell/assets/{lspLanguageFeatures-DNU8BRHj.js → lspLanguageFeatures-C7Ug4qkg.js} +1 -1
  20. package/dist/shell/assets/{mdx-Bt4opbQO.js → mdx-DdjSgAuG.js} +1 -1
  21. package/dist/shell/assets/{python-Crb-EuXe.js → python-DPUF4OK9.js} +1 -1
  22. package/dist/shell/assets/{razor-D3BlkbYO.js → razor-IRYJ0pOA.js} +1 -1
  23. package/dist/shell/assets/{tsMode-BYuAS95U.js → tsMode-CljE7MPO.js} +1 -1
  24. package/dist/shell/assets/{xml-G5xafDqw.js → xml-D8Zv3Gd0.js} +1 -1
  25. package/dist/shell/assets/{yaml-CRp1x3nR.js → yaml-Dxx9PL0B.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
@@ -11,14 +11,10 @@ var TOOL_SEARCH = "ToolSearch";
11
11
  var ASK_TOOL = "AskUserQuestion";
12
12
  var TOYON_MCP_SERVER = "toyon";
13
13
  var HANDOFF_TOOL = "handoff";
14
- var PREVIEW_TOOL = "preview";
15
14
 
16
15
  // ../shared/src/agent-tools.ts
17
16
  var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
18
17
  var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
19
- function isEditTool(ev) {
20
- return ev.kind ? EDIT_KINDS.has(ev.kind) : EDIT_TOOLS.has(ev.name);
21
- }
22
18
  var WRITE_KINDS = new Set(["edit", "delete", "move"]);
23
19
  var WRITE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
24
20
  function isWriteTool(ev) {
@@ -29,7 +25,7 @@ function isWrittenKind(kind) {
29
25
  return WRITTEN_KINDS.includes(kind ?? "");
30
26
  }
31
27
  function writingCall(call, input) {
32
- return (isWrittenKind(call.kind) || call.name === ASK_TOOL) && emptyInput(input);
28
+ return (isWrittenKind(call.kind) || call.name === ASK_TOOL || call.name === TOOL_SEARCH) && emptyInput(input);
33
29
  }
34
30
  function emptyInput(input) {
35
31
  if (!input || typeof input !== "object")
@@ -6602,11 +6598,9 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6602
6598
  }
6603
6599
 
6604
6600
  // ../shared/src/protocol/limits.ts
6605
- var PROTOCOL_VERSION = 107;
6601
+ var PROTOCOL_VERSION = 112;
6606
6602
  var PAGE_ERROR_MAX_CHARS = 2000;
6607
6603
  var PAGE_TITLE_MAX_CHARS = 1000;
6608
- var RENDER_ERRORS_MAX = 10;
6609
- var PAGE_URL_MAX_CHARS = 2048;
6610
6604
  var FILE_MAX_CHARS = 5000000;
6611
6605
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
6612
6606
  var IMAGE_MAX_EDGE = 2576;
@@ -6961,6 +6955,8 @@ var clientMsgSchema = discriminatedUnion("t", [
6961
6955
  object({ t: literal("fix"), worktreeId: id, toolId: string2().min(1).max(200) }),
6962
6956
  object({ t: literal("continue"), worktreeId: id }),
6963
6957
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6958
+ object({ t: literal("reveal-path"), path: relPath }),
6959
+ object({ t: literal("reveal-attachment"), worktreeId: id, file: string2().min(1).max(120) }),
6964
6960
  object({ t: literal("stop-agent"), worktreeId: id }),
6965
6961
  object({ t: literal("seen"), worktreeId: id }),
6966
6962
  object({ t: literal("mark-unread"), worktreeId: id }),
@@ -6977,17 +6973,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6977
6973
  object({ t: literal("forget-visit"), repoId: id, path: routePath }),
6978
6974
  object({ t: literal("page-error"), worktreeId: id, message: string2().min(1).max(PAGE_ERROR_MAX_CHARS) }),
6979
6975
  object({ t: literal("page-loaded"), worktreeId: id }),
6980
- object({ t: literal("fix-page"), worktreeId: id }),
6981
- object({
6982
- t: literal("rendered"),
6983
- worktreeId: id,
6984
- id: string2().min(1).max(64),
6985
- ok: boolean2(),
6986
- reason: string2().max(200).optional(),
6987
- url: string2().max(PAGE_URL_MAX_CHARS).optional(),
6988
- title: pageTitle.optional(),
6989
- errors: array(string2().max(PAGE_ERROR_MAX_CHARS)).max(RENDER_ERRORS_MAX)
6990
- }),
6991
6976
  object({ t: literal("pick-variant"), worktreeId: id }),
6992
6977
  object({
6993
6978
  t: literal("unqueue"),
@@ -7059,6 +7044,7 @@ var clientMsgSchema = discriminatedUnion("t", [
7059
7044
  }),
7060
7045
  object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
7061
7046
  object({ t: literal("agent-logout"), agent: id }),
7047
+ object({ t: literal("agent-check"), agent: id }),
7062
7048
  object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
7063
7049
  object({ t: literal("reveal-agent-file"), agent: id, file: string2().max(64), repoId: id.optional() }),
7064
7050
  object({ t: literal("import-theme"), name: string2().max(300), source: string2().max(2000000) }),
@@ -8528,7 +8514,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8528
8514
  // ../daemon/package.json
8529
8515
  var package_default = {
8530
8516
  name: "@toyon/daemon",
8531
- version: "0.14.0",
8517
+ version: "0.16.0",
8532
8518
  private: true,
8533
8519
  type: "module",
8534
8520
  main: "src/index.ts",
@@ -8544,6 +8530,10 @@ var package_default = {
8544
8530
  }
8545
8531
  };
8546
8532
 
8533
+ // ../daemon/src/agent/acp/transport.ts
8534
+ import { spawn } from "child_process";
8535
+ import { Readable, Writable } from "stream";
8536
+
8547
8537
  // ../../node_modules/.bun/@agentclientprotocol+sdk@1.4.0+ab3ed8e9421bfd7b/node_modules/@agentclientprotocol/sdk/dist/schema/index.js
8548
8538
  var AGENT_METHODS = {
8549
8539
  initialize: "initialize",
@@ -12620,16 +12610,6 @@ var legacyClientNotificationMethods = new Set([
12620
12610
  CLIENT_METHODS.elicitation_complete
12621
12611
  ]);
12622
12612
 
12623
- // ../daemon/src/core/errors.ts
12624
- class UserError extends Error {
12625
- delivered;
12626
- constructor(message, about = {}) {
12627
- super(message);
12628
- this.name = "UserError";
12629
- this.delivered = about.delivered ?? false;
12630
- }
12631
- }
12632
-
12633
12613
  // ../daemon/src/core/log.ts
12634
12614
  var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
12635
12615
  function envLevel() {
@@ -12675,131 +12655,6 @@ function fireAndForget(tag, p, what = "background task") {
12675
12655
  p.catch((e) => log.warn(tag, `${what} failed`, e));
12676
12656
  }
12677
12657
 
12678
- // ../daemon/src/agent/acp/authstatus.ts
12679
- var AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
12680
- var KINDS = new Set(["none", "api_key", "account", "external", "gateway"]);
12681
- function supportsLogout(caps) {
12682
- return !!caps?.auth?.logout;
12683
- }
12684
- var str = (v) => typeof v === "string" && v.trim() ? v : undefined;
12685
- function parseAuthStatus(params) {
12686
- const status = params?.authStatus;
12687
- if (!status || typeof status !== "object")
12688
- return null;
12689
- const kind = status.kind;
12690
- if (!KINDS.has(kind))
12691
- return null;
12692
- const account = status.account ?? undefined;
12693
- const fields = account ? {
12694
- ...str(account.email) ? { email: str(account.email) } : {},
12695
- ...str(account.organization) ? { organization: str(account.organization) } : {},
12696
- ...str(account.plan) ? { plan: str(account.plan) } : {}
12697
- } : {};
12698
- return {
12699
- kind,
12700
- label: str(status.label) ?? (kind === "none" ? "not logged in" : "logged in"),
12701
- ...str(status.detail) ? { detail: str(status.detail) } : {},
12702
- ...Object.keys(fields).length ? { account: fields } : {}
12703
- };
12704
- }
12705
-
12706
- // ../daemon/src/agent/accounts.ts
12707
- class AgentAccounts {
12708
- d;
12709
- known = new Map;
12710
- busy = new Map;
12711
- onChange = null;
12712
- constructor(d) {
12713
- this.d = d;
12714
- }
12715
- observe(agentId, o) {
12716
- const before = this.known.get(agentId) ?? { canLogout: false, status: undefined };
12717
- const entry = { ...before };
12718
- if (o.canLogout !== undefined)
12719
- entry.canLogout = o.canLogout;
12720
- if (o.status)
12721
- entry.status = o.status;
12722
- this.known.set(agentId, entry);
12723
- if (before.canLogout !== entry.canLogout || !same(before.status, entry.status))
12724
- this.onChange?.();
12725
- }
12726
- observeLimits(agentId, limits) {
12727
- const before = this.known.get(agentId) ?? { canLogout: false, status: undefined };
12728
- const entry = { ...before, limits };
12729
- this.known.set(agentId, entry);
12730
- if (!sameLimits(before.limits, limits))
12731
- this.onChange?.();
12732
- }
12733
- describe(infos) {
12734
- return infos.map((info) => {
12735
- const k = this.known.get(info.id);
12736
- if (!k)
12737
- return info;
12738
- return {
12739
- ...info,
12740
- ...k.status ? { auth: k.status } : {},
12741
- ...k.canLogout ? { canLogout: true } : {},
12742
- ...k.limits ? { limits: k.limits } : {}
12743
- };
12744
- });
12745
- }
12746
- logout(agentId) {
12747
- const running = this.busy.get(agentId);
12748
- if (running)
12749
- return running;
12750
- const p = this.runLogout(agentId).finally(() => this.busy.delete(agentId));
12751
- this.busy.set(agentId, p);
12752
- return p;
12753
- }
12754
- async runLogout(agentId) {
12755
- const spec = this.d.require(agentId);
12756
- let pushed = null;
12757
- let signingOut = false;
12758
- const app = client({ name: "toyon" }).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
12759
- if (c.params && signingOut)
12760
- pushed = c.params;
12761
- });
12762
- const link = await this.d.connect(app, spec);
12763
- try {
12764
- const init = await link.conn.agent.request(methods.agent.initialize, {
12765
- protocolVersion: PROTOCOL_VERSION2,
12766
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
12767
- clientInfo: { name: "toyon", version: "0" }
12768
- });
12769
- if (!supportsLogout(init.agentCapabilities))
12770
- throw new UserError(`${spec.name} cannot be signed out from here`);
12771
- signingOut = true;
12772
- await link.conn.agent.request(methods.agent.logout, {});
12773
- const entry = this.known.get(agentId);
12774
- this.known.set(agentId, { canLogout: true, ...pushed ? { status: pushed } : {} });
12775
- if (!entry || !same(entry.status, pushed ?? undefined))
12776
- this.onChange?.();
12777
- log.info("agents", `${agentId} signed out`);
12778
- } catch (e) {
12779
- if (e instanceof UserError)
12780
- throw e;
12781
- throw new UserError(`${spec.name} could not be signed out: ${e instanceof Error ? e.message : String(e)}`);
12782
- } finally {
12783
- await link.kill();
12784
- }
12785
- }
12786
- }
12787
- function sameLimits(a, b) {
12788
- if (!a || !b)
12789
- return a === b;
12790
- const win = (x) => x ? `${x.used}@${x.resetsAt}` : "";
12791
- return a.status === b.status && a.binding === b.binding && win(a.windows.five_hour) === win(b.windows.five_hour) && win(a.windows.seven_day) === win(b.windows.seven_day);
12792
- }
12793
- function same(a, b) {
12794
- if (!a || !b)
12795
- return a === b;
12796
- return a.kind === b.kind && a.label === b.label && a.detail === b.detail && a.account?.email === b.account?.email && a.account?.organization === b.account?.organization && a.account?.plan === b.account?.plan;
12797
- }
12798
-
12799
- // ../daemon/src/agent/acp/transport.ts
12800
- import { spawn } from "child_process";
12801
- import { Readable, Writable } from "stream";
12802
-
12803
12658
  // ../daemon/src/runtime/kill.ts
12804
12659
  var KILL_GRACE_MS = 3000;
12805
12660
  var EXIT_GRACE_MS = 200;
@@ -12903,6 +12758,16 @@ import { createHash } from "crypto";
12903
12758
  import { copyFile as copyFile2, mkdir, readFile, stat, writeFile } from "fs/promises";
12904
12759
  import { join as join2 } from "path";
12905
12760
 
12761
+ // ../daemon/src/core/errors.ts
12762
+ class UserError extends Error {
12763
+ delivered;
12764
+ constructor(message, about = {}) {
12765
+ super(message);
12766
+ this.name = "UserError";
12767
+ this.delivered = about.delivered ?? false;
12768
+ }
12769
+ }
12770
+
12906
12771
  // ../daemon/src/agent/uploads.ts
12907
12772
  import { randomBytes } from "crypto";
12908
12773
  import { mkdirSync, readdirSync, rmSync as rmSync2 } from "fs";
@@ -13187,13 +13052,7 @@ class AttachmentStore {
13187
13052
  const { n: _n, ...pick } = ref;
13188
13053
  return pick;
13189
13054
  }
13190
- let path = null;
13191
- for (const at of [this.fileFor(worktreeId, ref.file), archived?.(ref.file) ?? null]) {
13192
- if (at && await Bun.file(at).exists()) {
13193
- path = at;
13194
- break;
13195
- }
13196
- }
13055
+ const path = await this.locate(worktreeId, ref.file, archived);
13197
13056
  if (!path)
13198
13057
  return null;
13199
13058
  if (ref.kind === "paste") {
@@ -13211,6 +13070,13 @@ class AttachmentStore {
13211
13070
  const { name, bytes, width, height } = ref;
13212
13071
  return { kind: "image", upload, name, mimeType, bytes, width, height };
13213
13072
  }
13073
+ async locate(worktreeId, file, archived) {
13074
+ for (const at of [this.fileFor(worktreeId, file), archived?.(file) ?? null]) {
13075
+ if (at && await Bun.file(at).exists())
13076
+ return at;
13077
+ }
13078
+ return null;
13079
+ }
13214
13080
  async write(worktreeId, file, fill) {
13215
13081
  const wtDir = attachmentsDirFor(this.dir, worktreeId);
13216
13082
  const path = join2(wtDir, file);
@@ -13595,6 +13461,37 @@ async function readCapped(req, max) {
13595
13461
  return Buffer.concat(chunks).toString("utf8");
13596
13462
  }
13597
13463
 
13464
+ // ../daemon/src/agent/acp/authstatus.ts
13465
+ var AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
13466
+ var KINDS = new Set(["none", "api_key", "account", "external", "gateway"]);
13467
+ function supportsLogout(caps) {
13468
+ return !!caps?.auth?.logout;
13469
+ }
13470
+ var str = (v) => typeof v === "string" && v.trim() ? v : undefined;
13471
+ function parseAuthStatus(params) {
13472
+ const status = params?.authStatus;
13473
+ if (!status || typeof status !== "object")
13474
+ return null;
13475
+ const kind = status.kind;
13476
+ if (!KINDS.has(kind))
13477
+ return null;
13478
+ const account = status.account ?? undefined;
13479
+ const fields = account ? {
13480
+ ...str(account.email) ? { email: str(account.email) } : {},
13481
+ ...str(account.organization) ? { organization: str(account.organization) } : {},
13482
+ ...str(account.plan) ? { plan: str(account.plan) } : {}
13483
+ } : {};
13484
+ return {
13485
+ kind,
13486
+ label: str(status.label) ?? (kind === "none" ? "not logged in" : "logged in"),
13487
+ ...str(status.detail) ? { detail: str(status.detail) } : {},
13488
+ ...Object.keys(fields).length ? { account: fields } : {}
13489
+ };
13490
+ }
13491
+ function isAuthRequired(e) {
13492
+ return e instanceof RequestError && e.code === -32000;
13493
+ }
13494
+
13598
13495
  // ../daemon/src/agent/acp/options.ts
13599
13496
  var OPTION_FIELDS = { model: "model", thought_level: "effort" };
13600
13497
  var OPTION_CATEGORIES = Object.keys(OPTION_FIELDS);
@@ -13647,37 +13544,125 @@ function currentValues(options) {
13647
13544
  return out;
13648
13545
  }
13649
13546
 
13650
- // ../daemon/src/agent/probe.ts
13651
- class OptionProbe {
13547
+ // ../daemon/src/agent/readiness.ts
13548
+ var CHECK_COOLDOWN_MS = 30000;
13549
+
13550
+ class AgentReadiness {
13652
13551
  d;
13653
- tried = new Set;
13552
+ known = new Map;
13553
+ inflight = new Map;
13554
+ logouts = new Map;
13555
+ onChange = null;
13556
+ now;
13654
13557
  constructor(d) {
13655
13558
  this.d = d;
13559
+ this.now = d.now ?? Date.now;
13656
13560
  }
13657
- async missing() {
13561
+ entry(agentId) {
13562
+ let e = this.known.get(agentId);
13563
+ if (!e) {
13564
+ e = { canLogout: false };
13565
+ this.known.set(agentId, e);
13566
+ }
13567
+ return e;
13568
+ }
13569
+ observe(agentId, o) {
13570
+ const e = this.entry(agentId);
13571
+ const before = { ...e };
13572
+ if (o.canLogout !== undefined)
13573
+ e.canLogout = o.canLogout;
13574
+ if (o.status) {
13575
+ e.status = o.status;
13576
+ if (o.status.kind !== "none")
13577
+ delete e.failure;
13578
+ }
13579
+ if (before.canLogout !== e.canLogout || !same(before.status, e.status) || before.failure !== e.failure) {
13580
+ this.onChange?.();
13581
+ }
13582
+ }
13583
+ observeLimits(agentId, limits) {
13584
+ const e = this.entry(agentId);
13585
+ const before = e.limits;
13586
+ e.limits = limits;
13587
+ if (!sameLimits(before, limits))
13588
+ this.onChange?.();
13589
+ }
13590
+ describe() {
13591
+ return this.d.infos().map((info) => {
13592
+ const e = this.known.get(info.id);
13593
+ const models = this.d.options(info.id, "model");
13594
+ const efforts = this.d.options(info.id, "thought_level");
13595
+ return {
13596
+ ...info,
13597
+ ...models.length > 0 ? { models } : {},
13598
+ ...efforts.length > 0 ? { efforts } : {},
13599
+ ...this.d.quick(info.id) ? { quick: true } : {},
13600
+ ...e?.status ? { auth: e.status } : {},
13601
+ ...e?.canLogout ? { canLogout: true } : {},
13602
+ ...e?.limits ? { limits: e.limits } : {},
13603
+ ...e?.failure ? { checkFailed: e.failure } : {}
13604
+ };
13605
+ });
13606
+ }
13607
+ positive(agentId) {
13608
+ const e = this.known.get(agentId);
13609
+ return this.d.options(agentId, "model").length > 0 && !e?.failure && e?.status?.kind !== "none";
13610
+ }
13611
+ check(agentId) {
13612
+ const info = this.d.infos().find((a) => a.id === agentId);
13613
+ if (!info?.available)
13614
+ return Promise.resolve();
13615
+ const running = this.inflight.get(agentId);
13616
+ if (running)
13617
+ return running;
13618
+ if (this.positive(agentId))
13619
+ return Promise.resolve();
13620
+ const e = this.known.get(agentId);
13621
+ if (e?.checkedAt !== undefined && this.now() - e.checkedAt < CHECK_COOLDOWN_MS)
13622
+ return Promise.resolve();
13623
+ const p = this.run(agentId).finally(() => {
13624
+ if (this.inflight.get(agentId) === p)
13625
+ this.inflight.delete(agentId);
13626
+ });
13627
+ this.inflight.set(agentId, p);
13628
+ return p;
13629
+ }
13630
+ async checkMissing() {
13658
13631
  for (const a of this.d.infos()) {
13659
- if (!a.available || this.tried.has(a.id) || this.d.known(a.id))
13660
- continue;
13661
- this.tried.add(a.id);
13662
- await this.probe(a.id);
13632
+ if (a.available && this.d.options(a.id, "model").length === 0)
13633
+ await this.check(a.id);
13663
13634
  }
13664
13635
  }
13665
- async probe(agentId) {
13666
- const tag = `probe:${agentId}`;
13667
- const app = client({ name: "toyon" }).onNotification(methods.client.session.update, () => {});
13636
+ async run(agentId) {
13637
+ const tag = `check:${agentId}`;
13638
+ const found = {};
13639
+ let spec;
13640
+ try {
13641
+ spec = this.d.require(agentId);
13642
+ } catch (e) {
13643
+ this.finish(agentId, firstLine(e), found);
13644
+ return;
13645
+ }
13646
+ const app = client({ name: "toyon" }).onNotification(methods.client.session.update, () => {}).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
13647
+ if (c.params)
13648
+ found.status = c.params;
13649
+ });
13668
13650
  let link;
13669
13651
  try {
13670
- link = await this.d.connect(app, this.d.require(agentId));
13652
+ link = await this.d.connect(app, spec);
13671
13653
  } catch (e) {
13672
- log.warn(tag, "could not start the agent to read its models", e);
13654
+ log.warn(tag, "could not start the agent", e);
13655
+ this.finish(agentId, firstLine(e), found);
13673
13656
  return;
13674
13657
  }
13658
+ let failure;
13675
13659
  try {
13676
13660
  const init = await link.conn.agent.request(methods.agent.initialize, {
13677
13661
  protocolVersion: PROTOCOL_VERSION2,
13678
13662
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
13679
13663
  clientInfo: { name: "toyon", version: "0" }
13680
13664
  });
13665
+ found.canLogout = supportsLogout(init.agentCapabilities);
13681
13666
  const s = await link.conn.agent.request(methods.agent.session.new, { cwd: this.d.cwd, mcpServers: [] });
13682
13667
  for (const [category, opt] of readOptions(s.configOptions))
13683
13668
  this.d.learned(agentId, category, opt.choices);
@@ -13687,18 +13672,97 @@ class OptionProbe {
13687
13672
  });
13688
13673
  }
13689
13674
  } catch (e) {
13690
- log.info(tag, `could not read its models before a worktree runs it: ${e instanceof Error ? e.message : String(e)}`);
13675
+ failure = isAuthRequired(e) ? spec.loginHint : firstLine(e);
13676
+ log.info(tag, `could not read its models: ${failure}`);
13691
13677
  } finally {
13692
13678
  link.conn.close();
13693
13679
  await link.kill();
13694
13680
  }
13681
+ this.finish(agentId, failure, found);
13682
+ }
13683
+ finish(agentId, failure, found) {
13684
+ const e = this.entry(agentId);
13685
+ const before = { ...e };
13686
+ if (found.canLogout !== undefined)
13687
+ e.canLogout = found.canLogout;
13688
+ if (found.status)
13689
+ e.status = found.status;
13690
+ e.failure = failure;
13691
+ e.checkedAt = this.now();
13692
+ if (before.canLogout !== e.canLogout || !same(before.status, e.status) || before.failure !== e.failure) {
13693
+ this.onChange?.();
13694
+ }
13695
+ }
13696
+ logout(agentId) {
13697
+ const running = this.logouts.get(agentId);
13698
+ if (running)
13699
+ return running;
13700
+ const prior = this.inflight.get(agentId) ?? Promise.resolve();
13701
+ const p = prior.then(() => this.runLogout(agentId)).finally(() => {
13702
+ this.logouts.delete(agentId);
13703
+ this.inflight.delete(agentId);
13704
+ });
13705
+ this.logouts.set(agentId, p);
13706
+ this.inflight.set(agentId, p.catch(() => {
13707
+ return;
13708
+ }));
13709
+ return p;
13695
13710
  }
13711
+ async runLogout(agentId) {
13712
+ const spec = this.d.require(agentId);
13713
+ let pushed = null;
13714
+ let signingOut = false;
13715
+ const app = client({ name: "toyon" }).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
13716
+ if (c.params && signingOut)
13717
+ pushed = c.params;
13718
+ });
13719
+ const link = await this.d.connect(app, spec);
13720
+ try {
13721
+ const init = await link.conn.agent.request(methods.agent.initialize, {
13722
+ protocolVersion: PROTOCOL_VERSION2,
13723
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
13724
+ clientInfo: { name: "toyon", version: "0" }
13725
+ });
13726
+ if (!supportsLogout(init.agentCapabilities))
13727
+ throw new UserError(`${spec.name} cannot be signed out from here`);
13728
+ signingOut = true;
13729
+ await link.conn.agent.request(methods.agent.logout, {});
13730
+ const before = this.known.get(agentId);
13731
+ this.known.set(agentId, { canLogout: true, ...pushed ? { status: pushed } : {} });
13732
+ if (!before || !same(before.status, pushed ?? undefined) || before.failure !== undefined)
13733
+ this.onChange?.();
13734
+ log.info("agents", `${agentId} signed out`);
13735
+ } catch (e) {
13736
+ if (e instanceof UserError)
13737
+ throw e;
13738
+ throw new UserError(`${spec.name} could not be signed out: ${e instanceof Error ? e.message : String(e)}`);
13739
+ } finally {
13740
+ await link.kill();
13741
+ }
13742
+ }
13743
+ }
13744
+ function firstLine(e) {
13745
+ const details = e instanceof RequestError ? e.data?.details : undefined;
13746
+ const message = typeof details === "string" ? details : e instanceof Error ? e.message : String(e);
13747
+ return message.replace(/^Internal error: /, "").split(`
13748
+ `)[0] ?? message;
13749
+ }
13750
+ function sameLimits(a, b) {
13751
+ if (!a || !b)
13752
+ return a === b;
13753
+ const win = (x) => x ? `${x.used}@${x.resetsAt}` : "";
13754
+ return a.status === b.status && a.binding === b.binding && win(a.windows.five_hour) === win(b.windows.five_hour) && win(a.windows.seven_day) === win(b.windows.seven_day);
13755
+ }
13756
+ function same(a, b) {
13757
+ if (!a || !b)
13758
+ return a === b;
13759
+ return a.kind === b.kind && a.label === b.label && a.detail === b.detail && a.account?.email === b.account?.email && a.account?.organization === b.account?.organization && a.account?.plan === b.account?.plan;
13696
13760
  }
13697
13761
 
13698
13762
  // ../daemon/src/agent/registry.ts
13699
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
13763
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
13700
13764
  import { homedir as homedir3 } from "os";
13701
- import { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "path";
13765
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "path";
13702
13766
 
13703
13767
  // ../daemon/src/core/cloud.ts
13704
13768
  var enabled = process.env.TOYON_CLOUD === "1";
@@ -14180,9 +14244,9 @@ function opencodeEnv() {
14180
14244
  }
14181
14245
 
14182
14246
  // ../daemon/src/agent/sandbox.ts
14183
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync3 } from "fs";
14247
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync4 } from "fs";
14184
14248
  import { homedir as homedir2, tmpdir } from "os";
14185
- import { join as join5, resolve as resolve2 } from "path";
14249
+ import { join as join6, resolve as resolve2 } from "path";
14186
14250
 
14187
14251
  // ../daemon/src/core/paths.ts
14188
14252
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -14307,6 +14371,76 @@ function within2(path, root) {
14307
14371
  return path === root || path.startsWith(root + sep);
14308
14372
  }
14309
14373
 
14374
+ // ../daemon/src/agent/planDoc.ts
14375
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
14376
+ import { basename as basename2, isAbsolute, join as join5, relative, sep as sep2 } from "path";
14377
+ var PLANS_DIR = join5(".toyon", "plans");
14378
+ var PLAN_NAME = /^[A-Za-z0-9][\w.-]*\.md$/;
14379
+ function isPlanPath(rel) {
14380
+ return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
14381
+ }
14382
+ function ownPlanPath(cwd, file) {
14383
+ const dir = canonical(join5(cwd, PLANS_DIR));
14384
+ const abs = canonical(isAbsolute(file) ? file : join5(cwd, file));
14385
+ if (!within2(abs, dir) || relative(dir, abs).includes(sep2))
14386
+ return null;
14387
+ return PLAN_NAME.test(basename2(abs)) ? join5(PLANS_DIR, basename2(abs)) : null;
14388
+ }
14389
+ async function excludePlans(cwd) {
14390
+ await excludeFromGit(cwd, `${PLANS_DIR}/`);
14391
+ }
14392
+ function lastNumber(dir) {
14393
+ if (!existsSync7(dir))
14394
+ return 0;
14395
+ let max = 0;
14396
+ for (const name of readdirSync3(dir)) {
14397
+ const n = Number(/^(\d+)\.md$/.exec(name)?.[1]);
14398
+ if (n > max)
14399
+ max = n;
14400
+ }
14401
+ return max;
14402
+ }
14403
+ function sameAsLast(cwd, rel, markdown) {
14404
+ try {
14405
+ return readFileSync6(join5(cwd, rel), "utf8").trim() === markdown.trim();
14406
+ } catch (e) {
14407
+ log.warn(cwd, `could not read ${rel}`, e);
14408
+ return false;
14409
+ }
14410
+ }
14411
+ async function writePlanDoc(cwd, markdown) {
14412
+ const dir = join5(cwd, PLANS_DIR);
14413
+ const last = lastNumber(dir);
14414
+ if (last > 0) {
14415
+ const newest = join5(PLANS_DIR, `${last}.md`);
14416
+ if (sameAsLast(cwd, newest, markdown))
14417
+ return newest;
14418
+ }
14419
+ const rel = join5(PLANS_DIR, `${last + 1}.md`);
14420
+ try {
14421
+ mkdirSync4(dir, { recursive: true });
14422
+ writeFileSync3(join5(cwd, rel), markdown.endsWith(`
14423
+ `) ? markdown : `${markdown}
14424
+ `);
14425
+ } catch (e) {
14426
+ log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
14427
+ return null;
14428
+ }
14429
+ await excludePlans(cwd);
14430
+ return rel;
14431
+ }
14432
+ function planEdited(cwd, rel, proposed) {
14433
+ const file = join5(cwd, rel);
14434
+ if (!existsSync7(file))
14435
+ return false;
14436
+ try {
14437
+ return readFileSync6(file, "utf8").trim() !== proposed.trim();
14438
+ } catch (e) {
14439
+ log.warn(cwd, `could not read ${rel}`, e);
14440
+ return false;
14441
+ }
14442
+ }
14443
+
14310
14444
  // ../daemon/src/agent/sandbox.ts
14311
14445
  var CACHE_DIRS = [".bun", ".npm", ".cache", ".yarn", ".pnpm-store", "Library/Caches"].map((d) => resolve2(homedir2(), d));
14312
14446
  var AGENT_SETTINGS = [".claude", ".codex", ".opencode", "opencode.json", "opencode.jsonc"];
@@ -14335,9 +14469,10 @@ async function prepareLaunch(cwd, spec) {
14335
14469
  log.warn(cwd, `agent ${spec.id} runs without an OS sandbox`);
14336
14470
  return { bounds, env: setup?.env ?? {} };
14337
14471
  }
14338
- var SETTINGS_REL = join5(".claude", "settings.local.json");
14472
+ var SETTINGS_REL = join6(".claude", "settings.local.json");
14339
14473
  function claudeLocalSettings(b) {
14340
14474
  return {
14475
+ plansDirectory: PLANS_DIR,
14341
14476
  sandbox: {
14342
14477
  enabled: true,
14343
14478
  failIfUnavailable: true,
@@ -14349,12 +14484,12 @@ function claudeLocalSettings(b) {
14349
14484
  };
14350
14485
  }
14351
14486
  async function writeClaudeLocalSettings(cwd, b) {
14352
- const file = join5(cwd, SETTINGS_REL);
14353
- mkdirSync4(join5(cwd, ".claude"), { recursive: true });
14487
+ const file = join6(cwd, SETTINGS_REL);
14488
+ mkdirSync5(join6(cwd, ".claude"), { recursive: true });
14354
14489
  let existing = {};
14355
- if (existsSync7(file)) {
14490
+ if (existsSync8(file)) {
14356
14491
  try {
14357
- const parsed = JSON.parse(readFileSync6(file, "utf8"));
14492
+ const parsed = JSON.parse(readFileSync7(file, "utf8"));
14358
14493
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
14359
14494
  existing = parsed;
14360
14495
  } catch (e) {
@@ -14367,13 +14502,20 @@ async function writeClaudeLocalSettings(cwd, b) {
14367
14502
  const perms = existing.permissions && typeof existing.permissions === "object" ? existing.permissions : {};
14368
14503
  const theirs = Array.isArray(perms.deny) ? perms.deny.filter((r) => typeof r === "string") : [];
14369
14504
  const deny = uniq2([...theirs, ...ours.permissions.deny]);
14370
- const next = { ...existing, sandbox: ours.sandbox, permissions: { ...perms, ...ours.permissions, deny } };
14505
+ const next = {
14506
+ ...existing,
14507
+ sandbox: ours.sandbox,
14508
+ permissions: { ...perms, ...ours.permissions, deny },
14509
+ plansDirectory: ours.plansDirectory
14510
+ };
14371
14511
  const tmp = `${file}.tmp`;
14372
- writeFileSync3(tmp, `${JSON.stringify(next, null, 2)}
14512
+ writeFileSync4(tmp, `${JSON.stringify(next, null, 2)}
14373
14513
  `);
14374
14514
  renameSync(tmp, file);
14375
- if (existsSync7(join5(cwd, ".git")))
14515
+ if (existsSync8(join6(cwd, ".git"))) {
14376
14516
  await excludeFromGit(cwd, SETTINGS_REL);
14517
+ await excludePlans(cwd);
14518
+ }
14377
14519
  }
14378
14520
 
14379
14521
  // ../daemon/src/agent/registry.ts
@@ -14432,14 +14574,30 @@ var BUILTIN_AGENTS = [
14432
14574
  systemPrompt: "prompt-prefix",
14433
14575
  modes: { plan: "plan", build: "build" },
14434
14576
  quickModel: opencodeQuickModel,
14435
- loginHint: "OpenCode has no provider logged in; run `opencode auth login` in the terminal"
14577
+ loginHint: "OpenCode has no provider logged in; run opencode auth login in the terminal"
14436
14578
  }
14437
14579
  ];
14580
+ function installErrorLine(stderr) {
14581
+ const lines = stderr.split(`
14582
+ `).map((l) => l.trim()).filter(Boolean);
14583
+ const first = lines.find((l) => l.startsWith("error:"))?.slice("error:".length).trim();
14584
+ const line = first || lines.at(-1) || "no output";
14585
+ if (UNTRUSTED_CA.test(line))
14586
+ return `${line}. A certificate bun does not trust, usually a proxy's: set NODE_EXTRA_CA_CERTS to its file and restart Toyon`;
14587
+ return line;
14588
+ }
14589
+ var UNTRUSTED_CA = /UNKNOWN_CERTIFICATE_VERIFICATION_ERROR|SELF_SIGNED_CERT|UNABLE_TO_GET_ISSUER_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/;
14438
14590
  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(" ") };
14591
+ const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir, {
14592
+ NO_COLOR: "1",
14593
+ FORCE_COLOR: "0"
14594
+ });
14595
+ return { ok: r.ok, err: r.ok ? "" : installErrorLine(r.err) };
14442
14596
  };
14597
+ var RETRY_AFTER_MS = 30000;
14598
+ var RETRY_CAP_MS = 60 * 60000;
14599
+ var retryWindow = (retries) => Math.min(RETRY_AFTER_MS * 2 ** retries, RETRY_CAP_MS);
14600
+ var failedReason = (why) => `install failed: ${why}`;
14443
14601
  function jsRuntime() {
14444
14602
  if (process.env.TOYON_ACP_RUNTIME === "node") {
14445
14603
  const node = Bun.which("node");
@@ -14455,15 +14613,18 @@ class AgentRegistry {
14455
14613
  installer;
14456
14614
  host;
14457
14615
  sandbox;
14616
+ now;
14458
14617
  specs = new Map;
14459
14618
  installing = new Map;
14460
14619
  installErrors = new Map;
14620
+ failures = new Map;
14461
14621
  onChange = null;
14462
- constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }) {
14622
+ constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }, now = Date.now) {
14463
14623
  this.dir = dir;
14464
14624
  this.installer = installer;
14465
14625
  this.host = host;
14466
14626
  this.sandbox = sandbox;
14627
+ this.now = now;
14467
14628
  for (const s of specs)
14468
14629
  this.specs.set(s.id, s);
14469
14630
  }
@@ -14500,43 +14661,47 @@ class AgentRegistry {
14500
14661
  const name = this.packageName(spec);
14501
14662
  if (!name || spec.run.kind === "command")
14502
14663
  return null;
14503
- const pkgDir = join6(this.dir, spec.id, "node_modules", name);
14664
+ const pkgDir = join7(this.dir, spec.id, "node_modules", name);
14504
14665
  if (spec.run.kind === "npm-native") {
14505
- const bin = join6(pkgDir, spec.run.bin);
14506
- return existsSync8(bin) ? bin : null;
14666
+ const bin = join7(pkgDir, spec.run.bin);
14667
+ return existsSync9(bin) ? bin : null;
14507
14668
  }
14508
- const pkgJson = join6(pkgDir, "package.json");
14509
- if (!existsSync8(pkgJson))
14669
+ const pkgJson = join7(pkgDir, "package.json");
14670
+ if (!existsSync9(pkgJson))
14510
14671
  return null;
14511
14672
  try {
14512
- const meta = JSON.parse(readFileSync7(pkgJson, "utf8"));
14673
+ const meta = JSON.parse(readFileSync8(pkgJson, "utf8"));
14513
14674
  const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
14514
14675
  if (!rel)
14515
14676
  return null;
14516
- const script = join6(dirname3(pkgJson), rel);
14517
- return existsSync8(script) ? script : null;
14677
+ const script = join7(dirname3(pkgJson), rel);
14678
+ return existsSync9(script) ? script : null;
14518
14679
  } catch (e) {
14519
14680
  log.warn("agents", `cannot read ${pkgJson}`, e);
14520
14681
  return null;
14521
14682
  }
14522
14683
  }
14523
14684
  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`;
14685
+ return this.status(spec).reason;
14686
+ }
14687
+ status(spec) {
14688
+ const boxed = spec.confinement === "none" ? null : this.sandbox.problem();
14689
+ if (spec.run.kind === "command") {
14690
+ const cmd = spec.run.command;
14691
+ const found = isAbsolute2(cmd) ? existsSync9(cmd) : Bun.which(cmd);
14692
+ return { reason: found ? boxed : `${cmd} not found on PATH` };
14693
+ }
14694
+ if (!this.packageName(spec))
14695
+ return { reason: `no build for ${this.host.platform} ${this.host.arch}` };
14696
+ if (this.installed(spec))
14697
+ return { reason: boxed };
14698
+ if (this.installing.has(spec.id))
14699
+ return { reason: "installing" };
14700
+ const failed = this.installErrors.get(spec.id);
14701
+ const missing = failed !== undefined ? failedReason(failed) : spec.onDemand ? "not installed" : "not installed yet";
14702
+ if (boxed)
14703
+ return { reason: failed !== undefined ? `${missing}; ${boxed}` : boxed };
14704
+ return { reason: missing, install: failed !== undefined ? { failed } : "missing" };
14540
14705
  }
14541
14706
  command(spec) {
14542
14707
  const why = this.unavailable(spec);
@@ -14564,23 +14729,29 @@ class AgentRegistry {
14564
14729
  const running = this.installing.get(id);
14565
14730
  if (running)
14566
14731
  return running;
14567
- if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
14732
+ if (this.installed(spec) && this.installedVersion(spec) === spec.run.version) {
14733
+ this.installErrors.delete(id);
14734
+ this.failures.delete(id);
14568
14735
  return Promise.resolve();
14736
+ }
14569
14737
  const { version } = spec.run;
14570
- const dir = join6(this.dir, id);
14738
+ const dir = join7(this.dir, id);
14571
14739
  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)}
14740
+ try {
14741
+ mkdirSync6(dir, { recursive: true });
14742
+ writeFileSync5(join7(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
14574
14743
  `);
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}`);
14744
+ log.info("agents", `installing ${pkg}@${version} for ${id}`);
14745
+ const r = await this.installer(dir, pkg, version);
14746
+ if (r.ok && this.installed(spec)) {
14747
+ this.installErrors.delete(id);
14748
+ this.failures.delete(id);
14749
+ log.info("agents", `${id} ready`);
14750
+ return;
14751
+ }
14752
+ this.fail(id, r.err || "no bin after install");
14753
+ } catch (e) {
14754
+ this.fail(id, e instanceof Error ? e.message : String(e));
14584
14755
  }
14585
14756
  })().finally(() => {
14586
14757
  this.installing.delete(id);
@@ -14590,13 +14761,18 @@ class AgentRegistry {
14590
14761
  this.onChange?.();
14591
14762
  return p;
14592
14763
  }
14764
+ fail(id, why) {
14765
+ this.installErrors.set(id, why);
14766
+ this.failures.set(id, { at: this.now(), retries: this.failures.get(id)?.retries ?? 0 });
14767
+ log.warn("agents", `${id}: ${failedReason(why)}`);
14768
+ }
14593
14769
  installedVersion(spec) {
14594
14770
  const name = this.packageName(spec);
14595
14771
  if (!name)
14596
14772
  return null;
14597
- const pkgJson = join6(this.dir, spec.id, "node_modules", name, "package.json");
14773
+ const pkgJson = join7(this.dir, spec.id, "node_modules", name, "package.json");
14598
14774
  try {
14599
- return JSON.parse(readFileSync7(pkgJson, "utf8")).version ?? null;
14775
+ return JSON.parse(readFileSync8(pkgJson, "utf8")).version ?? null;
14600
14776
  } catch {
14601
14777
  return null;
14602
14778
  }
@@ -14609,22 +14785,37 @@ class AgentRegistry {
14609
14785
  await this.install(id);
14610
14786
  }
14611
14787
  }
14788
+ async retryFailed() {
14789
+ if (this.installing.size > 0)
14790
+ return;
14791
+ for (const [id, entry] of this.failures) {
14792
+ if (this.now() - entry.at < retryWindow(entry.retries))
14793
+ continue;
14794
+ entry.retries += 1;
14795
+ await this.install(id);
14796
+ }
14797
+ }
14612
14798
  adapters() {
14613
14799
  const out = [];
14614
14800
  for (const spec of this.list()) {
14615
14801
  if (spec.run.kind === "command" || !this.packageName(spec))
14616
14802
  continue;
14617
14803
  const installed = this.installed(spec) ? this.installedVersion(spec) : null;
14618
- const error = this.installErrors.get(spec.id);
14619
- if (spec.onDemand && installed === null && !error)
14804
+ const failed = this.installErrors.get(spec.id);
14805
+ if (spec.onDemand && installed === null && failed === undefined)
14620
14806
  continue;
14621
- out.push({ id: spec.id, installed, pinned: spec.run.version, ...error ? { error } : {} });
14807
+ out.push({
14808
+ id: spec.id,
14809
+ installed,
14810
+ pinned: spec.run.version,
14811
+ ...failed !== undefined ? { error: failedReason(failed) } : {}
14812
+ });
14622
14813
  }
14623
14814
  return out;
14624
14815
  }
14625
14816
  infos() {
14626
14817
  return this.list().map((spec) => {
14627
- const reason = this.unavailable(spec);
14818
+ const { reason, install } = this.status(spec);
14628
14819
  return {
14629
14820
  id: spec.id,
14630
14821
  name: spec.name,
@@ -14632,8 +14823,10 @@ class AgentRegistry {
14632
14823
  available: !reason,
14633
14824
  ...reason ? { reason } : {},
14634
14825
  ...this.installing.has(spec.id) ? { installing: true } : {},
14826
+ ...install !== undefined ? { install } : {},
14635
14827
  ...spec.onDemand ? { onDemand: true } : {},
14636
- sandboxed: spec.confinement !== "none"
14828
+ sandboxed: spec.confinement !== "none",
14829
+ loginHint: spec.loginHint
14637
14830
  };
14638
14831
  });
14639
14832
  }
@@ -14701,7 +14894,7 @@ function managedSpecs(specs, managed) {
14701
14894
  return managed.agents === null ? kept : kept.filter((s) => managed.agents?.includes(s.id));
14702
14895
  }
14703
14896
  function loadAgentRegistry(agentsFile, agentsDir, sandbox, managed = MANAGED_DEFAULTS) {
14704
- const specs = managed.customAgents && existsSync8(agentsFile) ? parseAgentsFile(readFileSync7(agentsFile, "utf8"), BUILTIN_AGENTS) : BUILTIN_AGENTS;
14897
+ const specs = managed.customAgents && existsSync9(agentsFile) ? parseAgentsFile(readFileSync8(agentsFile, "utf8"), BUILTIN_AGENTS) : BUILTIN_AGENTS;
14705
14898
  return new AgentRegistry(managedSpecs(specs, managed), agentsDir, undefined, undefined, sandbox);
14706
14899
  }
14707
14900
 
@@ -14720,21 +14913,33 @@ function agentModeFor(spec, mode, available) {
14720
14913
  function modeAfterPlan(optionName) {
14721
14914
  return /manual/i.test(optionName) ? "ask" : "auto";
14722
14915
  }
14916
+ function orderPlanOptions(options) {
14917
+ const rank = (id) => {
14918
+ if (id === "exit-plan-default")
14919
+ return 1;
14920
+ if (id.startsWith("exit-plan-clear-"))
14921
+ return 2;
14922
+ if (id.startsWith("exit-plan-"))
14923
+ return 0;
14924
+ return 3;
14925
+ };
14926
+ return [...options].sort((a, b) => rank(a.optionId) - rank(b.optionId));
14927
+ }
14723
14928
 
14724
14929
  // ../daemon/src/agent/origins.ts
14725
- import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
14930
+ import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
14726
14931
  import { homedir as homedir4 } from "os";
14727
- import { basename as basename2, join as join7 } from "path";
14932
+ import { basename as basename3, join as join8 } from "path";
14728
14933
  function commandFolders(spec, cwd, home = homedir4()) {
14729
14934
  if (spec.id === "claude") {
14730
14935
  return [
14731
- { origin: "project", skills: join7(cwd, ".claude", "skills"), commands: join7(cwd, ".claude", "commands") },
14732
- { origin: "user", skills: join7(home, ".claude", "skills"), commands: join7(home, ".claude", "commands") },
14733
- ...pluginPaths(spec.meta).map((p) => ({ origin: "plugin", skills: join7(p, "skills"), commands: join7(p, "commands") }))
14936
+ { origin: "project", skills: join8(cwd, ".claude", "skills"), commands: join8(cwd, ".claude", "commands") },
14937
+ { origin: "user", skills: join8(home, ".claude", "skills"), commands: join8(home, ".claude", "commands") },
14938
+ ...pluginPaths(spec.meta).map((p) => ({ origin: "plugin", skills: join8(p, "skills"), commands: join8(p, "commands") }))
14734
14939
  ];
14735
14940
  }
14736
14941
  if (spec.id === "codex")
14737
- return [{ origin: "user", commands: join7(home, ".codex", "prompts") }];
14942
+ return [{ origin: "user", commands: join8(home, ".codex", "prompts") }];
14738
14943
  return [];
14739
14944
  }
14740
14945
  function readCommandOrigins(folders) {
@@ -14774,23 +14979,23 @@ function pluginPaths(meta) {
14774
14979
  }
14775
14980
  function skillNames(dir) {
14776
14981
  return entries2(dir).flatMap((e) => {
14777
- const file = join7(dir, e, "SKILL.md");
14778
- if (!existsSync9(file))
14982
+ const file = join8(dir, e, "SKILL.md");
14983
+ if (!existsSync10(file))
14779
14984
  return [];
14780
14985
  return [frontMatterName(file) ?? e];
14781
14986
  });
14782
14987
  }
14783
14988
  function commandNames(dir) {
14784
14989
  return entries2(dir).flatMap((e) => {
14785
- const path = join7(dir, e);
14990
+ const path = join8(dir, e);
14786
14991
  if (e.endsWith(".md"))
14787
- return [basename2(e, ".md")];
14992
+ return [basename3(e, ".md")];
14788
14993
  return isDir(path) ? commandNames(path) : [];
14789
14994
  });
14790
14995
  }
14791
14996
  function frontMatterName(file) {
14792
14997
  try {
14793
- const text = readFileSync8(file, "utf8");
14998
+ const text = readFileSync9(file, "utf8");
14794
14999
  if (!text.startsWith("---"))
14795
15000
  return null;
14796
15001
  const end = text.indexOf(`
@@ -14804,10 +15009,10 @@ function frontMatterName(file) {
14804
15009
  }
14805
15010
  }
14806
15011
  function entries2(dir) {
14807
- if (!existsSync9(dir))
15012
+ if (!existsSync10(dir))
14808
15013
  return [];
14809
15014
  try {
14810
- return readdirSync3(dir).filter((e) => !e.startsWith("."));
15015
+ return readdirSync4(dir).filter((e) => !e.startsWith("."));
14811
15016
  } catch (e) {
14812
15017
  log.warn("origins", `${dir} could not be listed`, e);
14813
15018
  return [];
@@ -14851,13 +15056,13 @@ function unformatOutput(output) {
14851
15056
  }
14852
15057
 
14853
15058
  // ../daemon/src/agent/policy.ts
14854
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
15059
+ import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
14855
15060
 
14856
15061
  // ../daemon/src/agent/acp/background.ts
14857
- import { existsSync as existsSync10, statSync as statSync4 } from "fs";
15062
+ import { existsSync as existsSync11, statSync as statSync4 } from "fs";
14858
15063
  import { open as open2 } from "fs/promises";
14859
15064
  import { homedir as homedir5 } from "os";
14860
- import { basename as basename3, dirname as dirname4, join as join8 } from "path";
15065
+ import { basename as basename4, dirname as dirname4, join as join9 } from "path";
14861
15066
  var STARTED = /Command running in background with ID: (\S+)\. Output is being written to: (\S+)\./;
14862
15067
  function backgroundStart(input, output) {
14863
15068
  const flag = input && typeof input === "object" ? input.run_in_background : undefined;
@@ -14870,15 +15075,15 @@ function spawnDetached(output) {
14870
15075
  return output.includes("Async agent launched successfully");
14871
15076
  }
14872
15077
  function claudeConfigDir() {
14873
- return process.env.CLAUDE_CONFIG_DIR || join8(homedir5(), ".claude");
15078
+ return process.env.CLAUDE_CONFIG_DIR || join9(homedir5(), ".claude");
14874
15079
  }
14875
15080
  function sessionLogFor(file, configDir = claudeConfigDir()) {
14876
15081
  const tasks = dirname4(file);
14877
- if (basename3(tasks) !== "tasks")
15082
+ if (basename4(tasks) !== "tasks")
14878
15083
  return null;
14879
15084
  const session = dirname4(tasks);
14880
- const path = join8(configDir, "projects", basename3(dirname4(session)), `${basename3(session)}.jsonl`);
14881
- return existsSync10(path) ? path : null;
15085
+ const path = join9(configDir, "projects", basename4(dirname4(session)), `${basename4(session)}.jsonl`);
15086
+ return existsSync11(path) ? path : null;
14882
15087
  }
14883
15088
  function taskEnd(text, toolId) {
14884
15089
  const at = text.indexOf(`<tool-use-id>${toolId}</tool-use-id>`);
@@ -15091,14 +15296,16 @@ function isNetworkAsk(update) {
15091
15296
  function isToolSearch(update) {
15092
15297
  return update.name === TOOL_SEARCH || asRecord2(asRecord2(update._meta).claudeCode).toolName === TOOL_SEARCH;
15093
15298
  }
15094
- function toolSearchQuery(rawInput) {
15299
+ var TOOL_SEARCH_OPENING = "loading a tool";
15300
+ function toolSearchTitle(rawInput) {
15095
15301
  const query = asRecord2(rawInput).query;
15096
15302
  if (typeof query !== "string")
15097
15303
  return "";
15098
15304
  const names = /^select:(.*)$/.exec(query.trim());
15099
15305
  if (!names)
15100
- return query.trim();
15101
- return (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15306
+ return `looked for a tool: ${query.trim()}`;
15307
+ const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15308
+ return loaded ? `loaded ${loaded}` : "";
15102
15309
  }
15103
15310
  var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
15104
15311
  function toyonTool(update) {
@@ -15122,7 +15329,7 @@ function heading(update) {
15122
15329
  if (isNetworkAsk(update) && typeof host === "string")
15123
15330
  return { name: "", title: host, kind: "fetch" };
15124
15331
  if (isToolSearch(update)) {
15125
- return { name: TOOL_SEARCH, title: toolSearchQuery(update.rawInput) || TOOL_SEARCH, kind: "search" };
15332
+ return { name: TOOL_SEARCH, title: toolSearchTitle(update.rawInput) || TOOL_SEARCH_OPENING, kind: "other" };
15126
15333
  }
15127
15334
  const own = toyonTool(update);
15128
15335
  if (own)
@@ -15254,7 +15461,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15254
15461
  });
15255
15462
  }
15256
15463
  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;
15464
+ 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
15465
  if (title && title !== memo.title)
15259
15466
  refined.title = memo.title = title;
15260
15467
  if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
@@ -15455,7 +15662,7 @@ function decide(req, bounds, cwd, mode = "auto", sandboxed = true) {
15455
15662
  return { kind: "allow" };
15456
15663
  }
15457
15664
  for (const raw of paths) {
15458
- const target = canonical(isAbsolute2(raw) ? raw : resolve4(cwd, raw));
15665
+ const target = canonical(isAbsolute3(raw) ? raw : resolve4(cwd, raw));
15459
15666
  if (bounds.denyRead.some((d) => within2(target, d))) {
15460
15667
  return {
15461
15668
  kind: "reject",
@@ -15521,7 +15728,6 @@ var SYSTEM_APPEND = [
15521
15728
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
15522
15729
  '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.',
15523
15730
  "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.",
15524
- "To see whether a page renders without throwing, call the `preview` tool with its path when you have one: a browser the user has open loads it beside what they see and reports what it threw.",
15525
15731
  "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.",
15526
15732
  "When you scaffold a project or change how it installs or starts, finish by updating the settings file Toyon already reads, or writing .toyon/settings.json when there is none, so the preview can run it."
15527
15733
  ].join(" ");
@@ -15534,7 +15740,6 @@ function failedRun(f) {
15534
15740
  case "conflict":
15535
15741
  return f.step;
15536
15742
  case "preview":
15537
- case "page":
15538
15743
  return;
15539
15744
  }
15540
15745
  }
@@ -15550,8 +15755,6 @@ function fixPrompt(f) {
15550
15755
  return commandFixPrompt(f.command);
15551
15756
  case "preview":
15552
15757
  return procFixPrompt(f.procs, f.log);
15553
- case "page":
15554
- return pageFixPrompt(f.errors);
15555
15758
  }
15556
15759
  }
15557
15760
  var WHY_COMMAND_CHARS = 60;
@@ -15570,20 +15773,8 @@ function fixWhy(f) {
15570
15773
  }
15571
15774
  case "preview":
15572
15775
  return "the dev server is not reachable";
15573
- case "page":
15574
- return "the preview threw";
15575
15776
  }
15576
15777
  }
15577
- function pageFixPrompt(errors) {
15578
- return [
15579
- "Since your last edits the preview threw this in the user's browser:",
15580
- "",
15581
- ...errors.map((e) => `- ${e}`),
15582
- "",
15583
- "Find the cause and fix it, inside this worktree. If your change did not cause it, say what did instead of changing anything."
15584
- ].join(`
15585
- `);
15586
- }
15587
15778
  function failureContext(f) {
15588
15779
  const run = failedRun(f);
15589
15780
  if (!run)
@@ -15789,11 +15980,11 @@ ${a.text}
15789
15980
  }
15790
15981
 
15791
15982
  // ../daemon/src/agent/transcript.ts
15792
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
15983
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
15793
15984
  import { appendFile } from "fs/promises";
15794
- import { join as join9 } from "path";
15985
+ import { join as join10 } from "path";
15795
15986
  function transcriptPathFor(transcriptsDir, worktreeId) {
15796
- return join9(transcriptsDir, `${worktreeId}.jsonl`);
15987
+ return join10(transcriptsDir, `${worktreeId}.jsonl`);
15797
15988
  }
15798
15989
  function coalesce(entries) {
15799
15990
  const out = [];
@@ -15847,7 +16038,7 @@ class Transcript {
15847
16038
  this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
15848
16039
  }
15849
16040
  read() {
15850
- return existsSync11(this.path) ? parseTranscript(readFileSync9(this.path, "utf8"), this.tag) : [];
16041
+ return existsSync12(this.path) ? parseTranscript(readFileSync10(this.path, "utf8"), this.tag) : [];
15851
16042
  }
15852
16043
  append(event) {
15853
16044
  const entry = { seq: this.seq++, event };
@@ -16424,6 +16615,7 @@ class AcpSession {
16424
16615
  this.queue = [];
16425
16616
  this.steered = [];
16426
16617
  this.queueChanged();
16618
+ this.cancelAsks("expired");
16427
16619
  this.stop();
16428
16620
  await this.dropConn("closing");
16429
16621
  await this.log.flush();
@@ -16619,12 +16811,7 @@ class AcpSession {
16619
16811
  this.emit({ type: "turn-end", stopReason: mapStopReason(res.stopReason), ts: Date.now() });
16620
16812
  }
16621
16813
  contextFor(item) {
16622
- return ambientBlock([
16623
- ...item.context ?? [],
16624
- this.d.preview?.(),
16625
- item.asked === "page" ? undefined : this.d.pageErrors?.(),
16626
- ...this.landedSince()
16627
- ]);
16814
+ return ambientBlock([...item.context ?? [], this.d.preview?.(), this.d.pageErrors?.(), ...this.landedSince()]);
16628
16815
  }
16629
16816
  landedSince() {
16630
16817
  const entries = this.log.entries;
@@ -16864,6 +17051,9 @@ class AcpSession {
16864
17051
  }
16865
17052
  if (params.update.sessionUpdate === "current_mode_update") {
16866
17053
  live.modeId = params.update.currentModeId;
17054
+ const planId = agentModeFor(live.conn.spec, "plan", live.modeIds ?? undefined);
17055
+ if (planId && live.modeId === planId && this.mode() !== "plan")
17056
+ this.d.setMode?.("plan");
16867
17057
  return;
16868
17058
  }
16869
17059
  if (params.update.sessionUpdate === "config_option_update") {
@@ -16935,8 +17125,9 @@ class AcpSession {
16935
17125
  const plan = params.toolCall.kind === "switch_mode";
16936
17126
  const parts = permissionParts(params);
16937
17127
  const detail = parts.detail ?? "";
16938
- const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
16939
- const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
17128
+ const ownFile = plan ? stringField(params.toolCall.rawInput, "planFilePath") : undefined;
17129
+ const planPath = plan ? await this.d.onPlan?.(detail, ownFile) ?? null : null;
17130
+ const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
16940
17131
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
16941
17132
  return this.openAsk((id) => ({
16942
17133
  type: "agent-permission",
@@ -17022,9 +17213,9 @@ class AcpSession {
17022
17213
  const text = headers.length === 1 ? `with my answer to "${headers[0]}"` : "with my answers";
17023
17214
  this.post(text, { attachments: followUp.attachments });
17024
17215
  }
17025
- cancelAsks() {
17216
+ cancelAsks(outcome = "cancelled") {
17026
17217
  for (const ask of [...this.asks.values()])
17027
- ask.settle("cancelled");
17218
+ ask.settle(outcome);
17028
17219
  }
17029
17220
  syncStatus() {
17030
17221
  if (this.status === "error")
@@ -17106,9 +17297,6 @@ class AcpSession {
17106
17297
  this.noteProcess(conn.link, false);
17107
17298
  }
17108
17299
  }
17109
- function isAuthRequired(e) {
17110
- return e instanceof RequestError && e.code === -32000;
17111
- }
17112
17300
  function takesHttpMcp(init) {
17113
17301
  const http = init.agentCapabilities?.mcpCapabilities?.http;
17114
17302
  return http === true || typeof http === "object" && http !== null;
@@ -17185,66 +17373,6 @@ function sameCommands(a, b) {
17185
17373
  return a.length === b.length && a.every((c, i) => c.name === b[i]?.name && c.description === b[i]?.description && c.hint === b[i]?.hint && c.origin === b[i]?.origin);
17186
17374
  }
17187
17375
 
17188
- // ../daemon/src/agent/planDoc.ts
17189
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
17190
- import { join as join10 } from "path";
17191
- var PLANS_DIR = join10(".toyon", "plans");
17192
- var PLAN_NAME = /^(\d+)\.md$/;
17193
- function isPlanPath(rel) {
17194
- return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
17195
- }
17196
- function lastNumber(dir) {
17197
- if (!existsSync12(dir))
17198
- return 0;
17199
- let max = 0;
17200
- for (const name of readdirSync4(dir)) {
17201
- const n = Number(PLAN_NAME.exec(name)?.[1]);
17202
- if (n > max)
17203
- max = n;
17204
- }
17205
- return max;
17206
- }
17207
- function sameAsLast(cwd, rel, markdown) {
17208
- try {
17209
- return readFileSync10(join10(cwd, rel), "utf8").trim() === markdown.trim();
17210
- } catch (e) {
17211
- log.warn(cwd, `could not read ${rel}`, e);
17212
- return false;
17213
- }
17214
- }
17215
- async function writePlanDoc(cwd, markdown) {
17216
- const dir = join10(cwd, PLANS_DIR);
17217
- const last = lastNumber(dir);
17218
- if (last > 0) {
17219
- const newest = join10(PLANS_DIR, `${last}.md`);
17220
- if (sameAsLast(cwd, newest, markdown))
17221
- return newest;
17222
- }
17223
- const rel = join10(PLANS_DIR, `${last + 1}.md`);
17224
- try {
17225
- mkdirSync6(dir, { recursive: true });
17226
- writeFileSync5(join10(cwd, rel), markdown.endsWith(`
17227
- `) ? markdown : `${markdown}
17228
- `);
17229
- } catch (e) {
17230
- log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
17231
- return null;
17232
- }
17233
- await excludeFromGit(cwd, `${PLANS_DIR}/`);
17234
- return rel;
17235
- }
17236
- function planEdited(cwd, rel, proposed) {
17237
- const file = join10(cwd, rel);
17238
- if (!existsSync12(file))
17239
- return false;
17240
- try {
17241
- return readFileSync10(file, "utf8").trim() !== proposed.trim();
17242
- } catch (e) {
17243
- log.warn(cwd, `could not read ${rel}`, e);
17244
- return false;
17245
- }
17246
- }
17247
-
17248
17376
  // ../daemon/src/runtime/forward.ts
17249
17377
  var POLL_MS2 = 100;
17250
17378
  var HOLD_MS = 60000;
@@ -18458,8 +18586,8 @@ function defaultAgent(wt, d, preview, onProcess) {
18458
18586
  onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
18459
18587
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
18460
18588
  beforeTurn: async () => d.turnStarting?.(wt.id),
18461
- onAuth: (agentId, o) => d.accounts?.observe(agentId, o),
18462
- onLimits: (agentId, limits) => d.accounts?.observeLimits(agentId, limits),
18589
+ onAuth: (agentId, o) => d.readiness?.observe(agentId, o),
18590
+ onLimits: (agentId, limits) => d.readiness?.observeLimits(agentId, limits),
18463
18591
  seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
18464
18592
  onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
18465
18593
  commandOrigins: () => readCommandOrigins(commandFolders(spec(), wt.path)),
@@ -18468,13 +18596,14 @@ function defaultAgent(wt, d, preview, onProcess) {
18468
18596
  if (d.state.setMode(wt.id, mode))
18469
18597
  d.hub.emit("worktreesChanged");
18470
18598
  },
18471
- onPlan: async (markdown) => {
18599
+ onPlan: async (markdown, ownFile) => {
18472
18600
  const w = d.state.worktree(wt.id);
18473
18601
  if (w && !w.planned) {
18474
18602
  w.planned = true;
18475
18603
  d.state.save();
18476
18604
  }
18477
- const path = markdown.trim() ? await writePlanDoc(wt.path, markdown) : null;
18605
+ const own = ownFile ? ownPlanPath(wt.path, ownFile) : null;
18606
+ const path = own ?? (markdown.trim() ? await writePlanDoc(wt.path, markdown) : null);
18478
18607
  const fresh = path ? d.state.worktree(wt.id) : undefined;
18479
18608
  if (fresh && path && fresh.plan !== path) {
18480
18609
  fresh.plan = path;
@@ -19504,7 +19633,7 @@ var LAND_ASK = [
19504
19633
  "Line 2: blank",
19505
19634
  "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
19635
  "Line 4: blank",
19507
- "Line 5: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
19636
+ "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
19637
  "Line 6: blank",
19509
19638
  "Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
19510
19639
  ].join(`
@@ -19512,17 +19641,25 @@ var LAND_ASK = [
19512
19641
  var LAND_TURNS = 4;
19513
19642
  var LAND_CHARS = 5000;
19514
19643
  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)}`);
19644
+ const own = i.diffStat.trim();
19645
+ const committed = i.committedStat?.trim() ?? "";
19646
+ const head = [LAND_ASK, ""];
19647
+ if (own && i.earlier) {
19648
+ head.push(`Task, as the worktree is titled; background, since the branch committed or landed work before: ${clip(i.title, 200)}`);
19649
+ if (i.firstAsk)
19650
+ head.push(`First request here, which the earlier work may have answered: ${clip(i.firstAsk, 300)}`);
19651
+ } else {
19652
+ head.push(`Task: ${clip(i.title, 200)}`);
19653
+ if (i.firstAsk)
19654
+ head.push(`First request: ${clip(i.firstAsk, 300)}`);
19655
+ }
19518
19656
  if (i.recentSubjects.length)
19519
19657
  head.push(`Recent commit subjects:
19520
19658
  ${i.recentSubjects.map((s) => ` ${s}`).join(`
19521
19659
  `)}`);
19522
19660
  if (i.note?.trim())
19523
19661
  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)"}`;
19662
+ const diff = diffBlock(own, committed);
19526
19663
  let blocks = turnBlocks(i.turns);
19527
19664
  const size = () => [...head, "", ...blocks, "", diff].join(`
19528
19665
 
@@ -19534,6 +19671,22 @@ ${i.diffStat.trim() || "(none)"}`;
19534
19671
 
19535
19672
  `), diff].join(`
19536
19673
 
19674
+ `);
19675
+ }
19676
+ function diffBlock(own, committed) {
19677
+ if (!own && !committed)
19678
+ return `Diff summary:
19679
+ (none)`;
19680
+ const parts = [];
19681
+ if (own)
19682
+ parts.push(`What this commit will contain, which is all the message describes:
19683
+ ${own}`);
19684
+ if (committed)
19685
+ parts.push(own ? `Already committed on this branch, context only, not what the message describes:
19686
+ ${committed}` : `Nothing is uncommitted; the message is for the branch's commits as one:
19687
+ ${committed}`);
19688
+ return parts.join(`
19689
+
19537
19690
  `);
19538
19691
  }
19539
19692
  function turnBlocks(turns) {
@@ -19743,10 +19896,12 @@ function makePlanner(agents) {
19743
19896
  }
19744
19897
 
19745
19898
  // ../daemon/src/agent/transcriptForward.ts
19746
- import { readdirSync as readdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
19899
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
19747
19900
  import { join as join12 } from "path";
19901
+ var FORWARD_STEP = "1";
19902
+ var MARK = '"page';
19748
19903
  function forwardLine(line) {
19749
- if (!line.includes('"page-error"'))
19904
+ if (!line.includes(MARK))
19750
19905
  return null;
19751
19906
  let entry;
19752
19907
  try {
@@ -19755,31 +19910,35 @@ function forwardLine(line) {
19755
19910
  return null;
19756
19911
  }
19757
19912
  const e = entry.event;
19758
- if (e?.type !== "page-error" || Array.isArray(e.errors) || typeof e.message !== "string")
19759
- return null;
19760
- const { message, ...rest } = e;
19761
- return JSON.stringify({ ...entry, event: { ...rest, errors: [message] } });
19913
+ if (e?.type === "page-error")
19914
+ return { drop: true };
19915
+ if (e?.type === "fix-asked" && e.kind === "page")
19916
+ return { line: JSON.stringify({ ...entry, event: { ...e, kind: "command" } }) };
19917
+ return null;
19762
19918
  }
19763
19919
  function forwardText(text) {
19764
19920
  let changed = 0;
19765
- const lines = text.split(`
19766
- `).map((line) => {
19921
+ const lines = [];
19922
+ for (const line of text.split(`
19923
+ `)) {
19767
19924
  const next = forwardLine(line);
19768
19925
  if (next === null)
19769
- return line;
19770
- changed++;
19771
- return next;
19772
- });
19926
+ lines.push(line);
19927
+ else {
19928
+ changed++;
19929
+ if ("line" in next)
19930
+ lines.push(next.line);
19931
+ }
19932
+ }
19773
19933
  return { text: changed ? lines.join(`
19774
19934
  `) : text, changed };
19775
19935
  }
19776
- function forwardTranscripts(dir) {
19936
+ function forwardTranscripts(dirs, stamp) {
19937
+ if (readStamp(stamp) === FORWARD_STEP)
19938
+ return;
19777
19939
  let files = 0;
19778
19940
  let rows = 0;
19779
- for (const name of readdirSync5(dir)) {
19780
- if (!name.endsWith(".jsonl"))
19781
- continue;
19782
- const path = join12(dir, name);
19941
+ for (const path of dirs.flatMap(transcriptsUnder)) {
19783
19942
  const { text, changed } = forwardText(readFileSync11(path, "utf8"));
19784
19943
  if (!changed)
19785
19944
  continue;
@@ -19789,22 +19948,44 @@ function forwardTranscripts(dir) {
19789
19948
  files++;
19790
19949
  rows += changed;
19791
19950
  }
19951
+ writeFileSync6(stamp, FORWARD_STEP);
19792
19952
  if (files)
19793
- log.info("transcripts", `rewrote ${rows} page-error row(s) in ${files} transcript(s) to the list shape`);
19953
+ log.info("transcripts", `moved ${rows} row(s) in ${files} transcript(s) past the preview-threw row`);
19954
+ }
19955
+ function readStamp(stamp) {
19956
+ try {
19957
+ return readFileSync11(stamp, "utf8").trim();
19958
+ } catch {
19959
+ return null;
19960
+ }
19961
+ }
19962
+ function transcriptsUnder(dir) {
19963
+ const found = [];
19964
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
19965
+ if (entry.isFile() && entry.name.endsWith(".jsonl"))
19966
+ found.push(join12(dir, entry.name));
19967
+ else if (entry.isDirectory()) {
19968
+ const inner = join12(dir, entry.name, "transcript.jsonl");
19969
+ if (existsSync14(inner))
19970
+ found.push(inner);
19971
+ }
19972
+ }
19973
+ return found;
19794
19974
  }
19795
19975
 
19796
19976
  // ../daemon/src/core/assets.ts
19797
- import { existsSync as existsSync14, readdirSync as readdirSync6, rmSync as rmSync3, statSync as statSync5 } from "fs";
19977
+ import { existsSync as existsSync15, readdirSync as readdirSync6, rmSync as rmSync3, statSync as statSync5 } from "fs";
19798
19978
  import { join as join13, resolve as resolve5 } from "path";
19799
19979
  function locateAssets(here, env = process.env) {
19800
- const packaged = existsSync14(join13(here, "shell", "index.html"));
19980
+ const packaged = existsSync15(join13(here, "shell", "index.html"));
19801
19981
  return {
19802
19982
  shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join13(here, "shell") : join13(here, "../../shell/dist")),
19803
19983
  bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join13(here, "bridge.js") : join13(here, "../../bridge/dist/bridge.js")),
19804
19984
  helperStub: packaged ? join13(here, "helper-stub") : join13(here, "../dist/helper-stub"),
19805
19985
  cliEntry: packaged ? join13(here, "cli.js") : join13(here, "../../cli/src/cli.ts"),
19806
19986
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
19807
- packageJson: packaged ? join13(here, "..", "package.json") : null
19987
+ packageJson: packaged ? join13(here, "..", "package.json") : null,
19988
+ changelog: packaged ? join13(here, "CHANGELOG.md") : resolve5(here, "../../../CHANGELOG.md")
19808
19989
  };
19809
19990
  }
19810
19991
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
@@ -20263,7 +20444,7 @@ class KeepAwake {
20263
20444
  }
20264
20445
 
20265
20446
  // ../daemon/src/core/dialog.ts
20266
- import { existsSync as existsSync15 } from "fs";
20447
+ import { existsSync as existsSync16 } from "fs";
20267
20448
  import { homedir as homedir6 } from "os";
20268
20449
  var PROMPTS = {
20269
20450
  location: "Choose where the new project goes",
@@ -20281,7 +20462,7 @@ function folderDialog() {
20281
20462
  let cancelled = false;
20282
20463
  const ask = async (start, purpose) => {
20283
20464
  const typed = start === "~" || start.startsWith("~/") ? `${homedir6()}${start.slice(1)}` : start;
20284
- const from = typed.startsWith("/") && existsSync15(typed) ? typed : homedir6();
20465
+ const from = typed.startsWith("/") && existsSync16(typed) ? typed : homedir6();
20285
20466
  cancelled = false;
20286
20467
  const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
20287
20468
  stdout: "pipe",
@@ -20358,9 +20539,9 @@ class GrantCodes {
20358
20539
 
20359
20540
  // ../daemon/src/core/helper.ts
20360
20541
  import { createHash as createHash3 } from "crypto";
20361
- import { existsSync as existsSync16 } from "fs";
20542
+ import { existsSync as existsSync17 } from "fs";
20362
20543
  import { chmod, copyFile as copyFile3, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
20363
- import { basename as basename4, dirname as dirname5, join as join14 } from "path";
20544
+ import { basename as basename5, dirname as dirname5, join as join14 } from "path";
20364
20545
  var EXECUTABLE = "Toyon";
20365
20546
  function helperWanted(where) {
20366
20547
  return where.platform === "darwin" && !where.cloud && where.port === DAEMON_DEFAULT_PORT && where.home === undefined;
@@ -20439,7 +20620,7 @@ class Helper {
20439
20620
  }
20440
20621
  async write() {
20441
20622
  const { spec } = this;
20442
- if (!existsSync16(spec.stub))
20623
+ if (!existsSync17(spec.stub))
20443
20624
  return "skipped";
20444
20625
  const stubHash = createHash3("sha256").update(await readFile2(spec.stub)).digest("hex").slice(0, 16);
20445
20626
  const { plist, script } = helperFiles(spec, stubHash);
@@ -20447,7 +20628,7 @@ class Helper {
20447
20628
  const resources = join14(contents, "Resources");
20448
20629
  const stub = join14(contents, "MacOS", EXECUTABLE);
20449
20630
  const scriptFile = join14(resources, "launch.sh");
20450
- if (existsSync16(stub) && existsSync16(scriptFile) && await readFile2(scriptFile, "utf8") === script)
20631
+ if (existsSync17(stub) && existsSync17(scriptFile) && await readFile2(scriptFile, "utf8") === script)
20451
20632
  return "kept";
20452
20633
  await mkdir2(join14(contents, "MacOS"), { recursive: true });
20453
20634
  await mkdir2(resources, { recursive: true });
@@ -20455,7 +20636,7 @@ class Helper {
20455
20636
  await writeFile2(scriptFile, script, { mode: 493 });
20456
20637
  await copyFile3(spec.stub, stub);
20457
20638
  await chmod(stub, 493);
20458
- if (!existsSync16(join14(resources, "AppIcon.icns")))
20639
+ if (!existsSync17(join14(resources, "AppIcon.icns")))
20459
20640
  await this.drawIcon(resources);
20460
20641
  await this.run(["xattr", "-cr", spec.dir]);
20461
20642
  if (!await this.run(["codesign", "--force", "--deep", "-s", "-", spec.dir])) {
@@ -20470,8 +20651,8 @@ class Helper {
20470
20651
  const set = join14(tmp, "AppIcon.iconset");
20471
20652
  await mkdir2(set, { recursive: true });
20472
20653
  await this.run(["qlmanage", "-t", "-s", "1024", "-o", tmp, svg]);
20473
- const big = join14(tmp, `${basename4(svg)}.png`);
20474
- if (existsSync16(big)) {
20654
+ const big = join14(tmp, `${basename5(svg)}.png`);
20655
+ if (existsSync17(big)) {
20475
20656
  for (const size of [16, 32, 64, 128, 256, 512, 1024]) {
20476
20657
  await this.run(["sips", "-z", String(size), String(size), big, "--out", join14(set, `icon_${size}x${size}.png`)]);
20477
20658
  }
@@ -20914,7 +21095,7 @@ class SelfWatch {
20914
21095
 
20915
21096
  // ../daemon/src/core/state.ts
20916
21097
  import { randomBytes as randomBytes6 } from "crypto";
20917
- import { existsSync as existsSync17, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "fs";
21098
+ import { existsSync as existsSync18, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "fs";
20918
21099
  var TRUSTED_MAX = 16;
20919
21100
  var DAEMON_GROUPS = "daemon";
20920
21101
  var GROUPS_SAVE_MS = 250;
@@ -20923,7 +21104,7 @@ var empty = { repos: [], worktrees: [], sessions: {} };
20923
21104
  function loadState(paths) {
20924
21105
  ensureDirs(paths);
20925
21106
  const STATE_FILE = paths.stateFile;
20926
- if (!existsSync17(STATE_FILE))
21107
+ if (!existsSync18(STATE_FILE))
20927
21108
  return structuredClone(empty);
20928
21109
  let raw;
20929
21110
  try {
@@ -20975,7 +21156,7 @@ function saveState(paths, state) {
20975
21156
  function loadOrCreateToken(paths) {
20976
21157
  ensureDirs(paths);
20977
21158
  const TOKEN_FILE = paths.tokenFile;
20978
- if (existsSync17(TOKEN_FILE))
21159
+ if (existsSync18(TOKEN_FILE))
20979
21160
  return readFileSync12(TOKEN_FILE, "utf8").trim();
20980
21161
  const token = randomBytes6(32).toString("hex");
20981
21162
  writeFileSync8(TOKEN_FILE, token, { mode: 384 });
@@ -21063,9 +21244,9 @@ class StateStore {
21063
21244
  this.state.worktrees.push(wt);
21064
21245
  this.save();
21065
21246
  }
21066
- removeWorktree(id) {
21247
+ removeWorktree(id, opts = {}) {
21067
21248
  const wt = this.state.worktrees.find((w) => w.id === id);
21068
- const repo = wt && wt.kind !== "spare" ? this.repo(wt.repoId) : undefined;
21249
+ const repo = wt && wt.kind !== "spare" && !opts.refused ? this.repo(wt.repoId) : undefined;
21069
21250
  if (wt && repo)
21070
21251
  repo.promptedAt = Math.max(repo.promptedAt ?? 0, sentAt(wt));
21071
21252
  this.state.worktrees = this.state.worktrees.filter((w) => w.id !== id);
@@ -22010,7 +22191,7 @@ async function loadProjectTypescript(root) {
22010
22191
  }
22011
22192
 
22012
22193
  // ../daemon/src/drafts/store.ts
22013
- import { existsSync as existsSync18, readFileSync as readFileSync13, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "fs";
22194
+ import { existsSync as existsSync19, readFileSync as readFileSync13, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "fs";
22014
22195
  var SAVE_DELAY_MS = 1000;
22015
22196
 
22016
22197
  class DraftStore {
@@ -22147,7 +22328,7 @@ class DraftStore {
22147
22328
  }
22148
22329
  }
22149
22330
  function readRecord(file) {
22150
- if (!existsSync18(file))
22331
+ if (!existsSync19(file))
22151
22332
  return [];
22152
22333
  try {
22153
22334
  const raw = JSON.parse(readFileSync13(file, "utf8"));
@@ -22404,10 +22585,10 @@ class ExecService {
22404
22585
 
22405
22586
  // ../daemon/src/files/open.ts
22406
22587
  import { stat as stat3 } from "fs/promises";
22407
- import { basename as basename5, resolve as resolve7 } from "path";
22588
+ import { basename as basename6, resolve as resolve7 } from "path";
22408
22589
 
22409
22590
  // ../daemon/src/repos/browse.ts
22410
- import { existsSync as existsSync19, statSync as statSync6 } from "fs";
22591
+ import { existsSync as existsSync20, statSync as statSync6 } from "fs";
22411
22592
  import { readdir } from "fs/promises";
22412
22593
  import { homedir as homedir8 } from "os";
22413
22594
  import { dirname as dirname6, join as join16 } from "path";
@@ -22424,7 +22605,7 @@ function holdsNothing(names) {
22424
22605
  }
22425
22606
  async function describeFolder(abs) {
22426
22607
  const path = collapseTilde(abs);
22427
- if (existsSync19(join16(abs, ".git")))
22608
+ if (existsSync20(join16(abs, ".git")))
22428
22609
  return { path, kind: "project" };
22429
22610
  try {
22430
22611
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -22439,10 +22620,10 @@ function describe2(typed, parent) {
22439
22620
  isDir = statSync6(typed).isDirectory();
22440
22621
  } catch {}
22441
22622
  return {
22442
- exists: existsSync19(typed),
22623
+ exists: existsSync20(typed),
22443
22624
  isDir,
22444
- isRepo: isDir && existsSync19(join16(typed, ".git")),
22445
- parentExists: existsSync19(parent)
22625
+ isRepo: isDir && existsSync20(join16(typed, ".git")),
22626
+ parentExists: existsSync20(parent)
22446
22627
  };
22447
22628
  }
22448
22629
  async function browsePath(raw) {
@@ -22467,7 +22648,7 @@ async function browsePath(raw) {
22467
22648
  if (!name.toLowerCase().startsWith(prefix))
22468
22649
  continue;
22469
22650
  const abs = join16(dir, name);
22470
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync19(join16(abs, ".git")) });
22651
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync20(join16(abs, ".git")) });
22471
22652
  }
22472
22653
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
22473
22654
  return { entries: entries.slice(0, MAX_ENTRIES), target };
@@ -23291,6 +23472,9 @@ class OpenService {
23291
23472
  throw new UserError(`${raw} is a folder: Toyon opens files`);
23292
23473
  return this.openFile(raw, path, st);
23293
23474
  }
23475
+ async where(raw) {
23476
+ return (await this.locate(raw)).path;
23477
+ }
23294
23478
  async locate(raw) {
23295
23479
  const path = canonical(resolve7(expandTilde(raw)));
23296
23480
  const st = await stat3(path).catch((e) => {
@@ -23313,7 +23497,7 @@ class OpenService {
23313
23497
  return hit ? { kind: "file", worktreeId: hit.id, path: path.slice(hit.root.length + 1) } : null;
23314
23498
  }
23315
23499
  async grant(path, size, mtimeMs) {
23316
- const name = basename5(path);
23500
+ const name = basename6(path);
23317
23501
  if (this.d.refused.some((r) => within2(path, canonical(resolve7(r))))) {
23318
23502
  throw new UserError(`${name} is inside Toyon's own folder, which is not for editing`);
23319
23503
  }
@@ -23390,13 +23574,70 @@ function parseAllowed(json) {
23390
23574
  return null;
23391
23575
  }
23392
23576
  }
23393
- var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest";
23577
+ var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest,headRefOid";
23394
23578
  async function viewPr(repoPath, number) {
23395
23579
  const r = await gh(["pr", "view", String(number), "--json", PR_VIEW_FIELDS], repoPath, 20000);
23396
23580
  if (!r.ok || !r.out)
23397
23581
  return null;
23398
23582
  const parsed = parsePrView(r.out);
23399
- return parsed ? { ...parsed, at: Date.now() } : null;
23583
+ if (!parsed)
23584
+ return null;
23585
+ if (parsed.state === "open" && !parsed.preview) {
23586
+ const head = headOf(r.out);
23587
+ const preview = head ? await viewDeploymentPreview(repoPath, head) : null;
23588
+ if (preview)
23589
+ parsed.preview = preview;
23590
+ }
23591
+ return { ...parsed, at: Date.now() };
23592
+ }
23593
+ function headOf(json) {
23594
+ try {
23595
+ const o = JSON.parse(json);
23596
+ return typeof o.headRefOid === "string" && o.headRefOid ? o.headRefOid : null;
23597
+ } catch {
23598
+ return null;
23599
+ }
23600
+ }
23601
+ var DEPLOYMENTS_QUERY = `query($owner: String!, $name: String!, $oid: GitObjectID!) {
23602
+ repository(owner: $owner, name: $name) {
23603
+ object(oid: $oid) { ... on Commit {
23604
+ deployments(last: 10) { nodes { environment latestStatus { state environmentUrl } } }
23605
+ } }
23606
+ }
23607
+ }`;
23608
+ async function viewDeploymentPreview(repoPath, head) {
23609
+ const r = await gh([
23610
+ "api",
23611
+ "graphql",
23612
+ "-F",
23613
+ "owner={owner}",
23614
+ "-F",
23615
+ "name={repo}",
23616
+ "-f",
23617
+ `oid=${head}`,
23618
+ "-f",
23619
+ `query=${DEPLOYMENTS_QUERY}`
23620
+ ], repoPath, 20000);
23621
+ if (!r.ok || !r.out)
23622
+ return null;
23623
+ return parseDeployments(r.out);
23624
+ }
23625
+ function parseDeployments(json) {
23626
+ let nodes;
23627
+ try {
23628
+ const o = JSON.parse(json);
23629
+ nodes = o.data?.repository?.object?.deployments?.nodes;
23630
+ } catch {
23631
+ return null;
23632
+ }
23633
+ if (!Array.isArray(nodes))
23634
+ return null;
23635
+ for (const d of [...nodes].reverse()) {
23636
+ const st = d?.latestStatus;
23637
+ if (st?.state === "SUCCESS" && isPreviewUrl(st.environmentUrl))
23638
+ return st.environmentUrl;
23639
+ }
23640
+ return null;
23400
23641
  }
23401
23642
  function parsePrView(json) {
23402
23643
  let o;
@@ -23412,6 +23653,7 @@ function parsePrView(json) {
23412
23653
  const checks = foldChecks(o.statusCheckRollup);
23413
23654
  const mergeable = o.mergeable === "MERGEABLE" ? true : o.mergeable === "CONFLICTING" ? false : undefined;
23414
23655
  const automerge = o.autoMergeRequest !== null && o.autoMergeRequest !== undefined;
23656
+ const preview = state === "open" ? previewOf3(o.statusCheckRollup) : undefined;
23415
23657
  return {
23416
23658
  number: o.number,
23417
23659
  url: o.url,
@@ -23420,9 +23662,37 @@ function parsePrView(json) {
23420
23662
  ...review ? { review } : {},
23421
23663
  ...checks ? { checks } : {},
23422
23664
  ...mergeable !== undefined ? { mergeable } : {},
23423
- ...automerge ? { automerge } : {}
23665
+ ...automerge ? { automerge } : {},
23666
+ ...preview ? { preview } : {}
23424
23667
  };
23425
23668
  }
23669
+ function previewOf3(rollup) {
23670
+ if (!Array.isArray(rollup))
23671
+ return;
23672
+ for (const c of rollup) {
23673
+ const name = String(c.context ?? c.name ?? "");
23674
+ if (!/preview/i.test(name))
23675
+ continue;
23676
+ const word = String(c.conclusion ?? c.state ?? "").toUpperCase();
23677
+ if (word !== "SUCCESS")
23678
+ continue;
23679
+ const url = c.targetUrl ?? c.detailsUrl;
23680
+ if (isPreviewUrl(url))
23681
+ return url;
23682
+ }
23683
+ return;
23684
+ }
23685
+ function isPreviewUrl(url) {
23686
+ if (typeof url !== "string")
23687
+ return false;
23688
+ let host;
23689
+ try {
23690
+ host = new URL(url).hostname;
23691
+ } catch {
23692
+ return false;
23693
+ }
23694
+ return host !== "github.com" && !host.endsWith(".github.com");
23695
+ }
23426
23696
  function foldChecks(rollup) {
23427
23697
  if (!Array.isArray(rollup) || rollup.length === 0)
23428
23698
  return;
@@ -23447,7 +23717,6 @@ function prNumberOf(url) {
23447
23717
 
23448
23718
  // ../daemon/src/preview/errors.ts
23449
23719
  var KEPT = 5;
23450
- var ROWS_MAX = 3;
23451
23720
  var TWICE_MS = 1000;
23452
23721
 
23453
23722
  class PageErrorService {
@@ -23455,75 +23724,35 @@ class PageErrorService {
23455
23724
  held = new Map;
23456
23725
  constructor(d) {
23457
23726
  this.d = d;
23458
- d.hub.on("agent", (worktreeId, _seq, event) => this.heard(worktreeId, event));
23459
23727
  d.hub.on("worktreesChanged", () => {
23460
23728
  for (const id of this.held.keys())
23461
23729
  if (!d.known(id))
23462
23730
  this.held.delete(id);
23463
23731
  });
23464
23732
  }
23465
- of(worktreeId) {
23466
- let h = this.held.get(worktreeId);
23467
- if (!h) {
23468
- h = { recent: [], running: false, noted: new Set, rows: 0 };
23469
- this.held.set(worktreeId, h);
23470
- }
23471
- return h;
23472
- }
23473
- heard(worktreeId, event) {
23474
- const h = this.of(worktreeId);
23475
- switch (event.type) {
23476
- case "user-message":
23477
- case "fix-asked":
23478
- h.noted = new Set;
23479
- h.rows = 0;
23480
- break;
23481
- case "turn-start":
23482
- h.running = true;
23483
- break;
23484
- case "turn-end":
23485
- case "agent-error":
23486
- case "agent-auth-required":
23487
- h.running = false;
23488
- this.flush(worktreeId, h, event.ts);
23489
- break;
23490
- }
23491
- }
23492
23733
  report(worktreeId, message) {
23493
23734
  if (!this.d.known(worktreeId)) {
23494
23735
  log.debug("pages", `an error from unknown worktree ${worktreeId} was dropped`);
23495
23736
  return;
23496
23737
  }
23497
- const h = this.of(worktreeId);
23738
+ let h = this.held.get(worktreeId);
23739
+ if (!h) {
23740
+ h = [];
23741
+ this.held.set(worktreeId, h);
23742
+ }
23498
23743
  const now = this.d.now?.() ?? Date.now();
23499
- const last = h.recent.at(-1);
23744
+ const last = h.at(-1);
23500
23745
  if (last && last.message === message && now - last.ts < TWICE_MS)
23501
23746
  return;
23502
- h.recent.push({ message, ts: now });
23503
- if (h.recent.length > KEPT)
23504
- h.recent.splice(0, h.recent.length - KEPT);
23505
- if (!h.running)
23506
- this.flush(worktreeId, h, now);
23507
- }
23508
- flush(worktreeId, h, ts) {
23509
- const fresh = [...new Set(h.recent.map((e) => e.message))].filter((m) => !h.noted.has(m));
23510
- if (fresh.length === 0 || h.rows >= ROWS_MAX)
23511
- return;
23512
- const agent = this.d.runtime.agentFor(worktreeId);
23513
- if (!agent)
23514
- return;
23515
- for (const m of fresh)
23516
- h.noted.add(m);
23517
- h.rows++;
23518
- agent.note({ type: "page-error", errors: fresh, ts });
23747
+ h.push({ message, ts: now });
23748
+ if (h.length > KEPT)
23749
+ h.splice(0, h.length - KEPT);
23519
23750
  }
23520
23751
  loaded(worktreeId) {
23521
- const h = this.held.get(worktreeId);
23522
- if (h)
23523
- h.recent = [];
23752
+ this.held.delete(worktreeId);
23524
23753
  }
23525
23754
  recent(worktreeId) {
23526
- return this.held.get(worktreeId)?.recent.map((e) => e.message) ?? [];
23755
+ return this.held.get(worktreeId)?.map((e) => e.message) ?? [];
23527
23756
  }
23528
23757
  ambient(worktreeId) {
23529
23758
  const errors = this.recent(worktreeId);
@@ -23535,122 +23764,6 @@ ${errors.map((e) => `- ${e}`).join(`
23535
23764
  }
23536
23765
  }
23537
23766
 
23538
- // ../daemon/src/preview/render.ts
23539
- import { randomUUID as randomUUID2 } from "crypto";
23540
- var WAIT_MS2 = 1e4;
23541
- var PATH_MAX = 2048;
23542
- var refused = (text) => ({ text, isError: true });
23543
-
23544
- class RenderService {
23545
- d;
23546
- courier = () => false;
23547
- pending = new Map;
23548
- out = new Set;
23549
- constructor(d) {
23550
- this.d = d;
23551
- }
23552
- tool() {
23553
- return {
23554
- name: PREVIEW_TOOL,
23555
- description: "See whether a page of this worktree's preview renders without throwing. Pass `path` (for example `/about`): a browser the user has open loads it beside what they are looking at and reports the uncaught errors and unhandled rejections it threw in the moment after load. With no path, reports what the preview has thrown since it last loaded, without rendering. It does not describe what the page looks like, and it is not a substitute for the user's own checks.",
23556
- inputSchema: {
23557
- type: "object",
23558
- properties: {
23559
- path: {
23560
- type: "string",
23561
- description: "the route to render, starting with `/`; absent to report without rendering"
23562
- }
23563
- },
23564
- additionalProperties: false
23565
- }
23566
- };
23567
- }
23568
- async call(worktreeId, args) {
23569
- const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
23570
- if (a.path !== undefined && typeof a.path !== "string")
23571
- return refused("`path` is a string: a route, like `/about`.");
23572
- const path = a.path?.trim();
23573
- if (path !== undefined && (!/^(?:\/(?![/\\])|[?#])/.test(path) || path.length > PATH_MAX)) {
23574
- return refused("`path` is a route of the preview, starting with `/`, like `/about`.");
23575
- }
23576
- const standing = this.d.runtime.previewStanding(worktreeId);
23577
- if (!standing)
23578
- return refused("This project has nothing to run, so there is no page to render.");
23579
- if (standing.status !== "running")
23580
- return refused(previewContext(standing) ?? "The preview is not running.");
23581
- if (path === undefined)
23582
- return { text: this.held(worktreeId) };
23583
- if (this.out.has(worktreeId))
23584
- return refused("A render is already under way; call again when it has returned.");
23585
- const result = await this.render(worktreeId, path);
23586
- if (!result)
23587
- return refused(`No browser has this worktree open right now, so nothing can render it. ${this.held(worktreeId)}`);
23588
- if (!result.ok)
23589
- return refused(`Could not render ${path}: ${result.reason ?? "the page did not report"}.`);
23590
- return { text: this.report(path, result) };
23591
- }
23592
- render(worktreeId, path) {
23593
- const id = randomUUID2();
23594
- return new Promise((resolve) => {
23595
- const wait = this.d.waitMs ?? WAIT_MS2;
23596
- const timer = setTimeout(() => settle({
23597
- worktreeId,
23598
- id,
23599
- ok: false,
23600
- reason: `the page did not report within ${Math.round(wait / 1000)}s`,
23601
- errors: []
23602
- }), wait);
23603
- const settle = (r) => {
23604
- if (!this.pending.delete(id))
23605
- return;
23606
- clearTimeout(timer);
23607
- this.out.delete(worktreeId);
23608
- resolve(r);
23609
- };
23610
- this.pending.set(id, { worktreeId, settle });
23611
- this.out.add(worktreeId);
23612
- if (!this.courier(worktreeId, { t: "render", worktreeId, id, path }))
23613
- settle(null);
23614
- });
23615
- }
23616
- rendered(msg) {
23617
- const p = this.pending.get(msg.id);
23618
- if (!p || p.worktreeId !== msg.worktreeId) {
23619
- log.debug("render", `an answer for no render out on ${msg.worktreeId} was dropped`);
23620
- return;
23621
- }
23622
- p.settle(msg);
23623
- }
23624
- held(worktreeId) {
23625
- const errors = this.d.pageErrors.recent(worktreeId);
23626
- if (errors.length === 0)
23627
- return "The preview has thrown nothing since it last loaded.";
23628
- return `The preview has thrown since it last loaded:
23629
- ${errors.map((e) => `- ${e}`).join(`
23630
- `)}`;
23631
- }
23632
- report(path, r) {
23633
- const title = r.title ? ` (title "${r.title}")` : "";
23634
- const landed = r.url ? pathOf(r.url) : null;
23635
- const elsewhere = landed && landed !== path ? ` It landed on ${landed}.` : "";
23636
- const head = `Rendered ${path} in the user's browser${title}.${elsewhere}`;
23637
- if (r.errors.length === 0)
23638
- return `${head} No errors in the moment after load.`;
23639
- const n = r.errors.length;
23640
- return `${head} ${n} ${n === 1 ? "error" : "errors"} in the moment after load:
23641
- ${r.errors.map((e) => `- ${e}`).join(`
23642
- `)}`;
23643
- }
23644
- }
23645
- function pathOf(url) {
23646
- try {
23647
- const u = new URL(url);
23648
- return `${u.pathname}${u.search}`;
23649
- } catch {
23650
- return null;
23651
- }
23652
- }
23653
-
23654
23767
  // ../daemon/src/runtime/setup.ts
23655
23768
  var SETUP_RING = 64 * 1024;
23656
23769
  var SETUP_COLS = 120;
@@ -23779,9 +23892,9 @@ class AfterLand {
23779
23892
  }
23780
23893
 
23781
23894
  // ../daemon/src/repos/registry.ts
23782
- import { existsSync as existsSync23, mkdirSync as mkdirSync7, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
23895
+ import { existsSync as existsSync24, mkdirSync as mkdirSync7, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
23783
23896
  import { homedir as homedir10 } from "os";
23784
- import { basename as basename7, dirname as dirname8, join as join21 } from "path";
23897
+ import { basename as basename8, dirname as dirname8, join as join21 } from "path";
23785
23898
 
23786
23899
  // ../daemon/src/git/base.ts
23787
23900
  async function resolveBase(repoPath, defaultBranch, route) {
@@ -23792,7 +23905,7 @@ async function resolveBase(repoPath, defaultBranch, route) {
23792
23905
  }
23793
23906
 
23794
23907
  // ../daemon/src/repos/config.ts
23795
- import { closeSync, existsSync as existsSync20, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync14, readSync, statSync as statSync7 } from "fs";
23908
+ import { closeSync, existsSync as existsSync21, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync14, readSync, statSync as statSync7 } from "fs";
23796
23909
  import { join as join18 } from "path";
23797
23910
 
23798
23911
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -25157,7 +25270,7 @@ function applyEdits(text, edits) {
25157
25270
 
25158
25271
  // ../daemon/src/repos/config.ts
25159
25272
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
25160
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync20(join18(repoPath, rel)));
25273
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync21(join18(repoPath, rel)));
25161
25274
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25162
25275
  function parseSettings(text) {
25163
25276
  const errors = [];
@@ -25242,7 +25355,7 @@ function configTarget(repoPath, made) {
25242
25355
  function configBody(repoPath, rel, config) {
25243
25356
  const place = PLACES.find((p) => p.local === rel);
25244
25357
  const abs = place && join18(repoPath, place.shared);
25245
- if (!abs || !existsSync20(abs))
25358
+ if (!abs || !existsSync21(abs))
25246
25359
  return config;
25247
25360
  let shared;
25248
25361
  try {
@@ -25290,7 +25403,7 @@ function detectConfig(repoPath) {
25290
25403
  if (explicit)
25291
25404
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
25292
25405
  const pkgPath = join18(repoPath, "package.json");
25293
- if (existsSync20(pkgPath)) {
25406
+ if (existsSync21(pkgPath)) {
25294
25407
  const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
25295
25408
  const scripts = pkg.scripts ?? {};
25296
25409
  const runner = detectRunner(repoPath, pkg.packageManager);
@@ -25312,7 +25425,7 @@ function detectConfig(repoPath) {
25312
25425
  };
25313
25426
  }
25314
25427
  }
25315
- if (existsSync20(join18(repoPath, "start.sh"))) {
25428
+ if (existsSync21(join18(repoPath, "start.sh"))) {
25316
25429
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
25317
25430
  }
25318
25431
  const assumed = assumeNothingToRun(repoPath);
@@ -25355,7 +25468,7 @@ function assumeNothingToRun(repoPath) {
25355
25468
  return marker && !servesPage(repoPath) ? marker : undefined;
25356
25469
  }
25357
25470
  function markerIn(repoPath) {
25358
- const named = NO_PAGE_MARKERS.find((f) => existsSync20(join18(repoPath, f)));
25471
+ const named = NO_PAGE_MARKERS.find((f) => existsSync21(join18(repoPath, f)));
25359
25472
  if (named)
25360
25473
  return named;
25361
25474
  let entries;
@@ -25367,7 +25480,7 @@ function markerIn(repoPath) {
25367
25480
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
25368
25481
  }
25369
25482
  function servesPage(repoPath) {
25370
- if (WEB_FILES.some((f) => existsSync20(join18(repoPath, f))))
25483
+ if (WEB_FILES.some((f) => existsSync21(join18(repoPath, f))))
25371
25484
  return true;
25372
25485
  if (/^web:/m.test(head(join18(repoPath, "Procfile"))))
25373
25486
  return true;
@@ -25404,7 +25517,7 @@ function mentions(text, name) {
25404
25517
  return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
25405
25518
  }
25406
25519
  function head(path) {
25407
- if (!existsSync20(path))
25520
+ if (!existsSync21(path))
25408
25521
  return "";
25409
25522
  try {
25410
25523
  const fd = openSync2(path, "r");
@@ -25445,7 +25558,7 @@ function isLocalDatabase(url) {
25445
25558
  }
25446
25559
  function detectServices(repoPath) {
25447
25560
  const out = {};
25448
- const compose = COMPOSE_FILES.find((f) => existsSync20(join18(repoPath, f)));
25561
+ const compose = COMPOSE_FILES.find((f) => existsSync21(join18(repoPath, f)));
25449
25562
  if (compose)
25450
25563
  out.compose = compose;
25451
25564
  for (const file of ENV_FILES) {
@@ -25484,21 +25597,21 @@ function inferPaths(cmd, repoPath, depth = 1) {
25484
25597
  const t = tokens[i];
25485
25598
  if (t === "cd") {
25486
25599
  const dir = tokens[i + 1];
25487
- if (dir && !dir.startsWith("-") && existsSync20(join18(repoPath, dir)))
25600
+ if (dir && !dir.startsWith("-") && existsSync21(join18(repoPath, dir)))
25488
25601
  add(dir);
25489
25602
  continue;
25490
25603
  }
25491
25604
  if (t.startsWith("-") || t.startsWith("$"))
25492
25605
  continue;
25493
25606
  const mod = /^([A-Za-z_]\w*)(?:\.\w+)*:\w+$/.exec(t);
25494
- if (mod?.[1] && existsSync20(join18(repoPath, mod[1]))) {
25607
+ if (mod?.[1] && existsSync21(join18(repoPath, mod[1]))) {
25495
25608
  add(mod[1]);
25496
25609
  continue;
25497
25610
  }
25498
25611
  const rel = t.replace(/^\.\//, "");
25499
25612
  if (rel.startsWith("/") || rel.startsWith("..") || rel.includes("="))
25500
25613
  continue;
25501
- if (!existsSync20(join18(repoPath, rel)))
25614
+ if (!existsSync21(join18(repoPath, rel)))
25502
25615
  continue;
25503
25616
  add(rel);
25504
25617
  if (depth > 0 && rel.endsWith(".sh")) {
@@ -25525,9 +25638,9 @@ function procCommand(runner, script, body) {
25525
25638
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
25526
25639
  }
25527
25640
  function detectPages(repoPath, scripts, runner) {
25528
- if (!scripts.build || !existsSync20(join18(repoPath, "functions")))
25641
+ if (!scripts.build || !existsSync21(join18(repoPath, "functions")))
25529
25642
  return null;
25530
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join18(repoPath, f)).find((f) => existsSync20(f));
25643
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join18(repoPath, f)).find((f) => existsSync21(f));
25531
25644
  if (!cfg)
25532
25645
  return null;
25533
25646
  const out = readFileSync14(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
@@ -25536,17 +25649,17 @@ function detectPages(repoPath, scripts, runner) {
25536
25649
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
25537
25650
  }
25538
25651
  function detectRunner(repoPath, packageManager) {
25539
- if (existsSync20(join18(repoPath, "bun.lock")) || existsSync20(join18(repoPath, "bun.lockb")))
25652
+ if (existsSync21(join18(repoPath, "bun.lock")) || existsSync21(join18(repoPath, "bun.lockb")))
25540
25653
  return "bun";
25541
- if (existsSync20(join18(repoPath, "pnpm-lock.yaml")))
25654
+ if (existsSync21(join18(repoPath, "pnpm-lock.yaml")))
25542
25655
  return "pnpm";
25543
- if (existsSync20(join18(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
25656
+ if (existsSync21(join18(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
25544
25657
  return "yarn";
25545
25658
  return "npm";
25546
25659
  }
25547
25660
 
25548
25661
  // ../daemon/src/repos/create.ts
25549
- import { existsSync as existsSync21, realpathSync as realpathSync2, statSync as statSync8 } from "fs";
25662
+ import { existsSync as existsSync22, realpathSync as realpathSync2, statSync as statSync8 } from "fs";
25550
25663
  import { appendFile as appendFile2, mkdir as mkdir3, rm as rm2 } from "fs/promises";
25551
25664
  import { homedir as homedir9 } from "os";
25552
25665
  import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
@@ -25561,13 +25674,13 @@ function planProject(opts, home = homedir9()) {
25561
25674
  const first = firstProjectsFolder(opts.parent, home);
25562
25675
  const parent = first ?? resolveParent(opts.parent);
25563
25676
  const dir = join19(parent, opts.name.trim());
25564
- if (existsSync21(dir))
25677
+ if (existsSync22(dir))
25565
25678
  throw new UserError(`${dir} already exists`);
25566
25679
  return { parent, dir, makeParent: first !== null };
25567
25680
  }
25568
25681
  function firstProjectsFolder(raw, home) {
25569
25682
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
25570
- if (typed !== join19(home, PROJECTS_FOLDER) || existsSync21(typed) || !existsSync21(home))
25683
+ if (typed !== join19(home, PROJECTS_FOLDER) || existsSync22(typed) || !existsSync22(home))
25571
25684
  return null;
25572
25685
  return join19(realpathSync2(home), PROJECTS_FOLDER);
25573
25686
  }
@@ -25577,10 +25690,10 @@ function planInPlace(opts) {
25577
25690
  throw new UserError(`${opts.name} is not a folder name`);
25578
25691
  }
25579
25692
  const typed = join19(resolveParent(opts.parent), name);
25580
- if (!existsSync21(typed) || !statSync8(typed).isDirectory())
25693
+ if (!existsSync22(typed) || !statSync8(typed).isDirectory())
25581
25694
  throw new UserError(`${typed} is not a folder`);
25582
25695
  const dir = realpathSync2(typed);
25583
- if (existsSync21(join19(dir, ".git")))
25696
+ if (existsSync22(join19(dir, ".git")))
25584
25697
  throw new UserError(`${dir} is already a project; open it instead`);
25585
25698
  return { parent: dirname7(dir), dir, makeParent: false };
25586
25699
  }
@@ -25588,7 +25701,7 @@ function resolveParent(raw) {
25588
25701
  const typed = expandTilde(raw.trim());
25589
25702
  if (!typed.startsWith("/"))
25590
25703
  throw new UserError(`${raw} is not a folder path`);
25591
- if (!existsSync21(typed))
25704
+ if (!existsSync22(typed))
25592
25705
  throw new UserError(`${raw} does not exist; make it first`);
25593
25706
  if (!statSync8(typed).isDirectory())
25594
25707
  throw new UserError(`${raw} is not a folder`);
@@ -25705,8 +25818,8 @@ async function ok(p, what) {
25705
25818
  }
25706
25819
 
25707
25820
  // ../daemon/src/repos/watcher.ts
25708
- import { existsSync as existsSync22, watch } from "fs";
25709
- import { basename as basename6, join as join20 } from "path";
25821
+ import { existsSync as existsSync23, watch } from "fs";
25822
+ import { basename as basename7, join as join20 } from "path";
25710
25823
  function watchDefaultBranch(repoPath, branch, onMove) {
25711
25824
  const last = { local: null, upstream: null };
25712
25825
  const read = async () => {
@@ -25770,11 +25883,11 @@ function watchDefaultBranch(repoPath, branch, onMove) {
25770
25883
  let mid = null;
25771
25884
  const armInner = () => {
25772
25885
  inner?.close();
25773
- inner = existsSync22(mine) ? tryWatch(mine) : null;
25886
+ inner = existsSync23(mine) ? tryWatch(mine) : null;
25774
25887
  };
25775
25888
  const armMid = () => {
25776
25889
  mid?.close();
25777
- mid = existsSync22(remotes) ? tryWatch(remotes, (_event, filename) => {
25890
+ mid = existsSync23(remotes) ? tryWatch(remotes, (_event, filename) => {
25778
25891
  if (!filename || filename === remote.out)
25779
25892
  armInner();
25780
25893
  schedule();
@@ -25852,7 +25965,7 @@ function watchWorktreeDir(repoPath, onChange) {
25852
25965
  }
25853
25966
  function watchConfigFile(repoPath, onChange) {
25854
25967
  const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
25855
- const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename6(rel)));
25968
+ const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename7(rel)));
25856
25969
  const dir = join20(repoPath, CONFIG_DIR);
25857
25970
  let timer = null;
25858
25971
  let outer = null;
@@ -25868,7 +25981,7 @@ function watchConfigFile(repoPath, onChange) {
25868
25981
  const armInner = () => {
25869
25982
  inner?.close();
25870
25983
  inner = null;
25871
- if (!existsSync22(dir))
25984
+ if (!existsSync23(dir))
25872
25985
  return;
25873
25986
  try {
25874
25987
  inner = watch(dir, (_event, filename) => {
@@ -25922,7 +26035,7 @@ class RepoRegistry {
25922
26035
  }
25923
26036
  async boot() {
25924
26037
  const { state } = this.d;
25925
- const gone = state.worktrees.filter((wt) => !existsSync23(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
26038
+ const gone = state.worktrees.filter((wt) => !existsSync24(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
25926
26039
  for (const wt of gone)
25927
26040
  await this.d.worktrees.forgetGone(wt);
25928
26041
  for (const repo of state.repos) {
@@ -25990,7 +26103,7 @@ class RepoRegistry {
25990
26103
  return `${repo.name} is set up now`;
25991
26104
  if (main.promptedAt || main.lastTurn)
25992
26105
  return `${repo.name} has a chat now`;
25993
- if (existsSync23(join21(repo.path, ".claude")))
26106
+ if (existsSync24(join21(repo.path, ".claude")))
25994
26107
  return `an agent has already run in ${repo.name}`;
25995
26108
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
25996
26109
  return `${repo.name} has files in it now`;
@@ -26071,7 +26184,7 @@ class RepoRegistry {
26071
26184
  }
26072
26185
  async register(rawPath, made) {
26073
26186
  const path = expandTilde(rawPath);
26074
- if (!existsSync23(path))
26187
+ if (!existsSync24(path))
26075
26188
  throw new UserError(`${path} does not exist`);
26076
26189
  if (!await isGitRepo(path))
26077
26190
  throw new UserError(`${path} is not a git repository`);
@@ -26085,7 +26198,7 @@ class RepoRegistry {
26085
26198
  const repo = {
26086
26199
  id: shortId(),
26087
26200
  path: root,
26088
- name: basename7(root),
26201
+ name: basename8(root),
26089
26202
  defaultBranch: await defaultBranch(root),
26090
26203
  config: detected.config,
26091
26204
  configFile: configTarget(root, !!made),
@@ -26145,7 +26258,7 @@ class RepoRegistry {
26145
26258
  throw new UserError(current.reason);
26146
26259
  const was = configTarget(repo.path, !!repo.made);
26147
26260
  const rel = configSibling(was, kind);
26148
- const moved = rel !== was && existsSync23(join21(repo.path, was));
26261
+ const moved = rel !== was && existsSync24(join21(repo.path, was));
26149
26262
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
26150
26263
  try {
26151
26264
  rmSync4(join21(repo.path, was));
@@ -26261,7 +26374,7 @@ class RepoRegistry {
26261
26374
  }
26262
26375
  placeConfig(repo) {
26263
26376
  const rel = configTarget(repo.path, !!repo.made);
26264
- if (isLocalConfigFile(rel) && existsSync23(join21(repo.path, rel))) {
26377
+ if (isLocalConfigFile(rel) && existsSync24(join21(repo.path, rel))) {
26265
26378
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
26266
26379
  }
26267
26380
  if (repo.configFile === rel)
@@ -27840,7 +27953,7 @@ function isAlive(pid) {
27840
27953
  }
27841
27954
 
27842
27955
  // ../daemon/src/runtime/bridge-script.ts
27843
- import { existsSync as existsSync24, readFileSync as readFileSync15, statSync as statSync9 } from "fs";
27956
+ import { existsSync as existsSync25, readFileSync as readFileSync15, statSync as statSync9 } from "fs";
27844
27957
  var LEARNED_MAX = 8;
27845
27958
 
27846
27959
  class BridgeScript {
@@ -27870,7 +27983,7 @@ class BridgeScript {
27870
27983
  this.cache = null;
27871
27984
  }
27872
27985
  get() {
27873
- if (!existsSync24(this.path))
27986
+ if (!existsSync25(this.path))
27874
27987
  return "// toyon bridge not built";
27875
27988
  const s = statSync9(this.path);
27876
27989
  const stamp = `${s.mtimeMs}:${s.size}`;
@@ -27903,7 +28016,7 @@ import { homedir as homedir12, hostname as hostname2 } from "os";
27903
28016
  import { randomBytes as randomBytes7 } from "crypto";
27904
28017
 
27905
28018
  // ../daemon/src/agent/config.ts
27906
- import { existsSync as existsSync25, readFileSync as readFileSync16 } from "fs";
28019
+ import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
27907
28020
  import { homedir as homedir11 } from "os";
27908
28021
  import { join as join23 } from "path";
27909
28022
  function agentConfigFiles(spec, repoPath, home = homedir11()) {
@@ -27911,7 +28024,7 @@ function agentConfigFiles(spec, repoPath, home = homedir11()) {
27911
28024
  id,
27912
28025
  label,
27913
28026
  path,
27914
- exists: existsSync25(path)
28027
+ exists: existsSync26(path)
27915
28028
  });
27916
28029
  if (spec.id === "claude") {
27917
28030
  return [
@@ -27943,7 +28056,7 @@ function describeAgentConfig(spec, repoPath, home = homedir11()) {
27943
28056
  return { agent: spec.id, files, servers };
27944
28057
  }
27945
28058
  function readJson(path) {
27946
- if (!existsSync25(path))
28059
+ if (!existsSync26(path))
27947
28060
  return null;
27948
28061
  try {
27949
28062
  const v = JSON.parse(readFileSync16(path, "utf8"));
@@ -27964,7 +28077,7 @@ function serversOf(raw, scope) {
27964
28077
  });
27965
28078
  }
27966
28079
  function codexServers(path) {
27967
- if (!existsSync25(path))
28080
+ if (!existsSync26(path))
27968
28081
  return [];
27969
28082
  const out = [];
27970
28083
  let current = null;
@@ -28597,12 +28710,6 @@ var handlers = {
28597
28710
  "page-loaded"(msg, _ctx, s) {
28598
28711
  s.pageErrors.loaded(msg.worktreeId);
28599
28712
  },
28600
- "fix-page"(msg, _ctx, s) {
28601
- s.fix.pressPage(msg.worktreeId);
28602
- },
28603
- rendered(msg, _ctx, s) {
28604
- s.render.rendered(msg);
28605
- },
28606
28713
  async chat(msg, ctx, s) {
28607
28714
  await sending(s, ctx, msg, msg.attachments, () => s.worktrees.send(msg.worktreeId, {
28608
28715
  text: msg.text,
@@ -28859,6 +28966,15 @@ var handlers = {
28859
28966
  reveal(msg, _ctx, s) {
28860
28967
  s.files.reveal(msg.worktreeId, msg.path);
28861
28968
  },
28969
+ async "reveal-path"(msg, _ctx, s) {
28970
+ s.files.revealPath(await s.opens.where(msg.path));
28971
+ },
28972
+ async "reveal-attachment"(msg, _ctx, s) {
28973
+ const path = await s.attachments.locate(msg.worktreeId, msg.file, (file) => s.worktrees.archivedAttachment(msg.worktreeId, file));
28974
+ if (!path)
28975
+ throw new UserError("that attachment is no longer on this machine");
28976
+ s.files.revealPath(path);
28977
+ },
28862
28978
  async "discard-files"(msg, _ctx, s) {
28863
28979
  try {
28864
28980
  for (const path of msg.paths)
@@ -29015,9 +29131,12 @@ var handlers = {
29015
29131
  s.files.revealPath(file.path);
29016
29132
  },
29017
29133
  async "agent-logout"(msg, _ctx, s) {
29018
- await s.accounts.logout(msg.agent);
29134
+ await s.readiness.logout(msg.agent);
29019
29135
  s.hub.emit("agentsChanged");
29020
29136
  },
29137
+ "agent-check"(msg, _ctx, s) {
29138
+ fireAndForget("agents", s.readiness.check(msg.agent), "agent check");
29139
+ },
29021
29140
  "set-default-agent"(msg, _ctx, s) {
29022
29141
  s.agents.require(msg.agent);
29023
29142
  s.state.setDefaultAgent(msg.agent);
@@ -29140,7 +29259,7 @@ async function dispatch(msg, ctx, s) {
29140
29259
  }
29141
29260
 
29142
29261
  // ../daemon/src/server/http.ts
29143
- import { existsSync as existsSync26 } from "fs";
29262
+ import { existsSync as existsSync27 } from "fs";
29144
29263
  import { hostname } from "os";
29145
29264
  import { join as join24 } from "path";
29146
29265
 
@@ -29365,6 +29484,20 @@ function createFetch(opts) {
29365
29484
  const refused = await opts.restart(url.searchParams.has(RESTART_NOW));
29366
29485
  return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
29367
29486
  }
29487
+ if (url.pathname === "/changelog" && req.method === "GET") {
29488
+ if (!authed(req))
29489
+ return unauthorized();
29490
+ const text = await opts.changelog();
29491
+ if (text === null)
29492
+ return new Response("this copy of Toyon carries no release notes", { status: 404 });
29493
+ return new Response(text, {
29494
+ headers: {
29495
+ "content-type": "text/markdown; charset=utf-8",
29496
+ "cache-control": NO_STORE2,
29497
+ "x-content-type-options": "nosniff"
29498
+ }
29499
+ });
29500
+ }
29368
29501
  const paired = await opts.pairing(req, url, d);
29369
29502
  if (paired)
29370
29503
  return paired;
@@ -29431,7 +29564,7 @@ function createFetch(opts) {
29431
29564
  return new Response("unauthorized", { status: 401 });
29432
29565
  const id = url.pathname.slice("/uploads/".length);
29433
29566
  const path = opts.attachments.uploads.path(id);
29434
- if (!path || !existsSync26(path))
29567
+ if (!path || !existsSync27(path))
29435
29568
  return new Response("not found", { status: 404 });
29436
29569
  return new Response(Bun.file(path), {
29437
29570
  headers: { "cache-control": IMMUTABLE, ...attachedHeaders(opts.attachments.uploads.imageType(id)) }
@@ -29444,9 +29577,14 @@ function createFetch(opts) {
29444
29577
  const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
29445
29578
  if (worktreeId && file)
29446
29579
  await opts.attachments.whenWritten(worktreeId, file);
29447
- const path = places.find((p) => !!p && existsSync26(p));
29580
+ const path = places.find((p) => !!p && existsSync27(p));
29448
29581
  if (!path)
29449
29582
  return new Response("not found", { status: 404 });
29583
+ if (url.searchParams.has("where")) {
29584
+ return new Response(path, {
29585
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }
29586
+ });
29587
+ }
29450
29588
  return new Response(Bun.file(path), {
29451
29589
  headers: { "cache-control": IMMUTABLE, ...attachedHeaders(drawnType(file ?? "")) }
29452
29590
  });
@@ -29479,7 +29617,7 @@ function createFetch(opts) {
29479
29617
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
29480
29618
  const file = join24(opts.shellDist, rel.replaceAll("..", ""));
29481
29619
  const hashed = rel.startsWith("/assets/");
29482
- if (existsSync26(file) && Bun.file(file).size > 0) {
29620
+ if (existsSync27(file) && Bun.file(file).size > 0) {
29483
29621
  if (rel === "/manifest.json") {
29484
29622
  const { bar, ground } = opts.manifestColors();
29485
29623
  const manifest = { ...await Bun.file(file).json(), theme_color: bar, background_color: ground };
@@ -29499,7 +29637,7 @@ function createFetch(opts) {
29499
29637
  if (hashed)
29500
29638
  return new Response("not found", { status: 404 });
29501
29639
  const index = join24(opts.shellDist, "index.html");
29502
- if (existsSync26(index))
29640
+ if (existsSync27(index))
29503
29641
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE2 } });
29504
29642
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
29505
29643
  }
@@ -29574,14 +29712,6 @@ function startServer(opts) {
29574
29712
  }
29575
29713
  };
29576
29714
  const send = (ws, msg) => raw(ws, JSON.stringify(msg));
29577
- s.render.courier = (worktreeId, msg) => {
29578
- const shells = [...sockets].filter((ws) => ws.data.authed && !ws.data.preview);
29579
- const ws = shells.find((w) => w.data.view === worktreeId) ?? shells.find((w) => w.data.subs.has(worktreeId));
29580
- if (!ws)
29581
- return false;
29582
- send(ws, msg);
29583
- return true;
29584
- };
29585
29715
  const broadcast = (msg) => {
29586
29716
  const json = JSON.stringify(msg);
29587
29717
  for (const ws of sockets)
@@ -29759,20 +29889,9 @@ function startServer(opts) {
29759
29889
  };
29760
29890
  s.hub.on("themesChanged", themesChanged);
29761
29891
  themesChanged();
29762
- const agentInfos = () => s.accounts.describe(s.agents.infos()).map((a) => {
29763
- const models = s.state.cachedOptions(a.id, "model");
29764
- const efforts = s.state.cachedOptions(a.id, "thought_level");
29765
- const spec = s.agents.get(a.id);
29766
- return {
29767
- ...a,
29768
- ...models.length > 0 ? { models } : {},
29769
- ...efforts.length > 0 ? { efforts } : {},
29770
- ...spec && !lacksQuickModel(spec, s.state) ? { quick: true } : {}
29771
- };
29772
- });
29773
29892
  const agentsMsg = () => ({
29774
29893
  t: "agents",
29775
- agents: agentInfos(),
29894
+ agents: s.readiness.describe(),
29776
29895
  defaultAgent: s.agents.defaultId(s.state.defaultAgent),
29777
29896
  agentChosen: s.state.defaultAgent !== undefined
29778
29897
  });
@@ -29810,7 +29929,7 @@ function startServer(opts) {
29810
29929
  trunks,
29811
29930
  themes: s.themes.themes,
29812
29931
  themePrefs: s.themes.prefs,
29813
- agents: agentInfos(),
29932
+ agents: s.readiness.describe(),
29814
29933
  defaultAgent: s.agents.defaultId(s.state.defaultAgent),
29815
29934
  agentChosen: s.state.defaultAgent !== undefined,
29816
29935
  home: homedir12(),
@@ -29880,7 +29999,8 @@ function startServer(opts) {
29880
29999
  restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
29881
30000
  grants: opts.grants,
29882
30001
  trusted: () => s.state.trustedOrigins,
29883
- mcp: (req, id) => s.mcp.fetch(req, id)
30002
+ mcp: (req, id) => s.mcp.fetch(req, id),
30003
+ changelog: () => Bun.file(opts.changelog).text().catch(() => null)
29884
30004
  }),
29885
30005
  websocket: {
29886
30006
  async open(ws) {
@@ -29897,6 +30017,7 @@ function startServer(opts) {
29897
30017
  sockets.add(ws);
29898
30018
  link(ws);
29899
30019
  send(ws, await helloFrame());
30020
+ fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
29900
30021
  for (const o of s.opens.takePending())
29901
30022
  send(ws, openedFrame(o));
29902
30023
  },
@@ -29991,9 +30112,9 @@ function startServer(opts) {
29991
30112
  }
29992
30113
 
29993
30114
  // ../daemon/src/themes/store.ts
29994
- import { existsSync as existsSync27, readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync10, writeFileSync as writeFileSync11 } from "fs";
30115
+ import { existsSync as existsSync28, readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync10, writeFileSync as writeFileSync11 } from "fs";
29995
30116
  import { homedir as homedir13 } from "os";
29996
- import { basename as basename8, dirname as dirname9, join as join25, resolve as resolve9 } from "path";
30117
+ import { basename as basename9, dirname as dirname9, join as join25, resolve as resolve9 } from "path";
29997
30118
  var home = homedir13();
29998
30119
  var defaultExtensionDirs = [
29999
30120
  join25(home, ".vscode", "extensions"),
@@ -30078,7 +30199,7 @@ class ThemeStore {
30078
30199
  if (json && typeof json === "object" && typeof json.include === "string") {
30079
30200
  throw new ThemeImportError("this theme file uses `include`; drop the whole extension folder's theme into ~/.toyon/themes or install it in VS Code and rescan");
30080
30201
  }
30081
- const base = slug(basename8(name).replace(/\.(json|jsonc)$/i, ""));
30202
+ const base = slug(basename9(name).replace(/\.(json|jsonc)$/i, ""));
30082
30203
  const theme = vscodeToTheme(json, {
30083
30204
  id: `file:${base}`,
30084
30205
  name: json?.name ?? base,
@@ -30113,7 +30234,7 @@ function readVscodeTheme(file, depth = 0) {
30113
30234
  return t;
30114
30235
  }
30115
30236
  function loadDir(dir) {
30116
- if (!existsSync27(dir))
30237
+ if (!existsSync28(dir))
30117
30238
  return [];
30118
30239
  const out = [];
30119
30240
  for (const f of readdirSync8(dir)) {
@@ -30135,7 +30256,7 @@ function loadDir(dir) {
30135
30256
  return out;
30136
30257
  }
30137
30258
  function discover(dir) {
30138
- if (!existsSync27(dir))
30259
+ if (!existsSync28(dir))
30139
30260
  return [];
30140
30261
  const out = [];
30141
30262
  let entries;
@@ -30148,7 +30269,7 @@ function discover(dir) {
30148
30269
  const extDir = join25(dir, name);
30149
30270
  const pkgFile = join25(extDir, "package.json");
30150
30271
  try {
30151
- if (!statSync10(extDir).isDirectory() || !existsSync27(pkgFile))
30272
+ if (!statSync10(extDir).isDirectory() || !existsSync28(pkgFile))
30152
30273
  continue;
30153
30274
  const pkg = JSON.parse(readFileSync17(pkgFile, "utf8"));
30154
30275
  const themes = pkg.contributes?.themes;
@@ -30157,7 +30278,7 @@ function discover(dir) {
30157
30278
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
30158
30279
  let nls = {};
30159
30280
  const nlsFile = join25(extDir, "package.nls.json");
30160
- if (existsSync27(nlsFile)) {
30281
+ if (existsSync28(nlsFile)) {
30161
30282
  try {
30162
30283
  nls = JSON.parse(readFileSync17(nlsFile, "utf8"));
30163
30284
  } catch {}
@@ -30228,7 +30349,7 @@ var REFRESH_EVERY_MS = 60000;
30228
30349
  var CHECK_EVERY_MS = 6 * 60 * 60000;
30229
30350
  var FIRST_CHECK_MS = 2 * 60000;
30230
30351
  var SETTLED_MS = 2 * 60000;
30231
- var RETRY_AFTER_MS = 24 * 60 * 60000;
30352
+ var RETRY_AFTER_MS2 = 24 * 60 * 60000;
30232
30353
  var INSTALL_SETTLE_MS = 60000;
30233
30354
 
30234
30355
  class UpdateService {
@@ -30380,7 +30501,7 @@ class UpdateService {
30380
30501
  if (this.needsInstall() && this.d.command(target) === null)
30381
30502
  return;
30382
30503
  const failed = this.d.state.updateFailed;
30383
- if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS)
30504
+ if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS2)
30384
30505
  return;
30385
30506
  if (this.d.busy()) {
30386
30507
  this.busyAt = this.now();
@@ -30571,7 +30692,7 @@ class BackendShare {
30571
30692
 
30572
30693
  // ../daemon/src/worktrees/cache.ts
30573
30694
  import { createHash as createHash4 } from "crypto";
30574
- import { existsSync as existsSync28, readFileSync as readFileSync18 } from "fs";
30695
+ import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
30575
30696
  import { mkdir as mkdir5, readdir as readdir2, rename, rm as rm4, stat as stat4, utimes, writeFile as writeFile3 } from "fs/promises";
30576
30697
  import { join as join26 } from "path";
30577
30698
 
@@ -30604,7 +30725,7 @@ var STALE_MS = 30 * 24 * 3600000;
30604
30725
  var TMP = ".tmp-";
30605
30726
  var MANIFEST = "manifest.json";
30606
30727
  function defaultTools(dir) {
30607
- const has = (...files) => files.some((f) => existsSync28(join26(dir, f)));
30728
+ const has = (...files) => files.some((f) => existsSync29(join26(dir, f)));
30608
30729
  const out = [];
30609
30730
  if (has("bun.lock", "bun.lockb"))
30610
30731
  out.push("bun --version");
@@ -30617,7 +30738,7 @@ function defaultTools(dir) {
30617
30738
  return out;
30618
30739
  }
30619
30740
  function withSidecars(root, rel) {
30620
- return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync28(join26(root, n)));
30741
+ return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync29(join26(root, n)));
30621
30742
  }
30622
30743
  var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
30623
30744
  async function listEntries(familyDir) {
@@ -30633,7 +30754,7 @@ async function listEntries(familyDir) {
30633
30754
  if (!m?.[1] || !m[2])
30634
30755
  continue;
30635
30756
  const dir = join26(familyDir, name);
30636
- if (!existsSync28(join26(dir, MANIFEST)))
30757
+ if (!existsSync29(join26(dir, MANIFEST)))
30637
30758
  continue;
30638
30759
  out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
30639
30760
  }
@@ -30671,7 +30792,7 @@ class ArtifactCache {
30671
30792
  }
30672
30793
  const restored = [];
30673
30794
  for (const rel of policy.paths) {
30674
- if (existsSync28(join26(wt.path, rel)))
30795
+ if (existsSync29(join26(wt.path, rel)))
30675
30796
  continue;
30676
30797
  const names = withSidecars(entry.dir, rel);
30677
30798
  if (names.length === 0)
@@ -30712,7 +30833,7 @@ class ArtifactCache {
30712
30833
  let tmp;
30713
30834
  let slot;
30714
30835
  try {
30715
- const present = policy.paths.filter((rel) => existsSync28(join26(wt.path, rel)));
30836
+ const present = policy.paths.filter((rel) => existsSync29(join26(wt.path, rel)));
30716
30837
  if (present.length === 0)
30717
30838
  return;
30718
30839
  const family = await this.family(wt, repo, policy);
@@ -30996,8 +31117,7 @@ var RESPONSE = {
30996
31117
  conflict: { does: "ask" },
30997
31118
  check: { does: "ask", once: true },
30998
31119
  command: { does: "offer" },
30999
- preview: { does: "offer" },
31000
- page: { does: "offer" }
31120
+ preview: { does: "offer" }
31001
31121
  };
31002
31122
  function lastAsked(entries) {
31003
31123
  for (let i = entries.length - 1;i >= 0; i--) {
@@ -31015,7 +31135,7 @@ function askedFor(entries, toolId) {
31015
31135
  function lastStop(entries) {
31016
31136
  for (let i = entries.length - 1;i >= 0; i--) {
31017
31137
  const e = entries[i].event;
31018
- if (e.type === "usage" || e.type === "session-info" || e.type === "page-error")
31138
+ if (e.type === "usage" || e.type === "session-info")
31019
31139
  continue;
31020
31140
  if (e.type === "turn-end" && e.stopReason === "interrupted")
31021
31141
  return { kind: "stopped" };
@@ -31025,21 +31145,6 @@ function lastStop(entries) {
31025
31145
  }
31026
31146
  return null;
31027
31147
  }
31028
- function lastPageErrors(entries) {
31029
- const errors = [];
31030
- for (let i = entries.length - 1;i >= 0; i--) {
31031
- const e = entries[i].event;
31032
- if (e.type === "page-error") {
31033
- errors.unshift(...e.errors);
31034
- continue;
31035
- }
31036
- if (e.type === "user-message" || e.type === "fix-asked")
31037
- break;
31038
- if (e.type === "tool-start" && isEditTool({ name: e.name, kind: e.kind }))
31039
- break;
31040
- }
31041
- return errors;
31042
- }
31043
31148
  function failureOf(entries, toolId) {
31044
31149
  let command;
31045
31150
  for (const { event } of entries) {
@@ -31104,17 +31209,6 @@ class FixService {
31104
31209
  if (!this.report(worktreeId, failure, { pressed: true }))
31105
31210
  throw new UserError("nothing runs on main");
31106
31211
  }
31107
- pressPage(worktreeId) {
31108
- this.d.state.requireWorktree(worktreeId);
31109
- const agent = this.d.runtime.agentFor(worktreeId);
31110
- if (!agent)
31111
- throw new UserError("worktree still starting; try again in a moment");
31112
- const errors = lastPageErrors(agent.transcript());
31113
- if (errors.length === 0)
31114
- throw new UserError("nothing the preview threw to fix");
31115
- if (!this.report(worktreeId, { kind: "page", errors }, { pressed: true }))
31116
- throw new UserError("nothing runs on main");
31117
- }
31118
31212
  goOn(worktreeId) {
31119
31213
  const wt = this.d.state.requireWorktree(worktreeId);
31120
31214
  if (isMain(wt))
@@ -31134,12 +31228,12 @@ class FixService {
31134
31228
  }
31135
31229
 
31136
31230
  // ../daemon/src/worktrees/handoff.ts
31137
- import { randomUUID as randomUUID3 } from "crypto";
31231
+ import { randomUUID as randomUUID2 } from "crypto";
31138
31232
  var MESSAGE_MAX = 20000;
31139
31233
  var TITLE_MAX2 = 200;
31140
31234
  var PROJECT_MAX = 200;
31141
31235
  var PER_TURN_MAX = 3;
31142
- var refused2 = (text) => ({ text, isError: true });
31236
+ var refused = (text) => ({ text, isError: true });
31143
31237
  function openProposals(entries) {
31144
31238
  const open = new Map;
31145
31239
  for (const { event } of entries) {
@@ -31222,7 +31316,7 @@ class HandoffService {
31222
31316
  const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
31223
31317
  const shaped = a && typeof a.project === "string" && typeof a.message === "string" && (a.title === undefined || typeof a.title === "string");
31224
31318
  if (!shaped)
31225
- return refused2("handoff takes { project: string, message: string, title?: string }.");
31319
+ return refused("handoff takes { project: string, message: string, title?: string }.");
31226
31320
  return this.propose(worktreeId, { project: a.project, message: a.message, ...a.title ? { title: a.title } : {} }, "agent");
31227
31321
  }
31228
31322
  propose(worktreeId, p, by) {
@@ -31230,31 +31324,31 @@ class HandoffService {
31230
31324
  if (!wt)
31231
31325
  throw new UserError("that chat is gone");
31232
31326
  if (p.project.length > PROJECT_MAX)
31233
- return refused2(`The project name is over ${PROJECT_MAX} characters.`);
31327
+ return refused(`The project name is over ${PROJECT_MAX} characters.`);
31234
31328
  if ((p.title ?? "").length > TITLE_MAX2)
31235
- return refused2(`The title is over ${TITLE_MAX2} characters; a few words.`);
31329
+ return refused(`The title is over ${TITLE_MAX2} characters; a few words.`);
31236
31330
  if (p.message.length > MESSAGE_MAX) {
31237
- return refused2(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
31331
+ return refused(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
31238
31332
  }
31239
31333
  const target = this.resolve(worktreeId, p.project);
31240
31334
  if ("refused" in target)
31241
- return refused2(target.refused);
31335
+ return refused(target.refused);
31242
31336
  const message = p.message.trim();
31243
31337
  if (!message)
31244
- return refused2("Write the message the other project's agent should start from.");
31338
+ return refused("Write the message the other project's agent should start from.");
31245
31339
  const agent = this.d.runtime.ensureAgent(wt).agent;
31246
31340
  const entries = agent.transcript();
31247
31341
  if (openProposals(entries).length > 0) {
31248
- return refused2("A handoff is already waiting for the person on this chat; finish your turn.");
31342
+ return refused("A handoff is already waiting for the person on this chat; finish your turn.");
31249
31343
  }
31250
31344
  if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
31251
- return refused2("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
31345
+ return refused("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
31252
31346
  }
31253
31347
  const title = p.title?.trim();
31254
31348
  const { repo } = target;
31255
31349
  agent.note({
31256
31350
  type: "handoff-proposed",
31257
- id: randomUUID3(),
31351
+ id: randomUUID2(),
31258
31352
  repo: { id: repo.id, name: repo.name, path: repo.path },
31259
31353
  message,
31260
31354
  ...title ? { title } : {},
@@ -31370,6 +31464,7 @@ function match(repos, q) {
31370
31464
  // ../daemon/src/worktrees/landing.ts
31371
31465
  var TAIL_CHARS = 400;
31372
31466
  var DIFF_CHARS = 2000;
31467
+ var CONTEXT_CHARS2 = 800;
31373
31468
  var CHECK_SLOTS = 2;
31374
31469
 
31375
31470
  class LandingService {
@@ -31445,14 +31540,7 @@ class LandingService {
31445
31540
  const repo = this.d.state.requireRepo(wt.repoId);
31446
31541
  this.judging.set(worktreeId, turn.at);
31447
31542
  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
- });
31543
+ const prompt = landPrompt(await this.landInput(wt, repo));
31456
31544
  let verdict = null;
31457
31545
  try {
31458
31546
  verdict = await this.d.judge(wt, prompt);
@@ -31525,6 +31613,25 @@ class LandingService {
31525
31613
  return;
31526
31614
  fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
31527
31615
  }
31616
+ async landInput(wt, repo) {
31617
+ const base = baseOf(repo);
31618
+ const entries = this.d.transcript(wt.id);
31619
+ const [diff, subjects, committedAt] = await Promise.all([
31620
+ diffSummary(wt.id, wt.path, base),
31621
+ recentSubjects(wt.path, base),
31622
+ lastOwnCommitAt(wt.id, wt.path, base)
31623
+ ]);
31624
+ const since = Math.max(committedAt, wt.lands?.at(-1)?.at ?? 0);
31625
+ return {
31626
+ title: wt.title,
31627
+ firstAsk: firstAskOf(entries),
31628
+ turns: turnsSince(entries, since),
31629
+ earlier: since > 0,
31630
+ diffStat: diff.own,
31631
+ committedStat: diff.committed,
31632
+ recentSubjects: subjects
31633
+ };
31634
+ }
31528
31635
  async hasWork(wt) {
31529
31636
  const repo = this.d.state.requireRepo(wt.repoId);
31530
31637
  const files = await statusFiles(wt.path);
@@ -31547,14 +31654,14 @@ class LandingService {
31547
31654
  };
31548
31655
  this.judging.set(worktreeId, opts.at);
31549
31656
  const started = Date.now();
31550
- this.d.worktrees.setLanding(worktreeId, { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept });
31657
+ const pending = { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept };
31658
+ this.d.worktrees.setLanding(worktreeId, pending);
31551
31659
  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
31660
  const command = repo.config.check?.trim();
31557
- if (command) {
31661
+ let checkDone = false;
31662
+ const checked = async () => {
31663
+ if (!command)
31664
+ return { check: "none" };
31558
31665
  const ceiling = timeoutFor(repo.config, "check");
31559
31666
  this.d.runs.drop(worktreeId, "check");
31560
31667
  const run = this.d.runs.begin(worktreeId, "check", {
@@ -31563,9 +31670,11 @@ class LandingService {
31563
31670
  });
31564
31671
  if (!await run.admitted)
31565
31672
  return;
31566
- if (!live())
31567
- return run.finish(null);
31568
- waited = Date.now() - started;
31673
+ if (!live()) {
31674
+ run.finish(null);
31675
+ return;
31676
+ }
31677
+ const waited = Date.now() - started;
31569
31678
  let r;
31570
31679
  try {
31571
31680
  r = await this.d.check(worktreeId, command, { quiet: opts.quiet, timeoutMs: ceiling, onSpawn: run.spawned });
@@ -31576,8 +31685,8 @@ class LandingService {
31576
31685
  run.finish(r.exit);
31577
31686
  if (!live())
31578
31687
  return;
31579
- ran = r;
31580
- check = r.exit === 0 ? "pass" : "fail";
31688
+ const check = r.exit === 0 ? "pass" : "fail";
31689
+ let checkTail;
31581
31690
  if (r.exit === "timeout") {
31582
31691
  checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
31583
31692
  `);
@@ -31585,19 +31694,14 @@ class LandingService {
31585
31694
  checkTail = tail(r.text) || `exit ${r.exit}`;
31586
31695
  else
31587
31696
  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
- });
31697
+ return { check, ran: r, waited, ...checkTail ? { checkTail } : {} };
31698
+ };
31699
+ const asked = async () => {
31700
+ if (!opts.ask || !this.d.judge)
31701
+ return { verdict: null, unanswered: false };
31702
+ const prompt = landPrompt({ ...await this.landInput(wt, repo), ...opts.note ? { note: opts.note } : {} });
31703
+ let verdict = null;
31704
+ let unanswered = false;
31601
31705
  try {
31602
31706
  verdict = await this.d.judge(wt, prompt);
31603
31707
  } catch (e) {
@@ -31608,16 +31712,29 @@ class LandingService {
31608
31712
  }
31609
31713
  if (!live())
31610
31714
  return;
31611
- }
31715
+ if (!checkDone && verdict) {
31716
+ this.recapTurn(worktreeId, verdict);
31717
+ this.d.worktrees.setLanding(worktreeId, { ...pending, ...messageOf(verdict) });
31718
+ }
31719
+ return { verdict, unanswered };
31720
+ };
31721
+ const [checkResult, askResult] = await Promise.all([
31722
+ checked().finally(() => {
31723
+ checkDone = true;
31724
+ }),
31725
+ asked()
31726
+ ]);
31727
+ if (!checkResult || !askResult || !live())
31728
+ return;
31729
+ const { check, checkTail, ran, waited = 0 } = checkResult;
31730
+ const { verdict, unanswered } = askResult;
31612
31731
  const landing = {
31613
31732
  at: opts.at,
31614
31733
  check,
31615
31734
  ...checkTail ? { checkTail } : {},
31616
31735
  ready: check !== "fail",
31617
31736
  ...kept,
31618
- ...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
31619
- ...verdict?.subject ? { subject: verdict.subject } : {},
31620
- ...verdict?.body ? { body: verdict.body } : {},
31737
+ ...verdict ? messageOf(verdict) : {},
31621
31738
  ...unanswered ? { unanswered: true } : {},
31622
31739
  fingerprint: await treeFingerprint(wt.path)
31623
31740
  };
@@ -31627,14 +31744,18 @@ class LandingService {
31627
31744
  if (!live())
31628
31745
  return;
31629
31746
  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 };
31747
+ if (verdict)
31748
+ this.recapTurn(worktreeId, verdict);
31633
31749
  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
31750
  this.d.worktrees.setLanding(worktreeId, landing);
31635
31751
  if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
31636
31752
  this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
31637
31753
  }
31754
+ recapTurn(worktreeId, verdict) {
31755
+ const record = this.d.state.worktree(worktreeId)?.lastTurn;
31756
+ if (record && verdict.recap)
31757
+ record.recap = { at: Date.now(), text: verdict.recap };
31758
+ }
31638
31759
  async sharedMigration(wt, repo) {
31639
31760
  if (!repo.services || keepsCopiesApart(repo.config))
31640
31761
  return;
@@ -31644,31 +31765,44 @@ class LandingService {
31644
31765
  return "this changes migrations, and the worktrees share the database";
31645
31766
  }
31646
31767
  }
31768
+ function messageOf(verdict) {
31769
+ return {
31770
+ ...!verdict.ready && verdict.why ? { why: verdict.why } : {},
31771
+ ...verdict.subject ? { subject: verdict.subject } : {},
31772
+ ...verdict.body ? { body: verdict.body } : {}
31773
+ };
31774
+ }
31647
31775
  function tail(text) {
31648
31776
  const t = text.replace(/\s+$/, "");
31649
31777
  return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
31650
31778
  }
31651
- async function diffSummary(path, defaultBr) {
31779
+ async function diffSummary(id, path, defaultBr) {
31652
31780
  const [base, dirty, untracked] = await Promise.all([
31653
31781
  git(path, "merge-base", "HEAD", defaultBr),
31654
31782
  git(path, "diff", "HEAD", "--stat"),
31655
31783
  git(path, "ls-files", "--others", "--exclude-standard")
31656
31784
  ]);
31785
+ if (!base.ok)
31786
+ log.warn(id, `landing question: no merge base with ${defaultBr}: ${base.err.trim()}`);
31657
31787
  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(`
31788
+ const own = [dirty.out ? `Uncommitted:
31789
+ ${dirty.out}` : "", untracked.out ? `New files:
31790
+ ${untracked.out}` : ""].filter(Boolean).join(`
31667
31791
 
31668
31792
  `);
31669
- return text.length > DIFF_CHARS ? `${text.slice(0, DIFF_CHARS)}
31793
+ return { own: cap(own, DIFF_CHARS), committed: cap(committed?.out ?? "", CONTEXT_CHARS2) };
31794
+ }
31795
+ function cap(text, max) {
31796
+ return text.length > max ? `${text.slice(0, max)}
31670
31797
  ...` : text;
31671
31798
  }
31799
+ async function lastOwnCommitAt(id, path, defaultBr) {
31800
+ const r = await git(path, "log", "-1", "--no-merges", "--format=%at", `${defaultBr}..HEAD`);
31801
+ if (!r.ok)
31802
+ log.warn(id, `landing question: could not read the branch's commits over ${defaultBr}: ${r.err.trim()}`);
31803
+ const seconds = Number(r.out.trim());
31804
+ return r.ok && Number.isFinite(seconds) ? seconds * 1000 : 0;
31805
+ }
31672
31806
  async function recentSubjects(path, defaultBr) {
31673
31807
  const r = await git(path, "log", "--format=%s", "-n", "5", defaultBr);
31674
31808
  return r.ok ? r.out.split(`
@@ -31787,7 +31921,7 @@ async function mergedBranches(repoPath, defaultBr) {
31787
31921
 
31788
31922
  // ../daemon/src/worktrees/discover.ts
31789
31923
  import { createHash as createHash5 } from "crypto";
31790
- import { basename as basename9 } from "path";
31924
+ import { basename as basename10 } from "path";
31791
31925
 
31792
31926
  // ../daemon/src/git/worktrees.ts
31793
31927
  function parseWorktreeList(out) {
@@ -31876,7 +32010,7 @@ function subtractKnown(repoId, listed, known) {
31876
32010
  rows.push({
31877
32011
  id: discoveredId(g.path),
31878
32012
  repoId,
31879
- name: g.branch ?? basename9(g.path),
32013
+ name: g.branch ?? basename10(g.path),
31880
32014
  path: g.path,
31881
32015
  ...g.branch ? { branch: g.branch } : {},
31882
32016
  ...g.locked ? { locked: true } : {},
@@ -32038,7 +32172,7 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
32038
32172
  }
32039
32173
 
32040
32174
  // ../daemon/src/worktrees/service.ts
32041
- import { cpSync, existsSync as existsSync33, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
32175
+ import { cpSync, existsSync as existsSync34, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
32042
32176
  import { rm as rm5 } from "fs/promises";
32043
32177
  import { dirname as dirname12, join as join30 } from "path";
32044
32178
 
@@ -32164,7 +32298,7 @@ async function dropLandRefs(repoPath, worktreeId) {
32164
32298
  }
32165
32299
 
32166
32300
  // ../daemon/src/git/land.ts
32167
- import { existsSync as existsSync29 } from "fs";
32301
+ import { existsSync as existsSync30 } from "fs";
32168
32302
  var STEP_TIMEOUT_MS = 10 * 60000;
32169
32303
  function rowOf(r) {
32170
32304
  return r.shown && r.toolId && r.command ? { toolId: r.toolId, command: r.command, text: r.text } : undefined;
@@ -32174,7 +32308,7 @@ var UNWATCHED = {
32174
32308
  step() {},
32175
32309
  git: (cwd, args) => stepRun(cwd, args)
32176
32310
  };
32177
- function refused3(what, r, n = 200) {
32311
+ function refused2(what, r, n = 200) {
32178
32312
  if (r.exit === "timeout") {
32179
32313
  const after = describeDuration(r.ceilingMs ?? STEP_TIMEOUT_MS);
32180
32314
  return `${what} gave up after ${after}${r.shown ? "; what it printed is on the chat" : ""}`;
@@ -32185,7 +32319,7 @@ function stepRefused(what, r, n = 200) {
32185
32319
  const row = typeof r.exit === "number" && r.hook ? rowOf(r) : undefined;
32186
32320
  return {
32187
32321
  ok: false,
32188
- message: refused3(what, r, n),
32322
+ message: refused2(what, r, n),
32189
32323
  ...row && r.hook ? { hook: { ...row, name: r.hook } } : {}
32190
32324
  };
32191
32325
  }
@@ -32295,7 +32429,7 @@ async function midway(worktreePath) {
32295
32429
  if (!r.ok)
32296
32430
  return null;
32297
32431
  const at = r.out.split(`
32298
- `).findIndex((file) => file && existsSync29(file));
32432
+ `).findIndex((file) => file && existsSync30(file));
32299
32433
  return MIDWAY[at]?.[1] ?? null;
32300
32434
  }
32301
32435
  async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
@@ -32403,17 +32537,41 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
32403
32537
  }
32404
32538
  return { ok: true, message: `merged ${branch} into ${defaultBr}` };
32405
32539
  }
32406
- async function squashMessage(worktreePath, defaultBr, suggested) {
32540
+ async function squashMessage(worktreePath, defaultBr, suggested, typed = false) {
32407
32541
  const n = await git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`);
32408
32542
  if (n.out === "1") {
32543
+ if (typed && suggested)
32544
+ return suggested;
32409
32545
  const one = await git(worktreePath, "log", "-1", "--format=%B");
32410
32546
  if (one.ok && one.out.trim())
32411
32547
  return one.out.trim();
32412
32548
  }
32413
- if (suggested)
32414
- return suggested;
32415
- const subjects = await git(worktreePath, "log", "--format=%s", `${defaultBr}..HEAD`);
32416
- return subjects.out.trim() || "land";
32549
+ const log = await git(worktreePath, "log", "--reverse", "--no-merges", "--format=%s", `${defaultBr}..HEAD`);
32550
+ const subjects = log.out.split(`
32551
+ `).filter(Boolean);
32552
+ const [first = "", ...rest] = (suggested ?? subjects.at(-1) ?? "land").split(`
32553
+ `);
32554
+ const subject = first.trim();
32555
+ const others = subjects.at(-1) === subject ? subjects.slice(0, -1) : subjects;
32556
+ const body = [
32557
+ rest.join(`
32558
+ `).trim(),
32559
+ others.length ? `Squashed with the branch's earlier commits:
32560
+ ${others.join(`
32561
+ `)}` : ""
32562
+ ].filter(Boolean).join(`
32563
+
32564
+ `);
32565
+ return body ? `${subject}
32566
+
32567
+ ${body}` : subject;
32568
+ }
32569
+ function splitMessage(text) {
32570
+ const [first = "", ...rest] = text.split(`
32571
+ `);
32572
+ const body = rest.join(`
32573
+ `).trim();
32574
+ return { subject: first.trim(), ...body ? { body } : {} };
32417
32575
  }
32418
32576
  async function fastForwardFetched(repoPath, defaultBr) {
32419
32577
  const current = await git(repoPath, "branch", "--show-current");
@@ -32525,7 +32683,7 @@ async function fetchBase(worktreePath, repo, w = UNWATCHED) {
32525
32683
  w.step(`fetching ${base}`);
32526
32684
  const f = await w.git(worktreePath, ["fetch", "--quiet", tracked.remote, tracked.merge]);
32527
32685
  if (!f.ok)
32528
- return { ok: false, message: refused3("fetch failed", f) };
32686
+ return { ok: false, message: refused2("fetch failed", f) };
32529
32687
  return { ok: true, message: `fetched ${base}` };
32530
32688
  }
32531
32689
  async function pushBranch(worktreePath, branch, w = UNWATCHED) {
@@ -32615,7 +32773,7 @@ function compareUrl(remoteUrl, base, branch) {
32615
32773
  }
32616
32774
 
32617
32775
  // ../daemon/src/git/turn.ts
32618
- import { copyFileSync, existsSync as existsSync30, rmSync as rmSync6 } from "fs";
32776
+ import { copyFileSync, existsSync as existsSync31, rmSync as rmSync6 } from "fs";
32619
32777
  import { join as join27 } from "path";
32620
32778
  var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
32621
32779
  var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
@@ -32641,7 +32799,7 @@ async function treeNow(wtPath, head, dirty) {
32641
32799
  const tree = await run(GIT, ["write-tree"], wtPath, env);
32642
32800
  return tree.ok && tree.out ? tree.out : null;
32643
32801
  };
32644
- if (!existsSync30(index))
32802
+ if (!existsSync31(index))
32645
32803
  was = undefined;
32646
32804
  const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
32647
32805
  if (was && paths && was.head === head) {
@@ -32656,9 +32814,9 @@ async function treeNow(wtPath, head, dirty) {
32656
32814
  }
32657
32815
  }
32658
32816
  synced.delete(wtPath);
32659
- if (!existsSync30(index)) {
32817
+ if (!existsSync31(index)) {
32660
32818
  const own = join27(index, "..", "index");
32661
- if (existsSync30(own))
32819
+ if (existsSync31(own))
32662
32820
  copyFileSync(own, index);
32663
32821
  else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
32664
32822
  return null;
@@ -32739,8 +32897,8 @@ async function dropTurnRefs(repoPath, worktreeId) {
32739
32897
  }
32740
32898
 
32741
32899
  // ../daemon/src/worktrees/archive.ts
32742
- import { existsSync as existsSync31, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync19, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
32743
- import { basename as basename10, dirname as dirname11, join as join28 } from "path";
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";
32901
+ import { basename as basename11, dirname as dirname11, join as join28 } from "path";
32744
32902
  class WorktreeArchive {
32745
32903
  dir;
32746
32904
  records = new Map;
@@ -32749,7 +32907,7 @@ class WorktreeArchive {
32749
32907
  mkdirSync8(dir, { recursive: true });
32750
32908
  for (const id of readdirSync9(dir)) {
32751
32909
  const file = this.filesOf(id).record;
32752
- if (!existsSync31(file))
32910
+ if (!existsSync32(file))
32753
32911
  continue;
32754
32912
  try {
32755
32913
  this.records.set(id, JSON.parse(readFileSync19(file, "utf8")));
@@ -32807,8 +32965,8 @@ class WorktreeArchive {
32807
32965
  planFile(id, rel) {
32808
32966
  if (!this.records.has(id) || !isPlanPath(rel))
32809
32967
  return null;
32810
- const file = join28(this.filesOf(id).plans, basename10(rel));
32811
- if (!existsSync31(file))
32968
+ const file = join28(this.filesOf(id).plans, basename11(rel));
32969
+ if (!existsSync32(file))
32812
32970
  return null;
32813
32971
  try {
32814
32972
  return readFileSync19(file, "utf8");
@@ -32849,7 +33007,7 @@ function summarize(r, repoId, transcript) {
32849
33007
  };
32850
33008
  }
32851
33009
  function lastUsage(transcript) {
32852
- if (!existsSync31(transcript))
33010
+ if (!existsSync32(transcript))
32853
33011
  return null;
32854
33012
  const lines = readFileSync19(transcript, "utf8").split(`
32855
33013
  `);
@@ -32865,7 +33023,7 @@ function lastUsage(transcript) {
32865
33023
  return null;
32866
33024
  }
32867
33025
  function firstPrompt(transcript) {
32868
- if (!existsSync31(transcript))
33026
+ if (!existsSync32(transcript))
32869
33027
  return;
32870
33028
  for (const line of readFileSync19(transcript, "utf8").split(`
32871
33029
  `)) {
@@ -32880,7 +33038,7 @@ function firstPrompt(transcript) {
32880
33038
  return;
32881
33039
  }
32882
33040
  function move(from, to) {
32883
- if (!existsSync31(from))
33041
+ if (!existsSync32(from))
32884
33042
  return;
32885
33043
  mkdirSync8(dirname11(to), { recursive: true });
32886
33044
  rmSync7(to, { recursive: true, force: true });
@@ -33051,8 +33209,8 @@ async function fillLandMarks(jobs, read, width = 8) {
33051
33209
  }
33052
33210
 
33053
33211
  // ../daemon/src/worktrees/spare.ts
33054
- import { existsSync as existsSync32 } from "fs";
33055
- import { basename as basename11, join as join29 } from "path";
33212
+ import { existsSync as existsSync33 } from "fs";
33213
+ import { basename as basename12, join as join29 } from "path";
33056
33214
  class SparePool {
33057
33215
  d;
33058
33216
  inflight = new Map;
@@ -33071,7 +33229,7 @@ class SparePool {
33071
33229
  if (!text.trim())
33072
33230
  return;
33073
33231
  const wt = this.d.state.worktree(boxId);
33074
- if (wt?.kind !== "spare" || !existsSync32(wt.path))
33232
+ if (wt?.kind !== "spare" || !existsSync33(wt.path))
33075
33233
  return;
33076
33234
  const agent = this.d.runtime.ensureAgent(wt).agent;
33077
33235
  if (agent.runningAgent === null)
@@ -33135,9 +33293,18 @@ class SparePool {
33135
33293
  const repoId = repo.id;
33136
33294
  let cut = () => {};
33137
33295
  const added = (async () => {
33138
- if (existsSync32(wt.path))
33296
+ if (existsSync33(wt.path))
33139
33297
  return;
33140
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
33298
+ const run = this.d.runs.begin(wt.id, "setup", {
33299
+ timeoutMs: timeoutFor(repo.config, "setup"),
33300
+ stage: "checking out"
33301
+ });
33302
+ try {
33303
+ await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", "--no-checkout", wt.path, baseOf(repo)));
33304
+ await withLock(`checkout:${repo.path}`, () => gitOrThrow(wt.path, "reset", "--hard"));
33305
+ } finally {
33306
+ run.finish(0);
33307
+ }
33141
33308
  })();
33142
33309
  const done = (async () => {
33143
33310
  this.d.runtime.markSetup(wt.id, true);
@@ -33185,7 +33352,7 @@ class SparePool {
33185
33352
  }
33186
33353
  log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
33187
33354
  this.d.carryDraft?.(wt.id, wt.repoId);
33188
- if (existsSync32(wt.path)) {
33355
+ if (existsSync33(wt.path)) {
33189
33356
  await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
33190
33357
  return;
33191
33358
  }
@@ -33229,40 +33396,70 @@ class SparePool {
33229
33396
  this.refreshing.set(repoId, run);
33230
33397
  await run;
33231
33398
  }
33232
- async claim(repoId, title, worktreeId) {
33399
+ async claim(repoId, title, worktreeId, stamps, prepare) {
33233
33400
  const wt = this.spareOf(repoId);
33234
33401
  if (!wt)
33235
33402
  return null;
33236
33403
  if (worktreeId && wt.id !== worktreeId)
33237
33404
  return null;
33238
33405
  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);
33406
+ if (!wasReady && !worktreeId)
33407
+ return null;
33249
33408
  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));
33409
+ const running = this.inflight.get(repoId);
33410
+ const was = { title: wt.title, branch: wt.branch, phase: wt.phase, lockfile: wt.lockfile };
33252
33411
  wt.kind = "worktree";
33253
33412
  delete wt.phase;
33254
33413
  delete wt.lockfile;
33255
- wt.branch = branch;
33414
+ wt.branch = `toyon/${basename12(wt.path)}`;
33256
33415
  wt.title = title;
33257
- wt.unnamed = true;
33258
33416
  wt.createdAt = Date.now();
33259
- if (wasReady)
33260
- fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
33417
+ wt.promptedAt = wt.createdAt;
33418
+ Object.assign(wt, Object.fromEntries(Object.entries(stamps).filter(([, v]) => v !== undefined)));
33261
33419
  const next = repo.needsSetup ? null : await this.record(repoId, repo);
33262
33420
  this.d.state.save();
33263
33421
  if (next)
33264
33422
  fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
33265
- return wt;
33423
+ const settled = (async () => {
33424
+ try {
33425
+ await running?.added;
33426
+ running?.cut();
33427
+ if (!existsSync33(wt.path))
33428
+ throw new Error("the worktree's directory is gone");
33429
+ await this.refreshing.get(repoId);
33430
+ await prepare(wt);
33431
+ await withRepoLock(repo.path, async () => {
33432
+ await gitOrThrow(wt.path, "branch", wt.branch);
33433
+ await gitOrThrow(wt.path, "symbolic-ref", "HEAD", `refs/heads/${wt.branch}`);
33434
+ });
33435
+ if (wasReady)
33436
+ fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
33437
+ } catch (e) {
33438
+ await this.unclaim(wt, was, stamps, next, e);
33439
+ throw e;
33440
+ }
33441
+ })();
33442
+ return { wt, settled };
33443
+ }
33444
+ async unclaim(wt, was, stamps, next, e) {
33445
+ wt.kind = "spare";
33446
+ wt.title = was.title;
33447
+ wt.branch = was.branch;
33448
+ if (was.phase)
33449
+ wt.phase = was.phase;
33450
+ if (was.lockfile)
33451
+ wt.lockfile = was.lockfile;
33452
+ for (const k of Object.keys(stamps))
33453
+ delete wt[k];
33454
+ delete wt.promptedAt;
33455
+ if (!existsSync33(wt.path)) {
33456
+ await this.rollBack(wt, e);
33457
+ return;
33458
+ }
33459
+ this.d.state.save();
33460
+ this.d.hub.emit("worktreesChanged");
33461
+ if (next)
33462
+ await this.d.discard(next.id).catch((re) => log.warn(wt.repoId, "reserved spare removal failed", re));
33266
33463
  }
33267
33464
  }
33268
33465
 
@@ -33340,7 +33537,7 @@ class Trunk {
33340
33537
  const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
33341
33538
  this.noteFetch(repo.id, f);
33342
33539
  if (!f.ok)
33343
- return { ok: false, message: refused3("fetch failed", f) };
33540
+ return { ok: false, message: refused2("fetch failed", f) };
33344
33541
  this.d.invalidateCounts();
33345
33542
  return this.follow(main.id);
33346
33543
  }
@@ -33400,23 +33597,8 @@ class TurnService {
33400
33597
  stoppedByHand = new Set;
33401
33598
  constructor(d) {
33402
33599
  this.d = d;
33403
- this.boot();
33404
33600
  d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
33405
33601
  }
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
33602
  onStatus(worktreeId, status) {
33421
33603
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
33422
33604
  this.lastStatus.set(worktreeId, status);
@@ -33503,6 +33685,7 @@ var idsOf = (rows, trunks) => [
33503
33685
  ...rows.map((r) => r.id),
33504
33686
  ...Object.values(trunks).map((t) => t.id)
33505
33687
  ];
33688
+ var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
33506
33689
  var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
33507
33690
  `, 1)[0])).join(" ");
33508
33691
 
@@ -33517,6 +33700,8 @@ class WorktreeService {
33517
33700
  archiving = new Map;
33518
33701
  restoring = new Map;
33519
33702
  going = new Set;
33703
+ arriving = new Set;
33704
+ firstPrompt = new Map;
33520
33705
  counts;
33521
33706
  handChecked = new Map;
33522
33707
  naming = new Set;
@@ -33531,7 +33716,7 @@ class WorktreeService {
33531
33716
  this.archive = new WorktreeArchive(d.paths.archiveDir);
33532
33717
  this.counts = new Counts({
33533
33718
  read: (id) => this.readCounts(id),
33534
- going: (id) => this.going.has(id)
33719
+ going: (id) => this.going.has(id) || this.arriving.has(id)
33535
33720
  });
33536
33721
  this.trunk = new Trunk({
33537
33722
  state: d.state,
@@ -33544,6 +33729,7 @@ class WorktreeService {
33544
33729
  hub: d.hub,
33545
33730
  runtime: d.runtime,
33546
33731
  paths: d.paths,
33732
+ runs: this.runs,
33547
33733
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
33548
33734
  teardown: (wt, repo) => this.teardown(wt, repo),
33549
33735
  discard: async (id) => {
@@ -33615,39 +33801,55 @@ class WorktreeService {
33615
33801
  const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
33616
33802
 
33617
33803
  `) : 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);
33804
+ const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
33805
+ const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
33806
+ let carriedIn;
33807
+ let release;
33808
+ const stamps = {
33809
+ createdBy: opts.createdBy,
33810
+ agent,
33811
+ mode: opts.mode,
33812
+ model: opts.model,
33813
+ effort: opts.effort,
33814
+ variant,
33815
+ from: opts.from,
33816
+ profile,
33817
+ unnamed: given ? undefined : true
33818
+ };
33819
+ const claimed = await this.spare.claim(repoId, title, opts.worktreeId, stamps, async (wt) => {
33820
+ const rt = this.d.runtime.ensureAgent(wt);
33821
+ release = rt.agent.hold();
33644
33822
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
33645
33823
  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;
33824
+ carriedIn = await this.carryMain(repo, wt, !!opts.carry);
33825
+ rt.agent.send(agentPrompt, { context: withCarry(context(wt.id), carriedIn), attachments });
33826
+ });
33827
+ if (claimed) {
33828
+ const { wt } = claimed;
33829
+ const rt = this.d.runtime.ensureAgent(wt);
33830
+ this.firstPrompt.set(wt.id, claimed.settled.then(() => {
33831
+ return;
33832
+ }, () => {
33833
+ return;
33834
+ }));
33835
+ this.d.hub.emit("worktreesChanged");
33836
+ try {
33837
+ await claimed.settled;
33838
+ } catch (e) {
33839
+ if (carriedIn && rt.agent.queueLength > 0)
33840
+ rt.agent.unqueue(0);
33841
+ release?.();
33842
+ throw e instanceof UserError ? e : new UserError(e instanceof Error ? e.message : String(e));
33843
+ } finally {
33844
+ this.firstPrompt.delete(wt.id);
33845
+ }
33846
+ release?.();
33847
+ if (restart)
33848
+ this.restartProcs(wt);
33849
+ this.nameOrAsk(wt, task, given, variant);
33850
+ if (carriedIn?.unmoved)
33851
+ throw new UserError(carriedIn.unmoved, { delivered: true });
33852
+ return wt;
33651
33853
  }
33652
33854
  if (!await isGitRepo(repo.path)) {
33653
33855
  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.`);
@@ -33730,7 +33932,7 @@ class WorktreeService {
33730
33932
  };
33731
33933
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
33732
33934
  const restored = (carried.ok ? carried.out.split(`
33733
- `).filter(Boolean) : []).filter((p) => !existsSync33(join30(wt.path, p)));
33935
+ `).filter(Boolean) : []).filter((p) => !existsSync34(join30(wt.path, p)));
33734
33936
  const applied = await git(wt.path, "stash", "apply", sha);
33735
33937
  if (applied.ok) {
33736
33938
  const ref = await entry();
@@ -33770,7 +33972,7 @@ class WorktreeService {
33770
33972
  const branch = kind === "pr" ? `pr/${number}` : ref;
33771
33973
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
33772
33974
  let slug = branchSlug(title) || "branch";
33773
- if (existsSync33(join30(this.d.paths.worktreesDir, repo.name, slug)))
33975
+ if (existsSync34(join30(this.d.paths.worktreesDir, repo.name, slug)))
33774
33976
  slug = `${slug}-${shortId().slice(0, 4)}`;
33775
33977
  const wtPath = join30(this.d.paths.worktreesDir, repo.name, slug);
33776
33978
  await withRepoLock(repo.path, async () => {
@@ -33808,8 +34010,9 @@ class WorktreeService {
33808
34010
  this.launch(wt, repo, repo.path);
33809
34011
  return wt;
33810
34012
  }
33811
- async requireDiscovered(repoId, repoPath, target) {
33812
- const rows = await discoverIn(repoId, repoPath, this.d.state.worktrees);
34013
+ async requireDiscovered(repoId, repoPath, target, except) {
34014
+ const known = this.d.state.worktrees.filter((w) => w.id !== except);
34015
+ const rows = await discoverIn(repoId, repoPath, known);
33813
34016
  const found = rows.find((r) => canonical(r.path) === target);
33814
34017
  if (!found)
33815
34018
  throw new UserError("that worktree is gone, or Toyon already has it");
@@ -33824,38 +34027,50 @@ class WorktreeService {
33824
34027
  const repoId = r.repoId;
33825
34028
  const repo = this.d.state.requireRepo(repoId);
33826
34029
  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
- });
34030
+ const pushed = this.discoveredById(worktreeId);
34031
+ if (pushed?.locked)
34032
+ throw new UserError(heldBy(pushed));
34033
+ if (!r.branch)
34034
+ throw new UserError(`${r.name} is detached: check out a branch in it first`);
34035
+ const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((w) => isInside(target, canonical(w.path)));
34036
+ if (enclosing)
34037
+ throw new UserError(`${r.name} sits inside ${enclosing.path}, which Toyon already manages`);
34038
+ const wt = {
34039
+ id: shortId(),
34040
+ repoId,
34041
+ path: r.path,
34042
+ branch: r.branch,
34043
+ kind: "worktree",
34044
+ proxyPort: await allocateProxyPort(),
34045
+ title: r.name,
34046
+ createdAt: Date.now(),
34047
+ agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
34048
+ parked: true,
34049
+ ...message ? { promptedAt: Date.now() } : {},
34050
+ ...createdBy ? { createdBy } : {}
34051
+ };
34052
+ const { rt, release } = this.listAhead(wt, message);
33853
34053
  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 });
34054
+ this.d.hub.emit("worktreesChanged");
34055
+ try {
34056
+ await withRepoLock(repo.path, async () => {
34057
+ const found = await this.requireDiscovered(repoId, repo.path, target, wt.id);
34058
+ if (found.locked)
34059
+ throw new UserError(heldBy(found));
34060
+ if (!found.branch)
34061
+ throw new UserError(`${found.name} is detached: check out a branch in it first`);
34062
+ wt.branch = found.branch;
34063
+ });
34064
+ } catch (e) {
34065
+ await this.unlist(wt, !!message);
34066
+ this.invalidateDiscovered();
34067
+ this.d.hub.emit("worktreesChanged");
34068
+ throw e;
34069
+ }
34070
+ this.d.state.save();
34071
+ fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path, { setupCommands: false }), "setup + start");
34072
+ rt.agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
34073
+ release();
33859
34074
  return wt;
33860
34075
  }
33861
34076
  async ownProcs(worktreeId, names) {
@@ -34025,7 +34240,7 @@ class WorktreeService {
34025
34240
  }
34026
34241
  async teardown(wt, repo) {
34027
34242
  const cmds = repo.config.teardown ?? [];
34028
- if (cmds.length === 0 || !existsSync33(wt.path))
34243
+ if (cmds.length === 0 || !existsSync34(wt.path))
34029
34244
  return;
34030
34245
  for (const cmd of cmds) {
34031
34246
  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") });
@@ -34075,7 +34290,7 @@ class WorktreeService {
34075
34290
  }
34076
34291
  keepPlans(wt) {
34077
34292
  const from = join30(wt.path, PLANS_DIR);
34078
- if (!existsSync33(from))
34293
+ if (!existsSync34(from))
34079
34294
  return;
34080
34295
  try {
34081
34296
  cpSync(from, this.plansScratch(wt.id), { recursive: true });
@@ -34269,79 +34484,126 @@ class WorktreeService {
34269
34484
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
34270
34485
  if (rec.auto)
34271
34486
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
34272
- 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
- });
34487
+ const path = existsSync34(old.path) ? join30(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
34298
34488
  const { variant: _variant, ...rest } = old;
34299
34489
  const wt = {
34300
34490
  ...rest,
34301
34491
  repoId: repo.id,
34302
34492
  path,
34303
- branch,
34493
+ branch: old.branch,
34304
34494
  proxyPort: await allocateProxyPort(),
34305
34495
  promptedAt: Date.now(),
34306
34496
  parked: true,
34307
34497
  ...createdBy ? { createdBy } : {}
34308
34498
  };
34499
+ const chat = { ...this.chatFiles(old.id), plans: this.plansScratch(old.id) };
34309
34500
  try {
34310
- this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join30(path, PLANS_DIR) });
34501
+ this.archive.take(old.id, chat);
34311
34502
  } catch (e) {
34312
34503
  log.warn(old.id, "could not move its chat back out of the archive", e);
34504
+ throw new UserError(`could not move ${old.title}'s chat back out of the archive`);
34313
34505
  }
34314
34506
  if (rec.sessionId && path === old.path)
34315
34507
  this.d.state.setSession(wt.id, rec.sessionId);
34508
+ this.arriving.add(wt.id);
34509
+ const { rt, release } = this.listAhead(wt, message);
34510
+ const run = this.runs.begin(wt.id, "setup", {
34511
+ timeoutMs: timeoutFor(repo.config, "setup"),
34512
+ stage: "checking out the branch"
34513
+ });
34514
+ this.d.hub.emit("worktreesChanged");
34515
+ this.d.hub.emit("archiveChanged", repo.id);
34516
+ let branch;
34517
+ try {
34518
+ branch = await withRepoLock(repo.path, () => this.checkOutArchived(repo, old, kept, path));
34519
+ } catch (e) {
34520
+ await this.takeBack(wt, rec, chat, run, !!message);
34521
+ throw e;
34522
+ }
34523
+ run.finish(0);
34524
+ this.arriving.delete(wt.id);
34525
+ wt.branch = branch;
34526
+ move(chat.plans, join30(path, PLANS_DIR));
34316
34527
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
34317
34528
  if (!dropped.ok)
34318
34529
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
34319
34530
  this.counts.stale(wt.id);
34320
- this.launch(wt, repo, repo.path);
34321
- this.d.runtime.ensureAgent(wt).agent.note({
34531
+ this.d.state.save();
34532
+ this.d.hub.emit("worktreesChanged");
34533
+ fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path), "setup + start");
34534
+ rt.agent.note({
34322
34535
  type: "restored",
34323
34536
  archivedAt: rec.archivedAt,
34324
34537
  branch,
34325
34538
  uncommitted: !!kept.snapshot,
34326
34539
  ts: Date.now()
34327
34540
  });
34328
- if (message)
34329
- this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
34330
- this.d.hub.emit("archiveChanged", repo.id);
34541
+ release();
34542
+ this.d.hub.emit("filesChanged", wt.id);
34331
34543
  return wt;
34332
34544
  }
34545
+ async checkOutArchived(repo, old, kept, path) {
34546
+ if (!await commitOf(repo.path, archiveRef(old.id))) {
34547
+ throw new UserError(`${old.title}'s kept commits are gone from git`);
34548
+ }
34549
+ let name = old.branch;
34550
+ let create = true;
34551
+ const tip = await commitOf(repo.path, `refs/heads/${name}`);
34552
+ if (tip && hasOwnBranch(old)) {
34553
+ name = `${name}-${shortId().slice(0, 3)}`;
34554
+ } else if (tip) {
34555
+ if (tip !== kept.head && kept.snapshot) {
34556
+ throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
34557
+ }
34558
+ create = false;
34559
+ }
34560
+ try {
34561
+ await checkOutKept(repo.path, path, kept, { name, create });
34562
+ } catch (e) {
34563
+ await git(repo.path, "worktree", "remove", "--force", path);
34564
+ if (create)
34565
+ await git(repo.path, "branch", "-D", name);
34566
+ throw e;
34567
+ }
34568
+ return name;
34569
+ }
34570
+ listAhead(wt, message) {
34571
+ this.d.runtime.markSetup(wt.id, true);
34572
+ this.d.state.addWorktree(wt);
34573
+ const rt = this.d.runtime.ensureAgent(wt);
34574
+ const release = rt.agent.hold();
34575
+ if (message)
34576
+ rt.agent.send(message.text, { attachments: message.attachments });
34577
+ return { rt, release };
34578
+ }
34579
+ async takeBack(wt, rec, chat, run, queued) {
34580
+ run.finish(0);
34581
+ await this.unlist(wt, queued);
34582
+ this.arriving.delete(wt.id);
34583
+ this.archive.put(rec, chat);
34584
+ this.d.hub.emit("worktreesChanged");
34585
+ this.d.hub.emit("archiveChanged", wt.repoId);
34586
+ }
34587
+ async unlist(wt, queued) {
34588
+ const agent = this.d.runtime.agentFor(wt.id);
34589
+ if (queued && agent && agent.queueLength > 0)
34590
+ agent.unqueue(0);
34591
+ await this.d.runtime.stop(wt.id);
34592
+ this.d.state.removeWorktree(wt.id, { refused: true });
34593
+ releasePort(wt.proxyPort);
34594
+ }
34333
34595
  async deleteArchived(archiveId) {
34334
34596
  const rec = this.archive.get(archiveId);
34335
34597
  if (!rec)
34336
34598
  return;
34337
34599
  const repo = this.repoOf(rec);
34338
34600
  const repoPath = repo?.path ?? rec.repoPath;
34339
- if (rec.kept && existsSync33(repoPath)) {
34601
+ if (rec.kept && existsSync34(repoPath)) {
34340
34602
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
34341
34603
  if (!r.ok)
34342
34604
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
34343
34605
  }
34344
- if (rec.worktree.lands?.length && existsSync33(repoPath))
34606
+ if (rec.worktree.lands?.length && existsSync34(repoPath))
34345
34607
  await dropLandRefs(repoPath, archiveId);
34346
34608
  this.archive.delete(archiveId);
34347
34609
  this.d.drafts?.drop(archiveId);
@@ -34450,8 +34712,8 @@ class WorktreeService {
34450
34712
  }
34451
34713
  async mirrorBaseIgnore(repo) {
34452
34714
  const src = join30(repo.path, ".gitignore");
34453
- const tracked = existsSync33(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
34454
- const mirror = existsSync33(src) && !tracked;
34715
+ const tracked = existsSync34(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
34716
+ const mirror = existsSync34(src) && !tracked;
34455
34717
  const lines = mirror ? readFileSync20(src, "utf8").split(`
34456
34718
  `).filter((l) => l.trim() !== "") : [];
34457
34719
  await excludeBlock(repo.path, BASE_IGNORE, lines);
@@ -34466,8 +34728,10 @@ class WorktreeService {
34466
34728
  }
34467
34729
  const srcNm = join30(depsSource, "node_modules");
34468
34730
  const dstNm = join30(wt.path, "node_modules");
34469
- if (existsSync33(srcNm) && !existsSync33(dstNm)) {
34731
+ if (existsSync34(srcNm) && !existsSync34(dstNm)) {
34732
+ const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
34470
34733
  const r = await cloneTree(srcNm, dstNm);
34734
+ run.finish(0);
34471
34735
  if (r.ok)
34472
34736
  this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
34473
34737
  else
@@ -34475,7 +34739,7 @@ class WorktreeService {
34475
34739
  }
34476
34740
  for (const f of LOCAL_CONFIG_FILES) {
34477
34741
  const src = join30(depsSource, f);
34478
- if (!existsSync33(src) || existsSync33(join30(wt.path, f)))
34742
+ if (!existsSync34(src) || existsSync34(join30(wt.path, f)))
34479
34743
  continue;
34480
34744
  const r = await run("cp", [src, join30(wt.path, f)], wt.path);
34481
34745
  if (r.ok)
@@ -34746,9 +35010,10 @@ ${wt.landing.body}` : s;
34746
35010
  const policy = landPolicy(repo.config);
34747
35011
  const own = hasOwnBranch(wt);
34748
35012
  const w = this.landWatch(wt);
34749
- const suggested = wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
35013
+ const typed = message?.trim();
35014
+ const suggested = typed || (wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
34750
35015
 
34751
- ${wt.landing.body}` : wt.landing.subject : undefined;
35016
+ ${wt.landing.body}` : wt.landing.subject : undefined);
34752
35017
  const base = baseOf(repo);
34753
35018
  if (policy.land === "pr") {
34754
35019
  if (wt.pr?.state === "merged" && !prTaken(wt)) {
@@ -34792,13 +35057,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34792
35057
  const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
34793
35058
  return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
34794
35059
  }
35060
+ const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
34795
35061
  const result = await openPr({
34796
35062
  worktreePath: wt.path,
34797
35063
  branch: wt.branch,
34798
35064
  defaultBr: repo.defaultBranch,
34799
35065
  base,
34800
- subject: wt.landing?.subject,
34801
- body: wt.landing?.body,
35066
+ subject: named?.subject,
35067
+ body: named?.body,
34802
35068
  automerge: policy.automerge,
34803
35069
  method: policy.merge
34804
35070
  }, w);
@@ -34827,7 +35093,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34827
35093
  if (!taken.ok)
34828
35094
  return taken;
34829
35095
  const mark = await landingMark(wt.path, base);
34830
- const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
35096
+ const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
34831
35097
  const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
34832
35098
  if (!landed.ok)
34833
35099
  return landed;
@@ -34878,7 +35144,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34878
35144
  sha = await commitOf(wt.path, base) ?? "";
34879
35145
  break;
34880
35146
  }
34881
- squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
35147
+ squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
34882
35148
  const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
34883
35149
  await this.headMoved(wt.id);
34884
35150
  if (!built.ok || !built.sha)
@@ -34930,8 +35196,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34930
35196
  async landedWhat(repo, wt, mark, opts) {
34931
35197
  if (opts.pr?.title)
34932
35198
  return [opts.pr.title];
34933
- const subject = opts.squash?.split(`
34934
- `)[0]?.trim();
35199
+ const subject = opts.squash ? splitMessage(opts.squash).subject : "";
34935
35200
  if (subject)
34936
35201
  return [subject];
34937
35202
  const read = await rangeSubjects(repo.path, mark.base, mark.tip);
@@ -34949,7 +35214,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
34949
35214
  const r = await git(wt.path, "reset", "--keep", base);
34950
35215
  if (!r.ok)
34951
35216
  log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
34952
- await this.headMoved(wt.id);
35217
+ this.counts.stale(wt.id);
35218
+ await this.counts.refresh([wt.id]);
34953
35219
  }
34954
35220
  async fetchBase(wt, repo, w) {
34955
35221
  if (!baseIsRemote(repo))
@@ -35238,7 +35504,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35238
35504
  await markTurn(wt.path, wt.id);
35239
35505
  }
35240
35506
  async gitStatus(worktreeId) {
35241
- if (this.going.has(worktreeId))
35507
+ if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35242
35508
  return null;
35243
35509
  const r = this.readable(worktreeId);
35244
35510
  if (!r)
@@ -35281,7 +35547,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35281
35547
  }
35282
35548
  if (r.wt && isMain)
35283
35549
  this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
35284
- if (this.going.has(worktreeId))
35550
+ if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
35285
35551
  return null;
35286
35552
  this.noteCounts(worktreeId, isMain, files.length, counts);
35287
35553
  return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
@@ -35327,6 +35593,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
35327
35593
  const wt = this.requireRun(worktreeId);
35328
35594
  if (wt.kind === "main")
35329
35595
  throw new UserError("nothing runs on main: the plus starts a worktree for it");
35596
+ await this.firstPrompt.get(worktreeId);
35330
35597
  const agent = this.d.runtime.agentFor(worktreeId);
35331
35598
  if (!agent)
35332
35599
  throw new UserError("worktree still starting; try again in a moment");
@@ -35532,7 +35799,8 @@ var {
35532
35799
  helperStub: HELPER_STUB,
35533
35800
  cliEntry: CLI_ENTRY,
35534
35801
  sourceRoot: SOURCE_ROOT,
35535
- packageJson: PACKAGE_JSON
35802
+ packageJson: PACKAGE_JSON,
35803
+ changelog: CHANGELOG
35536
35804
  } = locateAssets(here);
35537
35805
  var pruned = pruneAssets(SHELL_DIST);
35538
35806
  if (pruned > 0)
@@ -35540,7 +35808,7 @@ if (pruned > 0)
35540
35808
  var managed = await loadManaged();
35541
35809
  var paths = makePaths();
35542
35810
  ensureDirs(paths);
35543
- forwardTranscripts(paths.transcriptsDir);
35811
+ forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join31(paths.home, "transcripts-forwarded"));
35544
35812
  var token = loadOrCreateToken(paths);
35545
35813
  var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
35546
35814
  var helperApp = join31(paths.home, DAEMON_FILES.helper);
@@ -35581,43 +35849,39 @@ await linuxSandbox.refresh();
35581
35849
  var agents = loadAgentRegistry(paths.agentsFile, paths.agentsDir, linuxSandbox, managed.policy);
35582
35850
  if (state.defaultAgent !== undefined && !agents.get(state.defaultAgent))
35583
35851
  state.clearDefaultAgent();
35584
- var probe2 = new OptionProbe({
35852
+ var readiness2 = new AgentReadiness({
35585
35853
  infos: () => agents.infos(),
35586
35854
  require: (id) => agents.require(id),
35587
- connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `probe:${spec.id}`),
35855
+ connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `check:${spec.id}`),
35588
35856
  cwd: paths.scratchDir,
35589
- known: (id) => state.cachedOptions(id, "model").length > 0,
35857
+ options: (id, category) => state.cachedOptions(id, category),
35590
35858
  learned: (id, category, choices) => {
35591
35859
  if (state.setCachedOptions(id, category, choices))
35592
35860
  hub.emit("agentsChanged");
35861
+ },
35862
+ quick: (id) => {
35863
+ const spec = agents.get(id);
35864
+ return !!spec && !lacksQuickModel(spec, state);
35593
35865
  }
35594
35866
  });
35867
+ readiness2.onChange = () => hub.emit("agentsChanged");
35595
35868
  agents.onChange = () => {
35596
35869
  hub.emit("agentsChanged");
35597
- fireAndForget("agents", probe2.missing(), "read agent models");
35870
+ fireAndForget("agents", readiness2.checkMissing(), "read agent models");
35598
35871
  };
35599
- var accounts = new AgentAccounts({
35600
- require: (id) => agents.require(id),
35601
- connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `auth:${spec.id}`)
35602
- });
35603
- accounts.onChange = () => hub.emit("agentsChanged");
35604
35872
  var uploads = new UploadStore(paths.uploadsDir);
35605
35873
  var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
35606
35874
  var mcpPort = port;
35607
35875
  var tools = new ToolSet;
35608
35876
  var mcp = new ToyonMcp({ url: (id) => `http://127.0.0.1:${mcpPort}/mcp/${id}`, version: package_default.version, tools });
35609
- var pageErrors = new PageErrorService({
35610
- hub,
35611
- runtime: { agentFor: (id) => runtime.agentFor(id) },
35612
- known: (id) => state.worktree(id) !== undefined
35613
- });
35877
+ var pageErrors = new PageErrorService({ hub, known: (id) => state.worktree(id) !== undefined });
35614
35878
  var runtime = new RuntimeRegistry({
35615
35879
  hub,
35616
35880
  state,
35617
35881
  paths,
35618
35882
  agents,
35619
35883
  managed: managed.policy,
35620
- accounts,
35884
+ readiness: readiness2,
35621
35885
  attachments: attachments2,
35622
35886
  bridgeScript: () => bridge.get(),
35623
35887
  pageErrors: (id) => pageErrors.ambient(id),
@@ -35660,8 +35924,6 @@ var handoff2 = new HandoffService({
35660
35924
  create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts)
35661
35925
  });
35662
35926
  tools.add(handoff2);
35663
- var render = new RenderService({ runtime, pageErrors });
35664
- tools.add(render);
35665
35927
  var turns = new TurnService({
35666
35928
  state,
35667
35929
  hub,
@@ -35776,6 +36038,7 @@ var {
35776
36038
  port,
35777
36039
  token,
35778
36040
  shellDist: SHELL_DIST,
36041
+ changelog: CHANGELOG,
35779
36042
  version: package_default.version,
35780
36043
  noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
35781
36044
  startLink: () => helper?.startLink() ?? null,
@@ -35819,12 +36082,11 @@ var {
35819
36082
  landing,
35820
36083
  fix,
35821
36084
  pageErrors,
35822
- render,
35823
36085
  handoff: handoff2,
35824
36086
  mcp,
35825
36087
  themes,
35826
36088
  agents,
35827
- accounts,
36089
+ readiness: readiness2,
35828
36090
  attachments: attachments2,
35829
36091
  self,
35830
36092
  afterLand,
@@ -35855,7 +36117,7 @@ idle.boot();
35855
36117
  runs.boot();
35856
36118
  landing.boot();
35857
36119
  fireAndForget("cache", cache.sweep(), "cache sweep");
35858
- fireAndForget("agents", agents.installMissing().then(() => probe2.missing()), "agent adapter install");
36120
+ fireAndForget("agents", agents.installMissing().then(() => readiness2.checkMissing()), "agent adapter install");
35859
36121
  var repoArg = process.argv[2];
35860
36122
  if (repoArg) {
35861
36123
  try {