toyon 0.18.0 → 0.19.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 +41 -0
- package/dist/bridge.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/daemon.js +1719 -684
- package/dist/shell/assets/{Editor-CU91PIeX.js → Editor-B-1lrUVF.js} +3 -3
- package/dist/shell/assets/Editor-B-1lrUVF.js.br +0 -0
- package/dist/shell/assets/Editor-B-1lrUVF.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-CZl-vWEO.js} +1 -1
- package/dist/shell/assets/XTerm-CZl-vWEO.js.br +0 -0
- package/dist/shell/assets/XTerm-CZl-vWEO.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-BpigOakf.js} +1 -1
- package/dist/shell/assets/cssMode-BpigOakf.js.br +0 -0
- package/dist/shell/assets/cssMode-BpigOakf.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-vvh2KLdJ.js} +1 -1
- package/dist/shell/assets/freemarker2-vvh2KLdJ.js.br +0 -0
- package/dist/shell/assets/freemarker2-vvh2KLdJ.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-DBMSnNcB.js} +1 -1
- package/dist/shell/assets/grammar-DBMSnNcB.js.br +0 -0
- package/dist/shell/assets/grammar-DBMSnNcB.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-ExFAR1bz.js} +1 -1
- package/dist/shell/assets/handlebars-ExFAR1bz.js.br +0 -0
- package/dist/shell/assets/handlebars-ExFAR1bz.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-DpIWoe_L.js} +1 -1
- package/dist/shell/assets/html-DpIWoe_L.js.br +0 -0
- package/dist/shell/assets/html-DpIWoe_L.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-vCEOKCM-.js} +1 -1
- package/dist/shell/assets/htmlMode-vCEOKCM-.js.br +0 -0
- package/dist/shell/assets/htmlMode-vCEOKCM-.js.gz +0 -0
- package/dist/shell/assets/index-CHbNBHbD.css +1 -0
- package/dist/shell/assets/index-CHbNBHbD.css.br +0 -0
- package/dist/shell/assets/index-CHbNBHbD.css.gz +0 -0
- package/dist/shell/assets/index-D5GN5OOv.js +276 -0
- package/dist/shell/assets/index-D5GN5OOv.js.br +0 -0
- package/dist/shell/assets/index-D5GN5OOv.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-CF6f9y1O.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-CZzpDv9U.js} +1 -1
- package/dist/shell/assets/jsonMode-CZzpDv9U.js.br +0 -0
- package/dist/shell/assets/jsonMode-CZzpDv9U.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-5T0ZgJhW.js} +1 -1
- package/dist/shell/assets/liquid-5T0ZgJhW.js.br +0 -0
- package/dist/shell/assets/liquid-5T0ZgJhW.js.gz +0 -0
- package/dist/shell/assets/{lspLanguageFeatures-CkiTdLmp.js → lspLanguageFeatures-DrkNrp6u.js} +1 -1
- package/dist/shell/assets/lspLanguageFeatures-DrkNrp6u.js.br +0 -0
- package/dist/shell/assets/lspLanguageFeatures-DrkNrp6u.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-DnLcCYKb.js} +1 -1
- package/dist/shell/assets/mdx-DnLcCYKb.js.br +0 -0
- package/dist/shell/assets/mdx-DnLcCYKb.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-Di4WuyJe.js} +1 -1
- package/dist/shell/assets/python-Di4WuyJe.js.br +0 -0
- package/dist/shell/assets/python-Di4WuyJe.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-NpEB1IHs.js} +1 -1
- package/dist/shell/assets/razor-NpEB1IHs.js.br +0 -0
- package/dist/shell/assets/razor-NpEB1IHs.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-BylSuxTF.js} +1 -1
- package/dist/shell/assets/tsMode-BylSuxTF.js.br +0 -0
- package/dist/shell/assets/tsMode-BylSuxTF.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-BYRl5HAh.js} +1 -1
- package/dist/shell/assets/xml-BYRl5HAh.js.br +0 -0
- package/dist/shell/assets/xml-BYRl5HAh.js.gz +0 -0
- package/dist/shell/assets/{yaml-CBnMyn4j.js → yaml-DeSD0TUR.js} +1 -1
- package/dist/shell/assets/yaml-DeSD0TUR.js.br +0 -0
- package/dist/shell/assets/yaml-DeSD0TUR.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,24 @@ 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
|
+
function coalesce(entries) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const last = out.at(-1);
|
|
19
|
+
const { event } = entry;
|
|
20
|
+
if (event.type === "text-delta" && last?.event.type === "text-delta" && last.event.messageId === event.messageId) {
|
|
21
|
+
out[out.length - 1] = { seq: last.seq, event: { ...last.event, text: last.event.text + event.text } };
|
|
22
|
+
} else if (event.type === "thinking-delta" && last?.event.type === "thinking-delta") {
|
|
23
|
+
out[out.length - 1] = { seq: last.seq, event: { type: "thinking-delta", text: last.event.text + event.text } };
|
|
24
|
+
} else if (event.type === "tool-delta" && last?.event.type === "tool-delta" && last.event.toolId === event.toolId) {
|
|
25
|
+
out[out.length - 1] = { seq: last.seq, event: { ...event, text: last.event.text + event.text } };
|
|
26
|
+
} else {
|
|
27
|
+
out.push(entry);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
// ../shared/src/agent-tools.ts
|
|
17
34
|
var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
|
|
@@ -39,12 +56,6 @@ function emptyInput(input) {
|
|
|
39
56
|
var DEFAULT_TIMEOUT_MS = 10 * 60000;
|
|
40
57
|
var DEFAULT_PERMISSION_MODE = "auto";
|
|
41
58
|
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
59
|
function hasOwnBranch(wt) {
|
|
49
60
|
return wt.branch.startsWith("toyon/");
|
|
50
61
|
}
|
|
@@ -100,45 +111,22 @@ function canGraft(wt) {
|
|
|
100
111
|
// ../shared/src/railOrder.ts
|
|
101
112
|
var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
|
|
102
113
|
var tierOf = (w) => isLead(w) ? 0 : 1;
|
|
103
|
-
var railUnitOf = (w) => w.variant ? `group:${w.variant.group}` : `row:${w.id}`;
|
|
104
114
|
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);
|
|
115
|
+
return [...rows].sort((a, b) => tierOf(a.worktree) - tierOf(b.worktree) || sentAt(b.worktree) - sentAt(a.worktree));
|
|
122
116
|
}
|
|
123
117
|
|
|
124
118
|
// ../shared/src/archiveRule.ts
|
|
125
119
|
var ARCHIVE_KEEP_RECENT = 5;
|
|
126
120
|
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)))
|
|
121
|
+
if (!nothingToKeep(wt, f))
|
|
134
122
|
return null;
|
|
135
|
-
if (
|
|
123
|
+
if (rowsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
|
|
136
124
|
return null;
|
|
137
|
-
const what =
|
|
125
|
+
const what = wt.landed ? "landed" : "no changes";
|
|
138
126
|
return `${what}, ${UNOPENED}${duration(f.afterMs)}`;
|
|
139
127
|
}
|
|
140
128
|
var UNOPENED = "not opened in ";
|
|
141
|
-
function nothingToKeep(w, f
|
|
129
|
+
function nothingToKeep(w, f) {
|
|
142
130
|
if (w.kind !== "worktree" || !hasOwnBranch(w))
|
|
143
131
|
return false;
|
|
144
132
|
if (w.lastTurn?.end !== "done" || isUnseen(w))
|
|
@@ -150,19 +138,12 @@ function nothingToKeep(w, f, lost) {
|
|
|
150
138
|
if (f.now - (w.viewedAt ?? w.createdAt) < f.afterMs)
|
|
151
139
|
return false;
|
|
152
140
|
const c = f.counts(w.id);
|
|
153
|
-
return c.dirty === 0 &&
|
|
141
|
+
return c.dirty === 0 && c.ahead === 0;
|
|
154
142
|
}
|
|
155
|
-
function
|
|
143
|
+
function rowsAhead(wt, rows) {
|
|
156
144
|
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;
|
|
145
|
+
const at = tasks.findIndex(({ worktree: w }) => w.id === wt.id);
|
|
146
|
+
return at < 0 ? tasks.length : at;
|
|
166
147
|
}
|
|
167
148
|
function duration(ms) {
|
|
168
149
|
if (ms >= 60 * 60000)
|
|
@@ -536,6 +517,9 @@ function prTaken(wt) {
|
|
|
536
517
|
function shipNoun(op) {
|
|
537
518
|
return { land: "a land", commit: "a commit", "sync-main": "a sync", "pull-main": "a pull" }[op];
|
|
538
519
|
}
|
|
520
|
+
function prStanding(wt) {
|
|
521
|
+
return prTaken(wt) && !wt.landed ? undefined : wt.pr;
|
|
522
|
+
}
|
|
539
523
|
|
|
540
524
|
// ../../node_modules/.bun/zod@4.5.4/node_modules/zod/v4/core/util.js
|
|
541
525
|
function getEnumValues(entries) {
|
|
@@ -6602,7 +6586,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
|
|
|
6602
6586
|
}
|
|
6603
6587
|
|
|
6604
6588
|
// ../shared/src/protocol/limits.ts
|
|
6605
|
-
var PROTOCOL_VERSION =
|
|
6589
|
+
var PROTOCOL_VERSION = 129;
|
|
6606
6590
|
var PAGE_ERROR_MAX_CHARS = 2000;
|
|
6607
6591
|
var PAGE_TITLE_MAX_CHARS = 1000;
|
|
6608
6592
|
var FILE_MAX_CHARS = 5000000;
|
|
@@ -6612,7 +6596,6 @@ var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
6612
6596
|
var PASTE_MAX_CHARS = 1e5;
|
|
6613
6597
|
var FILE_INLINE_CHARS = 20000;
|
|
6614
6598
|
var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
6615
|
-
var VARIANTS_MAX = 3;
|
|
6616
6599
|
|
|
6617
6600
|
// ../shared/src/paste.ts
|
|
6618
6601
|
var PREVIEW_CHARS = 80;
|
|
@@ -6677,6 +6660,19 @@ var elementTraitsSchema = object({
|
|
|
6677
6660
|
attrs: array(tuple([string2().max(40), string2().max(200)])).max(12)
|
|
6678
6661
|
});
|
|
6679
6662
|
|
|
6663
|
+
// ../shared/src/protocol/stamps.ts
|
|
6664
|
+
var STAMP_PARTS = ["rows", "repos", "visits", "themes"];
|
|
6665
|
+
var PART = /^[0-9a-f]*$/;
|
|
6666
|
+
function parseKnown(q) {
|
|
6667
|
+
if (!q)
|
|
6668
|
+
return null;
|
|
6669
|
+
const parts = q.split(".");
|
|
6670
|
+
if (parts.length !== STAMP_PARTS.length || !parts.every((p) => p.length <= 32 && PART.test(p)))
|
|
6671
|
+
return null;
|
|
6672
|
+
return { rows: parts[0], repos: parts[1], visits: parts[2], themes: parts[3] };
|
|
6673
|
+
}
|
|
6674
|
+
var sameStamp = (held, now) => held !== "" && held === now;
|
|
6675
|
+
|
|
6680
6676
|
// ../shared/src/protocol/ws.ts
|
|
6681
6677
|
var id = string2().min(1).max(200);
|
|
6682
6678
|
var permissionModeSchema = _enum(["auto", "ask", "plan"]);
|
|
@@ -6848,7 +6844,7 @@ var themePrefsSchema = object({
|
|
|
6848
6844
|
dark: string2()
|
|
6849
6845
|
});
|
|
6850
6846
|
var clientMsgSchema = discriminatedUnion("t", [
|
|
6851
|
-
object({ t: literal("subscribe"), worktreeId: id }),
|
|
6847
|
+
object({ t: literal("subscribe"), worktreeId: id, since: number2().int().min(0).optional() }),
|
|
6852
6848
|
object({ t: literal("unsubscribe"), worktreeId: id }),
|
|
6853
6849
|
object({ t: literal("view"), worktreeId: id.nullable() }),
|
|
6854
6850
|
object({
|
|
@@ -6866,7 +6862,6 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6866
6862
|
repoId: id,
|
|
6867
6863
|
worktreeId: id.optional(),
|
|
6868
6864
|
prompt,
|
|
6869
|
-
variants: number2().int().min(1).max(VARIANTS_MAX).optional(),
|
|
6870
6865
|
context: ambient.optional(),
|
|
6871
6866
|
attachments,
|
|
6872
6867
|
agent: id.optional(),
|
|
@@ -6877,15 +6872,6 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6877
6872
|
carry: boolean2().optional(),
|
|
6878
6873
|
boxId
|
|
6879
6874
|
}),
|
|
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
6875
|
object({ t: literal("set-worktree-mode"), worktreeId: id, mode: permissionModeSchema }),
|
|
6890
6876
|
object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
|
|
6891
6877
|
object({ t: literal("set-worktree-effort"), worktreeId: id, effort: string2().max(100) }),
|
|
@@ -6978,7 +6964,6 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6978
6964
|
object({ t: literal("forget-visit"), repoId: id, path: routePath }),
|
|
6979
6965
|
object({ t: literal("page-error"), worktreeId: id, message: string2().min(1).max(PAGE_ERROR_MAX_CHARS) }),
|
|
6980
6966
|
object({ t: literal("page-loaded"), worktreeId: id }),
|
|
6981
|
-
object({ t: literal("pick-variant"), worktreeId: id }),
|
|
6982
6967
|
object({
|
|
6983
6968
|
t: literal("unqueue"),
|
|
6984
6969
|
worktreeId: id,
|
|
@@ -7047,14 +7032,21 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
7047
7032
|
choiceId: string2().max(200),
|
|
7048
7033
|
note: string2().max(1e4).optional()
|
|
7049
7034
|
}),
|
|
7035
|
+
object({ t: literal("pieces-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
|
|
7036
|
+
object({
|
|
7037
|
+
t: literal("pieces-fate"),
|
|
7038
|
+
worktreeId: id,
|
|
7039
|
+
id: string2().max(64),
|
|
7040
|
+
pieceId: string2().max(16),
|
|
7041
|
+
fate: _enum(["here", "worktree", "send", "skip"])
|
|
7042
|
+
}),
|
|
7050
7043
|
object({
|
|
7051
|
-
t: literal("
|
|
7044
|
+
t: literal("pieces-answer"),
|
|
7052
7045
|
worktreeId: id,
|
|
7053
7046
|
id: string2().max(64),
|
|
7054
7047
|
go: boolean2(),
|
|
7055
7048
|
note: string2().max(5000).optional()
|
|
7056
7049
|
}),
|
|
7057
|
-
object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
|
|
7058
7050
|
object({ t: literal("agent-logout"), agent: id }),
|
|
7059
7051
|
object({ t: literal("agent-check"), agent: id }),
|
|
7060
7052
|
object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
|
|
@@ -8526,7 +8518,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
|
|
|
8526
8518
|
// ../daemon/package.json
|
|
8527
8519
|
var package_default = {
|
|
8528
8520
|
name: "@toyon/daemon",
|
|
8529
|
-
version: "0.
|
|
8521
|
+
version: "0.19.0",
|
|
8530
8522
|
private: true,
|
|
8531
8523
|
type: "module",
|
|
8532
8524
|
main: "src/index.ts",
|
|
@@ -13354,7 +13346,7 @@ class ToolSet {
|
|
|
13354
13346
|
}
|
|
13355
13347
|
var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
|
|
13356
13348
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
13357
|
-
var BODY_MAX_BYTES =
|
|
13349
|
+
var BODY_MAX_BYTES = 256 * 1024;
|
|
13358
13350
|
var PARSE_ERROR = -32700;
|
|
13359
13351
|
var INVALID_PARAMS = -32602;
|
|
13360
13352
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -13613,10 +13605,14 @@ class AgentReadiness {
|
|
|
13613
13605
|
const e = this.known.get(info.id);
|
|
13614
13606
|
const models = this.d.options(info.id, "model");
|
|
13615
13607
|
const efforts = this.d.options(info.id, "thought_level");
|
|
13608
|
+
const defaultModel = this.d.runs(info.id, "model");
|
|
13609
|
+
const defaultEffort = this.d.runs(info.id, "thought_level");
|
|
13616
13610
|
return {
|
|
13617
13611
|
...info,
|
|
13618
13612
|
...models.length > 0 ? { models } : {},
|
|
13619
13613
|
...efforts.length > 0 ? { efforts } : {},
|
|
13614
|
+
...defaultModel !== undefined ? { defaultModel } : {},
|
|
13615
|
+
...defaultEffort !== undefined ? { defaultEffort } : {},
|
|
13620
13616
|
...this.d.quick(info.id) ? { quick: true } : {},
|
|
13621
13617
|
...e?.status ? { auth: e.status } : {},
|
|
13622
13618
|
...e?.canLogout ? { canLogout: true } : {},
|
|
@@ -13685,8 +13681,9 @@ class AgentReadiness {
|
|
|
13685
13681
|
});
|
|
13686
13682
|
found.canLogout = supportsLogout(init.agentCapabilities);
|
|
13687
13683
|
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);
|
|
13684
|
+
for (const [category, opt] of readOptions(s.configOptions)) {
|
|
13685
|
+
this.d.learned(agentId, category, opt.choices, opt.current);
|
|
13686
|
+
}
|
|
13690
13687
|
if (init.agentCapabilities?.sessionCapabilities?.close) {
|
|
13691
13688
|
await link.conn.agent.request(methods.agent.session.close, { sessionId: s.sessionId }).catch((e) => {
|
|
13692
13689
|
log.debug(tag, `session/close failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -14591,6 +14588,12 @@ function sameAsLast(cwd, rel, markdown) {
|
|
|
14591
14588
|
}
|
|
14592
14589
|
}
|
|
14593
14590
|
async function writePlanDoc(cwd, markdown) {
|
|
14591
|
+
const rel = writePlanDocNow(cwd, markdown);
|
|
14592
|
+
if (rel)
|
|
14593
|
+
await excludePlans(cwd);
|
|
14594
|
+
return rel;
|
|
14595
|
+
}
|
|
14596
|
+
function writePlanDocNow(cwd, markdown) {
|
|
14594
14597
|
const dir = join5(cwd, PLANS_DIR);
|
|
14595
14598
|
const last = lastNumber(dir);
|
|
14596
14599
|
if (last > 0) {
|
|
@@ -14608,9 +14611,18 @@ async function writePlanDoc(cwd, markdown) {
|
|
|
14608
14611
|
log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
|
|
14609
14612
|
return null;
|
|
14610
14613
|
}
|
|
14611
|
-
|
|
14614
|
+
excludePlans(cwd).catch((e) => log.warn(cwd, "could not exclude the plans folder", e));
|
|
14612
14615
|
return rel;
|
|
14613
14616
|
}
|
|
14617
|
+
function listDoc(pieces) {
|
|
14618
|
+
return `# Worktrees
|
|
14619
|
+
|
|
14620
|
+
${pieces.map((p) => `## ${p.title}
|
|
14621
|
+
|
|
14622
|
+
${p.plan.trim()}
|
|
14623
|
+
`).join(`
|
|
14624
|
+
`)}`;
|
|
14625
|
+
}
|
|
14614
14626
|
function planEdited(cwd, rel, proposed) {
|
|
14615
14627
|
const file = join5(cwd, rel);
|
|
14616
14628
|
if (!existsSync7(file))
|
|
@@ -14728,9 +14740,8 @@ var BUILTIN_AGENTS = [
|
|
|
14728
14740
|
name: "Claude Code",
|
|
14729
14741
|
short: "Claude",
|
|
14730
14742
|
builtin: true,
|
|
14731
|
-
run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.
|
|
14743
|
+
run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.95.0", bin: "claude-agent-acp" },
|
|
14732
14744
|
env: {
|
|
14733
|
-
CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: "1",
|
|
14734
14745
|
...cloud.enabled ? { NO_BROWSER: "1" } : {}
|
|
14735
14746
|
},
|
|
14736
14747
|
terminalLogins: {
|
|
@@ -15600,11 +15611,8 @@ function toyonTool(update) {
|
|
|
15600
15611
|
}
|
|
15601
15612
|
return null;
|
|
15602
15613
|
}
|
|
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";
|
|
15614
|
+
function toyonTitle(tool, title) {
|
|
15615
|
+
return title && toyonTool({ name: title }) === null ? title : tool;
|
|
15608
15616
|
}
|
|
15609
15617
|
function heading(update) {
|
|
15610
15618
|
const name = typeof update.name === "string" ? update.name : "";
|
|
@@ -15616,7 +15624,7 @@ function heading(update) {
|
|
|
15616
15624
|
}
|
|
15617
15625
|
const own = toyonTool(update);
|
|
15618
15626
|
if (own)
|
|
15619
|
-
return { name: own, title: toyonTitle(own, update.
|
|
15627
|
+
return { name: own, title: toyonTitle(own, update.title), kind: "other" };
|
|
15620
15628
|
return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
|
|
15621
15629
|
}
|
|
15622
15630
|
function endOfAsk(req, res, memos) {
|
|
@@ -15641,18 +15649,21 @@ function release(toolId, memo) {
|
|
|
15641
15649
|
delete memo.held;
|
|
15642
15650
|
if (!emptyInput(memo.input))
|
|
15643
15651
|
memo.writing = false;
|
|
15652
|
+
memo.released = true;
|
|
15644
15653
|
return memo.ended ? null : { type: "tool-update", toolId, ...held };
|
|
15645
15654
|
}
|
|
15646
15655
|
function preempted(memos) {
|
|
15647
15656
|
for (const memo of memos.values()) {
|
|
15648
|
-
if (!memo.ended &&
|
|
15657
|
+
if (!memo.ended && !memo.parent)
|
|
15649
15658
|
memo.cut = true;
|
|
15650
15659
|
}
|
|
15651
15660
|
}
|
|
15652
15661
|
function abandoned(memos, parent, opening = false) {
|
|
15653
15662
|
const out = [];
|
|
15654
15663
|
for (const [toolId, memo] of memos) {
|
|
15655
|
-
if (memo.ended ||
|
|
15664
|
+
if (memo.ended || memo.parent !== parent)
|
|
15665
|
+
continue;
|
|
15666
|
+
if (!(memo.writing || memo.cut || memo.released && !opening))
|
|
15656
15667
|
continue;
|
|
15657
15668
|
if (opening && memo.held && !memo.cut) {
|
|
15658
15669
|
const sent = release(toolId, memo);
|
|
@@ -15661,7 +15672,7 @@ function abandoned(memos, parent, opening = false) {
|
|
|
15661
15672
|
continue;
|
|
15662
15673
|
}
|
|
15663
15674
|
memo.ended = true;
|
|
15664
|
-
out.push({ type: "tool-end", toolId });
|
|
15675
|
+
out.push({ type: "tool-end", toolId, cut: true });
|
|
15665
15676
|
}
|
|
15666
15677
|
return out;
|
|
15667
15678
|
}
|
|
@@ -15744,7 +15755,7 @@ function mapUpdate(update, memos, tag, sink, background) {
|
|
|
15744
15755
|
});
|
|
15745
15756
|
}
|
|
15746
15757
|
const refined = { ...memo.held };
|
|
15747
|
-
const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) :
|
|
15758
|
+
const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) : update.title;
|
|
15748
15759
|
if (title && title !== memo.title)
|
|
15749
15760
|
refined.title = memo.title = title;
|
|
15750
15761
|
if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
|
|
@@ -16000,7 +16011,9 @@ function pickOption(options, verdict) {
|
|
|
16000
16011
|
var SYSTEM_APPEND = [
|
|
16001
16012
|
"You are working inside a dedicated git worktree managed by Toyon.",
|
|
16002
16013
|
"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 `
|
|
16014
|
+
"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.",
|
|
16015
|
+
"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, as part of your reply): 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, `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`.",
|
|
16016
|
+
"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; 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
16017
|
"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
16018
|
"Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
|
|
16006
16019
|
"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.",
|
|
@@ -16009,6 +16022,7 @@ var SYSTEM_APPEND = [
|
|
|
16009
16022
|
"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
16023
|
"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
16024
|
"`@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.",
|
|
16025
|
+
"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.",
|
|
16012
16026
|
'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
16027
|
"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
16028
|
"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 +16160,85 @@ function procDiagnosis(p) {
|
|
|
16146
16160
|
return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
|
|
16147
16161
|
return "It never answered on that port.";
|
|
16148
16162
|
}
|
|
16149
|
-
function
|
|
16163
|
+
function piecesActedPrompt(pieces, outcomes) {
|
|
16164
|
+
const titled = (id) => `"${pieces.find((p) => p.id === id)?.title ?? id}"`;
|
|
16165
|
+
const list = (xs) => xs.join(", ");
|
|
16166
|
+
const started = outcomes.filter((o) => o.fate === "worktree" && !o.after && !o.error).map((o) => titled(o.pieceId));
|
|
16167
|
+
const queued = outcomes.filter((o) => o.fate === "worktree" && o.after && !o.error);
|
|
16168
|
+
const sent = outcomes.filter((o) => o.fate === "send" && !o.error);
|
|
16169
|
+
const here = outcomes.filter((o) => o.fate === "here").map((o) => titled(o.pieceId));
|
|
16170
|
+
const skipped = outcomes.filter((o) => o.fate === "skip").map((o) => titled(o.pieceId));
|
|
16171
|
+
const failed = outcomes.filter((o) => o.error).map((o) => `${titled(o.pieceId)} (${o.error})`);
|
|
16172
|
+
const parts = [];
|
|
16173
|
+
if (started.length)
|
|
16174
|
+
parts.push(`${list(started)} started in ${started.length === 1 ? "a sibling worktree" : "sibling worktrees"} of this project`);
|
|
16175
|
+
if (queued.length)
|
|
16176
|
+
parts.push(queued.map((o) => `${titled(o.pieceId)} waits for ${o.after} to land and then starts in its own worktree`).join("; "));
|
|
16177
|
+
if (sent.length)
|
|
16178
|
+
parts.push(sent.map((o) => `${titled(o.pieceId)} went to the open worktree "${o.title ?? "named"}"`).join("; "));
|
|
16179
|
+
if (skipped.length)
|
|
16180
|
+
parts.push(`${list(skipped)} ${skipped.length === 1 ? "was" : "were"} skipped by the person`);
|
|
16181
|
+
if (failed.length)
|
|
16182
|
+
parts.push(`${list(failed)} could not start`);
|
|
16183
|
+
const own = here.length ? `Do ${list(here)} here, and only that.` : "Nothing on the list is yours here.";
|
|
16184
|
+
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.`;
|
|
16185
|
+
}
|
|
16186
|
+
function handedPrompt(path, note, origin, via) {
|
|
16187
|
+
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.` : "";
|
|
16188
|
+
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}`;
|
|
16189
|
+
const where = path ? `Your plan is at ${path}.` : "Your plan is above.";
|
|
16190
|
+
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.`;
|
|
16191
|
+
}
|
|
16192
|
+
function pieceSendPrompt(text, note, origin) {
|
|
16150
16193
|
return [
|
|
16151
|
-
|
|
16194
|
+
text,
|
|
16152
16195
|
note ? `The person added, when they approved this: ${note}` : undefined,
|
|
16153
|
-
`This
|
|
16196
|
+
`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
16197
|
].filter((p) => !!p).join(`
|
|
16155
16198
|
|
|
16156
16199
|
`);
|
|
16157
16200
|
}
|
|
16158
|
-
function
|
|
16201
|
+
function pieceAskPrompt(text, project) {
|
|
16159
16202
|
const own = text?.trim();
|
|
16160
|
-
|
|
16203
|
+
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.";
|
|
16204
|
+
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.";
|
|
16205
|
+
}
|
|
16206
|
+
function pieceLandedContext(title, project, url, same, again) {
|
|
16207
|
+
const where = url ? ` (${url})` : "";
|
|
16208
|
+
if (again) {
|
|
16209
|
+
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.`;
|
|
16210
|
+
}
|
|
16211
|
+
if (same) {
|
|
16212
|
+
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.`;
|
|
16213
|
+
}
|
|
16214
|
+
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.`;
|
|
16161
16215
|
}
|
|
16162
|
-
function
|
|
16163
|
-
|
|
16216
|
+
function selfContext(w) {
|
|
16217
|
+
if (w.unnamed)
|
|
16218
|
+
return;
|
|
16219
|
+
return `This worktree is "${w.title}", on branch ${w.branch}.`;
|
|
16164
16220
|
}
|
|
16165
16221
|
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16222
|
+
var dayOf = (ts) => new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
16223
|
+
function archivedFate(a) {
|
|
16224
|
+
if (a.landed)
|
|
16225
|
+
return "after landing on main, so its work is in main's history";
|
|
16226
|
+
if (a.restorable)
|
|
16227
|
+
return "without landing; its commits are kept under an archive ref, not on any checkout";
|
|
16228
|
+
return "without landing, and git kept nothing of it";
|
|
16229
|
+
}
|
|
16166
16230
|
function worktreeContext(text, siblings) {
|
|
16167
16231
|
const out = [];
|
|
16232
|
+
const said = new Set;
|
|
16168
16233
|
for (const s of siblings) {
|
|
16234
|
+
if (said.has(s.branch))
|
|
16235
|
+
continue;
|
|
16169
16236
|
const named = new RegExp(`(^|\\s)@${escapeRegExp(s.branch)}(?=$|[\\s.,;:!?)\\]}'"])`);
|
|
16170
16237
|
if (!named.test(text))
|
|
16171
16238
|
continue;
|
|
16172
|
-
|
|
16239
|
+
said.add(s.branch);
|
|
16240
|
+
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.`;
|
|
16241
|
+
out.push(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}`);
|
|
16173
16242
|
}
|
|
16174
16243
|
return out;
|
|
16175
16244
|
}
|
|
@@ -16208,6 +16277,52 @@ function previewContext(p) {
|
|
|
16208
16277
|
}
|
|
16209
16278
|
return line;
|
|
16210
16279
|
}
|
|
16280
|
+
var STANDING_SUBJECTS = 3;
|
|
16281
|
+
var plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
16282
|
+
function standingContext(s) {
|
|
16283
|
+
if (!s)
|
|
16284
|
+
return;
|
|
16285
|
+
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"}.`;
|
|
16286
|
+
if (s.main) {
|
|
16287
|
+
const behind = s.behind ? ` It is ${plural(s.behind, "commit")} behind origin.` : "";
|
|
16288
|
+
return `This is the project's main checkout.${tree ? ` ${tree}` : ""}${behind}`;
|
|
16289
|
+
}
|
|
16290
|
+
const parts = [];
|
|
16291
|
+
if (s.landed) {
|
|
16292
|
+
const named = s.landed.subjects.slice(0, STANDING_SUBJECTS).map((t) => `"${t}"`);
|
|
16293
|
+
const more = s.landed.subjects.length - named.length;
|
|
16294
|
+
const as = named.length ? ` as ${named.join("; ")}${more > 0 ? ` (and ${more} more)` : ""}` : "";
|
|
16295
|
+
const through = s.landed.pr === undefined ? "" : `, through PR #${s.landed.pr}`;
|
|
16296
|
+
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.`);
|
|
16297
|
+
} else {
|
|
16298
|
+
if (tree)
|
|
16299
|
+
parts.push(tree);
|
|
16300
|
+
if (s.ahead !== undefined) {
|
|
16301
|
+
const behind = s.behind ? ` and ${s.behind} behind it` : "";
|
|
16302
|
+
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}.`);
|
|
16303
|
+
}
|
|
16304
|
+
}
|
|
16305
|
+
const pr = s.pr;
|
|
16306
|
+
if (pr && s.landed?.pr !== pr.number) {
|
|
16307
|
+
switch (pr.state) {
|
|
16308
|
+
case "open": {
|
|
16309
|
+
const checks = pr.checks ? `, checks ${{ pass: "passing", pending: "pending", fail: "failing" }[pr.checks]}` : "";
|
|
16310
|
+
const review = pr.review ? `, review ${{ approved: "approved", changes_requested: "changes requested", review_required: "required" }[pr.review]}` : "";
|
|
16311
|
+
const auto = pr.automerge ? ", auto-merge on" : "";
|
|
16312
|
+
const unpushed = s.unpushed ? `; ${plural(s.unpushed, "commit")} here ${s.unpushed === 1 ? "is" : "are"} not on it yet` : "";
|
|
16313
|
+
parts.push(`Pull request #${pr.number} is open at ${pr.url}${checks}${review}${auto}${unpushed}.`);
|
|
16314
|
+
break;
|
|
16315
|
+
}
|
|
16316
|
+
case "merged":
|
|
16317
|
+
parts.push(`PR #${pr.number} has merged on GitHub; Toyon lands it here on its next poll.`);
|
|
16318
|
+
break;
|
|
16319
|
+
case "closed":
|
|
16320
|
+
parts.push(`PR #${pr.number} was closed without merging; the next land press creates a new one.`);
|
|
16321
|
+
break;
|
|
16322
|
+
}
|
|
16323
|
+
}
|
|
16324
|
+
return parts.length ? parts.join(" ") : undefined;
|
|
16325
|
+
}
|
|
16211
16326
|
function imageCaption(ref, path) {
|
|
16212
16327
|
return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height}), saved at ${path}`;
|
|
16213
16328
|
}
|
|
@@ -16286,28 +16401,7 @@ import { join as join11 } from "path";
|
|
|
16286
16401
|
function transcriptPathFor(transcriptsDir, worktreeId) {
|
|
16287
16402
|
return join11(transcriptsDir, `${worktreeId}.jsonl`);
|
|
16288
16403
|
}
|
|
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"]);
|
|
16404
|
+
var NOT_THE_TURNS = new Set(["usage", "session-info", "user-message", "fix-asked", "handed"]);
|
|
16311
16405
|
function sealOpenTurn(entries) {
|
|
16312
16406
|
let reason = null;
|
|
16313
16407
|
let ts = 0;
|
|
@@ -16342,6 +16436,19 @@ function parseTranscript(text, tag) {
|
|
|
16342
16436
|
log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
|
|
16343
16437
|
return out;
|
|
16344
16438
|
}
|
|
16439
|
+
var lastSeq = (entries) => entries.at(-1)?.seq ?? -1;
|
|
16440
|
+
function after(entries, since) {
|
|
16441
|
+
let lo = 0;
|
|
16442
|
+
let hi = entries.length;
|
|
16443
|
+
while (lo < hi) {
|
|
16444
|
+
const mid = lo + hi >>> 1;
|
|
16445
|
+
if (entries[mid].seq <= since)
|
|
16446
|
+
lo = mid + 1;
|
|
16447
|
+
else
|
|
16448
|
+
hi = mid;
|
|
16449
|
+
}
|
|
16450
|
+
return entries.slice(lo);
|
|
16451
|
+
}
|
|
16345
16452
|
|
|
16346
16453
|
class Transcript {
|
|
16347
16454
|
path;
|
|
@@ -16656,7 +16763,10 @@ class AcpSession {
|
|
|
16656
16763
|
tasks;
|
|
16657
16764
|
asking = 0;
|
|
16658
16765
|
asks = new Map;
|
|
16659
|
-
|
|
16766
|
+
openCards = new Set;
|
|
16767
|
+
heldPieces = null;
|
|
16768
|
+
adopted = null;
|
|
16769
|
+
openPlanAsk = null;
|
|
16660
16770
|
log;
|
|
16661
16771
|
seq;
|
|
16662
16772
|
commandList = [];
|
|
@@ -16670,7 +16780,7 @@ class AcpSession {
|
|
|
16670
16780
|
...d.taskPollMs !== undefined ? { pollMs: d.taskPollMs } : {},
|
|
16671
16781
|
...d.claudeConfigDir !== undefined ? { configDir: d.claudeConfigDir } : {}
|
|
16672
16782
|
});
|
|
16673
|
-
this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" ? event.attachments : undefined));
|
|
16783
|
+
this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" || event.type === "handed" ? event.attachments : undefined));
|
|
16674
16784
|
const seal = sealOpenTurn(this.log.entries);
|
|
16675
16785
|
const open = new Set;
|
|
16676
16786
|
for (const { event } of this.log.entries) {
|
|
@@ -16697,18 +16807,18 @@ class AcpSession {
|
|
|
16697
16807
|
}
|
|
16698
16808
|
const going = new Set;
|
|
16699
16809
|
for (const { event } of this.log.entries) {
|
|
16700
|
-
this.
|
|
16701
|
-
if (event.type === "
|
|
16810
|
+
this.trackCards(event);
|
|
16811
|
+
if (event.type === "pieces-go")
|
|
16702
16812
|
going.add(event.id);
|
|
16703
|
-
else if (event.type === "
|
|
16813
|
+
else if (event.type === "pieces" || event.type === "pieces-declined")
|
|
16704
16814
|
going.delete(event.id);
|
|
16705
16815
|
}
|
|
16706
16816
|
for (const id of going) {
|
|
16707
16817
|
this.emit({
|
|
16708
|
-
type: "
|
|
16818
|
+
type: "pieces-declined",
|
|
16709
16819
|
id,
|
|
16710
16820
|
outcome: "failed",
|
|
16711
|
-
reason: "Toyon stopped while the
|
|
16821
|
+
reason: "Toyon stopped while the worktrees were being made",
|
|
16712
16822
|
ts: Date.now()
|
|
16713
16823
|
});
|
|
16714
16824
|
}
|
|
@@ -16716,16 +16826,77 @@ class AcpSession {
|
|
|
16716
16826
|
log.info(d.worktreeId, `sealed a turn left open: ${seal.stopReason}`);
|
|
16717
16827
|
this.emit(seal);
|
|
16718
16828
|
}
|
|
16719
|
-
|
|
16829
|
+
this.showHeld();
|
|
16830
|
+
if (this.openCards.size > 0)
|
|
16720
16831
|
this.syncStatus();
|
|
16721
|
-
|
|
16722
|
-
|
|
16723
|
-
|
|
16724
|
-
|
|
16725
|
-
if (event.type === "
|
|
16726
|
-
|
|
16832
|
+
this.restored = true;
|
|
16833
|
+
}
|
|
16834
|
+
restored = false;
|
|
16835
|
+
trackCards(event) {
|
|
16836
|
+
if (event.type === "pieces-proposed") {
|
|
16837
|
+
if (event.held) {
|
|
16838
|
+
this.heldPieces = event.id;
|
|
16839
|
+
if (this.openPlanAsk)
|
|
16840
|
+
this.adopted = { ask: this.openPlanAsk, pieces: event.id };
|
|
16841
|
+
return false;
|
|
16842
|
+
}
|
|
16843
|
+
return this.openCards.add(event.id).size > 0;
|
|
16844
|
+
}
|
|
16845
|
+
if (event.type === "pieces-shown") {
|
|
16846
|
+
if (this.heldPieces === event.id)
|
|
16847
|
+
this.heldPieces = null;
|
|
16848
|
+
return this.openCards.add(event.id).size > 0;
|
|
16849
|
+
}
|
|
16850
|
+
if (event.type === "agent-permission") {
|
|
16851
|
+
if (event.plan)
|
|
16852
|
+
this.openPlanAsk = event.id;
|
|
16853
|
+
if (event.pieces && this.heldPieces === event.pieces) {
|
|
16854
|
+
this.heldPieces = null;
|
|
16855
|
+
this.adopted = { ask: event.id, pieces: event.pieces };
|
|
16856
|
+
}
|
|
16857
|
+
return false;
|
|
16858
|
+
}
|
|
16859
|
+
if (event.type === "agent-ask-end") {
|
|
16860
|
+
if (this.openPlanAsk === event.id)
|
|
16861
|
+
this.openPlanAsk = null;
|
|
16862
|
+
if (this.adopted?.ask === event.id) {
|
|
16863
|
+
const pieces = this.adopted.pieces;
|
|
16864
|
+
this.adopted = null;
|
|
16865
|
+
if (event.outcome !== "answered") {
|
|
16866
|
+
const list = this.log.entries.findLast(({ event: e }) => e.type === "pieces-proposed" && e.id === pieces)?.event;
|
|
16867
|
+
if (list?.type === "pieces-proposed" && list.by === "toyon") {
|
|
16868
|
+
this.note({
|
|
16869
|
+
type: "pieces-declined",
|
|
16870
|
+
id: pieces,
|
|
16871
|
+
reason: "the plan card closed before it was answered",
|
|
16872
|
+
ts: Date.now()
|
|
16873
|
+
});
|
|
16874
|
+
} else
|
|
16875
|
+
this.heldPieces = pieces;
|
|
16876
|
+
}
|
|
16877
|
+
}
|
|
16878
|
+
return false;
|
|
16879
|
+
}
|
|
16880
|
+
if (event.type === "pieces-go" || event.type === "pieces" || event.type === "pieces-declined") {
|
|
16881
|
+
if (this.heldPieces === event.id)
|
|
16882
|
+
this.heldPieces = null;
|
|
16883
|
+
if (this.adopted?.pieces === event.id)
|
|
16884
|
+
this.adopted = null;
|
|
16885
|
+
return this.openCards.delete(event.id);
|
|
16886
|
+
}
|
|
16727
16887
|
return false;
|
|
16728
16888
|
}
|
|
16889
|
+
showHeld() {
|
|
16890
|
+
const id = this.heldPieces;
|
|
16891
|
+
if (!id || this.mode() === "plan")
|
|
16892
|
+
return;
|
|
16893
|
+
this.heldPieces = null;
|
|
16894
|
+
this.openCards.add(id);
|
|
16895
|
+
const proposed = this.log.entries.findLast(({ event }) => event.type === "pieces-proposed" && event.id === id)?.event;
|
|
16896
|
+
const pieces = proposed?.type === "pieces-proposed" ? proposed.pieces : [];
|
|
16897
|
+
const plan = this.d.onPiecesShown?.(id, pieces);
|
|
16898
|
+
this.emit({ type: "pieces-shown", id, ...typeof plan === "string" ? { plan } : {}, ts: Date.now() });
|
|
16899
|
+
}
|
|
16729
16900
|
get commands() {
|
|
16730
16901
|
return this.commandList;
|
|
16731
16902
|
}
|
|
@@ -16791,7 +16962,7 @@ class AcpSession {
|
|
|
16791
16962
|
return this.waiting().map(queued);
|
|
16792
16963
|
}
|
|
16793
16964
|
waiting() {
|
|
16794
|
-
return this.queue.filter((q) => !q.recorded);
|
|
16965
|
+
return this.queue.filter((q) => !q.recorded && !q.handed);
|
|
16795
16966
|
}
|
|
16796
16967
|
onQueueChange = null;
|
|
16797
16968
|
queueChanged() {
|
|
@@ -16815,12 +16986,12 @@ class AcpSession {
|
|
|
16815
16986
|
}
|
|
16816
16987
|
note(event) {
|
|
16817
16988
|
this.emit(event);
|
|
16818
|
-
if (this.trackHandoff(event))
|
|
16819
|
-
this.syncStatus();
|
|
16820
16989
|
}
|
|
16821
16990
|
emit(event) {
|
|
16822
16991
|
const entry = this.log.append(event);
|
|
16823
16992
|
this.d.onEvent(event, entry.seq);
|
|
16993
|
+
if (this.restored && this.trackCards(event))
|
|
16994
|
+
this.syncStatus();
|
|
16824
16995
|
}
|
|
16825
16996
|
setStatus(s) {
|
|
16826
16997
|
this.status = s;
|
|
@@ -16842,9 +17013,10 @@ class AcpSession {
|
|
|
16842
17013
|
send(text, opts = {}) {
|
|
16843
17014
|
if (this.stopped)
|
|
16844
17015
|
return log.warn(this.d.worktreeId, "send after close dropped");
|
|
16845
|
-
if (!opts.asked)
|
|
17016
|
+
if (!opts.asked && !opts.keepCards)
|
|
16846
17017
|
for (const ask of [...this.asks.values()])
|
|
16847
17018
|
ask.settle("skipped");
|
|
17019
|
+
this.showHeld();
|
|
16848
17020
|
this.post(text, opts);
|
|
16849
17021
|
}
|
|
16850
17022
|
post(text, opts = {}) {
|
|
@@ -16853,12 +17025,13 @@ class AcpSession {
|
|
|
16853
17025
|
fireAndForget(this.d.worktreeId, steer, "agent steer");
|
|
16854
17026
|
}
|
|
16855
17027
|
deliver(text, opts = {}) {
|
|
16856
|
-
const { context, attachments, asked } = opts;
|
|
17028
|
+
const { context, attachments, asked, handed } = opts;
|
|
16857
17029
|
const item = {
|
|
16858
17030
|
text,
|
|
16859
17031
|
context,
|
|
16860
17032
|
...attachments?.length ? { attachments } : {},
|
|
16861
|
-
...asked ? { asked: asked.kind } : {}
|
|
17033
|
+
...asked ? { asked: asked.kind } : {},
|
|
17034
|
+
...handed ? { handed } : {}
|
|
16862
17035
|
};
|
|
16863
17036
|
if (asked) {
|
|
16864
17037
|
this.emit({ type: "fix-asked", text, ts: Date.now(), ...asked });
|
|
@@ -17002,7 +17175,7 @@ class AcpSession {
|
|
|
17002
17175
|
q.handedBack = false;
|
|
17003
17176
|
this.setStopping(false);
|
|
17004
17177
|
}
|
|
17005
|
-
this.setStatus(this.
|
|
17178
|
+
this.setStatus(this.openCards.size > 0 ? "waiting" : "idle");
|
|
17006
17179
|
} catch (e) {
|
|
17007
17180
|
const aws = this.conn && AWS_EXPIRED_RE.test(this.describe(e)) ? this.awsAuth() : null;
|
|
17008
17181
|
if (this.conn && (aws || isAuthRequired(e) || this.rejectedCredential(e))) {
|
|
@@ -17046,6 +17219,7 @@ class AcpSession {
|
|
|
17046
17219
|
return;
|
|
17047
17220
|
turn.open = false;
|
|
17048
17221
|
this.emit({ type: "turn-end", stopReason, ts: Date.now() });
|
|
17222
|
+
this.showHeld();
|
|
17049
17223
|
this.setStopping(false);
|
|
17050
17224
|
}
|
|
17051
17225
|
async ask(system, prompt, opts = {}) {
|
|
@@ -17157,7 +17331,7 @@ class AcpSession {
|
|
|
17157
17331
|
const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
|
|
17158
17332
|
return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
|
|
17159
17333
|
}
|
|
17160
|
-
async record({ text, attachments }) {
|
|
17334
|
+
async record({ text, attachments, handed }) {
|
|
17161
17335
|
const stored = [];
|
|
17162
17336
|
try {
|
|
17163
17337
|
for (const a of attachments ?? [])
|
|
@@ -17165,12 +17339,11 @@ class AcpSession {
|
|
|
17165
17339
|
} finally {
|
|
17166
17340
|
this.d.attachments.uploads.release(uploadIds(attachments));
|
|
17167
17341
|
}
|
|
17168
|
-
|
|
17169
|
-
|
|
17170
|
-
text,
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
});
|
|
17342
|
+
const attached = stored.length ? { attachments: stored.map((s) => s.ref) } : {};
|
|
17343
|
+
if (handed)
|
|
17344
|
+
this.emit({ type: "handed", ...handed, text, ...attached, ts: Date.now() });
|
|
17345
|
+
else
|
|
17346
|
+
this.emit({ type: "user-message", text, ts: Date.now(), ...attached });
|
|
17174
17347
|
return { attachments: stored };
|
|
17175
17348
|
}
|
|
17176
17349
|
carried(live, stored) {
|
|
@@ -17202,17 +17375,20 @@ class AcpSession {
|
|
|
17202
17375
|
return;
|
|
17203
17376
|
}
|
|
17204
17377
|
const carried = this.carried(live, item.recorded.attachments);
|
|
17378
|
+
const standing = await this.d.standing?.();
|
|
17205
17379
|
const prefix = live.prefixPending ? SYSTEM_APPEND : undefined;
|
|
17206
17380
|
live.prefixPending = false;
|
|
17207
17381
|
const res = await live.conn.ctx.request(methods.agent.session.prompt, {
|
|
17208
17382
|
sessionId: live.sessionId,
|
|
17209
|
-
prompt: buildPrompt(item.text, this.contextFor(item), prefix, carried)
|
|
17383
|
+
prompt: buildPrompt(item.text, this.contextFor(item, standing), prefix, carried)
|
|
17210
17384
|
});
|
|
17211
17385
|
this.endTurn(mapStopReason(res.stopReason));
|
|
17212
17386
|
}
|
|
17213
|
-
contextFor(item) {
|
|
17387
|
+
contextFor(item, standing) {
|
|
17214
17388
|
return ambientBlock([
|
|
17215
17389
|
...item.context ?? [],
|
|
17390
|
+
this.d.self?.(),
|
|
17391
|
+
standing,
|
|
17216
17392
|
...this.d.mentioned?.(item.text) ?? [],
|
|
17217
17393
|
this.d.preview?.(),
|
|
17218
17394
|
this.d.pageErrors?.(),
|
|
@@ -17221,7 +17397,7 @@ class AcpSession {
|
|
|
17221
17397
|
}
|
|
17222
17398
|
landedSince() {
|
|
17223
17399
|
const entries = this.log.entries;
|
|
17224
|
-
const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked";
|
|
17400
|
+
const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked" || e.type === "handed";
|
|
17225
17401
|
let i = entries.length - 1;
|
|
17226
17402
|
while (i >= 0 && !isMessage(entries[i].event))
|
|
17227
17403
|
i--;
|
|
@@ -17230,8 +17406,8 @@ class AcpSession {
|
|
|
17230
17406
|
const e = entries[i].event;
|
|
17231
17407
|
if (isMessage(e))
|
|
17232
17408
|
break;
|
|
17233
|
-
if (e.type === "
|
|
17234
|
-
out.unshift(
|
|
17409
|
+
if (e.type === "piece-landed")
|
|
17410
|
+
out.unshift(pieceLandedContext(e.title, e.repoName, e.url, e.same, e.again));
|
|
17235
17411
|
}
|
|
17236
17412
|
return out;
|
|
17237
17413
|
}
|
|
@@ -17358,7 +17534,7 @@ class AcpSession {
|
|
|
17358
17534
|
}
|
|
17359
17535
|
const options = readOptions(configOptions);
|
|
17360
17536
|
this.emit({ type: "session-info", sessionId, ...currentValues(options) });
|
|
17361
|
-
this.learn(options);
|
|
17537
|
+
this.learn(options, !resumed);
|
|
17362
17538
|
const modeOption = modes ? null : readModeOption(configOptions);
|
|
17363
17539
|
const live = {
|
|
17364
17540
|
conn,
|
|
@@ -17426,9 +17602,10 @@ class AcpSession {
|
|
|
17426
17602
|
live.modeId = modes.current;
|
|
17427
17603
|
}
|
|
17428
17604
|
}
|
|
17429
|
-
learn(options) {
|
|
17430
|
-
for (const [category, opt] of options)
|
|
17431
|
-
this.d.onOptionsLearned?.(category, opt.choices);
|
|
17605
|
+
learn(options, fresh = false) {
|
|
17606
|
+
for (const [category, opt] of options) {
|
|
17607
|
+
this.d.onOptionsLearned?.(category, opt.choices, fresh ? opt.current : undefined);
|
|
17608
|
+
}
|
|
17432
17609
|
}
|
|
17433
17610
|
onUpdate(params, side, commands) {
|
|
17434
17611
|
if (params.update.sessionUpdate === "available_commands_update") {
|
|
@@ -17542,6 +17719,9 @@ class AcpSession {
|
|
|
17542
17719
|
const planPath = plan ? await this.d.onPlan?.(detail, ownFile) ?? null : null;
|
|
17543
17720
|
const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
|
|
17544
17721
|
const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
|
|
17722
|
+
const pieces = plan ? this.heldPieces : null;
|
|
17723
|
+
if (plan && planPath && !pieces && detail.trim())
|
|
17724
|
+
this.d.onPlanUnsplit?.(detail, planPath);
|
|
17545
17725
|
return this.openAsk((id) => ({
|
|
17546
17726
|
type: "agent-permission",
|
|
17547
17727
|
id,
|
|
@@ -17549,6 +17729,7 @@ class AcpSession {
|
|
|
17549
17729
|
...parts.command ? { command: parts.command } : {},
|
|
17550
17730
|
...detail ? { detail } : {},
|
|
17551
17731
|
...planPath ? { plan: planPath } : {},
|
|
17732
|
+
...pieces ? { pieces } : {},
|
|
17552
17733
|
choices,
|
|
17553
17734
|
...params.toolCall.toolCallId ? { toolId: params.toolCall.toolCallId } : {},
|
|
17554
17735
|
ts: Date.now()
|
|
@@ -17558,11 +17739,17 @@ class AcpSession {
|
|
|
17558
17739
|
if (!choice)
|
|
17559
17740
|
return { outcome: { outcome: "cancelled" } };
|
|
17560
17741
|
const res = { outcome: { outcome: "selected", optionId: choice.id } };
|
|
17561
|
-
|
|
17742
|
+
const approved = choice.kind === "allow_once" || choice.kind === "allow_always";
|
|
17743
|
+
if (plan && approved)
|
|
17562
17744
|
this.d.setMode?.(modeAfterPlan(choice.name));
|
|
17563
|
-
|
|
17564
|
-
|
|
17565
|
-
|
|
17745
|
+
const decided = pieces ?? (plan ? this.heldPieces : null);
|
|
17746
|
+
if (decided) {
|
|
17747
|
+
if (this.heldPieces === decided)
|
|
17748
|
+
this.heldPieces = null;
|
|
17749
|
+
this.d.onPieces?.(decided, approved);
|
|
17750
|
+
}
|
|
17751
|
+
if (plan && approved && planPath)
|
|
17752
|
+
return this.sayPlanEdited(detail, planPath).then(() => res);
|
|
17566
17753
|
return res;
|
|
17567
17754
|
});
|
|
17568
17755
|
}
|
|
@@ -17648,7 +17835,7 @@ class AcpSession {
|
|
|
17648
17835
|
syncStatus() {
|
|
17649
17836
|
if (this.status === "error")
|
|
17650
17837
|
return;
|
|
17651
|
-
const waiting = this.asks.size > 0 || this.
|
|
17838
|
+
const waiting = this.asks.size > 0 || this.openCards.size > 0;
|
|
17652
17839
|
this.setStatus(waiting ? "waiting" : this.running || this.own ? "working" : "idle");
|
|
17653
17840
|
}
|
|
17654
17841
|
trackOwn(events) {
|
|
@@ -19001,12 +19188,27 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
19001
19188
|
worktreeId: wt.id,
|
|
19002
19189
|
cwd: wt.path,
|
|
19003
19190
|
preview: () => previewContext(preview()),
|
|
19191
|
+
self: () => {
|
|
19192
|
+
const w = d.state.worktrees.find((w) => w.id === wt.id);
|
|
19193
|
+
return w ? selfContext(w) : undefined;
|
|
19194
|
+
},
|
|
19195
|
+
standing: async () => standingContext(await d.standing?.(wt.id) ?? null),
|
|
19004
19196
|
pageErrors: () => d.pageErrors?.(wt.id),
|
|
19005
|
-
mentioned: (text) =>
|
|
19006
|
-
|
|
19007
|
-
|
|
19008
|
-
|
|
19009
|
-
|
|
19197
|
+
mentioned: (text) => !text.includes("@") ? [] : worktreeContext(text, [
|
|
19198
|
+
...d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree").map((w) => ({
|
|
19199
|
+
branch: w.branch,
|
|
19200
|
+
title: w.title,
|
|
19201
|
+
path: w.path,
|
|
19202
|
+
transcript: transcriptPathFor(d.paths.transcriptsDir, w.id)
|
|
19203
|
+
})),
|
|
19204
|
+
...(d.archived?.(wt.repoId) ?? []).map((a) => ({
|
|
19205
|
+
branch: a.branch,
|
|
19206
|
+
title: a.title,
|
|
19207
|
+
path: a.path,
|
|
19208
|
+
transcript: a.transcript,
|
|
19209
|
+
archived: { at: a.archivedAt, landed: a.landed, restorable: a.restorable }
|
|
19210
|
+
}))
|
|
19211
|
+
]),
|
|
19010
19212
|
onProcess,
|
|
19011
19213
|
spec,
|
|
19012
19214
|
connect: (app, spec, prepared) => spawnAcp(app, d.agents.launch(spec, prepared), wt.path, wt.id),
|
|
@@ -19030,6 +19232,9 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
19030
19232
|
if (d.state.setMode(wt.id, mode))
|
|
19031
19233
|
d.hub.emit("worktreesChanged");
|
|
19032
19234
|
},
|
|
19235
|
+
onPieces: (id, approved) => d.hub.emit("piecesDecided", wt.id, id, approved),
|
|
19236
|
+
onPlanUnsplit: (markdown, planPath) => d.hub.emit("planUnsplit", wt.id, markdown, planPath),
|
|
19237
|
+
onPiecesShown: (_id, pieces) => writePlanDocNow(wt.path, listDoc(pieces)),
|
|
19033
19238
|
onPlan: async (markdown, ownFile) => {
|
|
19034
19239
|
const w = d.state.worktree(wt.id);
|
|
19035
19240
|
if (w && !w.planned) {
|
|
@@ -19048,9 +19253,9 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
19048
19253
|
},
|
|
19049
19254
|
planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
|
|
19050
19255
|
option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
|
|
19051
|
-
onOptionsLearned: (category, choices) => {
|
|
19256
|
+
onOptionsLearned: (category, choices, runs) => {
|
|
19052
19257
|
const agentId = d.state.requireWorktree(wt.id).agent ?? "";
|
|
19053
|
-
if (d.state.setCachedOptions(agentId, category, choices))
|
|
19258
|
+
if (d.state.setCachedOptions(agentId, category, choices, runs))
|
|
19054
19259
|
d.hub.emit("agentsChanged");
|
|
19055
19260
|
},
|
|
19056
19261
|
onOptionChanged: (category, value) => {
|
|
@@ -19877,9 +20082,19 @@ function freeSlot(parent) {
|
|
|
19877
20082
|
return { id, dir };
|
|
19878
20083
|
}
|
|
19879
20084
|
}
|
|
20085
|
+
var NAME_WORDS_MAX = 3;
|
|
20086
|
+
var NAME_CHARS_MAX = 20;
|
|
20087
|
+
var NAME_SHAPE = `1 to ${NAME_WORDS_MAX} plain words, ${NAME_CHARS_MAX} characters at most, like "sticky header"`;
|
|
20088
|
+
function overNameShape(title) {
|
|
20089
|
+
const words = title.split(/\s+/).filter(Boolean).length;
|
|
20090
|
+
const over = words > NAME_WORDS_MAX ? `${words} words` : title.length > NAME_CHARS_MAX ? `${title.length} characters` : null;
|
|
20091
|
+
return over && `runs to ${over}; a title is ${NAME_SHAPE}.`;
|
|
20092
|
+
}
|
|
19880
20093
|
function titleFrom(prompt) {
|
|
19881
|
-
const words = prompt.split(/\s+/).filter(Boolean).slice(0,
|
|
19882
|
-
|
|
20094
|
+
const words = prompt.split(/\s+/).filter(Boolean).slice(0, NAME_WORDS_MAX);
|
|
20095
|
+
while (words.length > 1 && words.join(" ").length > NAME_CHARS_MAX)
|
|
20096
|
+
words.pop();
|
|
20097
|
+
return sentenceCase(cleanTitle2(words.join(" ").slice(0, NAME_CHARS_MAX)) || "task");
|
|
19883
20098
|
}
|
|
19884
20099
|
function cleanTitle2(title) {
|
|
19885
20100
|
return title.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").slice(0, 40).replace(/^[\s:;,.!?]+|[\s:;,.!?]+$/g, "");
|
|
@@ -19890,15 +20105,6 @@ function branchSlug(title) {
|
|
|
19890
20105
|
function sentenceCase(title) {
|
|
19891
20106
|
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
19892
20107
|
}
|
|
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
20108
|
|
|
19903
20109
|
// ../daemon/src/agent/recap.ts
|
|
19904
20110
|
var DAEMON_STOPS = new Set(["interrupted", "failed", "auth", "lost"]);
|
|
@@ -19939,6 +20145,11 @@ function turnsSince(entries, seenAt) {
|
|
|
19939
20145
|
(into?.fixes ?? pendingFixes).push(e.why);
|
|
19940
20146
|
break;
|
|
19941
20147
|
}
|
|
20148
|
+
case "handed": {
|
|
20149
|
+
const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
|
|
20150
|
+
(into?.asks ?? pending).push(e.body ?? e.text);
|
|
20151
|
+
break;
|
|
20152
|
+
}
|
|
19942
20153
|
case "turn-start":
|
|
19943
20154
|
open = start(e.ts);
|
|
19944
20155
|
break;
|
|
@@ -20014,10 +20225,6 @@ function openAskOf(entries) {
|
|
|
20014
20225
|
open.set(e.id, e.title);
|
|
20015
20226
|
else if (e.type === "agent-ask-end")
|
|
20016
20227
|
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
20228
|
}
|
|
20022
20229
|
const last = [...open.values()].at(-1)?.trim();
|
|
20023
20230
|
return last ? clip(last) : undefined;
|
|
@@ -20037,9 +20244,15 @@ function factsOf(turns, end, ask) {
|
|
|
20037
20244
|
};
|
|
20038
20245
|
}
|
|
20039
20246
|
function firstAskOf(entries) {
|
|
20040
|
-
for (const { event: e } of entries)
|
|
20247
|
+
for (const { event: e } of entries) {
|
|
20041
20248
|
if (e.type === "user-message" && e.text.trim())
|
|
20042
20249
|
return e.text;
|
|
20250
|
+
if (e.type === "handed") {
|
|
20251
|
+
const said = e.body ?? e.text;
|
|
20252
|
+
if (said.trim())
|
|
20253
|
+
return said;
|
|
20254
|
+
}
|
|
20255
|
+
}
|
|
20043
20256
|
return;
|
|
20044
20257
|
}
|
|
20045
20258
|
var RECAP_MAX = 160;
|
|
@@ -20195,49 +20408,6 @@ function plain(line) {
|
|
|
20195
20408
|
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
20409
|
}
|
|
20197
20410
|
|
|
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
20411
|
// ../daemon/src/agent/tasks.ts
|
|
20242
20412
|
var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
|
|
20243
20413
|
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 +20435,34 @@ function parseName(text) {
|
|
|
20265
20435
|
const line = text.trim().split(`
|
|
20266
20436
|
`).at(-1) ?? "";
|
|
20267
20437
|
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)
|
|
20438
|
+
if (name.length < 3 || overNameShape(name))
|
|
20270
20439
|
return null;
|
|
20271
20440
|
if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
|
|
20272
20441
|
return null;
|
|
20273
20442
|
return sentenceCase(name);
|
|
20274
20443
|
}
|
|
20275
|
-
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20444
|
+
function makeNamer(runtime) {
|
|
20445
|
+
return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
|
|
20446
|
+
}
|
|
20447
|
+
function lacksQuickModel(spec, state) {
|
|
20448
|
+
const quick = spec.quickModel;
|
|
20449
|
+
if (!quick)
|
|
20450
|
+
return true;
|
|
20451
|
+
if (typeof quick === "function")
|
|
20452
|
+
return false;
|
|
20453
|
+
const offered = state.cachedOptions(spec.id, "model");
|
|
20454
|
+
return offered.length > 0 && !offered.some((m) => m.id === quick);
|
|
20455
|
+
}
|
|
20456
|
+
var PIECES_SYSTEM = "You read plans and reply with only the requested JSON.";
|
|
20457
|
+
var piecesPrompt = (markdown) => [
|
|
20458
|
+
"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.",
|
|
20459
|
+
`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}.`,
|
|
20460
|
+
"Reply with an empty array when the plan is one piece of work with no part set apart.",
|
|
20461
|
+
`Plan:
|
|
20462
|
+
${markdown.slice(0, 12000)}`
|
|
20281
20463
|
].join(`
|
|
20282
20464
|
`);
|
|
20283
|
-
function
|
|
20465
|
+
function parsePieces(text) {
|
|
20284
20466
|
if (!text)
|
|
20285
20467
|
return null;
|
|
20286
20468
|
const start = text.indexOf("[");
|
|
@@ -20291,23 +20473,31 @@ function parsePlan(text) {
|
|
|
20291
20473
|
const arr = JSON.parse(text.slice(start, end + 1));
|
|
20292
20474
|
if (!Array.isArray(arr))
|
|
20293
20475
|
return null;
|
|
20294
|
-
const
|
|
20295
|
-
|
|
20476
|
+
const out = [];
|
|
20477
|
+
for (const x of arr) {
|
|
20478
|
+
if (!x || typeof x !== "object")
|
|
20479
|
+
continue;
|
|
20480
|
+
const { title, section, plan, where, after } = x;
|
|
20481
|
+
if (typeof title !== "string" || !title.trim() || typeof section !== "string" || !section.trim())
|
|
20482
|
+
continue;
|
|
20483
|
+
if (where !== "here" && where !== "worktree")
|
|
20484
|
+
continue;
|
|
20485
|
+
out.push({
|
|
20486
|
+
title: title.trim(),
|
|
20487
|
+
section: section.trim(),
|
|
20488
|
+
...typeof plan === "string" && plan.trim() ? { plan: plan.trim() } : {},
|
|
20489
|
+
where,
|
|
20490
|
+
...typeof after === "string" && after.trim() ? { after: after.trim() } : {}
|
|
20491
|
+
});
|
|
20492
|
+
}
|
|
20493
|
+
return out.slice(0, 6);
|
|
20296
20494
|
} catch {
|
|
20297
20495
|
return null;
|
|
20298
20496
|
}
|
|
20299
20497
|
}
|
|
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);
|
|
20498
|
+
function makePiecesReader(...deps) {
|
|
20499
|
+
const ask = makeQuickAsker(...deps);
|
|
20500
|
+
return async (wt, markdown) => parsePieces(await ask(wt, PIECES_SYSTEM, piecesPrompt(markdown)));
|
|
20311
20501
|
}
|
|
20312
20502
|
function makeQuickAsker(runtime, agents, state) {
|
|
20313
20503
|
return async (wt, system, prompt) => {
|
|
@@ -20326,17 +20516,14 @@ function makeAnswerRecapper(...deps) {
|
|
|
20326
20516
|
const ask = makeQuickAsker(...deps);
|
|
20327
20517
|
return async (wt, prompt) => parseRecap(await ask(wt, ANSWER_SYSTEM, prompt));
|
|
20328
20518
|
}
|
|
20329
|
-
function makePlanner(agents) {
|
|
20330
|
-
return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
|
|
20331
|
-
}
|
|
20332
20519
|
|
|
20333
20520
|
// ../daemon/src/agent/transcriptForward.ts
|
|
20334
20521
|
import { existsSync as existsSync15, readdirSync as readdirSync5, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
20335
20522
|
import { join as join13 } from "path";
|
|
20336
|
-
var FORWARD_STEP = "
|
|
20337
|
-
var
|
|
20523
|
+
var FORWARD_STEP = "3";
|
|
20524
|
+
var MARKS = ['"page', '"brief"', '"handoff'];
|
|
20338
20525
|
function forwardLine(line) {
|
|
20339
|
-
if (!line.includes(
|
|
20526
|
+
if (!MARKS.some((m) => line.includes(m)))
|
|
20340
20527
|
return null;
|
|
20341
20528
|
let entry;
|
|
20342
20529
|
try {
|
|
@@ -20349,6 +20536,73 @@ function forwardLine(line) {
|
|
|
20349
20536
|
return { drop: true };
|
|
20350
20537
|
if (e?.type === "fix-asked" && e.kind === "page")
|
|
20351
20538
|
return { line: JSON.stringify({ ...entry, event: { ...e, kind: "command" } }) };
|
|
20539
|
+
if (e?.type === "user-message" && e.brief && typeof e.brief === "object") {
|
|
20540
|
+
const brief = e.brief;
|
|
20541
|
+
const { brief: _brief, ...rest } = e;
|
|
20542
|
+
return {
|
|
20543
|
+
line: JSON.stringify({
|
|
20544
|
+
...entry,
|
|
20545
|
+
event: {
|
|
20546
|
+
...rest,
|
|
20547
|
+
type: "handed",
|
|
20548
|
+
via: "message",
|
|
20549
|
+
body: brief.text,
|
|
20550
|
+
origin: brief.origin,
|
|
20551
|
+
...brief.note ? { note: brief.note } : {}
|
|
20552
|
+
}
|
|
20553
|
+
})
|
|
20554
|
+
};
|
|
20555
|
+
}
|
|
20556
|
+
if (e?.type === "handoff-proposed") {
|
|
20557
|
+
const repo = e.repo ?? {};
|
|
20558
|
+
return {
|
|
20559
|
+
line: JSON.stringify({
|
|
20560
|
+
...entry,
|
|
20561
|
+
event: {
|
|
20562
|
+
type: "pieces-proposed",
|
|
20563
|
+
id: e.id,
|
|
20564
|
+
pieces: [
|
|
20565
|
+
{
|
|
20566
|
+
id: "p1",
|
|
20567
|
+
title: e.title ?? "Continue",
|
|
20568
|
+
plan: e.message ?? "",
|
|
20569
|
+
attachments: [],
|
|
20570
|
+
fate: "worktree",
|
|
20571
|
+
project: { id: repo.id, name: repo.name }
|
|
20572
|
+
}
|
|
20573
|
+
],
|
|
20574
|
+
mode: e.mode,
|
|
20575
|
+
ts: e.ts
|
|
20576
|
+
}
|
|
20577
|
+
})
|
|
20578
|
+
};
|
|
20579
|
+
}
|
|
20580
|
+
if (e?.type === "handoff-landed")
|
|
20581
|
+
return { line: JSON.stringify({ ...entry, event: { ...e, type: "piece-landed" } }) };
|
|
20582
|
+
if (e?.type === "pieces-proposed" && Array.isArray(e.pieces) && e.pieces.some((p) => p && typeof p === "object" && ("brief" in p))) {
|
|
20583
|
+
const pieces = e.pieces.map(({ brief, ...rest }) => brief === undefined ? rest : { ...rest, plan: brief });
|
|
20584
|
+
return { line: JSON.stringify({ ...entry, event: { ...e, pieces } }) };
|
|
20585
|
+
}
|
|
20586
|
+
if (e?.type === "handoff-go")
|
|
20587
|
+
return { line: JSON.stringify({ ...entry, event: { ...e, type: "pieces-go" } }) };
|
|
20588
|
+
if (e?.type === "handoff") {
|
|
20589
|
+
return {
|
|
20590
|
+
line: JSON.stringify({
|
|
20591
|
+
...entry,
|
|
20592
|
+
event: {
|
|
20593
|
+
type: "pieces",
|
|
20594
|
+
id: e.id,
|
|
20595
|
+
outcomes: [
|
|
20596
|
+
{ pieceId: "p1", fate: "worktree", worktreeId: e.worktreeId, title: e.title, repoName: e.repoName }
|
|
20597
|
+
],
|
|
20598
|
+
ts: e.ts
|
|
20599
|
+
}
|
|
20600
|
+
})
|
|
20601
|
+
};
|
|
20602
|
+
}
|
|
20603
|
+
if (e?.type === "handoff-declined") {
|
|
20604
|
+
return { line: JSON.stringify({ ...entry, event: { ...e, type: "pieces-declined" } }) };
|
|
20605
|
+
}
|
|
20352
20606
|
return null;
|
|
20353
20607
|
}
|
|
20354
20608
|
function forwardText(text) {
|
|
@@ -21576,6 +21830,15 @@ function loadState(paths) {
|
|
|
21576
21830
|
log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
|
|
21577
21831
|
return structuredClone(empty);
|
|
21578
21832
|
}
|
|
21833
|
+
for (const w of state.worktrees) {
|
|
21834
|
+
for (const q of w.queued ?? []) {
|
|
21835
|
+
const old = q;
|
|
21836
|
+
if (old.brief !== undefined && old.plan === undefined) {
|
|
21837
|
+
old.plan = old.brief;
|
|
21838
|
+
delete old.brief;
|
|
21839
|
+
}
|
|
21840
|
+
}
|
|
21841
|
+
}
|
|
21579
21842
|
const ids = new Set(state.worktrees.map((w) => w.id));
|
|
21580
21843
|
for (const id of Object.keys(state.sessions))
|
|
21581
21844
|
if (!ids.has(id))
|
|
@@ -21786,12 +22049,22 @@ class StateStore {
|
|
|
21786
22049
|
cachedOptions(agentId, category) {
|
|
21787
22050
|
return this.state.optionCache?.[agentId]?.[category] ?? [];
|
|
21788
22051
|
}
|
|
21789
|
-
|
|
21790
|
-
|
|
22052
|
+
cachedDefault(agentId, category) {
|
|
22053
|
+
return this.state.optionDefaults?.[agentId]?.[category];
|
|
22054
|
+
}
|
|
22055
|
+
setCachedOptions(agentId, category, choices, runs) {
|
|
22056
|
+
const sameList = JSON.stringify(this.cachedOptions(agentId, category)) === JSON.stringify(choices);
|
|
22057
|
+
const sameDefault = runs === undefined || runs === this.cachedDefault(agentId, category);
|
|
22058
|
+
if (sameList && sameDefault)
|
|
21791
22059
|
return false;
|
|
21792
22060
|
this.state.optionCache ??= {};
|
|
21793
22061
|
this.state.optionCache[agentId] ??= {};
|
|
21794
22062
|
this.state.optionCache[agentId][category] = choices;
|
|
22063
|
+
if (runs !== undefined) {
|
|
22064
|
+
this.state.optionDefaults ??= {};
|
|
22065
|
+
this.state.optionDefaults[agentId] ??= {};
|
|
22066
|
+
this.state.optionDefaults[agentId][category] = runs;
|
|
22067
|
+
}
|
|
21795
22068
|
this.save();
|
|
21796
22069
|
return true;
|
|
21797
22070
|
}
|
|
@@ -28610,9 +28883,6 @@ class Interest {
|
|
|
28610
28883
|
// ../daemon/src/server/ws.ts
|
|
28611
28884
|
import { homedir as homedir12, hostname as hostname2 } from "os";
|
|
28612
28885
|
|
|
28613
|
-
// ../daemon/src/server/handlers.ts
|
|
28614
|
-
import { randomBytes as randomBytes7 } from "crypto";
|
|
28615
|
-
|
|
28616
28886
|
// ../daemon/src/agent/config.ts
|
|
28617
28887
|
import { existsSync as existsSync28, readFileSync as readFileSync17 } from "fs";
|
|
28618
28888
|
import { homedir as homedir11 } from "os";
|
|
@@ -29234,6 +29504,12 @@ var notify = async (s, ctx, worktreeId, msg) => {
|
|
|
29234
29504
|
await gitStatus(s, ctx, worktreeId);
|
|
29235
29505
|
};
|
|
29236
29506
|
var requireRun = (s, id) => s.worktrees.requireRun(id);
|
|
29507
|
+
function backfillOf(entries, since) {
|
|
29508
|
+
const last = lastSeq(entries);
|
|
29509
|
+
if (since === undefined || since > last)
|
|
29510
|
+
return { events: coalesce(entries), last };
|
|
29511
|
+
return { events: coalesce(after(entries, since)), last, after: since };
|
|
29512
|
+
}
|
|
29237
29513
|
var handlers = {
|
|
29238
29514
|
async subscribe(msg, ctx, s) {
|
|
29239
29515
|
const r = s.worktrees.readable(msg.worktreeId);
|
|
@@ -29243,7 +29519,7 @@ var handlers = {
|
|
|
29243
29519
|
throw new UserError("unknown worktree");
|
|
29244
29520
|
if (!ctx.subscribe(msg.worktreeId))
|
|
29245
29521
|
return;
|
|
29246
|
-
ctx.reply({ t: "backfill", worktreeId: msg.worktreeId,
|
|
29522
|
+
ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, ...backfillOf(kept, msg.since), log: [] });
|
|
29247
29523
|
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
|
|
29248
29524
|
ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
|
|
29249
29525
|
await gitStatus(s, ctx, msg.worktreeId);
|
|
@@ -29252,7 +29528,7 @@ var handlers = {
|
|
|
29252
29528
|
if (!ctx.subscribe(msg.worktreeId))
|
|
29253
29529
|
return;
|
|
29254
29530
|
if (!r.wt) {
|
|
29255
|
-
ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: [], log: [] });
|
|
29531
|
+
ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: [], last: -1, log: [] });
|
|
29256
29532
|
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
|
|
29257
29533
|
ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
|
|
29258
29534
|
await gitAndPages(s, ctx, msg.worktreeId);
|
|
@@ -29262,7 +29538,7 @@ var handlers = {
|
|
|
29262
29538
|
ctx.reply({
|
|
29263
29539
|
t: "backfill",
|
|
29264
29540
|
worktreeId: msg.worktreeId,
|
|
29265
|
-
|
|
29541
|
+
...backfillOf(agent.transcript(), msg.since),
|
|
29266
29542
|
log: s.runtime.recentLogs(msg.worktreeId)
|
|
29267
29543
|
});
|
|
29268
29544
|
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
|
|
@@ -29329,29 +29605,8 @@ var handlers = {
|
|
|
29329
29605
|
effort: msg.effort,
|
|
29330
29606
|
carry: msg.carry
|
|
29331
29607
|
};
|
|
29332
|
-
const of = msg.variants ?? 1;
|
|
29333
29608
|
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];
|
|
29609
|
+
await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
|
|
29355
29610
|
});
|
|
29356
29611
|
},
|
|
29357
29612
|
"set-worktree-profile"(msg, _ctx, s) {
|
|
@@ -29372,27 +29627,6 @@ var handlers = {
|
|
|
29372
29627
|
"set-worktree-effort"(msg, _ctx, s) {
|
|
29373
29628
|
s.worktrees.setEffort(msg.worktreeId, msg.effort);
|
|
29374
29629
|
},
|
|
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
29630
|
async "archive-worktree"(msg, _ctx, s) {
|
|
29397
29631
|
await s.worktrees.archiveWorktree(msg.worktreeId);
|
|
29398
29632
|
},
|
|
@@ -29541,9 +29775,6 @@ var handlers = {
|
|
|
29541
29775
|
s.turns.stoppedByPerson(msg.worktreeId);
|
|
29542
29776
|
s.runtime.agentFor(msg.worktreeId)?.stop();
|
|
29543
29777
|
},
|
|
29544
|
-
async "pick-variant"(msg, _ctx, s) {
|
|
29545
|
-
await s.worktrees.pickVariant(msg.worktreeId);
|
|
29546
|
-
},
|
|
29547
29778
|
unqueue(msg, _ctx, s) {
|
|
29548
29779
|
requireRun(s, msg.worktreeId);
|
|
29549
29780
|
s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index, msg.edit ? (m) => s.drafts.putBack(msg.worktreeId, { text: m.text, items: m.attachments ?? [] }) : undefined);
|
|
@@ -29718,13 +29949,17 @@ var handlers = {
|
|
|
29718
29949
|
s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId, ...msg.note ? { note: msg.note } : {} });
|
|
29719
29950
|
s.worktrees.markPrompted(msg.worktreeId);
|
|
29720
29951
|
},
|
|
29721
|
-
async "
|
|
29952
|
+
async "pieces-ask"(msg, _ctx, s) {
|
|
29953
|
+
requireRun(s, msg.worktreeId);
|
|
29954
|
+
await s.pieces.ask(msg.worktreeId, msg.repoId, msg.text);
|
|
29955
|
+
},
|
|
29956
|
+
"pieces-fate"(msg, _ctx, s) {
|
|
29722
29957
|
requireRun(s, msg.worktreeId);
|
|
29723
|
-
|
|
29958
|
+
s.pieces.fate(msg.worktreeId, msg.id, msg.pieceId, msg.fate);
|
|
29724
29959
|
},
|
|
29725
|
-
async "
|
|
29960
|
+
async "pieces-answer"(msg, _ctx, s) {
|
|
29726
29961
|
requireRun(s, msg.worktreeId);
|
|
29727
|
-
await s.
|
|
29962
|
+
await s.pieces.answer(msg.worktreeId, msg.id, msg.go, msg.note);
|
|
29728
29963
|
},
|
|
29729
29964
|
"agent-config"(msg, ctx, s) {
|
|
29730
29965
|
const spec = s.agents.require(msg.agent);
|
|
@@ -29806,7 +30041,7 @@ var handlers = {
|
|
|
29806
30041
|
},
|
|
29807
30042
|
"preview-start"(msg, _ctx, s) {
|
|
29808
30043
|
requireRun(s, msg.worktreeId);
|
|
29809
|
-
s.worktrees.
|
|
30044
|
+
s.worktrees.retryPreview(msg.worktreeId);
|
|
29810
30045
|
},
|
|
29811
30046
|
async "search-refs"(msg, ctx, s) {
|
|
29812
30047
|
const refs = await s.refs.search(msg.repoId, msg.query);
|
|
@@ -29889,6 +30124,47 @@ function withCors(res, origin) {
|
|
|
29889
30124
|
return res;
|
|
29890
30125
|
}
|
|
29891
30126
|
|
|
30127
|
+
// ../daemon/src/server/encoding.ts
|
|
30128
|
+
import { promisify } from "util";
|
|
30129
|
+
import { gzip } from "zlib";
|
|
30130
|
+
var gzipAsync = promisify(gzip);
|
|
30131
|
+
function acceptedEncodings(header) {
|
|
30132
|
+
const out = new Set;
|
|
30133
|
+
if (!header)
|
|
30134
|
+
return out;
|
|
30135
|
+
for (const part of header.split(",")) {
|
|
30136
|
+
const [name = "", ...params] = part.trim().toLowerCase().split(";");
|
|
30137
|
+
const coding = name.trim();
|
|
30138
|
+
if (coding !== "br" && coding !== "gzip")
|
|
30139
|
+
continue;
|
|
30140
|
+
const refused = params.some((p) => /^\s*q\s*=\s*0(\.0*)?\s*$/.test(p));
|
|
30141
|
+
if (!refused)
|
|
30142
|
+
out.add(coding);
|
|
30143
|
+
}
|
|
30144
|
+
return out;
|
|
30145
|
+
}
|
|
30146
|
+
function pickSibling(file, accepted) {
|
|
30147
|
+
const candidates = [
|
|
30148
|
+
["br", `${file}.br`],
|
|
30149
|
+
["gzip", `${file}.gz`]
|
|
30150
|
+
];
|
|
30151
|
+
for (const [encoding, path] of candidates) {
|
|
30152
|
+
if (accepted.has(encoding) && Bun.file(path).size > 0)
|
|
30153
|
+
return { path, encoding };
|
|
30154
|
+
}
|
|
30155
|
+
return { path: file, encoding: null };
|
|
30156
|
+
}
|
|
30157
|
+
var GZIP_MIN_BYTES = 1024;
|
|
30158
|
+
async function encoded(req, body, headers) {
|
|
30159
|
+
const bytes = Buffer.from(body);
|
|
30160
|
+
if (bytes.byteLength < GZIP_MIN_BYTES || !acceptedEncodings(req.headers.get("accept-encoding")).has("gzip")) {
|
|
30161
|
+
return new Response(bytes, { headers });
|
|
30162
|
+
}
|
|
30163
|
+
return new Response(await gzipAsync(bytes), {
|
|
30164
|
+
headers: { ...headers, "content-encoding": "gzip", vary: "accept-encoding" }
|
|
30165
|
+
});
|
|
30166
|
+
}
|
|
30167
|
+
|
|
29892
30168
|
// ../daemon/src/server/pairing.ts
|
|
29893
30169
|
var NO_STORE = { "cache-control": "no-store" };
|
|
29894
30170
|
var json = (body, headers = {}) => Response.json(body, { headers: { ...NO_STORE, ...headers } });
|
|
@@ -30008,6 +30284,7 @@ function createFetch(opts) {
|
|
|
30008
30284
|
const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`);
|
|
30009
30285
|
const unauthorized = () => new Response("unauthorized", { status: 401 });
|
|
30010
30286
|
const gateFor = (remote) => remote && { grant, host: remote.host, redeem: (code) => opts.grants.redeem(code) };
|
|
30287
|
+
const page = async (req, file) => encoded(req, await Bun.file(file).text(), { "content-type": "text/html;charset=utf-8", "cache-control": NO_STORE2 });
|
|
30011
30288
|
const health = async () => {
|
|
30012
30289
|
const remote = opts.remote();
|
|
30013
30290
|
return Response.json({
|
|
@@ -30060,6 +30337,7 @@ function createFetch(opts) {
|
|
|
30060
30337
|
const authed = sameSecret(url.searchParams.get("token"), opts.token);
|
|
30061
30338
|
if (authed)
|
|
30062
30339
|
opts.noteShellOrigin(req.headers.get("origin"));
|
|
30340
|
+
const known = parseKnown(url.searchParams.get("known"));
|
|
30063
30341
|
const data = {
|
|
30064
30342
|
authed,
|
|
30065
30343
|
subs: new Set,
|
|
@@ -30068,6 +30346,7 @@ function createFetch(opts) {
|
|
|
30068
30346
|
sent: 0,
|
|
30069
30347
|
bytes: 0,
|
|
30070
30348
|
view: null,
|
|
30349
|
+
...known ? { known } : {},
|
|
30071
30350
|
...remoteShell ? { remote: true } : {}
|
|
30072
30351
|
};
|
|
30073
30352
|
const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
|
|
@@ -30080,10 +30359,13 @@ function createFetch(opts) {
|
|
|
30080
30359
|
if (url.pathname === "/bootstrap") {
|
|
30081
30360
|
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
30082
30361
|
return new Response("unauthorized", { status: 401 });
|
|
30083
|
-
const headers = {
|
|
30362
|
+
const headers = {
|
|
30363
|
+
"content-type": "application/json;charset=utf-8",
|
|
30364
|
+
"cache-control": NO_STORE2
|
|
30365
|
+
};
|
|
30084
30366
|
if (remote && remoteShell)
|
|
30085
30367
|
headers["set-cookie"] = grantCookie(grant, remote.host);
|
|
30086
|
-
return
|
|
30368
|
+
return encoded(req, JSON.stringify(await opts.bootstrap()), headers);
|
|
30087
30369
|
}
|
|
30088
30370
|
if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
|
|
30089
30371
|
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
@@ -30107,6 +30389,9 @@ function createFetch(opts) {
|
|
|
30107
30389
|
}
|
|
30108
30390
|
});
|
|
30109
30391
|
}
|
|
30392
|
+
if (url.pathname === "/share" && req.method === "POST") {
|
|
30393
|
+
return new Response(null, { status: 303, headers: { location: "/", "cache-control": NO_STORE2 } });
|
|
30394
|
+
}
|
|
30110
30395
|
const paired = await opts.pairing(req, url, d);
|
|
30111
30396
|
if (paired)
|
|
30112
30397
|
return paired;
|
|
@@ -30228,7 +30513,7 @@ function createFetch(opts) {
|
|
|
30228
30513
|
const hashed = rel.startsWith("/assets/");
|
|
30229
30514
|
if (existsSync29(file) && Bun.file(file).size > 0) {
|
|
30230
30515
|
if (rel === "/manifest.json") {
|
|
30231
|
-
const ground = opts.manifestGround();
|
|
30516
|
+
const ground = url.searchParams.get("black") === "1" ? "#000000" : opts.manifestGround();
|
|
30232
30517
|
const manifest = { ...await Bun.file(file).json(), theme_color: ground, background_color: ground };
|
|
30233
30518
|
return Response.json(manifest, {
|
|
30234
30519
|
headers: { "content-type": "application/manifest+json", "cache-control": NO_STORE2 }
|
|
@@ -30241,13 +30526,26 @@ function createFetch(opts) {
|
|
|
30241
30526
|
headers: { "content-type": "text/html; charset=utf-8", "cache-control": NO_STORE2 }
|
|
30242
30527
|
});
|
|
30243
30528
|
}
|
|
30244
|
-
|
|
30529
|
+
if (hashed) {
|
|
30530
|
+
const sibling = pickSibling(file, acceptedEncodings(req.headers.get("accept-encoding")));
|
|
30531
|
+
return new Response(Bun.file(sibling.path), {
|
|
30532
|
+
headers: {
|
|
30533
|
+
"content-type": Bun.file(file).type,
|
|
30534
|
+
"cache-control": IMMUTABLE,
|
|
30535
|
+
vary: "accept-encoding",
|
|
30536
|
+
...sibling.encoding ? { "content-encoding": sibling.encoding } : {}
|
|
30537
|
+
}
|
|
30538
|
+
});
|
|
30539
|
+
}
|
|
30540
|
+
if (rel === "/index.html")
|
|
30541
|
+
return page(req, file);
|
|
30542
|
+
return new Response(Bun.file(file), { headers: { "cache-control": NO_STORE2 } });
|
|
30245
30543
|
}
|
|
30246
30544
|
if (hashed)
|
|
30247
30545
|
return new Response("not found", { status: 404 });
|
|
30248
30546
|
const index = join25(opts.shellDist, "index.html");
|
|
30249
30547
|
if (existsSync29(index))
|
|
30250
|
-
return
|
|
30548
|
+
return page(req, index);
|
|
30251
30549
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
30252
30550
|
}
|
|
30253
30551
|
return async function fetch2(req, srv) {
|
|
@@ -30274,12 +30572,35 @@ function createFetch(opts) {
|
|
|
30274
30572
|
};
|
|
30275
30573
|
}
|
|
30276
30574
|
|
|
30575
|
+
// ../daemon/src/server/stamps.ts
|
|
30576
|
+
var stampOf = (part) => Bun.hash(JSON.stringify(part)).toString(16);
|
|
30577
|
+
var visitsStamp = (visits) => stampOf(Object.entries(visits).map(([repoId, list]) => [repoId, list.map((e) => [e.path, e.title ?? ""])]));
|
|
30578
|
+
function stampsOf(h) {
|
|
30579
|
+
return {
|
|
30580
|
+
rows: stampOf({ rows: h.rows, trunks: h.trunks }),
|
|
30581
|
+
repos: stampOf(h.repos),
|
|
30582
|
+
visits: visitsStamp(h.visits),
|
|
30583
|
+
themes: stampOf(h.themes)
|
|
30584
|
+
};
|
|
30585
|
+
}
|
|
30586
|
+
function shapeHello(full, known) {
|
|
30587
|
+
if (!known)
|
|
30588
|
+
return full;
|
|
30589
|
+
const { themes, ...rest } = full;
|
|
30590
|
+
const withThemes = sameStamp(known.themes, full.stamps.themes) ? rest : full;
|
|
30591
|
+
const same = sameStamp(known.rows, full.stamps.rows) && sameStamp(known.repos, full.stamps.repos) && sameStamp(known.visits, full.stamps.visits);
|
|
30592
|
+
if (!same)
|
|
30593
|
+
return withThemes;
|
|
30594
|
+
const { rows: _r, trunks: _t, repos: _p, visits: _v, same: _s, ...light } = withThemes;
|
|
30595
|
+
return { ...light, same: true };
|
|
30596
|
+
}
|
|
30597
|
+
|
|
30277
30598
|
// ../daemon/src/server/ws.ts
|
|
30278
30599
|
var DROP_ABOVE_BYTES = 4 * 1024 * 1024;
|
|
30279
30600
|
var DROPPED_NOTICE = `\r
|
|
30280
30601
|
[toyon] output dropped: this pane was too far behind\r
|
|
30281
30602
|
`;
|
|
30282
|
-
var SENDS = new Set(["chat", "create-worktree", "restore-worktree"
|
|
30603
|
+
var SENDS = new Set(["chat", "create-worktree", "restore-worktree"]);
|
|
30283
30604
|
function sentFrom(json) {
|
|
30284
30605
|
if (!json || typeof json !== "object")
|
|
30285
30606
|
return null;
|
|
@@ -30313,7 +30634,7 @@ function startServer(opts) {
|
|
|
30313
30634
|
};
|
|
30314
30635
|
const raw = (ws, json) => {
|
|
30315
30636
|
try {
|
|
30316
|
-
ws.send(json);
|
|
30637
|
+
ws.send(json, true);
|
|
30317
30638
|
ws.data.sent++;
|
|
30318
30639
|
ws.data.bytes += json.length;
|
|
30319
30640
|
} catch (e) {
|
|
@@ -30413,7 +30734,7 @@ function startServer(opts) {
|
|
|
30413
30734
|
statusesQueued = false;
|
|
30414
30735
|
try {
|
|
30415
30736
|
const { rows, trunks } = s.worktrees.snapshot();
|
|
30416
|
-
broadcast({ t: "worktrees", rows, trunks });
|
|
30737
|
+
broadcast({ t: "worktrees", rows, trunks, stamp: stampOf({ rows, trunks }) });
|
|
30417
30738
|
} catch (e) {
|
|
30418
30739
|
log.warn("ws", "worktrees broadcast failed", e);
|
|
30419
30740
|
}
|
|
@@ -30422,7 +30743,7 @@ function startServer(opts) {
|
|
|
30422
30743
|
s.hub.on("worktreesChanged", worktreesChanged);
|
|
30423
30744
|
s.hub.on("agentStatus", worktreesChanged);
|
|
30424
30745
|
s.hub.on("agentStopping", worktreesChanged);
|
|
30425
|
-
s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos }));
|
|
30746
|
+
s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos, stamp: stampOf(s.state.repos) }));
|
|
30426
30747
|
s.hub.on("pendingChanged", () => broadcast({ t: "pending-repos", pending: s.repos.pending }));
|
|
30427
30748
|
s.hub.on("proc", (worktreeId, proc) => broadcast({ t: "proc", worktreeId, proc }));
|
|
30428
30749
|
s.hub.on("log", (worktreeId, proc, line, retract) => sendTo(worktreeId, retract ? { t: "log", worktreeId, proc, line, retract } : { t: "log", worktreeId, proc, line }));
|
|
@@ -30463,7 +30784,7 @@ function startServer(opts) {
|
|
|
30463
30784
|
const themesChanged = () => {
|
|
30464
30785
|
const cur = s.themes.current();
|
|
30465
30786
|
setWaitingColors({ bg: cur.colors.surface0, fg: cur.colors.text1 });
|
|
30466
|
-
broadcast({ t: "themes", themes: s.themes.themes, prefs: s.themes.prefs });
|
|
30787
|
+
broadcast({ t: "themes", themes: s.themes.themes, prefs: s.themes.prefs, stamp: stampOf(s.themes.themes) });
|
|
30467
30788
|
};
|
|
30468
30789
|
s.hub.on("themesChanged", themesChanged);
|
|
30469
30790
|
themesChanged();
|
|
@@ -30489,23 +30810,22 @@ function startServer(opts) {
|
|
|
30489
30810
|
s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
|
|
30490
30811
|
s.hub.on("failed", (worktreeId, message) => broadcast({ t: "error", message, worktreeId }));
|
|
30491
30812
|
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) }));
|
|
30813
|
+
s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId), stamp: visitsStamp(s.routes.historyAll()) }));
|
|
30493
30814
|
s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
|
|
30494
30815
|
s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
|
|
30495
30816
|
s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
|
|
30496
|
-
const helloFrame = async () => {
|
|
30817
|
+
const helloFrame = async (known = null) => {
|
|
30497
30818
|
await s.update.refresh();
|
|
30498
30819
|
const { rows, trunks } = s.worktrees.snapshot();
|
|
30499
|
-
|
|
30820
|
+
const parts = { rows, trunks, repos: s.state.repos, visits: s.routes.historyAll(), themes: s.themes.themes };
|
|
30821
|
+
const full = {
|
|
30500
30822
|
t: "hello",
|
|
30501
30823
|
version,
|
|
30502
30824
|
install: s.update.install(),
|
|
30503
30825
|
registry: await s.update.registry(),
|
|
30504
30826
|
protocol: PROTOCOL_VERSION,
|
|
30505
|
-
|
|
30506
|
-
|
|
30507
|
-
trunks,
|
|
30508
|
-
themes: s.themes.themes,
|
|
30827
|
+
...parts,
|
|
30828
|
+
stamps: stampsOf(parts),
|
|
30509
30829
|
themePrefs: s.themes.prefs,
|
|
30510
30830
|
agents: s.readiness.describe(),
|
|
30511
30831
|
defaultAgent: s.agents.defaultId(s.state.defaultAgent),
|
|
@@ -30521,13 +30841,13 @@ function startServer(opts) {
|
|
|
30521
30841
|
knocks: opts.knocks.pending(),
|
|
30522
30842
|
gitIdentity: await s.repos.gitIdentity(),
|
|
30523
30843
|
pending: s.repos.pending,
|
|
30524
|
-
visits: s.routes.historyAll(),
|
|
30525
30844
|
self: s.self.get(),
|
|
30526
30845
|
update: s.update.get(),
|
|
30527
30846
|
drafts: s.drafts.all(),
|
|
30528
30847
|
attachments: s.drafts.allAttachments(),
|
|
30529
30848
|
managed: managedView(opts.managed)
|
|
30530
30849
|
};
|
|
30850
|
+
return shapeHello(full, known);
|
|
30531
30851
|
};
|
|
30532
30852
|
let branded = false;
|
|
30533
30853
|
const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${token}`);
|
|
@@ -30578,6 +30898,7 @@ function startServer(opts) {
|
|
|
30578
30898
|
changelog: () => Bun.file(opts.changelog).text().catch(() => null)
|
|
30579
30899
|
}),
|
|
30580
30900
|
websocket: {
|
|
30901
|
+
perMessageDeflate: true,
|
|
30581
30902
|
async open(ws) {
|
|
30582
30903
|
const preview = ws.data.preview;
|
|
30583
30904
|
if (preview) {
|
|
@@ -30591,7 +30912,7 @@ function startServer(opts) {
|
|
|
30591
30912
|
}
|
|
30592
30913
|
sockets.add(ws);
|
|
30593
30914
|
link(ws);
|
|
30594
|
-
send(ws, await helloFrame());
|
|
30915
|
+
send(ws, await helloFrame(ws.data.known ?? null));
|
|
30595
30916
|
fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
|
|
30596
30917
|
for (const o of s.opens.takePending())
|
|
30597
30918
|
send(ws, openedFrame(o));
|
|
@@ -30889,7 +31210,7 @@ function discover(dir) {
|
|
|
30889
31210
|
import { homedir as homedir14 } from "os";
|
|
30890
31211
|
async function latestVersion() {
|
|
30891
31212
|
const registry = await npmRegistry();
|
|
30892
|
-
const r = await run("npm", ["view", "toyon", "version", "--json"], homedir14());
|
|
31213
|
+
const r = await run("npm", ["view", "toyon", "version", "--json"], homedir14(), undefined, { timeoutMs: NPM_MS });
|
|
30893
31214
|
if (!r.ok)
|
|
30894
31215
|
return { version: null, registry };
|
|
30895
31216
|
try {
|
|
@@ -30899,8 +31220,9 @@ async function latestVersion() {
|
|
|
30899
31220
|
return { version: null, registry };
|
|
30900
31221
|
}
|
|
30901
31222
|
}
|
|
31223
|
+
var NPM_MS = 30000;
|
|
30902
31224
|
async function npmRegistry() {
|
|
30903
|
-
const config = await run("npm", ["config", "get", "registry"], homedir14());
|
|
31225
|
+
const config = await run("npm", ["config", "get", "registry"], homedir14(), undefined, { timeoutMs: NPM_MS });
|
|
30904
31226
|
return config.ok && config.out !== "" ? config.out : "the npm registry";
|
|
30905
31227
|
}
|
|
30906
31228
|
var LOG_POINTER = /complete log of this run/i;
|
|
@@ -30939,6 +31261,7 @@ class UpdateService {
|
|
|
30939
31261
|
latest = null;
|
|
30940
31262
|
unreachable = null;
|
|
30941
31263
|
registryName = null;
|
|
31264
|
+
registryAsk = null;
|
|
30942
31265
|
seen = null;
|
|
30943
31266
|
seenAt = 0;
|
|
30944
31267
|
installing = false;
|
|
@@ -30969,6 +31292,7 @@ class UpdateService {
|
|
|
30969
31292
|
d.hub.on("holdsChanged", () => {
|
|
30970
31293
|
this.busyAt = this.now();
|
|
30971
31294
|
});
|
|
31295
|
+
fireAndForget("update", this.registry(), "npm registry");
|
|
30972
31296
|
}
|
|
30973
31297
|
async tick() {
|
|
30974
31298
|
await this.refresh();
|
|
@@ -30993,7 +31317,15 @@ class UpdateService {
|
|
|
30993
31317
|
async registry() {
|
|
30994
31318
|
if (this.d.method === "none" || this.d.managedBy)
|
|
30995
31319
|
return null;
|
|
30996
|
-
this.registryName
|
|
31320
|
+
if (this.registryName !== null)
|
|
31321
|
+
return this.registryName;
|
|
31322
|
+
this.registryAsk ??= this.d.registry();
|
|
31323
|
+
try {
|
|
31324
|
+
this.registryName = await this.registryAsk;
|
|
31325
|
+
} catch (e) {
|
|
31326
|
+
this.registryAsk = null;
|
|
31327
|
+
throw e;
|
|
31328
|
+
}
|
|
30997
31329
|
return this.registryName;
|
|
30998
31330
|
}
|
|
30999
31331
|
async check() {
|
|
@@ -31572,6 +31904,9 @@ function saidRows(entries) {
|
|
|
31572
31904
|
if (event.type === "user-message") {
|
|
31573
31905
|
turnTs = event.ts;
|
|
31574
31906
|
push(seq, "user", event.text, event.ts);
|
|
31907
|
+
} else if (event.type === "handed") {
|
|
31908
|
+
turnTs = event.ts;
|
|
31909
|
+
push(seq, "user", event.body ?? event.text, event.ts);
|
|
31575
31910
|
} else if (event.type === "turn-start") {
|
|
31576
31911
|
turnTs = event.ts;
|
|
31577
31912
|
} else if (event.type === "text-delta") {
|
|
@@ -31705,7 +32040,7 @@ function lastAsked(entries) {
|
|
|
31705
32040
|
const e = entries[i].event;
|
|
31706
32041
|
if (e.type === "fix-asked")
|
|
31707
32042
|
return e.kind;
|
|
31708
|
-
if (e.type === "user-message")
|
|
32043
|
+
if (e.type === "user-message" || e.type === "handed")
|
|
31709
32044
|
return;
|
|
31710
32045
|
}
|
|
31711
32046
|
return;
|
|
@@ -31810,240 +32145,6 @@ class FixService {
|
|
|
31810
32145
|
}
|
|
31811
32146
|
}
|
|
31812
32147
|
|
|
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
32148
|
// ../daemon/src/worktrees/landing.ts
|
|
32048
32149
|
var TAIL_CHARS = 400;
|
|
32049
32150
|
var DIFF_CHARS = 2000;
|
|
@@ -32395,6 +32496,900 @@ async function recentSubjects(path, defaultBr) {
|
|
|
32395
32496
|
`).filter(Boolean) : [];
|
|
32396
32497
|
}
|
|
32397
32498
|
|
|
32499
|
+
// ../daemon/src/worktrees/pieces.ts
|
|
32500
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
32501
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
32502
|
+
import { join as join28 } from "path";
|
|
32503
|
+
|
|
32504
|
+
// ../daemon/src/worktrees/proposals.ts
|
|
32505
|
+
var PER_TURN_MAX = 3;
|
|
32506
|
+
function proposedThisTurn(entries) {
|
|
32507
|
+
let n = 0;
|
|
32508
|
+
for (let i = entries.length - 1;i >= 0; i--) {
|
|
32509
|
+
const e = entries[i].event;
|
|
32510
|
+
if (e.type === "turn-start")
|
|
32511
|
+
break;
|
|
32512
|
+
if (e.type === "pieces-proposed")
|
|
32513
|
+
n++;
|
|
32514
|
+
}
|
|
32515
|
+
return n;
|
|
32516
|
+
}
|
|
32517
|
+
function matchBy(items, q, keys) {
|
|
32518
|
+
if (!q.trim())
|
|
32519
|
+
return [];
|
|
32520
|
+
const lower = q.toLowerCase();
|
|
32521
|
+
const rungs = [
|
|
32522
|
+
(v) => v === q,
|
|
32523
|
+
(v) => v.toLowerCase() === lower,
|
|
32524
|
+
(v) => v.toLowerCase().startsWith(lower)
|
|
32525
|
+
];
|
|
32526
|
+
for (const rung of rungs) {
|
|
32527
|
+
const found = items.filter((item) => keys.some((key) => rung(key(item) ?? "")));
|
|
32528
|
+
if (found.length > 0)
|
|
32529
|
+
return found;
|
|
32530
|
+
}
|
|
32531
|
+
return [];
|
|
32532
|
+
}
|
|
32533
|
+
|
|
32534
|
+
// ../daemon/src/worktrees/pieces.ts
|
|
32535
|
+
function handedOrigin(origin, project) {
|
|
32536
|
+
return {
|
|
32537
|
+
id: origin.id,
|
|
32538
|
+
repoId: origin.repoId,
|
|
32539
|
+
branch: origin.branch,
|
|
32540
|
+
title: origin.title,
|
|
32541
|
+
...project ? { project } : {}
|
|
32542
|
+
};
|
|
32543
|
+
}
|
|
32544
|
+
function pieceDoc(piece) {
|
|
32545
|
+
return `# ${piece.title}
|
|
32546
|
+
|
|
32547
|
+
${piece.plan.trim()}
|
|
32548
|
+
`;
|
|
32549
|
+
}
|
|
32550
|
+
var PIECES_MAX = 6;
|
|
32551
|
+
var PLAN_MAX = 20000;
|
|
32552
|
+
var ATTACH_MAX = 10;
|
|
32553
|
+
var PROJECT_MAX = 200;
|
|
32554
|
+
var refused = (text) => ({ text, isError: true });
|
|
32555
|
+
var HELD = "Handed to the person; the list shows beside your plan and they decide where each piece goes. Ask for plan approval now.";
|
|
32556
|
+
var SHOWN = "Handed to the person as a card; they decide. Finish your turn.";
|
|
32557
|
+
function openPlanCard(entries) {
|
|
32558
|
+
const open = new Map;
|
|
32559
|
+
for (const { event } of entries) {
|
|
32560
|
+
if (event.type === "agent-permission" && event.plan)
|
|
32561
|
+
open.set(event.id, event);
|
|
32562
|
+
else if (event.type === "agent-ask-end")
|
|
32563
|
+
open.delete(event.id);
|
|
32564
|
+
}
|
|
32565
|
+
return [...open.values()].at(-1) ?? null;
|
|
32566
|
+
}
|
|
32567
|
+
function sectionText(markdown, heading) {
|
|
32568
|
+
const norm = (s) => s.toLowerCase().replace(/[`*_~]/g, "").replace(/[^a-z0-9]+/g, " ").trim();
|
|
32569
|
+
const want = norm(heading);
|
|
32570
|
+
if (!want)
|
|
32571
|
+
return null;
|
|
32572
|
+
const lines = markdown.split(`
|
|
32573
|
+
`);
|
|
32574
|
+
const heads = [];
|
|
32575
|
+
for (let i = 0;i < lines.length; i++) {
|
|
32576
|
+
const m = /^(#{1,6})\s+(.*)$/.exec(lines[i] ?? "");
|
|
32577
|
+
const have = m ? norm(m[2] ?? "") : "";
|
|
32578
|
+
if (m && have)
|
|
32579
|
+
heads.push({ i, level: m[1].length, have });
|
|
32580
|
+
}
|
|
32581
|
+
const rungs = [
|
|
32582
|
+
(have) => have === want,
|
|
32583
|
+
(have) => have.includes(want),
|
|
32584
|
+
(have) => have.length >= 12 && want.includes(have)
|
|
32585
|
+
];
|
|
32586
|
+
for (const rung of rungs) {
|
|
32587
|
+
const hit = heads.find((h) => rung(h.have));
|
|
32588
|
+
if (!hit)
|
|
32589
|
+
continue;
|
|
32590
|
+
const { i, level } = hit;
|
|
32591
|
+
let end = lines.length;
|
|
32592
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
32593
|
+
const n = /^(#{1,6})\s/.exec(lines[j] ?? "");
|
|
32594
|
+
if (n && n[1].length <= level) {
|
|
32595
|
+
end = j;
|
|
32596
|
+
break;
|
|
32597
|
+
}
|
|
32598
|
+
}
|
|
32599
|
+
const text = lines.slice(i, end).join(`
|
|
32600
|
+
`).trim();
|
|
32601
|
+
return text.length > PLAN_MAX ? text.slice(0, PLAN_MAX) : text;
|
|
32602
|
+
}
|
|
32603
|
+
return null;
|
|
32604
|
+
}
|
|
32605
|
+
function hashText(text) {
|
|
32606
|
+
return createHash5("sha1").update(text.trim()).digest("hex").slice(0, 16);
|
|
32607
|
+
}
|
|
32608
|
+
function heldUnshown(entries) {
|
|
32609
|
+
const shown = new Set;
|
|
32610
|
+
for (const { event } of entries)
|
|
32611
|
+
if (event.type === "pieces-shown")
|
|
32612
|
+
shown.add(event.id);
|
|
32613
|
+
return openPieces(entries).find((p) => p.held && !shown.has(p.id)) ?? null;
|
|
32614
|
+
}
|
|
32615
|
+
function openPieces(entries) {
|
|
32616
|
+
const open = new Map;
|
|
32617
|
+
for (const { event } of entries) {
|
|
32618
|
+
if (event.type === "pieces-proposed")
|
|
32619
|
+
open.set(event.id, event);
|
|
32620
|
+
else if (event.type === "pieces-go" || event.type === "pieces" || event.type === "pieces-declined")
|
|
32621
|
+
open.delete(event.id);
|
|
32622
|
+
}
|
|
32623
|
+
return [...open.values()];
|
|
32624
|
+
}
|
|
32625
|
+
function listFile(entries, id) {
|
|
32626
|
+
let file;
|
|
32627
|
+
for (const { event } of entries) {
|
|
32628
|
+
if ((event.type === "pieces-proposed" || event.type === "pieces-shown") && event.id === id && event.plan) {
|
|
32629
|
+
file = event.plan;
|
|
32630
|
+
} else if (event.type === "agent-permission" && event.pieces === id && event.plan)
|
|
32631
|
+
file = event.plan;
|
|
32632
|
+
}
|
|
32633
|
+
return file;
|
|
32634
|
+
}
|
|
32635
|
+
function effectiveFate(entries, proposal, piece) {
|
|
32636
|
+
let fate = piece.fate;
|
|
32637
|
+
for (const { event } of entries) {
|
|
32638
|
+
if (event.type === "pieces-fate" && event.id === proposal.id && event.pieceId === piece.id)
|
|
32639
|
+
fate = event.fate;
|
|
32640
|
+
}
|
|
32641
|
+
return fate;
|
|
32642
|
+
}
|
|
32643
|
+
var LABEL = /^(image|paste|pasted text|file|element)\s+(\d+)$/i;
|
|
32644
|
+
var KIND_OF = {
|
|
32645
|
+
image: "image",
|
|
32646
|
+
paste: "paste",
|
|
32647
|
+
"pasted text": "paste",
|
|
32648
|
+
file: "file",
|
|
32649
|
+
element: "pick"
|
|
32650
|
+
};
|
|
32651
|
+
|
|
32652
|
+
class PiecesService {
|
|
32653
|
+
d;
|
|
32654
|
+
constructor(d) {
|
|
32655
|
+
this.d = d;
|
|
32656
|
+
d.hub.on("piecesDecided", (worktreeId, id, approved) => fireAndForget(worktreeId, approved ? this.act(worktreeId, id, undefined, true) : this.decline(worktreeId, id), "pieces"));
|
|
32657
|
+
d.hub.on("planUnsplit", (worktreeId, markdown, planPath) => fireAndForget(worktreeId, this.read(worktreeId, markdown, planPath), "pieces read off the plan"));
|
|
32658
|
+
d.hub.on("treeChanged", (worktreeId) => fireAndForget(worktreeId, this.stale(worktreeId), "pieces stale"));
|
|
32659
|
+
d.hub.on("landed", (worktreeId) => fireAndForget(worktreeId, this.landed(worktreeId), "pieces after landing"));
|
|
32660
|
+
d.hub.on("worktreesChanged", () => this.sweep());
|
|
32661
|
+
this.sweep();
|
|
32662
|
+
}
|
|
32663
|
+
siblings(wt, repoId = wt.repoId) {
|
|
32664
|
+
return this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree" && w.id !== wt.id);
|
|
32665
|
+
}
|
|
32666
|
+
otherProjects(wt) {
|
|
32667
|
+
return this.d.state.repos.filter((r) => r.id !== wt.repoId);
|
|
32668
|
+
}
|
|
32669
|
+
resolveProject(wt, project) {
|
|
32670
|
+
const q = project.trim();
|
|
32671
|
+
const others = this.otherProjects(wt);
|
|
32672
|
+
if (others.length === 0)
|
|
32673
|
+
return { refused: "no other project is open in Toyon." };
|
|
32674
|
+
const names = others.map((r) => r.name).join(", ");
|
|
32675
|
+
if (!q)
|
|
32676
|
+
return { refused: `name the project. Open projects: ${names}.` };
|
|
32677
|
+
const found = matchRepos(others, q);
|
|
32678
|
+
if (found.length === 1)
|
|
32679
|
+
return { repo: found[0] };
|
|
32680
|
+
if (found.length > 1) {
|
|
32681
|
+
return {
|
|
32682
|
+
refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
|
|
32683
|
+
};
|
|
32684
|
+
}
|
|
32685
|
+
const own = this.d.state.repo(wt.repoId);
|
|
32686
|
+
if (own && matchRepos([own], q).length === 1) {
|
|
32687
|
+
return { refused: `${own.name} is this worktree's own project; leave \`project\` out for a worktree here.` };
|
|
32688
|
+
}
|
|
32689
|
+
return { refused: `no open project is called "${q}". Open projects: ${names}.` };
|
|
32690
|
+
}
|
|
32691
|
+
tool(worktreeId) {
|
|
32692
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
32693
|
+
const names = wt ? this.siblings(wt).map((w) => `${w.branch}, "${w.title}"`) : [];
|
|
32694
|
+
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.";
|
|
32695
|
+
const projects = wt ? this.otherProjects(wt).map((r) => r.name) : [];
|
|
32696
|
+
const elsewhere = projects.length ? ` Other projects open in Toyon you can name in \`project\`: ${projects.join(", ")}.` : "";
|
|
32697
|
+
return {
|
|
32698
|
+
name: SPLIT_TOOL,
|
|
32699
|
+
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}`,
|
|
32700
|
+
inputSchema: {
|
|
32701
|
+
type: "object",
|
|
32702
|
+
properties: {
|
|
32703
|
+
pieces: {
|
|
32704
|
+
type: "array",
|
|
32705
|
+
minItems: 1,
|
|
32706
|
+
maxItems: PIECES_MAX,
|
|
32707
|
+
items: {
|
|
32708
|
+
type: "object",
|
|
32709
|
+
properties: {
|
|
32710
|
+
title: {
|
|
32711
|
+
type: "string",
|
|
32712
|
+
description: `A name for the piece, the way a teammate would say it: ${NAME_SHAPE}. It titles the worktree.`
|
|
32713
|
+
},
|
|
32714
|
+
plan: {
|
|
32715
|
+
type: "string",
|
|
32716
|
+
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."
|
|
32717
|
+
},
|
|
32718
|
+
attachments: {
|
|
32719
|
+
type: "array",
|
|
32720
|
+
items: { type: "string" },
|
|
32721
|
+
description: 'The attachments from this chat the piece needs, by their labels as captioned: "Image 2", "Paste 1".'
|
|
32722
|
+
},
|
|
32723
|
+
where: {
|
|
32724
|
+
type: "string",
|
|
32725
|
+
enum: ["here", "worktree", "send"],
|
|
32726
|
+
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."
|
|
32727
|
+
},
|
|
32728
|
+
after: {
|
|
32729
|
+
type: "string",
|
|
32730
|
+
description: 'worktree only: what the piece waits for before it starts: a branch, another piece of this list by its title, or "this" for your own branch.'
|
|
32731
|
+
},
|
|
32732
|
+
to: { type: "string", description: "send only: the open worktree it goes to, by branch or title." },
|
|
32733
|
+
project: {
|
|
32734
|
+
type: "string",
|
|
32735
|
+
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."
|
|
32736
|
+
}
|
|
32737
|
+
},
|
|
32738
|
+
required: ["title", "plan", "where"],
|
|
32739
|
+
additionalProperties: false
|
|
32740
|
+
}
|
|
32741
|
+
}
|
|
32742
|
+
},
|
|
32743
|
+
required: ["pieces"],
|
|
32744
|
+
additionalProperties: false
|
|
32745
|
+
}
|
|
32746
|
+
};
|
|
32747
|
+
}
|
|
32748
|
+
async call(worktreeId, args) {
|
|
32749
|
+
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.`;
|
|
32750
|
+
const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
|
|
32751
|
+
const list = a && Array.isArray(a.pieces) ? a.pieces : null;
|
|
32752
|
+
if (!list || list.length === 0)
|
|
32753
|
+
return refused(shape);
|
|
32754
|
+
const pieces = [];
|
|
32755
|
+
for (const raw of list) {
|
|
32756
|
+
const p = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
32757
|
+
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");
|
|
32758
|
+
if (!shaped)
|
|
32759
|
+
return refused(shape);
|
|
32760
|
+
pieces.push({
|
|
32761
|
+
title: p.title,
|
|
32762
|
+
plan: p.plan,
|
|
32763
|
+
where: p.where,
|
|
32764
|
+
...p.attachments ? { attachments: p.attachments } : {},
|
|
32765
|
+
...p.after !== undefined ? { after: p.after } : {},
|
|
32766
|
+
...p.to !== undefined ? { to: p.to } : {},
|
|
32767
|
+
...p.project !== undefined ? { project: p.project } : {}
|
|
32768
|
+
});
|
|
32769
|
+
}
|
|
32770
|
+
return this.propose(worktreeId, pieces, "agent");
|
|
32771
|
+
}
|
|
32772
|
+
async propose(worktreeId, raw, by = "agent", read) {
|
|
32773
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
32774
|
+
if (!wt)
|
|
32775
|
+
throw new UserError("that chat is gone");
|
|
32776
|
+
if (raw.length > PIECES_MAX)
|
|
32777
|
+
return refused(`At most ${PIECES_MAX} pieces in one list; fold the rest into \`here\`.`);
|
|
32778
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
32779
|
+
const entries = agent.transcript();
|
|
32780
|
+
const pieces = [];
|
|
32781
|
+
for (const [i, p] of raw.entries()) {
|
|
32782
|
+
const title = sentenceCase(p.title.trim());
|
|
32783
|
+
const plan = p.plan.trim();
|
|
32784
|
+
if (!title)
|
|
32785
|
+
return refused(`Piece ${i + 1} has no title.`);
|
|
32786
|
+
const over = overNameShape(title);
|
|
32787
|
+
if (over)
|
|
32788
|
+
return refused(`Piece ${i + 1}'s title ${over}`);
|
|
32789
|
+
if (!plan)
|
|
32790
|
+
return refused(`Piece ${i + 1} ("${title}") has no plan; write the plan its agent builds from.`);
|
|
32791
|
+
if (plan.length > PLAN_MAX) {
|
|
32792
|
+
return refused(`Piece ${i + 1}'s plan is over ${PLAN_MAX} characters; cut it to what its agent needs.`);
|
|
32793
|
+
}
|
|
32794
|
+
if ((p.attachments?.length ?? 0) > ATTACH_MAX) {
|
|
32795
|
+
return refused(`Piece ${i + 1} names more than ${ATTACH_MAX} attachments.`);
|
|
32796
|
+
}
|
|
32797
|
+
if ((p.project?.length ?? 0) > PROJECT_MAX)
|
|
32798
|
+
return refused(`Piece ${i + 1}'s project name is too long.`);
|
|
32799
|
+
const attachments = [];
|
|
32800
|
+
for (const label of p.attachments ?? []) {
|
|
32801
|
+
const ref = attachmentNamed(entries, label);
|
|
32802
|
+
if (!ref)
|
|
32803
|
+
return refused(`This chat has no attachment "${label.trim()}"; name them as the messages caption them.`);
|
|
32804
|
+
attachments.push(ref);
|
|
32805
|
+
}
|
|
32806
|
+
const piece = { id: `p${i + 1}`, title, plan, attachments, fate: p.where };
|
|
32807
|
+
if (p.project !== undefined) {
|
|
32808
|
+
if (p.where === "here")
|
|
32809
|
+
return refused(`Piece ${i + 1} ("${title}") is \`here\` and names a project.`);
|
|
32810
|
+
const found = this.resolveProject(wt, p.project);
|
|
32811
|
+
if ("refused" in found)
|
|
32812
|
+
return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
|
|
32813
|
+
piece.project = { id: found.repo.id, name: found.repo.name };
|
|
32814
|
+
}
|
|
32815
|
+
if (p.where !== "worktree" && p.after !== undefined) {
|
|
32816
|
+
return refused(`Piece ${i + 1} ("${title}") has \`after\`, which only a \`worktree\` piece takes.`);
|
|
32817
|
+
}
|
|
32818
|
+
if (p.where !== "send" && p.to !== undefined) {
|
|
32819
|
+
return refused(`Piece ${i + 1} ("${title}") has \`to\`, which only a \`send\` piece takes.`);
|
|
32820
|
+
}
|
|
32821
|
+
if (p.where === "worktree" && p.after !== undefined) {
|
|
32822
|
+
const q = p.after.trim();
|
|
32823
|
+
if (!q)
|
|
32824
|
+
return refused(`Piece ${i + 1} ("${title}") has a blank \`after\`; name what it waits for, or leave it out.`);
|
|
32825
|
+
const after = this.resolveAfter(wt, raw, i, q);
|
|
32826
|
+
if ("refused" in after)
|
|
32827
|
+
return refused(`Piece ${i + 1} ("${title}"): ${after.refused}`);
|
|
32828
|
+
piece.after = after.after;
|
|
32829
|
+
}
|
|
32830
|
+
if (p.where === "send") {
|
|
32831
|
+
const q = p.to?.trim() ?? "";
|
|
32832
|
+
if (!q)
|
|
32833
|
+
return refused(`Piece ${i + 1} ("${title}") is \`send\` and names no worktree in \`to\`.`);
|
|
32834
|
+
const found = this.named(this.siblings(wt, piece.project?.id).filter((w) => !w.landed), q, piece.project?.name);
|
|
32835
|
+
if ("refused" in found)
|
|
32836
|
+
return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
|
|
32837
|
+
piece.to = found.worktree;
|
|
32838
|
+
}
|
|
32839
|
+
pieces.push(piece);
|
|
32840
|
+
}
|
|
32841
|
+
const waiting = heldUnshown(entries);
|
|
32842
|
+
const replaced = openPieces(entries).filter((o) => by === "agent" && o.id === waiting?.id && o.by !== "toyon");
|
|
32843
|
+
if (openPieces(entries).some((o) => !replaced.includes(o))) {
|
|
32844
|
+
return refused("A pieces list is already waiting for the person on this chat; finish your turn.");
|
|
32845
|
+
}
|
|
32846
|
+
if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
|
|
32847
|
+
return refused(`${PER_TURN_MAX} proposals were made this turn already; finish your turn and let the person answer.`);
|
|
32848
|
+
}
|
|
32849
|
+
for (const o of replaced) {
|
|
32850
|
+
agent.note({ type: "pieces-declined", id: o.id, reason: "handed over again", ts: Date.now() });
|
|
32851
|
+
}
|
|
32852
|
+
const held = by !== "person" && wt.mode === "plan";
|
|
32853
|
+
const plan = held ? null : writePlanDocNow(wt.path, listDoc(pieces));
|
|
32854
|
+
agent.note({
|
|
32855
|
+
type: "pieces-proposed",
|
|
32856
|
+
id: randomUUID2(),
|
|
32857
|
+
pieces,
|
|
32858
|
+
mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
|
|
32859
|
+
...held ? { held: true } : {},
|
|
32860
|
+
...plan ? { plan } : {},
|
|
32861
|
+
...by === "toyon" ? { by: "toyon" } : {},
|
|
32862
|
+
...by === "toyon" && read ? { read } : {},
|
|
32863
|
+
ts: Date.now()
|
|
32864
|
+
});
|
|
32865
|
+
return { text: held ? HELD : SHOWN };
|
|
32866
|
+
}
|
|
32867
|
+
resolveAfter(wt, raw, at, q) {
|
|
32868
|
+
const bare = q.startsWith("@") ? q.slice(1) : q;
|
|
32869
|
+
const titled = raw.map((p, i) => ({ i, title: p.title.trim() }));
|
|
32870
|
+
const onList = matchBy(titled.filter((p) => p.i < at), bare, [(p) => p.title]);
|
|
32871
|
+
if (onList.length === 1)
|
|
32872
|
+
return { after: { piece: `p${onList[0].i + 1}` } };
|
|
32873
|
+
if (onList.length > 1)
|
|
32874
|
+
return { refused: `"${q}" names more than one piece on the list; give them different titles.` };
|
|
32875
|
+
if (matchBy(titled.filter((p) => p.i >= at), bare, [(p) => p.title]).length > 0) {
|
|
32876
|
+
return { refused: `"${q}" is this piece or a later one; a piece waits only for one earlier on the list.` };
|
|
32877
|
+
}
|
|
32878
|
+
if (bare === "this" || bare === wt.branch)
|
|
32879
|
+
return { after: { id: wt.id, branch: wt.branch, title: wt.title } };
|
|
32880
|
+
const found = this.named(this.siblings(wt), bare);
|
|
32881
|
+
if ("refused" in found)
|
|
32882
|
+
return found;
|
|
32883
|
+
return { after: found.worktree };
|
|
32884
|
+
}
|
|
32885
|
+
named(among, q, where = "this project") {
|
|
32886
|
+
const bare = q.startsWith("@") ? q.slice(1) : q;
|
|
32887
|
+
const list = among.map((w) => `${w.branch}, "${w.title}"`).join("; ");
|
|
32888
|
+
if (among.length === 0)
|
|
32889
|
+
return { refused: `no other worktree of ${where} is open.` };
|
|
32890
|
+
const found = matchBy(among, bare, [(w) => w.branch, (w) => w.title]);
|
|
32891
|
+
if (found.length === 1) {
|
|
32892
|
+
const w = found[0];
|
|
32893
|
+
return { worktree: { id: w.id, branch: w.branch, title: w.title } };
|
|
32894
|
+
}
|
|
32895
|
+
if (found.length > 1) {
|
|
32896
|
+
return {
|
|
32897
|
+
refused: `"${q}" names more than one worktree: ${found.map((w) => w.branch).join(", ")}. Say which by its branch.`
|
|
32898
|
+
};
|
|
32899
|
+
}
|
|
32900
|
+
return { refused: `no open worktree of ${where} is called "${q}". Open: ${list}.` };
|
|
32901
|
+
}
|
|
32902
|
+
fate(worktreeId, id, pieceId, fate) {
|
|
32903
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
32904
|
+
if (!wt)
|
|
32905
|
+
throw new UserError("that chat is gone");
|
|
32906
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
32907
|
+
const proposed = openPieces(agent.transcript()).find((p) => p.id === id);
|
|
32908
|
+
if (!proposed)
|
|
32909
|
+
throw new UserError("That list is not open in this chat");
|
|
32910
|
+
const piece = proposed.pieces.find((p) => p.id === pieceId);
|
|
32911
|
+
if (!piece)
|
|
32912
|
+
throw new UserError("That piece is not on the list");
|
|
32913
|
+
if (fate === "send" && !piece.to)
|
|
32914
|
+
throw new UserError("That piece has no worktree to send to");
|
|
32915
|
+
if (piece.fate === "send" && (fate === "here" || fate === "worktree")) {
|
|
32916
|
+
throw new UserError("A piece for an open worktree is sent there or skipped");
|
|
32917
|
+
}
|
|
32918
|
+
agent.note({ type: "pieces-fate", id, pieceId, fate, ts: Date.now() });
|
|
32919
|
+
}
|
|
32920
|
+
async answer(worktreeId, id, go, note) {
|
|
32921
|
+
if (!this.d.state.worktree(worktreeId))
|
|
32922
|
+
throw new UserError("that chat is gone");
|
|
32923
|
+
if (go)
|
|
32924
|
+
await this.act(worktreeId, id, note?.trim() || undefined);
|
|
32925
|
+
else
|
|
32926
|
+
await this.decline(worktreeId, id);
|
|
32927
|
+
}
|
|
32928
|
+
async decline(worktreeId, id, reason) {
|
|
32929
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
32930
|
+
if (!wt)
|
|
32931
|
+
return;
|
|
32932
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
32933
|
+
if (!openPieces(agent.transcript()).some((p) => p.id === id)) {
|
|
32934
|
+
return log.debug(worktreeId, `decline for a pieces list that already closed (${id})`);
|
|
32935
|
+
}
|
|
32936
|
+
agent.note({
|
|
32937
|
+
type: "pieces-declined",
|
|
32938
|
+
id,
|
|
32939
|
+
...reason ? { outcome: "failed", reason } : {},
|
|
32940
|
+
ts: Date.now()
|
|
32941
|
+
});
|
|
32942
|
+
}
|
|
32943
|
+
async act(worktreeId, id, note, tell = false) {
|
|
32944
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
32945
|
+
if (!wt)
|
|
32946
|
+
return;
|
|
32947
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
32948
|
+
const entries = agent.transcript();
|
|
32949
|
+
const proposed = openPieces(entries).find((p) => p.id === id);
|
|
32950
|
+
if (!proposed)
|
|
32951
|
+
return log.debug(worktreeId, `go for a pieces list that already closed (${id})`);
|
|
32952
|
+
agent.note({ type: "pieces-go", id, ts: Date.now() });
|
|
32953
|
+
try {
|
|
32954
|
+
const repo = this.d.state.repo(wt.repoId);
|
|
32955
|
+
if (!repo)
|
|
32956
|
+
throw new UserError(`${wt.title}'s project is no longer open in Toyon`);
|
|
32957
|
+
const origin = this.originOf(wt, repo.name, listFile(entries, id));
|
|
32958
|
+
const outcomes = [];
|
|
32959
|
+
const became = new Map;
|
|
32960
|
+
for (const piece of proposed.pieces) {
|
|
32961
|
+
const fate = effectiveFate(entries, proposed, piece);
|
|
32962
|
+
try {
|
|
32963
|
+
outcomes.push(await this.actOn(wt, origin, proposed, piece, fate, note, became));
|
|
32964
|
+
} catch (e) {
|
|
32965
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
32966
|
+
log.warn(worktreeId, `piece "${piece.title}" (${fate}) failed`, e);
|
|
32967
|
+
became.set(piece.id, { fate: "skip" });
|
|
32968
|
+
outcomes.push({ pieceId: piece.id, fate, error });
|
|
32969
|
+
}
|
|
32970
|
+
}
|
|
32971
|
+
agent.note({ type: "pieces", id, outcomes, ts: Date.now() });
|
|
32972
|
+
if (tell && outcomes.some((o) => o.fate !== "here")) {
|
|
32973
|
+
for (let i = 0;i < 30 && openPlanCard(agent.transcript()); i++)
|
|
32974
|
+
await Bun.sleep(100);
|
|
32975
|
+
await Bun.sleep(100);
|
|
32976
|
+
agent.send(piecesActedPrompt(proposed.pieces, outcomes), {
|
|
32977
|
+
asked: { kind: "pieces", why: "what the split started, so the agent builds only its own piece" },
|
|
32978
|
+
keepCards: true
|
|
32979
|
+
});
|
|
32980
|
+
}
|
|
32981
|
+
} catch (e) {
|
|
32982
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
32983
|
+
agent.note({ type: "pieces-declined", id, outcome: "failed", reason, ts: Date.now() });
|
|
32984
|
+
throw e;
|
|
32985
|
+
}
|
|
32986
|
+
}
|
|
32987
|
+
async read(worktreeId, markdown, planPath) {
|
|
32988
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
32989
|
+
if (!wt)
|
|
32990
|
+
return;
|
|
32991
|
+
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
32992
|
+
if (openPieces(agent.transcript()).length > 0)
|
|
32993
|
+
return;
|
|
32994
|
+
const found = await this.d.reader(wt, markdown);
|
|
32995
|
+
if (!found || found.length < 2)
|
|
32996
|
+
return;
|
|
32997
|
+
const card = openPlanCard(agent.transcript());
|
|
32998
|
+
if (!card || card.plan !== planPath || openPieces(agent.transcript()).length > 0)
|
|
32999
|
+
return;
|
|
33000
|
+
await this.proposeRead(wt, markdown, found);
|
|
33001
|
+
}
|
|
33002
|
+
async proposeRead(wt, markdown, found) {
|
|
33003
|
+
const siblings = this.siblings(wt);
|
|
33004
|
+
const known = (q) => q === "this" || q === wt.branch || !("refused" in this.named(siblings, q.startsWith("@") ? q.slice(1) : q));
|
|
33005
|
+
const sections = new Map;
|
|
33006
|
+
for (const p of found)
|
|
33007
|
+
sections.set(p.section, (sections.get(p.section) ?? 0) + 1);
|
|
33008
|
+
const raw = found.map((p) => {
|
|
33009
|
+
const own = sections.get(p.section) === 1 ? sectionText(markdown, p.section) : null;
|
|
33010
|
+
const plan = own ?? p.plan ?? sectionText(markdown, p.section) ?? `${p.title}
|
|
33011
|
+
|
|
33012
|
+
${p.section}`;
|
|
33013
|
+
const after = p.where === "worktree" && p.after && known(p.after) ? p.after : undefined;
|
|
33014
|
+
return { title: p.title, plan, where: p.where, ...after ? { after } : {} };
|
|
33015
|
+
});
|
|
33016
|
+
const r = await this.propose(wt.id, raw, "toyon", hashText(markdown));
|
|
33017
|
+
if (r.isError) {
|
|
33018
|
+
log.warn(wt.id, `the list read off the plan was refused: ${r.text}`);
|
|
33019
|
+
return null;
|
|
33020
|
+
}
|
|
33021
|
+
const agent = this.d.runtime.agentFor(wt.id);
|
|
33022
|
+
return agent ? openPieces(agent.transcript()).find((p) => p.by === "toyon") ?? null : null;
|
|
33023
|
+
}
|
|
33024
|
+
async stale(worktreeId) {
|
|
33025
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
33026
|
+
if (wt?.mode !== "plan")
|
|
33027
|
+
return;
|
|
33028
|
+
const agent = this.d.runtime.agentFor(worktreeId);
|
|
33029
|
+
if (!agent)
|
|
33030
|
+
return;
|
|
33031
|
+
const entries = agent.transcript();
|
|
33032
|
+
const open = heldUnshown(entries);
|
|
33033
|
+
if (!open)
|
|
33034
|
+
return;
|
|
33035
|
+
if (entries.some(({ event }) => event.type === "pieces-stale" && event.id === open.id))
|
|
33036
|
+
return;
|
|
33037
|
+
const card = openPlanCard(entries);
|
|
33038
|
+
if (!card?.plan || !card.detail)
|
|
33039
|
+
return;
|
|
33040
|
+
let markdown;
|
|
33041
|
+
try {
|
|
33042
|
+
markdown = readFileSync20(join28(wt.path, card.plan), "utf8");
|
|
33043
|
+
} catch (e) {
|
|
33044
|
+
log.warn(worktreeId, `could not read ${card.plan} to see whether the plan moved`, e);
|
|
33045
|
+
return;
|
|
33046
|
+
}
|
|
33047
|
+
const moved = open.read ? hashText(markdown) !== open.read : planEdited(wt.path, card.plan, card.detail);
|
|
33048
|
+
if (!moved)
|
|
33049
|
+
return;
|
|
33050
|
+
agent.note({ type: "pieces-stale", id: open.id, ts: Date.now() });
|
|
33051
|
+
if (open.by !== "toyon")
|
|
33052
|
+
return;
|
|
33053
|
+
const found = await this.d.reader(wt, markdown);
|
|
33054
|
+
if (!found || found.length < 2)
|
|
33055
|
+
return;
|
|
33056
|
+
if (openPlanCard(agent.transcript())?.id !== card.id)
|
|
33057
|
+
return;
|
|
33058
|
+
const pressed = new Map;
|
|
33059
|
+
for (const { event } of agent.transcript()) {
|
|
33060
|
+
if (event.type === "pieces-fate" && event.id === open.id) {
|
|
33061
|
+
const title = open.pieces.find((p) => p.id === event.pieceId)?.title;
|
|
33062
|
+
if (title)
|
|
33063
|
+
pressed.set(title.toLowerCase(), event.fate);
|
|
33064
|
+
}
|
|
33065
|
+
}
|
|
33066
|
+
agent.note({ type: "pieces-declined", id: open.id, reason: "the plan changed; read again", ts: Date.now() });
|
|
33067
|
+
const fresh = await this.proposeRead(wt, markdown, found);
|
|
33068
|
+
if (!fresh)
|
|
33069
|
+
return;
|
|
33070
|
+
for (const piece of fresh.pieces) {
|
|
33071
|
+
const fate = pressed.get(piece.title.toLowerCase());
|
|
33072
|
+
if (fate && fate !== piece.fate && !(fate === "send" && !piece.to)) {
|
|
33073
|
+
agent.note({ type: "pieces-fate", id: fresh.id, pieceId: piece.id, fate, ts: Date.now() });
|
|
33074
|
+
}
|
|
33075
|
+
}
|
|
33076
|
+
}
|
|
33077
|
+
async ask(worktreeId, repoId, text) {
|
|
33078
|
+
const wt = this.d.state.requireWorktree(worktreeId);
|
|
33079
|
+
const repo = this.d.state.repo(repoId);
|
|
33080
|
+
if (!repo)
|
|
33081
|
+
throw new UserError("no such project");
|
|
33082
|
+
const here = repo.id === wt.repoId;
|
|
33083
|
+
const agent = this.d.runtime.agentFor(worktreeId);
|
|
33084
|
+
if (!agent)
|
|
33085
|
+
throw new UserError("worktree still starting; try again in a moment");
|
|
33086
|
+
if (agent.mcpTools === null)
|
|
33087
|
+
await agent.warm();
|
|
33088
|
+
if (agent.mcpTools === true) {
|
|
33089
|
+
agent.send(pieceAskPrompt(text, here ? undefined : repo), {
|
|
33090
|
+
asked: { kind: "pieces", why: here ? "a new worktree here" : `a new worktree in ${repo.name}` }
|
|
33091
|
+
});
|
|
33092
|
+
return;
|
|
33093
|
+
}
|
|
33094
|
+
const own = text?.trim();
|
|
33095
|
+
if (!own)
|
|
33096
|
+
throw new UserError(`Write what ${here ? "the new worktree" : repo.name} should do first`);
|
|
33097
|
+
const r = await this.propose(worktreeId, [{ title: titleFrom(own), plan: own, where: "worktree", ...here ? {} : { project: repo.path } }], "person");
|
|
33098
|
+
if (r.isError)
|
|
33099
|
+
throw new UserError(r.text);
|
|
33100
|
+
}
|
|
33101
|
+
originOf(wt, project, file) {
|
|
33102
|
+
return { path: wt.path, project, branch: wt.branch, ...file ? { plan: file } : {} };
|
|
33103
|
+
}
|
|
33104
|
+
async actOn(wt, origin, proposed, piece, fate, note, became) {
|
|
33105
|
+
const crossed = piece.project ? { repoName: piece.project.name } : {};
|
|
33106
|
+
if (fate === "here" || fate === "skip") {
|
|
33107
|
+
became.set(piece.id, { fate });
|
|
33108
|
+
return { pieceId: piece.id, fate };
|
|
33109
|
+
}
|
|
33110
|
+
if (fate === "send") {
|
|
33111
|
+
const target = piece.to ? this.d.state.worktree(piece.to.id) : undefined;
|
|
33112
|
+
const repoId = piece.project?.id ?? wt.repoId;
|
|
33113
|
+
if (target?.kind !== "worktree" || target.repoId !== repoId) {
|
|
33114
|
+
throw new UserError(`"${piece.to?.title ?? "the worktree"}" is no longer open`);
|
|
33115
|
+
}
|
|
33116
|
+
const attachments = await this.reattachAll(wt.id, piece.attachments);
|
|
33117
|
+
this.d.runtime.ensureAgent(target).agent.send(pieceSendPrompt(piece.plan, note, origin), {
|
|
33118
|
+
keepCards: true,
|
|
33119
|
+
...attachments.length ? { attachments } : {},
|
|
33120
|
+
handed: {
|
|
33121
|
+
via: "message",
|
|
33122
|
+
body: piece.plan,
|
|
33123
|
+
origin: handedOrigin(wt, piece.project ? origin.project : undefined),
|
|
33124
|
+
...note ? { note } : {}
|
|
33125
|
+
}
|
|
33126
|
+
});
|
|
33127
|
+
became.set(piece.id, { fate, worktree: { id: target.id, branch: target.branch, title: target.title } });
|
|
33128
|
+
return { pieceId: piece.id, fate, worktreeId: target.id, title: target.title, ...crossed };
|
|
33129
|
+
}
|
|
33130
|
+
const waits = this.waitsFor(wt, piece, became);
|
|
33131
|
+
if (waits) {
|
|
33132
|
+
const queued = {
|
|
33133
|
+
proposalId: proposed.id,
|
|
33134
|
+
pieceId: piece.id,
|
|
33135
|
+
title: piece.title,
|
|
33136
|
+
plan: piece.plan,
|
|
33137
|
+
after: waits,
|
|
33138
|
+
...piece.project ? { project: piece.project } : {},
|
|
33139
|
+
attachments: piece.attachments,
|
|
33140
|
+
...note ? { note } : {},
|
|
33141
|
+
queuedAt: Date.now()
|
|
33142
|
+
};
|
|
33143
|
+
const live = this.d.state.requireWorktree(wt.id);
|
|
33144
|
+
live.queued = [...live.queued ?? [], queued];
|
|
33145
|
+
this.d.state.save();
|
|
33146
|
+
this.d.hub.emit("worktreesChanged");
|
|
33147
|
+
became.set(piece.id, { fate, queued: true });
|
|
33148
|
+
const after = "piece" in waits ? proposed.pieces.find((p) => p.id === waits.piece)?.title ?? waits.piece : waits.branch;
|
|
33149
|
+
return { pieceId: piece.id, fate, after, ...crossed };
|
|
33150
|
+
}
|
|
33151
|
+
const made = await this.make(wt, origin, piece, note);
|
|
33152
|
+
became.set(piece.id, { fate, worktree: { id: made.id, branch: made.branch, title: made.title } });
|
|
33153
|
+
return { pieceId: piece.id, fate, worktreeId: made.id, title: made.title, ...crossed };
|
|
33154
|
+
}
|
|
33155
|
+
waitsFor(wt, piece, became) {
|
|
33156
|
+
const after = piece.after;
|
|
33157
|
+
if (!after)
|
|
33158
|
+
return null;
|
|
33159
|
+
if (!("piece" in after)) {
|
|
33160
|
+
const waits = this.d.state.worktree(after.id);
|
|
33161
|
+
return waits && waits.kind === "worktree" && !waits.landed ? after : null;
|
|
33162
|
+
}
|
|
33163
|
+
const on = became.get(after.piece);
|
|
33164
|
+
if (!on)
|
|
33165
|
+
return null;
|
|
33166
|
+
if (on.queued)
|
|
33167
|
+
return { piece: after.piece };
|
|
33168
|
+
if (on.worktree)
|
|
33169
|
+
return on.worktree;
|
|
33170
|
+
if (on.fate === "here")
|
|
33171
|
+
return { id: wt.id, branch: wt.branch, title: wt.title };
|
|
33172
|
+
return null;
|
|
33173
|
+
}
|
|
33174
|
+
async make(origin, at, piece, note, after) {
|
|
33175
|
+
const repoId = piece.project?.id ?? origin.repoId;
|
|
33176
|
+
if (piece.project && !this.d.state.repo(repoId)) {
|
|
33177
|
+
throw new UserError(`${piece.project.name} is no longer open in Toyon`);
|
|
33178
|
+
}
|
|
33179
|
+
const attachments = await this.reattachAll(origin.id, piece.attachments);
|
|
33180
|
+
const mode = origin.mode === "plan" ? "auto" : origin.mode;
|
|
33181
|
+
const via = piece.project ? "project" : "piece";
|
|
33182
|
+
return this.d.create(repoId, piece.title, {
|
|
33183
|
+
title: piece.title,
|
|
33184
|
+
...mode ? { mode } : {},
|
|
33185
|
+
...attachments.length ? { attachments } : {},
|
|
33186
|
+
from: {
|
|
33187
|
+
kind: "worktree",
|
|
33188
|
+
ref: origin.branch,
|
|
33189
|
+
origin: { id: origin.id, repoId: origin.repoId },
|
|
33190
|
+
...at.plan ? { plan: at.plan } : {},
|
|
33191
|
+
...after ? { after } : {}
|
|
33192
|
+
},
|
|
33193
|
+
plan: {
|
|
33194
|
+
markdown: pieceDoc(piece),
|
|
33195
|
+
handed: {
|
|
33196
|
+
via: "plan",
|
|
33197
|
+
body: piece.plan,
|
|
33198
|
+
origin: handedOrigin(origin, piece.project ? at.project : undefined),
|
|
33199
|
+
...note ? { note } : {}
|
|
33200
|
+
},
|
|
33201
|
+
prompt: (path) => path ? handedPrompt(path, note, at, via) : `${pieceDoc(piece)}
|
|
33202
|
+
${handedPrompt(null, note, at, via)}`
|
|
33203
|
+
},
|
|
33204
|
+
cold: true
|
|
33205
|
+
});
|
|
33206
|
+
}
|
|
33207
|
+
async reattachAll(worktreeId, refs) {
|
|
33208
|
+
const out = [];
|
|
33209
|
+
for (const ref of refs) {
|
|
33210
|
+
const again = await this.d.attachments.reattach(worktreeId, ref);
|
|
33211
|
+
if (again)
|
|
33212
|
+
out.push(again);
|
|
33213
|
+
else
|
|
33214
|
+
log.warn(worktreeId, `a piece's attachment is gone from the store (${ref.kind} ${ref.n})`);
|
|
33215
|
+
}
|
|
33216
|
+
return out;
|
|
33217
|
+
}
|
|
33218
|
+
async landed(landedId) {
|
|
33219
|
+
this.tellOrigin(landedId);
|
|
33220
|
+
const madeNow = new Set;
|
|
33221
|
+
for (const origin of this.d.state.worktrees) {
|
|
33222
|
+
const due = origin.queued?.filter((q) => !("piece" in q.after) && q.after.id === landedId) ?? [];
|
|
33223
|
+
if (due.length === 0)
|
|
33224
|
+
continue;
|
|
33225
|
+
const repo = this.d.state.repo(origin.repoId);
|
|
33226
|
+
origin.queued = origin.queued?.filter((q) => !due.includes(q));
|
|
33227
|
+
if (origin.queued?.length === 0)
|
|
33228
|
+
delete origin.queued;
|
|
33229
|
+
this.d.state.save();
|
|
33230
|
+
this.d.hub.emit("worktreesChanged");
|
|
33231
|
+
const agent = this.d.runtime.ensureAgent(origin).agent;
|
|
33232
|
+
for (const q of due) {
|
|
33233
|
+
try {
|
|
33234
|
+
if (!repo)
|
|
33235
|
+
throw new UserError("the project is no longer open in Toyon");
|
|
33236
|
+
const at = this.originOf(origin, repo.name, listFile(agent.transcript(), q.proposalId));
|
|
33237
|
+
const made = await this.make(origin, at, q, q.note, "piece" in q.after ? undefined : q.after);
|
|
33238
|
+
agent.note({
|
|
33239
|
+
type: "piece-made",
|
|
33240
|
+
id: q.proposalId,
|
|
33241
|
+
pieceId: q.pieceId,
|
|
33242
|
+
worktreeId: made.id,
|
|
33243
|
+
title: made.title,
|
|
33244
|
+
ts: Date.now()
|
|
33245
|
+
});
|
|
33246
|
+
madeNow.add(made.id);
|
|
33247
|
+
this.repoint(origin, q, { id: made.id, branch: made.branch, title: made.title });
|
|
33248
|
+
} catch (e) {
|
|
33249
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
33250
|
+
log.warn(origin.id, `queued piece "${q.title}" could not be made`, e);
|
|
33251
|
+
agent.note({ type: "piece-dropped", id: q.proposalId, pieceId: q.pieceId, reason, ts: Date.now() });
|
|
33252
|
+
this.dropDependents(origin, q);
|
|
33253
|
+
}
|
|
33254
|
+
}
|
|
33255
|
+
}
|
|
33256
|
+
this.tellWaiters(landedId, madeNow);
|
|
33257
|
+
}
|
|
33258
|
+
repoint(origin, made, worktree) {
|
|
33259
|
+
const live = this.d.state.worktree(origin.id);
|
|
33260
|
+
if (!live?.queued)
|
|
33261
|
+
return;
|
|
33262
|
+
let changed = false;
|
|
33263
|
+
for (const q of live.queued) {
|
|
33264
|
+
if (q.proposalId === made.proposalId && "piece" in q.after && q.after.piece === made.pieceId) {
|
|
33265
|
+
q.after = worktree;
|
|
33266
|
+
changed = true;
|
|
33267
|
+
}
|
|
33268
|
+
}
|
|
33269
|
+
if (changed) {
|
|
33270
|
+
this.d.state.save();
|
|
33271
|
+
this.d.hub.emit("worktreesChanged");
|
|
33272
|
+
}
|
|
33273
|
+
}
|
|
33274
|
+
dropDependents(origin, dropped) {
|
|
33275
|
+
const live = this.d.state.worktree(origin.id);
|
|
33276
|
+
if (!live?.queued)
|
|
33277
|
+
return;
|
|
33278
|
+
const gone = live.queued.filter((q) => q.proposalId === dropped.proposalId && ("piece" in q.after) && q.after.piece === dropped.pieceId);
|
|
33279
|
+
if (gone.length === 0)
|
|
33280
|
+
return;
|
|
33281
|
+
live.queued = live.queued.filter((q) => !gone.includes(q));
|
|
33282
|
+
if (live.queued.length === 0)
|
|
33283
|
+
delete live.queued;
|
|
33284
|
+
this.d.state.save();
|
|
33285
|
+
this.d.hub.emit("worktreesChanged");
|
|
33286
|
+
const agent = this.d.runtime.ensureAgent(live).agent;
|
|
33287
|
+
for (const q of gone) {
|
|
33288
|
+
agent.note({
|
|
33289
|
+
type: "piece-dropped",
|
|
33290
|
+
id: q.proposalId,
|
|
33291
|
+
pieceId: q.pieceId,
|
|
33292
|
+
reason: `the piece it waited for, "${dropped.title}", was dropped`,
|
|
33293
|
+
ts: Date.now()
|
|
33294
|
+
});
|
|
33295
|
+
this.dropDependents(live, q);
|
|
33296
|
+
}
|
|
33297
|
+
}
|
|
33298
|
+
tellOrigin(landedId) {
|
|
33299
|
+
const wt = this.d.state.worktree(landedId);
|
|
33300
|
+
if (wt?.from?.kind !== "worktree")
|
|
33301
|
+
return;
|
|
33302
|
+
const origin = this.d.state.worktree(wt.from.origin.id);
|
|
33303
|
+
if (origin?.kind !== "worktree")
|
|
33304
|
+
return;
|
|
33305
|
+
const repo = this.d.state.repo(wt.repoId);
|
|
33306
|
+
if (!repo)
|
|
33307
|
+
return;
|
|
33308
|
+
this.d.runtime.ensureAgent(origin).agent.note({
|
|
33309
|
+
type: "piece-landed",
|
|
33310
|
+
worktreeId: wt.id,
|
|
33311
|
+
repoName: repo.name,
|
|
33312
|
+
title: wt.title,
|
|
33313
|
+
...wt.pr?.url ? { url: wt.pr.url } : {},
|
|
33314
|
+
...wt.repoId === origin.repoId ? { same: true } : {},
|
|
33315
|
+
ts: Date.now()
|
|
33316
|
+
});
|
|
33317
|
+
}
|
|
33318
|
+
tellWaiters(landedId, madeNow) {
|
|
33319
|
+
const landed = this.d.state.worktree(landedId);
|
|
33320
|
+
if (!landed)
|
|
33321
|
+
return;
|
|
33322
|
+
const repo = this.d.state.repo(landed.repoId);
|
|
33323
|
+
if (!repo)
|
|
33324
|
+
return;
|
|
33325
|
+
for (const w of this.d.state.worktrees) {
|
|
33326
|
+
if (w.kind !== "worktree" || w.landed || w.id === landedId || madeNow.has(w.id))
|
|
33327
|
+
continue;
|
|
33328
|
+
if (w.from?.kind !== "worktree" || w.from.after?.id !== landedId)
|
|
33329
|
+
continue;
|
|
33330
|
+
this.d.runtime.ensureAgent(w).agent.note({
|
|
33331
|
+
type: "piece-landed",
|
|
33332
|
+
worktreeId: landed.id,
|
|
33333
|
+
repoName: repo.name,
|
|
33334
|
+
title: landed.title,
|
|
33335
|
+
...landed.pr?.url ? { url: landed.pr.url } : {},
|
|
33336
|
+
...w.repoId === landed.repoId ? { same: true } : {},
|
|
33337
|
+
again: true,
|
|
33338
|
+
ts: Date.now()
|
|
33339
|
+
});
|
|
33340
|
+
}
|
|
33341
|
+
}
|
|
33342
|
+
sweep() {
|
|
33343
|
+
let changed = false;
|
|
33344
|
+
for (const origin of this.d.state.worktrees) {
|
|
33345
|
+
if (!origin.queued?.length)
|
|
33346
|
+
continue;
|
|
33347
|
+
const live = (id) => this.d.state.worktree(id)?.kind === "worktree";
|
|
33348
|
+
const gone = origin.queued.filter((q) => !("piece" in q.after) && !live(q.after.id));
|
|
33349
|
+
if (gone.length === 0)
|
|
33350
|
+
continue;
|
|
33351
|
+
origin.queued = origin.queued.filter((q) => !gone.includes(q));
|
|
33352
|
+
if (origin.queued.length === 0)
|
|
33353
|
+
delete origin.queued;
|
|
33354
|
+
changed = true;
|
|
33355
|
+
const agent = this.d.runtime.ensureAgent(origin).agent;
|
|
33356
|
+
for (const q of gone) {
|
|
33357
|
+
const title = "piece" in q.after ? q.after.piece : q.after.title;
|
|
33358
|
+
agent.note({
|
|
33359
|
+
type: "piece-dropped",
|
|
33360
|
+
id: q.proposalId,
|
|
33361
|
+
pieceId: q.pieceId,
|
|
33362
|
+
reason: `the worktree it waited for, "${title}", is gone`,
|
|
33363
|
+
ts: Date.now()
|
|
33364
|
+
});
|
|
33365
|
+
this.dropDependents(origin, q);
|
|
33366
|
+
}
|
|
33367
|
+
}
|
|
33368
|
+
if (changed) {
|
|
33369
|
+
this.d.state.save();
|
|
33370
|
+
this.d.hub.emit("worktreesChanged");
|
|
33371
|
+
}
|
|
33372
|
+
}
|
|
33373
|
+
}
|
|
33374
|
+
function matchRepos(repos, q) {
|
|
33375
|
+
return matchBy(repos, q, [(r) => r.path, (r) => r.name]);
|
|
33376
|
+
}
|
|
33377
|
+
function attachmentNamed(entries, label) {
|
|
33378
|
+
const m = LABEL.exec(label.trim());
|
|
33379
|
+
if (!m)
|
|
33380
|
+
return null;
|
|
33381
|
+
const kind = KIND_OF[m[1].toLowerCase()];
|
|
33382
|
+
const n = Number(m[2]);
|
|
33383
|
+
for (const { event } of entries) {
|
|
33384
|
+
if (event.type !== "user-message" && event.type !== "handed")
|
|
33385
|
+
continue;
|
|
33386
|
+
const ref = event.attachments?.find((a) => a.kind === kind && a.n === n);
|
|
33387
|
+
if (ref)
|
|
33388
|
+
return ref;
|
|
33389
|
+
}
|
|
33390
|
+
return null;
|
|
33391
|
+
}
|
|
33392
|
+
|
|
32398
33393
|
// ../daemon/src/worktrees/prs.ts
|
|
32399
33394
|
var POLL_MS3 = 3 * 60000;
|
|
32400
33395
|
|
|
@@ -32503,7 +33498,7 @@ async function mergedBranches(repoPath, defaultBr) {
|
|
|
32503
33498
|
}
|
|
32504
33499
|
|
|
32505
33500
|
// ../daemon/src/worktrees/discover.ts
|
|
32506
|
-
import { createHash as
|
|
33501
|
+
import { createHash as createHash6 } from "crypto";
|
|
32507
33502
|
import { basename as basename10 } from "path";
|
|
32508
33503
|
|
|
32509
33504
|
// ../daemon/src/git/worktrees.ts
|
|
@@ -32572,7 +33567,7 @@ async function listWorktrees(repoPath) {
|
|
|
32572
33567
|
|
|
32573
33568
|
// ../daemon/src/worktrees/discover.ts
|
|
32574
33569
|
function discoveredId(path) {
|
|
32575
|
-
return `disc-${
|
|
33570
|
+
return `disc-${createHash6("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
|
|
32576
33571
|
}
|
|
32577
33572
|
function knownPaths(known) {
|
|
32578
33573
|
const paths = new Set;
|
|
@@ -32755,9 +33750,9 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
32755
33750
|
}
|
|
32756
33751
|
|
|
32757
33752
|
// ../daemon/src/worktrees/service.ts
|
|
32758
|
-
import { cpSync, existsSync as existsSync36, readFileSync as
|
|
33753
|
+
import { cpSync, existsSync as existsSync36, readFileSync as readFileSync22, rmSync as rmSync8 } from "fs";
|
|
32759
33754
|
import { rm as rm5 } from "fs/promises";
|
|
32760
|
-
import { dirname as dirname13, join as
|
|
33755
|
+
import { dirname as dirname13, join as join33 } from "path";
|
|
32761
33756
|
|
|
32762
33757
|
// ../daemon/src/git/archive.ts
|
|
32763
33758
|
import { rmSync as rmSync5 } from "fs";
|
|
@@ -33366,7 +34361,7 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
33366
34361
|
|
|
33367
34362
|
// ../daemon/src/git/turn.ts
|
|
33368
34363
|
import { copyFileSync, existsSync as existsSync33, rmSync as rmSync6 } from "fs";
|
|
33369
|
-
import { join as
|
|
34364
|
+
import { join as join29 } from "path";
|
|
33370
34365
|
var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
|
|
33371
34366
|
var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
|
|
33372
34367
|
var queue = new Map;
|
|
@@ -33384,7 +34379,7 @@ async function treeNow(wtPath, head, dirty) {
|
|
|
33384
34379
|
const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
|
|
33385
34380
|
if (!dir.ok || !dir.out)
|
|
33386
34381
|
return null;
|
|
33387
|
-
index =
|
|
34382
|
+
index = join29(dir.out, "toyon-turn-index");
|
|
33388
34383
|
}
|
|
33389
34384
|
const env = { GIT_INDEX_FILE: index };
|
|
33390
34385
|
const writeTree = async () => {
|
|
@@ -33407,7 +34402,7 @@ async function treeNow(wtPath, head, dirty) {
|
|
|
33407
34402
|
}
|
|
33408
34403
|
synced.delete(wtPath);
|
|
33409
34404
|
if (!existsSync33(index)) {
|
|
33410
|
-
const own =
|
|
34405
|
+
const own = join29(index, "..", "index");
|
|
33411
34406
|
if (existsSync33(own))
|
|
33412
34407
|
copyFileSync(own, index);
|
|
33413
34408
|
else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
|
|
@@ -33489,8 +34484,8 @@ async function dropTurnRefs(repoPath, worktreeId) {
|
|
|
33489
34484
|
}
|
|
33490
34485
|
|
|
33491
34486
|
// ../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
|
|
34487
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync21, renameSync as renameSync7, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
34488
|
+
import { basename as basename11, dirname as dirname12, join as join30 } from "path";
|
|
33494
34489
|
class WorktreeArchive {
|
|
33495
34490
|
dir;
|
|
33496
34491
|
records = new Map;
|
|
@@ -33502,7 +34497,7 @@ class WorktreeArchive {
|
|
|
33502
34497
|
if (!existsSync34(file))
|
|
33503
34498
|
continue;
|
|
33504
34499
|
try {
|
|
33505
|
-
this.records.set(id, JSON.parse(
|
|
34500
|
+
this.records.set(id, JSON.parse(readFileSync21(file, "utf8")));
|
|
33506
34501
|
} catch (e) {
|
|
33507
34502
|
log.warn("archive", `skipped an unreadable record: ${file}`, e);
|
|
33508
34503
|
}
|
|
@@ -33557,24 +34552,24 @@ class WorktreeArchive {
|
|
|
33557
34552
|
planFile(id, rel) {
|
|
33558
34553
|
if (!this.records.has(id) || !isPlanPath(rel))
|
|
33559
34554
|
return null;
|
|
33560
|
-
const file =
|
|
34555
|
+
const file = join30(this.filesOf(id).plans, basename11(rel));
|
|
33561
34556
|
if (!existsSync34(file))
|
|
33562
34557
|
return null;
|
|
33563
34558
|
try {
|
|
33564
|
-
return
|
|
34559
|
+
return readFileSync21(file, "utf8");
|
|
33565
34560
|
} catch (e) {
|
|
33566
34561
|
log.warn(id, `could not read its archived plan ${rel}`, e);
|
|
33567
34562
|
return null;
|
|
33568
34563
|
}
|
|
33569
34564
|
}
|
|
33570
34565
|
filesOf(id) {
|
|
33571
|
-
const dir =
|
|
34566
|
+
const dir = join30(this.dir, id);
|
|
33572
34567
|
return {
|
|
33573
34568
|
dir,
|
|
33574
|
-
record:
|
|
33575
|
-
transcript:
|
|
33576
|
-
attachments:
|
|
33577
|
-
plans:
|
|
34569
|
+
record: join30(dir, "record.json"),
|
|
34570
|
+
transcript: join30(dir, "transcript.jsonl"),
|
|
34571
|
+
attachments: join30(dir, "attachments"),
|
|
34572
|
+
plans: join30(dir, "plans")
|
|
33578
34573
|
};
|
|
33579
34574
|
}
|
|
33580
34575
|
}
|
|
@@ -33601,7 +34596,7 @@ function summarize(r, repoId, transcript) {
|
|
|
33601
34596
|
function lastUsage(transcript) {
|
|
33602
34597
|
if (!existsSync34(transcript))
|
|
33603
34598
|
return null;
|
|
33604
|
-
const lines =
|
|
34599
|
+
const lines = readFileSync21(transcript, "utf8").split(`
|
|
33605
34600
|
`);
|
|
33606
34601
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
33607
34602
|
if (!lines[i]?.includes('"usage"'))
|
|
@@ -33617,14 +34612,16 @@ function lastUsage(transcript) {
|
|
|
33617
34612
|
function firstPrompt(transcript) {
|
|
33618
34613
|
if (!existsSync34(transcript))
|
|
33619
34614
|
return;
|
|
33620
|
-
for (const line of
|
|
34615
|
+
for (const line of readFileSync21(transcript, "utf8").split(`
|
|
33621
34616
|
`)) {
|
|
33622
|
-
if (!line.includes('"user-message"'))
|
|
34617
|
+
if (!line.includes('"user-message"') && !line.includes('"handed"'))
|
|
33623
34618
|
continue;
|
|
33624
34619
|
try {
|
|
33625
34620
|
const event = JSON.parse(line).event;
|
|
33626
34621
|
if (event?.type === "user-message")
|
|
33627
34622
|
return event.text.slice(0, 500);
|
|
34623
|
+
if (event?.type === "handed")
|
|
34624
|
+
return (event.body ?? event.text).slice(0, 500);
|
|
33628
34625
|
} catch {}
|
|
33629
34626
|
}
|
|
33630
34627
|
return;
|
|
@@ -33713,7 +34710,7 @@ async function fillLandMarks(jobs, read, width = 8) {
|
|
|
33713
34710
|
|
|
33714
34711
|
// ../daemon/src/worktrees/spare.ts
|
|
33715
34712
|
import { existsSync as existsSync35 } from "fs";
|
|
33716
|
-
import { basename as basename12, join as
|
|
34713
|
+
import { basename as basename12, join as join31 } from "path";
|
|
33717
34714
|
class SparePool {
|
|
33718
34715
|
d;
|
|
33719
34716
|
inflight = new Map;
|
|
@@ -33781,11 +34778,11 @@ class SparePool {
|
|
|
33781
34778
|
return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
|
|
33782
34779
|
}
|
|
33783
34780
|
async record(repoId, repo) {
|
|
33784
|
-
const { id, dir } = freeSlot(
|
|
34781
|
+
const { id, dir } = freeSlot(join31(this.d.paths.worktreesDir, repo.name));
|
|
33785
34782
|
const wt = {
|
|
33786
34783
|
id,
|
|
33787
34784
|
repoId,
|
|
33788
|
-
path:
|
|
34785
|
+
path: join31(this.d.paths.worktreesDir, repo.name, dir),
|
|
33789
34786
|
branch: repo.defaultBranch,
|
|
33790
34787
|
kind: "spare",
|
|
33791
34788
|
phase: "reserved",
|
|
@@ -33975,7 +34972,7 @@ class SparePool {
|
|
|
33975
34972
|
// ../daemon/src/worktrees/treeState.ts
|
|
33976
34973
|
import { watch as watch2 } from "fs";
|
|
33977
34974
|
import { stat as stat5 } from "fs/promises";
|
|
33978
|
-
import { join as
|
|
34975
|
+
import { join as join32 } from "path";
|
|
33979
34976
|
var EDIT_DELAY_MS = 150;
|
|
33980
34977
|
var SHELL_QUIET_MS2 = 1500;
|
|
33981
34978
|
var WATCH_DELAY_MS = 300;
|
|
@@ -34358,7 +35355,7 @@ class TreeState {
|
|
|
34358
35355
|
xy: f.xy,
|
|
34359
35356
|
...entries.folded.has(f.path) ? { add: f.add, del: f.del } : {}
|
|
34360
35357
|
}));
|
|
34361
|
-
const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat5(
|
|
35358
|
+
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
35359
|
const t0 = performance.now();
|
|
34363
35360
|
const counts = { ...ab, ...unpushed === null ? {} : { unpushed } };
|
|
34364
35361
|
const signature = String(Bun.hash([
|
|
@@ -34692,7 +35689,7 @@ async function deleteTree(path) {
|
|
|
34692
35689
|
}
|
|
34693
35690
|
}
|
|
34694
35691
|
function withoutAttachments(event) {
|
|
34695
|
-
if (event.type !== "user-message")
|
|
35692
|
+
if (event.type !== "user-message" && event.type !== "handed")
|
|
34696
35693
|
return event;
|
|
34697
35694
|
const { attachments: _attachments, ...rest } = event;
|
|
34698
35695
|
return rest;
|
|
@@ -34797,7 +35794,7 @@ class WorktreeService {
|
|
|
34797
35794
|
const wt = d.state.worktree(worktreeId);
|
|
34798
35795
|
const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
|
|
34799
35796
|
if (wt && prompt)
|
|
34800
|
-
this.scheduleNaming(wt, prompt
|
|
35797
|
+
this.scheduleNaming(wt, prompt);
|
|
34801
35798
|
}
|
|
34802
35799
|
if (turn.end !== "asking")
|
|
34803
35800
|
this.syncAfterTurn(worktreeId);
|
|
@@ -34812,7 +35809,7 @@ class WorktreeService {
|
|
|
34812
35809
|
});
|
|
34813
35810
|
}
|
|
34814
35811
|
async create(repoId, prompt, opts = {}) {
|
|
34815
|
-
const {
|
|
35812
|
+
const { attachments } = opts;
|
|
34816
35813
|
const repo = this.d.state.requireRepo(repoId);
|
|
34817
35814
|
const lead = opts.worktreeId ?? this.d.state.worktrees.find((w) => w.repoId === repoId && isMain(w))?.id;
|
|
34818
35815
|
const context = (id) => {
|
|
@@ -34823,14 +35820,7 @@ class WorktreeService {
|
|
|
34823
35820
|
const profile = this.checkProfile(repo, opts.profile);
|
|
34824
35821
|
const task = taskText(prompt, attachments);
|
|
34825
35822
|
const given = cleanTitle2(opts.title ?? "") || undefined;
|
|
34826
|
-
const
|
|
34827
|
-
const title = variant ? `${base} v${variant.index}` : base;
|
|
34828
|
-
if (opts.carry && variant && variant.of > 1) {
|
|
34829
|
-
throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
|
|
34830
|
-
}
|
|
34831
|
-
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
34832
|
-
|
|
34833
|
-
`) : prompt;
|
|
35823
|
+
const title = given ?? titleFrom(task);
|
|
34834
35824
|
const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
|
|
34835
35825
|
const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
|
|
34836
35826
|
let carriedIn;
|
|
@@ -34841,7 +35831,6 @@ class WorktreeService {
|
|
|
34841
35831
|
mode: opts.mode,
|
|
34842
35832
|
model: opts.model,
|
|
34843
35833
|
effort: opts.effort,
|
|
34844
|
-
variant,
|
|
34845
35834
|
from: opts.from,
|
|
34846
35835
|
profile,
|
|
34847
35836
|
unnamed: given ? undefined : true
|
|
@@ -34852,7 +35841,12 @@ class WorktreeService {
|
|
|
34852
35841
|
if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
|
|
34853
35842
|
await rt.agent.restart();
|
|
34854
35843
|
carriedIn = await this.carryMain(repo, wt, !!opts.carry);
|
|
34855
|
-
|
|
35844
|
+
const born = this.bornWith(wt, opts.plan);
|
|
35845
|
+
rt.agent.send(born?.text ?? prompt, {
|
|
35846
|
+
context: withCarry(context(wt.id), carriedIn),
|
|
35847
|
+
attachments,
|
|
35848
|
+
...born ? { handed: born.handed } : {}
|
|
35849
|
+
});
|
|
34856
35850
|
});
|
|
34857
35851
|
if (claimed) {
|
|
34858
35852
|
const { wt } = claimed;
|
|
@@ -34876,7 +35870,7 @@ class WorktreeService {
|
|
|
34876
35870
|
release?.();
|
|
34877
35871
|
if (restart)
|
|
34878
35872
|
this.restartProcs(wt);
|
|
34879
|
-
this.nameOrAsk(wt, task, given
|
|
35873
|
+
this.nameOrAsk(wt, task, given);
|
|
34880
35874
|
if (carriedIn?.unmoved)
|
|
34881
35875
|
throw new UserError(carriedIn.unmoved, { delivered: true });
|
|
34882
35876
|
return wt;
|
|
@@ -34884,8 +35878,8 @@ class WorktreeService {
|
|
|
34884
35878
|
if (!await isGitRepo(repo.path)) {
|
|
34885
35879
|
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
35880
|
}
|
|
34887
|
-
const { id, dir } = freeSlot(
|
|
34888
|
-
const wtPath =
|
|
35881
|
+
const { id, dir } = freeSlot(join33(this.d.paths.worktreesDir, repo.name));
|
|
35882
|
+
const wtPath = join33(this.d.paths.worktreesDir, repo.name, dir);
|
|
34889
35883
|
const branch = `toyon/${dir}`;
|
|
34890
35884
|
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
|
|
34891
35885
|
const wt = {
|
|
@@ -34900,7 +35894,6 @@ class WorktreeService {
|
|
|
34900
35894
|
createdAt: Date.now(),
|
|
34901
35895
|
promptedAt: Date.now(),
|
|
34902
35896
|
agent,
|
|
34903
|
-
...variant ? { variant } : {},
|
|
34904
35897
|
...opts.from ? { from: opts.from } : {},
|
|
34905
35898
|
...opts.createdBy ? { createdBy: opts.createdBy } : {},
|
|
34906
35899
|
...profile !== undefined ? { profile } : {},
|
|
@@ -34909,16 +35902,34 @@ class WorktreeService {
|
|
|
34909
35902
|
...opts.effort ? { effort: opts.effort } : {}
|
|
34910
35903
|
};
|
|
34911
35904
|
const carried = await this.carryMain(repo, wt, !!opts.carry);
|
|
34912
|
-
this.launch(wt, repo, repo.path);
|
|
34913
|
-
this.
|
|
34914
|
-
this.
|
|
35905
|
+
this.launch(wt, repo, repo.path, opts.cold ? { start: false } : {});
|
|
35906
|
+
const born = this.bornWith(wt, opts.plan);
|
|
35907
|
+
this.d.runtime.ensureAgent(wt).agent.send(born?.text ?? prompt, {
|
|
35908
|
+
context: withCarry(context(id), carried),
|
|
35909
|
+
attachments,
|
|
35910
|
+
...born ? { handed: born.handed } : {}
|
|
35911
|
+
});
|
|
35912
|
+
this.nameOrAsk(wt, task, given);
|
|
34915
35913
|
if (carried.unmoved)
|
|
34916
35914
|
throw new UserError(carried.unmoved, { delivered: true });
|
|
34917
35915
|
return wt;
|
|
34918
35916
|
}
|
|
34919
|
-
|
|
35917
|
+
bornWith(wt, plan) {
|
|
35918
|
+
if (!plan)
|
|
35919
|
+
return null;
|
|
35920
|
+
const path = writePlanDocNow(wt.path, plan.markdown);
|
|
35921
|
+
if (path) {
|
|
35922
|
+
const live = this.d.state.requireWorktree(wt.id);
|
|
35923
|
+
live.plan = path;
|
|
35924
|
+
live.planned = true;
|
|
35925
|
+
this.d.state.save();
|
|
35926
|
+
this.d.hub.emit("worktreesChanged");
|
|
35927
|
+
}
|
|
35928
|
+
return { text: plan.prompt(path), handed: { ...plan.handed, ...path ? { path } : {} } };
|
|
35929
|
+
}
|
|
35930
|
+
nameOrAsk(wt, task, given) {
|
|
34920
35931
|
if (given === undefined) {
|
|
34921
|
-
this.scheduleNaming(wt, task
|
|
35932
|
+
this.scheduleNaming(wt, task);
|
|
34922
35933
|
return;
|
|
34923
35934
|
}
|
|
34924
35935
|
fireAndForget(wt.id, this.rename(wt.id, given), "branch for the given title");
|
|
@@ -34959,7 +35970,7 @@ class WorktreeService {
|
|
|
34959
35970
|
};
|
|
34960
35971
|
const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
|
|
34961
35972
|
const restored = (carried.ok ? carried.out.split(`
|
|
34962
|
-
`).filter(Boolean) : []).filter((p) => !existsSync36(
|
|
35973
|
+
`).filter(Boolean) : []).filter((p) => !existsSync36(join33(wt.path, p)));
|
|
34963
35974
|
const applied = await git(wt.path, "stash", "apply", sha);
|
|
34964
35975
|
if (applied.ok) {
|
|
34965
35976
|
const ref = await entry();
|
|
@@ -34996,9 +36007,9 @@ class WorktreeService {
|
|
|
34996
36007
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
34997
36008
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
34998
36009
|
let slug = branchSlug(title) || "branch";
|
|
34999
|
-
if (existsSync36(
|
|
36010
|
+
if (existsSync36(join33(this.d.paths.worktreesDir, repo.name, slug)))
|
|
35000
36011
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
35001
|
-
const wtPath =
|
|
36012
|
+
const wtPath = join33(this.d.paths.worktreesDir, repo.name, slug);
|
|
35002
36013
|
await withRepoLock(repo.path, async () => {
|
|
35003
36014
|
const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
|
|
35004
36015
|
if (out)
|
|
@@ -35167,24 +36178,14 @@ class WorktreeService {
|
|
|
35167
36178
|
this.d.state.save();
|
|
35168
36179
|
this.d.hub.emit("worktreesChanged");
|
|
35169
36180
|
}
|
|
35170
|
-
scheduleNaming(wt, prompt
|
|
36181
|
+
scheduleNaming(wt, prompt) {
|
|
35171
36182
|
if (!prompt.trim())
|
|
35172
36183
|
return;
|
|
35173
|
-
if (variant && variant.index !== 1)
|
|
35174
|
-
return;
|
|
35175
36184
|
if (this.naming.has(wt.id))
|
|
35176
36185
|
return;
|
|
35177
36186
|
this.naming.add(wt.id);
|
|
35178
36187
|
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) => {
|
|
36188
|
+
const applied = asked.then((name) => {
|
|
35188
36189
|
if (name)
|
|
35189
36190
|
return this.rename(wt.id, name);
|
|
35190
36191
|
});
|
|
@@ -35274,7 +36275,7 @@ class WorktreeService {
|
|
|
35274
36275
|
}
|
|
35275
36276
|
}
|
|
35276
36277
|
async keep(repo, wt) {
|
|
35277
|
-
const index =
|
|
36278
|
+
const index = join33(this.d.paths.archiveDir, `${wt.id}.index`);
|
|
35278
36279
|
const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
|
|
35279
36280
|
if (!kept)
|
|
35280
36281
|
log.warn(wt.id, "could not keep its git state");
|
|
@@ -35311,10 +36312,10 @@ class WorktreeService {
|
|
|
35311
36312
|
log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
|
|
35312
36313
|
}
|
|
35313
36314
|
plansScratch(worktreeId) {
|
|
35314
|
-
return
|
|
36315
|
+
return join33(this.d.paths.archiveDir, `${worktreeId}.plans`);
|
|
35315
36316
|
}
|
|
35316
36317
|
keepPlans(wt) {
|
|
35317
|
-
const from =
|
|
36318
|
+
const from = join33(wt.path, PLANS_DIR);
|
|
35318
36319
|
if (!existsSync36(from))
|
|
35319
36320
|
return;
|
|
35320
36321
|
try {
|
|
@@ -35459,7 +36460,7 @@ class WorktreeService {
|
|
|
35459
36460
|
}
|
|
35460
36461
|
archivedAttachment(archiveId, file) {
|
|
35461
36462
|
const files = this.archive.chatFiles(archiveId);
|
|
35462
|
-
return files && isAttachmentFile(file) ?
|
|
36463
|
+
return files && isAttachmentFile(file) ? join33(files.attachments, file) : null;
|
|
35463
36464
|
}
|
|
35464
36465
|
archivedGit(archiveId) {
|
|
35465
36466
|
const rec = this.archive.get(archiveId);
|
|
@@ -35515,10 +36516,9 @@ class WorktreeService {
|
|
|
35515
36516
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
35516
36517
|
if (rec.auto)
|
|
35517
36518
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
35518
|
-
const path = existsSync36(old.path) ?
|
|
35519
|
-
const { variant: _variant, ...rest } = old;
|
|
36519
|
+
const path = existsSync36(old.path) ? join33(dirname13(old.path), freeSlot(dirname13(old.path)).dir) : old.path;
|
|
35520
36520
|
const wt = {
|
|
35521
|
-
...
|
|
36521
|
+
...old,
|
|
35522
36522
|
repoId: repo.id,
|
|
35523
36523
|
path,
|
|
35524
36524
|
branch: old.branch,
|
|
@@ -35554,7 +36554,7 @@ class WorktreeService {
|
|
|
35554
36554
|
run.finish(0);
|
|
35555
36555
|
this.arriving.delete(wt.id);
|
|
35556
36556
|
wt.branch = branch;
|
|
35557
|
-
move(chat.plans,
|
|
36557
|
+
move(chat.plans, join33(path, PLANS_DIR));
|
|
35558
36558
|
const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
|
|
35559
36559
|
if (!dropped.ok)
|
|
35560
36560
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
@@ -35670,17 +36670,6 @@ class WorktreeService {
|
|
|
35670
36670
|
this.d.state.save();
|
|
35671
36671
|
this.d.hub.emit("worktreesChanged");
|
|
35672
36672
|
}
|
|
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
36673
|
async graft(targetId, sourceIds) {
|
|
35685
36674
|
const target = this.d.state.worktree(targetId);
|
|
35686
36675
|
if (!target)
|
|
@@ -35733,6 +36722,24 @@ class WorktreeService {
|
|
|
35733
36722
|
this.d.hub.emit("worktreesChanged");
|
|
35734
36723
|
fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start a parked preview");
|
|
35735
36724
|
}
|
|
36725
|
+
retryPreview(worktreeId) {
|
|
36726
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
36727
|
+
if (!wt)
|
|
36728
|
+
return;
|
|
36729
|
+
if (wt.parked) {
|
|
36730
|
+
this.startPreview(worktreeId);
|
|
36731
|
+
return;
|
|
36732
|
+
}
|
|
36733
|
+
const procs = this.d.runtime.get(worktreeId)?.procs;
|
|
36734
|
+
const down = procs && !procs.asleep ? procs.states().filter((p) => p.status === "crashed" || p.status === "unreachable" || p.status === "stopped") : [];
|
|
36735
|
+
if (down.length === 0) {
|
|
36736
|
+
fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "preview asked for");
|
|
36737
|
+
return;
|
|
36738
|
+
}
|
|
36739
|
+
for (const p of down) {
|
|
36740
|
+
fireAndForget(worktreeId, this.d.runtime.restartStream(worktreeId, p.name), "preview asked for");
|
|
36741
|
+
}
|
|
36742
|
+
}
|
|
35736
36743
|
async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
|
|
35737
36744
|
this.d.runtime.markSetup(wt.id, true);
|
|
35738
36745
|
try {
|
|
@@ -35742,14 +36749,14 @@ class WorktreeService {
|
|
|
35742
36749
|
}
|
|
35743
36750
|
}
|
|
35744
36751
|
async mirrorBaseIgnore(repo) {
|
|
35745
|
-
const src =
|
|
36752
|
+
const src = join33(repo.path, ".gitignore");
|
|
35746
36753
|
const tracked = existsSync36(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
|
|
35747
36754
|
const mirror = existsSync36(src) && !tracked;
|
|
35748
|
-
const lines = mirror ?
|
|
36755
|
+
const lines = mirror ? readFileSync22(src, "utf8").split(`
|
|
35749
36756
|
`).filter((l) => l.trim() !== "") : [];
|
|
35750
36757
|
await excludeBlock(repo.path, BASE_IGNORE, lines);
|
|
35751
36758
|
}
|
|
35752
|
-
async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
|
|
36759
|
+
async setupThenStart(wt, repo, depsSource, { setupCommands = true, start = true }) {
|
|
35753
36760
|
await this.mirrorBaseIgnore(repo);
|
|
35754
36761
|
const ceiling = timeoutFor(repo.config, "setup");
|
|
35755
36762
|
if (this.d.cache && cachePolicy(repo.config)) {
|
|
@@ -35757,8 +36764,8 @@ class WorktreeService {
|
|
|
35757
36764
|
await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
|
|
35758
36765
|
run.finish(0);
|
|
35759
36766
|
}
|
|
35760
|
-
const srcNm =
|
|
35761
|
-
const dstNm =
|
|
36767
|
+
const srcNm = join33(depsSource, "node_modules");
|
|
36768
|
+
const dstNm = join33(wt.path, "node_modules");
|
|
35762
36769
|
if (existsSync36(srcNm) && !existsSync36(dstNm)) {
|
|
35763
36770
|
const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
|
|
35764
36771
|
const r = await cloneTree(srcNm, dstNm);
|
|
@@ -35769,10 +36776,10 @@ class WorktreeService {
|
|
|
35769
36776
|
log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
|
|
35770
36777
|
}
|
|
35771
36778
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
35772
|
-
const src =
|
|
35773
|
-
if (!existsSync36(src) || existsSync36(
|
|
36779
|
+
const src = join33(depsSource, f);
|
|
36780
|
+
if (!existsSync36(src) || existsSync36(join33(wt.path, f)))
|
|
35774
36781
|
continue;
|
|
35775
|
-
const r = await run("cp", [src,
|
|
36782
|
+
const r = await run("cp", [src, join33(wt.path, f)], wt.path);
|
|
35776
36783
|
if (r.ok)
|
|
35777
36784
|
this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
|
|
35778
36785
|
else
|
|
@@ -35789,7 +36796,8 @@ class WorktreeService {
|
|
|
35789
36796
|
} else if (code !== 0)
|
|
35790
36797
|
this.d.hub.emit("log", wt.id, "setup", `setup failed (exit ${code}): ${cmd}`);
|
|
35791
36798
|
}
|
|
35792
|
-
|
|
36799
|
+
if (start)
|
|
36800
|
+
await this.d.runtime.start(wt, repo);
|
|
35793
36801
|
}
|
|
35794
36802
|
setPr(worktreeId, pr) {
|
|
35795
36803
|
const wt = this.d.state.worktree(worktreeId);
|
|
@@ -35940,6 +36948,7 @@ ${wt.landing.body}` : s;
|
|
|
35940
36948
|
await this.noteLand(repo, wt, mark);
|
|
35941
36949
|
await this.restartFromMain(wt, baseOf(repo));
|
|
35942
36950
|
await this.settleLanded(wt);
|
|
36951
|
+
this.d.hub.emit("landed", wt.id);
|
|
35943
36952
|
}
|
|
35944
36953
|
async land(worktreeId, message) {
|
|
35945
36954
|
const { wt, repo } = this.landable(worktreeId, "land");
|
|
@@ -35950,12 +36959,9 @@ ${wt.landing.body}` : s;
|
|
|
35950
36959
|
return { result: moved };
|
|
35951
36960
|
return this.landOp(wt, repo, message);
|
|
35952
36961
|
});
|
|
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
|
-
};
|
|
36962
|
+
if (out.result.ok && out.landed)
|
|
36963
|
+
this.noteLanded(wt, out.result.message, out.mark);
|
|
36964
|
+
return { result: this.fixShip(wt, out.result, baseOf(repo)) };
|
|
35959
36965
|
}
|
|
35960
36966
|
fixShip(wt, result, base) {
|
|
35961
36967
|
if (result.ok)
|
|
@@ -35999,13 +37005,10 @@ ${wt.landing.body}` : s;
|
|
|
35999
37005
|
return null;
|
|
36000
37006
|
return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
|
|
36001
37007
|
}
|
|
36002
|
-
noteLanded(wt, message,
|
|
36003
|
-
this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message,
|
|
37008
|
+
noteLanded(wt, message, mark) {
|
|
37009
|
+
this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, ...mark ? { mark } : {}, ts: Date.now() });
|
|
36004
37010
|
this.d.hub.emit("landed", wt.id);
|
|
36005
37011
|
}
|
|
36006
|
-
siblingIds(wt) {
|
|
36007
|
-
return siblingsOf(wt, this.d.state.worktrees).map((s) => s.id);
|
|
36008
|
-
}
|
|
36009
37012
|
async followBranch(wt, current) {
|
|
36010
37013
|
if (wt.kind === "main")
|
|
36011
37014
|
return;
|
|
@@ -36064,7 +37067,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
36064
37067
|
if (policy.land === "pr") {
|
|
36065
37068
|
if (wt.pr?.state === "merged" && !prTaken(wt)) {
|
|
36066
37069
|
const { mark, ...result } = await this.prMergedOp(wt.id, undefined, w);
|
|
36067
|
-
return result.ok ? { result,
|
|
37070
|
+
return result.ok ? { result, landed: true, mark } : { result };
|
|
36068
37071
|
}
|
|
36069
37072
|
const fetched = await this.fetchBase(wt, repo, w);
|
|
36070
37073
|
if (!fetched.ok)
|
|
@@ -36104,7 +37107,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
36104
37107
|
const { mark: noted, ...r } = await this.prMergedOp(wt.id, mark, w);
|
|
36105
37108
|
const on = `origin's ${repo.defaultBranch} has this work already`;
|
|
36106
37109
|
const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
|
|
36107
|
-
return r.ok ? { result,
|
|
37110
|
+
return r.ok ? { result, landed: true, mark: noted } : { result };
|
|
36108
37111
|
}
|
|
36109
37112
|
const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
|
|
36110
37113
|
const result = await openPr({
|
|
@@ -36160,7 +37163,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
36160
37163
|
const did = committedHere ? "committed and merged" : "merged";
|
|
36161
37164
|
return {
|
|
36162
37165
|
result: { ...result, message: `${did} into ${repo.defaultBranch}` },
|
|
36163
|
-
|
|
37166
|
+
landed: true,
|
|
36164
37167
|
mark: noted
|
|
36165
37168
|
};
|
|
36166
37169
|
}
|
|
@@ -36223,7 +37226,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
36223
37226
|
const here = followed.ok ? `${repo.defaultBranch} here ${followed.moved ? "pulled it" : "has it"}` : `${repo.defaultBranch} here was left where it is: ${followed.message}`;
|
|
36224
37227
|
const how = { merge: "merged into", squash: "squashed onto", rebase: "rebased onto" }[method];
|
|
36225
37228
|
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}` },
|
|
37229
|
+
return { result: { ok: true, message: `${did}; ${here}` }, landed: true, mark: noted };
|
|
36227
37230
|
}
|
|
36228
37231
|
async noteLand(repo, wt, mark, opts = {}) {
|
|
36229
37232
|
if (!mark)
|
|
@@ -36293,7 +37296,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
36293
37296
|
else
|
|
36294
37297
|
log.warn(worktreeId, `landing failed: ${result.message}`);
|
|
36295
37298
|
if (result.ok && wt)
|
|
36296
|
-
this.noteLanded(wt, result.message,
|
|
37299
|
+
this.noteLanded(wt, result.message, mark);
|
|
36297
37300
|
return result;
|
|
36298
37301
|
}
|
|
36299
37302
|
async prMergedOp(worktreeId, carried, w = UNWATCHED) {
|
|
@@ -36550,6 +37553,37 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
36550
37553
|
base: baseOf(repo)
|
|
36551
37554
|
};
|
|
36552
37555
|
}
|
|
37556
|
+
async standing(worktreeId) {
|
|
37557
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
37558
|
+
if (!wt || wt.kind === "spare" || this.going.has(worktreeId))
|
|
37559
|
+
return null;
|
|
37560
|
+
const repo = this.d.state.repo(wt.repoId);
|
|
37561
|
+
if (!repo)
|
|
37562
|
+
return null;
|
|
37563
|
+
const rec = await this.tree.refresh(worktreeId) ?? this.tree.get(worktreeId);
|
|
37564
|
+
const core = rec?.core;
|
|
37565
|
+
const last = wt.lands?.at(-1);
|
|
37566
|
+
const pr = prStanding(wt);
|
|
37567
|
+
return {
|
|
37568
|
+
main: wt.kind === "main",
|
|
37569
|
+
base: baseOf(repo),
|
|
37570
|
+
...core ? { dirty: core.files.length } : {},
|
|
37571
|
+
...core?.ahead === undefined ? {} : { ahead: core.ahead },
|
|
37572
|
+
...core?.behind === undefined ? {} : { behind: core.behind },
|
|
37573
|
+
...core?.unpushed === undefined ? {} : { unpushed: core.unpushed },
|
|
37574
|
+
...wt.landed ? { landed: { subjects: last?.subjects ?? [], ...last?.pr === undefined ? {} : { pr: last.pr } } } : {},
|
|
37575
|
+
...pr ? {
|
|
37576
|
+
pr: {
|
|
37577
|
+
number: pr.number,
|
|
37578
|
+
url: pr.url,
|
|
37579
|
+
state: pr.state,
|
|
37580
|
+
...pr.checks ? { checks: pr.checks } : {},
|
|
37581
|
+
...pr.review ? { review: pr.review } : {},
|
|
37582
|
+
...pr.automerge ? { automerge: pr.automerge } : {}
|
|
37583
|
+
}
|
|
37584
|
+
} : {}
|
|
37585
|
+
};
|
|
37586
|
+
}
|
|
36553
37587
|
async turnStarted(worktreeId) {
|
|
36554
37588
|
const wt = this.d.state.worktree(worktreeId);
|
|
36555
37589
|
if (wt && !this.going.has(worktreeId))
|
|
@@ -36809,11 +37843,8 @@ class ArchiveSweep {
|
|
|
36809
37843
|
if (maybe.length === 0)
|
|
36810
37844
|
continue;
|
|
36811
37845
|
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;
|
|
37846
|
+
for (const w of maybe)
|
|
36815
37847
|
fresh.set(w.id, await this.d.worktrees.freshCounts(w.id));
|
|
36816
|
-
}
|
|
36817
37848
|
const counts = (id) => fresh.get(id) ?? {};
|
|
36818
37849
|
for (const w of maybe) {
|
|
36819
37850
|
const reason = archiveReason(w, this.facts(repo.id, counts));
|
|
@@ -36861,17 +37892,17 @@ if (pruned > 0)
|
|
|
36861
37892
|
var managed = await loadManaged();
|
|
36862
37893
|
var paths = makePaths();
|
|
36863
37894
|
ensureDirs(paths);
|
|
36864
|
-
forwardTranscripts([paths.transcriptsDir, paths.archiveDir],
|
|
37895
|
+
forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join34(paths.home, "transcripts-forwarded"));
|
|
36865
37896
|
var token = loadOrCreateToken(paths);
|
|
36866
37897
|
var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
|
|
36867
|
-
var helperApp =
|
|
37898
|
+
var helperApp = join34(paths.home, DAEMON_FILES.helper);
|
|
36868
37899
|
var helper = helperWanted({ platform: process.platform, cloud: cloud.enabled, port, home: process.env.TOYON_HOME }) ? new Helper({
|
|
36869
37900
|
dir: helperApp,
|
|
36870
37901
|
stub: HELPER_STUB,
|
|
36871
37902
|
bun: process.execPath,
|
|
36872
37903
|
cli: CLI_ENTRY,
|
|
36873
|
-
log:
|
|
36874
|
-
icon:
|
|
37904
|
+
log: join34(paths.home, DAEMON_FILES.launcherLog),
|
|
37905
|
+
icon: join34(SHELL_DIST, "icon.svg"),
|
|
36875
37906
|
version: package_default.version
|
|
36876
37907
|
}) : null;
|
|
36877
37908
|
if (helper) {
|
|
@@ -36908,8 +37939,9 @@ var readiness2 = new AgentReadiness({
|
|
|
36908
37939
|
connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `check:${spec.id}`),
|
|
36909
37940
|
cwd: paths.scratchDir,
|
|
36910
37941
|
options: (id, category) => state.cachedOptions(id, category),
|
|
36911
|
-
|
|
36912
|
-
|
|
37942
|
+
runs: (id, category) => state.cachedDefault(id, category),
|
|
37943
|
+
learned: (id, category, choices, runs) => {
|
|
37944
|
+
if (state.setCachedOptions(id, category, choices, runs))
|
|
36913
37945
|
hub.emit("agentsChanged");
|
|
36914
37946
|
},
|
|
36915
37947
|
quick: (id) => {
|
|
@@ -36939,6 +37971,8 @@ var runtime = new RuntimeRegistry({
|
|
|
36939
37971
|
bridgeScript: () => bridge.get(),
|
|
36940
37972
|
pageErrors: (id) => pageErrors.ambient(id),
|
|
36941
37973
|
turnStarting: (id) => worktrees.turnStarted(id),
|
|
37974
|
+
archived: (repoId) => worktrees.archived(repoId),
|
|
37975
|
+
standing: (id) => worktrees.standing(id),
|
|
36942
37976
|
remote: () => setting.get(),
|
|
36943
37977
|
gate: previewGate,
|
|
36944
37978
|
viewed: (id) => idle.isViewed(id),
|
|
@@ -36975,13 +38009,15 @@ var worktrees = new WorktreeService({
|
|
|
36975
38009
|
interest.onFirst((id) => worktrees.tree.watch(id));
|
|
36976
38010
|
interest.onLast((id) => worktrees.tree.unwatch(id));
|
|
36977
38011
|
new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
|
|
36978
|
-
var
|
|
38012
|
+
var pieces = new PiecesService({
|
|
36979
38013
|
state,
|
|
36980
38014
|
hub,
|
|
36981
38015
|
runtime,
|
|
36982
|
-
|
|
38016
|
+
attachments: attachments2,
|
|
38017
|
+
create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts),
|
|
38018
|
+
reader: makePiecesReader(runtime, agents, state)
|
|
36983
38019
|
});
|
|
36984
|
-
tools.add(
|
|
38020
|
+
tools.add(pieces);
|
|
36985
38021
|
var turns = new TurnService({
|
|
36986
38022
|
state,
|
|
36987
38023
|
hub,
|
|
@@ -37149,7 +38185,7 @@ var {
|
|
|
37149
38185
|
landing,
|
|
37150
38186
|
fix,
|
|
37151
38187
|
pageErrors,
|
|
37152
|
-
|
|
38188
|
+
pieces,
|
|
37153
38189
|
mcp,
|
|
37154
38190
|
themes,
|
|
37155
38191
|
agents,
|
|
@@ -37159,7 +38195,6 @@ var {
|
|
|
37159
38195
|
afterLand,
|
|
37160
38196
|
restarter,
|
|
37161
38197
|
update,
|
|
37162
|
-
planTasks: makePlanner(agents),
|
|
37163
38198
|
folderDialog: folderDialog()
|
|
37164
38199
|
}
|
|
37165
38200
|
});
|