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.
- package/dist/CHANGELOG.md +74 -0
- package/dist/bridge.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/daemon.js +2279 -731
- package/dist/shell/assets/{Editor-CU91PIeX.js → Editor--Z8ktewC.js} +3 -3
- package/dist/shell/assets/Editor--Z8ktewC.js.br +0 -0
- package/dist/shell/assets/Editor--Z8ktewC.js.gz +0 -0
- package/dist/shell/assets/Editor-CB2SckMT.css.br +0 -0
- package/dist/shell/assets/Editor-CB2SckMT.css.gz +0 -0
- package/dist/shell/assets/XTerm-6GBZ9nXN.css.br +0 -0
- package/dist/shell/assets/XTerm-6GBZ9nXN.css.gz +0 -0
- package/dist/shell/assets/{XTerm-y7vQoOEu.js → XTerm-DsUQ0Xew.js} +1 -1
- package/dist/shell/assets/XTerm-DsUQ0Xew.js.br +0 -0
- package/dist/shell/assets/XTerm-DsUQ0Xew.js.gz +0 -0
- package/dist/shell/assets/abap-DLDM7-KI.js.br +0 -0
- package/dist/shell/assets/abap-DLDM7-KI.js.gz +0 -0
- package/dist/shell/assets/apex-DNDY2TF8.js.br +0 -0
- package/dist/shell/assets/apex-DNDY2TF8.js.gz +0 -0
- package/dist/shell/assets/bat-BwHxbl9M.js.br +0 -0
- package/dist/shell/assets/bat-BwHxbl9M.js.gz +0 -0
- package/dist/shell/assets/bicep-CFznDFnq.js.br +0 -0
- package/dist/shell/assets/bicep-CFznDFnq.js.gz +0 -0
- package/dist/shell/assets/cameligo-Bf6VGUru.js.br +0 -0
- package/dist/shell/assets/cameligo-Bf6VGUru.js.gz +0 -0
- package/dist/shell/assets/clojure-Dnu-v4kV.js.br +0 -0
- package/dist/shell/assets/clojure-Dnu-v4kV.js.gz +0 -0
- package/dist/shell/assets/coffee-Bd8akH9Z.js.br +0 -0
- package/dist/shell/assets/coffee-Bd8akH9Z.js.gz +0 -0
- package/dist/shell/assets/cpp-BbWJElDN.js.br +0 -0
- package/dist/shell/assets/cpp-BbWJElDN.js.gz +0 -0
- package/dist/shell/assets/csharp-Co3qMtFm.js.br +0 -0
- package/dist/shell/assets/csharp-Co3qMtFm.js.gz +0 -0
- package/dist/shell/assets/csp-D-4FJmMZ.js.br +0 -0
- package/dist/shell/assets/csp-D-4FJmMZ.js.gz +0 -0
- package/dist/shell/assets/css-DdJfP1eB.js.br +0 -0
- package/dist/shell/assets/css-DdJfP1eB.js.gz +0 -0
- package/dist/shell/assets/css.worker-CqwWTjeG.js.br +0 -0
- package/dist/shell/assets/css.worker-CqwWTjeG.js.gz +0 -0
- package/dist/shell/assets/{cssMode-B7sFzM6K.js → cssMode-BjAkCPrp.js} +1 -1
- package/dist/shell/assets/cssMode-BjAkCPrp.js.br +0 -0
- package/dist/shell/assets/cssMode-BjAkCPrp.js.gz +0 -0
- package/dist/shell/assets/cypher-cTPe9QuQ.js.br +0 -0
- package/dist/shell/assets/cypher-cTPe9QuQ.js.gz +0 -0
- package/dist/shell/assets/dart-BOtBlQCF.js.br +0 -0
- package/dist/shell/assets/dart-BOtBlQCF.js.gz +0 -0
- package/dist/shell/assets/dockerfile-BG73LgW2.js.br +0 -0
- package/dist/shell/assets/dockerfile-BG73LgW2.js.gz +0 -0
- package/dist/shell/assets/ecl-BEgZUVRK.js.br +0 -0
- package/dist/shell/assets/ecl-BEgZUVRK.js.gz +0 -0
- package/dist/shell/assets/editor.worker-DCKwvLbM.js.br +0 -0
- package/dist/shell/assets/editor.worker-DCKwvLbM.js.gz +0 -0
- package/dist/shell/assets/elixir-BkW5O-1t.js.br +0 -0
- package/dist/shell/assets/elixir-BkW5O-1t.js.gz +0 -0
- package/dist/shell/assets/flow9-BeJ5waoc.js.br +0 -0
- package/dist/shell/assets/flow9-BeJ5waoc.js.gz +0 -0
- package/dist/shell/assets/{freemarker2-ChTLQoDC.js → freemarker2-BZCFxXwq.js} +1 -1
- package/dist/shell/assets/freemarker2-BZCFxXwq.js.br +0 -0
- package/dist/shell/assets/freemarker2-BZCFxXwq.js.gz +0 -0
- package/dist/shell/assets/fsharp-PahG7c26.js.br +0 -0
- package/dist/shell/assets/fsharp-PahG7c26.js.gz +0 -0
- package/dist/shell/assets/go-acbASCJo.js.br +0 -0
- package/dist/shell/assets/go-acbASCJo.js.gz +0 -0
- package/dist/shell/assets/{grammar-xXcxTc3V.js → grammar-CQ1uaD6_.js} +1 -1
- package/dist/shell/assets/grammar-CQ1uaD6_.js.br +0 -0
- package/dist/shell/assets/grammar-CQ1uaD6_.js.gz +0 -0
- package/dist/shell/assets/graphql-BxJiqAUM.js.br +0 -0
- package/dist/shell/assets/graphql-BxJiqAUM.js.gz +0 -0
- package/dist/shell/assets/{handlebars-DPJSozQx.js → handlebars-G1zJeDLP.js} +1 -1
- package/dist/shell/assets/handlebars-G1zJeDLP.js.br +0 -0
- package/dist/shell/assets/handlebars-G1zJeDLP.js.gz +0 -0
- package/dist/shell/assets/hcl-DtV1sZF8.js.br +0 -0
- package/dist/shell/assets/hcl-DtV1sZF8.js.gz +0 -0
- package/dist/shell/assets/{html-Cog2rLcD.js → html-CUHSCFMy.js} +1 -1
- package/dist/shell/assets/html-CUHSCFMy.js.br +0 -0
- package/dist/shell/assets/html-CUHSCFMy.js.gz +0 -0
- package/dist/shell/assets/html.worker-DhJ4VZMp.js.br +0 -0
- package/dist/shell/assets/html.worker-DhJ4VZMp.js.gz +0 -0
- package/dist/shell/assets/{htmlMode-Xm5_ujKb.js → htmlMode-gpMedtd9.js} +1 -1
- package/dist/shell/assets/htmlMode-gpMedtd9.js.br +0 -0
- package/dist/shell/assets/htmlMode-gpMedtd9.js.gz +0 -0
- package/dist/shell/assets/index-B3Alj0WH.css +1 -0
- package/dist/shell/assets/index-B3Alj0WH.css.br +0 -0
- package/dist/shell/assets/index-B3Alj0WH.css.gz +0 -0
- package/dist/shell/assets/index-VVd_p5tJ.js +278 -0
- package/dist/shell/assets/index-VVd_p5tJ.js.br +0 -0
- package/dist/shell/assets/index-VVd_p5tJ.js.gz +0 -0
- package/dist/shell/assets/java-CXBNlu9o.js.br +0 -0
- package/dist/shell/assets/java-CXBNlu9o.js.gz +0 -0
- package/dist/shell/assets/{javascript-Dcv6lw7b.js → javascript-BHWH8gQN.js} +1 -1
- package/dist/shell/assets/json.worker-BF9eV5mS.js.br +0 -0
- package/dist/shell/assets/json.worker-BF9eV5mS.js.gz +0 -0
- package/dist/shell/assets/{jsonMode-D5_V0TW5.js → jsonMode-DyV5nvVT.js} +1 -1
- package/dist/shell/assets/jsonMode-DyV5nvVT.js.br +0 -0
- package/dist/shell/assets/jsonMode-DyV5nvVT.js.gz +0 -0
- package/dist/shell/assets/julia-cl7-CwDS.js.br +0 -0
- package/dist/shell/assets/julia-cl7-CwDS.js.gz +0 -0
- package/dist/shell/assets/kotlin-s7OhZKlX.js.br +0 -0
- package/dist/shell/assets/kotlin-s7OhZKlX.js.gz +0 -0
- package/dist/shell/assets/less-9HpZscsL.js.br +0 -0
- package/dist/shell/assets/less-9HpZscsL.js.gz +0 -0
- package/dist/shell/assets/lexon-OrD6JF1K.js.br +0 -0
- package/dist/shell/assets/lexon-OrD6JF1K.js.gz +0 -0
- package/dist/shell/assets/{liquid-C_zXIF5F.js → liquid-Dy3tUgRC.js} +1 -1
- package/dist/shell/assets/liquid-Dy3tUgRC.js.br +0 -0
- package/dist/shell/assets/liquid-Dy3tUgRC.js.gz +0 -0
- package/dist/shell/assets/{lspLanguageFeatures-CkiTdLmp.js → lspLanguageFeatures-CB_Ow-w5.js} +1 -1
- package/dist/shell/assets/lspLanguageFeatures-CB_Ow-w5.js.br +0 -0
- package/dist/shell/assets/lspLanguageFeatures-CB_Ow-w5.js.gz +0 -0
- package/dist/shell/assets/lua-Cyyb5UIc.js.br +0 -0
- package/dist/shell/assets/lua-Cyyb5UIc.js.gz +0 -0
- package/dist/shell/assets/m3-B8OfTtLu.js.br +0 -0
- package/dist/shell/assets/m3-B8OfTtLu.js.gz +0 -0
- package/dist/shell/assets/markdown-BFxVWTOG.js.br +0 -0
- package/dist/shell/assets/markdown-BFxVWTOG.js.gz +0 -0
- package/dist/shell/assets/{mdx-gvbILQkr.js → mdx-DD5jgco_.js} +1 -1
- package/dist/shell/assets/mdx-DD5jgco_.js.br +0 -0
- package/dist/shell/assets/mdx-DD5jgco_.js.gz +0 -0
- package/dist/shell/assets/mips-CiqrrVzr.js.br +0 -0
- package/dist/shell/assets/mips-CiqrrVzr.js.gz +0 -0
- package/dist/shell/assets/msdax-DmeGPVcC.js.br +0 -0
- package/dist/shell/assets/msdax-DmeGPVcC.js.gz +0 -0
- package/dist/shell/assets/mysql-C_tMU-Nz.js.br +0 -0
- package/dist/shell/assets/mysql-C_tMU-Nz.js.gz +0 -0
- package/dist/shell/assets/objective-c-BDtDVThU.js.br +0 -0
- package/dist/shell/assets/objective-c-BDtDVThU.js.gz +0 -0
- package/dist/shell/assets/pascal-vHIfCaH5.js.br +0 -0
- package/dist/shell/assets/pascal-vHIfCaH5.js.gz +0 -0
- package/dist/shell/assets/pascaligo-DtZ0uQbO.js.br +0 -0
- package/dist/shell/assets/pascaligo-DtZ0uQbO.js.gz +0 -0
- package/dist/shell/assets/perl-Ub6l9XKa.js.br +0 -0
- package/dist/shell/assets/perl-Ub6l9XKa.js.gz +0 -0
- package/dist/shell/assets/pgsql-BlNEE0v7.js.br +0 -0
- package/dist/shell/assets/pgsql-BlNEE0v7.js.gz +0 -0
- package/dist/shell/assets/php-BBUBE1dy.js.br +0 -0
- package/dist/shell/assets/php-BBUBE1dy.js.gz +0 -0
- package/dist/shell/assets/pla-DSh2-awV.js.br +0 -0
- package/dist/shell/assets/pla-DSh2-awV.js.gz +0 -0
- package/dist/shell/assets/postiats-CocnycG-.js.br +0 -0
- package/dist/shell/assets/postiats-CocnycG-.js.gz +0 -0
- package/dist/shell/assets/powerquery-tScXyioY.js.br +0 -0
- package/dist/shell/assets/powerquery-tScXyioY.js.gz +0 -0
- package/dist/shell/assets/powershell-COWaemsV.js.br +0 -0
- package/dist/shell/assets/powershell-COWaemsV.js.gz +0 -0
- package/dist/shell/assets/protobuf-Brw8urJB.js.br +0 -0
- package/dist/shell/assets/protobuf-Brw8urJB.js.gz +0 -0
- package/dist/shell/assets/pug-8SOpv6rk.js.br +0 -0
- package/dist/shell/assets/pug-8SOpv6rk.js.gz +0 -0
- package/dist/shell/assets/{python-CZTrOkh_.js → python-BvLKUXll.js} +1 -1
- package/dist/shell/assets/python-BvLKUXll.js.br +0 -0
- package/dist/shell/assets/python-BvLKUXll.js.gz +0 -0
- package/dist/shell/assets/qsharp-Bw9ernYp.js.br +0 -0
- package/dist/shell/assets/qsharp-Bw9ernYp.js.gz +0 -0
- package/dist/shell/assets/r-j7ic8hl3.js.br +0 -0
- package/dist/shell/assets/r-j7ic8hl3.js.gz +0 -0
- package/dist/shell/assets/{razor-DowlBj1i.js → razor-B2tUlbzR.js} +1 -1
- package/dist/shell/assets/razor-B2tUlbzR.js.br +0 -0
- package/dist/shell/assets/razor-B2tUlbzR.js.gz +0 -0
- package/dist/shell/assets/redis-Bu5POkcn.js.br +0 -0
- package/dist/shell/assets/redis-Bu5POkcn.js.gz +0 -0
- package/dist/shell/assets/redshift-Bs9aos_-.js.br +0 -0
- package/dist/shell/assets/redshift-Bs9aos_-.js.gz +0 -0
- package/dist/shell/assets/restructuredtext-CqXO7rUv.js.br +0 -0
- package/dist/shell/assets/restructuredtext-CqXO7rUv.js.gz +0 -0
- package/dist/shell/assets/ruby-zBfavPgS.js.br +0 -0
- package/dist/shell/assets/ruby-zBfavPgS.js.gz +0 -0
- package/dist/shell/assets/rust-BzKRNQWT.js.br +0 -0
- package/dist/shell/assets/rust-BzKRNQWT.js.gz +0 -0
- package/dist/shell/assets/sb-BBc9UKZt.js.br +0 -0
- package/dist/shell/assets/sb-BBc9UKZt.js.gz +0 -0
- package/dist/shell/assets/scala-D9hQfWCl.js.br +0 -0
- package/dist/shell/assets/scala-D9hQfWCl.js.gz +0 -0
- package/dist/shell/assets/scheme-BPhDTwHR.js.br +0 -0
- package/dist/shell/assets/scheme-BPhDTwHR.js.gz +0 -0
- package/dist/shell/assets/scss-CBJaRo0y.js.br +0 -0
- package/dist/shell/assets/scss-CBJaRo0y.js.gz +0 -0
- package/dist/shell/assets/shell-DiJ1NA_G.js.br +0 -0
- package/dist/shell/assets/shell-DiJ1NA_G.js.gz +0 -0
- package/dist/shell/assets/solidity-Db0IVjzk.js.br +0 -0
- package/dist/shell/assets/solidity-Db0IVjzk.js.gz +0 -0
- package/dist/shell/assets/sophia-CnS9iZB_.js.br +0 -0
- package/dist/shell/assets/sophia-CnS9iZB_.js.gz +0 -0
- package/dist/shell/assets/sparql-CJmd_6j2.js.br +0 -0
- package/dist/shell/assets/sparql-CJmd_6j2.js.gz +0 -0
- package/dist/shell/assets/sql-ClhHkBeG.js.br +0 -0
- package/dist/shell/assets/sql-ClhHkBeG.js.gz +0 -0
- package/dist/shell/assets/st-CHwy0fLd.js.br +0 -0
- package/dist/shell/assets/st-CHwy0fLd.js.gz +0 -0
- package/dist/shell/assets/swift-Bqt4WxQ4.js.br +0 -0
- package/dist/shell/assets/swift-Bqt4WxQ4.js.gz +0 -0
- package/dist/shell/assets/systemverilog-Bs9z6M-B.js.br +0 -0
- package/dist/shell/assets/systemverilog-Bs9z6M-B.js.gz +0 -0
- package/dist/shell/assets/tcl-Dm6ycUr_.js.br +0 -0
- package/dist/shell/assets/tcl-Dm6ycUr_.js.gz +0 -0
- package/dist/shell/assets/ts.worker-DTZAwq0V.js.br +0 -0
- package/dist/shell/assets/ts.worker-DTZAwq0V.js.gz +0 -0
- package/dist/shell/assets/{tsMode-BNl2cV_K.js → tsMode-DMuNnqM7.js} +1 -1
- package/dist/shell/assets/tsMode-DMuNnqM7.js.br +0 -0
- package/dist/shell/assets/tsMode-DMuNnqM7.js.gz +0 -0
- package/dist/shell/assets/twig-Csy3S7wG.js.br +0 -0
- package/dist/shell/assets/twig-Csy3S7wG.js.gz +0 -0
- package/dist/shell/assets/typespec-Btyra-wh.js.br +0 -0
- package/dist/shell/assets/typespec-Btyra-wh.js.gz +0 -0
- package/dist/shell/assets/vb-Db0cS2oM.js.br +0 -0
- package/dist/shell/assets/vb-Db0cS2oM.js.gz +0 -0
- package/dist/shell/assets/wasm-inlined-CG6Dc4jp.js.br +0 -0
- package/dist/shell/assets/wasm-inlined-CG6Dc4jp.js.gz +0 -0
- package/dist/shell/assets/wgsl-BTesnYfV.js.br +0 -0
- package/dist/shell/assets/wgsl-BTesnYfV.js.gz +0 -0
- package/dist/shell/assets/{xml-DIiuHFHw.js → xml-DdYgb5a2.js} +1 -1
- package/dist/shell/assets/xml-DdYgb5a2.js.br +0 -0
- package/dist/shell/assets/xml-DdYgb5a2.js.gz +0 -0
- package/dist/shell/assets/{yaml-CBnMyn4j.js → yaml-BDujJdoL.js} +1 -1
- package/dist/shell/assets/yaml-BDujJdoL.js.br +0 -0
- package/dist/shell/assets/yaml-BDujJdoL.js.gz +0 -0
- package/dist/shell/index.html +11 -8
- package/dist/shell/manifest.json +13 -0
- package/dist/shell/sw.js +117 -22
- package/package.json +2 -2
- package/dist/shell/assets/index-BpW1Kufz.css +0 -1
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
162
|
+
if (rowsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
|
|
136
163
|
return null;
|
|
137
|
-
const what =
|
|
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
|
|
168
|
+
function nothingToKeep(w, f) {
|
|
142
169
|
if (w.kind !== "worktree" || !hasOwnBranch(w))
|
|
143
170
|
return false;
|
|
144
|
-
if (
|
|
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
|
-
|
|
153
|
-
return c.dirty === 0 && (lost ? c.ahead !== undefined : c.ahead === 0);
|
|
175
|
+
return heldBy(w, f) === null;
|
|
154
176
|
}
|
|
155
|
-
function
|
|
177
|
+
function rowsAhead(wt, rows) {
|
|
156
178
|
const tasks = railOrder(rows.filter((w) => w.kind === "worktree").map((worktree) => ({ worktree })));
|
|
157
|
-
const
|
|
158
|
-
|
|
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 =
|
|
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("
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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).
|
|
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,
|
|
15604
|
-
|
|
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.
|
|
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 &&
|
|
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 ||
|
|
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) :
|
|
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 `
|
|
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
|
|
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
|
-
|
|
16316
|
+
text,
|
|
16152
16317
|
note ? `The person added, when they approved this: ${note}` : undefined,
|
|
16153
|
-
`This
|
|
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
|
|
16323
|
+
function pieceAskPrompt(text, project) {
|
|
16159
16324
|
const own = text?.trim();
|
|
16160
|
-
|
|
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
|
|
16163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
16900
|
+
onEnd: () => this.holdingChanged(),
|
|
16670
16901
|
...d.taskPollMs !== undefined ? { pollMs: d.taskPollMs } : {},
|
|
16671
16902
|
...d.claudeConfigDir !== undefined ? { configDir: d.claudeConfigDir } : {}
|
|
16672
16903
|
});
|
|
16673
|
-
this.
|
|
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.
|
|
16701
|
-
if (event.type === "
|
|
16936
|
+
this.trackCards(event);
|
|
16937
|
+
if (event.type === "pieces-go")
|
|
16702
16938
|
going.add(event.id);
|
|
16703
|
-
else if (event.type === "
|
|
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: "
|
|
16944
|
+
type: "pieces-declined",
|
|
16709
16945
|
id,
|
|
16710
16946
|
outcome: "failed",
|
|
16711
|
-
reason: "Toyon stopped while the
|
|
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
|
-
|
|
16955
|
+
this.showHeld();
|
|
16956
|
+
if (this.openCards.size > 0)
|
|
16720
16957
|
this.syncStatus();
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
if (event.type === "
|
|
16726
|
-
|
|
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.
|
|
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
|
-
|
|
17169
|
-
|
|
17170
|
-
text,
|
|
17171
|
-
|
|
17172
|
-
|
|
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 === "
|
|
17234
|
-
out.unshift(
|
|
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) =>
|
|
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
|
-
|
|
17883
|
+
const approved = choice.kind === "allow_once" || choice.kind === "allow_always";
|
|
17884
|
+
if (plan && approved)
|
|
17562
17885
|
this.d.setMode?.(modeAfterPlan(choice.name));
|
|
17563
|
-
|
|
17564
|
-
|
|
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.
|
|
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.
|
|
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) =>
|
|
19006
|
-
|
|
19007
|
-
|
|
19008
|
-
|
|
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,
|
|
19882
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
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
|
|
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
|
|
20295
|
-
|
|
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
|
|
20301
|
-
|
|
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 = "
|
|
20337
|
-
var
|
|
20681
|
+
var FORWARD_STEP = "3";
|
|
20682
|
+
var MARKS = ['"page', '"brief"', '"handoff'];
|
|
20338
20683
|
function forwardLine(line) {
|
|
20339
|
-
if (!line.includes(
|
|
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
|
|
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: { ...
|
|
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
|
-
|
|
21790
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
30148
|
+
s.pieces.fate(msg.worktreeId, msg.id, msg.pieceId, msg.fate);
|
|
29724
30149
|
},
|
|
29725
|
-
async "
|
|
30150
|
+
async "pieces-answer"(msg, _ctx, s) {
|
|
29726
30151
|
requireRun(s, msg.worktreeId);
|
|
29727
|
-
await s.
|
|
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.
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
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("
|
|
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
|
|
31064
|
+
const helloReads = async () => {
|
|
30497
31065
|
await s.update.refresh();
|
|
30498
|
-
|
|
30499
|
-
|
|
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:
|
|
31076
|
+
registry: read.registry,
|
|
30504
31077
|
protocol: PROTOCOL_VERSION,
|
|
30505
|
-
|
|
30506
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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-${
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
33493
|
-
import { basename as basename11, dirname as dirname12, join as
|
|
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(
|
|
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 =
|
|
35045
|
+
const file = join30(this.filesOf(id).plans, basename11(rel));
|
|
33561
35046
|
if (!existsSync34(file))
|
|
33562
35047
|
return null;
|
|
33563
35048
|
try {
|
|
33564
|
-
return
|
|
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 =
|
|
35056
|
+
const dir = join30(this.dir, id);
|
|
33572
35057
|
return {
|
|
33573
35058
|
dir,
|
|
33574
|
-
record:
|
|
33575
|
-
transcript:
|
|
33576
|
-
attachments:
|
|
33577
|
-
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 =
|
|
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
|
|
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
|
|
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(
|
|
35271
|
+
const { id, dir } = freeSlot(join31(this.d.paths.worktreesDir, repo.name));
|
|
33785
35272
|
const wt = {
|
|
33786
35273
|
id,
|
|
33787
35274
|
repoId,
|
|
33788
|
-
path:
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
34827
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
34888
|
-
const wtPath =
|
|
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.
|
|
34914
|
-
this.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
36501
|
+
if (existsSync36(join33(this.d.paths.worktreesDir, repo.name, slug)))
|
|
35000
36502
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
35001
|
-
const wtPath =
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
36806
|
+
return join33(this.d.paths.archiveDir, `${worktreeId}.plans`);
|
|
35315
36807
|
}
|
|
35316
36808
|
keepPlans(wt) {
|
|
35317
|
-
const from =
|
|
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) ?
|
|
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) ?
|
|
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
|
-
...
|
|
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,
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
35761
|
-
const dstNm =
|
|
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 =
|
|
35773
|
-
if (!existsSync36(src) || existsSync36(
|
|
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,
|
|
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
|
-
|
|
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.
|
|
35954
|
-
this.noteLanded(wt, out.result.message, out.
|
|
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,
|
|
36003
|
-
this.d.runtime.ensureAgent(wt).agent.note({
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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}` },
|
|
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,
|
|
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:
|
|
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
|
|
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],
|
|
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 =
|
|
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:
|
|
36874
|
-
icon:
|
|
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
|
-
|
|
36912
|
-
|
|
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
|
|
38521
|
+
var pieces = new PiecesService({
|
|
36979
38522
|
state,
|
|
36980
38523
|
hub,
|
|
36981
38524
|
runtime,
|
|
36982
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|