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