toyon 0.13.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 +2 -2
- package/dist/CHANGELOG.md +602 -0
- package/dist/bridge.js +1 -1
- package/dist/cli.js +60 -53
- package/dist/daemon.js +695 -283
- package/dist/shell/assets/{Editor-CUcgZuoM.js → Editor-jjf6IGvI.js} +3 -3
- package/dist/shell/assets/{XTerm-V6sBDIe-.js → XTerm-ClBPMY5e.js} +1 -1
- package/dist/shell/assets/{cssMode-CSyiAvIn.js → cssMode-CgTk4Slg.js} +1 -1
- package/dist/shell/assets/{freemarker2-C4-FNK3V.js → freemarker2-B4Eirl4Y.js} +1 -1
- package/dist/shell/assets/{grammar-DzHb7e7O.js → grammar-E8nWrhy3.js} +1 -1
- package/dist/shell/assets/{handlebars-DcjVaOCM.js → handlebars-C6uEGz8T.js} +1 -1
- package/dist/shell/assets/{html-Csl8cogA.js → html-tDAdsEYh.js} +1 -1
- package/dist/shell/assets/{htmlMode-rewe9UaO.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--pKV-E-G.js → javascript-BeM3ebwd.js} +1 -1
- package/dist/shell/assets/{jsonMode-zPVc0E6l.js → jsonMode-CXStIFU4.js} +1 -1
- package/dist/shell/assets/{liquid-jZT5xQG-.js → liquid-2UNKoWGQ.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-BuiqKs90.js → lspLanguageFeatures-G4xsCwWV.js} +1 -1
- package/dist/shell/assets/{mdx-DwnR-ia2.js → mdx-CoF6St9y.js} +1 -1
- package/dist/shell/assets/{python-DU3pl9Zu.js → python-rULGuAEe.js} +1 -1
- package/dist/shell/assets/{razor-BzVUPqHs.js → razor-0I41g6kD.js} +1 -1
- package/dist/shell/assets/{tsMode-ONmZhHHZ.js → tsMode-CyMsmAXC.js} +1 -1
- package/dist/shell/assets/{xml-BXW7X3BT.js → xml-BNhTd1tm.js} +1 -1
- package/dist/shell/assets/{yaml-DFE79ex1.js → yaml-Cp69rkq5.js} +1 -1
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-Bkn7zNFw.js +0 -267
- package/dist/shell/assets/index-iVaPr_If.css +0 -1
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)
|
|
@@ -15275,7 +15340,7 @@ function mapUpdate(update, memos, tag, sink, background) {
|
|
|
15275
15340
|
out.push({ type: "tool-update", toolId: update.toolCallId, ...refined });
|
|
15276
15341
|
}
|
|
15277
15342
|
if (update.content)
|
|
15278
|
-
memo.content =
|
|
15343
|
+
memo.content = update.content;
|
|
15279
15344
|
if (update.rawOutput !== undefined)
|
|
15280
15345
|
memo.rawOutput = update.rawOutput;
|
|
15281
15346
|
if ((update.status === "completed" || update.status === "failed") && !memo.ended) {
|
|
@@ -16182,15 +16247,30 @@ class AcpSession {
|
|
|
16182
16247
|
this.emit({ type: "tool-end", toolId, output: output.join(`
|
|
16183
16248
|
`), isError: true });
|
|
16184
16249
|
}
|
|
16185
|
-
|
|
16250
|
+
const going = new Set;
|
|
16251
|
+
for (const { event } of this.log.entries) {
|
|
16186
16252
|
this.trackHandoff(event);
|
|
16253
|
+
if (event.type === "handoff-go")
|
|
16254
|
+
going.add(event.id);
|
|
16255
|
+
else if (event.type === "handoff" || event.type === "handoff-declined")
|
|
16256
|
+
going.delete(event.id);
|
|
16257
|
+
}
|
|
16258
|
+
for (const id of going) {
|
|
16259
|
+
this.emit({
|
|
16260
|
+
type: "handoff-declined",
|
|
16261
|
+
id,
|
|
16262
|
+
outcome: "failed",
|
|
16263
|
+
reason: "Toyon stopped while the worktree was being made",
|
|
16264
|
+
ts: Date.now()
|
|
16265
|
+
});
|
|
16266
|
+
}
|
|
16187
16267
|
if (this.openHandoffs.size > 0)
|
|
16188
16268
|
this.syncStatus();
|
|
16189
16269
|
}
|
|
16190
16270
|
trackHandoff(event) {
|
|
16191
16271
|
if (event.type === "handoff-proposed")
|
|
16192
16272
|
return this.openHandoffs.add(event.id).size > 0;
|
|
16193
|
-
if (event.type === "handoff" || event.type === "handoff-declined")
|
|
16273
|
+
if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
|
|
16194
16274
|
return this.openHandoffs.delete(event.id);
|
|
16195
16275
|
return false;
|
|
16196
16276
|
}
|
|
@@ -16409,6 +16489,7 @@ class AcpSession {
|
|
|
16409
16489
|
this.queue = [];
|
|
16410
16490
|
this.steered = [];
|
|
16411
16491
|
this.queueChanged();
|
|
16492
|
+
this.cancelAsks("expired");
|
|
16412
16493
|
this.stop();
|
|
16413
16494
|
await this.dropConn("closing");
|
|
16414
16495
|
await this.log.flush();
|
|
@@ -16849,6 +16930,9 @@ class AcpSession {
|
|
|
16849
16930
|
}
|
|
16850
16931
|
if (params.update.sessionUpdate === "current_mode_update") {
|
|
16851
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");
|
|
16852
16936
|
return;
|
|
16853
16937
|
}
|
|
16854
16938
|
if (params.update.sessionUpdate === "config_option_update") {
|
|
@@ -16921,7 +17005,7 @@ class AcpSession {
|
|
|
16921
17005
|
const parts = permissionParts(params);
|
|
16922
17006
|
const detail = parts.detail ?? "";
|
|
16923
17007
|
const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
|
|
16924
|
-
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");
|
|
16925
17009
|
const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
|
|
16926
17010
|
return this.openAsk((id) => ({
|
|
16927
17011
|
type: "agent-permission",
|
|
@@ -17007,9 +17091,9 @@ class AcpSession {
|
|
|
17007
17091
|
const text = headers.length === 1 ? `with my answer to "${headers[0]}"` : "with my answers";
|
|
17008
17092
|
this.post(text, { attachments: followUp.attachments });
|
|
17009
17093
|
}
|
|
17010
|
-
cancelAsks() {
|
|
17094
|
+
cancelAsks(outcome = "cancelled") {
|
|
17011
17095
|
for (const ask of [...this.asks.values()])
|
|
17012
|
-
ask.settle(
|
|
17096
|
+
ask.settle(outcome);
|
|
17013
17097
|
}
|
|
17014
17098
|
syncStatus() {
|
|
17015
17099
|
if (this.status === "error")
|
|
@@ -18641,6 +18725,8 @@ class RuntimeRegistry {
|
|
|
18641
18725
|
}
|
|
18642
18726
|
if (this.settingUp.has(id))
|
|
18643
18727
|
return;
|
|
18728
|
+
if (wt.kind === "spare" && wt.phase !== undefined && wt.phase !== "ready")
|
|
18729
|
+
return;
|
|
18644
18730
|
const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
|
|
18645
18731
|
if (repo)
|
|
18646
18732
|
await this.start(wt, repo);
|
|
@@ -19435,7 +19521,7 @@ function openAskOf(entries) {
|
|
|
19435
19521
|
open.delete(e.id);
|
|
19436
19522
|
else if (e.type === "handoff-proposed")
|
|
19437
19523
|
open.set(e.id, `Continue in ${e.repo.name}?`);
|
|
19438
|
-
else if (e.type === "handoff" || e.type === "handoff-declined")
|
|
19524
|
+
else if (e.type === "handoff-go" || e.type === "handoff" || e.type === "handoff-declined")
|
|
19439
19525
|
open.delete(e.id);
|
|
19440
19526
|
}
|
|
19441
19527
|
const last = [...open.values()].at(-1)?.trim();
|
|
@@ -19487,7 +19573,7 @@ var LAND_ASK = [
|
|
|
19487
19573
|
"Line 2: blank",
|
|
19488
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.",
|
|
19489
19575
|
"Line 4: blank",
|
|
19490
|
-
"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",
|
|
19491
19577
|
"Line 6: blank",
|
|
19492
19578
|
"Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
|
|
19493
19579
|
].join(`
|
|
@@ -19495,17 +19581,25 @@ var LAND_ASK = [
|
|
|
19495
19581
|
var LAND_TURNS = 4;
|
|
19496
19582
|
var LAND_CHARS = 5000;
|
|
19497
19583
|
function landPrompt(i) {
|
|
19498
|
-
const
|
|
19499
|
-
|
|
19500
|
-
|
|
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
|
+
}
|
|
19501
19596
|
if (i.recentSubjects.length)
|
|
19502
19597
|
head.push(`Recent commit subjects:
|
|
19503
19598
|
${i.recentSubjects.map((s) => ` ${s}`).join(`
|
|
19504
19599
|
`)}`);
|
|
19505
19600
|
if (i.note?.trim())
|
|
19506
19601
|
head.push(`Note from the user for the commit message, to be reflected in its wording: ${clip(i.note.trim(), 500)}`);
|
|
19507
|
-
const diff =
|
|
19508
|
-
${i.diffStat.trim() || "(none)"}`;
|
|
19602
|
+
const diff = diffBlock(own, committed);
|
|
19509
19603
|
let blocks = turnBlocks(i.turns);
|
|
19510
19604
|
const size = () => [...head, "", ...blocks, "", diff].join(`
|
|
19511
19605
|
|
|
@@ -19517,6 +19611,22 @@ ${i.diffStat.trim() || "(none)"}`;
|
|
|
19517
19611
|
|
|
19518
19612
|
`), diff].join(`
|
|
19519
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
|
+
|
|
19520
19630
|
`);
|
|
19521
19631
|
}
|
|
19522
19632
|
function turnBlocks(turns) {
|
|
@@ -19787,7 +19897,8 @@ function locateAssets(here, env = process.env) {
|
|
|
19787
19897
|
helperStub: packaged ? join13(here, "helper-stub") : join13(here, "../dist/helper-stub"),
|
|
19788
19898
|
cliEntry: packaged ? join13(here, "cli.js") : join13(here, "../../cli/src/cli.ts"),
|
|
19789
19899
|
sourceRoot: packaged ? null : resolve5(here, "../../.."),
|
|
19790
|
-
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")
|
|
19791
19902
|
};
|
|
19792
19903
|
}
|
|
19793
19904
|
var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
|
|
@@ -21046,9 +21157,9 @@ class StateStore {
|
|
|
21046
21157
|
this.state.worktrees.push(wt);
|
|
21047
21158
|
this.save();
|
|
21048
21159
|
}
|
|
21049
|
-
removeWorktree(id) {
|
|
21160
|
+
removeWorktree(id, opts = {}) {
|
|
21050
21161
|
const wt = this.state.worktrees.find((w) => w.id === id);
|
|
21051
|
-
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;
|
|
21052
21163
|
if (wt && repo)
|
|
21053
21164
|
repo.promptedAt = Math.max(repo.promptedAt ?? 0, sentAt(wt));
|
|
21054
21165
|
this.state.worktrees = this.state.worktrees.filter((w) => w.id !== id);
|
|
@@ -22738,9 +22849,14 @@ async function behindUpstream(worktreePath) {
|
|
|
22738
22849
|
const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
|
|
22739
22850
|
return r.ok ? Number(r.out) || 0 : null;
|
|
22740
22851
|
}
|
|
22741
|
-
async function aheadUpstream(worktreePath) {
|
|
22852
|
+
async function aheadUpstream(worktreePath, branch) {
|
|
22742
22853
|
const r = await git(worktreePath, "rev-list", "--count", "@{upstream}..HEAD");
|
|
22743
|
-
|
|
22854
|
+
if (r.ok)
|
|
22855
|
+
return Number(r.out) || 0;
|
|
22856
|
+
if (!branch)
|
|
22857
|
+
return null;
|
|
22858
|
+
const o = await git(worktreePath, "rev-list", "--count", `origin/${branch}..HEAD`);
|
|
22859
|
+
return o.ok ? Number(o.out) || 0 : null;
|
|
22744
22860
|
}
|
|
22745
22861
|
async function aheadBehind(worktreePath, defaultBr) {
|
|
22746
22862
|
const [a, b] = await Promise.all([
|
|
@@ -23269,6 +23385,9 @@ class OpenService {
|
|
|
23269
23385
|
throw new UserError(`${raw} is a folder: Toyon opens files`);
|
|
23270
23386
|
return this.openFile(raw, path, st);
|
|
23271
23387
|
}
|
|
23388
|
+
async where(raw) {
|
|
23389
|
+
return (await this.locate(raw)).path;
|
|
23390
|
+
}
|
|
23272
23391
|
async locate(raw) {
|
|
23273
23392
|
const path = canonical(resolve7(expandTilde(raw)));
|
|
23274
23393
|
const st = await stat3(path).catch((e) => {
|
|
@@ -23368,13 +23487,70 @@ function parseAllowed(json) {
|
|
|
23368
23487
|
return null;
|
|
23369
23488
|
}
|
|
23370
23489
|
}
|
|
23371
|
-
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";
|
|
23372
23491
|
async function viewPr(repoPath, number) {
|
|
23373
23492
|
const r = await gh(["pr", "view", String(number), "--json", PR_VIEW_FIELDS], repoPath, 20000);
|
|
23374
23493
|
if (!r.ok || !r.out)
|
|
23375
23494
|
return null;
|
|
23376
23495
|
const parsed = parsePrView(r.out);
|
|
23377
|
-
|
|
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;
|
|
23378
23554
|
}
|
|
23379
23555
|
function parsePrView(json) {
|
|
23380
23556
|
let o;
|
|
@@ -23390,6 +23566,7 @@ function parsePrView(json) {
|
|
|
23390
23566
|
const checks = foldChecks(o.statusCheckRollup);
|
|
23391
23567
|
const mergeable = o.mergeable === "MERGEABLE" ? true : o.mergeable === "CONFLICTING" ? false : undefined;
|
|
23392
23568
|
const automerge = o.autoMergeRequest !== null && o.autoMergeRequest !== undefined;
|
|
23569
|
+
const preview = state === "open" ? previewOf3(o.statusCheckRollup) : undefined;
|
|
23393
23570
|
return {
|
|
23394
23571
|
number: o.number,
|
|
23395
23572
|
url: o.url,
|
|
@@ -23398,9 +23575,37 @@ function parsePrView(json) {
|
|
|
23398
23575
|
...review ? { review } : {},
|
|
23399
23576
|
...checks ? { checks } : {},
|
|
23400
23577
|
...mergeable !== undefined ? { mergeable } : {},
|
|
23401
|
-
...automerge ? { automerge } : {}
|
|
23578
|
+
...automerge ? { automerge } : {},
|
|
23579
|
+
...preview ? { preview } : {}
|
|
23402
23580
|
};
|
|
23403
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
|
+
}
|
|
23404
23609
|
function foldChecks(rollup) {
|
|
23405
23610
|
if (!Array.isArray(rollup) || rollup.length === 0)
|
|
23406
23611
|
return;
|
|
@@ -28837,6 +29042,9 @@ var handlers = {
|
|
|
28837
29042
|
reveal(msg, _ctx, s) {
|
|
28838
29043
|
s.files.reveal(msg.worktreeId, msg.path);
|
|
28839
29044
|
},
|
|
29045
|
+
async "reveal-path"(msg, _ctx, s) {
|
|
29046
|
+
s.files.revealPath(await s.opens.where(msg.path));
|
|
29047
|
+
},
|
|
28840
29048
|
async "discard-files"(msg, _ctx, s) {
|
|
28841
29049
|
try {
|
|
28842
29050
|
for (const path of msg.paths)
|
|
@@ -28964,10 +29172,12 @@ var handlers = {
|
|
|
28964
29172
|
const followUp = msg.attachments?.length ? { attachments: msg.attachments } : undefined;
|
|
28965
29173
|
s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "answers", answers: msg.answers }, followUp);
|
|
28966
29174
|
});
|
|
29175
|
+
s.worktrees.markPrompted(msg.worktreeId);
|
|
28967
29176
|
},
|
|
28968
29177
|
"agent-decide"(msg, _ctx, s) {
|
|
28969
29178
|
requireRun(s, msg.worktreeId);
|
|
28970
29179
|
s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId });
|
|
29180
|
+
s.worktrees.markPrompted(msg.worktreeId);
|
|
28971
29181
|
},
|
|
28972
29182
|
async "handoff-answer"(msg, _ctx, s) {
|
|
28973
29183
|
requireRun(s, msg.worktreeId);
|
|
@@ -29061,8 +29271,8 @@ var handlers = {
|
|
|
29061
29271
|
ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
|
|
29062
29272
|
},
|
|
29063
29273
|
async "search-chats"(msg, ctx, s) {
|
|
29064
|
-
const { hits, truncated } = await s.chats.search(msg.repoId, msg.query);
|
|
29065
|
-
ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated });
|
|
29274
|
+
const { hits, truncated, asks } = await s.chats.search(msg.repoId, msg.query);
|
|
29275
|
+
ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated, asks });
|
|
29066
29276
|
},
|
|
29067
29277
|
async "open-ref"(msg, _ctx, s) {
|
|
29068
29278
|
await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
|
|
@@ -29341,6 +29551,20 @@ function createFetch(opts) {
|
|
|
29341
29551
|
const refused = await opts.restart(url.searchParams.has(RESTART_NOW));
|
|
29342
29552
|
return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
|
|
29343
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
|
+
}
|
|
29344
29568
|
const paired = await opts.pairing(req, url, d);
|
|
29345
29569
|
if (paired)
|
|
29346
29570
|
return paired;
|
|
@@ -29856,7 +30080,8 @@ function startServer(opts) {
|
|
|
29856
30080
|
restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
|
|
29857
30081
|
grants: opts.grants,
|
|
29858
30082
|
trusted: () => s.state.trustedOrigins,
|
|
29859
|
-
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)
|
|
29860
30085
|
}),
|
|
29861
30086
|
websocket: {
|
|
29862
30087
|
async open(ws) {
|
|
@@ -29873,6 +30098,7 @@ function startServer(opts) {
|
|
|
29873
30098
|
sockets.add(ws);
|
|
29874
30099
|
link(ws);
|
|
29875
30100
|
send(ws, await helloFrame());
|
|
30101
|
+
fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
|
|
29876
30102
|
for (const o of s.opens.takePending())
|
|
29877
30103
|
send(ws, openedFrame(o));
|
|
29878
30104
|
},
|
|
@@ -30204,7 +30430,7 @@ var REFRESH_EVERY_MS = 60000;
|
|
|
30204
30430
|
var CHECK_EVERY_MS = 6 * 60 * 60000;
|
|
30205
30431
|
var FIRST_CHECK_MS = 2 * 60000;
|
|
30206
30432
|
var SETTLED_MS = 2 * 60000;
|
|
30207
|
-
var
|
|
30433
|
+
var RETRY_AFTER_MS2 = 24 * 60 * 60000;
|
|
30208
30434
|
var INSTALL_SETTLE_MS = 60000;
|
|
30209
30435
|
|
|
30210
30436
|
class UpdateService {
|
|
@@ -30356,7 +30582,7 @@ class UpdateService {
|
|
|
30356
30582
|
if (this.needsInstall() && this.d.command(target) === null)
|
|
30357
30583
|
return;
|
|
30358
30584
|
const failed = this.d.state.updateFailed;
|
|
30359
|
-
if (failed?.version === target && this.now() - failed.at <
|
|
30585
|
+
if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS2)
|
|
30360
30586
|
return;
|
|
30361
30587
|
if (this.d.busy()) {
|
|
30362
30588
|
this.busyAt = this.now();
|
|
@@ -30815,6 +31041,7 @@ var CHAT_HITS_MAX = 200;
|
|
|
30815
31041
|
var MIN_QUERY = 2;
|
|
30816
31042
|
var SNIPPET = 160;
|
|
30817
31043
|
var BEFORE = 50;
|
|
31044
|
+
var ASK = 500;
|
|
30818
31045
|
var bunFs2 = {
|
|
30819
31046
|
async stat(path) {
|
|
30820
31047
|
try {
|
|
@@ -30886,6 +31113,9 @@ function searchRows(rows, terms, chat, limit = CHAT_HITS_PER_WORKTREE) {
|
|
|
30886
31113
|
}
|
|
30887
31114
|
return { hits, more: false };
|
|
30888
31115
|
}
|
|
31116
|
+
function firstAsk(rows) {
|
|
31117
|
+
return rows.find((r) => r.role === "user")?.text.slice(0, ASK);
|
|
31118
|
+
}
|
|
30889
31119
|
function termRanges(lower, terms) {
|
|
30890
31120
|
const ranges = [];
|
|
30891
31121
|
for (const term of terms) {
|
|
@@ -30907,7 +31137,7 @@ class ChatSearch {
|
|
|
30907
31137
|
this.d.state.requireRepo(repoId);
|
|
30908
31138
|
const terms = needleTerms(query);
|
|
30909
31139
|
if (terms.length === 0)
|
|
30910
|
-
return { hits: [], truncated: false };
|
|
31140
|
+
return { hits: [], truncated: false, asks: {} };
|
|
30911
31141
|
const chats = [
|
|
30912
31142
|
...this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree").map((w) => ({ id: w.id, archived: false, path: this.d.transcriptPath(w.id) })),
|
|
30913
31143
|
...this.d.archivedChats(repoId).map((a) => ({ id: a.id, archived: true, path: a.transcript }))
|
|
@@ -30915,13 +31145,23 @@ class ChatSearch {
|
|
|
30915
31145
|
const read = new Set;
|
|
30916
31146
|
const found = await Promise.all(chats.map(async (c) => {
|
|
30917
31147
|
const rows = await this.rowsOf(c.id, c.archived, c.path, read);
|
|
30918
|
-
return
|
|
31148
|
+
return {
|
|
31149
|
+
id: c.id,
|
|
31150
|
+
...searchRows(rows, terms, { worktreeId: c.id, archived: c.archived }),
|
|
31151
|
+
ask: firstAsk(rows)
|
|
31152
|
+
};
|
|
30919
31153
|
}));
|
|
30920
31154
|
for (const key of this.said.keys())
|
|
30921
31155
|
if (!read.has(key))
|
|
30922
31156
|
this.said.delete(key);
|
|
30923
31157
|
const all = found.filter((f) => f.hits.length > 0).sort((a, b) => b.hits[0].ts - a.hits[0].ts).flatMap((f) => f.hits);
|
|
30924
|
-
|
|
31158
|
+
const hits = all.slice(0, CHAT_HITS_MAX);
|
|
31159
|
+
const kept = new Set(hits.map((h) => h.worktreeId));
|
|
31160
|
+
const asks = {};
|
|
31161
|
+
for (const f of found)
|
|
31162
|
+
if (f.ask && kept.has(f.id))
|
|
31163
|
+
asks[f.id] = f.ask;
|
|
31164
|
+
return { hits, truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more), asks };
|
|
30925
31165
|
}
|
|
30926
31166
|
async rowsOf(id, archived, path, read) {
|
|
30927
31167
|
const live = archived ? undefined : this.d.live(id);
|
|
@@ -31107,7 +31347,7 @@ function openProposals(entries) {
|
|
|
31107
31347
|
for (const { event } of entries) {
|
|
31108
31348
|
if (event.type === "handoff-proposed")
|
|
31109
31349
|
open.set(event.id, event);
|
|
31110
|
-
else if (event.type === "handoff" || event.type === "handoff-declined")
|
|
31350
|
+
else if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
|
|
31111
31351
|
open.delete(event.id);
|
|
31112
31352
|
}
|
|
31113
31353
|
return [...open.values()];
|
|
@@ -31126,7 +31366,6 @@ function proposedThisTurn(entries) {
|
|
|
31126
31366
|
|
|
31127
31367
|
class HandoffService {
|
|
31128
31368
|
d;
|
|
31129
|
-
settling = new Set;
|
|
31130
31369
|
constructor(d) {
|
|
31131
31370
|
this.d = d;
|
|
31132
31371
|
d.hub.on("landed", (worktreeId) => this.landed(worktreeId));
|
|
@@ -31239,13 +31478,11 @@ class HandoffService {
|
|
|
31239
31478
|
if (!openProposals(entries).some((p) => p.id === id)) {
|
|
31240
31479
|
return log.debug(worktreeId, `answer for a handoff that already closed (${id})`);
|
|
31241
31480
|
}
|
|
31242
|
-
if (this.settling.has(id))
|
|
31243
|
-
return;
|
|
31244
31481
|
if (!go) {
|
|
31245
31482
|
agent.note({ type: "handoff-declined", id, ts: Date.now() });
|
|
31246
31483
|
return;
|
|
31247
31484
|
}
|
|
31248
|
-
|
|
31485
|
+
agent.note({ type: "handoff-go", id, ts: Date.now() });
|
|
31249
31486
|
try {
|
|
31250
31487
|
const repo = this.d.state.repo(proposed.repo.id);
|
|
31251
31488
|
if (!repo)
|
|
@@ -31271,8 +31508,6 @@ class HandoffService {
|
|
|
31271
31508
|
const reason = e instanceof Error ? e.message : String(e);
|
|
31272
31509
|
agent.note({ type: "handoff-declined", id, outcome: "failed", reason, ts: Date.now() });
|
|
31273
31510
|
throw e;
|
|
31274
|
-
} finally {
|
|
31275
|
-
this.settling.delete(id);
|
|
31276
31511
|
}
|
|
31277
31512
|
}
|
|
31278
31513
|
async ask(worktreeId, repoId, text) {
|
|
@@ -31337,6 +31572,7 @@ function match(repos, q) {
|
|
|
31337
31572
|
// ../daemon/src/worktrees/landing.ts
|
|
31338
31573
|
var TAIL_CHARS = 400;
|
|
31339
31574
|
var DIFF_CHARS = 2000;
|
|
31575
|
+
var CONTEXT_CHARS2 = 800;
|
|
31340
31576
|
var CHECK_SLOTS = 2;
|
|
31341
31577
|
|
|
31342
31578
|
class LandingService {
|
|
@@ -31412,14 +31648,7 @@ class LandingService {
|
|
|
31412
31648
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
31413
31649
|
this.judging.set(worktreeId, turn.at);
|
|
31414
31650
|
const started = Date.now();
|
|
31415
|
-
const
|
|
31416
|
-
const prompt = landPrompt({
|
|
31417
|
-
title: wt.title,
|
|
31418
|
-
firstAsk: firstAskOf(entries),
|
|
31419
|
-
turns: turnsSince(entries, 0),
|
|
31420
|
-
diffStat: await diffSummary(wt.path, baseOf(repo)),
|
|
31421
|
-
recentSubjects: await recentSubjects(wt.path, baseOf(repo))
|
|
31422
|
-
});
|
|
31651
|
+
const prompt = landPrompt(await this.landInput(wt, repo));
|
|
31423
31652
|
let verdict = null;
|
|
31424
31653
|
try {
|
|
31425
31654
|
verdict = await this.d.judge(wt, prompt);
|
|
@@ -31492,6 +31721,25 @@ class LandingService {
|
|
|
31492
31721
|
return;
|
|
31493
31722
|
fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
|
|
31494
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
|
+
}
|
|
31495
31743
|
async hasWork(wt) {
|
|
31496
31744
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
31497
31745
|
const files = await statusFiles(wt.path);
|
|
@@ -31514,14 +31762,14 @@ class LandingService {
|
|
|
31514
31762
|
};
|
|
31515
31763
|
this.judging.set(worktreeId, opts.at);
|
|
31516
31764
|
const started = Date.now();
|
|
31517
|
-
|
|
31765
|
+
const pending = { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept };
|
|
31766
|
+
this.d.worktrees.setLanding(worktreeId, pending);
|
|
31518
31767
|
const live = () => !this.stopping && this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
|
|
31519
|
-
let check = "none";
|
|
31520
|
-
let checkTail;
|
|
31521
|
-
let waited = 0;
|
|
31522
|
-
let ran;
|
|
31523
31768
|
const command = repo.config.check?.trim();
|
|
31524
|
-
|
|
31769
|
+
let checkDone = false;
|
|
31770
|
+
const checked = async () => {
|
|
31771
|
+
if (!command)
|
|
31772
|
+
return { check: "none" };
|
|
31525
31773
|
const ceiling = timeoutFor(repo.config, "check");
|
|
31526
31774
|
this.d.runs.drop(worktreeId, "check");
|
|
31527
31775
|
const run = this.d.runs.begin(worktreeId, "check", {
|
|
@@ -31530,9 +31778,11 @@ class LandingService {
|
|
|
31530
31778
|
});
|
|
31531
31779
|
if (!await run.admitted)
|
|
31532
31780
|
return;
|
|
31533
|
-
if (!live())
|
|
31534
|
-
|
|
31535
|
-
|
|
31781
|
+
if (!live()) {
|
|
31782
|
+
run.finish(null);
|
|
31783
|
+
return;
|
|
31784
|
+
}
|
|
31785
|
+
const waited = Date.now() - started;
|
|
31536
31786
|
let r;
|
|
31537
31787
|
try {
|
|
31538
31788
|
r = await this.d.check(worktreeId, command, { quiet: opts.quiet, timeoutMs: ceiling, onSpawn: run.spawned });
|
|
@@ -31543,8 +31793,8 @@ class LandingService {
|
|
|
31543
31793
|
run.finish(r.exit);
|
|
31544
31794
|
if (!live())
|
|
31545
31795
|
return;
|
|
31546
|
-
|
|
31547
|
-
|
|
31796
|
+
const check = r.exit === 0 ? "pass" : "fail";
|
|
31797
|
+
let checkTail;
|
|
31548
31798
|
if (r.exit === "timeout") {
|
|
31549
31799
|
checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
|
|
31550
31800
|
`);
|
|
@@ -31552,19 +31802,14 @@ class LandingService {
|
|
|
31552
31802
|
checkTail = tail(r.text) || `exit ${r.exit}`;
|
|
31553
31803
|
else
|
|
31554
31804
|
this.d.hub.emit("checkPassed", worktreeId);
|
|
31555
|
-
|
|
31556
|
-
|
|
31557
|
-
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
const prompt = landPrompt({
|
|
31561
|
-
|
|
31562
|
-
|
|
31563
|
-
turns: turnsSince(entries, 0),
|
|
31564
|
-
diffStat: await diffSummary(wt.path, baseOf(repo)),
|
|
31565
|
-
recentSubjects: await recentSubjects(wt.path, baseOf(repo)),
|
|
31566
|
-
...opts.note ? { note: opts.note } : {}
|
|
31567
|
-
});
|
|
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;
|
|
31568
31813
|
try {
|
|
31569
31814
|
verdict = await this.d.judge(wt, prompt);
|
|
31570
31815
|
} catch (e) {
|
|
@@ -31575,16 +31820,29 @@ class LandingService {
|
|
|
31575
31820
|
}
|
|
31576
31821
|
if (!live())
|
|
31577
31822
|
return;
|
|
31578
|
-
|
|
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;
|
|
31579
31839
|
const landing = {
|
|
31580
31840
|
at: opts.at,
|
|
31581
31841
|
check,
|
|
31582
31842
|
...checkTail ? { checkTail } : {},
|
|
31583
31843
|
ready: check !== "fail",
|
|
31584
31844
|
...kept,
|
|
31585
|
-
...verdict
|
|
31586
|
-
...verdict?.subject ? { subject: verdict.subject } : {},
|
|
31587
|
-
...verdict?.body ? { body: verdict.body } : {},
|
|
31845
|
+
...verdict ? messageOf(verdict) : {},
|
|
31588
31846
|
...unanswered ? { unanswered: true } : {},
|
|
31589
31847
|
fingerprint: await treeFingerprint(wt.path)
|
|
31590
31848
|
};
|
|
@@ -31594,14 +31852,18 @@ class LandingService {
|
|
|
31594
31852
|
if (!live())
|
|
31595
31853
|
return;
|
|
31596
31854
|
this.judging.delete(worktreeId);
|
|
31597
|
-
|
|
31598
|
-
|
|
31599
|
-
record.recap = { at: Date.now(), text: verdict.recap };
|
|
31855
|
+
if (verdict)
|
|
31856
|
+
this.recapTurn(worktreeId, verdict);
|
|
31600
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)` : ""}`);
|
|
31601
31858
|
this.d.worktrees.setLanding(worktreeId, landing);
|
|
31602
31859
|
if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
|
|
31603
31860
|
this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
|
|
31604
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
|
+
}
|
|
31605
31867
|
async sharedMigration(wt, repo) {
|
|
31606
31868
|
if (!repo.services || keepsCopiesApart(repo.config))
|
|
31607
31869
|
return;
|
|
@@ -31611,31 +31873,44 @@ class LandingService {
|
|
|
31611
31873
|
return "this changes migrations, and the worktrees share the database";
|
|
31612
31874
|
}
|
|
31613
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
|
+
}
|
|
31614
31883
|
function tail(text) {
|
|
31615
31884
|
const t = text.replace(/\s+$/, "");
|
|
31616
31885
|
return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
|
|
31617
31886
|
}
|
|
31618
|
-
async function diffSummary(path, defaultBr) {
|
|
31887
|
+
async function diffSummary(id, path, defaultBr) {
|
|
31619
31888
|
const [base, dirty, untracked] = await Promise.all([
|
|
31620
31889
|
git(path, "merge-base", "HEAD", defaultBr),
|
|
31621
31890
|
git(path, "diff", "HEAD", "--stat"),
|
|
31622
31891
|
git(path, "ls-files", "--others", "--exclude-standard")
|
|
31623
31892
|
]);
|
|
31893
|
+
if (!base.ok)
|
|
31894
|
+
log.warn(id, `landing question: no merge base with ${defaultBr}: ${base.err.trim()}`);
|
|
31624
31895
|
const committed = base.ok && base.out ? await git(path, "diff", "--stat", base.out, "HEAD") : null;
|
|
31625
|
-
const
|
|
31626
|
-
|
|
31627
|
-
${
|
|
31628
|
-
dirty.out ? `Uncommitted:
|
|
31629
|
-
${dirty.out}` : "",
|
|
31630
|
-
untracked.out ? `New files:
|
|
31631
|
-
${untracked.out}` : ""
|
|
31632
|
-
].filter(Boolean);
|
|
31633
|
-
const text = parts.join(`
|
|
31896
|
+
const own = [dirty.out ? `Uncommitted:
|
|
31897
|
+
${dirty.out}` : "", untracked.out ? `New files:
|
|
31898
|
+
${untracked.out}` : ""].filter(Boolean).join(`
|
|
31634
31899
|
|
|
31635
31900
|
`);
|
|
31636
|
-
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)}
|
|
31637
31905
|
...` : text;
|
|
31638
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
|
+
}
|
|
31639
31914
|
async function recentSubjects(path, defaultBr) {
|
|
31640
31915
|
const r = await git(path, "log", "--format=%s", "-n", "5", defaultBr);
|
|
31641
31916
|
return r.ok ? r.out.split(`
|
|
@@ -32370,17 +32645,41 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
|
|
|
32370
32645
|
}
|
|
32371
32646
|
return { ok: true, message: `merged ${branch} into ${defaultBr}` };
|
|
32372
32647
|
}
|
|
32373
|
-
async function squashMessage(worktreePath, defaultBr, suggested) {
|
|
32648
|
+
async function squashMessage(worktreePath, defaultBr, suggested, typed = false) {
|
|
32374
32649
|
const n = await git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`);
|
|
32375
32650
|
if (n.out === "1") {
|
|
32651
|
+
if (typed && suggested)
|
|
32652
|
+
return suggested;
|
|
32376
32653
|
const one = await git(worktreePath, "log", "-1", "--format=%B");
|
|
32377
32654
|
if (one.ok && one.out.trim())
|
|
32378
32655
|
return one.out.trim();
|
|
32379
32656
|
}
|
|
32380
|
-
|
|
32381
|
-
|
|
32382
|
-
|
|
32383
|
-
|
|
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 } : {} };
|
|
32384
32683
|
}
|
|
32385
32684
|
async function fastForwardFetched(repoPath, defaultBr) {
|
|
32386
32685
|
const current = await git(repoPath, "branch", "--show-current");
|
|
@@ -33104,9 +33403,19 @@ class SparePool {
|
|
|
33104
33403
|
const added = (async () => {
|
|
33105
33404
|
if (existsSync32(wt.path))
|
|
33106
33405
|
return;
|
|
33107
|
-
|
|
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
|
+
}
|
|
33108
33416
|
})();
|
|
33109
33417
|
const done = (async () => {
|
|
33418
|
+
this.d.runtime.markSetup(wt.id, true);
|
|
33110
33419
|
try {
|
|
33111
33420
|
await added;
|
|
33112
33421
|
if (after) {
|
|
@@ -33136,6 +33445,7 @@ class SparePool {
|
|
|
33136
33445
|
} catch (e) {
|
|
33137
33446
|
await this.rollBack(wt, e);
|
|
33138
33447
|
} finally {
|
|
33448
|
+
this.d.runtime.markSetup(wt.id, false);
|
|
33139
33449
|
if (this.inflight.get(repoId)?.added === added)
|
|
33140
33450
|
this.inflight.delete(repoId);
|
|
33141
33451
|
}
|
|
@@ -33194,40 +33504,70 @@ class SparePool {
|
|
|
33194
33504
|
this.refreshing.set(repoId, run);
|
|
33195
33505
|
await run;
|
|
33196
33506
|
}
|
|
33197
|
-
async claim(repoId, title, worktreeId) {
|
|
33507
|
+
async claim(repoId, title, worktreeId, stamps, prepare) {
|
|
33198
33508
|
const wt = this.spareOf(repoId);
|
|
33199
33509
|
if (!wt)
|
|
33200
33510
|
return null;
|
|
33201
33511
|
if (worktreeId && wt.id !== worktreeId)
|
|
33202
33512
|
return null;
|
|
33203
33513
|
const wasReady = wt.phase === "ready";
|
|
33204
|
-
if (!wasReady)
|
|
33205
|
-
|
|
33206
|
-
return null;
|
|
33207
|
-
const running = this.inflight.get(repoId);
|
|
33208
|
-
await running?.added.catch(() => {});
|
|
33209
|
-
running?.cut();
|
|
33210
|
-
if (wt.kind !== "spare" || !existsSync32(wt.path))
|
|
33211
|
-
return null;
|
|
33212
|
-
}
|
|
33213
|
-
await this.refreshing.get(repoId);
|
|
33514
|
+
if (!wasReady && !worktreeId)
|
|
33515
|
+
return null;
|
|
33214
33516
|
const repo = this.d.state.requireRepo(repoId);
|
|
33215
|
-
const
|
|
33216
|
-
|
|
33517
|
+
const running = this.inflight.get(repoId);
|
|
33518
|
+
const was = { title: wt.title, branch: wt.branch, phase: wt.phase, lockfile: wt.lockfile };
|
|
33217
33519
|
wt.kind = "worktree";
|
|
33218
33520
|
delete wt.phase;
|
|
33219
33521
|
delete wt.lockfile;
|
|
33220
|
-
wt.branch =
|
|
33522
|
+
wt.branch = `toyon/${basename11(wt.path)}`;
|
|
33221
33523
|
wt.title = title;
|
|
33222
|
-
wt.unnamed = true;
|
|
33223
33524
|
wt.createdAt = Date.now();
|
|
33224
|
-
|
|
33225
|
-
|
|
33525
|
+
wt.promptedAt = wt.createdAt;
|
|
33526
|
+
Object.assign(wt, Object.fromEntries(Object.entries(stamps).filter(([, v]) => v !== undefined)));
|
|
33226
33527
|
const next = repo.needsSetup ? null : await this.record(repoId, repo);
|
|
33227
33528
|
this.d.state.save();
|
|
33228
33529
|
if (next)
|
|
33229
33530
|
fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
|
|
33230
|
-
|
|
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));
|
|
33231
33571
|
}
|
|
33232
33572
|
}
|
|
33233
33573
|
|
|
@@ -33365,23 +33705,8 @@ class TurnService {
|
|
|
33365
33705
|
stoppedByHand = new Set;
|
|
33366
33706
|
constructor(d) {
|
|
33367
33707
|
this.d = d;
|
|
33368
|
-
this.boot();
|
|
33369
33708
|
d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
|
|
33370
33709
|
}
|
|
33371
|
-
boot() {
|
|
33372
|
-
let changed = false;
|
|
33373
|
-
for (const wt of this.d.state.worktrees) {
|
|
33374
|
-
const turn = wt.lastTurn;
|
|
33375
|
-
if (turn?.end !== "asking")
|
|
33376
|
-
continue;
|
|
33377
|
-
const { ask: _expired, ...facts } = turn.facts;
|
|
33378
|
-
turn.end = "stopped";
|
|
33379
|
-
turn.facts = facts;
|
|
33380
|
-
changed = true;
|
|
33381
|
-
}
|
|
33382
|
-
if (changed)
|
|
33383
|
-
this.d.state.save();
|
|
33384
|
-
}
|
|
33385
33710
|
onStatus(worktreeId, status) {
|
|
33386
33711
|
const prev = this.lastStatus.get(worktreeId) ?? "idle";
|
|
33387
33712
|
this.lastStatus.set(worktreeId, status);
|
|
@@ -33468,6 +33793,7 @@ var idsOf = (rows, trunks) => [
|
|
|
33468
33793
|
...rows.map((r) => r.id),
|
|
33469
33794
|
...Object.values(trunks).map((t) => t.id)
|
|
33470
33795
|
];
|
|
33796
|
+
var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
|
|
33471
33797
|
var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
|
|
33472
33798
|
`, 1)[0])).join(" ");
|
|
33473
33799
|
|
|
@@ -33482,6 +33808,8 @@ class WorktreeService {
|
|
|
33482
33808
|
archiving = new Map;
|
|
33483
33809
|
restoring = new Map;
|
|
33484
33810
|
going = new Set;
|
|
33811
|
+
arriving = new Set;
|
|
33812
|
+
firstPrompt = new Map;
|
|
33485
33813
|
counts;
|
|
33486
33814
|
handChecked = new Map;
|
|
33487
33815
|
naming = new Set;
|
|
@@ -33496,7 +33824,7 @@ class WorktreeService {
|
|
|
33496
33824
|
this.archive = new WorktreeArchive(d.paths.archiveDir);
|
|
33497
33825
|
this.counts = new Counts({
|
|
33498
33826
|
read: (id) => this.readCounts(id),
|
|
33499
|
-
going: (id) => this.going.has(id)
|
|
33827
|
+
going: (id) => this.going.has(id) || this.arriving.has(id)
|
|
33500
33828
|
});
|
|
33501
33829
|
this.trunk = new Trunk({
|
|
33502
33830
|
state: d.state,
|
|
@@ -33509,6 +33837,7 @@ class WorktreeService {
|
|
|
33509
33837
|
hub: d.hub,
|
|
33510
33838
|
runtime: d.runtime,
|
|
33511
33839
|
paths: d.paths,
|
|
33840
|
+
runs: this.runs,
|
|
33512
33841
|
setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
|
|
33513
33842
|
teardown: (wt, repo) => this.teardown(wt, repo),
|
|
33514
33843
|
discard: async (id) => {
|
|
@@ -33580,39 +33909,55 @@ class WorktreeService {
|
|
|
33580
33909
|
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
33581
33910
|
|
|
33582
33911
|
`) : prompt;
|
|
33583
|
-
const
|
|
33584
|
-
|
|
33585
|
-
|
|
33586
|
-
|
|
33587
|
-
|
|
33588
|
-
|
|
33589
|
-
|
|
33590
|
-
|
|
33591
|
-
|
|
33592
|
-
|
|
33593
|
-
|
|
33594
|
-
|
|
33595
|
-
|
|
33596
|
-
|
|
33597
|
-
|
|
33598
|
-
|
|
33599
|
-
|
|
33600
|
-
|
|
33601
|
-
if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
|
|
33602
|
-
claimed.profile = profile;
|
|
33603
|
-
this.restartProcs(claimed, repo);
|
|
33604
|
-
}
|
|
33605
|
-
const carried = await this.carryMain(repo, claimed, !!opts.carry);
|
|
33606
|
-
this.d.state.save();
|
|
33607
|
-
this.d.hub.emit("worktreesChanged");
|
|
33608
|
-
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();
|
|
33609
33930
|
if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
|
|
33610
33931
|
await rt.agent.restart();
|
|
33611
|
-
|
|
33612
|
-
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
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;
|
|
33616
33961
|
}
|
|
33617
33962
|
if (!await isGitRepo(repo.path)) {
|
|
33618
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.`);
|
|
@@ -33773,8 +34118,9 @@ class WorktreeService {
|
|
|
33773
34118
|
this.launch(wt, repo, repo.path);
|
|
33774
34119
|
return wt;
|
|
33775
34120
|
}
|
|
33776
|
-
async requireDiscovered(repoId, repoPath, target) {
|
|
33777
|
-
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);
|
|
33778
34124
|
const found = rows.find((r) => canonical(r.path) === target);
|
|
33779
34125
|
if (!found)
|
|
33780
34126
|
throw new UserError("that worktree is gone, or Toyon already has it");
|
|
@@ -33789,38 +34135,50 @@ class WorktreeService {
|
|
|
33789
34135
|
const repoId = r.repoId;
|
|
33790
34136
|
const repo = this.d.state.requireRepo(repoId);
|
|
33791
34137
|
const target = canonical(r.path);
|
|
33792
|
-
const
|
|
33793
|
-
|
|
33794
|
-
|
|
33795
|
-
|
|
33796
|
-
}
|
|
33797
|
-
|
|
33798
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
33806
|
-
|
|
33807
|
-
|
|
33808
|
-
|
|
33809
|
-
|
|
33810
|
-
|
|
33811
|
-
|
|
33812
|
-
|
|
33813
|
-
|
|
33814
|
-
|
|
33815
|
-
};
|
|
33816
|
-
return rec;
|
|
33817
|
-
});
|
|
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);
|
|
33818
34161
|
this.invalidateDiscovered();
|
|
33819
|
-
this.
|
|
33820
|
-
|
|
33821
|
-
|
|
33822
|
-
|
|
33823
|
-
|
|
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();
|
|
33824
34182
|
return wt;
|
|
33825
34183
|
}
|
|
33826
34184
|
async ownProcs(worktreeId, names) {
|
|
@@ -33852,8 +34210,8 @@ class WorktreeService {
|
|
|
33852
34210
|
throw new UserError(`no profile "${name}" in ${repo.configFile}`);
|
|
33853
34211
|
return name;
|
|
33854
34212
|
}
|
|
33855
|
-
restartProcs(wt
|
|
33856
|
-
fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.
|
|
34213
|
+
restartProcs(wt) {
|
|
34214
|
+
fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.wake(wt.id)), "profile restart");
|
|
33857
34215
|
}
|
|
33858
34216
|
setProfile(worktreeId, name) {
|
|
33859
34217
|
const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
|
|
@@ -33865,7 +34223,7 @@ class WorktreeService {
|
|
|
33865
34223
|
wt.profile = profile;
|
|
33866
34224
|
this.d.state.save();
|
|
33867
34225
|
this.d.hub.emit("worktreesChanged");
|
|
33868
|
-
this.restartProcs(wt
|
|
34226
|
+
this.restartProcs(wt);
|
|
33869
34227
|
}
|
|
33870
34228
|
setMode(worktreeId, mode) {
|
|
33871
34229
|
if (!this.d.state.setMode(worktreeId, mode))
|
|
@@ -34235,66 +34593,113 @@ class WorktreeService {
|
|
|
34235
34593
|
if (rec.auto)
|
|
34236
34594
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
34237
34595
|
const path = existsSync33(old.path) ? join30(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
|
|
34238
|
-
const branch = await withRepoLock(repo.path, async () => {
|
|
34239
|
-
if (!await commitOf(repo.path, archiveRef(old.id))) {
|
|
34240
|
-
throw new UserError(`${old.title}'s kept commits are gone from git`);
|
|
34241
|
-
}
|
|
34242
|
-
let name = old.branch;
|
|
34243
|
-
let create = true;
|
|
34244
|
-
const tip = await commitOf(repo.path, `refs/heads/${name}`);
|
|
34245
|
-
if (tip && hasOwnBranch(old)) {
|
|
34246
|
-
name = `${name}-${shortId().slice(0, 3)}`;
|
|
34247
|
-
} else if (tip) {
|
|
34248
|
-
if (tip !== kept.head && kept.snapshot) {
|
|
34249
|
-
throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
|
|
34250
|
-
}
|
|
34251
|
-
create = false;
|
|
34252
|
-
}
|
|
34253
|
-
try {
|
|
34254
|
-
await checkOutKept(repo.path, path, kept, { name, create });
|
|
34255
|
-
} catch (e) {
|
|
34256
|
-
await git(repo.path, "worktree", "remove", "--force", path);
|
|
34257
|
-
if (create)
|
|
34258
|
-
await git(repo.path, "branch", "-D", name);
|
|
34259
|
-
throw e;
|
|
34260
|
-
}
|
|
34261
|
-
return name;
|
|
34262
|
-
});
|
|
34263
34596
|
const { variant: _variant, ...rest } = old;
|
|
34264
34597
|
const wt = {
|
|
34265
34598
|
...rest,
|
|
34266
34599
|
repoId: repo.id,
|
|
34267
34600
|
path,
|
|
34268
|
-
branch,
|
|
34601
|
+
branch: old.branch,
|
|
34269
34602
|
proxyPort: await allocateProxyPort(),
|
|
34270
34603
|
promptedAt: Date.now(),
|
|
34271
34604
|
parked: true,
|
|
34272
34605
|
...createdBy ? { createdBy } : {}
|
|
34273
34606
|
};
|
|
34607
|
+
const chat = { ...this.chatFiles(old.id), plans: this.plansScratch(old.id) };
|
|
34274
34608
|
try {
|
|
34275
|
-
this.archive.take(old.id,
|
|
34609
|
+
this.archive.take(old.id, chat);
|
|
34276
34610
|
} catch (e) {
|
|
34277
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`);
|
|
34278
34613
|
}
|
|
34279
34614
|
if (rec.sessionId && path === old.path)
|
|
34280
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));
|
|
34281
34635
|
const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
|
|
34282
34636
|
if (!dropped.ok)
|
|
34283
34637
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
34284
34638
|
this.counts.stale(wt.id);
|
|
34285
|
-
this.
|
|
34286
|
-
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({
|
|
34287
34643
|
type: "restored",
|
|
34288
34644
|
archivedAt: rec.archivedAt,
|
|
34289
34645
|
branch,
|
|
34290
34646
|
uncommitted: !!kept.snapshot,
|
|
34291
34647
|
ts: Date.now()
|
|
34292
34648
|
});
|
|
34293
|
-
|
|
34294
|
-
|
|
34295
|
-
this.d.hub.emit("archiveChanged", repo.id);
|
|
34649
|
+
release();
|
|
34650
|
+
this.d.hub.emit("filesChanged", wt.id);
|
|
34296
34651
|
return wt;
|
|
34297
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
|
+
}
|
|
34298
34703
|
async deleteArchived(archiveId) {
|
|
34299
34704
|
const rec = this.archive.get(archiveId);
|
|
34300
34705
|
if (!rec)
|
|
@@ -34432,7 +34837,9 @@ class WorktreeService {
|
|
|
34432
34837
|
const srcNm = join30(depsSource, "node_modules");
|
|
34433
34838
|
const dstNm = join30(wt.path, "node_modules");
|
|
34434
34839
|
if (existsSync33(srcNm) && !existsSync33(dstNm)) {
|
|
34840
|
+
const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
|
|
34435
34841
|
const r = await cloneTree(srcNm, dstNm);
|
|
34842
|
+
run.finish(0);
|
|
34436
34843
|
if (r.ok)
|
|
34437
34844
|
this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
|
|
34438
34845
|
else
|
|
@@ -34711,9 +35118,10 @@ ${wt.landing.body}` : s;
|
|
|
34711
35118
|
const policy = landPolicy(repo.config);
|
|
34712
35119
|
const own = hasOwnBranch(wt);
|
|
34713
35120
|
const w = this.landWatch(wt);
|
|
34714
|
-
const
|
|
35121
|
+
const typed = message?.trim();
|
|
35122
|
+
const suggested = typed || (wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
|
|
34715
35123
|
|
|
34716
|
-
${wt.landing.body}` : wt.landing.subject : undefined;
|
|
35124
|
+
${wt.landing.body}` : wt.landing.subject : undefined);
|
|
34717
35125
|
const base = baseOf(repo);
|
|
34718
35126
|
if (policy.land === "pr") {
|
|
34719
35127
|
if (wt.pr?.state === "merged" && !prTaken(wt)) {
|
|
@@ -34725,7 +35133,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34725
35133
|
return { result: fetched };
|
|
34726
35134
|
if (wt.pr?.state === "open") {
|
|
34727
35135
|
const dirty = (await statusFiles(wt.path)).length > 0;
|
|
34728
|
-
const missing = dirty || (await aheadUpstream(wt.path) ?? 0) > 0;
|
|
35136
|
+
const missing = dirty || (await aheadUpstream(wt.path, wt.branch) ?? 0) > 0;
|
|
34729
35137
|
if (!missing)
|
|
34730
35138
|
return { result: await mergePr(wt.path, wt.pr.number, policy.merge, w) };
|
|
34731
35139
|
const committed = await this.commitIfDirty(wt, message);
|
|
@@ -34757,13 +35165,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34757
35165
|
const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
|
|
34758
35166
|
return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
|
|
34759
35167
|
}
|
|
35168
|
+
const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
|
|
34760
35169
|
const result = await openPr({
|
|
34761
35170
|
worktreePath: wt.path,
|
|
34762
35171
|
branch: wt.branch,
|
|
34763
35172
|
defaultBr: repo.defaultBranch,
|
|
34764
35173
|
base,
|
|
34765
|
-
subject:
|
|
34766
|
-
body:
|
|
35174
|
+
subject: named?.subject,
|
|
35175
|
+
body: named?.body,
|
|
34767
35176
|
automerge: policy.automerge,
|
|
34768
35177
|
method: policy.merge
|
|
34769
35178
|
}, w);
|
|
@@ -34792,7 +35201,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34792
35201
|
if (!taken.ok)
|
|
34793
35202
|
return taken;
|
|
34794
35203
|
const mark = await landingMark(wt.path, base);
|
|
34795
|
-
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
|
|
35204
|
+
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
34796
35205
|
const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
|
|
34797
35206
|
if (!landed.ok)
|
|
34798
35207
|
return landed;
|
|
@@ -34843,7 +35252,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34843
35252
|
sha = await commitOf(wt.path, base) ?? "";
|
|
34844
35253
|
break;
|
|
34845
35254
|
}
|
|
34846
|
-
squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
|
|
35255
|
+
squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
34847
35256
|
const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
|
|
34848
35257
|
await this.headMoved(wt.id);
|
|
34849
35258
|
if (!built.ok || !built.sha)
|
|
@@ -34895,8 +35304,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34895
35304
|
async landedWhat(repo, wt, mark, opts) {
|
|
34896
35305
|
if (opts.pr?.title)
|
|
34897
35306
|
return [opts.pr.title];
|
|
34898
|
-
const subject = opts.squash
|
|
34899
|
-
`)[0]?.trim();
|
|
35307
|
+
const subject = opts.squash ? splitMessage(opts.squash).subject : "";
|
|
34900
35308
|
if (subject)
|
|
34901
35309
|
return [subject];
|
|
34902
35310
|
const read = await rangeSubjects(repo.path, mark.base, mark.tip);
|
|
@@ -35157,9 +35565,10 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35157
35565
|
return { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty };
|
|
35158
35566
|
}
|
|
35159
35567
|
async unpushed(id, path) {
|
|
35160
|
-
|
|
35568
|
+
const wt = this.d.state.worktree(id);
|
|
35569
|
+
if (wt?.pr?.state !== "open")
|
|
35161
35570
|
return;
|
|
35162
|
-
return await aheadUpstream(path) ?? undefined;
|
|
35571
|
+
return await aheadUpstream(path, wt.branch) ?? undefined;
|
|
35163
35572
|
}
|
|
35164
35573
|
async freshCounts(worktreeId) {
|
|
35165
35574
|
if (this.going.has(worktreeId))
|
|
@@ -35202,7 +35611,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35202
35611
|
await markTurn(wt.path, wt.id);
|
|
35203
35612
|
}
|
|
35204
35613
|
async gitStatus(worktreeId) {
|
|
35205
|
-
if (this.going.has(worktreeId))
|
|
35614
|
+
if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
|
|
35206
35615
|
return null;
|
|
35207
35616
|
const r = this.readable(worktreeId);
|
|
35208
35617
|
if (!r)
|
|
@@ -35245,7 +35654,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35245
35654
|
}
|
|
35246
35655
|
if (r.wt && isMain)
|
|
35247
35656
|
this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
|
|
35248
|
-
if (this.going.has(worktreeId))
|
|
35657
|
+
if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
|
|
35249
35658
|
return null;
|
|
35250
35659
|
this.noteCounts(worktreeId, isMain, files.length, counts);
|
|
35251
35660
|
return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
|
|
@@ -35291,6 +35700,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35291
35700
|
const wt = this.requireRun(worktreeId);
|
|
35292
35701
|
if (wt.kind === "main")
|
|
35293
35702
|
throw new UserError("nothing runs on main: the plus starts a worktree for it");
|
|
35703
|
+
await this.firstPrompt.get(worktreeId);
|
|
35294
35704
|
const agent = this.d.runtime.agentFor(worktreeId);
|
|
35295
35705
|
if (!agent)
|
|
35296
35706
|
throw new UserError("worktree still starting; try again in a moment");
|
|
@@ -35496,7 +35906,8 @@ var {
|
|
|
35496
35906
|
helperStub: HELPER_STUB,
|
|
35497
35907
|
cliEntry: CLI_ENTRY,
|
|
35498
35908
|
sourceRoot: SOURCE_ROOT,
|
|
35499
|
-
packageJson: PACKAGE_JSON
|
|
35909
|
+
packageJson: PACKAGE_JSON,
|
|
35910
|
+
changelog: CHANGELOG
|
|
35500
35911
|
} = locateAssets(here);
|
|
35501
35912
|
var pruned = pruneAssets(SHELL_DIST);
|
|
35502
35913
|
if (pruned > 0)
|
|
@@ -35740,6 +36151,7 @@ var {
|
|
|
35740
36151
|
port,
|
|
35741
36152
|
token,
|
|
35742
36153
|
shellDist: SHELL_DIST,
|
|
36154
|
+
changelog: CHANGELOG,
|
|
35743
36155
|
version: package_default.version,
|
|
35744
36156
|
noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
|
|
35745
36157
|
startLink: () => helper?.startLink() ?? null,
|