toyon 0.18.0 → 0.20.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 (220) hide show
  1. package/dist/CHANGELOG.md +74 -0
  2. package/dist/bridge.js +1 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/daemon.js +2279 -731
  5. package/dist/shell/assets/{Editor-CU91PIeX.js → Editor--Z8ktewC.js} +3 -3
  6. package/dist/shell/assets/Editor--Z8ktewC.js.br +0 -0
  7. package/dist/shell/assets/Editor--Z8ktewC.js.gz +0 -0
  8. package/dist/shell/assets/Editor-CB2SckMT.css.br +0 -0
  9. package/dist/shell/assets/Editor-CB2SckMT.css.gz +0 -0
  10. package/dist/shell/assets/XTerm-6GBZ9nXN.css.br +0 -0
  11. package/dist/shell/assets/XTerm-6GBZ9nXN.css.gz +0 -0
  12. package/dist/shell/assets/{XTerm-y7vQoOEu.js → XTerm-DsUQ0Xew.js} +1 -1
  13. package/dist/shell/assets/XTerm-DsUQ0Xew.js.br +0 -0
  14. package/dist/shell/assets/XTerm-DsUQ0Xew.js.gz +0 -0
  15. package/dist/shell/assets/abap-DLDM7-KI.js.br +0 -0
  16. package/dist/shell/assets/abap-DLDM7-KI.js.gz +0 -0
  17. package/dist/shell/assets/apex-DNDY2TF8.js.br +0 -0
  18. package/dist/shell/assets/apex-DNDY2TF8.js.gz +0 -0
  19. package/dist/shell/assets/bat-BwHxbl9M.js.br +0 -0
  20. package/dist/shell/assets/bat-BwHxbl9M.js.gz +0 -0
  21. package/dist/shell/assets/bicep-CFznDFnq.js.br +0 -0
  22. package/dist/shell/assets/bicep-CFznDFnq.js.gz +0 -0
  23. package/dist/shell/assets/cameligo-Bf6VGUru.js.br +0 -0
  24. package/dist/shell/assets/cameligo-Bf6VGUru.js.gz +0 -0
  25. package/dist/shell/assets/clojure-Dnu-v4kV.js.br +0 -0
  26. package/dist/shell/assets/clojure-Dnu-v4kV.js.gz +0 -0
  27. package/dist/shell/assets/coffee-Bd8akH9Z.js.br +0 -0
  28. package/dist/shell/assets/coffee-Bd8akH9Z.js.gz +0 -0
  29. package/dist/shell/assets/cpp-BbWJElDN.js.br +0 -0
  30. package/dist/shell/assets/cpp-BbWJElDN.js.gz +0 -0
  31. package/dist/shell/assets/csharp-Co3qMtFm.js.br +0 -0
  32. package/dist/shell/assets/csharp-Co3qMtFm.js.gz +0 -0
  33. package/dist/shell/assets/csp-D-4FJmMZ.js.br +0 -0
  34. package/dist/shell/assets/csp-D-4FJmMZ.js.gz +0 -0
  35. package/dist/shell/assets/css-DdJfP1eB.js.br +0 -0
  36. package/dist/shell/assets/css-DdJfP1eB.js.gz +0 -0
  37. package/dist/shell/assets/css.worker-CqwWTjeG.js.br +0 -0
  38. package/dist/shell/assets/css.worker-CqwWTjeG.js.gz +0 -0
  39. package/dist/shell/assets/{cssMode-B7sFzM6K.js → cssMode-BjAkCPrp.js} +1 -1
  40. package/dist/shell/assets/cssMode-BjAkCPrp.js.br +0 -0
  41. package/dist/shell/assets/cssMode-BjAkCPrp.js.gz +0 -0
  42. package/dist/shell/assets/cypher-cTPe9QuQ.js.br +0 -0
  43. package/dist/shell/assets/cypher-cTPe9QuQ.js.gz +0 -0
  44. package/dist/shell/assets/dart-BOtBlQCF.js.br +0 -0
  45. package/dist/shell/assets/dart-BOtBlQCF.js.gz +0 -0
  46. package/dist/shell/assets/dockerfile-BG73LgW2.js.br +0 -0
  47. package/dist/shell/assets/dockerfile-BG73LgW2.js.gz +0 -0
  48. package/dist/shell/assets/ecl-BEgZUVRK.js.br +0 -0
  49. package/dist/shell/assets/ecl-BEgZUVRK.js.gz +0 -0
  50. package/dist/shell/assets/editor.worker-DCKwvLbM.js.br +0 -0
  51. package/dist/shell/assets/editor.worker-DCKwvLbM.js.gz +0 -0
  52. package/dist/shell/assets/elixir-BkW5O-1t.js.br +0 -0
  53. package/dist/shell/assets/elixir-BkW5O-1t.js.gz +0 -0
  54. package/dist/shell/assets/flow9-BeJ5waoc.js.br +0 -0
  55. package/dist/shell/assets/flow9-BeJ5waoc.js.gz +0 -0
  56. package/dist/shell/assets/{freemarker2-ChTLQoDC.js → freemarker2-BZCFxXwq.js} +1 -1
  57. package/dist/shell/assets/freemarker2-BZCFxXwq.js.br +0 -0
  58. package/dist/shell/assets/freemarker2-BZCFxXwq.js.gz +0 -0
  59. package/dist/shell/assets/fsharp-PahG7c26.js.br +0 -0
  60. package/dist/shell/assets/fsharp-PahG7c26.js.gz +0 -0
  61. package/dist/shell/assets/go-acbASCJo.js.br +0 -0
  62. package/dist/shell/assets/go-acbASCJo.js.gz +0 -0
  63. package/dist/shell/assets/{grammar-xXcxTc3V.js → grammar-CQ1uaD6_.js} +1 -1
  64. package/dist/shell/assets/grammar-CQ1uaD6_.js.br +0 -0
  65. package/dist/shell/assets/grammar-CQ1uaD6_.js.gz +0 -0
  66. package/dist/shell/assets/graphql-BxJiqAUM.js.br +0 -0
  67. package/dist/shell/assets/graphql-BxJiqAUM.js.gz +0 -0
  68. package/dist/shell/assets/{handlebars-DPJSozQx.js → handlebars-G1zJeDLP.js} +1 -1
  69. package/dist/shell/assets/handlebars-G1zJeDLP.js.br +0 -0
  70. package/dist/shell/assets/handlebars-G1zJeDLP.js.gz +0 -0
  71. package/dist/shell/assets/hcl-DtV1sZF8.js.br +0 -0
  72. package/dist/shell/assets/hcl-DtV1sZF8.js.gz +0 -0
  73. package/dist/shell/assets/{html-Cog2rLcD.js → html-CUHSCFMy.js} +1 -1
  74. package/dist/shell/assets/html-CUHSCFMy.js.br +0 -0
  75. package/dist/shell/assets/html-CUHSCFMy.js.gz +0 -0
  76. package/dist/shell/assets/html.worker-DhJ4VZMp.js.br +0 -0
  77. package/dist/shell/assets/html.worker-DhJ4VZMp.js.gz +0 -0
  78. package/dist/shell/assets/{htmlMode-Xm5_ujKb.js → htmlMode-gpMedtd9.js} +1 -1
  79. package/dist/shell/assets/htmlMode-gpMedtd9.js.br +0 -0
  80. package/dist/shell/assets/htmlMode-gpMedtd9.js.gz +0 -0
  81. package/dist/shell/assets/index-B3Alj0WH.css +1 -0
  82. package/dist/shell/assets/index-B3Alj0WH.css.br +0 -0
  83. package/dist/shell/assets/index-B3Alj0WH.css.gz +0 -0
  84. package/dist/shell/assets/index-VVd_p5tJ.js +278 -0
  85. package/dist/shell/assets/index-VVd_p5tJ.js.br +0 -0
  86. package/dist/shell/assets/index-VVd_p5tJ.js.gz +0 -0
  87. package/dist/shell/assets/java-CXBNlu9o.js.br +0 -0
  88. package/dist/shell/assets/java-CXBNlu9o.js.gz +0 -0
  89. package/dist/shell/assets/{javascript-Dcv6lw7b.js → javascript-BHWH8gQN.js} +1 -1
  90. package/dist/shell/assets/json.worker-BF9eV5mS.js.br +0 -0
  91. package/dist/shell/assets/json.worker-BF9eV5mS.js.gz +0 -0
  92. package/dist/shell/assets/{jsonMode-D5_V0TW5.js → jsonMode-DyV5nvVT.js} +1 -1
  93. package/dist/shell/assets/jsonMode-DyV5nvVT.js.br +0 -0
  94. package/dist/shell/assets/jsonMode-DyV5nvVT.js.gz +0 -0
  95. package/dist/shell/assets/julia-cl7-CwDS.js.br +0 -0
  96. package/dist/shell/assets/julia-cl7-CwDS.js.gz +0 -0
  97. package/dist/shell/assets/kotlin-s7OhZKlX.js.br +0 -0
  98. package/dist/shell/assets/kotlin-s7OhZKlX.js.gz +0 -0
  99. package/dist/shell/assets/less-9HpZscsL.js.br +0 -0
  100. package/dist/shell/assets/less-9HpZscsL.js.gz +0 -0
  101. package/dist/shell/assets/lexon-OrD6JF1K.js.br +0 -0
  102. package/dist/shell/assets/lexon-OrD6JF1K.js.gz +0 -0
  103. package/dist/shell/assets/{liquid-C_zXIF5F.js → liquid-Dy3tUgRC.js} +1 -1
  104. package/dist/shell/assets/liquid-Dy3tUgRC.js.br +0 -0
  105. package/dist/shell/assets/liquid-Dy3tUgRC.js.gz +0 -0
  106. package/dist/shell/assets/{lspLanguageFeatures-CkiTdLmp.js → lspLanguageFeatures-CB_Ow-w5.js} +1 -1
  107. package/dist/shell/assets/lspLanguageFeatures-CB_Ow-w5.js.br +0 -0
  108. package/dist/shell/assets/lspLanguageFeatures-CB_Ow-w5.js.gz +0 -0
  109. package/dist/shell/assets/lua-Cyyb5UIc.js.br +0 -0
  110. package/dist/shell/assets/lua-Cyyb5UIc.js.gz +0 -0
  111. package/dist/shell/assets/m3-B8OfTtLu.js.br +0 -0
  112. package/dist/shell/assets/m3-B8OfTtLu.js.gz +0 -0
  113. package/dist/shell/assets/markdown-BFxVWTOG.js.br +0 -0
  114. package/dist/shell/assets/markdown-BFxVWTOG.js.gz +0 -0
  115. package/dist/shell/assets/{mdx-gvbILQkr.js → mdx-DD5jgco_.js} +1 -1
  116. package/dist/shell/assets/mdx-DD5jgco_.js.br +0 -0
  117. package/dist/shell/assets/mdx-DD5jgco_.js.gz +0 -0
  118. package/dist/shell/assets/mips-CiqrrVzr.js.br +0 -0
  119. package/dist/shell/assets/mips-CiqrrVzr.js.gz +0 -0
  120. package/dist/shell/assets/msdax-DmeGPVcC.js.br +0 -0
  121. package/dist/shell/assets/msdax-DmeGPVcC.js.gz +0 -0
  122. package/dist/shell/assets/mysql-C_tMU-Nz.js.br +0 -0
  123. package/dist/shell/assets/mysql-C_tMU-Nz.js.gz +0 -0
  124. package/dist/shell/assets/objective-c-BDtDVThU.js.br +0 -0
  125. package/dist/shell/assets/objective-c-BDtDVThU.js.gz +0 -0
  126. package/dist/shell/assets/pascal-vHIfCaH5.js.br +0 -0
  127. package/dist/shell/assets/pascal-vHIfCaH5.js.gz +0 -0
  128. package/dist/shell/assets/pascaligo-DtZ0uQbO.js.br +0 -0
  129. package/dist/shell/assets/pascaligo-DtZ0uQbO.js.gz +0 -0
  130. package/dist/shell/assets/perl-Ub6l9XKa.js.br +0 -0
  131. package/dist/shell/assets/perl-Ub6l9XKa.js.gz +0 -0
  132. package/dist/shell/assets/pgsql-BlNEE0v7.js.br +0 -0
  133. package/dist/shell/assets/pgsql-BlNEE0v7.js.gz +0 -0
  134. package/dist/shell/assets/php-BBUBE1dy.js.br +0 -0
  135. package/dist/shell/assets/php-BBUBE1dy.js.gz +0 -0
  136. package/dist/shell/assets/pla-DSh2-awV.js.br +0 -0
  137. package/dist/shell/assets/pla-DSh2-awV.js.gz +0 -0
  138. package/dist/shell/assets/postiats-CocnycG-.js.br +0 -0
  139. package/dist/shell/assets/postiats-CocnycG-.js.gz +0 -0
  140. package/dist/shell/assets/powerquery-tScXyioY.js.br +0 -0
  141. package/dist/shell/assets/powerquery-tScXyioY.js.gz +0 -0
  142. package/dist/shell/assets/powershell-COWaemsV.js.br +0 -0
  143. package/dist/shell/assets/powershell-COWaemsV.js.gz +0 -0
  144. package/dist/shell/assets/protobuf-Brw8urJB.js.br +0 -0
  145. package/dist/shell/assets/protobuf-Brw8urJB.js.gz +0 -0
  146. package/dist/shell/assets/pug-8SOpv6rk.js.br +0 -0
  147. package/dist/shell/assets/pug-8SOpv6rk.js.gz +0 -0
  148. package/dist/shell/assets/{python-CZTrOkh_.js → python-BvLKUXll.js} +1 -1
  149. package/dist/shell/assets/python-BvLKUXll.js.br +0 -0
  150. package/dist/shell/assets/python-BvLKUXll.js.gz +0 -0
  151. package/dist/shell/assets/qsharp-Bw9ernYp.js.br +0 -0
  152. package/dist/shell/assets/qsharp-Bw9ernYp.js.gz +0 -0
  153. package/dist/shell/assets/r-j7ic8hl3.js.br +0 -0
  154. package/dist/shell/assets/r-j7ic8hl3.js.gz +0 -0
  155. package/dist/shell/assets/{razor-DowlBj1i.js → razor-B2tUlbzR.js} +1 -1
  156. package/dist/shell/assets/razor-B2tUlbzR.js.br +0 -0
  157. package/dist/shell/assets/razor-B2tUlbzR.js.gz +0 -0
  158. package/dist/shell/assets/redis-Bu5POkcn.js.br +0 -0
  159. package/dist/shell/assets/redis-Bu5POkcn.js.gz +0 -0
  160. package/dist/shell/assets/redshift-Bs9aos_-.js.br +0 -0
  161. package/dist/shell/assets/redshift-Bs9aos_-.js.gz +0 -0
  162. package/dist/shell/assets/restructuredtext-CqXO7rUv.js.br +0 -0
  163. package/dist/shell/assets/restructuredtext-CqXO7rUv.js.gz +0 -0
  164. package/dist/shell/assets/ruby-zBfavPgS.js.br +0 -0
  165. package/dist/shell/assets/ruby-zBfavPgS.js.gz +0 -0
  166. package/dist/shell/assets/rust-BzKRNQWT.js.br +0 -0
  167. package/dist/shell/assets/rust-BzKRNQWT.js.gz +0 -0
  168. package/dist/shell/assets/sb-BBc9UKZt.js.br +0 -0
  169. package/dist/shell/assets/sb-BBc9UKZt.js.gz +0 -0
  170. package/dist/shell/assets/scala-D9hQfWCl.js.br +0 -0
  171. package/dist/shell/assets/scala-D9hQfWCl.js.gz +0 -0
  172. package/dist/shell/assets/scheme-BPhDTwHR.js.br +0 -0
  173. package/dist/shell/assets/scheme-BPhDTwHR.js.gz +0 -0
  174. package/dist/shell/assets/scss-CBJaRo0y.js.br +0 -0
  175. package/dist/shell/assets/scss-CBJaRo0y.js.gz +0 -0
  176. package/dist/shell/assets/shell-DiJ1NA_G.js.br +0 -0
  177. package/dist/shell/assets/shell-DiJ1NA_G.js.gz +0 -0
  178. package/dist/shell/assets/solidity-Db0IVjzk.js.br +0 -0
  179. package/dist/shell/assets/solidity-Db0IVjzk.js.gz +0 -0
  180. package/dist/shell/assets/sophia-CnS9iZB_.js.br +0 -0
  181. package/dist/shell/assets/sophia-CnS9iZB_.js.gz +0 -0
  182. package/dist/shell/assets/sparql-CJmd_6j2.js.br +0 -0
  183. package/dist/shell/assets/sparql-CJmd_6j2.js.gz +0 -0
  184. package/dist/shell/assets/sql-ClhHkBeG.js.br +0 -0
  185. package/dist/shell/assets/sql-ClhHkBeG.js.gz +0 -0
  186. package/dist/shell/assets/st-CHwy0fLd.js.br +0 -0
  187. package/dist/shell/assets/st-CHwy0fLd.js.gz +0 -0
  188. package/dist/shell/assets/swift-Bqt4WxQ4.js.br +0 -0
  189. package/dist/shell/assets/swift-Bqt4WxQ4.js.gz +0 -0
  190. package/dist/shell/assets/systemverilog-Bs9z6M-B.js.br +0 -0
  191. package/dist/shell/assets/systemverilog-Bs9z6M-B.js.gz +0 -0
  192. package/dist/shell/assets/tcl-Dm6ycUr_.js.br +0 -0
  193. package/dist/shell/assets/tcl-Dm6ycUr_.js.gz +0 -0
  194. package/dist/shell/assets/ts.worker-DTZAwq0V.js.br +0 -0
  195. package/dist/shell/assets/ts.worker-DTZAwq0V.js.gz +0 -0
  196. package/dist/shell/assets/{tsMode-BNl2cV_K.js → tsMode-DMuNnqM7.js} +1 -1
  197. package/dist/shell/assets/tsMode-DMuNnqM7.js.br +0 -0
  198. package/dist/shell/assets/tsMode-DMuNnqM7.js.gz +0 -0
  199. package/dist/shell/assets/twig-Csy3S7wG.js.br +0 -0
  200. package/dist/shell/assets/twig-Csy3S7wG.js.gz +0 -0
  201. package/dist/shell/assets/typespec-Btyra-wh.js.br +0 -0
  202. package/dist/shell/assets/typespec-Btyra-wh.js.gz +0 -0
  203. package/dist/shell/assets/vb-Db0cS2oM.js.br +0 -0
  204. package/dist/shell/assets/vb-Db0cS2oM.js.gz +0 -0
  205. package/dist/shell/assets/wasm-inlined-CG6Dc4jp.js.br +0 -0
  206. package/dist/shell/assets/wasm-inlined-CG6Dc4jp.js.gz +0 -0
  207. package/dist/shell/assets/wgsl-BTesnYfV.js.br +0 -0
  208. package/dist/shell/assets/wgsl-BTesnYfV.js.gz +0 -0
  209. package/dist/shell/assets/{xml-DIiuHFHw.js → xml-DdYgb5a2.js} +1 -1
  210. package/dist/shell/assets/xml-DdYgb5a2.js.br +0 -0
  211. package/dist/shell/assets/xml-DdYgb5a2.js.gz +0 -0
  212. package/dist/shell/assets/{yaml-CBnMyn4j.js → yaml-BDujJdoL.js} +1 -1
  213. package/dist/shell/assets/yaml-BDujJdoL.js.br +0 -0
  214. package/dist/shell/assets/yaml-BDujJdoL.js.gz +0 -0
  215. package/dist/shell/index.html +11 -8
  216. package/dist/shell/manifest.json +13 -0
  217. package/dist/shell/sw.js +117 -22
  218. package/package.json +2 -2
  219. package/dist/shell/assets/index-BpW1Kufz.css +0 -1
  220. package/dist/shell/assets/index-CdY8WF4P.js +0 -270
package/dist/daemon.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // ../daemon/src/index.ts
3
3
  import { rmSync as rmSync9, writeFileSync as writeFileSync14 } from "fs";
4
4
  import { homedir as homedir15 } from "os";
5
- import { dirname as dirname14, join as join33 } from "path";
5
+ import { dirname as dirname14, join as join34 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // ../shared/src/protocol/events.ts
@@ -11,7 +11,25 @@ var CHECK_TOOL = "check";
11
11
  var TOOL_SEARCH = "ToolSearch";
12
12
  var ASK_TOOL = "AskUserQuestion";
13
13
  var TOYON_MCP_SERVER = "toyon";
14
- var HANDOFF_TOOL = "handoff";
14
+ var SPLIT_TOOL = "split";
15
+ var WORKTREES_TOOL = "worktrees";
16
+ function coalesce(entries) {
17
+ const out = [];
18
+ for (const entry of entries) {
19
+ const last = out.at(-1);
20
+ const { event } = entry;
21
+ if (event.type === "text-delta" && last?.event.type === "text-delta" && last.event.messageId === event.messageId) {
22
+ out[out.length - 1] = { seq: last.seq, event: { ...last.event, text: last.event.text + event.text } };
23
+ } else if (event.type === "thinking-delta" && last?.event.type === "thinking-delta") {
24
+ out[out.length - 1] = { seq: last.seq, event: { type: "thinking-delta", text: last.event.text + event.text } };
25
+ } else if (event.type === "tool-delta" && last?.event.type === "tool-delta" && last.event.toolId === event.toolId) {
26
+ out[out.length - 1] = { seq: last.seq, event: { ...event, text: last.event.text + event.text } };
27
+ } else {
28
+ out.push(entry);
29
+ }
30
+ }
31
+ return out;
32
+ }
15
33
 
16
34
  // ../shared/src/agent-tools.ts
17
35
  var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
@@ -28,6 +46,15 @@ function isWrittenKind(kind) {
28
46
  function writingCall(call, input) {
29
47
  return (isWrittenKind(call.kind) || call.name === ASK_TOOL || call.name === TOOL_SEARCH) && emptyInput(input);
30
48
  }
49
+ var MCP_NAME = /^(?:mcp__(.+?)__(.+)|([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+))$/;
50
+ function mcpTool(name) {
51
+ const m = MCP_NAME.exec(name);
52
+ if (!m)
53
+ return null;
54
+ const server = m[1] ?? m[3] ?? "";
55
+ const tool = m[2] ?? m[4] ?? "";
56
+ return { server, tool: tool.replace(/[_-]+/g, " ").trim() };
57
+ }
31
58
  function emptyInput(input) {
32
59
  if (!input || typeof input !== "object")
33
60
  return true;
@@ -39,12 +66,6 @@ function emptyInput(input) {
39
66
  var DEFAULT_TIMEOUT_MS = 10 * 60000;
40
67
  var DEFAULT_PERMISSION_MODE = "auto";
41
68
  var LAND_SUBJECTS_MAX = 8;
42
- function siblingsOf(wt, rows) {
43
- if (!wt.variant)
44
- return [];
45
- const group = wt.variant.group;
46
- return rows.filter((w) => w.id !== wt.id && w.variant?.group === group);
47
- }
48
69
  function hasOwnBranch(wt) {
49
70
  return wt.branch.startsWith("toyon/");
50
71
  }
@@ -100,69 +121,63 @@ function canGraft(wt) {
100
121
  // ../shared/src/railOrder.ts
101
122
  var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
102
123
  var tierOf = (w) => isLead(w) ? 0 : 1;
103
- var railUnitOf = (w) => w.variant ? `group:${w.variant.group}` : `row:${w.id}`;
104
124
  function railOrder(rows) {
105
- const units = [];
106
- const groups = new Map;
107
- for (const row of rows) {
108
- const w = row.worktree;
109
- const key = railUnitOf(w);
110
- const unit = groups.get(key);
111
- if (unit) {
112
- unit.rows.push(row);
113
- unit.at = Math.max(unit.at, sentAt(w));
114
- continue;
115
- }
116
- const fresh = { tier: tierOf(w), at: sentAt(w), rows: [row] };
117
- units.push(fresh);
118
- groups.set(key, fresh);
119
- }
120
- units.sort((a, b) => a.tier - b.tier || b.at - a.at);
121
- return units.flatMap((u) => u.rows.length > 1 ? u.rows.sort((a, b) => (a.worktree.variant?.index ?? 0) - (b.worktree.variant?.index ?? 0)) : u.rows);
125
+ return [...rows].sort((a, b) => tierOf(a.worktree) - tierOf(b.worktree) || sentAt(b.worktree) - sentAt(a.worktree));
122
126
  }
123
127
 
124
128
  // ../shared/src/archiveRule.ts
125
129
  var ARCHIVE_KEEP_RECENT = 5;
130
+ function heldBy(w, f) {
131
+ const c = f.counts(w.id);
132
+ if (c.dirty === undefined || c.ahead === undefined)
133
+ return "its changes are not counted yet";
134
+ if (c.dirty > 0)
135
+ return "it has uncommitted changes";
136
+ if (c.ahead > 0)
137
+ return "it has commits not on main";
138
+ if (f.drafted(w.id))
139
+ return "unsent text waits in its composer";
140
+ if (f.pending(w.id))
141
+ return "its agent is still busy";
142
+ const end = w.lastTurn?.end;
143
+ if (!end)
144
+ return "its agent has not run yet";
145
+ if (end === "stopped")
146
+ return "its last turn was stopped";
147
+ if (end === "failed")
148
+ return "its last turn failed";
149
+ if (end === "asking")
150
+ return "its agent is waiting on an answer";
151
+ if (isUnseen(w))
152
+ return "its last reply is unread";
153
+ if (w.planned)
154
+ return "it showed a plan";
155
+ if (w.pr?.state === "open")
156
+ return "its pull request is still open";
157
+ return null;
158
+ }
126
159
  function archiveReason(wt, f) {
127
- const group = [wt, ...siblingsOf(wt, f.rows)];
128
- const decided = group.some((w) => !!w.lands?.length);
129
- const lost = decided && !wt.lands?.length;
130
- if (decided) {
131
- if (!nothingToKeep(wt, f, lost))
132
- return null;
133
- } else if (!group.every((w) => nothingToKeep(w, f, false)))
160
+ if (!nothingToKeep(wt, f))
134
161
  return null;
135
- if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
162
+ if (rowsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
136
163
  return null;
137
- const what = lost ? "sibling landed" : wt.landed ? "landed" : "no changes";
164
+ const what = wt.landed ? "landed" : "no changes";
138
165
  return `${what}, ${UNOPENED}${duration(f.afterMs)}`;
139
166
  }
140
167
  var UNOPENED = "not opened in ";
141
- function nothingToKeep(w, f, lost) {
168
+ function nothingToKeep(w, f) {
142
169
  if (w.kind !== "worktree" || !hasOwnBranch(w))
143
170
  return false;
144
- if (w.lastTurn?.end !== "done" || isUnseen(w))
145
- return false;
146
- if (w.planned || w.pr?.state === "open")
147
- return false;
148
- if (f.viewed(w.id) || f.pending(w.id) || f.drafted(w.id))
171
+ if (f.viewed(w.id))
149
172
  return false;
150
173
  if (f.now - (w.viewedAt ?? w.createdAt) < f.afterMs)
151
174
  return false;
152
- const c = f.counts(w.id);
153
- return c.dirty === 0 && (lost ? c.ahead !== undefined : c.ahead === 0);
175
+ return heldBy(w, f) === null;
154
176
  }
155
- function unitsAhead(wt, rows) {
177
+ function rowsAhead(wt, rows) {
156
178
  const tasks = railOrder(rows.filter((w) => w.kind === "worktree").map((worktree) => ({ worktree })));
157
- const mine = railUnitOf(wt);
158
- const seen = new Set;
159
- for (const { worktree: w } of tasks) {
160
- const unit = railUnitOf(w);
161
- if (unit === mine)
162
- return seen.size;
163
- seen.add(unit);
164
- }
165
- return seen.size;
179
+ const at = tasks.findIndex(({ worktree: w }) => w.id === wt.id);
180
+ return at < 0 ? tasks.length : at;
166
181
  }
167
182
  function duration(ms) {
168
183
  if (ms >= 60 * 60000)
@@ -536,6 +551,9 @@ function prTaken(wt) {
536
551
  function shipNoun(op) {
537
552
  return { land: "a land", commit: "a commit", "sync-main": "a sync", "pull-main": "a pull" }[op];
538
553
  }
554
+ function prStanding(wt) {
555
+ return prTaken(wt) && !wt.landed ? undefined : wt.pr;
556
+ }
539
557
 
540
558
  // ../../node_modules/.bun/zod@4.5.4/node_modules/zod/v4/core/util.js
541
559
  function getEnumValues(entries) {
@@ -6602,7 +6620,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6602
6620
  }
6603
6621
 
6604
6622
  // ../shared/src/protocol/limits.ts
6605
- var PROTOCOL_VERSION = 119;
6623
+ var PROTOCOL_VERSION = 133;
6606
6624
  var PAGE_ERROR_MAX_CHARS = 2000;
6607
6625
  var PAGE_TITLE_MAX_CHARS = 1000;
6608
6626
  var FILE_MAX_CHARS = 5000000;
@@ -6612,7 +6630,6 @@ var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
6612
6630
  var PASTE_MAX_CHARS = 1e5;
6613
6631
  var FILE_INLINE_CHARS = 20000;
6614
6632
  var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
6615
- var VARIANTS_MAX = 3;
6616
6633
 
6617
6634
  // ../shared/src/paste.ts
6618
6635
  var PREVIEW_CHARS = 80;
@@ -6677,6 +6694,19 @@ var elementTraitsSchema = object({
6677
6694
  attrs: array(tuple([string2().max(40), string2().max(200)])).max(12)
6678
6695
  });
6679
6696
 
6697
+ // ../shared/src/protocol/stamps.ts
6698
+ var STAMP_PARTS = ["rows", "repos", "visits", "themes"];
6699
+ var PART = /^[0-9a-f]*$/;
6700
+ function parseKnown(q) {
6701
+ if (!q)
6702
+ return null;
6703
+ const parts = q.split(".");
6704
+ if (parts.length !== STAMP_PARTS.length || !parts.every((p) => p.length <= 32 && PART.test(p)))
6705
+ return null;
6706
+ return { rows: parts[0], repos: parts[1], visits: parts[2], themes: parts[3] };
6707
+ }
6708
+ var sameStamp = (held, now) => held !== "" && held === now;
6709
+
6680
6710
  // ../shared/src/protocol/ws.ts
6681
6711
  var id = string2().min(1).max(200);
6682
6712
  var permissionModeSchema = _enum(["auto", "ask", "plan"]);
@@ -6848,7 +6878,7 @@ var themePrefsSchema = object({
6848
6878
  dark: string2()
6849
6879
  });
6850
6880
  var clientMsgSchema = discriminatedUnion("t", [
6851
- object({ t: literal("subscribe"), worktreeId: id }),
6881
+ object({ t: literal("subscribe"), worktreeId: id, since: number2().int().min(0).optional() }),
6852
6882
  object({ t: literal("unsubscribe"), worktreeId: id }),
6853
6883
  object({ t: literal("view"), worktreeId: id.nullable() }),
6854
6884
  object({
@@ -6866,7 +6896,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6866
6896
  repoId: id,
6867
6897
  worktreeId: id.optional(),
6868
6898
  prompt,
6869
- variants: number2().int().min(1).max(VARIANTS_MAX).optional(),
6870
6899
  context: ambient.optional(),
6871
6900
  attachments,
6872
6901
  agent: id.optional(),
@@ -6877,15 +6906,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6877
6906
  carry: boolean2().optional(),
6878
6907
  boxId
6879
6908
  }),
6880
- object({
6881
- t: literal("batch-worktrees"),
6882
- repoId: id,
6883
- prompt,
6884
- agent: id.optional(),
6885
- model: string2().max(200).optional(),
6886
- effort: string2().max(100).optional(),
6887
- boxId
6888
- }),
6889
6909
  object({ t: literal("set-worktree-mode"), worktreeId: id, mode: permissionModeSchema }),
6890
6910
  object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
6891
6911
  object({ t: literal("set-worktree-effort"), worktreeId: id, effort: string2().max(100) }),
@@ -6962,6 +6982,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6962
6982
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6963
6983
  object({ t: literal("reveal-path"), path: relPath }),
6964
6984
  object({ t: literal("reveal-attachment"), worktreeId: id, file: string2().min(1).max(120) }),
6985
+ object({ t: literal("reveal-transcript"), worktreeId: id }),
6965
6986
  object({ t: literal("stop-agent"), worktreeId: id }),
6966
6987
  object({ t: literal("seen"), worktreeId: id }),
6967
6988
  object({ t: literal("mark-unread"), worktreeId: id }),
@@ -6978,7 +6999,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6978
6999
  object({ t: literal("forget-visit"), repoId: id, path: routePath }),
6979
7000
  object({ t: literal("page-error"), worktreeId: id, message: string2().min(1).max(PAGE_ERROR_MAX_CHARS) }),
6980
7001
  object({ t: literal("page-loaded"), worktreeId: id }),
6981
- object({ t: literal("pick-variant"), worktreeId: id }),
6982
7002
  object({
6983
7003
  t: literal("unqueue"),
6984
7004
  worktreeId: id,
@@ -7047,14 +7067,21 @@ var clientMsgSchema = discriminatedUnion("t", [
7047
7067
  choiceId: string2().max(200),
7048
7068
  note: string2().max(1e4).optional()
7049
7069
  }),
7070
+ object({ t: literal("pieces-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
7071
+ object({
7072
+ t: literal("pieces-fate"),
7073
+ worktreeId: id,
7074
+ id: string2().max(64),
7075
+ pieceId: string2().max(16),
7076
+ fate: _enum(["here", "worktree", "send", "skip"])
7077
+ }),
7050
7078
  object({
7051
- t: literal("handoff-answer"),
7079
+ t: literal("pieces-answer"),
7052
7080
  worktreeId: id,
7053
7081
  id: string2().max(64),
7054
7082
  go: boolean2(),
7055
7083
  note: string2().max(5000).optional()
7056
7084
  }),
7057
- object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
7058
7085
  object({ t: literal("agent-logout"), agent: id }),
7059
7086
  object({ t: literal("agent-check"), agent: id }),
7060
7087
  object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
@@ -8526,7 +8553,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8526
8553
  // ../daemon/package.json
8527
8554
  var package_default = {
8528
8555
  name: "@toyon/daemon",
8529
- version: "0.18.0",
8556
+ version: "0.20.0",
8530
8557
  private: true,
8531
8558
  type: "module",
8532
8559
  main: "src/index.ts",
@@ -13354,7 +13381,7 @@ class ToolSet {
13354
13381
  }
13355
13382
  var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
13356
13383
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
13357
- var BODY_MAX_BYTES = 64 * 1024;
13384
+ var BODY_MAX_BYTES = 256 * 1024;
13358
13385
  var PARSE_ERROR = -32700;
13359
13386
  var INVALID_PARAMS = -32602;
13360
13387
  var METHOD_NOT_FOUND = -32601;
@@ -13613,10 +13640,14 @@ class AgentReadiness {
13613
13640
  const e = this.known.get(info.id);
13614
13641
  const models = this.d.options(info.id, "model");
13615
13642
  const efforts = this.d.options(info.id, "thought_level");
13643
+ const defaultModel = this.d.runs(info.id, "model");
13644
+ const defaultEffort = this.d.runs(info.id, "thought_level");
13616
13645
  return {
13617
13646
  ...info,
13618
13647
  ...models.length > 0 ? { models } : {},
13619
13648
  ...efforts.length > 0 ? { efforts } : {},
13649
+ ...defaultModel !== undefined ? { defaultModel } : {},
13650
+ ...defaultEffort !== undefined ? { defaultEffort } : {},
13620
13651
  ...this.d.quick(info.id) ? { quick: true } : {},
13621
13652
  ...e?.status ? { auth: e.status } : {},
13622
13653
  ...e?.canLogout ? { canLogout: true } : {},
@@ -13685,8 +13716,9 @@ class AgentReadiness {
13685
13716
  });
13686
13717
  found.canLogout = supportsLogout(init.agentCapabilities);
13687
13718
  const s = await link.conn.agent.request(methods.agent.session.new, { cwd: this.d.cwd, mcpServers: [] });
13688
- for (const [category, opt] of readOptions(s.configOptions))
13689
- this.d.learned(agentId, category, opt.choices);
13719
+ for (const [category, opt] of readOptions(s.configOptions)) {
13720
+ this.d.learned(agentId, category, opt.choices, opt.current);
13721
+ }
13690
13722
  if (init.agentCapabilities?.sessionCapabilities?.close) {
13691
13723
  await link.conn.agent.request(methods.agent.session.close, { sessionId: s.sessionId }).catch((e) => {
13692
13724
  log.debug(tag, `session/close failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -14375,7 +14407,13 @@ var DENIED_COMMANDS = [
14375
14407
  "git branch -D",
14376
14408
  "git branch -d",
14377
14409
  "git branch --delete",
14378
- "git worktree",
14410
+ "git worktree add",
14411
+ "git worktree remove",
14412
+ "git worktree move",
14413
+ "git worktree prune",
14414
+ "git worktree lock",
14415
+ "git worktree unlock",
14416
+ "git worktree repair",
14379
14417
  "gh pr create",
14380
14418
  "gh pr merge"
14381
14419
  ];
@@ -14425,6 +14463,17 @@ function opencodeEnv() {
14425
14463
  };
14426
14464
  }
14427
14465
 
14466
+ // ../daemon/src/agent/quick.ts
14467
+ function quickModelFor(spec, offered, current) {
14468
+ const quick = spec.quickModel;
14469
+ if (typeof quick === "function")
14470
+ return quick(offered, current);
14471
+ return quick && offered.includes(quick) ? quick : undefined;
14472
+ }
14473
+ function quickModelNamed(name) {
14474
+ return (offered) => offered.includes(name) ? name : offered.find((id) => id.includes(name));
14475
+ }
14476
+
14428
14477
  // ../daemon/src/agent/sandbox.ts
14429
14478
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync4 } from "fs";
14430
14479
  import { homedir as homedir2, tmpdir } from "os";
@@ -14591,6 +14640,12 @@ function sameAsLast(cwd, rel, markdown) {
14591
14640
  }
14592
14641
  }
14593
14642
  async function writePlanDoc(cwd, markdown) {
14643
+ const rel = writePlanDocNow(cwd, markdown);
14644
+ if (rel)
14645
+ await excludePlans(cwd);
14646
+ return rel;
14647
+ }
14648
+ function writePlanDocNow(cwd, markdown) {
14594
14649
  const dir = join5(cwd, PLANS_DIR);
14595
14650
  const last = lastNumber(dir);
14596
14651
  if (last > 0) {
@@ -14608,9 +14663,18 @@ async function writePlanDoc(cwd, markdown) {
14608
14663
  log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
14609
14664
  return null;
14610
14665
  }
14611
- await excludePlans(cwd);
14666
+ excludePlans(cwd).catch((e) => log.warn(cwd, "could not exclude the plans folder", e));
14612
14667
  return rel;
14613
14668
  }
14669
+ function listDoc(pieces) {
14670
+ return `# Worktrees
14671
+
14672
+ ${pieces.map((p) => `## ${p.title}
14673
+
14674
+ ${p.plan.trim()}
14675
+ `).join(`
14676
+ `)}`;
14677
+ }
14614
14678
  function planEdited(cwd, rel, proposed) {
14615
14679
  const file = join5(cwd, rel);
14616
14680
  if (!existsSync7(file))
@@ -14728,9 +14792,8 @@ var BUILTIN_AGENTS = [
14728
14792
  name: "Claude Code",
14729
14793
  short: "Claude",
14730
14794
  builtin: true,
14731
- run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.84.0", bin: "claude-agent-acp" },
14795
+ run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.95.0", bin: "claude-agent-acp" },
14732
14796
  env: {
14733
- CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: "1",
14734
14797
  ...cloud.enabled ? { NO_BROWSER: "1" } : {}
14735
14798
  },
14736
14799
  terminalLogins: {
@@ -14743,7 +14806,7 @@ var BUILTIN_AGENTS = [
14743
14806
  systemPrompt: "meta-append",
14744
14807
  modes: { plan: "plan", build: "default" },
14745
14808
  sideMeta: { claudeCode: { options: { tools: [], persistSession: false } } },
14746
- quickModel: "haiku",
14809
+ quickModel: quickModelNamed("haiku"),
14747
14810
  loginHint: "Claude is not logged in"
14748
14811
  },
14749
14812
  {
@@ -15560,6 +15623,65 @@ function unifiedDiff(oldText, newText, context = 3) {
15560
15623
  `);
15561
15624
  }
15562
15625
 
15626
+ // ../daemon/src/agent/acp/wakeup.ts
15627
+ var WAKEUP_TOOL = "ScheduleWakeup";
15628
+ var MIN_DELAY_S = 60;
15629
+ var MAX_DELAY_S = 3600;
15630
+ var GRACE_MS2 = 60000;
15631
+ function wakeupAt(name, input, now = Date.now()) {
15632
+ if (name !== WAKEUP_TOOL)
15633
+ return null;
15634
+ const rec = input && typeof input === "object" ? input : null;
15635
+ if (!rec || rec.stop === true)
15636
+ return null;
15637
+ const delay = rec.delaySeconds;
15638
+ if (typeof delay !== "number" || !Number.isFinite(delay))
15639
+ return null;
15640
+ return now + Math.min(MAX_DELAY_S, Math.max(MIN_DELAY_S, delay)) * 1000;
15641
+ }
15642
+
15643
+ class Wakeups {
15644
+ deps;
15645
+ held = new Map;
15646
+ constructor(deps) {
15647
+ this.deps = deps;
15648
+ }
15649
+ get size() {
15650
+ return this.held.size;
15651
+ }
15652
+ get next() {
15653
+ let next = null;
15654
+ for (const w of this.held.values())
15655
+ if (next === null || w.until < next)
15656
+ next = w.until;
15657
+ return next;
15658
+ }
15659
+ start(toolId, until) {
15660
+ const wait = Math.max(0, until - Date.now()) + (this.deps.graceMs ?? GRACE_MS2);
15661
+ const timer = setTimeout(() => this.finish(toolId, "no turn came; the row stops waiting", false), wait);
15662
+ timer.unref?.();
15663
+ this.held.set(toolId, { until, timer });
15664
+ this.deps.onChange?.();
15665
+ }
15666
+ wake(how) {
15667
+ for (const toolId of [...this.held.keys()])
15668
+ this.finish(toolId, how, false);
15669
+ }
15670
+ endAll(reason) {
15671
+ for (const toolId of [...this.held.keys()])
15672
+ this.finish(toolId, reason, true);
15673
+ }
15674
+ finish(toolId, output, isError) {
15675
+ const w = this.held.get(toolId);
15676
+ if (!w)
15677
+ return;
15678
+ clearTimeout(w.timer);
15679
+ this.held.delete(toolId);
15680
+ this.deps.note({ type: "tool-end", toolId, output, isError });
15681
+ this.deps.onChange?.();
15682
+ }
15683
+ }
15684
+
15563
15685
  // ../daemon/src/agent/acp/map.ts
15564
15686
  function spawnOf(meta) {
15565
15687
  const claude = asRecord2(asRecord2(meta).claudeCode);
@@ -15587,7 +15709,10 @@ function toolSearchTitle(rawInput) {
15587
15709
  const names = /^select:(.*)$/.exec(query.trim());
15588
15710
  if (!names)
15589
15711
  return `looked for a tool: ${query.trim()}`;
15590
- const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15712
+ const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
15713
+ const mcp = mcpTool(s);
15714
+ return mcp ? `${mcp.server} ${mcp.tool}` : s;
15715
+ }).join(", ");
15591
15716
  return loaded ? `loaded ${loaded}` : "";
15592
15717
  }
15593
15718
  var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
@@ -15600,11 +15725,8 @@ function toyonTool(update) {
15600
15725
  }
15601
15726
  return null;
15602
15727
  }
15603
- function toyonTitle(tool, rawInput, title) {
15604
- if (tool !== HANDOFF_TOOL)
15605
- return title && toyonTool({ name: title }) === null ? title : tool;
15606
- const project = asRecord2(rawInput).project;
15607
- return typeof project === "string" && project.trim() ? `Continue in ${project.trim()}` : "Continue in another project";
15728
+ function toyonTitle(tool, title) {
15729
+ return title && toyonTool({ name: title }) === null ? title : tool;
15608
15730
  }
15609
15731
  function heading(update) {
15610
15732
  const name = typeof update.name === "string" ? update.name : "";
@@ -15616,7 +15738,7 @@ function heading(update) {
15616
15738
  }
15617
15739
  const own = toyonTool(update);
15618
15740
  if (own)
15619
- return { name: own, title: toyonTitle(own, update.rawInput, update.title), kind: "other" };
15741
+ return { name: own, title: toyonTitle(own, update.title), kind: "other" };
15620
15742
  return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
15621
15743
  }
15622
15744
  function endOfAsk(req, res, memos) {
@@ -15641,18 +15763,21 @@ function release(toolId, memo) {
15641
15763
  delete memo.held;
15642
15764
  if (!emptyInput(memo.input))
15643
15765
  memo.writing = false;
15766
+ memo.released = true;
15644
15767
  return memo.ended ? null : { type: "tool-update", toolId, ...held };
15645
15768
  }
15646
15769
  function preempted(memos) {
15647
15770
  for (const memo of memos.values()) {
15648
- if (!memo.ended && memo.writing && !memo.parent)
15771
+ if (!memo.ended && !memo.parent)
15649
15772
  memo.cut = true;
15650
15773
  }
15651
15774
  }
15652
15775
  function abandoned(memos, parent, opening = false) {
15653
15776
  const out = [];
15654
15777
  for (const [toolId, memo] of memos) {
15655
- if (memo.ended || !memo.writing || memo.parent !== parent)
15778
+ if (memo.ended || memo.parent !== parent)
15779
+ continue;
15780
+ if (!(memo.writing || memo.cut || memo.released && !opening))
15656
15781
  continue;
15657
15782
  if (opening && memo.held && !memo.cut) {
15658
15783
  const sent = release(toolId, memo);
@@ -15661,11 +15786,11 @@ function abandoned(memos, parent, opening = false) {
15661
15786
  continue;
15662
15787
  }
15663
15788
  memo.ended = true;
15664
- out.push({ type: "tool-end", toolId });
15789
+ out.push({ type: "tool-end", toolId, cut: true });
15665
15790
  }
15666
15791
  return out;
15667
15792
  }
15668
- function mapUpdate(update, memos, tag, sink, background) {
15793
+ function mapUpdate(update, memos, tag, sink, background, wakeup) {
15669
15794
  switch (update.sessionUpdate) {
15670
15795
  case "agent_message_chunk": {
15671
15796
  const { parentToolId } = spawnOf(update._meta);
@@ -15712,7 +15837,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15712
15837
  ...spawn
15713
15838
  });
15714
15839
  if (update.status === "completed" || update.status === "failed")
15715
- out.push(endOf(update.toolCallId, memo, update.status, sink, background));
15840
+ out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup));
15716
15841
  return out;
15717
15842
  }
15718
15843
  case "tool_call_update": {
@@ -15744,7 +15869,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15744
15869
  });
15745
15870
  }
15746
15871
  const refined = { ...memo.held };
15747
- 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;
15872
+ const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) : update.title;
15748
15873
  if (title && title !== memo.title)
15749
15874
  refined.title = memo.title = title;
15750
15875
  if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
@@ -15769,7 +15894,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15769
15894
  if (update.rawOutput !== undefined)
15770
15895
  memo.rawOutput = update.rawOutput;
15771
15896
  if ((update.status === "completed" || update.status === "failed") && !memo.ended) {
15772
- out.push(endOf(update.toolCallId, memo, update.status, sink, background));
15897
+ out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup));
15773
15898
  }
15774
15899
  return out;
15775
15900
  }
@@ -15800,7 +15925,7 @@ function textOf(content, tag, what) {
15800
15925
  log.debug(tag, `acp: dropping non-text ${what} block (${content.type})`);
15801
15926
  return null;
15802
15927
  }
15803
- function endOf(toolId, memo, status, sink, background) {
15928
+ function endOf(toolId, memo, status, sink, background, wakeup) {
15804
15929
  memo.ended = true;
15805
15930
  const output = summarizeToolOutput(memo.content, memo.rawOutput);
15806
15931
  if (status === "completed" && memo.kind === "execute" && background) {
@@ -15808,6 +15933,13 @@ function endOf(toolId, memo, status, sink, background) {
15808
15933
  if (start && background(toolId, start, memo.input))
15809
15934
  return { type: "tool-update", toolId, background: true };
15810
15935
  }
15936
+ if (status === "completed" && wakeup) {
15937
+ const until = wakeupAt(memo.name || memo.title, memo.input);
15938
+ if (until !== null) {
15939
+ wakeup(toolId, until);
15940
+ return { type: "tool-update", toolId, background: true, until };
15941
+ }
15942
+ }
15811
15943
  const images = sink ? toolImages(memo.content, sink) : [];
15812
15944
  const detached = status === "completed" && !!memo.spawns && spawnDetached(wholeText(memo.content, memo.rawOutput));
15813
15945
  return {
@@ -16000,15 +16132,19 @@ function pickOption(options, verdict) {
16000
16132
  var SYSTEM_APPEND = [
16001
16133
  "You are working inside a dedicated git worktree managed by Toyon.",
16002
16134
  "Make every change inside the current working directory and never modify files outside it. Reading files outside it is fine when the user points you there.",
16003
- "When a change belongs in another project Toyon has open rather than in this worktree, do not edit outside it and do not stop at saying so: propose it with the `handoff` tool when you have one, naming the project and writing the message its agent will start from, with what is needed and why, how to reproduce it, and the paths here that depend on it; the user decides on a card, and once it is proposed you finish your turn.",
16135
+ "When a change belongs in another project Toyon has open rather than in this worktree, do not edit outside it and do not stop at saying so: propose it with the `split` tool when you have one, as one piece with `where` set to `worktree` and `project` naming that project, its `plan` in markdown that project's agent builds from, with what is needed and why, how to reproduce it, and the paths here that depend on it; the user decides on a card, and once it is proposed you finish your turn.",
16136
+ "When the work you are planning splits into pieces that could go on in parallel, hand Toyon the list with the `split` tool when you have one, right before you ask for plan approval, or, outside plan mode, before you start on your own pieces, never at the end of the turn: a sibling's agent then works while you do, and the user sees the card at once. For each piece, a title, a plan in markdown standing on its own, which its agent builds from as an approved plan, the attachments from this chat it needs by their labels (`Image 2`, `Paste 1`), and where it goes: `here` for what you will do yourself in this worktree (with `after` naming a branch or a piece of the list your own work must wait for, which Toyon then shows under the box until it lands), `worktree` for a new worktree of this project or, with `project`, of another open one (with `after` naming the branch it must wait for, another piece of the list by its title, or `this` for your own), or `send` for an open worktree already working on that part, named by its branch or title. The user reads the list beside your plan and may move any piece; Toyon acts on their answer, so never start a sibling's piece yourself, and do only the pieces marked `here`.",
16137
+ "The same tool, with one piece, is how you offer a separate thing you come across mid-task: a feature you would call its own, a follow-up you would otherwise park in a sentence, work the person asks to have done in a new worktree. Offer it once, as `worktree`, with a plan its agent builds from, as soon as you see it rather than when your own work is done; a sentence saying it could be its own branch is not an offer, and a card the person can decline in one press is.",
16004
16138
  "A write Toyon refuses (outside the worktree, or to an agent's settings, hooks or plugin code such as .claude/settings.json, .claude/hooks or opencode.json) was refused by Toyon and not by the user, who was never asked. Report it as the boundary that stopped it, never as the user declining.",
16005
- "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
16139
+ "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI. Never add, remove, move or prune worktrees; Toyon owns them. Listing them with `git worktree list` is fine.",
16006
16140
  "Never run `git commit` unless the user explicitly asks you to, and never offer to, through the question tool or otherwise; leave changes uncommitted for the user to review and commit themselves.",
16007
16141
  "Keep the scope tight: do the asked task well, then stop. A follow-up the user might want later is a sentence in your reply, not a question.",
16008
16142
  "When the work is done and the only thing left is whether to do something you recommend, do not end your turn on a prose offer waiting for a yes. Ask it with your question tool when you have one: one question, two options with the recommended one first, each saying what it changes, and typed text allowed so the answer can carry instructions. The card is the first thing the user reads, before any prose of yours, so the question itself states what you propose and why in a sentence or two; a question that is only the yes-or-no leaves them guessing what it is. Ask this way only where you would otherwise have stopped and waited; never to confirm work already asked for.",
16009
16143
  "An answer to a question of yours can carry typed text beside the option picked, and the text outranks the pick. Text that doubts the pick, asks you something or says the user cannot decide is not a yes: answer it in prose and change nothing until they have decided. Text that only adds detail to the pick is an instruction for carrying it out.",
16010
16144
  "When referencing a file in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor, whether it is in this worktree or anywhere else on this machine. A folder, a URL route or a command is not a file: name it in backticks, never as a link.",
16011
16145
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists; `@<branch>` names another worktree of this project, and the context attached to the message says where its chat is on disk.",
16146
+ "Toyon knows every worktree by a title and a branch; its directory, `wt-xxxx`, is an id that outlives both. When you name this worktree or another in prose, use its title in quotes or its branch, never the directory: the context attached to each message gives this worktree's title and branch, and a sibling's when the message names it.",
16147
+ "To find which worktree of this project worked on a topic, a ticket or a feature, or to see what is open and what was archived, call the `worktrees` tool when you have one: it lists them by title and branch with where each chat is on disk, and with a `query` it keeps only those whose title, branch or chat mention it. `git worktree list` knows neither titles nor chats, so it cannot answer that.",
16012
16148
  '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.',
16013
16149
  "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.",
16014
16150
  "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.",
@@ -16146,30 +16282,91 @@ function procDiagnosis(p) {
16146
16282
  return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
16147
16283
  return "It never answered on that port.";
16148
16284
  }
16149
- function handoffPrompt(message, note, origin) {
16285
+ function piecesActedPrompt(pieces, outcomes) {
16286
+ const titled = (id) => `"${pieces.find((p) => p.id === id)?.title ?? id}"`;
16287
+ const list = (xs) => xs.join(", ");
16288
+ const started = outcomes.filter((o) => o.fate === "worktree" && !o.after && !o.error).map((o) => titled(o.pieceId));
16289
+ const queued = outcomes.filter((o) => o.fate === "worktree" && o.after && !o.error);
16290
+ const sent = outcomes.filter((o) => o.fate === "send" && !o.error);
16291
+ const here = outcomes.filter((o) => o.fate === "here").map((o) => o.after ? `${titled(o.pieceId)} (once ${o.after} lands)` : titled(o.pieceId));
16292
+ const skipped = outcomes.filter((o) => o.fate === "skip").map((o) => titled(o.pieceId));
16293
+ const failed = outcomes.filter((o) => o.error).map((o) => `${titled(o.pieceId)} (${o.error})`);
16294
+ const parts = [];
16295
+ if (started.length)
16296
+ parts.push(`${list(started)} started in ${started.length === 1 ? "a sibling worktree" : "sibling worktrees"} of this project`);
16297
+ if (queued.length)
16298
+ parts.push(queued.map((o) => `${titled(o.pieceId)} waits for ${o.after} to land and then starts in its own worktree`).join("; "));
16299
+ if (sent.length)
16300
+ parts.push(sent.map((o) => `${titled(o.pieceId)} went to the open worktree "${o.title ?? "named"}"`).join("; "));
16301
+ if (skipped.length)
16302
+ parts.push(`${list(skipped)} ${skipped.length === 1 ? "was" : "were"} skipped by the person`);
16303
+ if (failed.length)
16304
+ parts.push(`${list(failed)} could not start`);
16305
+ const own = here.length ? `Do ${list(here)} here, and only that.` : "Nothing on the list is yours here.";
16306
+ return `Toyon acted on the pieces you handed over with the plan's approval: ${parts.join("; ")}. ${own} The siblings read the plan at its path in this worktree; never do their pieces yourself.`;
16307
+ }
16308
+ function handedPrompt(path, note, origin, via) {
16309
+ const whole = origin.plan ? ` The plan it is a piece of is at ${origin.path}/${origin.plan}; read it for the whole picture, and do only this piece.` : "";
16310
+ const what = via === "piece" ? `as one piece of the work planned there; the other pieces are being done there and in sibling worktrees.${whole}` : `as work to continue in this project.${whole}`;
16311
+ const where = path ? `Your plan is at ${path}.` : "Your plan is above.";
16312
+ return `${where} It was written by the agent of the worktree at ${origin.path} (the project ${origin.project}, on its branch ${origin.branch}) ${what} The person approved it${note ? `, adding: ${note}` : ""}. Build it, and only it. The plan was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`;
16313
+ }
16314
+ function pieceSendPrompt(text, note, origin) {
16150
16315
  return [
16151
- message,
16316
+ text,
16152
16317
  note ? `The person added, when they approved this: ${note}` : undefined,
16153
- `This task was handed off from the worktree at ${origin.path} of the project ${origin.project}, on its branch ${origin.branch}. The message above was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`
16318
+ `This is one piece of the work planned in the worktree at ${origin.path} of the project ${origin.project}, on its branch ${origin.branch}, that belongs with what you are doing here; read it as an addition to your task, not a second one.${origin.plan ? ` The plan it is a piece of is at ${origin.path}/${origin.plan}; read it for the whole picture.` : ""} The text above was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`
16154
16319
  ].filter((p) => !!p).join(`
16155
16320
 
16156
16321
  `);
16157
16322
  }
16158
- function handoffAskPrompt(repo, text) {
16323
+ function pieceAskPrompt(text, project) {
16159
16324
  const own = text?.trim();
16160
- return `The user wants to continue this work in ${repo.name} (${repo.path}), another project open in Toyon. Propose it with the handoff tool: project "${repo.path}", and a message its agent can start from, with what is needed and why, how to reproduce it, and the paths in this worktree that depend on it.` + (own ? ` The user's own words for what ${repo.name} should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
16325
+ const where = project ? `in a new worktree of ${project.name} (${project.path}), another project open in Toyon. Propose it with the \`split\` tool: one piece, \`where\` set to \`worktree\` and \`project\` to "${project.path}", with a title and a plan that project's agent builds from, with what is needed and why, how to reproduce it, and the paths here that depend on it.` : "in a new worktree of this project. Propose it with the `split` tool: one piece, `where` set to `worktree`, with a title and a plan its agent builds from, with what is needed and why and the paths here that depend on it.";
16326
+ return `The user wants part of this work done ${where}` + (own ? ` The user's own words for what it should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
16161
16327
  }
16162
- function handoffLandedContext(title, project, url) {
16163
- return `The work this worktree handed off to ${project}, "${title}", has landed there${url ? ` (${url})` : ""}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
16328
+ function pieceLandedContext(title, project, url, same, again, awaited) {
16329
+ const where = url ? ` (${url})` : "";
16330
+ if (awaited) {
16331
+ return `The branch your own piece waited for, "${title}", has landed on this project's main${where}; that piece can go ahead now, and Toyon rebases onto main when this lands.`;
16332
+ }
16333
+ if (again) {
16334
+ return `The branch this worktree waited for before it started, "${title}", landed again on this project's main${where} with follow-up commits; Toyon rebases onto main when this lands, and its latest commits are readable now if they touch your piece.`;
16335
+ }
16336
+ if (same) {
16337
+ return `The sibling worktree "${title}", a piece of the work planned here, has landed on this project's main${where}; anything here that waited on it can take the real change now, and a later piece that waited for it has started.`;
16338
+ }
16339
+ return `The work this worktree handed off to ${project}, "${title}", has landed there${where}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
16340
+ }
16341
+ function selfContext(w) {
16342
+ if (w.unnamed)
16343
+ return;
16344
+ return `This worktree is "${w.title}", on branch ${w.branch}.`;
16164
16345
  }
16165
16346
  var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16347
+ var dayOf = (ts) => new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
16348
+ function archivedFate(a) {
16349
+ if (a.landed)
16350
+ return "after landing on main, so its work is in main's history";
16351
+ if (a.restorable)
16352
+ return "without landing; its commits are kept under an archive ref, not on any checkout";
16353
+ return "without landing, and git kept nothing of it";
16354
+ }
16355
+ function siblingParagraph(s) {
16356
+ const chat = `Its chat is at ${s.transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`;
16357
+ return s.archived ? `@${s.branch} was the worktree "${s.title}" in this project, archived on ${dayOf(s.archived.at)} ${archivedFate(s.archived)}. Its checkout at ${s.path} is gone. ${chat}` : `@${s.branch} is the worktree "${s.title}" in this project, checked out at ${s.path}. ${chat}`;
16358
+ }
16166
16359
  function worktreeContext(text, siblings) {
16167
16360
  const out = [];
16361
+ const said = new Set;
16168
16362
  for (const s of siblings) {
16363
+ if (said.has(s.branch))
16364
+ continue;
16169
16365
  const named = new RegExp(`(^|\\s)@${escapeRegExp(s.branch)}(?=$|[\\s.,;:!?)\\]}'"])`);
16170
16366
  if (!named.test(text))
16171
16367
  continue;
16172
- out.push(`@${s.branch} is the worktree "${s.title}" in this project. Its chat is at ${s.transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`);
16368
+ said.add(s.branch);
16369
+ out.push(siblingParagraph(s));
16173
16370
  }
16174
16371
  return out;
16175
16372
  }
@@ -16208,6 +16405,52 @@ function previewContext(p) {
16208
16405
  }
16209
16406
  return line;
16210
16407
  }
16408
+ var STANDING_SUBJECTS = 3;
16409
+ var plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
16410
+ function standingContext(s) {
16411
+ if (!s)
16412
+ return;
16413
+ const tree = s.dirty === undefined ? undefined : s.dirty === 0 ? "The tree is clean." : `The tree has ${plural(s.dirty, "uncommitted file")}${s.main ? "" : ", which the user commits from the commit box"}.`;
16414
+ if (s.main) {
16415
+ const behind = s.behind ? ` It is ${plural(s.behind, "commit")} behind origin.` : "";
16416
+ return `This is the project's main checkout.${tree ? ` ${tree}` : ""}${behind}`;
16417
+ }
16418
+ const parts = [];
16419
+ if (s.landed) {
16420
+ const named = s.landed.subjects.slice(0, STANDING_SUBJECTS).map((t) => `"${t}"`);
16421
+ const more = s.landed.subjects.length - named.length;
16422
+ const as = named.length ? ` as ${named.join("; ")}${more > 0 ? ` (and ${more} more)` : ""}` : "";
16423
+ const through = s.landed.pr === undefined ? "" : `, through PR #${s.landed.pr}`;
16424
+ parts.push(`This worktree's work has landed on ${s.base}${as}${through}. The branch restarted from ${s.base}, so there is nothing here to commit or land until new work is written.`);
16425
+ } else {
16426
+ if (tree)
16427
+ parts.push(tree);
16428
+ if (s.ahead !== undefined) {
16429
+ const behind = s.behind ? ` and ${s.behind} behind it` : "";
16430
+ parts.push(s.ahead === 0 && !s.behind ? `The branch is level with ${s.base}.` : `The branch is ${plural(s.ahead, "commit")} ahead of ${s.base}${behind}.`);
16431
+ }
16432
+ }
16433
+ const pr = s.pr;
16434
+ if (pr && s.landed?.pr !== pr.number) {
16435
+ switch (pr.state) {
16436
+ case "open": {
16437
+ const checks = pr.checks ? `, checks ${{ pass: "passing", pending: "pending", fail: "failing" }[pr.checks]}` : "";
16438
+ const review = pr.review ? `, review ${{ approved: "approved", changes_requested: "changes requested", review_required: "required" }[pr.review]}` : "";
16439
+ const auto = pr.automerge ? ", auto-merge on" : "";
16440
+ const unpushed = s.unpushed ? `; ${plural(s.unpushed, "commit")} here ${s.unpushed === 1 ? "is" : "are"} not on it yet` : "";
16441
+ parts.push(`Pull request #${pr.number} is open at ${pr.url}${checks}${review}${auto}${unpushed}.`);
16442
+ break;
16443
+ }
16444
+ case "merged":
16445
+ parts.push(`PR #${pr.number} has merged on GitHub; Toyon lands it here on its next poll.`);
16446
+ break;
16447
+ case "closed":
16448
+ parts.push(`PR #${pr.number} was closed without merging; the next land press creates a new one.`);
16449
+ break;
16450
+ }
16451
+ }
16452
+ return parts.length ? parts.join(" ") : undefined;
16453
+ }
16211
16454
  function imageCaption(ref, path) {
16212
16455
  return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height}), saved at ${path}`;
16213
16456
  }
@@ -16286,28 +16529,7 @@ import { join as join11 } from "path";
16286
16529
  function transcriptPathFor(transcriptsDir, worktreeId) {
16287
16530
  return join11(transcriptsDir, `${worktreeId}.jsonl`);
16288
16531
  }
16289
- function coalesce(entries) {
16290
- const out = [];
16291
- for (const entry of entries) {
16292
- const last = out.at(-1);
16293
- const { event } = entry;
16294
- if (event.type === "text-delta" && last?.event.type === "text-delta") {
16295
- const messageId = event.messageId ?? last.event.messageId;
16296
- out[out.length - 1] = {
16297
- seq: last.seq,
16298
- event: { type: "text-delta", text: last.event.text + event.text, ...messageId ? { messageId } : {} }
16299
- };
16300
- } else if (event.type === "thinking-delta" && last?.event.type === "thinking-delta") {
16301
- out[out.length - 1] = { seq: last.seq, event: { type: "thinking-delta", text: last.event.text + event.text } };
16302
- } else if (event.type === "tool-delta" && last?.event.type === "tool-delta" && last.event.toolId === event.toolId) {
16303
- out[out.length - 1] = { seq: last.seq, event: { ...event, text: last.event.text + event.text } };
16304
- } else {
16305
- out.push(entry);
16306
- }
16307
- }
16308
- return out;
16309
- }
16310
- var NOT_THE_TURNS = new Set(["usage", "session-info", "user-message", "fix-asked"]);
16532
+ var NOT_THE_TURNS = new Set(["usage", "session-info", "user-message", "fix-asked", "handed"]);
16311
16533
  function sealOpenTurn(entries) {
16312
16534
  let reason = null;
16313
16535
  let ts = 0;
@@ -16342,6 +16564,19 @@ function parseTranscript(text, tag) {
16342
16564
  log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
16343
16565
  return out;
16344
16566
  }
16567
+ var lastSeq = (entries) => entries.at(-1)?.seq ?? -1;
16568
+ function after(entries, since) {
16569
+ let lo = 0;
16570
+ let hi = entries.length;
16571
+ while (lo < hi) {
16572
+ const mid = lo + hi >>> 1;
16573
+ if (entries[mid].seq <= since)
16574
+ lo = mid + 1;
16575
+ else
16576
+ hi = mid;
16577
+ }
16578
+ return entries.slice(lo);
16579
+ }
16345
16580
 
16346
16581
  class Transcript {
16347
16582
  path;
@@ -16371,14 +16606,6 @@ class Transcript {
16371
16606
  }
16372
16607
  }
16373
16608
 
16374
- // ../daemon/src/agent/quick.ts
16375
- function quickModelFor(spec, offered, current) {
16376
- const quick = spec.quickModel;
16377
- if (typeof quick === "function")
16378
- return quick(offered, current);
16379
- return quick && offered.includes(quick) ? quick : undefined;
16380
- }
16381
-
16382
16609
  // ../daemon/src/agent/acp/ask.ts
16383
16610
  async function askOnce(ctx, a) {
16384
16611
  const meta = a.spec.systemPrompt === "meta-append";
@@ -16654,9 +16881,13 @@ class AcpSession {
16654
16881
  reaper = null;
16655
16882
  own = null;
16656
16883
  tasks;
16884
+ wakeups;
16657
16885
  asking = 0;
16658
16886
  asks = new Map;
16659
- openHandoffs = new Set;
16887
+ openCards = new Set;
16888
+ heldPieces = null;
16889
+ adopted = null;
16890
+ openPlanAsk = null;
16660
16891
  log;
16661
16892
  seq;
16662
16893
  commandList = [];
@@ -16666,11 +16897,16 @@ class AcpSession {
16666
16897
  this.log = new Transcript(transcriptPathFor(d.transcriptsDir, d.worktreeId), d.worktreeId);
16667
16898
  this.tasks = new BackgroundTasks({
16668
16899
  note: (e) => this.emit(e),
16669
- onEnd: () => this.maybeArmReaper(),
16900
+ onEnd: () => this.holdingChanged(),
16670
16901
  ...d.taskPollMs !== undefined ? { pollMs: d.taskPollMs } : {},
16671
16902
  ...d.claudeConfigDir !== undefined ? { configDir: d.claudeConfigDir } : {}
16672
16903
  });
16673
- this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" ? event.attachments : undefined));
16904
+ this.wakeups = new Wakeups({
16905
+ note: (e) => this.emit(e),
16906
+ onChange: () => this.holdingChanged(),
16907
+ ...d.wakeupGraceMs !== undefined ? { graceMs: d.wakeupGraceMs } : {}
16908
+ });
16909
+ this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" || event.type === "handed" ? event.attachments : undefined));
16674
16910
  const seal = sealOpenTurn(this.log.entries);
16675
16911
  const open = new Set;
16676
16912
  for (const { event } of this.log.entries) {
@@ -16697,18 +16933,18 @@ class AcpSession {
16697
16933
  }
16698
16934
  const going = new Set;
16699
16935
  for (const { event } of this.log.entries) {
16700
- this.trackHandoff(event);
16701
- if (event.type === "handoff-go")
16936
+ this.trackCards(event);
16937
+ if (event.type === "pieces-go")
16702
16938
  going.add(event.id);
16703
- else if (event.type === "handoff" || event.type === "handoff-declined")
16939
+ else if (event.type === "pieces" || event.type === "pieces-declined")
16704
16940
  going.delete(event.id);
16705
16941
  }
16706
16942
  for (const id of going) {
16707
16943
  this.emit({
16708
- type: "handoff-declined",
16944
+ type: "pieces-declined",
16709
16945
  id,
16710
16946
  outcome: "failed",
16711
- reason: "Toyon stopped while the worktree was being made",
16947
+ reason: "Toyon stopped while the worktrees were being made",
16712
16948
  ts: Date.now()
16713
16949
  });
16714
16950
  }
@@ -16716,16 +16952,74 @@ class AcpSession {
16716
16952
  log.info(d.worktreeId, `sealed a turn left open: ${seal.stopReason}`);
16717
16953
  this.emit(seal);
16718
16954
  }
16719
- if (this.openHandoffs.size > 0)
16955
+ this.showHeld();
16956
+ if (this.openCards.size > 0)
16720
16957
  this.syncStatus();
16721
- }
16722
- trackHandoff(event) {
16723
- if (event.type === "handoff-proposed")
16724
- return this.openHandoffs.add(event.id).size > 0;
16725
- if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
16726
- return this.openHandoffs.delete(event.id);
16958
+ this.restored = true;
16959
+ }
16960
+ restored = false;
16961
+ trackCards(event) {
16962
+ if (event.type === "pieces-proposed") {
16963
+ if (event.held) {
16964
+ this.heldPieces = event.id;
16965
+ if (this.openPlanAsk)
16966
+ this.adopted = { ask: this.openPlanAsk, pieces: event.id };
16967
+ return false;
16968
+ }
16969
+ return this.openCards.add(event.id).size > 0;
16970
+ }
16971
+ if (event.type === "pieces-shown") {
16972
+ if (this.heldPieces === event.id)
16973
+ this.heldPieces = null;
16974
+ return this.openCards.add(event.id).size > 0;
16975
+ }
16976
+ if (event.type === "agent-permission") {
16977
+ if (event.plan)
16978
+ this.openPlanAsk = event.id;
16979
+ if (event.pieces && this.heldPieces === event.pieces) {
16980
+ this.heldPieces = null;
16981
+ this.adopted = { ask: event.id, pieces: event.pieces };
16982
+ }
16983
+ return false;
16984
+ }
16985
+ if (event.type === "agent-ask-end") {
16986
+ if (this.openPlanAsk === event.id)
16987
+ this.openPlanAsk = null;
16988
+ if (this.adopted?.ask === event.id) {
16989
+ const pieces = this.adopted.pieces;
16990
+ this.adopted = null;
16991
+ if (event.outcome !== "answered") {
16992
+ if (this.proposed(pieces)?.by === "toyon") {
16993
+ this.note({
16994
+ type: "pieces-declined",
16995
+ id: pieces,
16996
+ reason: "the plan card closed before it was answered",
16997
+ ts: Date.now()
16998
+ });
16999
+ } else
17000
+ this.heldPieces = pieces;
17001
+ }
17002
+ }
17003
+ return false;
17004
+ }
17005
+ if (event.type === "pieces-go" || event.type === "pieces" || event.type === "pieces-declined") {
17006
+ if (this.heldPieces === event.id)
17007
+ this.heldPieces = null;
17008
+ if (this.adopted?.pieces === event.id)
17009
+ this.adopted = null;
17010
+ return this.openCards.delete(event.id);
17011
+ }
16727
17012
  return false;
16728
17013
  }
17014
+ showHeld() {
17015
+ const id = this.heldPieces;
17016
+ if (!id || this.mode() === "plan")
17017
+ return;
17018
+ this.heldPieces = null;
17019
+ this.openCards.add(id);
17020
+ const plan = this.d.onPiecesShown?.(id, this.proposed(id)?.pieces ?? []);
17021
+ this.emit({ type: "pieces-shown", id, ...typeof plan === "string" ? { plan } : {}, ts: Date.now() });
17022
+ }
16729
17023
  get commands() {
16730
17024
  return this.commandList;
16731
17025
  }
@@ -16791,7 +17085,7 @@ class AcpSession {
16791
17085
  return this.waiting().map(queued);
16792
17086
  }
16793
17087
  waiting() {
16794
- return this.queue.filter((q) => !q.recorded);
17088
+ return this.queue.filter((q) => !q.recorded && !q.handed);
16795
17089
  }
16796
17090
  onQueueChange = null;
16797
17091
  queueChanged() {
@@ -16815,12 +17109,12 @@ class AcpSession {
16815
17109
  }
16816
17110
  note(event) {
16817
17111
  this.emit(event);
16818
- if (this.trackHandoff(event))
16819
- this.syncStatus();
16820
17112
  }
16821
17113
  emit(event) {
16822
17114
  const entry = this.log.append(event);
16823
17115
  this.d.onEvent(event, entry.seq);
17116
+ if (this.restored && this.trackCards(event))
17117
+ this.syncStatus();
16824
17118
  }
16825
17119
  setStatus(s) {
16826
17120
  this.status = s;
@@ -16832,6 +17126,16 @@ class AcpSession {
16832
17126
  get stopping() {
16833
17127
  return this.turn?.stopping ?? false;
16834
17128
  }
17129
+ get holding() {
17130
+ return this.tasks.size + this.wakeups.size;
17131
+ }
17132
+ get wakes() {
17133
+ return this.wakeups.next;
17134
+ }
17135
+ holdingChanged() {
17136
+ this.d.onHolding?.();
17137
+ this.maybeArmReaper();
17138
+ }
16835
17139
  setStopping(on) {
16836
17140
  const turn = this.turn;
16837
17141
  if (!turn || turn.stopping === on)
@@ -16842,9 +17146,10 @@ class AcpSession {
16842
17146
  send(text, opts = {}) {
16843
17147
  if (this.stopped)
16844
17148
  return log.warn(this.d.worktreeId, "send after close dropped");
16845
- if (!opts.asked)
17149
+ if (!opts.asked && !opts.keepCards)
16846
17150
  for (const ask of [...this.asks.values()])
16847
17151
  ask.settle("skipped");
17152
+ this.showHeld();
16848
17153
  this.post(text, opts);
16849
17154
  }
16850
17155
  post(text, opts = {}) {
@@ -16853,12 +17158,13 @@ class AcpSession {
16853
17158
  fireAndForget(this.d.worktreeId, steer, "agent steer");
16854
17159
  }
16855
17160
  deliver(text, opts = {}) {
16856
- const { context, attachments, asked } = opts;
17161
+ const { context, attachments, asked, handed } = opts;
16857
17162
  const item = {
16858
17163
  text,
16859
17164
  context,
16860
17165
  ...attachments?.length ? { attachments } : {},
16861
- ...asked ? { asked: asked.kind } : {}
17166
+ ...asked ? { asked: asked.kind } : {},
17167
+ ...handed ? { handed } : {}
16862
17168
  };
16863
17169
  if (asked) {
16864
17170
  this.emit({ type: "fix-asked", text, ts: Date.now(), ...asked });
@@ -16977,6 +17283,7 @@ class AcpSession {
16977
17283
  }
16978
17284
  async drain() {
16979
17285
  this.endOwn("end_turn");
17286
+ this.wakeups.wake("a message came first");
16980
17287
  this.turn = { open: false, stopping: false };
16981
17288
  this.clearReaper();
16982
17289
  this.setStatus("working");
@@ -17002,7 +17309,7 @@ class AcpSession {
17002
17309
  q.handedBack = false;
17003
17310
  this.setStopping(false);
17004
17311
  }
17005
- this.setStatus(this.openHandoffs.size > 0 ? "waiting" : "idle");
17312
+ this.setStatus(this.openCards.size > 0 ? "waiting" : "idle");
17006
17313
  } catch (e) {
17007
17314
  const aws = this.conn && AWS_EXPIRED_RE.test(this.describe(e)) ? this.awsAuth() : null;
17008
17315
  if (this.conn && (aws || isAuthRequired(e) || this.rejectedCredential(e))) {
@@ -17046,6 +17353,7 @@ class AcpSession {
17046
17353
  return;
17047
17354
  turn.open = false;
17048
17355
  this.emit({ type: "turn-end", stopReason, ts: Date.now() });
17356
+ this.showHeld();
17049
17357
  this.setStopping(false);
17050
17358
  }
17051
17359
  async ask(system, prompt, opts = {}) {
@@ -17157,7 +17465,7 @@ class AcpSession {
17157
17465
  const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
17158
17466
  return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
17159
17467
  }
17160
- async record({ text, attachments }) {
17468
+ async record({ text, attachments, handed }) {
17161
17469
  const stored = [];
17162
17470
  try {
17163
17471
  for (const a of attachments ?? [])
@@ -17165,12 +17473,11 @@ class AcpSession {
17165
17473
  } finally {
17166
17474
  this.d.attachments.uploads.release(uploadIds(attachments));
17167
17475
  }
17168
- this.emit({
17169
- type: "user-message",
17170
- text,
17171
- ts: Date.now(),
17172
- ...stored.length ? { attachments: stored.map((s) => s.ref) } : {}
17173
- });
17476
+ const attached = stored.length ? { attachments: stored.map((s) => s.ref) } : {};
17477
+ if (handed)
17478
+ this.emit({ type: "handed", ...handed, text, ...attached, ts: Date.now() });
17479
+ else
17480
+ this.emit({ type: "user-message", text, ts: Date.now(), ...attached });
17174
17481
  return { attachments: stored };
17175
17482
  }
17176
17483
  carried(live, stored) {
@@ -17202,17 +17509,20 @@ class AcpSession {
17202
17509
  return;
17203
17510
  }
17204
17511
  const carried = this.carried(live, item.recorded.attachments);
17512
+ const standing = await this.d.standing?.();
17205
17513
  const prefix = live.prefixPending ? SYSTEM_APPEND : undefined;
17206
17514
  live.prefixPending = false;
17207
17515
  const res = await live.conn.ctx.request(methods.agent.session.prompt, {
17208
17516
  sessionId: live.sessionId,
17209
- prompt: buildPrompt(item.text, this.contextFor(item), prefix, carried)
17517
+ prompt: buildPrompt(item.text, this.contextFor(item, standing), prefix, carried)
17210
17518
  });
17211
17519
  this.endTurn(mapStopReason(res.stopReason));
17212
17520
  }
17213
- contextFor(item) {
17521
+ contextFor(item, standing) {
17214
17522
  return ambientBlock([
17215
17523
  ...item.context ?? [],
17524
+ this.d.self?.(),
17525
+ standing,
17216
17526
  ...this.d.mentioned?.(item.text) ?? [],
17217
17527
  this.d.preview?.(),
17218
17528
  this.d.pageErrors?.(),
@@ -17221,7 +17531,7 @@ class AcpSession {
17221
17531
  }
17222
17532
  landedSince() {
17223
17533
  const entries = this.log.entries;
17224
- const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked";
17534
+ const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked" || e.type === "handed";
17225
17535
  let i = entries.length - 1;
17226
17536
  while (i >= 0 && !isMessage(entries[i].event))
17227
17537
  i--;
@@ -17230,8 +17540,9 @@ class AcpSession {
17230
17540
  const e = entries[i].event;
17231
17541
  if (isMessage(e))
17232
17542
  break;
17233
- if (e.type === "handoff-landed")
17234
- out.unshift(handoffLandedContext(e.title, e.repoName, e.url));
17543
+ if (e.type === "piece-landed") {
17544
+ out.unshift(pieceLandedContext(e.title, e.repoName, e.url, e.same, e.again, e.awaited));
17545
+ }
17235
17546
  }
17236
17547
  return out;
17237
17548
  }
@@ -17269,6 +17580,7 @@ class AcpSession {
17269
17580
  this.d.mcp?.close();
17270
17581
  this.cancelAsks();
17271
17582
  this.tasks.endAll("the agent stopped, and the command with it");
17583
+ this.wakeups.endAll("the agent stopped before it woke");
17272
17584
  this.endOwn("interrupted");
17273
17585
  }
17274
17586
  });
@@ -17358,7 +17670,7 @@ class AcpSession {
17358
17670
  }
17359
17671
  const options = readOptions(configOptions);
17360
17672
  this.emit({ type: "session-info", sessionId, ...currentValues(options) });
17361
- this.learn(options);
17673
+ this.learn(options, !resumed);
17362
17674
  const modeOption = modes ? null : readModeOption(configOptions);
17363
17675
  const live = {
17364
17676
  conn,
@@ -17426,9 +17738,10 @@ class AcpSession {
17426
17738
  live.modeId = modes.current;
17427
17739
  }
17428
17740
  }
17429
- learn(options) {
17430
- for (const [category, opt] of options)
17431
- this.d.onOptionsLearned?.(category, opt.choices);
17741
+ learn(options, fresh = false) {
17742
+ for (const [category, opt] of options) {
17743
+ this.d.onOptionsLearned?.(category, opt.choices, fresh ? opt.current : undefined);
17744
+ }
17432
17745
  }
17433
17746
  onUpdate(params, side, commands) {
17434
17747
  if (params.update.sessionUpdate === "available_commands_update") {
@@ -17470,7 +17783,12 @@ class AcpSession {
17470
17783
  this.d.onOptionChanged?.(category, opt.current);
17471
17784
  }
17472
17785
  }
17473
- const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => this.tasks.start(toolId, start, input));
17786
+ const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => {
17787
+ const taken = this.tasks.start(toolId, start, input);
17788
+ if (taken)
17789
+ this.holdingChanged();
17790
+ return taken;
17791
+ }, (toolId, until) => this.wakeups.start(toolId, until));
17474
17792
  this.forward(live, events);
17475
17793
  this.holdInput(live, params.update);
17476
17794
  }
@@ -17542,6 +17860,9 @@ class AcpSession {
17542
17860
  const planPath = plan ? await this.d.onPlan?.(detail, ownFile) ?? null : null;
17543
17861
  const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
17544
17862
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
17863
+ const pieces = plan ? this.heldPieces : null;
17864
+ if (plan && planPath && !pieces && detail.trim())
17865
+ this.d.onPlanUnsplit?.(detail, planPath);
17545
17866
  return this.openAsk((id) => ({
17546
17867
  type: "agent-permission",
17547
17868
  id,
@@ -17549,6 +17870,7 @@ class AcpSession {
17549
17870
  ...parts.command ? { command: parts.command } : {},
17550
17871
  ...detail ? { detail } : {},
17551
17872
  ...planPath ? { plan: planPath } : {},
17873
+ ...pieces ? { pieces } : {},
17552
17874
  choices,
17553
17875
  ...params.toolCall.toolCallId ? { toolId: params.toolCall.toolCallId } : {},
17554
17876
  ts: Date.now()
@@ -17558,14 +17880,34 @@ class AcpSession {
17558
17880
  if (!choice)
17559
17881
  return { outcome: { outcome: "cancelled" } };
17560
17882
  const res = { outcome: { outcome: "selected", optionId: choice.id } };
17561
- if (plan && (choice.kind === "allow_once" || choice.kind === "allow_always")) {
17883
+ const approved = choice.kind === "allow_once" || choice.kind === "allow_always";
17884
+ if (plan && approved)
17562
17885
  this.d.setMode?.(modeAfterPlan(choice.name));
17563
- if (planPath)
17564
- return this.sayPlanEdited(detail, planPath).then(() => res);
17886
+ const decided = pieces ?? (plan ? this.heldPieces : null);
17887
+ if (decided) {
17888
+ if (this.heldPieces === decided)
17889
+ this.heldPieces = null;
17890
+ this.d.onPieces?.(decided, approved);
17891
+ }
17892
+ if (plan && approved && planPath)
17893
+ return this.sayPlanEdited(detail, planPath).then(() => res);
17894
+ if (plan && !approved && decided && this.proposed(decided)?.by !== "toyon") {
17895
+ return this.sayPiecesDeclined().then(() => res);
17565
17896
  }
17566
17897
  return res;
17567
17898
  });
17568
17899
  }
17900
+ proposed(id) {
17901
+ const list = this.log.entries.findLast(({ event }) => event.type === "pieces-proposed" && event.id === id)?.event;
17902
+ return list?.type === "pieces-proposed" ? list : undefined;
17903
+ }
17904
+ async sayPiecesDeclined() {
17905
+ try {
17906
+ await this.deliver("The pieces list you handed over went back with the plan. Hand over a list again with `split` before you ask for approval.");
17907
+ } catch (e) {
17908
+ log.warn(this.d.worktreeId, "the declined list was not sent", e);
17909
+ }
17910
+ }
17569
17911
  async sayPlanEdited(proposed, path) {
17570
17912
  try {
17571
17913
  if (!await this.d.planEdited?.(path, proposed))
@@ -17648,7 +17990,7 @@ class AcpSession {
17648
17990
  syncStatus() {
17649
17991
  if (this.status === "error")
17650
17992
  return;
17651
- const waiting = this.asks.size > 0 || this.openHandoffs.size > 0;
17993
+ const waiting = this.asks.size > 0 || this.openCards.size > 0;
17652
17994
  this.setStatus(waiting ? "waiting" : this.running || this.own ? "working" : "idle");
17653
17995
  }
17654
17996
  trackOwn(events) {
@@ -17658,6 +18000,7 @@ class AcpSession {
17658
18000
  return;
17659
18001
  this.own = { open: new Set, settle: null };
17660
18002
  this.clearReaper();
18003
+ this.wakeups.wake("woke");
17661
18004
  this.emit({ type: "turn-start", ts: Date.now() });
17662
18005
  this.syncStatus();
17663
18006
  }
@@ -17687,7 +18030,7 @@ class AcpSession {
17687
18030
  this.maybeArmReaper();
17688
18031
  }
17689
18032
  quiet() {
17690
- return !this.running && !this.own && !this.asking && this.asks.size === 0 && this.tasks.size === 0;
18033
+ return !this.running && !this.own && !this.asking && this.asks.size === 0 && this.holding === 0;
17691
18034
  }
17692
18035
  maybeArmReaper() {
17693
18036
  if (this.quiet() && this.conn && !this.stopped)
@@ -17713,6 +18056,7 @@ class AcpSession {
17713
18056
  async dropConn(why) {
17714
18057
  this.cancelAsks();
17715
18058
  this.tasks.endAll("the agent stopped, and the command with it");
18059
+ this.wakeups.endAll("the agent stopped before it woke");
17716
18060
  const conn = this.conn;
17717
18061
  this.conn = null;
17718
18062
  this.live = null;
@@ -19001,12 +19345,27 @@ function defaultAgent(wt, d, preview, onProcess) {
19001
19345
  worktreeId: wt.id,
19002
19346
  cwd: wt.path,
19003
19347
  preview: () => previewContext(preview()),
19348
+ self: () => {
19349
+ const w = d.state.worktrees.find((w) => w.id === wt.id);
19350
+ return w ? selfContext(w) : undefined;
19351
+ },
19352
+ standing: async () => standingContext(await d.standing?.(wt.id) ?? null),
19004
19353
  pageErrors: () => d.pageErrors?.(wt.id),
19005
- mentioned: (text) => worktreeContext(text, d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree").map((w) => ({
19006
- branch: w.branch,
19007
- title: w.title,
19008
- transcript: transcriptPathFor(d.paths.transcriptsDir, w.id)
19009
- }))),
19354
+ mentioned: (text) => !text.includes("@") ? [] : worktreeContext(text, [
19355
+ ...d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree").map((w) => ({
19356
+ branch: w.branch,
19357
+ title: w.title,
19358
+ path: w.path,
19359
+ transcript: transcriptPathFor(d.paths.transcriptsDir, w.id)
19360
+ })),
19361
+ ...(d.archived?.(wt.repoId) ?? []).map((a) => ({
19362
+ branch: a.branch,
19363
+ title: a.title,
19364
+ path: a.path,
19365
+ transcript: a.transcript,
19366
+ archived: { at: a.archivedAt, landed: a.landed, restorable: a.restorable }
19367
+ }))
19368
+ ]),
19010
19369
  onProcess,
19011
19370
  spec,
19012
19371
  connect: (app, spec, prepared) => spawnAcp(app, d.agents.launch(spec, prepared), wt.path, wt.id),
@@ -19019,6 +19378,7 @@ function defaultAgent(wt, d, preview, onProcess) {
19019
19378
  onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
19020
19379
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
19021
19380
  onStopping: () => d.hub.emit("agentStopping", wt.id),
19381
+ onHolding: () => d.hub.emit("agentHolding", wt.id),
19022
19382
  beforeTurn: async () => d.turnStarting?.(wt.id),
19023
19383
  onAuth: (agentId, o) => d.readiness?.observe(agentId, o),
19024
19384
  onLimits: (agentId, limits) => d.readiness?.observeLimits(agentId, limits),
@@ -19030,6 +19390,9 @@ function defaultAgent(wt, d, preview, onProcess) {
19030
19390
  if (d.state.setMode(wt.id, mode))
19031
19391
  d.hub.emit("worktreesChanged");
19032
19392
  },
19393
+ onPieces: (id, approved) => d.hub.emit("piecesDecided", wt.id, id, approved),
19394
+ onPlanUnsplit: (markdown, planPath) => d.hub.emit("planUnsplit", wt.id, markdown, planPath),
19395
+ onPiecesShown: (_id, pieces) => writePlanDocNow(wt.path, listDoc(pieces)),
19033
19396
  onPlan: async (markdown, ownFile) => {
19034
19397
  const w = d.state.worktree(wt.id);
19035
19398
  if (w && !w.planned) {
@@ -19048,9 +19411,9 @@ function defaultAgent(wt, d, preview, onProcess) {
19048
19411
  },
19049
19412
  planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
19050
19413
  option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
19051
- onOptionsLearned: (category, choices) => {
19414
+ onOptionsLearned: (category, choices, runs) => {
19052
19415
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
19053
- if (d.state.setCachedOptions(agentId, category, choices))
19416
+ if (d.state.setCachedOptions(agentId, category, choices, runs))
19054
19417
  d.hub.emit("agentsChanged");
19055
19418
  },
19056
19419
  onOptionChanged: (category, value) => {
@@ -19877,9 +20240,19 @@ function freeSlot(parent) {
19877
20240
  return { id, dir };
19878
20241
  }
19879
20242
  }
20243
+ var NAME_WORDS_MAX = 3;
20244
+ var NAME_CHARS_MAX = 20;
20245
+ var NAME_SHAPE = `1 to ${NAME_WORDS_MAX} plain words, ${NAME_CHARS_MAX} characters at most, like "sticky header"`;
20246
+ function overNameShape(title) {
20247
+ const words = title.split(/\s+/).filter(Boolean).length;
20248
+ const over = words > NAME_WORDS_MAX ? `${words} words` : title.length > NAME_CHARS_MAX ? `${title.length} characters` : null;
20249
+ return over && `runs to ${over}; a title is ${NAME_SHAPE}.`;
20250
+ }
19880
20251
  function titleFrom(prompt) {
19881
- const words = prompt.split(/\s+/).filter(Boolean).slice(0, 3).join(" ");
19882
- return sentenceCase(cleanTitle2(words) || "task");
20252
+ const words = prompt.split(/\s+/).filter(Boolean).slice(0, NAME_WORDS_MAX);
20253
+ while (words.length > 1 && words.join(" ").length > NAME_CHARS_MAX)
20254
+ words.pop();
20255
+ return sentenceCase(cleanTitle2(words.join(" ").slice(0, NAME_CHARS_MAX)) || "task");
19883
20256
  }
19884
20257
  function cleanTitle2(title) {
19885
20258
  return title.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").slice(0, 40).replace(/^[\s:;,.!?]+|[\s:;,.!?]+$/g, "");
@@ -19890,15 +20263,6 @@ function branchSlug(title) {
19890
20263
  function sentenceCase(title) {
19891
20264
  return title.charAt(0).toUpperCase() + title.slice(1);
19892
20265
  }
19893
- var VARIANT_LENSES = [
19894
- "Take the most direct approach: the change most people would expect, in the smallest diff.",
19895
- "Lean toward how it looks and reads: polish what the task touches using only the design tokens, components and voice the project already has. Add no new colours, fonts or effects, and restyle nothing outside the task.",
19896
- "Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
19897
- ];
19898
- function variantLens(index) {
19899
- const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
19900
- return `(You are attempt ${index} of several parallel attempts at this task. Stay within the task and follow the project's existing styles, components, tokens and writing conventions; the attempts differ in emphasis, not in how much they change. ${lens})`;
19901
- }
19902
20266
 
19903
20267
  // ../daemon/src/agent/recap.ts
19904
20268
  var DAEMON_STOPS = new Set(["interrupted", "failed", "auth", "lost"]);
@@ -19939,6 +20303,11 @@ function turnsSince(entries, seenAt) {
19939
20303
  (into?.fixes ?? pendingFixes).push(e.why);
19940
20304
  break;
19941
20305
  }
20306
+ case "handed": {
20307
+ const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
20308
+ (into?.asks ?? pending).push(e.body ?? e.text);
20309
+ break;
20310
+ }
19942
20311
  case "turn-start":
19943
20312
  open = start(e.ts);
19944
20313
  break;
@@ -20014,10 +20383,6 @@ function openAskOf(entries) {
20014
20383
  open.set(e.id, e.title);
20015
20384
  else if (e.type === "agent-ask-end")
20016
20385
  open.delete(e.id);
20017
- else if (e.type === "handoff-proposed")
20018
- open.set(e.id, `Continue in ${e.repo.name}?`);
20019
- else if (e.type === "handoff-go" || e.type === "handoff" || e.type === "handoff-declined")
20020
- open.delete(e.id);
20021
20386
  }
20022
20387
  const last = [...open.values()].at(-1)?.trim();
20023
20388
  return last ? clip(last) : undefined;
@@ -20037,9 +20402,15 @@ function factsOf(turns, end, ask) {
20037
20402
  };
20038
20403
  }
20039
20404
  function firstAskOf(entries) {
20040
- for (const { event: e } of entries)
20405
+ for (const { event: e } of entries) {
20041
20406
  if (e.type === "user-message" && e.text.trim())
20042
20407
  return e.text;
20408
+ if (e.type === "handed") {
20409
+ const said = e.body ?? e.text;
20410
+ if (said.trim())
20411
+ return said;
20412
+ }
20413
+ }
20043
20414
  return;
20044
20415
  }
20045
20416
  var RECAP_MAX = 160;
@@ -20195,49 +20566,6 @@ function plain(line) {
20195
20566
  return line.replace(/(\d)\s*[\u2013\u2014]\s*(\d)/g, "$1-$2").replace(/\s*[\u2013\u2014]\s*/g, ", ").replace(/\s+--\s+/g, ", ").replace(/\s*(\u2192|->)\s*/g, " to ").replace(/\u2026/g, "...");
20196
20567
  }
20197
20568
 
20198
- // ../daemon/src/agent/oneshot.ts
20199
- async function askFreshAgent(agents, agentId, cwd, system, prompt, opts = {}) {
20200
- const spec = agents.require(agentId);
20201
- const prepared = await prepareLaunch(cwd, spec);
20202
- const listeners = new Map;
20203
- const app = client({ name: "toyon" }).onRequest(methods.client.session.requestPermission, (c) => decideUnattended(c.params, prepared.bounds, cwd)).onNotification(methods.client.session.update, (c) => {
20204
- const u = c.params.update;
20205
- if (u.sessionUpdate === "agent_message_chunk" && u.content.type === "text") {
20206
- listeners.get(c.params.sessionId)?.(u.content.text);
20207
- }
20208
- });
20209
- const link = spawnAcp(app, agents.launch(spec, prepared), cwd, `ask:${agentId}`);
20210
- try {
20211
- const init = await link.conn.agent.request(methods.agent.initialize, {
20212
- protocolVersion: PROTOCOL_VERSION2,
20213
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
20214
- clientInfo: { name: "toyon", version: "0" }
20215
- });
20216
- return await askOnce(link.conn.agent, {
20217
- cwd,
20218
- system,
20219
- prompt,
20220
- spec,
20221
- ...opts.quick ? { quick: opts.quick } : {},
20222
- route: (id, onText) => {
20223
- listeners.set(id, onText);
20224
- return () => listeners.delete(id);
20225
- },
20226
- caps: {
20227
- close: !!init.agentCapabilities?.sessionCapabilities?.close,
20228
- delete: !!init.agentCapabilities?.sessionCapabilities?.delete
20229
- },
20230
- tag: `ask:${agentId}`
20231
- });
20232
- } catch (e) {
20233
- log.warn(`ask:${agentId}`, "one-shot question failed", e);
20234
- return null;
20235
- } finally {
20236
- link.conn.close();
20237
- await link.kill();
20238
- }
20239
- }
20240
-
20241
20569
  // ../daemon/src/agent/tasks.ts
20242
20570
  var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
20243
20571
  var namePrompt = (task) => `Name this coding task the way a teammate would say it out loud: 1 or 2 plain lowercase words, 16 characters at most (like "sticky header", "dark mode", "price badge"). Leave out words every task in the project would share, like the product or the app. Reply with ONLY the name, nothing else.
@@ -20265,22 +20593,34 @@ function parseName(text) {
20265
20593
  const line = text.trim().split(`
20266
20594
  `).at(-1) ?? "";
20267
20595
  const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
20268
- const words = name.split(" ").filter(Boolean);
20269
- if (words.length < 1 || words.length > 3 || name.length < 3 || name.length > 20)
20596
+ if (name.length < 3 || overNameShape(name))
20270
20597
  return null;
20271
20598
  if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
20272
20599
  return null;
20273
20600
  return sentenceCase(name);
20274
20601
  }
20275
- var PLAN_SYSTEM = "You are a task-planning assistant. Reply with only the requested JSON.";
20276
- var planPrompt = (request) => [
20277
- "Split this request into independent coding tasks that could each be done in a separate git branch by a separate engineer.",
20278
- "Reply with ONLY a JSON array of task description strings (1 to 5 items), nothing else.",
20279
- "If the request is really one task, reply with a single-item array.",
20280
- `Request: ${request.slice(0, 2000)}`
20602
+ function makeNamer(runtime) {
20603
+ return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
20604
+ }
20605
+ function lacksQuickModel(spec, state) {
20606
+ const quick = spec.quickModel;
20607
+ if (!quick)
20608
+ return true;
20609
+ if (typeof quick === "function")
20610
+ return false;
20611
+ const offered = state.cachedOptions(spec.id, "model");
20612
+ return offered.length > 0 && !offered.some((m) => m.id === quick);
20613
+ }
20614
+ var PIECES_SYSTEM = "You read plans and reply with only the requested JSON.";
20615
+ var piecesPrompt = (markdown) => [
20616
+ "Below is a plan. List the parts of it the plan itself means to build apart from the rest: a step or section it marks as its own branch, a sibling worktree, a follow-up, or independent work. Also list the part it keeps in this worktree, if it says so.",
20617
+ `Reply with ONLY a JSON array of 0 to 6 objects, each {"title": a name for the part, ${NAME_SHAPE}, "section": the exact heading text of that part, "plan": the plan for that part's agent in markdown, from the plan's own words: what to build, why, the files, and what done looks like, 150 to 500 words, "where": "worktree" or "here", "after": optional, "this" when the plan says the part waits for this branch to land}.`,
20618
+ "Reply with an empty array when the plan is one piece of work with no part set apart.",
20619
+ `Plan:
20620
+ ${markdown.slice(0, 12000)}`
20281
20621
  ].join(`
20282
20622
  `);
20283
- function parsePlan(text) {
20623
+ function parsePieces(text) {
20284
20624
  if (!text)
20285
20625
  return null;
20286
20626
  const start = text.indexOf("[");
@@ -20291,23 +20631,31 @@ function parsePlan(text) {
20291
20631
  const arr = JSON.parse(text.slice(start, end + 1));
20292
20632
  if (!Array.isArray(arr))
20293
20633
  return null;
20294
- const tasks = arr.filter((x) => typeof x === "string" && x.trim()).slice(0, 5);
20295
- return tasks.length > 0 ? tasks : null;
20634
+ const out = [];
20635
+ for (const x of arr) {
20636
+ if (!x || typeof x !== "object")
20637
+ continue;
20638
+ const { title, section, plan, where, after } = x;
20639
+ if (typeof title !== "string" || !title.trim() || typeof section !== "string" || !section.trim())
20640
+ continue;
20641
+ if (where !== "here" && where !== "worktree")
20642
+ continue;
20643
+ out.push({
20644
+ title: title.trim(),
20645
+ section: section.trim(),
20646
+ ...typeof plan === "string" && plan.trim() ? { plan: plan.trim() } : {},
20647
+ where,
20648
+ ...typeof after === "string" && after.trim() ? { after: after.trim() } : {}
20649
+ });
20650
+ }
20651
+ return out.slice(0, 6);
20296
20652
  } catch {
20297
20653
  return null;
20298
20654
  }
20299
20655
  }
20300
- function makeNamer(runtime) {
20301
- return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
20302
- }
20303
- function lacksQuickModel(spec, state) {
20304
- const quick = spec.quickModel;
20305
- if (!quick)
20306
- return true;
20307
- if (typeof quick === "function")
20308
- return false;
20309
- const offered = state.cachedOptions(spec.id, "model");
20310
- return offered.length > 0 && !offered.some((m) => m.id === quick);
20656
+ function makePiecesReader(...deps) {
20657
+ const ask = makeQuickAsker(...deps);
20658
+ return async (wt, markdown) => parsePieces(await ask(wt, PIECES_SYSTEM, piecesPrompt(markdown)));
20311
20659
  }
20312
20660
  function makeQuickAsker(runtime, agents, state) {
20313
20661
  return async (wt, system, prompt) => {
@@ -20326,17 +20674,14 @@ function makeAnswerRecapper(...deps) {
20326
20674
  const ask = makeQuickAsker(...deps);
20327
20675
  return async (wt, prompt) => parseRecap(await ask(wt, ANSWER_SYSTEM, prompt));
20328
20676
  }
20329
- function makePlanner(agents) {
20330
- return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
20331
- }
20332
20677
 
20333
20678
  // ../daemon/src/agent/transcriptForward.ts
20334
20679
  import { existsSync as existsSync15, readdirSync as readdirSync5, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
20335
20680
  import { join as join13 } from "path";
20336
- var FORWARD_STEP = "1";
20337
- var MARK = '"page';
20681
+ var FORWARD_STEP = "3";
20682
+ var MARKS = ['"page', '"brief"', '"handoff'];
20338
20683
  function forwardLine(line) {
20339
- if (!line.includes(MARK))
20684
+ if (!MARKS.some((m) => line.includes(m)))
20340
20685
  return null;
20341
20686
  let entry;
20342
20687
  try {
@@ -20349,6 +20694,73 @@ function forwardLine(line) {
20349
20694
  return { drop: true };
20350
20695
  if (e?.type === "fix-asked" && e.kind === "page")
20351
20696
  return { line: JSON.stringify({ ...entry, event: { ...e, kind: "command" } }) };
20697
+ if (e?.type === "user-message" && e.brief && typeof e.brief === "object") {
20698
+ const brief = e.brief;
20699
+ const { brief: _brief, ...rest } = e;
20700
+ return {
20701
+ line: JSON.stringify({
20702
+ ...entry,
20703
+ event: {
20704
+ ...rest,
20705
+ type: "handed",
20706
+ via: "message",
20707
+ body: brief.text,
20708
+ origin: brief.origin,
20709
+ ...brief.note ? { note: brief.note } : {}
20710
+ }
20711
+ })
20712
+ };
20713
+ }
20714
+ if (e?.type === "handoff-proposed") {
20715
+ const repo = e.repo ?? {};
20716
+ return {
20717
+ line: JSON.stringify({
20718
+ ...entry,
20719
+ event: {
20720
+ type: "pieces-proposed",
20721
+ id: e.id,
20722
+ pieces: [
20723
+ {
20724
+ id: "p1",
20725
+ title: e.title ?? "Continue",
20726
+ plan: e.message ?? "",
20727
+ attachments: [],
20728
+ fate: "worktree",
20729
+ project: { id: repo.id, name: repo.name }
20730
+ }
20731
+ ],
20732
+ mode: e.mode,
20733
+ ts: e.ts
20734
+ }
20735
+ })
20736
+ };
20737
+ }
20738
+ if (e?.type === "handoff-landed")
20739
+ return { line: JSON.stringify({ ...entry, event: { ...e, type: "piece-landed" } }) };
20740
+ if (e?.type === "pieces-proposed" && Array.isArray(e.pieces) && e.pieces.some((p) => p && typeof p === "object" && ("brief" in p))) {
20741
+ const pieces = e.pieces.map(({ brief, ...rest }) => brief === undefined ? rest : { ...rest, plan: brief });
20742
+ return { line: JSON.stringify({ ...entry, event: { ...e, pieces } }) };
20743
+ }
20744
+ if (e?.type === "handoff-go")
20745
+ return { line: JSON.stringify({ ...entry, event: { ...e, type: "pieces-go" } }) };
20746
+ if (e?.type === "handoff") {
20747
+ return {
20748
+ line: JSON.stringify({
20749
+ ...entry,
20750
+ event: {
20751
+ type: "pieces",
20752
+ id: e.id,
20753
+ outcomes: [
20754
+ { pieceId: "p1", fate: "worktree", worktreeId: e.worktreeId, title: e.title, repoName: e.repoName }
20755
+ ],
20756
+ ts: e.ts
20757
+ }
20758
+ })
20759
+ };
20760
+ }
20761
+ if (e?.type === "handoff-declined") {
20762
+ return { line: JSON.stringify({ ...entry, event: { ...e, type: "pieces-declined" } }) };
20763
+ }
20352
20764
  return null;
20353
20765
  }
20354
20766
  function forwardText(text) {
@@ -20765,8 +21177,13 @@ function humanMs(ms) {
20765
21177
 
20766
21178
  // ../daemon/src/core/awake.ts
20767
21179
  var WAIT_MS = 30 * 60000;
21180
+ var BRIDGE_MS = 60000;
21181
+ var CARRIED_ENV = "TOYON_KEEP_AWAKE_CARRIED";
21182
+ function hasIdleSleep(raw, platform) {
21183
+ return raw !== "off" && platform === "darwin";
21184
+ }
20768
21185
  function idleSleepAssertion(raw, platform = process.platform) {
20769
- if (raw === "off" || platform !== "darwin")
21186
+ if (!hasIdleSleep(raw, platform))
20770
21187
  return null;
20771
21188
  return () => {
20772
21189
  const child = spawn3("/usr/bin/caffeinate", ["-i", "-w", String(process.pid)], { stdio: "ignore" });
@@ -20777,6 +21194,22 @@ function idleSleepAssertion(raw, platform = process.platform) {
20777
21194
  };
20778
21195
  };
20779
21196
  }
21197
+ function idleSleepBridge(raw, platform = process.platform) {
21198
+ if (!hasIdleSleep(raw, platform))
21199
+ return null;
21200
+ return (ms) => {
21201
+ const child = spawn3("/usr/bin/caffeinate", ["-i", "-t", String(Math.ceil(ms / 1000))], {
21202
+ stdio: "ignore",
21203
+ detached: true
21204
+ });
21205
+ child.on("error", (e) => log.warn("awake", "caffeinate did not start, so the Mac may sleep during the restart", e));
21206
+ child.unref();
21207
+ };
21208
+ }
21209
+ function carryEnv(env, held) {
21210
+ const { [CARRIED_ENV]: _stale, ...rest } = env;
21211
+ return held ? { ...rest, [CARRIED_ENV]: "1" } : rest;
21212
+ }
20780
21213
 
20781
21214
  class KeepAwake {
20782
21215
  d;
@@ -20792,6 +21225,10 @@ class KeepAwake {
20792
21225
  this.d = d;
20793
21226
  this.waitMs = d.waitMs ?? WAIT_MS;
20794
21227
  this.now = d.now ?? Date.now;
21228
+ if (d.carried && d.assert) {
21229
+ this.remoteLeftAt = this.now();
21230
+ log.info("awake", `the last daemon was holding off idle sleep; holding on for ${humanMs(this.waitMs)} unless a device comes back`);
21231
+ }
20795
21232
  d.hub.on("agentStatus", (_id, status) => {
20796
21233
  if (status === "waiting")
20797
21234
  this.waitingAt = this.now();
@@ -20813,10 +21250,14 @@ class KeepAwake {
20813
21250
  this.remote = n;
20814
21251
  this.reconsider();
20815
21252
  }
20816
- stop() {
21253
+ stop(opts = {}) {
21254
+ const held = this.release !== null;
20817
21255
  this.gone = true;
20818
21256
  this.disarm();
21257
+ if (held && opts.handoff)
21258
+ this.d.bridge?.(BRIDGE_MS);
20819
21259
  this.hold(false);
21260
+ return held;
20820
21261
  }
20821
21262
  reconsider() {
20822
21263
  if (this.gone || !this.d.assert)
@@ -21401,12 +21842,12 @@ async function replacementRuns() {
21401
21842
  reason: `the bun Toyon runs on did not start (${process.execPath}: ${why}); an install may still be writing it`
21402
21843
  };
21403
21844
  }
21404
- function respawn(logFile) {
21845
+ function respawn(logFile, env = process.env) {
21405
21846
  const logFd = openLog(logFile);
21406
21847
  const child = spawn4(process.execPath, process.argv.slice(1), {
21407
21848
  detached: true,
21408
21849
  stdio: ["ignore", logFd, logFd],
21409
- env: { ...process.env }
21850
+ env: { ...env }
21410
21851
  });
21411
21852
  child.on("error", (e) => log.warn("daemon", `the replacement did not start: ${e.message}`));
21412
21853
  child.unref();
@@ -21576,6 +22017,15 @@ function loadState(paths) {
21576
22017
  log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
21577
22018
  return structuredClone(empty);
21578
22019
  }
22020
+ for (const w of state.worktrees) {
22021
+ for (const q of w.queued ?? []) {
22022
+ const old = q;
22023
+ if (old.brief !== undefined && old.plan === undefined) {
22024
+ old.plan = old.brief;
22025
+ delete old.brief;
22026
+ }
22027
+ }
22028
+ }
21579
22029
  const ids = new Set(state.worktrees.map((w) => w.id));
21580
22030
  for (const id of Object.keys(state.sessions))
21581
22031
  if (!ids.has(id))
@@ -21786,12 +22236,22 @@ class StateStore {
21786
22236
  cachedOptions(agentId, category) {
21787
22237
  return this.state.optionCache?.[agentId]?.[category] ?? [];
21788
22238
  }
21789
- setCachedOptions(agentId, category, choices) {
21790
- if (JSON.stringify(this.cachedOptions(agentId, category)) === JSON.stringify(choices))
22239
+ cachedDefault(agentId, category) {
22240
+ return this.state.optionDefaults?.[agentId]?.[category];
22241
+ }
22242
+ setCachedOptions(agentId, category, choices, runs) {
22243
+ const sameList = JSON.stringify(this.cachedOptions(agentId, category)) === JSON.stringify(choices);
22244
+ const sameDefault = runs === undefined || runs === this.cachedDefault(agentId, category);
22245
+ if (sameList && sameDefault)
21791
22246
  return false;
21792
22247
  this.state.optionCache ??= {};
21793
22248
  this.state.optionCache[agentId] ??= {};
21794
22249
  this.state.optionCache[agentId][category] = choices;
22250
+ if (runs !== undefined) {
22251
+ this.state.optionDefaults ??= {};
22252
+ this.state.optionDefaults[agentId] ??= {};
22253
+ this.state.optionDefaults[agentId][category] = runs;
22254
+ }
21795
22255
  this.save();
21796
22256
  return true;
21797
22257
  }
@@ -28610,9 +29070,6 @@ class Interest {
28610
29070
  // ../daemon/src/server/ws.ts
28611
29071
  import { homedir as homedir12, hostname as hostname2 } from "os";
28612
29072
 
28613
- // ../daemon/src/server/handlers.ts
28614
- import { randomBytes as randomBytes7 } from "crypto";
28615
-
28616
29073
  // ../daemon/src/agent/config.ts
28617
29074
  import { existsSync as existsSync28, readFileSync as readFileSync17 } from "fs";
28618
29075
  import { homedir as homedir11 } from "os";
@@ -29234,6 +29691,12 @@ var notify = async (s, ctx, worktreeId, msg) => {
29234
29691
  await gitStatus(s, ctx, worktreeId);
29235
29692
  };
29236
29693
  var requireRun = (s, id) => s.worktrees.requireRun(id);
29694
+ function backfillOf(entries, since) {
29695
+ const last = lastSeq(entries);
29696
+ if (since === undefined || since > last)
29697
+ return { events: coalesce(entries), last };
29698
+ return { events: coalesce(after(entries, since)), last, after: since };
29699
+ }
29237
29700
  var handlers = {
29238
29701
  async subscribe(msg, ctx, s) {
29239
29702
  const r = s.worktrees.readable(msg.worktreeId);
@@ -29243,7 +29706,7 @@ var handlers = {
29243
29706
  throw new UserError("unknown worktree");
29244
29707
  if (!ctx.subscribe(msg.worktreeId))
29245
29708
  return;
29246
- ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
29709
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, ...backfillOf(kept, msg.since), log: [] });
29247
29710
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
29248
29711
  ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
29249
29712
  await gitStatus(s, ctx, msg.worktreeId);
@@ -29252,7 +29715,7 @@ var handlers = {
29252
29715
  if (!ctx.subscribe(msg.worktreeId))
29253
29716
  return;
29254
29717
  if (!r.wt) {
29255
- ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: [], log: [] });
29718
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: [], last: -1, log: [] });
29256
29719
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
29257
29720
  ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
29258
29721
  await gitAndPages(s, ctx, msg.worktreeId);
@@ -29262,7 +29725,7 @@ var handlers = {
29262
29725
  ctx.reply({
29263
29726
  t: "backfill",
29264
29727
  worktreeId: msg.worktreeId,
29265
- events: coalesce(agent.transcript()),
29728
+ ...backfillOf(agent.transcript(), msg.since),
29266
29729
  log: s.runtime.recentLogs(msg.worktreeId)
29267
29730
  });
29268
29731
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
@@ -29329,29 +29792,8 @@ var handlers = {
29329
29792
  effort: msg.effort,
29330
29793
  carry: msg.carry
29331
29794
  };
29332
- const of = msg.variants ?? 1;
29333
29795
  await sending(s, ctx, msg, msg.attachments, async () => {
29334
- if (of === 1) {
29335
- await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
29336
- return;
29337
- }
29338
- const group = randomBytes7(4).toString("hex");
29339
- const made = await Promise.allSettled(Array.from({ length: of }, (_, i) => s.worktrees.create(msg.repoId, msg.prompt, {
29340
- ...opts,
29341
- ...i === 0 ? { worktreeId: msg.worktreeId } : {},
29342
- variant: { group, index: i + 1, of }
29343
- })));
29344
- const failed = made.flatMap((m) => m.status === "rejected" ? [m.reason] : []);
29345
- if (failed.length === 0)
29346
- return;
29347
- const went = failed.find((e) => e instanceof UserError && e.delivered);
29348
- if (went)
29349
- throw went;
29350
- if (failed.length < of) {
29351
- const why = failed[0] instanceof Error ? failed[0].message : String(failed[0]);
29352
- throw new UserError(`${failed.length} of ${of} attempts did not start: ${why}`, { delivered: true });
29353
- }
29354
- throw failed[0];
29796
+ await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
29355
29797
  });
29356
29798
  },
29357
29799
  "set-worktree-profile"(msg, _ctx, s) {
@@ -29372,27 +29814,6 @@ var handlers = {
29372
29814
  "set-worktree-effort"(msg, _ctx, s) {
29373
29815
  s.worktrees.setEffort(msg.worktreeId, msg.effort);
29374
29816
  },
29375
- "batch-worktrees"(msg, ctx, s) {
29376
- const repo = s.state.requireRepo(msg.repoId);
29377
- if (msg.boxId)
29378
- s.drafts.take(msg.boxId);
29379
- fireAndForget(msg.repoId, (async () => {
29380
- const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
29381
- const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
29382
- let failed = 0;
29383
- for (const task of tasks) {
29384
- try {
29385
- await s.worktrees.create(msg.repoId, task, { agent: msg.agent, model: msg.model, effort: msg.effort });
29386
- } catch (e) {
29387
- failed++;
29388
- log.warn(msg.repoId, `batch: could not start "${task.slice(0, 60)}"`, e);
29389
- }
29390
- }
29391
- const started = tasks.length - failed;
29392
- if (failed)
29393
- ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
29394
- })(), "batch");
29395
- },
29396
29817
  async "archive-worktree"(msg, _ctx, s) {
29397
29818
  await s.worktrees.archiveWorktree(msg.worktreeId);
29398
29819
  },
@@ -29541,9 +29962,6 @@ var handlers = {
29541
29962
  s.turns.stoppedByPerson(msg.worktreeId);
29542
29963
  s.runtime.agentFor(msg.worktreeId)?.stop();
29543
29964
  },
29544
- async "pick-variant"(msg, _ctx, s) {
29545
- await s.worktrees.pickVariant(msg.worktreeId);
29546
- },
29547
29965
  unqueue(msg, _ctx, s) {
29548
29966
  requireRun(s, msg.worktreeId);
29549
29967
  s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index, msg.edit ? (m) => s.drafts.putBack(msg.worktreeId, { text: m.text, items: m.attachments ?? [] }) : undefined);
@@ -29578,6 +29996,9 @@ var handlers = {
29578
29996
  throw new UserError("that attachment is no longer on this machine");
29579
29997
  s.files.revealPath(path);
29580
29998
  },
29999
+ "reveal-transcript"(msg, _ctx, s) {
30000
+ s.files.revealPath(s.worktrees.transcriptPath(msg.worktreeId));
30001
+ },
29581
30002
  async "discard-files"(msg, _ctx, s) {
29582
30003
  try {
29583
30004
  for (const path of msg.paths)
@@ -29718,13 +30139,17 @@ var handlers = {
29718
30139
  s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId, ...msg.note ? { note: msg.note } : {} });
29719
30140
  s.worktrees.markPrompted(msg.worktreeId);
29720
30141
  },
29721
- async "handoff-answer"(msg, _ctx, s) {
30142
+ async "pieces-ask"(msg, _ctx, s) {
30143
+ requireRun(s, msg.worktreeId);
30144
+ await s.pieces.ask(msg.worktreeId, msg.repoId, msg.text);
30145
+ },
30146
+ "pieces-fate"(msg, _ctx, s) {
29722
30147
  requireRun(s, msg.worktreeId);
29723
- await s.handoff.answer(msg.worktreeId, msg.id, msg.go, msg.note);
30148
+ s.pieces.fate(msg.worktreeId, msg.id, msg.pieceId, msg.fate);
29724
30149
  },
29725
- async "handoff-ask"(msg, _ctx, s) {
30150
+ async "pieces-answer"(msg, _ctx, s) {
29726
30151
  requireRun(s, msg.worktreeId);
29727
- await s.handoff.ask(msg.worktreeId, msg.repoId, msg.text);
30152
+ await s.pieces.answer(msg.worktreeId, msg.id, msg.go, msg.note);
29728
30153
  },
29729
30154
  "agent-config"(msg, ctx, s) {
29730
30155
  const spec = s.agents.require(msg.agent);
@@ -29806,7 +30231,7 @@ var handlers = {
29806
30231
  },
29807
30232
  "preview-start"(msg, _ctx, s) {
29808
30233
  requireRun(s, msg.worktreeId);
29809
- s.worktrees.startPreview(msg.worktreeId);
30234
+ s.worktrees.retryPreview(msg.worktreeId);
29810
30235
  },
29811
30236
  async "search-refs"(msg, ctx, s) {
29812
30237
  const refs = await s.refs.search(msg.repoId, msg.query);
@@ -29889,6 +30314,47 @@ function withCors(res, origin) {
29889
30314
  return res;
29890
30315
  }
29891
30316
 
30317
+ // ../daemon/src/server/encoding.ts
30318
+ import { promisify } from "util";
30319
+ import { gzip } from "zlib";
30320
+ var gzipAsync = promisify(gzip);
30321
+ function acceptedEncodings(header) {
30322
+ const out = new Set;
30323
+ if (!header)
30324
+ return out;
30325
+ for (const part of header.split(",")) {
30326
+ const [name = "", ...params] = part.trim().toLowerCase().split(";");
30327
+ const coding = name.trim();
30328
+ if (coding !== "br" && coding !== "gzip")
30329
+ continue;
30330
+ const refused = params.some((p) => /^\s*q\s*=\s*0(\.0*)?\s*$/.test(p));
30331
+ if (!refused)
30332
+ out.add(coding);
30333
+ }
30334
+ return out;
30335
+ }
30336
+ function pickSibling(file, accepted) {
30337
+ const candidates = [
30338
+ ["br", `${file}.br`],
30339
+ ["gzip", `${file}.gz`]
30340
+ ];
30341
+ for (const [encoding, path] of candidates) {
30342
+ if (accepted.has(encoding) && Bun.file(path).size > 0)
30343
+ return { path, encoding };
30344
+ }
30345
+ return { path: file, encoding: null };
30346
+ }
30347
+ var GZIP_MIN_BYTES = 1024;
30348
+ async function encoded(req, body, headers) {
30349
+ const bytes = Buffer.from(body);
30350
+ if (bytes.byteLength < GZIP_MIN_BYTES || !acceptedEncodings(req.headers.get("accept-encoding")).has("gzip")) {
30351
+ return new Response(bytes, { headers });
30352
+ }
30353
+ return new Response(await gzipAsync(bytes), {
30354
+ headers: { ...headers, "content-encoding": "gzip", vary: "accept-encoding" }
30355
+ });
30356
+ }
30357
+
29892
30358
  // ../daemon/src/server/pairing.ts
29893
30359
  var NO_STORE = { "cache-control": "no-store" };
29894
30360
  var json = (body, headers = {}) => Response.json(body, { headers: { ...NO_STORE, ...headers } });
@@ -30008,6 +30474,7 @@ function createFetch(opts) {
30008
30474
  const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`);
30009
30475
  const unauthorized = () => new Response("unauthorized", { status: 401 });
30010
30476
  const gateFor = (remote) => remote && { grant, host: remote.host, redeem: (code) => opts.grants.redeem(code) };
30477
+ const page = async (req, file) => encoded(req, await Bun.file(file).text(), { "content-type": "text/html;charset=utf-8", "cache-control": NO_STORE2 });
30011
30478
  const health = async () => {
30012
30479
  const remote = opts.remote();
30013
30480
  return Response.json({
@@ -30060,6 +30527,7 @@ function createFetch(opts) {
30060
30527
  const authed = sameSecret(url.searchParams.get("token"), opts.token);
30061
30528
  if (authed)
30062
30529
  opts.noteShellOrigin(req.headers.get("origin"));
30530
+ const known = parseKnown(url.searchParams.get("known"));
30063
30531
  const data = {
30064
30532
  authed,
30065
30533
  subs: new Set,
@@ -30068,6 +30536,7 @@ function createFetch(opts) {
30068
30536
  sent: 0,
30069
30537
  bytes: 0,
30070
30538
  view: null,
30539
+ ...known ? { known } : {},
30071
30540
  ...remoteShell ? { remote: true } : {}
30072
30541
  };
30073
30542
  const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
@@ -30080,10 +30549,13 @@ function createFetch(opts) {
30080
30549
  if (url.pathname === "/bootstrap") {
30081
30550
  if (!sameSecret(url.searchParams.get("token"), opts.token))
30082
30551
  return new Response("unauthorized", { status: 401 });
30083
- const headers = { "cache-control": NO_STORE2 };
30552
+ const headers = {
30553
+ "content-type": "application/json;charset=utf-8",
30554
+ "cache-control": NO_STORE2
30555
+ };
30084
30556
  if (remote && remoteShell)
30085
30557
  headers["set-cookie"] = grantCookie(grant, remote.host);
30086
- return Response.json(await opts.bootstrap(), { headers });
30558
+ return encoded(req, JSON.stringify(await opts.bootstrap()), headers);
30087
30559
  }
30088
30560
  if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
30089
30561
  if (!sameSecret(url.searchParams.get("token"), opts.token))
@@ -30107,6 +30579,9 @@ function createFetch(opts) {
30107
30579
  }
30108
30580
  });
30109
30581
  }
30582
+ if (url.pathname === "/share" && req.method === "POST") {
30583
+ return new Response(null, { status: 303, headers: { location: "/", "cache-control": NO_STORE2 } });
30584
+ }
30110
30585
  const paired = await opts.pairing(req, url, d);
30111
30586
  if (paired)
30112
30587
  return paired;
@@ -30228,7 +30703,7 @@ function createFetch(opts) {
30228
30703
  const hashed = rel.startsWith("/assets/");
30229
30704
  if (existsSync29(file) && Bun.file(file).size > 0) {
30230
30705
  if (rel === "/manifest.json") {
30231
- const ground = opts.manifestGround();
30706
+ const ground = url.searchParams.get("black") === "1" ? "#000000" : opts.manifestGround();
30232
30707
  const manifest = { ...await Bun.file(file).json(), theme_color: ground, background_color: ground };
30233
30708
  return Response.json(manifest, {
30234
30709
  headers: { "content-type": "application/manifest+json", "cache-control": NO_STORE2 }
@@ -30241,13 +30716,26 @@ function createFetch(opts) {
30241
30716
  headers: { "content-type": "text/html; charset=utf-8", "cache-control": NO_STORE2 }
30242
30717
  });
30243
30718
  }
30244
- return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE2 } });
30719
+ if (hashed) {
30720
+ const sibling = pickSibling(file, acceptedEncodings(req.headers.get("accept-encoding")));
30721
+ return new Response(Bun.file(sibling.path), {
30722
+ headers: {
30723
+ "content-type": Bun.file(file).type,
30724
+ "cache-control": IMMUTABLE,
30725
+ vary: "accept-encoding",
30726
+ ...sibling.encoding ? { "content-encoding": sibling.encoding } : {}
30727
+ }
30728
+ });
30729
+ }
30730
+ if (rel === "/index.html")
30731
+ return page(req, file);
30732
+ return new Response(Bun.file(file), { headers: { "cache-control": NO_STORE2 } });
30245
30733
  }
30246
30734
  if (hashed)
30247
30735
  return new Response("not found", { status: 404 });
30248
30736
  const index = join25(opts.shellDist, "index.html");
30249
30737
  if (existsSync29(index))
30250
- return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE2 } });
30738
+ return page(req, index);
30251
30739
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
30252
30740
  }
30253
30741
  return async function fetch2(req, srv) {
@@ -30274,12 +30762,80 @@ function createFetch(opts) {
30274
30762
  };
30275
30763
  }
30276
30764
 
30765
+ // ../daemon/src/server/rows.ts
30766
+ var rowsStampOf = (rowsJson, trunksJson) => Bun.hash(`{"rows":[${rowsJson.join(",")}],"trunks":${trunksJson}}`).toString(16);
30767
+
30768
+ class Published {
30769
+ byId = new Map;
30770
+ ids = [];
30771
+ trunks = {};
30772
+ trunksJson = "{}";
30773
+ stampNow = "";
30774
+ current() {
30775
+ return { rows: this.ids.map((id) => this.byId.get(id).row), trunks: this.trunks, stamp: this.stampNow };
30776
+ }
30777
+ publish(next) {
30778
+ const seen = new Map;
30779
+ const ids = [];
30780
+ const jsons = [];
30781
+ const changed = [];
30782
+ for (const row of next.rows) {
30783
+ const json = JSON.stringify(row);
30784
+ ids.push(row.id);
30785
+ jsons.push(json);
30786
+ if (this.byId.get(row.id)?.json !== json)
30787
+ changed.push(row);
30788
+ seen.set(row.id, { row, json });
30789
+ }
30790
+ const trunksJson = JSON.stringify(next.trunks);
30791
+ const trunksMoved = trunksJson !== this.trunksJson;
30792
+ const orderMoved = ids.length !== this.ids.length || ids.some((id, i) => id !== this.ids[i]);
30793
+ this.byId = seen;
30794
+ this.ids = ids;
30795
+ this.trunks = next.trunks;
30796
+ this.trunksJson = trunksJson;
30797
+ this.stampNow = rowsStampOf(jsons, trunksJson);
30798
+ if (changed.length === 0 && !trunksMoved && !orderMoved)
30799
+ return null;
30800
+ return {
30801
+ t: "worktrees",
30802
+ ids,
30803
+ rows: changed,
30804
+ ...trunksMoved ? { trunks: next.trunks } : {},
30805
+ stamp: this.stampNow
30806
+ };
30807
+ }
30808
+ }
30809
+
30810
+ // ../daemon/src/server/stamps.ts
30811
+ var stampOf = (part) => Bun.hash(JSON.stringify(part)).toString(16);
30812
+ var visitsStamp = (visits) => stampOf(Object.entries(visits).map(([repoId, list]) => [repoId, list.map((e) => [e.path, e.title ?? ""])]));
30813
+ function stampsOf(h) {
30814
+ return {
30815
+ rows: stampOf({ rows: h.rows, trunks: h.trunks }),
30816
+ repos: stampOf(h.repos),
30817
+ visits: visitsStamp(h.visits),
30818
+ themes: stampOf(h.themes)
30819
+ };
30820
+ }
30821
+ function shapeHello(full, known) {
30822
+ if (!known)
30823
+ return full;
30824
+ const { themes, ...rest } = full;
30825
+ const withThemes = sameStamp(known.themes, full.stamps.themes) ? rest : full;
30826
+ const same = sameStamp(known.rows, full.stamps.rows) && sameStamp(known.repos, full.stamps.repos) && sameStamp(known.visits, full.stamps.visits);
30827
+ if (!same)
30828
+ return withThemes;
30829
+ const { rows: _r, trunks: _t, repos: _p, visits: _v, same: _s, ...light } = withThemes;
30830
+ return { ...light, same: true };
30831
+ }
30832
+
30277
30833
  // ../daemon/src/server/ws.ts
30278
30834
  var DROP_ABOVE_BYTES = 4 * 1024 * 1024;
30279
30835
  var DROPPED_NOTICE = `\r
30280
30836
  [toyon] output dropped: this pane was too far behind\r
30281
30837
  `;
30282
- var SENDS = new Set(["chat", "create-worktree", "restore-worktree", "batch-worktrees"]);
30838
+ var SENDS = new Set(["chat", "create-worktree", "restore-worktree"]);
30283
30839
  function sentFrom(json) {
30284
30840
  if (!json || typeof json !== "object")
30285
30841
  return null;
@@ -30313,7 +30869,7 @@ function startServer(opts) {
30313
30869
  };
30314
30870
  const raw = (ws, json) => {
30315
30871
  try {
30316
- ws.send(json);
30872
+ ws.send(json, true);
30317
30873
  ws.data.sent++;
30318
30874
  ws.data.bytes += json.length;
30319
30875
  } catch (e) {
@@ -30404,6 +30960,22 @@ function startServer(opts) {
30404
30960
  agents: s.agents.adapters(),
30405
30961
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
30406
30962
  });
30963
+ const published = new Published;
30964
+ const sendRows = (frame) => {
30965
+ const json = JSON.stringify(frame);
30966
+ for (const ws of sockets)
30967
+ if (ws.data.helloed)
30968
+ raw(ws, json);
30969
+ };
30970
+ const publishRows = () => {
30971
+ try {
30972
+ const frame = published.publish(s.worktrees.snapshot());
30973
+ if (frame)
30974
+ sendRows(frame);
30975
+ } catch (e) {
30976
+ log.warn("ws", "worktrees broadcast failed", e);
30977
+ }
30978
+ };
30407
30979
  let statusesQueued = false;
30408
30980
  const worktreesChanged = () => {
30409
30981
  if (statusesQueued)
@@ -30411,20 +30983,16 @@ function startServer(opts) {
30411
30983
  statusesQueued = true;
30412
30984
  setTimeout(() => {
30413
30985
  statusesQueued = false;
30414
- try {
30415
- const { rows, trunks } = s.worktrees.snapshot();
30416
- broadcast({ t: "worktrees", rows, trunks });
30417
- } catch (e) {
30418
- log.warn("ws", "worktrees broadcast failed", e);
30419
- }
30986
+ publishRows();
30420
30987
  }, 0);
30421
30988
  };
30422
30989
  s.hub.on("worktreesChanged", worktreesChanged);
30423
30990
  s.hub.on("agentStatus", worktreesChanged);
30424
30991
  s.hub.on("agentStopping", worktreesChanged);
30425
- s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos }));
30992
+ s.hub.on("agentHolding", worktreesChanged);
30993
+ s.hub.on("proc", worktreesChanged);
30994
+ s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos, stamp: stampOf(s.state.repos) }));
30426
30995
  s.hub.on("pendingChanged", () => broadcast({ t: "pending-repos", pending: s.repos.pending }));
30427
- s.hub.on("proc", (worktreeId, proc) => broadcast({ t: "proc", worktreeId, proc }));
30428
30996
  s.hub.on("log", (worktreeId, proc, line, retract) => sendTo(worktreeId, retract ? { t: "log", worktreeId, proc, line, retract } : { t: "log", worktreeId, proc, line }));
30429
30997
  s.hub.on("agent", (worktreeId, seq, event) => sendTo(worktreeId, { t: "agent", worktreeId, seq, event }));
30430
30998
  s.hub.on("queue", (worktreeId, items) => sendTo(worktreeId, { t: "queue", worktreeId, items }));
@@ -30463,7 +31031,7 @@ function startServer(opts) {
30463
31031
  const themesChanged = () => {
30464
31032
  const cur = s.themes.current();
30465
31033
  setWaitingColors({ bg: cur.colors.surface0, fg: cur.colors.text1 });
30466
- broadcast({ t: "themes", themes: s.themes.themes, prefs: s.themes.prefs });
31034
+ broadcast({ t: "themes", themes: s.themes.themes, prefs: s.themes.prefs, stamp: stampOf(s.themes.themes) });
30467
31035
  };
30468
31036
  s.hub.on("themesChanged", themesChanged);
30469
31037
  themesChanged();
@@ -30489,23 +31057,26 @@ function startServer(opts) {
30489
31057
  s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
30490
31058
  s.hub.on("failed", (worktreeId, message) => broadcast({ t: "error", message, worktreeId }));
30491
31059
  s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
30492
- s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
31060
+ s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId), stamp: visitsStamp(s.routes.historyAll()) }));
30493
31061
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
30494
31062
  s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
30495
31063
  s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
30496
- const helloFrame = async () => {
31064
+ const helloReads = async () => {
30497
31065
  await s.update.refresh();
30498
- const { rows, trunks } = s.worktrees.snapshot();
30499
- return {
31066
+ return { registry: await s.update.registry(), gitIdentity: await s.repos.gitIdentity() };
31067
+ };
31068
+ const helloNow = (read, known) => {
31069
+ publishRows();
31070
+ const { rows, trunks } = published.current();
31071
+ const parts = { rows, trunks, repos: s.state.repos, visits: s.routes.historyAll(), themes: s.themes.themes };
31072
+ const full = {
30500
31073
  t: "hello",
30501
31074
  version,
30502
31075
  install: s.update.install(),
30503
- registry: await s.update.registry(),
31076
+ registry: read.registry,
30504
31077
  protocol: PROTOCOL_VERSION,
30505
- repos: s.state.repos,
30506
- rows,
30507
- trunks,
30508
- themes: s.themes.themes,
31078
+ ...parts,
31079
+ stamps: stampsOf(parts),
30509
31080
  themePrefs: s.themes.prefs,
30510
31081
  agents: s.readiness.describe(),
30511
31082
  defaultAgent: s.agents.defaultId(s.state.defaultAgent),
@@ -30519,15 +31090,15 @@ function startServer(opts) {
30519
31090
  machines: s.state.machines,
30520
31091
  letIn: s.state.letIn,
30521
31092
  knocks: opts.knocks.pending(),
30522
- gitIdentity: await s.repos.gitIdentity(),
31093
+ gitIdentity: read.gitIdentity,
30523
31094
  pending: s.repos.pending,
30524
- visits: s.routes.historyAll(),
30525
31095
  self: s.self.get(),
30526
31096
  update: s.update.get(),
30527
31097
  drafts: s.drafts.all(),
30528
31098
  attachments: s.drafts.allAttachments(),
30529
31099
  managed: managedView(opts.managed)
30530
31100
  };
31101
+ return shapeHello(full, known);
30531
31102
  };
30532
31103
  let branded = false;
30533
31104
  const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${token}`);
@@ -30567,7 +31138,7 @@ function startServer(opts) {
30567
31138
  }),
30568
31139
  managed: { source: opts.managed.source, hash: opts.managed.hash },
30569
31140
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
30570
- bootstrap: helloFrame,
31141
+ bootstrap: async () => helloNow(await helloReads(), null),
30571
31142
  manifestGround: () => s.themes.current().colors.surface1,
30572
31143
  open: (path) => s.opens.open(path),
30573
31144
  restart: (now) => s.restarter.request({ now }),
@@ -30578,6 +31149,7 @@ function startServer(opts) {
30578
31149
  changelog: () => Bun.file(opts.changelog).text().catch(() => null)
30579
31150
  }),
30580
31151
  websocket: {
31152
+ perMessageDeflate: true,
30581
31153
  async open(ws) {
30582
31154
  const preview = ws.data.preview;
30583
31155
  if (preview) {
@@ -30591,7 +31163,9 @@ function startServer(opts) {
30591
31163
  }
30592
31164
  sockets.add(ws);
30593
31165
  link(ws);
30594
- send(ws, await helloFrame());
31166
+ const read = await helloReads();
31167
+ send(ws, helloNow(read, ws.data.known ?? null));
31168
+ ws.data.helloed = true;
30595
31169
  fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
30596
31170
  for (const o of s.opens.takePending())
30597
31171
  send(ws, openedFrame(o));
@@ -30889,7 +31463,7 @@ function discover(dir) {
30889
31463
  import { homedir as homedir14 } from "os";
30890
31464
  async function latestVersion() {
30891
31465
  const registry = await npmRegistry();
30892
- const r = await run("npm", ["view", "toyon", "version", "--json"], homedir14());
31466
+ const r = await run("npm", ["view", "toyon", "version", "--json"], homedir14(), undefined, { timeoutMs: NPM_MS });
30893
31467
  if (!r.ok)
30894
31468
  return { version: null, registry };
30895
31469
  try {
@@ -30899,8 +31473,9 @@ async function latestVersion() {
30899
31473
  return { version: null, registry };
30900
31474
  }
30901
31475
  }
31476
+ var NPM_MS = 30000;
30902
31477
  async function npmRegistry() {
30903
- const config = await run("npm", ["config", "get", "registry"], homedir14());
31478
+ const config = await run("npm", ["config", "get", "registry"], homedir14(), undefined, { timeoutMs: NPM_MS });
30904
31479
  return config.ok && config.out !== "" ? config.out : "the npm registry";
30905
31480
  }
30906
31481
  var LOG_POINTER = /complete log of this run/i;
@@ -30939,6 +31514,7 @@ class UpdateService {
30939
31514
  latest = null;
30940
31515
  unreachable = null;
30941
31516
  registryName = null;
31517
+ registryAsk = null;
30942
31518
  seen = null;
30943
31519
  seenAt = 0;
30944
31520
  installing = false;
@@ -30969,6 +31545,7 @@ class UpdateService {
30969
31545
  d.hub.on("holdsChanged", () => {
30970
31546
  this.busyAt = this.now();
30971
31547
  });
31548
+ fireAndForget("update", this.registry(), "npm registry");
30972
31549
  }
30973
31550
  async tick() {
30974
31551
  await this.refresh();
@@ -30993,7 +31570,15 @@ class UpdateService {
30993
31570
  async registry() {
30994
31571
  if (this.d.method === "none" || this.d.managedBy)
30995
31572
  return null;
30996
- this.registryName ??= await this.d.registry();
31573
+ if (this.registryName !== null)
31574
+ return this.registryName;
31575
+ this.registryAsk ??= this.d.registry();
31576
+ try {
31577
+ this.registryName = await this.registryAsk;
31578
+ } catch (e) {
31579
+ this.registryAsk = null;
31580
+ throw e;
31581
+ }
30997
31582
  return this.registryName;
30998
31583
  }
30999
31584
  async check() {
@@ -31572,6 +32157,9 @@ function saidRows(entries) {
31572
32157
  if (event.type === "user-message") {
31573
32158
  turnTs = event.ts;
31574
32159
  push(seq, "user", event.text, event.ts);
32160
+ } else if (event.type === "handed") {
32161
+ turnTs = event.ts;
32162
+ push(seq, "user", event.body ?? event.text, event.ts);
31575
32163
  } else if (event.type === "turn-start") {
31576
32164
  turnTs = event.ts;
31577
32165
  } else if (event.type === "text-delta") {
@@ -31705,7 +32293,7 @@ function lastAsked(entries) {
31705
32293
  const e = entries[i].event;
31706
32294
  if (e.type === "fix-asked")
31707
32295
  return e.kind;
31708
- if (e.type === "user-message")
32296
+ if (e.type === "user-message" || e.type === "handed")
31709
32297
  return;
31710
32298
  }
31711
32299
  return;
@@ -31810,240 +32398,6 @@ class FixService {
31810
32398
  }
31811
32399
  }
31812
32400
 
31813
- // ../daemon/src/worktrees/handoff.ts
31814
- import { randomUUID as randomUUID2 } from "crypto";
31815
- var MESSAGE_MAX = 20000;
31816
- var TITLE_MAX2 = 200;
31817
- var PROJECT_MAX = 200;
31818
- var PER_TURN_MAX = 3;
31819
- var refused = (text) => ({ text, isError: true });
31820
- function openProposals(entries) {
31821
- const open = new Map;
31822
- for (const { event } of entries) {
31823
- if (event.type === "handoff-proposed")
31824
- open.set(event.id, event);
31825
- else if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
31826
- open.delete(event.id);
31827
- }
31828
- return [...open.values()];
31829
- }
31830
- function proposedThisTurn(entries) {
31831
- let n = 0;
31832
- for (let i = entries.length - 1;i >= 0; i--) {
31833
- const e = entries[i].event;
31834
- if (e.type === "turn-start")
31835
- break;
31836
- if (e.type === "handoff-proposed")
31837
- n++;
31838
- }
31839
- return n;
31840
- }
31841
-
31842
- class HandoffService {
31843
- d;
31844
- constructor(d) {
31845
- this.d = d;
31846
- d.hub.on("landed", (worktreeId) => this.landed(worktreeId));
31847
- }
31848
- targets(worktreeId) {
31849
- const own = this.d.state.worktree(worktreeId)?.repoId;
31850
- return this.d.state.repos.filter((r) => r.id !== own);
31851
- }
31852
- resolve(worktreeId, project) {
31853
- const q = project.trim();
31854
- const others = this.targets(worktreeId);
31855
- if (others.length === 0)
31856
- return { refused: "No other project is open in Toyon, so there is nothing to hand off to." };
31857
- const names = others.map((r) => r.name).join(", ");
31858
- if (!q)
31859
- return { refused: `Name the project to continue in. Open projects: ${names}.` };
31860
- const found = match(others, q);
31861
- if (found.length === 1)
31862
- return { repo: found[0] };
31863
- if (found.length > 1) {
31864
- return {
31865
- refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
31866
- };
31867
- }
31868
- const wt = this.d.state.worktree(worktreeId);
31869
- const own = wt ? this.d.state.repo(wt.repoId) : undefined;
31870
- if (own && match([own], q).length === 1) {
31871
- return {
31872
- refused: `${own.name} is this worktree's own project; a handoff goes to another one. Open projects: ${names}.`
31873
- };
31874
- }
31875
- return { refused: `No open project is called "${q}". Open projects: ${names}.` };
31876
- }
31877
- tool(worktreeId) {
31878
- const names = this.targets(worktreeId).map((r) => r.name);
31879
- const open = names.length ? `Open projects you can name: ${names.join(", ")}.` : "No other project is open right now.";
31880
- return {
31881
- name: HANDOFF_TOOL,
31882
- description: `Propose continuing the current task in another project Toyon has open, when a change belongs there and not in this worktree. Nothing starts: a card with your message appears in this chat and the person decides. ${open}`,
31883
- inputSchema: {
31884
- type: "object",
31885
- properties: {
31886
- project: { type: "string", description: "The other project's name as listed, or its path." },
31887
- message: {
31888
- type: "string",
31889
- description: "What that project's agent starts from: what is needed and why, how to reproduce it, and the paths in this worktree that depend on it. It is sent as that agent's first message."
31890
- },
31891
- title: { type: "string", description: "A short name for the new worktree, a few words." }
31892
- },
31893
- required: ["project", "message"],
31894
- additionalProperties: false
31895
- }
31896
- };
31897
- }
31898
- async call(worktreeId, args) {
31899
- const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
31900
- const shaped = a && typeof a.project === "string" && typeof a.message === "string" && (a.title === undefined || typeof a.title === "string");
31901
- if (!shaped)
31902
- return refused("handoff takes { project: string, message: string, title?: string }.");
31903
- return this.propose(worktreeId, { project: a.project, message: a.message, ...a.title ? { title: a.title } : {} }, "agent");
31904
- }
31905
- propose(worktreeId, p, by) {
31906
- const wt = this.d.state.worktree(worktreeId);
31907
- if (!wt)
31908
- throw new UserError("that chat is gone");
31909
- if (p.project.length > PROJECT_MAX)
31910
- return refused(`The project name is over ${PROJECT_MAX} characters.`);
31911
- if ((p.title ?? "").length > TITLE_MAX2)
31912
- return refused(`The title is over ${TITLE_MAX2} characters; a few words.`);
31913
- if (p.message.length > MESSAGE_MAX) {
31914
- return refused(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
31915
- }
31916
- const target = this.resolve(worktreeId, p.project);
31917
- if ("refused" in target)
31918
- return refused(target.refused);
31919
- const message = p.message.trim();
31920
- if (!message)
31921
- return refused("Write the message the other project's agent should start from.");
31922
- const agent = this.d.runtime.ensureAgent(wt).agent;
31923
- const entries = agent.transcript();
31924
- if (openProposals(entries).length > 0) {
31925
- return refused("A handoff is already waiting for the person on this chat; finish your turn.");
31926
- }
31927
- if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
31928
- return refused("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
31929
- }
31930
- const title = p.title?.trim();
31931
- const { repo } = target;
31932
- agent.note({
31933
- type: "handoff-proposed",
31934
- id: randomUUID2(),
31935
- repo: { id: repo.id, name: repo.name, path: repo.path },
31936
- message,
31937
- ...title ? { title } : {},
31938
- by,
31939
- mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
31940
- ts: Date.now()
31941
- });
31942
- return { text: "Proposed to the person as a card; they decide. Finish your turn." };
31943
- }
31944
- async answer(worktreeId, id, go, note) {
31945
- const wt = this.d.state.worktree(worktreeId);
31946
- if (!wt)
31947
- throw new UserError("that chat is gone");
31948
- const agent = this.d.runtime.ensureAgent(wt).agent;
31949
- const entries = agent.transcript();
31950
- const proposed = entries.find(({ event }) => event.type === "handoff-proposed" && event.id === id)?.event;
31951
- if (!proposed)
31952
- throw new UserError("That card is not in this chat");
31953
- if (!openProposals(entries).some((p) => p.id === id)) {
31954
- return log.debug(worktreeId, `answer for a handoff that already closed (${id})`);
31955
- }
31956
- if (!go) {
31957
- agent.note({ type: "handoff-declined", id, ts: Date.now() });
31958
- return;
31959
- }
31960
- agent.note({ type: "handoff-go", id, ts: Date.now() });
31961
- try {
31962
- const repo = this.d.state.repo(proposed.repo.id);
31963
- if (!repo)
31964
- throw new UserError(`${proposed.repo.name} is no longer open in Toyon`);
31965
- const origin = this.d.state.requireRepo(wt.repoId);
31966
- const added = note?.trim() || undefined;
31967
- const prompt = handoffPrompt(proposed.message, added, { path: wt.path, project: origin.name, branch: wt.branch });
31968
- const made = await this.d.create(repo.id, prompt, {
31969
- ...proposed.title ? { title: proposed.title } : {},
31970
- mode: proposed.mode,
31971
- from: { kind: "worktree", ref: wt.branch, origin: { id: wt.id, repoId: wt.repoId } }
31972
- });
31973
- agent.note({
31974
- type: "handoff",
31975
- id,
31976
- worktreeId: made.id,
31977
- repoId: repo.id,
31978
- repoName: repo.name,
31979
- title: made.title,
31980
- ts: Date.now()
31981
- });
31982
- } catch (e) {
31983
- const reason = e instanceof Error ? e.message : String(e);
31984
- agent.note({ type: "handoff-declined", id, outcome: "failed", reason, ts: Date.now() });
31985
- throw e;
31986
- }
31987
- }
31988
- async ask(worktreeId, repoId, text) {
31989
- const wt = this.d.state.requireWorktree(worktreeId);
31990
- const repo = this.d.state.repo(repoId);
31991
- if (!repo)
31992
- throw new UserError("no such project");
31993
- if (repo.id === wt.repoId)
31994
- throw new UserError(`${repo.name} is this worktree's own project`);
31995
- const agent = this.d.runtime.agentFor(worktreeId);
31996
- if (!agent)
31997
- throw new UserError("worktree still starting; try again in a moment");
31998
- if (agent.mcpTools === null)
31999
- await agent.warm();
32000
- if (agent.mcpTools === true) {
32001
- agent.send(handoffAskPrompt(repo, text), { asked: { kind: "handoff", why: `continue in ${repo.name}` } });
32002
- return;
32003
- }
32004
- const own = text?.trim();
32005
- if (!own)
32006
- throw new UserError(`Write what ${repo.name} should do first`);
32007
- const r = this.propose(worktreeId, { project: repo.path, message: own }, "person");
32008
- if (r.isError)
32009
- throw new UserError(r.text);
32010
- }
32011
- landed(worktreeId) {
32012
- const wt = this.d.state.worktree(worktreeId);
32013
- if (wt?.from?.kind !== "worktree")
32014
- return;
32015
- const origin = this.d.state.worktree(wt.from.origin.id);
32016
- if (origin?.kind !== "worktree")
32017
- return;
32018
- const repo = this.d.state.repo(wt.repoId);
32019
- if (!repo)
32020
- return;
32021
- this.d.runtime.ensureAgent(origin).agent.note({
32022
- type: "handoff-landed",
32023
- worktreeId: wt.id,
32024
- repoName: repo.name,
32025
- title: wt.title,
32026
- ...wt.pr?.url ? { url: wt.pr.url } : {},
32027
- ts: Date.now()
32028
- });
32029
- }
32030
- }
32031
- function match(repos, q) {
32032
- const lower = q.toLowerCase();
32033
- const ladder = [
32034
- (r) => r.path === q,
32035
- (r) => r.name === q,
32036
- (r) => r.name.toLowerCase() === lower,
32037
- (r) => r.name.toLowerCase().startsWith(lower)
32038
- ];
32039
- for (const rung of ladder) {
32040
- const found = repos.filter(rung);
32041
- if (found.length > 0)
32042
- return found;
32043
- }
32044
- return [];
32045
- }
32046
-
32047
32401
  // ../daemon/src/worktrees/landing.ts
32048
32402
  var TAIL_CHARS = 400;
32049
32403
  var DIFF_CHARS = 2000;
@@ -32395,6 +32749,1137 @@ async function recentSubjects(path, defaultBr) {
32395
32749
  `).filter(Boolean) : [];
32396
32750
  }
32397
32751
 
32752
+ // ../daemon/src/worktrees/lookup.ts
32753
+ import { readFile as readFile3 } from "fs/promises";
32754
+ var ARCHIVED_MAX = 20;
32755
+ var ASKED_MAX = 200;
32756
+
32757
+ class WorktreeLookup {
32758
+ d;
32759
+ constructor(d) {
32760
+ this.d = d;
32761
+ }
32762
+ tool(worktreeId) {
32763
+ const wt = this.d.state.worktree(worktreeId);
32764
+ const open = wt ? this.siblings(wt).length : 0;
32765
+ const gone = wt ? this.d.archived(wt.repoId).length : 0;
32766
+ return {
32767
+ name: WORKTREES_TOOL,
32768
+ description: `List the other worktrees of this project, open and archived, by title and branch, with where each chat is on disk and what it was about. Call it to find which worktree worked on a topic, a ticket or a feature, or to see what else is open. Right now ${open} other ${open === 1 ? "worktree is" : "worktrees are"} open and ${gone} ${gone === 1 ? "is" : "are"} archived. Without a query the open ones are listed and the newest ${ARCHIVED_MAX} archived; with one, only the worktrees whose title, branch or chat mention every word of it.`,
32769
+ inputSchema: {
32770
+ type: "object",
32771
+ properties: {
32772
+ query: {
32773
+ type: "string",
32774
+ description: "Words to look for, such as a ticket key, a product name or a feature: a worktree is listed when its title, branch or what the person wrote in its chat contains every word, in any case. Leave it out to list everything."
32775
+ }
32776
+ }
32777
+ }
32778
+ };
32779
+ }
32780
+ async call(worktreeId, args) {
32781
+ const wt = this.d.state.worktree(worktreeId);
32782
+ if (!wt)
32783
+ return { text: "unknown worktree", isError: true };
32784
+ const raw = args && typeof args === "object" ? args.query : undefined;
32785
+ const query = typeof raw === "string" ? raw.trim() : "";
32786
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
32787
+ const open = await Promise.all(this.siblings(wt).map((w) => this.read(w)));
32788
+ const archived = await Promise.all(this.d.archived(wt.repoId).map((a) => this.readArchived(a)));
32789
+ open.sort((a, b) => b.active - a.active);
32790
+ if (words.length === 0) {
32791
+ const shown = [...open, ...archived.slice(0, ARCHIVED_MAX)];
32792
+ const more = archived.length - ARCHIVED_MAX;
32793
+ const head = `${open.length} other ${open.length === 1 ? "worktree" : "worktrees"} of this project ${open.length === 1 ? "is" : "are"} open and ${archived.length} ${archived.length === 1 ? "is" : "are"} archived.`;
32794
+ const tail = more > 0 ? `
32795
+
32796
+ ${more} older archived ${more === 1 ? "worktree is" : "worktrees are"} not listed; pass a query to search them.` : "";
32797
+ return { text: shown.length ? `${head}
32798
+
32799
+ ${shown.map((c) => row(c, [])).join(`
32800
+
32801
+ `)}${tail}` : head };
32802
+ }
32803
+ const hits = [...open, ...archived].filter((c) => matches(c, words));
32804
+ if (hits.length === 0) {
32805
+ return {
32806
+ text: `No worktree of this project matches "${query}". Searched ${open.length} open and ${archived.length} archived by title, branch and what the person wrote in each chat.`
32807
+ };
32808
+ }
32809
+ return {
32810
+ text: `${hits.length} ${hits.length === 1 ? "worktree" : "worktrees"} of this project ${hits.length === 1 ? "matches" : "match"} "${query}".
32811
+
32812
+ ${hits.map((c) => row(c, words)).join(`
32813
+
32814
+ `)}`
32815
+ };
32816
+ }
32817
+ siblings(wt) {
32818
+ return this.d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree");
32819
+ }
32820
+ async read(w) {
32821
+ const transcript = transcriptPathFor(this.d.transcriptsDir, w.id);
32822
+ const said = await userMessages(transcript);
32823
+ return {
32824
+ sibling: { branch: w.branch, title: w.title, path: w.path, transcript },
32825
+ said,
32826
+ active: said.at(-1)?.ts ?? w.createdAt
32827
+ };
32828
+ }
32829
+ async readArchived(a) {
32830
+ const said = await userMessages(a.transcript);
32831
+ return {
32832
+ sibling: {
32833
+ branch: a.branch,
32834
+ title: a.title,
32835
+ path: a.path,
32836
+ transcript: a.transcript,
32837
+ archived: { at: a.archivedAt, landed: a.landed, restorable: a.restorable }
32838
+ },
32839
+ said,
32840
+ prompt: a.prompt,
32841
+ active: said.at(-1)?.ts ?? a.archivedAt
32842
+ };
32843
+ }
32844
+ }
32845
+ async function userMessages(transcript) {
32846
+ let text;
32847
+ try {
32848
+ text = await readFile3(transcript, "utf8");
32849
+ } catch {
32850
+ return [];
32851
+ }
32852
+ const out = [];
32853
+ for (const line of text.split(`
32854
+ `)) {
32855
+ if (!line.includes('"user-message"'))
32856
+ continue;
32857
+ try {
32858
+ const e = JSON.parse(line)?.event;
32859
+ if (e?.type === "user-message" && typeof e.text === "string")
32860
+ out.push({ text: e.text, ts: Number(e.ts) || 0 });
32861
+ } catch {}
32862
+ }
32863
+ return out;
32864
+ }
32865
+ function matches(c, words) {
32866
+ const corpus = [c.sibling.title, c.sibling.branch, c.prompt ?? "", ...c.said.map((s) => s.text)].join(`
32867
+ `).toLowerCase();
32868
+ return words.every((w) => corpus.includes(w));
32869
+ }
32870
+ function row(c, words) {
32871
+ const parts = [siblingParagraph(c.sibling)];
32872
+ const first = c.said[0]?.text ?? c.prompt;
32873
+ if (first)
32874
+ parts.push(`The person first asked: "${clip2(first)}"${first.length > ASKED_MAX ? " (cut there)." : ""}`);
32875
+ if (c.said.length > 0)
32876
+ parts.push(`They last wrote there on ${dayOf(c.active)}.`);
32877
+ if (words.length > 0 && c.said.length > 0) {
32878
+ const mentioning = c.said.filter((s) => {
32879
+ const t = s.text.toLowerCase();
32880
+ return words.some((w) => t.includes(w));
32881
+ }).length;
32882
+ if (mentioning > 0)
32883
+ parts.push(`${mentioning} of their ${c.said.length} messages mention the query.`);
32884
+ }
32885
+ return parts.join(" ");
32886
+ }
32887
+ function clip2(text) {
32888
+ const one = text.replace(/\s+/g, " ").trim();
32889
+ return one.length > ASKED_MAX ? one.slice(0, ASKED_MAX).trimEnd() : one;
32890
+ }
32891
+
32892
+ // ../daemon/src/worktrees/pieces.ts
32893
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
32894
+ import { readFileSync as readFileSync20 } from "fs";
32895
+ import { join as join28 } from "path";
32896
+
32897
+ // ../daemon/src/worktrees/proposals.ts
32898
+ var PER_TURN_MAX = 3;
32899
+ function proposedThisTurn(entries) {
32900
+ let n = 0;
32901
+ for (let i = entries.length - 1;i >= 0; i--) {
32902
+ const e = entries[i].event;
32903
+ if (e.type === "turn-start")
32904
+ break;
32905
+ if (e.type === "pieces-proposed")
32906
+ n++;
32907
+ }
32908
+ return n;
32909
+ }
32910
+ function matchBy(items, q, keys) {
32911
+ if (!q.trim())
32912
+ return [];
32913
+ const lower = q.toLowerCase();
32914
+ const rungs = [
32915
+ (v) => v === q,
32916
+ (v) => v.toLowerCase() === lower,
32917
+ (v) => v.toLowerCase().startsWith(lower)
32918
+ ];
32919
+ for (const rung of rungs) {
32920
+ const found = items.filter((item) => keys.some((key) => rung(key(item) ?? "")));
32921
+ if (found.length > 0)
32922
+ return found;
32923
+ }
32924
+ return [];
32925
+ }
32926
+
32927
+ // ../daemon/src/worktrees/pieces.ts
32928
+ function handedOrigin(origin, project) {
32929
+ return {
32930
+ id: origin.id,
32931
+ repoId: origin.repoId,
32932
+ branch: origin.branch,
32933
+ title: origin.title,
32934
+ ...project ? { project } : {}
32935
+ };
32936
+ }
32937
+ function pieceDoc(piece) {
32938
+ return `# ${piece.title}
32939
+
32940
+ ${piece.plan.trim()}
32941
+ `;
32942
+ }
32943
+ var PIECES_MAX = 6;
32944
+ var PLAN_MAX = 20000;
32945
+ var ATTACH_MAX = 10;
32946
+ var PROJECT_MAX = 200;
32947
+ var refused = (text) => ({ text, isError: true });
32948
+ var HELD = "Handed to the person; the list shows beside your plan and they decide where each piece goes. Ask for plan approval now.";
32949
+ var SHOWN = "Handed to the person as a card; they decide. Finish your turn.";
32950
+ var SHOWN_HERE = "Handed to the person as a card. This worktree holds no work yet, so the card leads with doing it here; they decide. Finish your turn.";
32951
+ function openPlanCard(entries) {
32952
+ const open = new Map;
32953
+ for (const { event } of entries) {
32954
+ if (event.type === "agent-permission" && event.plan)
32955
+ open.set(event.id, event);
32956
+ else if (event.type === "agent-ask-end")
32957
+ open.delete(event.id);
32958
+ }
32959
+ return [...open.values()].at(-1) ?? null;
32960
+ }
32961
+ function sectionText(markdown, heading) {
32962
+ const norm = (s) => s.toLowerCase().replace(/[`*_~]/g, "").replace(/[^a-z0-9]+/g, " ").trim();
32963
+ const want = norm(heading);
32964
+ if (!want)
32965
+ return null;
32966
+ const lines = markdown.split(`
32967
+ `);
32968
+ const heads = [];
32969
+ for (let i = 0;i < lines.length; i++) {
32970
+ const m = /^(#{1,6})\s+(.*)$/.exec(lines[i] ?? "");
32971
+ const have = m ? norm(m[2] ?? "") : "";
32972
+ if (m && have)
32973
+ heads.push({ i, level: m[1].length, have });
32974
+ }
32975
+ const rungs = [
32976
+ (have) => have === want,
32977
+ (have) => have.includes(want),
32978
+ (have) => have.length >= 12 && want.includes(have)
32979
+ ];
32980
+ for (const rung of rungs) {
32981
+ const hit = heads.find((h) => rung(h.have));
32982
+ if (!hit)
32983
+ continue;
32984
+ const { i, level } = hit;
32985
+ let end = lines.length;
32986
+ for (let j = i + 1;j < lines.length; j++) {
32987
+ const n = /^(#{1,6})\s/.exec(lines[j] ?? "");
32988
+ if (n && n[1].length <= level) {
32989
+ end = j;
32990
+ break;
32991
+ }
32992
+ }
32993
+ const text = lines.slice(i, end).join(`
32994
+ `).trim();
32995
+ return text.length > PLAN_MAX ? text.slice(0, PLAN_MAX) : text;
32996
+ }
32997
+ return null;
32998
+ }
32999
+ function hashText(text) {
33000
+ return createHash5("sha1").update(text.trim()).digest("hex").slice(0, 16);
33001
+ }
33002
+ function heldUnshown(entries) {
33003
+ const shown = new Set;
33004
+ for (const { event } of entries)
33005
+ if (event.type === "pieces-shown")
33006
+ shown.add(event.id);
33007
+ return openPieces(entries).find((p) => p.held && !shown.has(p.id)) ?? null;
33008
+ }
33009
+ function openPieces(entries) {
33010
+ const open = new Map;
33011
+ for (const { event } of entries) {
33012
+ if (event.type === "pieces-proposed")
33013
+ open.set(event.id, event);
33014
+ else if (event.type === "pieces-go" || event.type === "pieces" || event.type === "pieces-declined")
33015
+ open.delete(event.id);
33016
+ }
33017
+ return [...open.values()];
33018
+ }
33019
+ function listFile(entries, id) {
33020
+ let file;
33021
+ for (const { event } of entries) {
33022
+ if ((event.type === "pieces-proposed" || event.type === "pieces-shown") && event.id === id && event.plan) {
33023
+ file = event.plan;
33024
+ } else if (event.type === "agent-permission" && event.pieces === id && event.plan)
33025
+ file = event.plan;
33026
+ }
33027
+ return file;
33028
+ }
33029
+ function effectiveFate(entries, proposal, piece) {
33030
+ let fate = piece.fate;
33031
+ for (const { event } of entries) {
33032
+ if (event.type === "pieces-fate" && event.id === proposal.id && event.pieceId === piece.id)
33033
+ fate = event.fate;
33034
+ }
33035
+ return fate;
33036
+ }
33037
+ var LABEL = /^(image|paste|pasted text|file|element)\s+(\d+)$/i;
33038
+ var KIND_OF = {
33039
+ image: "image",
33040
+ paste: "paste",
33041
+ "pasted text": "paste",
33042
+ file: "file",
33043
+ element: "pick"
33044
+ };
33045
+
33046
+ class PiecesService {
33047
+ d;
33048
+ constructor(d) {
33049
+ this.d = d;
33050
+ d.hub.on("piecesDecided", (worktreeId, id, approved) => fireAndForget(worktreeId, approved ? this.act(worktreeId, id, undefined, true) : this.decline(worktreeId, id), "pieces"));
33051
+ d.hub.on("planUnsplit", (worktreeId, markdown, planPath) => fireAndForget(worktreeId, this.read(worktreeId, markdown, planPath), "pieces read off the plan"));
33052
+ d.hub.on("treeChanged", (worktreeId) => fireAndForget(worktreeId, this.stale(worktreeId), "pieces stale"));
33053
+ d.hub.on("landed", (worktreeId) => fireAndForget(worktreeId, this.landed(worktreeId), "pieces after landing"));
33054
+ d.hub.on("worktreesChanged", () => this.sweep());
33055
+ this.sweep();
33056
+ }
33057
+ siblings(wt, repoId = wt.repoId) {
33058
+ return this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree" && w.id !== wt.id);
33059
+ }
33060
+ otherProjects(wt) {
33061
+ return this.d.state.repos.filter((r) => r.id !== wt.repoId);
33062
+ }
33063
+ resolveProject(wt, project) {
33064
+ const q = project.trim();
33065
+ const others = this.otherProjects(wt);
33066
+ if (others.length === 0)
33067
+ return { refused: "no other project is open in Toyon." };
33068
+ const names = others.map((r) => r.name).join(", ");
33069
+ if (!q)
33070
+ return { refused: `name the project. Open projects: ${names}.` };
33071
+ const found = matchRepos(others, q);
33072
+ if (found.length === 1)
33073
+ return { repo: found[0] };
33074
+ if (found.length > 1) {
33075
+ return {
33076
+ refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
33077
+ };
33078
+ }
33079
+ const own = this.d.state.repo(wt.repoId);
33080
+ if (own && matchRepos([own], q).length === 1) {
33081
+ return { refused: `${own.name} is this worktree's own project; leave \`project\` out for a worktree here.` };
33082
+ }
33083
+ return { refused: `no open project is called "${q}". Open projects: ${names}.` };
33084
+ }
33085
+ tool(worktreeId) {
33086
+ const wt = this.d.state.worktree(worktreeId);
33087
+ const names = wt ? this.siblings(wt).map((w) => `${w.branch}, "${w.title}"`) : [];
33088
+ const open = names.length ? `Open worktrees of this project you can name in \`after\` or \`to\`: ${names.join("; ")}.` : "No other worktree of this project is open right now.";
33089
+ const projects = wt ? this.otherProjects(wt).map((r) => r.name) : [];
33090
+ const elsewhere = projects.length ? ` Other projects open in Toyon you can name in \`project\`: ${projects.join(", ")}.` : "";
33091
+ return {
33092
+ name: SPLIT_TOOL,
33093
+ description: `Hand Toyon the pieces of the work you are planning, or one piece you came across: each goes on this worktree, a new worktree of this project or of another open one, or an open worktree already on that part; the person decides on the list beside your plan. Nothing starts. ${open}${elsewhere}`,
33094
+ inputSchema: {
33095
+ type: "object",
33096
+ properties: {
33097
+ pieces: {
33098
+ type: "array",
33099
+ minItems: 1,
33100
+ maxItems: PIECES_MAX,
33101
+ items: {
33102
+ type: "object",
33103
+ properties: {
33104
+ title: {
33105
+ type: "string",
33106
+ description: `A name for the piece, the way a teammate would say it: ${NAME_SHAPE}. It titles the worktree.`
33107
+ },
33108
+ plan: {
33109
+ type: "string",
33110
+ description: "The plan its agent builds from, in markdown that stands on its own: what is needed and why, the paths that depend on it, and what done looks like."
33111
+ },
33112
+ attachments: {
33113
+ type: "array",
33114
+ items: { type: "string" },
33115
+ description: 'The attachments from this chat the piece needs, by their labels as captioned: "Image 2", "Paste 1".'
33116
+ },
33117
+ where: {
33118
+ type: "string",
33119
+ enum: ["here", "worktree", "send"],
33120
+ description: "here: you do it in this worktree. worktree: a new worktree, of this project or of the one `project` names. send: an open worktree already working on that part."
33121
+ },
33122
+ after: {
33123
+ type: "string",
33124
+ description: 'worktree: what the piece waits for before it starts: a branch, another piece of this list by its title, or "this" for your own branch. here: what your own work on the piece waits for, a branch or another piece of this list; Toyon shows the wait under the box until it lands.'
33125
+ },
33126
+ to: { type: "string", description: "send only: the open worktree it goes to, by branch or title." },
33127
+ project: {
33128
+ type: "string",
33129
+ description: "Another project open in Toyon, by its name as listed or its path: the worktree is made there, or `to` is one of its worktrees. Leave out for this project."
33130
+ }
33131
+ },
33132
+ required: ["title", "plan", "where"],
33133
+ additionalProperties: false
33134
+ }
33135
+ }
33136
+ },
33137
+ required: ["pieces"],
33138
+ additionalProperties: false
33139
+ }
33140
+ };
33141
+ }
33142
+ async call(worktreeId, args) {
33143
+ const shape = `split takes { pieces: [{ title: string, plan: string, where: "here" | "worktree" | "send", attachments?: string[], after?: string, to?: string, project?: string }] }, 1 to ${PIECES_MAX} pieces.`;
33144
+ const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
33145
+ const list = a && Array.isArray(a.pieces) ? a.pieces : null;
33146
+ if (!list || list.length === 0)
33147
+ return refused(shape);
33148
+ const pieces = [];
33149
+ for (const raw of list) {
33150
+ const p = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
33151
+ const shaped = p && typeof p.title === "string" && typeof p.plan === "string" && (p.where === "here" || p.where === "worktree" || p.where === "send") && (p.attachments === undefined || Array.isArray(p.attachments) && p.attachments.every((l) => typeof l === "string")) && (p.after === undefined || typeof p.after === "string") && (p.to === undefined || typeof p.to === "string") && (p.project === undefined || typeof p.project === "string");
33152
+ if (!shaped)
33153
+ return refused(shape);
33154
+ pieces.push({
33155
+ title: p.title,
33156
+ plan: p.plan,
33157
+ where: p.where,
33158
+ ...p.attachments ? { attachments: p.attachments } : {},
33159
+ ...p.after !== undefined ? { after: p.after } : {},
33160
+ ...p.to !== undefined ? { to: p.to } : {},
33161
+ ...p.project !== undefined ? { project: p.project } : {}
33162
+ });
33163
+ }
33164
+ return this.propose(worktreeId, pieces, "agent");
33165
+ }
33166
+ async propose(worktreeId, raw, by = "agent", read) {
33167
+ const wt = this.d.state.worktree(worktreeId);
33168
+ if (!wt)
33169
+ throw new UserError("that chat is gone");
33170
+ if (raw.length > PIECES_MAX)
33171
+ return refused(`At most ${PIECES_MAX} pieces in one list; fold the rest into \`here\`.`);
33172
+ const agent = this.d.runtime.ensureAgent(wt).agent;
33173
+ const entries = agent.transcript();
33174
+ const pieces = [];
33175
+ for (const [i, p] of raw.entries()) {
33176
+ const title = sentenceCase(p.title.trim());
33177
+ const plan = p.plan.trim();
33178
+ if (!title)
33179
+ return refused(`Piece ${i + 1} has no title.`);
33180
+ const over = overNameShape(title);
33181
+ if (over)
33182
+ return refused(`Piece ${i + 1}'s title ${over}`);
33183
+ if (!plan)
33184
+ return refused(`Piece ${i + 1} ("${title}") has no plan; write the plan its agent builds from.`);
33185
+ if (plan.length > PLAN_MAX) {
33186
+ return refused(`Piece ${i + 1}'s plan is over ${PLAN_MAX} characters; cut it to what its agent needs.`);
33187
+ }
33188
+ if ((p.attachments?.length ?? 0) > ATTACH_MAX) {
33189
+ return refused(`Piece ${i + 1} names more than ${ATTACH_MAX} attachments.`);
33190
+ }
33191
+ if ((p.project?.length ?? 0) > PROJECT_MAX)
33192
+ return refused(`Piece ${i + 1}'s project name is too long.`);
33193
+ const attachments = [];
33194
+ for (const label of p.attachments ?? []) {
33195
+ const ref = attachmentNamed(entries, label);
33196
+ if (!ref)
33197
+ return refused(`This chat has no attachment "${label.trim()}"; name them as the messages caption them.`);
33198
+ attachments.push(ref);
33199
+ }
33200
+ const piece = { id: `p${i + 1}`, title, plan, attachments, fate: p.where };
33201
+ if (p.project !== undefined) {
33202
+ if (p.where === "here")
33203
+ return refused(`Piece ${i + 1} ("${title}") is \`here\` and names a project.`);
33204
+ const found = this.resolveProject(wt, p.project);
33205
+ if ("refused" in found)
33206
+ return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
33207
+ piece.project = { id: found.repo.id, name: found.repo.name };
33208
+ }
33209
+ if (p.where === "send" && p.after !== undefined) {
33210
+ return refused(`Piece ${i + 1} ("${title}") has \`after\`, which a \`send\` piece does not take.`);
33211
+ }
33212
+ if (p.where !== "send" && p.to !== undefined) {
33213
+ return refused(`Piece ${i + 1} ("${title}") has \`to\`, which only a \`send\` piece takes.`);
33214
+ }
33215
+ if (p.after !== undefined) {
33216
+ const q = p.after.trim();
33217
+ if (!q)
33218
+ return refused(`Piece ${i + 1} ("${title}") has a blank \`after\`; name what it waits for, or leave it out.`);
33219
+ const after = this.resolveAfter(wt, raw, i, q, p.where === "here");
33220
+ if ("refused" in after)
33221
+ return refused(`Piece ${i + 1} ("${title}"): ${after.refused}`);
33222
+ if (p.where === "here" && !("piece" in after.after) && after.after.id === wt.id) {
33223
+ return refused(`Piece ${i + 1} ("${title}") is \`here\` and waits for its own branch.`);
33224
+ }
33225
+ if (p.where === "here" && "piece" in after.after) {
33226
+ const on = raw[Number(after.after.piece.slice(1)) - 1];
33227
+ const onTitle = sentenceCase(on.title.trim());
33228
+ if (on.where === "here") {
33229
+ return refused(`Piece ${i + 1} ("${title}") waits for "${onTitle}", which is \`here\` too and lands with it.`);
33230
+ }
33231
+ const back = on.after?.trim() ?? "";
33232
+ const bare = back.startsWith("@") ? back.slice(1) : back;
33233
+ const onOrigin = bare !== "" && (bare === "this" || bare === wt.branch || raw.some((o) => o.where === "here" && matchBy([o], bare, [(x) => x.title.trim()]).length > 0));
33234
+ if (onOrigin) {
33235
+ return refused(`Piece ${i + 1} ("${title}") waits for "${onTitle}", which waits for this branch: a cycle.`);
33236
+ }
33237
+ }
33238
+ piece.after = after.after;
33239
+ }
33240
+ if (p.where === "send") {
33241
+ const q = p.to?.trim() ?? "";
33242
+ if (!q)
33243
+ return refused(`Piece ${i + 1} ("${title}") is \`send\` and names no worktree in \`to\`.`);
33244
+ const found = this.named(this.siblings(wt, piece.project?.id).filter((w) => !w.landed), q, piece.project?.name);
33245
+ if ("refused" in found)
33246
+ return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
33247
+ piece.to = found.worktree;
33248
+ }
33249
+ pieces.push(piece);
33250
+ }
33251
+ const waiting = heldUnshown(entries);
33252
+ const replaced = openPieces(entries).filter((o) => by === "agent" && o.id === waiting?.id && o.by !== "toyon");
33253
+ if (openPieces(entries).some((o) => !replaced.includes(o))) {
33254
+ return refused("A pieces list is already waiting for the person on this chat; finish your turn.");
33255
+ }
33256
+ if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
33257
+ return refused(`${PER_TURN_MAX} proposals were made this turn already; finish your turn and let the person answer.`);
33258
+ }
33259
+ for (const o of replaced) {
33260
+ agent.note({ type: "pieces-declined", id: o.id, reason: "handed over again", ts: Date.now() });
33261
+ }
33262
+ const held = by !== "person" && wt.mode === "plan";
33263
+ const here = by === "agent" && !held && this.offerHere(wt, pieces);
33264
+ if (here)
33265
+ pieces[0].fate = "here";
33266
+ const plan = held ? null : writePlanDocNow(wt.path, listDoc(pieces));
33267
+ agent.note({
33268
+ type: "pieces-proposed",
33269
+ id: randomUUID2(),
33270
+ pieces,
33271
+ mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
33272
+ ...held ? { held: true } : {},
33273
+ ...plan ? { plan } : {},
33274
+ ...by === "toyon" ? { by: "toyon" } : {},
33275
+ ...by === "toyon" && read ? { read } : {},
33276
+ ts: Date.now()
33277
+ });
33278
+ return { text: held ? HELD : here ? SHOWN_HERE : SHOWN };
33279
+ }
33280
+ offerHere(wt, pieces) {
33281
+ if (pieces.length !== 1)
33282
+ return false;
33283
+ const p = pieces[0];
33284
+ if (p.fate !== "worktree" || p.project || p.to || p.after)
33285
+ return false;
33286
+ const counts = this.d.counts(wt.id);
33287
+ return counts?.dirty === 0 && counts.ahead === 0;
33288
+ }
33289
+ resolveAfter(wt, raw, at, q, anywhere = false) {
33290
+ const bare = q.startsWith("@") ? q.slice(1) : q;
33291
+ const titled = raw.map((p, i) => ({ i, title: p.title.trim() }));
33292
+ const onList = matchBy(titled.filter((p) => anywhere ? p.i !== at : p.i < at), bare, [(p) => p.title]);
33293
+ if (onList.length === 1)
33294
+ return { after: { piece: `p${onList[0].i + 1}` } };
33295
+ if (onList.length > 1)
33296
+ return { refused: `"${q}" names more than one piece on the list; give them different titles.` };
33297
+ const rest = matchBy(titled.filter((p) => anywhere ? p.i === at : p.i >= at), bare, [(p) => p.title]);
33298
+ if (rest.length > 0) {
33299
+ return {
33300
+ refused: anywhere ? `"${q}" is this piece itself.` : `"${q}" is this piece or a later one; a piece waits only for one earlier on the list.`
33301
+ };
33302
+ }
33303
+ if (bare === "this" || bare === wt.branch)
33304
+ return { after: { id: wt.id, branch: wt.branch, title: wt.title } };
33305
+ const found = this.named(this.siblings(wt), bare);
33306
+ if ("refused" in found)
33307
+ return found;
33308
+ return { after: found.worktree };
33309
+ }
33310
+ named(among, q, where = "this project") {
33311
+ const bare = q.startsWith("@") ? q.slice(1) : q;
33312
+ const list = among.map((w) => `${w.branch}, "${w.title}"`).join("; ");
33313
+ if (among.length === 0)
33314
+ return { refused: `no other worktree of ${where} is open.` };
33315
+ const found = matchBy(among, bare, [(w) => w.branch, (w) => w.title]);
33316
+ if (found.length === 1) {
33317
+ const w = found[0];
33318
+ return { worktree: { id: w.id, branch: w.branch, title: w.title } };
33319
+ }
33320
+ if (found.length > 1) {
33321
+ return {
33322
+ refused: `"${q}" names more than one worktree: ${found.map((w) => w.branch).join(", ")}. Say which by its branch.`
33323
+ };
33324
+ }
33325
+ return { refused: `no open worktree of ${where} is called "${q}". Open: ${list}.` };
33326
+ }
33327
+ fate(worktreeId, id, pieceId, fate) {
33328
+ const wt = this.d.state.worktree(worktreeId);
33329
+ if (!wt)
33330
+ throw new UserError("that chat is gone");
33331
+ const agent = this.d.runtime.ensureAgent(wt).agent;
33332
+ const proposed = openPieces(agent.transcript()).find((p) => p.id === id);
33333
+ if (!proposed)
33334
+ throw new UserError("That list is not open in this chat");
33335
+ const piece = proposed.pieces.find((p) => p.id === pieceId);
33336
+ if (!piece)
33337
+ throw new UserError("That piece is not on the list");
33338
+ if (fate === "send" && !piece.to)
33339
+ throw new UserError("That piece has no worktree to send to");
33340
+ const after = piece.after;
33341
+ if (fate === "worktree" && after && "piece" in after) {
33342
+ const at = proposed.pieces.findIndex((p) => p.id === pieceId);
33343
+ if (proposed.pieces.findIndex((p) => p.id === after.piece) > at) {
33344
+ throw new UserError("That piece waits for one later on the list, so it is done here or skipped");
33345
+ }
33346
+ }
33347
+ if (piece.fate === "send" && (fate === "here" || fate === "worktree")) {
33348
+ throw new UserError("A piece for an open worktree is sent there or skipped");
33349
+ }
33350
+ agent.note({ type: "pieces-fate", id, pieceId, fate, ts: Date.now() });
33351
+ }
33352
+ async answer(worktreeId, id, go, note) {
33353
+ if (!this.d.state.worktree(worktreeId))
33354
+ throw new UserError("that chat is gone");
33355
+ if (go)
33356
+ await this.act(worktreeId, id, note?.trim() || undefined);
33357
+ else
33358
+ await this.decline(worktreeId, id);
33359
+ }
33360
+ async decline(worktreeId, id, reason) {
33361
+ const wt = this.d.state.worktree(worktreeId);
33362
+ if (!wt)
33363
+ return;
33364
+ const agent = this.d.runtime.ensureAgent(wt).agent;
33365
+ if (!openPieces(agent.transcript()).some((p) => p.id === id)) {
33366
+ return log.debug(worktreeId, `decline for a pieces list that already closed (${id})`);
33367
+ }
33368
+ agent.note({
33369
+ type: "pieces-declined",
33370
+ id,
33371
+ ...reason ? { outcome: "failed", reason } : {},
33372
+ ts: Date.now()
33373
+ });
33374
+ }
33375
+ async act(worktreeId, id, note, tell = false) {
33376
+ const wt = this.d.state.worktree(worktreeId);
33377
+ if (!wt)
33378
+ return;
33379
+ const agent = this.d.runtime.ensureAgent(wt).agent;
33380
+ const entries = agent.transcript();
33381
+ const proposed = openPieces(entries).find((p) => p.id === id);
33382
+ if (!proposed)
33383
+ return log.debug(worktreeId, `go for a pieces list that already closed (${id})`);
33384
+ agent.note({ type: "pieces-go", id, ts: Date.now() });
33385
+ try {
33386
+ const repo = this.d.state.repo(wt.repoId);
33387
+ if (!repo)
33388
+ throw new UserError(`${wt.title}'s project is no longer open in Toyon`);
33389
+ const origin = this.originOf(wt, repo.name, listFile(entries, id));
33390
+ const outcomes = [];
33391
+ const became = new Map;
33392
+ for (const piece of proposed.pieces) {
33393
+ const fate = effectiveFate(entries, proposed, piece);
33394
+ try {
33395
+ outcomes.push(await this.actOn(wt, origin, proposed, piece, fate, note, became));
33396
+ } catch (e) {
33397
+ const error = e instanceof Error ? e.message : String(e);
33398
+ log.warn(worktreeId, `piece "${piece.title}" (${fate}) failed`, e);
33399
+ became.set(piece.id, { fate: "skip" });
33400
+ outcomes.push({ pieceId: piece.id, fate, error });
33401
+ }
33402
+ }
33403
+ agent.note({ type: "pieces", id, outcomes, ts: Date.now() });
33404
+ this.recordAwaits(wt.id, entries, proposed, became);
33405
+ if (tell && outcomes.some((o) => o.fate !== "here")) {
33406
+ for (let i = 0;i < 30 && openPlanCard(agent.transcript()); i++)
33407
+ await Bun.sleep(100);
33408
+ await Bun.sleep(100);
33409
+ agent.send(piecesActedPrompt(proposed.pieces, outcomes), {
33410
+ asked: { kind: "pieces", why: "what the split started, so the agent builds only its own piece" },
33411
+ keepCards: true
33412
+ });
33413
+ }
33414
+ } catch (e) {
33415
+ const reason = e instanceof Error ? e.message : String(e);
33416
+ agent.note({ type: "pieces-declined", id, outcome: "failed", reason, ts: Date.now() });
33417
+ throw e;
33418
+ }
33419
+ }
33420
+ async read(worktreeId, markdown, planPath) {
33421
+ const wt = this.d.state.worktree(worktreeId);
33422
+ if (!wt)
33423
+ return;
33424
+ const agent = this.d.runtime.ensureAgent(wt).agent;
33425
+ if (openPieces(agent.transcript()).length > 0)
33426
+ return;
33427
+ const found = await this.d.reader(wt, markdown);
33428
+ if (!found || found.length < 2)
33429
+ return;
33430
+ const card = openPlanCard(agent.transcript());
33431
+ if (!card || card.plan !== planPath || openPieces(agent.transcript()).length > 0)
33432
+ return;
33433
+ await this.proposeRead(wt, markdown, found);
33434
+ }
33435
+ async proposeRead(wt, markdown, found) {
33436
+ const siblings = this.siblings(wt);
33437
+ const known = (q) => q === "this" || q === wt.branch || !("refused" in this.named(siblings, q.startsWith("@") ? q.slice(1) : q));
33438
+ const sections = new Map;
33439
+ for (const p of found)
33440
+ sections.set(p.section, (sections.get(p.section) ?? 0) + 1);
33441
+ const raw = found.map((p) => {
33442
+ const own = sections.get(p.section) === 1 ? sectionText(markdown, p.section) : null;
33443
+ const plan = own ?? p.plan ?? sectionText(markdown, p.section) ?? `${p.title}
33444
+
33445
+ ${p.section}`;
33446
+ const after = p.where === "worktree" && p.after && known(p.after) ? p.after : undefined;
33447
+ return { title: p.title, plan, where: p.where, ...after ? { after } : {} };
33448
+ });
33449
+ const r = await this.propose(wt.id, raw, "toyon", hashText(markdown));
33450
+ if (r.isError) {
33451
+ log.warn(wt.id, `the list read off the plan was refused: ${r.text}`);
33452
+ return null;
33453
+ }
33454
+ const agent = this.d.runtime.agentFor(wt.id);
33455
+ return agent ? openPieces(agent.transcript()).find((p) => p.by === "toyon") ?? null : null;
33456
+ }
33457
+ async stale(worktreeId) {
33458
+ const wt = this.d.state.worktree(worktreeId);
33459
+ if (wt?.mode !== "plan")
33460
+ return;
33461
+ const agent = this.d.runtime.agentFor(worktreeId);
33462
+ if (!agent)
33463
+ return;
33464
+ const entries = agent.transcript();
33465
+ const open = heldUnshown(entries);
33466
+ if (!open)
33467
+ return;
33468
+ if (entries.some(({ event }) => event.type === "pieces-stale" && event.id === open.id))
33469
+ return;
33470
+ const card = openPlanCard(entries);
33471
+ if (!card?.plan || !card.detail)
33472
+ return;
33473
+ let markdown;
33474
+ try {
33475
+ markdown = readFileSync20(join28(wt.path, card.plan), "utf8");
33476
+ } catch (e) {
33477
+ log.warn(worktreeId, `could not read ${card.plan} to see whether the plan moved`, e);
33478
+ return;
33479
+ }
33480
+ const moved = open.read ? hashText(markdown) !== open.read : planEdited(wt.path, card.plan, card.detail);
33481
+ if (!moved)
33482
+ return;
33483
+ agent.note({ type: "pieces-stale", id: open.id, ts: Date.now() });
33484
+ if (open.by !== "toyon")
33485
+ return;
33486
+ const found = await this.d.reader(wt, markdown);
33487
+ if (!found || found.length < 2)
33488
+ return;
33489
+ if (openPlanCard(agent.transcript())?.id !== card.id)
33490
+ return;
33491
+ const pressed = new Map;
33492
+ for (const { event } of agent.transcript()) {
33493
+ if (event.type === "pieces-fate" && event.id === open.id) {
33494
+ const title = open.pieces.find((p) => p.id === event.pieceId)?.title;
33495
+ if (title)
33496
+ pressed.set(title.toLowerCase(), event.fate);
33497
+ }
33498
+ }
33499
+ agent.note({ type: "pieces-declined", id: open.id, reason: "the plan changed; read again", ts: Date.now() });
33500
+ const fresh = await this.proposeRead(wt, markdown, found);
33501
+ if (!fresh)
33502
+ return;
33503
+ for (const piece of fresh.pieces) {
33504
+ const fate = pressed.get(piece.title.toLowerCase());
33505
+ if (fate && fate !== piece.fate && !(fate === "send" && !piece.to)) {
33506
+ agent.note({ type: "pieces-fate", id: fresh.id, pieceId: piece.id, fate, ts: Date.now() });
33507
+ }
33508
+ }
33509
+ }
33510
+ async ask(worktreeId, repoId, text) {
33511
+ const wt = this.d.state.requireWorktree(worktreeId);
33512
+ const repo = this.d.state.repo(repoId);
33513
+ if (!repo)
33514
+ throw new UserError("no such project");
33515
+ const here = repo.id === wt.repoId;
33516
+ const agent = this.d.runtime.agentFor(worktreeId);
33517
+ if (!agent)
33518
+ throw new UserError("worktree still starting; try again in a moment");
33519
+ if (agent.mcpTools === null)
33520
+ await agent.warm();
33521
+ if (agent.mcpTools === true) {
33522
+ agent.send(pieceAskPrompt(text, here ? undefined : repo), {
33523
+ asked: { kind: "pieces", why: here ? "a new worktree here" : `a new worktree in ${repo.name}` }
33524
+ });
33525
+ return;
33526
+ }
33527
+ const own = text?.trim();
33528
+ if (!own)
33529
+ throw new UserError(`Write what ${here ? "the new worktree" : repo.name} should do first`);
33530
+ const r = await this.propose(worktreeId, [{ title: titleFrom(own), plan: own, where: "worktree", ...here ? {} : { project: repo.path } }], "person");
33531
+ if (r.isError)
33532
+ throw new UserError(r.text);
33533
+ }
33534
+ originOf(wt, project, file) {
33535
+ return { path: wt.path, project, branch: wt.branch, ...file ? { plan: file } : {} };
33536
+ }
33537
+ async actOn(wt, origin, proposed, piece, fate, note, became) {
33538
+ const crossed = piece.project ? { repoName: piece.project.name } : {};
33539
+ if (fate === "here" || fate === "skip") {
33540
+ became.set(piece.id, { fate });
33541
+ const waits = fate === "here" ? piece.after : undefined;
33542
+ const after = !waits ? undefined : ("piece" in waits) ? proposed.pieces.find((p) => p.id === waits.piece)?.title ?? waits.piece : waits.branch;
33543
+ return { pieceId: piece.id, fate, ...after ? { after } : {} };
33544
+ }
33545
+ if (fate === "send") {
33546
+ const target = piece.to ? this.d.state.worktree(piece.to.id) : undefined;
33547
+ const repoId = piece.project?.id ?? wt.repoId;
33548
+ if (target?.kind !== "worktree" || target.repoId !== repoId) {
33549
+ throw new UserError(`"${piece.to?.title ?? "the worktree"}" is no longer open`);
33550
+ }
33551
+ const attachments = await this.reattachAll(wt.id, piece.attachments);
33552
+ this.d.runtime.ensureAgent(target).agent.send(pieceSendPrompt(piece.plan, note, origin), {
33553
+ keepCards: true,
33554
+ ...attachments.length ? { attachments } : {},
33555
+ handed: {
33556
+ via: "message",
33557
+ body: piece.plan,
33558
+ origin: handedOrigin(wt, piece.project ? origin.project : undefined),
33559
+ ...note ? { note } : {}
33560
+ }
33561
+ });
33562
+ became.set(piece.id, { fate, worktree: { id: target.id, branch: target.branch, title: target.title } });
33563
+ return { pieceId: piece.id, fate, worktreeId: target.id, title: target.title, ...crossed };
33564
+ }
33565
+ const waits = this.waitsFor(wt, piece, became);
33566
+ if (waits) {
33567
+ const queued = {
33568
+ proposalId: proposed.id,
33569
+ pieceId: piece.id,
33570
+ title: piece.title,
33571
+ plan: piece.plan,
33572
+ after: waits,
33573
+ ...piece.project ? { project: piece.project } : {},
33574
+ attachments: piece.attachments,
33575
+ ...note ? { note } : {},
33576
+ queuedAt: Date.now()
33577
+ };
33578
+ const live = this.d.state.requireWorktree(wt.id);
33579
+ live.queued = [...live.queued ?? [], queued];
33580
+ this.d.state.save();
33581
+ this.d.hub.emit("worktreesChanged");
33582
+ became.set(piece.id, { fate, queued: true });
33583
+ const after = "piece" in waits ? proposed.pieces.find((p) => p.id === waits.piece)?.title ?? waits.piece : waits.branch;
33584
+ return { pieceId: piece.id, fate, after, ...crossed };
33585
+ }
33586
+ const made = await this.make(wt, origin, piece, note);
33587
+ became.set(piece.id, { fate, worktree: { id: made.id, branch: made.branch, title: made.title } });
33588
+ return { pieceId: piece.id, fate, worktreeId: made.id, title: made.title, ...crossed };
33589
+ }
33590
+ recordAwaits(originId, entries, proposed, became) {
33591
+ const awaits = [];
33592
+ for (const piece of proposed.pieces) {
33593
+ const after = piece.after;
33594
+ if (!after || effectiveFate(entries, proposed, piece) !== "here")
33595
+ continue;
33596
+ const w = "piece" in after ? became.get(after.piece)?.worktree : after;
33597
+ if (w && !awaits.some((a) => a.id === w.id))
33598
+ awaits.push(w);
33599
+ }
33600
+ if (awaits.length === 0)
33601
+ return;
33602
+ const live = this.d.state.requireWorktree(originId);
33603
+ live.awaits = [...(live.awaits ?? []).filter((a) => !awaits.some((b) => b.id === a.id)), ...awaits];
33604
+ this.d.state.save();
33605
+ this.d.hub.emit("worktreesChanged");
33606
+ }
33607
+ waitsFor(wt, piece, became) {
33608
+ const after = piece.after;
33609
+ if (!after)
33610
+ return null;
33611
+ if (!("piece" in after)) {
33612
+ const waits = this.d.state.worktree(after.id);
33613
+ return waits && waits.kind === "worktree" && !waits.landed ? after : null;
33614
+ }
33615
+ const on = became.get(after.piece);
33616
+ if (!on)
33617
+ return null;
33618
+ if (on.queued)
33619
+ return { piece: after.piece };
33620
+ if (on.worktree)
33621
+ return on.worktree;
33622
+ if (on.fate === "here")
33623
+ return { id: wt.id, branch: wt.branch, title: wt.title };
33624
+ return null;
33625
+ }
33626
+ async make(origin, at, piece, note, after) {
33627
+ const repoId = piece.project?.id ?? origin.repoId;
33628
+ if (piece.project && !this.d.state.repo(repoId)) {
33629
+ throw new UserError(`${piece.project.name} is no longer open in Toyon`);
33630
+ }
33631
+ const attachments = await this.reattachAll(origin.id, piece.attachments);
33632
+ const mode = origin.mode === "plan" ? "auto" : origin.mode;
33633
+ const via = piece.project ? "project" : "piece";
33634
+ return this.d.create(repoId, piece.title, {
33635
+ title: piece.title,
33636
+ ...mode ? { mode } : {},
33637
+ ...attachments.length ? { attachments } : {},
33638
+ from: {
33639
+ kind: "worktree",
33640
+ ref: origin.branch,
33641
+ origin: { id: origin.id, repoId: origin.repoId },
33642
+ ...at.plan ? { plan: at.plan } : {},
33643
+ ...after ? { after } : {}
33644
+ },
33645
+ plan: {
33646
+ markdown: pieceDoc(piece),
33647
+ handed: {
33648
+ via: "plan",
33649
+ body: piece.plan,
33650
+ origin: handedOrigin(origin, piece.project ? at.project : undefined),
33651
+ ...note ? { note } : {}
33652
+ },
33653
+ prompt: (path) => path ? handedPrompt(path, note, at, via) : `${pieceDoc(piece)}
33654
+ ${handedPrompt(null, note, at, via)}`
33655
+ },
33656
+ cold: true
33657
+ });
33658
+ }
33659
+ async reattachAll(worktreeId, refs) {
33660
+ const out = [];
33661
+ for (const ref of refs) {
33662
+ const again = await this.d.attachments.reattach(worktreeId, ref);
33663
+ if (again)
33664
+ out.push(again);
33665
+ else
33666
+ log.warn(worktreeId, `a piece's attachment is gone from the store (${ref.kind} ${ref.n})`);
33667
+ }
33668
+ return out;
33669
+ }
33670
+ async landed(landedId) {
33671
+ const told = new Set;
33672
+ const origin = this.tellOrigin(landedId);
33673
+ if (origin)
33674
+ told.add(origin);
33675
+ const madeNow = new Set;
33676
+ for (const origin of this.d.state.worktrees) {
33677
+ const due = origin.queued?.filter((q) => !("piece" in q.after) && q.after.id === landedId) ?? [];
33678
+ if (due.length === 0)
33679
+ continue;
33680
+ const repo = this.d.state.repo(origin.repoId);
33681
+ origin.queued = origin.queued?.filter((q) => !due.includes(q));
33682
+ if (origin.queued?.length === 0)
33683
+ delete origin.queued;
33684
+ this.d.state.save();
33685
+ this.d.hub.emit("worktreesChanged");
33686
+ const agent = this.d.runtime.ensureAgent(origin).agent;
33687
+ for (const q of due) {
33688
+ try {
33689
+ if (!repo)
33690
+ throw new UserError("the project is no longer open in Toyon");
33691
+ const at = this.originOf(origin, repo.name, listFile(agent.transcript(), q.proposalId));
33692
+ const made = await this.make(origin, at, q, q.note, "piece" in q.after ? undefined : q.after);
33693
+ agent.note({
33694
+ type: "piece-made",
33695
+ id: q.proposalId,
33696
+ pieceId: q.pieceId,
33697
+ worktreeId: made.id,
33698
+ title: made.title,
33699
+ ts: Date.now()
33700
+ });
33701
+ madeNow.add(made.id);
33702
+ this.repoint(origin, q, { id: made.id, branch: made.branch, title: made.title });
33703
+ } catch (e) {
33704
+ const reason = e instanceof Error ? e.message : String(e);
33705
+ log.warn(origin.id, `queued piece "${q.title}" could not be made`, e);
33706
+ agent.note({ type: "piece-dropped", id: q.proposalId, pieceId: q.pieceId, reason, ts: Date.now() });
33707
+ this.dropDependents(origin, q);
33708
+ }
33709
+ }
33710
+ }
33711
+ for (const id of this.tellWaiters(landedId, madeNow))
33712
+ told.add(id);
33713
+ this.tellAwaiting(landedId, told);
33714
+ }
33715
+ tellAwaiting(landedId, told) {
33716
+ const landed = this.d.state.worktree(landedId);
33717
+ const repo = landed ? this.d.state.repo(landed.repoId) : undefined;
33718
+ let changed = false;
33719
+ for (const w of this.d.state.worktrees) {
33720
+ if (!w.awaits?.some((a) => a.id === landedId))
33721
+ continue;
33722
+ w.awaits = w.awaits.filter((a) => a.id !== landedId);
33723
+ if (w.awaits.length === 0)
33724
+ delete w.awaits;
33725
+ changed = true;
33726
+ if (!landed || !repo || w.kind !== "worktree" || told.has(w.id))
33727
+ continue;
33728
+ this.d.runtime.ensureAgent(w).agent.note({
33729
+ type: "piece-landed",
33730
+ worktreeId: landed.id,
33731
+ repoName: repo.name,
33732
+ title: landed.title,
33733
+ ...landed.pr?.url ? { url: landed.pr.url } : {},
33734
+ ...w.repoId === landed.repoId ? { same: true } : {},
33735
+ awaited: true,
33736
+ ts: Date.now()
33737
+ });
33738
+ }
33739
+ if (changed) {
33740
+ this.d.state.save();
33741
+ this.d.hub.emit("worktreesChanged");
33742
+ }
33743
+ }
33744
+ repoint(origin, made, worktree) {
33745
+ const live = this.d.state.worktree(origin.id);
33746
+ if (!live?.queued)
33747
+ return;
33748
+ let changed = false;
33749
+ for (const q of live.queued) {
33750
+ if (q.proposalId === made.proposalId && "piece" in q.after && q.after.piece === made.pieceId) {
33751
+ q.after = worktree;
33752
+ changed = true;
33753
+ }
33754
+ }
33755
+ if (changed) {
33756
+ this.d.state.save();
33757
+ this.d.hub.emit("worktreesChanged");
33758
+ }
33759
+ }
33760
+ dropDependents(origin, dropped) {
33761
+ const live = this.d.state.worktree(origin.id);
33762
+ if (!live?.queued)
33763
+ return;
33764
+ const gone = live.queued.filter((q) => q.proposalId === dropped.proposalId && ("piece" in q.after) && q.after.piece === dropped.pieceId);
33765
+ if (gone.length === 0)
33766
+ return;
33767
+ live.queued = live.queued.filter((q) => !gone.includes(q));
33768
+ if (live.queued.length === 0)
33769
+ delete live.queued;
33770
+ this.d.state.save();
33771
+ this.d.hub.emit("worktreesChanged");
33772
+ const agent = this.d.runtime.ensureAgent(live).agent;
33773
+ for (const q of gone) {
33774
+ agent.note({
33775
+ type: "piece-dropped",
33776
+ id: q.proposalId,
33777
+ pieceId: q.pieceId,
33778
+ reason: `the piece it waited for, "${dropped.title}", was dropped`,
33779
+ ts: Date.now()
33780
+ });
33781
+ this.dropDependents(live, q);
33782
+ }
33783
+ }
33784
+ tellOrigin(landedId) {
33785
+ const wt = this.d.state.worktree(landedId);
33786
+ if (wt?.from?.kind !== "worktree")
33787
+ return null;
33788
+ const origin = this.d.state.worktree(wt.from.origin.id);
33789
+ if (origin?.kind !== "worktree")
33790
+ return null;
33791
+ const repo = this.d.state.repo(wt.repoId);
33792
+ if (!repo)
33793
+ return null;
33794
+ this.d.runtime.ensureAgent(origin).agent.note({
33795
+ type: "piece-landed",
33796
+ worktreeId: wt.id,
33797
+ repoName: repo.name,
33798
+ title: wt.title,
33799
+ ...wt.pr?.url ? { url: wt.pr.url } : {},
33800
+ ...wt.repoId === origin.repoId ? { same: true } : {},
33801
+ ts: Date.now()
33802
+ });
33803
+ return origin.id;
33804
+ }
33805
+ tellWaiters(landedId, madeNow) {
33806
+ const landed = this.d.state.worktree(landedId);
33807
+ if (!landed)
33808
+ return [];
33809
+ const repo = this.d.state.repo(landed.repoId);
33810
+ if (!repo)
33811
+ return [];
33812
+ const told = [];
33813
+ for (const w of this.d.state.worktrees) {
33814
+ if (w.kind !== "worktree" || w.landed || w.id === landedId || madeNow.has(w.id))
33815
+ continue;
33816
+ if (w.from?.kind !== "worktree" || w.from.after?.id !== landedId)
33817
+ continue;
33818
+ told.push(w.id);
33819
+ this.d.runtime.ensureAgent(w).agent.note({
33820
+ type: "piece-landed",
33821
+ worktreeId: landed.id,
33822
+ repoName: repo.name,
33823
+ title: landed.title,
33824
+ ...landed.pr?.url ? { url: landed.pr.url } : {},
33825
+ ...w.repoId === landed.repoId ? { same: true } : {},
33826
+ again: true,
33827
+ ts: Date.now()
33828
+ });
33829
+ }
33830
+ return told;
33831
+ }
33832
+ sweep() {
33833
+ let changed = false;
33834
+ for (const origin of this.d.state.worktrees) {
33835
+ if (!origin.queued?.length)
33836
+ continue;
33837
+ const live = (id) => this.d.state.worktree(id)?.kind === "worktree";
33838
+ const gone = origin.queued.filter((q) => !("piece" in q.after) && !live(q.after.id));
33839
+ if (gone.length === 0)
33840
+ continue;
33841
+ origin.queued = origin.queued.filter((q) => !gone.includes(q));
33842
+ if (origin.queued.length === 0)
33843
+ delete origin.queued;
33844
+ changed = true;
33845
+ const agent = this.d.runtime.ensureAgent(origin).agent;
33846
+ for (const q of gone) {
33847
+ const title = "piece" in q.after ? q.after.piece : q.after.title;
33848
+ agent.note({
33849
+ type: "piece-dropped",
33850
+ id: q.proposalId,
33851
+ pieceId: q.pieceId,
33852
+ reason: `the worktree it waited for, "${title}", is gone`,
33853
+ ts: Date.now()
33854
+ });
33855
+ this.dropDependents(origin, q);
33856
+ }
33857
+ }
33858
+ if (changed) {
33859
+ this.d.state.save();
33860
+ this.d.hub.emit("worktreesChanged");
33861
+ }
33862
+ }
33863
+ }
33864
+ function matchRepos(repos, q) {
33865
+ return matchBy(repos, q, [(r) => r.path, (r) => r.name]);
33866
+ }
33867
+ function attachmentNamed(entries, label) {
33868
+ const m = LABEL.exec(label.trim());
33869
+ if (!m)
33870
+ return null;
33871
+ const kind = KIND_OF[m[1].toLowerCase()];
33872
+ const n = Number(m[2]);
33873
+ for (const { event } of entries) {
33874
+ if (event.type !== "user-message" && event.type !== "handed")
33875
+ continue;
33876
+ const ref = event.attachments?.find((a) => a.kind === kind && a.n === n);
33877
+ if (ref)
33878
+ return ref;
33879
+ }
33880
+ return null;
33881
+ }
33882
+
32398
33883
  // ../daemon/src/worktrees/prs.ts
32399
33884
  var POLL_MS3 = 3 * 60000;
32400
33885
 
@@ -32503,7 +33988,7 @@ async function mergedBranches(repoPath, defaultBr) {
32503
33988
  }
32504
33989
 
32505
33990
  // ../daemon/src/worktrees/discover.ts
32506
- import { createHash as createHash5 } from "crypto";
33991
+ import { createHash as createHash6 } from "crypto";
32507
33992
  import { basename as basename10 } from "path";
32508
33993
 
32509
33994
  // ../daemon/src/git/worktrees.ts
@@ -32572,7 +34057,7 @@ async function listWorktrees(repoPath) {
32572
34057
 
32573
34058
  // ../daemon/src/worktrees/discover.ts
32574
34059
  function discoveredId(path) {
32575
- return `disc-${createHash5("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
34060
+ return `disc-${createHash6("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
32576
34061
  }
32577
34062
  function knownPaths(known) {
32578
34063
  const paths = new Set;
@@ -32755,9 +34240,9 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
32755
34240
  }
32756
34241
 
32757
34242
  // ../daemon/src/worktrees/service.ts
32758
- import { cpSync, existsSync as existsSync36, readFileSync as readFileSync21, rmSync as rmSync8 } from "fs";
34243
+ import { cpSync, existsSync as existsSync36, readFileSync as readFileSync22, rmSync as rmSync8 } from "fs";
32759
34244
  import { rm as rm5 } from "fs/promises";
32760
- import { dirname as dirname13, join as join32 } from "path";
34245
+ import { dirname as dirname13, join as join33 } from "path";
32761
34246
 
32762
34247
  // ../daemon/src/git/archive.ts
32763
34248
  import { rmSync as rmSync5 } from "fs";
@@ -33366,7 +34851,7 @@ function compareUrl(remoteUrl, base, branch) {
33366
34851
 
33367
34852
  // ../daemon/src/git/turn.ts
33368
34853
  import { copyFileSync, existsSync as existsSync33, rmSync as rmSync6 } from "fs";
33369
- import { join as join28 } from "path";
34854
+ import { join as join29 } from "path";
33370
34855
  var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
33371
34856
  var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
33372
34857
  var queue = new Map;
@@ -33384,7 +34869,7 @@ async function treeNow(wtPath, head, dirty) {
33384
34869
  const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
33385
34870
  if (!dir.ok || !dir.out)
33386
34871
  return null;
33387
- index = join28(dir.out, "toyon-turn-index");
34872
+ index = join29(dir.out, "toyon-turn-index");
33388
34873
  }
33389
34874
  const env = { GIT_INDEX_FILE: index };
33390
34875
  const writeTree = async () => {
@@ -33407,7 +34892,7 @@ async function treeNow(wtPath, head, dirty) {
33407
34892
  }
33408
34893
  synced.delete(wtPath);
33409
34894
  if (!existsSync33(index)) {
33410
- const own = join28(index, "..", "index");
34895
+ const own = join29(index, "..", "index");
33411
34896
  if (existsSync33(own))
33412
34897
  copyFileSync(own, index);
33413
34898
  else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
@@ -33489,8 +34974,8 @@ async function dropTurnRefs(repoPath, worktreeId) {
33489
34974
  }
33490
34975
 
33491
34976
  // ../daemon/src/worktrees/archive.ts
33492
- import { existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync20, renameSync as renameSync7, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
33493
- import { basename as basename11, dirname as dirname12, join as join29 } from "path";
34977
+ import { existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync21, renameSync as renameSync7, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
34978
+ import { basename as basename11, dirname as dirname12, join as join30 } from "path";
33494
34979
  class WorktreeArchive {
33495
34980
  dir;
33496
34981
  records = new Map;
@@ -33502,7 +34987,7 @@ class WorktreeArchive {
33502
34987
  if (!existsSync34(file))
33503
34988
  continue;
33504
34989
  try {
33505
- this.records.set(id, JSON.parse(readFileSync20(file, "utf8")));
34990
+ this.records.set(id, JSON.parse(readFileSync21(file, "utf8")));
33506
34991
  } catch (e) {
33507
34992
  log.warn("archive", `skipped an unreadable record: ${file}`, e);
33508
34993
  }
@@ -33557,24 +35042,24 @@ class WorktreeArchive {
33557
35042
  planFile(id, rel) {
33558
35043
  if (!this.records.has(id) || !isPlanPath(rel))
33559
35044
  return null;
33560
- const file = join29(this.filesOf(id).plans, basename11(rel));
35045
+ const file = join30(this.filesOf(id).plans, basename11(rel));
33561
35046
  if (!existsSync34(file))
33562
35047
  return null;
33563
35048
  try {
33564
- return readFileSync20(file, "utf8");
35049
+ return readFileSync21(file, "utf8");
33565
35050
  } catch (e) {
33566
35051
  log.warn(id, `could not read its archived plan ${rel}`, e);
33567
35052
  return null;
33568
35053
  }
33569
35054
  }
33570
35055
  filesOf(id) {
33571
- const dir = join29(this.dir, id);
35056
+ const dir = join30(this.dir, id);
33572
35057
  return {
33573
35058
  dir,
33574
- record: join29(dir, "record.json"),
33575
- transcript: join29(dir, "transcript.jsonl"),
33576
- attachments: join29(dir, "attachments"),
33577
- plans: join29(dir, "plans")
35059
+ record: join30(dir, "record.json"),
35060
+ transcript: join30(dir, "transcript.jsonl"),
35061
+ attachments: join30(dir, "attachments"),
35062
+ plans: join30(dir, "plans")
33578
35063
  };
33579
35064
  }
33580
35065
  }
@@ -33601,7 +35086,7 @@ function summarize(r, repoId, transcript) {
33601
35086
  function lastUsage(transcript) {
33602
35087
  if (!existsSync34(transcript))
33603
35088
  return null;
33604
- const lines = readFileSync20(transcript, "utf8").split(`
35089
+ const lines = readFileSync21(transcript, "utf8").split(`
33605
35090
  `);
33606
35091
  for (let i = lines.length - 1;i >= 0; i--) {
33607
35092
  if (!lines[i]?.includes('"usage"'))
@@ -33617,14 +35102,16 @@ function lastUsage(transcript) {
33617
35102
  function firstPrompt(transcript) {
33618
35103
  if (!existsSync34(transcript))
33619
35104
  return;
33620
- for (const line of readFileSync20(transcript, "utf8").split(`
35105
+ for (const line of readFileSync21(transcript, "utf8").split(`
33621
35106
  `)) {
33622
- if (!line.includes('"user-message"'))
35107
+ if (!line.includes('"user-message"') && !line.includes('"handed"'))
33623
35108
  continue;
33624
35109
  try {
33625
35110
  const event = JSON.parse(line).event;
33626
35111
  if (event?.type === "user-message")
33627
35112
  return event.text.slice(0, 500);
35113
+ if (event?.type === "handed")
35114
+ return (event.body ?? event.text).slice(0, 500);
33628
35115
  } catch {}
33629
35116
  }
33630
35117
  return;
@@ -33713,7 +35200,7 @@ async function fillLandMarks(jobs, read, width = 8) {
33713
35200
 
33714
35201
  // ../daemon/src/worktrees/spare.ts
33715
35202
  import { existsSync as existsSync35 } from "fs";
33716
- import { basename as basename12, join as join30 } from "path";
35203
+ import { basename as basename12, join as join31 } from "path";
33717
35204
  class SparePool {
33718
35205
  d;
33719
35206
  inflight = new Map;
@@ -33781,11 +35268,11 @@ class SparePool {
33781
35268
  return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
33782
35269
  }
33783
35270
  async record(repoId, repo) {
33784
- const { id, dir } = freeSlot(join30(this.d.paths.worktreesDir, repo.name));
35271
+ const { id, dir } = freeSlot(join31(this.d.paths.worktreesDir, repo.name));
33785
35272
  const wt = {
33786
35273
  id,
33787
35274
  repoId,
33788
- path: join30(this.d.paths.worktreesDir, repo.name, dir),
35275
+ path: join31(this.d.paths.worktreesDir, repo.name, dir),
33789
35276
  branch: repo.defaultBranch,
33790
35277
  kind: "spare",
33791
35278
  phase: "reserved",
@@ -33975,7 +35462,7 @@ class SparePool {
33975
35462
  // ../daemon/src/worktrees/treeState.ts
33976
35463
  import { watch as watch2 } from "fs";
33977
35464
  import { stat as stat5 } from "fs/promises";
33978
- import { join as join31 } from "path";
35465
+ import { join as join32 } from "path";
33979
35466
  var EDIT_DELAY_MS = 150;
33980
35467
  var SHELL_QUIET_MS2 = 1500;
33981
35468
  var WATCH_DELAY_MS = 300;
@@ -34358,7 +35845,7 @@ class TreeState {
34358
35845
  xy: f.xy,
34359
35846
  ...entries.folded.has(f.path) ? { add: f.add, del: f.del } : {}
34360
35847
  }));
34361
- const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat5(join31(r.path, f.path)).then((s) => `${s.mtimeMs}:${s.size}`, () => "-")));
35848
+ const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat5(join32(r.path, f.path)).then((s) => `${s.mtimeMs}:${s.size}`, () => "-")));
34362
35849
  const t0 = performance.now();
34363
35850
  const counts = { ...ab, ...unpushed === null ? {} : { unpushed } };
34364
35851
  const signature = String(Bun.hash([
@@ -34692,7 +36179,7 @@ async function deleteTree(path) {
34692
36179
  }
34693
36180
  }
34694
36181
  function withoutAttachments(event) {
34695
- if (event.type !== "user-message")
36182
+ if (event.type !== "user-message" && event.type !== "handed")
34696
36183
  return event;
34697
36184
  const { attachments: _attachments, ...rest } = event;
34698
36185
  return rest;
@@ -34708,7 +36195,7 @@ var idsOf = (rows, trunks) => [
34708
36195
  ...rows.map((r) => r.id),
34709
36196
  ...Object.values(trunks).map((t) => t.id)
34710
36197
  ];
34711
- var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
36198
+ var heldBy2 = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
34712
36199
  var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
34713
36200
  `, 1)[0])).join(" ");
34714
36201
 
@@ -34797,7 +36284,7 @@ class WorktreeService {
34797
36284
  const wt = d.state.worktree(worktreeId);
34798
36285
  const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
34799
36286
  if (wt && prompt)
34800
- this.scheduleNaming(wt, prompt, wt.variant);
36287
+ this.scheduleNaming(wt, prompt);
34801
36288
  }
34802
36289
  if (turn.end !== "asking")
34803
36290
  this.syncAfterTurn(worktreeId);
@@ -34812,7 +36299,7 @@ class WorktreeService {
34812
36299
  });
34813
36300
  }
34814
36301
  async create(repoId, prompt, opts = {}) {
34815
- const { variant, attachments } = opts;
36302
+ const { attachments } = opts;
34816
36303
  const repo = this.d.state.requireRepo(repoId);
34817
36304
  const lead = opts.worktreeId ?? this.d.state.worktrees.find((w) => w.repoId === repoId && isMain(w))?.id;
34818
36305
  const context = (id) => {
@@ -34823,15 +36310,8 @@ class WorktreeService {
34823
36310
  const profile = this.checkProfile(repo, opts.profile);
34824
36311
  const task = taskText(prompt, attachments);
34825
36312
  const given = cleanTitle2(opts.title ?? "") || undefined;
34826
- const base = given ?? titleFrom(task);
34827
- const title = variant ? `${base} v${variant.index}` : base;
34828
- if (opts.carry && variant && variant.of > 1) {
34829
- throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
34830
- }
34831
- const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
34832
-
34833
- `) : prompt;
34834
- const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
36313
+ const title = given ?? titleFrom(task);
36314
+ const spare = opts.cold ? undefined : this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
34835
36315
  const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
34836
36316
  let carriedIn;
34837
36317
  let release;
@@ -34841,19 +36321,24 @@ class WorktreeService {
34841
36321
  mode: opts.mode,
34842
36322
  model: opts.model,
34843
36323
  effort: opts.effort,
34844
- variant,
34845
36324
  from: opts.from,
34846
36325
  profile,
34847
36326
  unnamed: given ? undefined : true
34848
36327
  };
34849
- const claimed = await this.spare.claim(repoId, title, opts.worktreeId, stamps, async (wt) => {
36328
+ const prepare = async (wt) => {
34850
36329
  const rt = this.d.runtime.ensureAgent(wt);
34851
36330
  release = rt.agent.hold();
34852
36331
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
34853
36332
  await rt.agent.restart();
34854
36333
  carriedIn = await this.carryMain(repo, wt, !!opts.carry);
34855
- rt.agent.send(agentPrompt, { context: withCarry(context(wt.id), carriedIn), attachments });
34856
- });
36334
+ const born = this.bornWith(wt, opts.plan);
36335
+ rt.agent.send(born?.text ?? prompt, {
36336
+ context: withCarry(context(wt.id), carriedIn),
36337
+ attachments,
36338
+ ...born ? { handed: born.handed } : {}
36339
+ });
36340
+ };
36341
+ const claimed = spare ? await this.spare.claim(repoId, title, opts.worktreeId, stamps, prepare) : null;
34857
36342
  if (claimed) {
34858
36343
  const { wt } = claimed;
34859
36344
  const rt = this.d.runtime.ensureAgent(wt);
@@ -34876,7 +36361,7 @@ class WorktreeService {
34876
36361
  release?.();
34877
36362
  if (restart)
34878
36363
  this.restartProcs(wt);
34879
- this.nameOrAsk(wt, task, given, variant);
36364
+ this.nameOrAsk(wt, task, given);
34880
36365
  if (carriedIn?.unmoved)
34881
36366
  throw new UserError(carriedIn.unmoved, { delivered: true });
34882
36367
  return wt;
@@ -34884,8 +36369,8 @@ class WorktreeService {
34884
36369
  if (!await isGitRepo(repo.path)) {
34885
36370
  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.`);
34886
36371
  }
34887
- const { id, dir } = freeSlot(join32(this.d.paths.worktreesDir, repo.name));
34888
- const wtPath = join32(this.d.paths.worktreesDir, repo.name, dir);
36372
+ const { id, dir } = freeSlot(join33(this.d.paths.worktreesDir, repo.name));
36373
+ const wtPath = join33(this.d.paths.worktreesDir, repo.name, dir);
34889
36374
  const branch = `toyon/${dir}`;
34890
36375
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
34891
36376
  const wt = {
@@ -34900,7 +36385,6 @@ class WorktreeService {
34900
36385
  createdAt: Date.now(),
34901
36386
  promptedAt: Date.now(),
34902
36387
  agent,
34903
- ...variant ? { variant } : {},
34904
36388
  ...opts.from ? { from: opts.from } : {},
34905
36389
  ...opts.createdBy ? { createdBy: opts.createdBy } : {},
34906
36390
  ...profile !== undefined ? { profile } : {},
@@ -34909,16 +36393,34 @@ class WorktreeService {
34909
36393
  ...opts.effort ? { effort: opts.effort } : {}
34910
36394
  };
34911
36395
  const carried = await this.carryMain(repo, wt, !!opts.carry);
34912
- this.launch(wt, repo, repo.path);
34913
- this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context(id), carried), attachments });
34914
- this.nameOrAsk(wt, task, given, variant);
36396
+ this.launch(wt, repo, repo.path, opts.cold ? { start: false } : {});
36397
+ const born = this.bornWith(wt, opts.plan);
36398
+ this.d.runtime.ensureAgent(wt).agent.send(born?.text ?? prompt, {
36399
+ context: withCarry(context(id), carried),
36400
+ attachments,
36401
+ ...born ? { handed: born.handed } : {}
36402
+ });
36403
+ this.nameOrAsk(wt, task, given);
34915
36404
  if (carried.unmoved)
34916
36405
  throw new UserError(carried.unmoved, { delivered: true });
34917
36406
  return wt;
34918
36407
  }
34919
- nameOrAsk(wt, task, given, variant) {
36408
+ bornWith(wt, plan) {
36409
+ if (!plan)
36410
+ return null;
36411
+ const path = writePlanDocNow(wt.path, plan.markdown);
36412
+ if (path) {
36413
+ const live = this.d.state.requireWorktree(wt.id);
36414
+ live.plan = path;
36415
+ live.planned = true;
36416
+ this.d.state.save();
36417
+ this.d.hub.emit("worktreesChanged");
36418
+ }
36419
+ return { text: plan.prompt(path), handed: { ...plan.handed, ...path ? { path } : {} } };
36420
+ }
36421
+ nameOrAsk(wt, task, given) {
34920
36422
  if (given === undefined) {
34921
- this.scheduleNaming(wt, task, variant);
36423
+ this.scheduleNaming(wt, task);
34922
36424
  return;
34923
36425
  }
34924
36426
  fireAndForget(wt.id, this.rename(wt.id, given), "branch for the given title");
@@ -34959,7 +36461,7 @@ class WorktreeService {
34959
36461
  };
34960
36462
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
34961
36463
  const restored = (carried.ok ? carried.out.split(`
34962
- `).filter(Boolean) : []).filter((p) => !existsSync36(join32(wt.path, p)));
36464
+ `).filter(Boolean) : []).filter((p) => !existsSync36(join33(wt.path, p)));
34963
36465
  const applied = await git(wt.path, "stash", "apply", sha);
34964
36466
  if (applied.ok) {
34965
36467
  const ref = await entry();
@@ -34996,9 +36498,9 @@ class WorktreeService {
34996
36498
  const branch = kind === "pr" ? `pr/${number}` : ref;
34997
36499
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
34998
36500
  let slug = branchSlug(title) || "branch";
34999
- if (existsSync36(join32(this.d.paths.worktreesDir, repo.name, slug)))
36501
+ if (existsSync36(join33(this.d.paths.worktreesDir, repo.name, slug)))
35000
36502
  slug = `${slug}-${shortId().slice(0, 4)}`;
35001
- const wtPath = join32(this.d.paths.worktreesDir, repo.name, slug);
36503
+ const wtPath = join33(this.d.paths.worktreesDir, repo.name, slug);
35002
36504
  await withRepoLock(repo.path, async () => {
35003
36505
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
35004
36506
  if (out)
@@ -35053,7 +36555,7 @@ class WorktreeService {
35053
36555
  const target = canonical(r.path);
35054
36556
  const pushed = this.discoveredById(worktreeId);
35055
36557
  if (pushed?.locked)
35056
- throw new UserError(heldBy(pushed));
36558
+ throw new UserError(heldBy2(pushed));
35057
36559
  if (!r.branch)
35058
36560
  throw new UserError(`${r.name} is detached: check out a branch in it first`);
35059
36561
  const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((w) => isInside(target, canonical(w.path)));
@@ -35080,7 +36582,7 @@ class WorktreeService {
35080
36582
  await withRepoLock(repo.path, async () => {
35081
36583
  const found = await this.requireDiscovered(repoId, repo.path, target, wt.id);
35082
36584
  if (found.locked)
35083
- throw new UserError(heldBy(found));
36585
+ throw new UserError(heldBy2(found));
35084
36586
  if (!found.branch)
35085
36587
  throw new UserError(`${found.name} is detached: check out a branch in it first`);
35086
36588
  wt.branch = found.branch;
@@ -35167,24 +36669,14 @@ class WorktreeService {
35167
36669
  this.d.state.save();
35168
36670
  this.d.hub.emit("worktreesChanged");
35169
36671
  }
35170
- scheduleNaming(wt, prompt, variant) {
36672
+ scheduleNaming(wt, prompt) {
35171
36673
  if (!prompt.trim())
35172
36674
  return;
35173
- if (variant && variant.index !== 1)
35174
- return;
35175
36675
  if (this.naming.has(wt.id))
35176
36676
  return;
35177
36677
  this.naming.add(wt.id);
35178
36678
  const asked = (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt);
35179
- const applied = variant ? asked.then(async (name) => {
35180
- if (!name)
35181
- return;
35182
- for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
35183
- await this.rename(sibling.id, `${name} v${sibling.variant.index}`).catch((e) => {
35184
- log.warn(sibling.id, "variant rename failed", e);
35185
- });
35186
- }
35187
- }) : asked.then((name) => {
36679
+ const applied = asked.then((name) => {
35188
36680
  if (name)
35189
36681
  return this.rename(wt.id, name);
35190
36682
  });
@@ -35274,7 +36766,7 @@ class WorktreeService {
35274
36766
  }
35275
36767
  }
35276
36768
  async keep(repo, wt) {
35277
- const index = join32(this.d.paths.archiveDir, `${wt.id}.index`);
36769
+ const index = join33(this.d.paths.archiveDir, `${wt.id}.index`);
35278
36770
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
35279
36771
  if (!kept)
35280
36772
  log.warn(wt.id, "could not keep its git state");
@@ -35311,10 +36803,10 @@ class WorktreeService {
35311
36803
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
35312
36804
  }
35313
36805
  plansScratch(worktreeId) {
35314
- return join32(this.d.paths.archiveDir, `${worktreeId}.plans`);
36806
+ return join33(this.d.paths.archiveDir, `${worktreeId}.plans`);
35315
36807
  }
35316
36808
  keepPlans(wt) {
35317
- const from = join32(wt.path, PLANS_DIR);
36809
+ const from = join33(wt.path, PLANS_DIR);
35318
36810
  if (!existsSync36(from))
35319
36811
  return;
35320
36812
  try {
@@ -35459,7 +36951,7 @@ class WorktreeService {
35459
36951
  }
35460
36952
  archivedAttachment(archiveId, file) {
35461
36953
  const files = this.archive.chatFiles(archiveId);
35462
- return files && isAttachmentFile(file) ? join32(files.attachments, file) : null;
36954
+ return files && isAttachmentFile(file) ? join33(files.attachments, file) : null;
35463
36955
  }
35464
36956
  archivedGit(archiveId) {
35465
36957
  const rec = this.archive.get(archiveId);
@@ -35515,10 +37007,9 @@ class WorktreeService {
35515
37007
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
35516
37008
  if (rec.auto)
35517
37009
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
35518
- const path = existsSync36(old.path) ? join32(dirname13(old.path), freeSlot(dirname13(old.path)).dir) : old.path;
35519
- const { variant: _variant, ...rest } = old;
37010
+ const path = existsSync36(old.path) ? join33(dirname13(old.path), freeSlot(dirname13(old.path)).dir) : old.path;
35520
37011
  const wt = {
35521
- ...rest,
37012
+ ...old,
35522
37013
  repoId: repo.id,
35523
37014
  path,
35524
37015
  branch: old.branch,
@@ -35554,7 +37045,7 @@ class WorktreeService {
35554
37045
  run.finish(0);
35555
37046
  this.arriving.delete(wt.id);
35556
37047
  wt.branch = branch;
35557
- move(chat.plans, join32(path, PLANS_DIR));
37048
+ move(chat.plans, join33(path, PLANS_DIR));
35558
37049
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
35559
37050
  if (!dropped.ok)
35560
37051
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
@@ -35670,17 +37161,6 @@ class WorktreeService {
35670
37161
  this.d.state.save();
35671
37162
  this.d.hub.emit("worktreesChanged");
35672
37163
  }
35673
- async pickVariant(worktreeId) {
35674
- const wt = this.d.state.worktree(worktreeId);
35675
- if (!wt?.variant)
35676
- return;
35677
- for (const sibling of siblingsOf(wt, this.d.state.worktrees)) {
35678
- await this.archiveWorktree(sibling.id).catch((e) => log.warn(sibling.id, "could not archive variant sibling", e));
35679
- }
35680
- delete wt.variant;
35681
- this.d.state.save();
35682
- this.d.hub.emit("worktreesChanged");
35683
- }
35684
37164
  async graft(targetId, sourceIds) {
35685
37165
  const target = this.d.state.worktree(targetId);
35686
37166
  if (!target)
@@ -35733,6 +37213,24 @@ class WorktreeService {
35733
37213
  this.d.hub.emit("worktreesChanged");
35734
37214
  fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start a parked preview");
35735
37215
  }
37216
+ retryPreview(worktreeId) {
37217
+ const wt = this.d.state.worktree(worktreeId);
37218
+ if (!wt)
37219
+ return;
37220
+ if (wt.parked) {
37221
+ this.startPreview(worktreeId);
37222
+ return;
37223
+ }
37224
+ const procs = this.d.runtime.get(worktreeId)?.procs;
37225
+ const down = procs && !procs.asleep ? procs.states().filter((p) => p.status === "crashed" || p.status === "unreachable" || p.status === "stopped") : [];
37226
+ if (down.length === 0) {
37227
+ fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "preview asked for");
37228
+ return;
37229
+ }
37230
+ for (const p of down) {
37231
+ fireAndForget(worktreeId, this.d.runtime.restartStream(worktreeId, p.name), "preview asked for");
37232
+ }
37233
+ }
35736
37234
  async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
35737
37235
  this.d.runtime.markSetup(wt.id, true);
35738
37236
  try {
@@ -35742,14 +37240,14 @@ class WorktreeService {
35742
37240
  }
35743
37241
  }
35744
37242
  async mirrorBaseIgnore(repo) {
35745
- const src = join32(repo.path, ".gitignore");
37243
+ const src = join33(repo.path, ".gitignore");
35746
37244
  const tracked = existsSync36(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
35747
37245
  const mirror = existsSync36(src) && !tracked;
35748
- const lines = mirror ? readFileSync21(src, "utf8").split(`
37246
+ const lines = mirror ? readFileSync22(src, "utf8").split(`
35749
37247
  `).filter((l) => l.trim() !== "") : [];
35750
37248
  await excludeBlock(repo.path, BASE_IGNORE, lines);
35751
37249
  }
35752
- async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
37250
+ async setupThenStart(wt, repo, depsSource, { setupCommands = true, start = true }) {
35753
37251
  await this.mirrorBaseIgnore(repo);
35754
37252
  const ceiling = timeoutFor(repo.config, "setup");
35755
37253
  if (this.d.cache && cachePolicy(repo.config)) {
@@ -35757,8 +37255,8 @@ class WorktreeService {
35757
37255
  await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
35758
37256
  run.finish(0);
35759
37257
  }
35760
- const srcNm = join32(depsSource, "node_modules");
35761
- const dstNm = join32(wt.path, "node_modules");
37258
+ const srcNm = join33(depsSource, "node_modules");
37259
+ const dstNm = join33(wt.path, "node_modules");
35762
37260
  if (existsSync36(srcNm) && !existsSync36(dstNm)) {
35763
37261
  const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
35764
37262
  const r = await cloneTree(srcNm, dstNm);
@@ -35769,10 +37267,10 @@ class WorktreeService {
35769
37267
  log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
35770
37268
  }
35771
37269
  for (const f of LOCAL_CONFIG_FILES) {
35772
- const src = join32(depsSource, f);
35773
- if (!existsSync36(src) || existsSync36(join32(wt.path, f)))
37270
+ const src = join33(depsSource, f);
37271
+ if (!existsSync36(src) || existsSync36(join33(wt.path, f)))
35774
37272
  continue;
35775
- const r = await run("cp", [src, join32(wt.path, f)], wt.path);
37273
+ const r = await run("cp", [src, join33(wt.path, f)], wt.path);
35776
37274
  if (r.ok)
35777
37275
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
35778
37276
  else
@@ -35789,7 +37287,8 @@ class WorktreeService {
35789
37287
  } else if (code !== 0)
35790
37288
  this.d.hub.emit("log", wt.id, "setup", `setup failed (exit ${code}): ${cmd}`);
35791
37289
  }
35792
- await this.d.runtime.start(wt, repo);
37290
+ if (start)
37291
+ await this.d.runtime.start(wt, repo);
35793
37292
  }
35794
37293
  setPr(worktreeId, pr) {
35795
37294
  const wt = this.d.state.worktree(worktreeId);
@@ -35940,6 +37439,7 @@ ${wt.landing.body}` : s;
35940
37439
  await this.noteLand(repo, wt, mark);
35941
37440
  await this.restartFromMain(wt, baseOf(repo));
35942
37441
  await this.settleLanded(wt);
37442
+ this.d.hub.emit("landed", wt.id);
35943
37443
  }
35944
37444
  async land(worktreeId, message) {
35945
37445
  const { wt, repo } = this.landable(worktreeId, "land");
@@ -35950,12 +37450,9 @@ ${wt.landing.body}` : s;
35950
37450
  return { result: moved };
35951
37451
  return this.landOp(wt, repo, message);
35952
37452
  });
35953
- if (out.result.ok && out.archiveIds)
35954
- this.noteLanded(wt, out.result.message, out.archiveIds, out.mark);
35955
- return {
35956
- result: this.fixShip(wt, out.result, baseOf(repo)),
35957
- ...out.archiveIds ? { archiveIds: out.archiveIds } : {}
35958
- };
37453
+ if (out.result.ok && out.landed)
37454
+ this.noteLanded(wt, out.result.message, out.mark, out.url);
37455
+ return { result: this.fixShip(wt, out.result, baseOf(repo)) };
35959
37456
  }
35960
37457
  fixShip(wt, result, base) {
35961
37458
  if (result.ok)
@@ -35999,13 +37496,16 @@ ${wt.landing.body}` : s;
35999
37496
  return null;
36000
37497
  return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
36001
37498
  }
36002
- noteLanded(wt, message, archiveIds, mark) {
36003
- this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, archiveIds, ...mark ? { mark } : {}, ts: Date.now() });
37499
+ noteLanded(wt, message, mark, url) {
37500
+ this.d.runtime.ensureAgent(wt).agent.note({
37501
+ type: "landed",
37502
+ message,
37503
+ ...mark ? { mark } : {},
37504
+ ...url ? { url } : {},
37505
+ ts: Date.now()
37506
+ });
36004
37507
  this.d.hub.emit("landed", wt.id);
36005
37508
  }
36006
- siblingIds(wt) {
36007
- return siblingsOf(wt, this.d.state.worktrees).map((s) => s.id);
36008
- }
36009
37509
  async followBranch(wt, current) {
36010
37510
  if (wt.kind === "main")
36011
37511
  return;
@@ -36064,7 +37564,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36064
37564
  if (policy.land === "pr") {
36065
37565
  if (wt.pr?.state === "merged" && !prTaken(wt)) {
36066
37566
  const { mark, ...result } = await this.prMergedOp(wt.id, undefined, w);
36067
- return result.ok ? { result, archiveIds: this.siblingIds(wt), mark } : { result };
37567
+ return result.ok ? { result, landed: true, mark, url: wt.pr.url } : { result };
36068
37568
  }
36069
37569
  const fetched = await this.fetchBase(wt, repo, w);
36070
37570
  if (!fetched.ok)
@@ -36104,7 +37604,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36104
37604
  const { mark: noted, ...r } = await this.prMergedOp(wt.id, mark, w);
36105
37605
  const on = `origin's ${repo.defaultBranch} has this work already`;
36106
37606
  const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
36107
- return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
37607
+ return r.ok ? { result, landed: true, mark: noted, url: wt.pr?.url } : { result };
36108
37608
  }
36109
37609
  const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
36110
37610
  const result = await openPr({
@@ -36160,7 +37660,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36160
37660
  const did = committedHere ? "committed and merged" : "merged";
36161
37661
  return {
36162
37662
  result: { ...result, message: `${did} into ${repo.defaultBranch}` },
36163
- archiveIds: this.siblingIds(wt),
37663
+ landed: true,
36164
37664
  mark: noted
36165
37665
  };
36166
37666
  }
@@ -36223,7 +37723,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36223
37723
  const here = followed.ok ? `${repo.defaultBranch} here ${followed.moved ? "pulled it" : "has it"}` : `${repo.defaultBranch} here was left where it is: ${followed.message}`;
36224
37724
  const how = { merge: "merged into", squash: "squashed onto", rebase: "rebased onto" }[method];
36225
37725
  const did = already ? `origin's ${repo.defaultBranch} has this work already` : `${committed ? `committed and ${how}` : how} ${repo.defaultBranch} and pushed`;
36226
- return { result: { ok: true, message: `${did}; ${here}` }, archiveIds: this.siblingIds(wt), mark: noted };
37726
+ return { result: { ok: true, message: `${did}; ${here}` }, landed: true, mark: noted };
36227
37727
  }
36228
37728
  async noteLand(repo, wt, mark, opts = {}) {
36229
37729
  if (!mark)
@@ -36293,7 +37793,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36293
37793
  else
36294
37794
  log.warn(worktreeId, `landing failed: ${result.message}`);
36295
37795
  if (result.ok && wt)
36296
- this.noteLanded(wt, result.message, this.siblingIds(wt), mark);
37796
+ this.noteLanded(wt, result.message, mark, wt.pr?.url);
36297
37797
  return result;
36298
37798
  }
36299
37799
  async prMergedOp(worktreeId, carried, w = UNWATCHED) {
@@ -36550,6 +38050,37 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36550
38050
  base: baseOf(repo)
36551
38051
  };
36552
38052
  }
38053
+ async standing(worktreeId) {
38054
+ const wt = this.d.state.worktree(worktreeId);
38055
+ if (!wt || wt.kind === "spare" || this.going.has(worktreeId))
38056
+ return null;
38057
+ const repo = this.d.state.repo(wt.repoId);
38058
+ if (!repo)
38059
+ return null;
38060
+ const rec = await this.tree.refresh(worktreeId) ?? this.tree.get(worktreeId);
38061
+ const core = rec?.core;
38062
+ const last = wt.lands?.at(-1);
38063
+ const pr = prStanding(wt);
38064
+ return {
38065
+ main: wt.kind === "main",
38066
+ base: baseOf(repo),
38067
+ ...core ? { dirty: core.files.length } : {},
38068
+ ...core?.ahead === undefined ? {} : { ahead: core.ahead },
38069
+ ...core?.behind === undefined ? {} : { behind: core.behind },
38070
+ ...core?.unpushed === undefined ? {} : { unpushed: core.unpushed },
38071
+ ...wt.landed ? { landed: { subjects: last?.subjects ?? [], ...last?.pr === undefined ? {} : { pr: last.pr } } } : {},
38072
+ ...pr ? {
38073
+ pr: {
38074
+ number: pr.number,
38075
+ url: pr.url,
38076
+ state: pr.state,
38077
+ ...pr.checks ? { checks: pr.checks } : {},
38078
+ ...pr.review ? { review: pr.review } : {},
38079
+ ...pr.automerge ? { automerge: pr.automerge } : {}
38080
+ }
38081
+ } : {}
38082
+ };
38083
+ }
36553
38084
  async turnStarted(worktreeId) {
36554
38085
  const wt = this.d.state.worktree(worktreeId);
36555
38086
  if (wt && !this.going.has(worktreeId))
@@ -36628,6 +38159,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36628
38159
  }
36629
38160
  return this.d.state.requireWorktree(worktreeId);
36630
38161
  }
38162
+ writtenTranscript(worktreeId) {
38163
+ const path = transcriptPathFor(this.d.paths.transcriptsDir, worktreeId);
38164
+ return existsSync36(path) ? path : undefined;
38165
+ }
38166
+ transcriptPath(id) {
38167
+ const path = this.d.state.worktree(id) ? this.writtenTranscript(id) : this.archive.chatFiles(id)?.transcript;
38168
+ if (!path || !existsSync36(path))
38169
+ throw new UserError("nothing has been written to this chat yet");
38170
+ return path;
38171
+ }
36631
38172
  async send(worktreeId, msg) {
36632
38173
  await this.archivingNow(worktreeId);
36633
38174
  if (!this.d.state.worktree(worktreeId) && this.hasArchived(worktreeId)) {
@@ -36721,6 +38262,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36721
38262
  borrowed: rt?.borrowed.size ? [...rt.borrowed] : undefined,
36722
38263
  agent: rt?.agent.status ?? "idle",
36723
38264
  stopping: rt?.agent.stopping || undefined,
38265
+ background: rt?.agent.holding || undefined,
38266
+ wakes: rt?.agent.wakes ?? undefined,
36724
38267
  login: !!rt?.login,
36725
38268
  ahead,
36726
38269
  behind,
@@ -36729,7 +38272,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36729
38272
  queued: rt?.agent.queueLength || undefined,
36730
38273
  unseen: isUnseen(wt) || undefined,
36731
38274
  usage: this.usageFor(wt.id),
36732
- transcript: transcriptPathFor(this.d.paths.transcriptsDir, wt.id),
38275
+ transcript: this.writtenTranscript(wt.id),
36733
38276
  sessionId: this.d.state.session(wt.id),
36734
38277
  shipping: this.ops.get(wt.id),
36735
38278
  syncQueued: this.syncQueued.has(wt.id) || undefined
@@ -36809,11 +38352,8 @@ class ArchiveSweep {
36809
38352
  if (maybe.length === 0)
36810
38353
  continue;
36811
38354
  const fresh = new Map;
36812
- for (const w of beforeGit.rows) {
36813
- if (!maybe.some((m) => m.id === w.id || !!m.variant && m.variant.group === w.variant?.group))
36814
- continue;
38355
+ for (const w of maybe)
36815
38356
  fresh.set(w.id, await this.d.worktrees.freshCounts(w.id));
36816
- }
36817
38357
  const counts = (id) => fresh.get(id) ?? {};
36818
38358
  for (const w of maybe) {
36819
38359
  const reason = archiveReason(w, this.facts(repo.id, counts));
@@ -36861,17 +38401,17 @@ if (pruned > 0)
36861
38401
  var managed = await loadManaged();
36862
38402
  var paths = makePaths();
36863
38403
  ensureDirs(paths);
36864
- forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join33(paths.home, "transcripts-forwarded"));
38404
+ forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join34(paths.home, "transcripts-forwarded"));
36865
38405
  var token = loadOrCreateToken(paths);
36866
38406
  var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
36867
- var helperApp = join33(paths.home, DAEMON_FILES.helper);
38407
+ var helperApp = join34(paths.home, DAEMON_FILES.helper);
36868
38408
  var helper = helperWanted({ platform: process.platform, cloud: cloud.enabled, port, home: process.env.TOYON_HOME }) ? new Helper({
36869
38409
  dir: helperApp,
36870
38410
  stub: HELPER_STUB,
36871
38411
  bun: process.execPath,
36872
38412
  cli: CLI_ENTRY,
36873
- log: join33(paths.home, DAEMON_FILES.launcherLog),
36874
- icon: join33(SHELL_DIST, "icon.svg"),
38413
+ log: join34(paths.home, DAEMON_FILES.launcherLog),
38414
+ icon: join34(SHELL_DIST, "icon.svg"),
36875
38415
  version: package_default.version
36876
38416
  }) : null;
36877
38417
  if (helper) {
@@ -36908,8 +38448,9 @@ var readiness2 = new AgentReadiness({
36908
38448
  connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `check:${spec.id}`),
36909
38449
  cwd: paths.scratchDir,
36910
38450
  options: (id, category) => state.cachedOptions(id, category),
36911
- learned: (id, category, choices) => {
36912
- if (state.setCachedOptions(id, category, choices))
38451
+ runs: (id, category) => state.cachedDefault(id, category),
38452
+ learned: (id, category, choices, runs) => {
38453
+ if (state.setCachedOptions(id, category, choices, runs))
36913
38454
  hub.emit("agentsChanged");
36914
38455
  },
36915
38456
  quick: (id) => {
@@ -36939,6 +38480,8 @@ var runtime = new RuntimeRegistry({
36939
38480
  bridgeScript: () => bridge.get(),
36940
38481
  pageErrors: (id) => pageErrors.ambient(id),
36941
38482
  turnStarting: (id) => worktrees.turnStarted(id),
38483
+ archived: (repoId) => worktrees.archived(repoId),
38484
+ standing: (id) => worktrees.standing(id),
36942
38485
  remote: () => setting.get(),
36943
38486
  gate: previewGate,
36944
38487
  viewed: (id) => idle.isViewed(id),
@@ -36975,13 +38518,17 @@ var worktrees = new WorktreeService({
36975
38518
  interest.onFirst((id) => worktrees.tree.watch(id));
36976
38519
  interest.onLast((id) => worktrees.tree.unwatch(id));
36977
38520
  new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
36978
- var handoff2 = new HandoffService({
38521
+ var pieces = new PiecesService({
36979
38522
  state,
36980
38523
  hub,
36981
38524
  runtime,
36982
- create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts)
38525
+ attachments: attachments2,
38526
+ create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts),
38527
+ counts: (id) => worktrees.tree.counts(id),
38528
+ reader: makePiecesReader(runtime, agents, state)
36983
38529
  });
36984
- tools.add(handoff2);
38530
+ tools.add(pieces);
38531
+ tools.add(new WorktreeLookup({ state, transcriptsDir: paths.transcriptsDir, archived: (repoId) => worktrees.archived(repoId) }));
36985
38532
  var turns = new TurnService({
36986
38533
  state,
36987
38534
  hub,
@@ -37091,6 +38638,8 @@ var keepAwake = new KeepAwake({
37091
38638
  hub,
37092
38639
  demand: () => runtime.demand(),
37093
38640
  assert: idleSleepAssertion(process.env.TOYON_KEEP_AWAKE),
38641
+ bridge: idleSleepBridge(process.env.TOYON_KEEP_AWAKE),
38642
+ carried: process.env[CARRIED_ENV] === "1",
37094
38643
  mode: () => state.keepAwake,
37095
38644
  answerable: () => setting.get() !== null
37096
38645
  });
@@ -37149,7 +38698,7 @@ var {
37149
38698
  landing,
37150
38699
  fix,
37151
38700
  pageErrors,
37152
- handoff: handoff2,
38701
+ pieces,
37153
38702
  mcp,
37154
38703
  themes,
37155
38704
  agents,
@@ -37159,7 +38708,6 @@ var {
37159
38708
  afterLand,
37160
38709
  restarter,
37161
38710
  update,
37162
- planTasks: makePlanner(agents),
37163
38711
  folderDialog: folderDialog()
37164
38712
  }
37165
38713
  });
@@ -37224,7 +38772,7 @@ async function shutdown(signal, opts = {}) {
37224
38772
  shuttingDown = true;
37225
38773
  log.info("daemon", `${signal}: stopping dev servers`);
37226
38774
  idleExit.stop();
37227
- keepAwake.stop();
38775
+ const heldAwake = keepAwake.stop({ handoff: opts.respawn === true });
37228
38776
  tailnet.stop();
37229
38777
  idle.shutdown();
37230
38778
  landing.stop();
@@ -37245,7 +38793,7 @@ async function shutdown(signal, opts = {}) {
37245
38793
  await Bun.sleep(250);
37246
38794
  log.info("daemon", "starting the replacement");
37247
38795
  try {
37248
- respawn(paths.logFile);
38796
+ respawn(paths.logFile, carryEnv(process.env, heldAwake));
37249
38797
  } catch (e) {
37250
38798
  log.error("daemon", "the replacement did not start; run `toyon` to start it again", e);
37251
38799
  process.exit(1);