toyon 0.14.0 → 0.16.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 +629 -0
- package/dist/bridge.js +1 -1
- package/dist/cli.js +41 -22
- package/dist/daemon.js +1233 -971
- package/dist/shell/assets/{Editor-Gzhl3Oqu.js → Editor-BATywBIx.js} +3 -3
- package/dist/shell/assets/{XTerm-D6TL52yQ.js → XTerm-So8-Vafj.js} +1 -1
- package/dist/shell/assets/{cssMode-BfNm0a39.js → cssMode-DlHcrkXj.js} +1 -1
- package/dist/shell/assets/{freemarker2-DhiwR6KJ.js → freemarker2-D3xOnVxc.js} +1 -1
- package/dist/shell/assets/{grammar-thv9InXF.js → grammar-RZJ5oG33.js} +1 -1
- package/dist/shell/assets/{handlebars-BbkhCZXb.js → handlebars-UbIG7080.js} +1 -1
- package/dist/shell/assets/{html-yzhYlR33.js → html-D92eLK6U.js} +1 -1
- package/dist/shell/assets/{htmlMode-DPICAUY8.js → htmlMode-DsZIJzoj.js} +1 -1
- package/dist/shell/assets/index-BvYzBRq_.css +1 -0
- package/dist/shell/assets/index-CHQwm2uN.js +268 -0
- package/dist/shell/assets/{javascript-D9eBjR4g.js → javascript-DUkgFtV2.js} +1 -1
- package/dist/shell/assets/{jsonMode-cNulMi5B.js → jsonMode-BiIooPg9.js} +1 -1
- package/dist/shell/assets/{liquid-C_SKIx1n.js → liquid-DFFjZ5TH.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-DNU8BRHj.js → lspLanguageFeatures-C7Ug4qkg.js} +1 -1
- package/dist/shell/assets/{mdx-Bt4opbQO.js → mdx-DdjSgAuG.js} +1 -1
- package/dist/shell/assets/{python-Crb-EuXe.js → python-DPUF4OK9.js} +1 -1
- package/dist/shell/assets/{razor-D3BlkbYO.js → razor-IRYJ0pOA.js} +1 -1
- package/dist/shell/assets/{tsMode-BYuAS95U.js → tsMode-CljE7MPO.js} +1 -1
- package/dist/shell/assets/{xml-G5xafDqw.js → xml-D8Zv3Gd0.js} +1 -1
- package/dist/shell/assets/{yaml-CRp1x3nR.js → yaml-Dxx9PL0B.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
|
@@ -11,14 +11,10 @@ var TOOL_SEARCH = "ToolSearch";
|
|
|
11
11
|
var ASK_TOOL = "AskUserQuestion";
|
|
12
12
|
var TOYON_MCP_SERVER = "toyon";
|
|
13
13
|
var HANDOFF_TOOL = "handoff";
|
|
14
|
-
var PREVIEW_TOOL = "preview";
|
|
15
14
|
|
|
16
15
|
// ../shared/src/agent-tools.ts
|
|
17
16
|
var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
|
|
18
17
|
var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
|
|
19
|
-
function isEditTool(ev) {
|
|
20
|
-
return ev.kind ? EDIT_KINDS.has(ev.kind) : EDIT_TOOLS.has(ev.name);
|
|
21
|
-
}
|
|
22
18
|
var WRITE_KINDS = new Set(["edit", "delete", "move"]);
|
|
23
19
|
var WRITE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
24
20
|
function isWriteTool(ev) {
|
|
@@ -29,7 +25,7 @@ function isWrittenKind(kind) {
|
|
|
29
25
|
return WRITTEN_KINDS.includes(kind ?? "");
|
|
30
26
|
}
|
|
31
27
|
function writingCall(call, input) {
|
|
32
|
-
return (isWrittenKind(call.kind) || call.name === ASK_TOOL) && emptyInput(input);
|
|
28
|
+
return (isWrittenKind(call.kind) || call.name === ASK_TOOL || call.name === TOOL_SEARCH) && emptyInput(input);
|
|
33
29
|
}
|
|
34
30
|
function emptyInput(input) {
|
|
35
31
|
if (!input || typeof input !== "object")
|
|
@@ -6602,11 +6598,9 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
|
|
|
6602
6598
|
}
|
|
6603
6599
|
|
|
6604
6600
|
// ../shared/src/protocol/limits.ts
|
|
6605
|
-
var PROTOCOL_VERSION =
|
|
6601
|
+
var PROTOCOL_VERSION = 112;
|
|
6606
6602
|
var PAGE_ERROR_MAX_CHARS = 2000;
|
|
6607
6603
|
var PAGE_TITLE_MAX_CHARS = 1000;
|
|
6608
|
-
var RENDER_ERRORS_MAX = 10;
|
|
6609
|
-
var PAGE_URL_MAX_CHARS = 2048;
|
|
6610
6604
|
var FILE_MAX_CHARS = 5000000;
|
|
6611
6605
|
var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
6612
6606
|
var IMAGE_MAX_EDGE = 2576;
|
|
@@ -6961,6 +6955,8 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6961
6955
|
object({ t: literal("fix"), worktreeId: id, toolId: string2().min(1).max(200) }),
|
|
6962
6956
|
object({ t: literal("continue"), worktreeId: id }),
|
|
6963
6957
|
object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
|
|
6958
|
+
object({ t: literal("reveal-path"), path: relPath }),
|
|
6959
|
+
object({ t: literal("reveal-attachment"), worktreeId: id, file: string2().min(1).max(120) }),
|
|
6964
6960
|
object({ t: literal("stop-agent"), worktreeId: id }),
|
|
6965
6961
|
object({ t: literal("seen"), worktreeId: id }),
|
|
6966
6962
|
object({ t: literal("mark-unread"), worktreeId: id }),
|
|
@@ -6977,17 +6973,6 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6977
6973
|
object({ t: literal("forget-visit"), repoId: id, path: routePath }),
|
|
6978
6974
|
object({ t: literal("page-error"), worktreeId: id, message: string2().min(1).max(PAGE_ERROR_MAX_CHARS) }),
|
|
6979
6975
|
object({ t: literal("page-loaded"), worktreeId: id }),
|
|
6980
|
-
object({ t: literal("fix-page"), worktreeId: id }),
|
|
6981
|
-
object({
|
|
6982
|
-
t: literal("rendered"),
|
|
6983
|
-
worktreeId: id,
|
|
6984
|
-
id: string2().min(1).max(64),
|
|
6985
|
-
ok: boolean2(),
|
|
6986
|
-
reason: string2().max(200).optional(),
|
|
6987
|
-
url: string2().max(PAGE_URL_MAX_CHARS).optional(),
|
|
6988
|
-
title: pageTitle.optional(),
|
|
6989
|
-
errors: array(string2().max(PAGE_ERROR_MAX_CHARS)).max(RENDER_ERRORS_MAX)
|
|
6990
|
-
}),
|
|
6991
6976
|
object({ t: literal("pick-variant"), worktreeId: id }),
|
|
6992
6977
|
object({
|
|
6993
6978
|
t: literal("unqueue"),
|
|
@@ -7059,6 +7044,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
7059
7044
|
}),
|
|
7060
7045
|
object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
|
|
7061
7046
|
object({ t: literal("agent-logout"), agent: id }),
|
|
7047
|
+
object({ t: literal("agent-check"), agent: id }),
|
|
7062
7048
|
object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
|
|
7063
7049
|
object({ t: literal("reveal-agent-file"), agent: id, file: string2().max(64), repoId: id.optional() }),
|
|
7064
7050
|
object({ t: literal("import-theme"), name: string2().max(300), source: string2().max(2000000) }),
|
|
@@ -8528,7 +8514,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
|
|
|
8528
8514
|
// ../daemon/package.json
|
|
8529
8515
|
var package_default = {
|
|
8530
8516
|
name: "@toyon/daemon",
|
|
8531
|
-
version: "0.
|
|
8517
|
+
version: "0.16.0",
|
|
8532
8518
|
private: true,
|
|
8533
8519
|
type: "module",
|
|
8534
8520
|
main: "src/index.ts",
|
|
@@ -8544,6 +8530,10 @@ var package_default = {
|
|
|
8544
8530
|
}
|
|
8545
8531
|
};
|
|
8546
8532
|
|
|
8533
|
+
// ../daemon/src/agent/acp/transport.ts
|
|
8534
|
+
import { spawn } from "child_process";
|
|
8535
|
+
import { Readable, Writable } from "stream";
|
|
8536
|
+
|
|
8547
8537
|
// ../../node_modules/.bun/@agentclientprotocol+sdk@1.4.0+ab3ed8e9421bfd7b/node_modules/@agentclientprotocol/sdk/dist/schema/index.js
|
|
8548
8538
|
var AGENT_METHODS = {
|
|
8549
8539
|
initialize: "initialize",
|
|
@@ -12620,16 +12610,6 @@ var legacyClientNotificationMethods = new Set([
|
|
|
12620
12610
|
CLIENT_METHODS.elicitation_complete
|
|
12621
12611
|
]);
|
|
12622
12612
|
|
|
12623
|
-
// ../daemon/src/core/errors.ts
|
|
12624
|
-
class UserError extends Error {
|
|
12625
|
-
delivered;
|
|
12626
|
-
constructor(message, about = {}) {
|
|
12627
|
-
super(message);
|
|
12628
|
-
this.name = "UserError";
|
|
12629
|
-
this.delivered = about.delivered ?? false;
|
|
12630
|
-
}
|
|
12631
|
-
}
|
|
12632
|
-
|
|
12633
12613
|
// ../daemon/src/core/log.ts
|
|
12634
12614
|
var LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
12635
12615
|
function envLevel() {
|
|
@@ -12675,131 +12655,6 @@ function fireAndForget(tag, p, what = "background task") {
|
|
|
12675
12655
|
p.catch((e) => log.warn(tag, `${what} failed`, e));
|
|
12676
12656
|
}
|
|
12677
12657
|
|
|
12678
|
-
// ../daemon/src/agent/acp/authstatus.ts
|
|
12679
|
-
var AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
|
|
12680
|
-
var KINDS = new Set(["none", "api_key", "account", "external", "gateway"]);
|
|
12681
|
-
function supportsLogout(caps) {
|
|
12682
|
-
return !!caps?.auth?.logout;
|
|
12683
|
-
}
|
|
12684
|
-
var str = (v) => typeof v === "string" && v.trim() ? v : undefined;
|
|
12685
|
-
function parseAuthStatus(params) {
|
|
12686
|
-
const status = params?.authStatus;
|
|
12687
|
-
if (!status || typeof status !== "object")
|
|
12688
|
-
return null;
|
|
12689
|
-
const kind = status.kind;
|
|
12690
|
-
if (!KINDS.has(kind))
|
|
12691
|
-
return null;
|
|
12692
|
-
const account = status.account ?? undefined;
|
|
12693
|
-
const fields = account ? {
|
|
12694
|
-
...str(account.email) ? { email: str(account.email) } : {},
|
|
12695
|
-
...str(account.organization) ? { organization: str(account.organization) } : {},
|
|
12696
|
-
...str(account.plan) ? { plan: str(account.plan) } : {}
|
|
12697
|
-
} : {};
|
|
12698
|
-
return {
|
|
12699
|
-
kind,
|
|
12700
|
-
label: str(status.label) ?? (kind === "none" ? "not logged in" : "logged in"),
|
|
12701
|
-
...str(status.detail) ? { detail: str(status.detail) } : {},
|
|
12702
|
-
...Object.keys(fields).length ? { account: fields } : {}
|
|
12703
|
-
};
|
|
12704
|
-
}
|
|
12705
|
-
|
|
12706
|
-
// ../daemon/src/agent/accounts.ts
|
|
12707
|
-
class AgentAccounts {
|
|
12708
|
-
d;
|
|
12709
|
-
known = new Map;
|
|
12710
|
-
busy = new Map;
|
|
12711
|
-
onChange = null;
|
|
12712
|
-
constructor(d) {
|
|
12713
|
-
this.d = d;
|
|
12714
|
-
}
|
|
12715
|
-
observe(agentId, o) {
|
|
12716
|
-
const before = this.known.get(agentId) ?? { canLogout: false, status: undefined };
|
|
12717
|
-
const entry = { ...before };
|
|
12718
|
-
if (o.canLogout !== undefined)
|
|
12719
|
-
entry.canLogout = o.canLogout;
|
|
12720
|
-
if (o.status)
|
|
12721
|
-
entry.status = o.status;
|
|
12722
|
-
this.known.set(agentId, entry);
|
|
12723
|
-
if (before.canLogout !== entry.canLogout || !same(before.status, entry.status))
|
|
12724
|
-
this.onChange?.();
|
|
12725
|
-
}
|
|
12726
|
-
observeLimits(agentId, limits) {
|
|
12727
|
-
const before = this.known.get(agentId) ?? { canLogout: false, status: undefined };
|
|
12728
|
-
const entry = { ...before, limits };
|
|
12729
|
-
this.known.set(agentId, entry);
|
|
12730
|
-
if (!sameLimits(before.limits, limits))
|
|
12731
|
-
this.onChange?.();
|
|
12732
|
-
}
|
|
12733
|
-
describe(infos) {
|
|
12734
|
-
return infos.map((info) => {
|
|
12735
|
-
const k = this.known.get(info.id);
|
|
12736
|
-
if (!k)
|
|
12737
|
-
return info;
|
|
12738
|
-
return {
|
|
12739
|
-
...info,
|
|
12740
|
-
...k.status ? { auth: k.status } : {},
|
|
12741
|
-
...k.canLogout ? { canLogout: true } : {},
|
|
12742
|
-
...k.limits ? { limits: k.limits } : {}
|
|
12743
|
-
};
|
|
12744
|
-
});
|
|
12745
|
-
}
|
|
12746
|
-
logout(agentId) {
|
|
12747
|
-
const running = this.busy.get(agentId);
|
|
12748
|
-
if (running)
|
|
12749
|
-
return running;
|
|
12750
|
-
const p = this.runLogout(agentId).finally(() => this.busy.delete(agentId));
|
|
12751
|
-
this.busy.set(agentId, p);
|
|
12752
|
-
return p;
|
|
12753
|
-
}
|
|
12754
|
-
async runLogout(agentId) {
|
|
12755
|
-
const spec = this.d.require(agentId);
|
|
12756
|
-
let pushed = null;
|
|
12757
|
-
let signingOut = false;
|
|
12758
|
-
const app = client({ name: "toyon" }).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
|
|
12759
|
-
if (c.params && signingOut)
|
|
12760
|
-
pushed = c.params;
|
|
12761
|
-
});
|
|
12762
|
-
const link = await this.d.connect(app, spec);
|
|
12763
|
-
try {
|
|
12764
|
-
const init = await link.conn.agent.request(methods.agent.initialize, {
|
|
12765
|
-
protocolVersion: PROTOCOL_VERSION2,
|
|
12766
|
-
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
12767
|
-
clientInfo: { name: "toyon", version: "0" }
|
|
12768
|
-
});
|
|
12769
|
-
if (!supportsLogout(init.agentCapabilities))
|
|
12770
|
-
throw new UserError(`${spec.name} cannot be signed out from here`);
|
|
12771
|
-
signingOut = true;
|
|
12772
|
-
await link.conn.agent.request(methods.agent.logout, {});
|
|
12773
|
-
const entry = this.known.get(agentId);
|
|
12774
|
-
this.known.set(agentId, { canLogout: true, ...pushed ? { status: pushed } : {} });
|
|
12775
|
-
if (!entry || !same(entry.status, pushed ?? undefined))
|
|
12776
|
-
this.onChange?.();
|
|
12777
|
-
log.info("agents", `${agentId} signed out`);
|
|
12778
|
-
} catch (e) {
|
|
12779
|
-
if (e instanceof UserError)
|
|
12780
|
-
throw e;
|
|
12781
|
-
throw new UserError(`${spec.name} could not be signed out: ${e instanceof Error ? e.message : String(e)}`);
|
|
12782
|
-
} finally {
|
|
12783
|
-
await link.kill();
|
|
12784
|
-
}
|
|
12785
|
-
}
|
|
12786
|
-
}
|
|
12787
|
-
function sameLimits(a, b) {
|
|
12788
|
-
if (!a || !b)
|
|
12789
|
-
return a === b;
|
|
12790
|
-
const win = (x) => x ? `${x.used}@${x.resetsAt}` : "";
|
|
12791
|
-
return a.status === b.status && a.binding === b.binding && win(a.windows.five_hour) === win(b.windows.five_hour) && win(a.windows.seven_day) === win(b.windows.seven_day);
|
|
12792
|
-
}
|
|
12793
|
-
function same(a, b) {
|
|
12794
|
-
if (!a || !b)
|
|
12795
|
-
return a === b;
|
|
12796
|
-
return a.kind === b.kind && a.label === b.label && a.detail === b.detail && a.account?.email === b.account?.email && a.account?.organization === b.account?.organization && a.account?.plan === b.account?.plan;
|
|
12797
|
-
}
|
|
12798
|
-
|
|
12799
|
-
// ../daemon/src/agent/acp/transport.ts
|
|
12800
|
-
import { spawn } from "child_process";
|
|
12801
|
-
import { Readable, Writable } from "stream";
|
|
12802
|
-
|
|
12803
12658
|
// ../daemon/src/runtime/kill.ts
|
|
12804
12659
|
var KILL_GRACE_MS = 3000;
|
|
12805
12660
|
var EXIT_GRACE_MS = 200;
|
|
@@ -12903,6 +12758,16 @@ import { createHash } from "crypto";
|
|
|
12903
12758
|
import { copyFile as copyFile2, mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
12904
12759
|
import { join as join2 } from "path";
|
|
12905
12760
|
|
|
12761
|
+
// ../daemon/src/core/errors.ts
|
|
12762
|
+
class UserError extends Error {
|
|
12763
|
+
delivered;
|
|
12764
|
+
constructor(message, about = {}) {
|
|
12765
|
+
super(message);
|
|
12766
|
+
this.name = "UserError";
|
|
12767
|
+
this.delivered = about.delivered ?? false;
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
|
|
12906
12771
|
// ../daemon/src/agent/uploads.ts
|
|
12907
12772
|
import { randomBytes } from "crypto";
|
|
12908
12773
|
import { mkdirSync, readdirSync, rmSync as rmSync2 } from "fs";
|
|
@@ -13187,13 +13052,7 @@ class AttachmentStore {
|
|
|
13187
13052
|
const { n: _n, ...pick } = ref;
|
|
13188
13053
|
return pick;
|
|
13189
13054
|
}
|
|
13190
|
-
|
|
13191
|
-
for (const at of [this.fileFor(worktreeId, ref.file), archived?.(ref.file) ?? null]) {
|
|
13192
|
-
if (at && await Bun.file(at).exists()) {
|
|
13193
|
-
path = at;
|
|
13194
|
-
break;
|
|
13195
|
-
}
|
|
13196
|
-
}
|
|
13055
|
+
const path = await this.locate(worktreeId, ref.file, archived);
|
|
13197
13056
|
if (!path)
|
|
13198
13057
|
return null;
|
|
13199
13058
|
if (ref.kind === "paste") {
|
|
@@ -13211,6 +13070,13 @@ class AttachmentStore {
|
|
|
13211
13070
|
const { name, bytes, width, height } = ref;
|
|
13212
13071
|
return { kind: "image", upload, name, mimeType, bytes, width, height };
|
|
13213
13072
|
}
|
|
13073
|
+
async locate(worktreeId, file, archived) {
|
|
13074
|
+
for (const at of [this.fileFor(worktreeId, file), archived?.(file) ?? null]) {
|
|
13075
|
+
if (at && await Bun.file(at).exists())
|
|
13076
|
+
return at;
|
|
13077
|
+
}
|
|
13078
|
+
return null;
|
|
13079
|
+
}
|
|
13214
13080
|
async write(worktreeId, file, fill) {
|
|
13215
13081
|
const wtDir = attachmentsDirFor(this.dir, worktreeId);
|
|
13216
13082
|
const path = join2(wtDir, file);
|
|
@@ -13595,6 +13461,37 @@ async function readCapped(req, max) {
|
|
|
13595
13461
|
return Buffer.concat(chunks).toString("utf8");
|
|
13596
13462
|
}
|
|
13597
13463
|
|
|
13464
|
+
// ../daemon/src/agent/acp/authstatus.ts
|
|
13465
|
+
var AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
|
|
13466
|
+
var KINDS = new Set(["none", "api_key", "account", "external", "gateway"]);
|
|
13467
|
+
function supportsLogout(caps) {
|
|
13468
|
+
return !!caps?.auth?.logout;
|
|
13469
|
+
}
|
|
13470
|
+
var str = (v) => typeof v === "string" && v.trim() ? v : undefined;
|
|
13471
|
+
function parseAuthStatus(params) {
|
|
13472
|
+
const status = params?.authStatus;
|
|
13473
|
+
if (!status || typeof status !== "object")
|
|
13474
|
+
return null;
|
|
13475
|
+
const kind = status.kind;
|
|
13476
|
+
if (!KINDS.has(kind))
|
|
13477
|
+
return null;
|
|
13478
|
+
const account = status.account ?? undefined;
|
|
13479
|
+
const fields = account ? {
|
|
13480
|
+
...str(account.email) ? { email: str(account.email) } : {},
|
|
13481
|
+
...str(account.organization) ? { organization: str(account.organization) } : {},
|
|
13482
|
+
...str(account.plan) ? { plan: str(account.plan) } : {}
|
|
13483
|
+
} : {};
|
|
13484
|
+
return {
|
|
13485
|
+
kind,
|
|
13486
|
+
label: str(status.label) ?? (kind === "none" ? "not logged in" : "logged in"),
|
|
13487
|
+
...str(status.detail) ? { detail: str(status.detail) } : {},
|
|
13488
|
+
...Object.keys(fields).length ? { account: fields } : {}
|
|
13489
|
+
};
|
|
13490
|
+
}
|
|
13491
|
+
function isAuthRequired(e) {
|
|
13492
|
+
return e instanceof RequestError && e.code === -32000;
|
|
13493
|
+
}
|
|
13494
|
+
|
|
13598
13495
|
// ../daemon/src/agent/acp/options.ts
|
|
13599
13496
|
var OPTION_FIELDS = { model: "model", thought_level: "effort" };
|
|
13600
13497
|
var OPTION_CATEGORIES = Object.keys(OPTION_FIELDS);
|
|
@@ -13647,37 +13544,125 @@ function currentValues(options) {
|
|
|
13647
13544
|
return out;
|
|
13648
13545
|
}
|
|
13649
13546
|
|
|
13650
|
-
// ../daemon/src/agent/
|
|
13651
|
-
|
|
13547
|
+
// ../daemon/src/agent/readiness.ts
|
|
13548
|
+
var CHECK_COOLDOWN_MS = 30000;
|
|
13549
|
+
|
|
13550
|
+
class AgentReadiness {
|
|
13652
13551
|
d;
|
|
13653
|
-
|
|
13552
|
+
known = new Map;
|
|
13553
|
+
inflight = new Map;
|
|
13554
|
+
logouts = new Map;
|
|
13555
|
+
onChange = null;
|
|
13556
|
+
now;
|
|
13654
13557
|
constructor(d) {
|
|
13655
13558
|
this.d = d;
|
|
13559
|
+
this.now = d.now ?? Date.now;
|
|
13656
13560
|
}
|
|
13657
|
-
|
|
13561
|
+
entry(agentId) {
|
|
13562
|
+
let e = this.known.get(agentId);
|
|
13563
|
+
if (!e) {
|
|
13564
|
+
e = { canLogout: false };
|
|
13565
|
+
this.known.set(agentId, e);
|
|
13566
|
+
}
|
|
13567
|
+
return e;
|
|
13568
|
+
}
|
|
13569
|
+
observe(agentId, o) {
|
|
13570
|
+
const e = this.entry(agentId);
|
|
13571
|
+
const before = { ...e };
|
|
13572
|
+
if (o.canLogout !== undefined)
|
|
13573
|
+
e.canLogout = o.canLogout;
|
|
13574
|
+
if (o.status) {
|
|
13575
|
+
e.status = o.status;
|
|
13576
|
+
if (o.status.kind !== "none")
|
|
13577
|
+
delete e.failure;
|
|
13578
|
+
}
|
|
13579
|
+
if (before.canLogout !== e.canLogout || !same(before.status, e.status) || before.failure !== e.failure) {
|
|
13580
|
+
this.onChange?.();
|
|
13581
|
+
}
|
|
13582
|
+
}
|
|
13583
|
+
observeLimits(agentId, limits) {
|
|
13584
|
+
const e = this.entry(agentId);
|
|
13585
|
+
const before = e.limits;
|
|
13586
|
+
e.limits = limits;
|
|
13587
|
+
if (!sameLimits(before, limits))
|
|
13588
|
+
this.onChange?.();
|
|
13589
|
+
}
|
|
13590
|
+
describe() {
|
|
13591
|
+
return this.d.infos().map((info) => {
|
|
13592
|
+
const e = this.known.get(info.id);
|
|
13593
|
+
const models = this.d.options(info.id, "model");
|
|
13594
|
+
const efforts = this.d.options(info.id, "thought_level");
|
|
13595
|
+
return {
|
|
13596
|
+
...info,
|
|
13597
|
+
...models.length > 0 ? { models } : {},
|
|
13598
|
+
...efforts.length > 0 ? { efforts } : {},
|
|
13599
|
+
...this.d.quick(info.id) ? { quick: true } : {},
|
|
13600
|
+
...e?.status ? { auth: e.status } : {},
|
|
13601
|
+
...e?.canLogout ? { canLogout: true } : {},
|
|
13602
|
+
...e?.limits ? { limits: e.limits } : {},
|
|
13603
|
+
...e?.failure ? { checkFailed: e.failure } : {}
|
|
13604
|
+
};
|
|
13605
|
+
});
|
|
13606
|
+
}
|
|
13607
|
+
positive(agentId) {
|
|
13608
|
+
const e = this.known.get(agentId);
|
|
13609
|
+
return this.d.options(agentId, "model").length > 0 && !e?.failure && e?.status?.kind !== "none";
|
|
13610
|
+
}
|
|
13611
|
+
check(agentId) {
|
|
13612
|
+
const info = this.d.infos().find((a) => a.id === agentId);
|
|
13613
|
+
if (!info?.available)
|
|
13614
|
+
return Promise.resolve();
|
|
13615
|
+
const running = this.inflight.get(agentId);
|
|
13616
|
+
if (running)
|
|
13617
|
+
return running;
|
|
13618
|
+
if (this.positive(agentId))
|
|
13619
|
+
return Promise.resolve();
|
|
13620
|
+
const e = this.known.get(agentId);
|
|
13621
|
+
if (e?.checkedAt !== undefined && this.now() - e.checkedAt < CHECK_COOLDOWN_MS)
|
|
13622
|
+
return Promise.resolve();
|
|
13623
|
+
const p = this.run(agentId).finally(() => {
|
|
13624
|
+
if (this.inflight.get(agentId) === p)
|
|
13625
|
+
this.inflight.delete(agentId);
|
|
13626
|
+
});
|
|
13627
|
+
this.inflight.set(agentId, p);
|
|
13628
|
+
return p;
|
|
13629
|
+
}
|
|
13630
|
+
async checkMissing() {
|
|
13658
13631
|
for (const a of this.d.infos()) {
|
|
13659
|
-
if (
|
|
13660
|
-
|
|
13661
|
-
this.tried.add(a.id);
|
|
13662
|
-
await this.probe(a.id);
|
|
13632
|
+
if (a.available && this.d.options(a.id, "model").length === 0)
|
|
13633
|
+
await this.check(a.id);
|
|
13663
13634
|
}
|
|
13664
13635
|
}
|
|
13665
|
-
async
|
|
13666
|
-
const tag = `
|
|
13667
|
-
const
|
|
13636
|
+
async run(agentId) {
|
|
13637
|
+
const tag = `check:${agentId}`;
|
|
13638
|
+
const found = {};
|
|
13639
|
+
let spec;
|
|
13640
|
+
try {
|
|
13641
|
+
spec = this.d.require(agentId);
|
|
13642
|
+
} catch (e) {
|
|
13643
|
+
this.finish(agentId, firstLine(e), found);
|
|
13644
|
+
return;
|
|
13645
|
+
}
|
|
13646
|
+
const app = client({ name: "toyon" }).onNotification(methods.client.session.update, () => {}).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
|
|
13647
|
+
if (c.params)
|
|
13648
|
+
found.status = c.params;
|
|
13649
|
+
});
|
|
13668
13650
|
let link;
|
|
13669
13651
|
try {
|
|
13670
|
-
link = await this.d.connect(app,
|
|
13652
|
+
link = await this.d.connect(app, spec);
|
|
13671
13653
|
} catch (e) {
|
|
13672
|
-
log.warn(tag, "could not start the agent
|
|
13654
|
+
log.warn(tag, "could not start the agent", e);
|
|
13655
|
+
this.finish(agentId, firstLine(e), found);
|
|
13673
13656
|
return;
|
|
13674
13657
|
}
|
|
13658
|
+
let failure;
|
|
13675
13659
|
try {
|
|
13676
13660
|
const init = await link.conn.agent.request(methods.agent.initialize, {
|
|
13677
13661
|
protocolVersion: PROTOCOL_VERSION2,
|
|
13678
13662
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
13679
13663
|
clientInfo: { name: "toyon", version: "0" }
|
|
13680
13664
|
});
|
|
13665
|
+
found.canLogout = supportsLogout(init.agentCapabilities);
|
|
13681
13666
|
const s = await link.conn.agent.request(methods.agent.session.new, { cwd: this.d.cwd, mcpServers: [] });
|
|
13682
13667
|
for (const [category, opt] of readOptions(s.configOptions))
|
|
13683
13668
|
this.d.learned(agentId, category, opt.choices);
|
|
@@ -13687,18 +13672,97 @@ class OptionProbe {
|
|
|
13687
13672
|
});
|
|
13688
13673
|
}
|
|
13689
13674
|
} catch (e) {
|
|
13690
|
-
|
|
13675
|
+
failure = isAuthRequired(e) ? spec.loginHint : firstLine(e);
|
|
13676
|
+
log.info(tag, `could not read its models: ${failure}`);
|
|
13691
13677
|
} finally {
|
|
13692
13678
|
link.conn.close();
|
|
13693
13679
|
await link.kill();
|
|
13694
13680
|
}
|
|
13681
|
+
this.finish(agentId, failure, found);
|
|
13682
|
+
}
|
|
13683
|
+
finish(agentId, failure, found) {
|
|
13684
|
+
const e = this.entry(agentId);
|
|
13685
|
+
const before = { ...e };
|
|
13686
|
+
if (found.canLogout !== undefined)
|
|
13687
|
+
e.canLogout = found.canLogout;
|
|
13688
|
+
if (found.status)
|
|
13689
|
+
e.status = found.status;
|
|
13690
|
+
e.failure = failure;
|
|
13691
|
+
e.checkedAt = this.now();
|
|
13692
|
+
if (before.canLogout !== e.canLogout || !same(before.status, e.status) || before.failure !== e.failure) {
|
|
13693
|
+
this.onChange?.();
|
|
13694
|
+
}
|
|
13695
|
+
}
|
|
13696
|
+
logout(agentId) {
|
|
13697
|
+
const running = this.logouts.get(agentId);
|
|
13698
|
+
if (running)
|
|
13699
|
+
return running;
|
|
13700
|
+
const prior = this.inflight.get(agentId) ?? Promise.resolve();
|
|
13701
|
+
const p = prior.then(() => this.runLogout(agentId)).finally(() => {
|
|
13702
|
+
this.logouts.delete(agentId);
|
|
13703
|
+
this.inflight.delete(agentId);
|
|
13704
|
+
});
|
|
13705
|
+
this.logouts.set(agentId, p);
|
|
13706
|
+
this.inflight.set(agentId, p.catch(() => {
|
|
13707
|
+
return;
|
|
13708
|
+
}));
|
|
13709
|
+
return p;
|
|
13695
13710
|
}
|
|
13711
|
+
async runLogout(agentId) {
|
|
13712
|
+
const spec = this.d.require(agentId);
|
|
13713
|
+
let pushed = null;
|
|
13714
|
+
let signingOut = false;
|
|
13715
|
+
const app = client({ name: "toyon" }).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
|
|
13716
|
+
if (c.params && signingOut)
|
|
13717
|
+
pushed = c.params;
|
|
13718
|
+
});
|
|
13719
|
+
const link = await this.d.connect(app, spec);
|
|
13720
|
+
try {
|
|
13721
|
+
const init = await link.conn.agent.request(methods.agent.initialize, {
|
|
13722
|
+
protocolVersion: PROTOCOL_VERSION2,
|
|
13723
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
13724
|
+
clientInfo: { name: "toyon", version: "0" }
|
|
13725
|
+
});
|
|
13726
|
+
if (!supportsLogout(init.agentCapabilities))
|
|
13727
|
+
throw new UserError(`${spec.name} cannot be signed out from here`);
|
|
13728
|
+
signingOut = true;
|
|
13729
|
+
await link.conn.agent.request(methods.agent.logout, {});
|
|
13730
|
+
const before = this.known.get(agentId);
|
|
13731
|
+
this.known.set(agentId, { canLogout: true, ...pushed ? { status: pushed } : {} });
|
|
13732
|
+
if (!before || !same(before.status, pushed ?? undefined) || before.failure !== undefined)
|
|
13733
|
+
this.onChange?.();
|
|
13734
|
+
log.info("agents", `${agentId} signed out`);
|
|
13735
|
+
} catch (e) {
|
|
13736
|
+
if (e instanceof UserError)
|
|
13737
|
+
throw e;
|
|
13738
|
+
throw new UserError(`${spec.name} could not be signed out: ${e instanceof Error ? e.message : String(e)}`);
|
|
13739
|
+
} finally {
|
|
13740
|
+
await link.kill();
|
|
13741
|
+
}
|
|
13742
|
+
}
|
|
13743
|
+
}
|
|
13744
|
+
function firstLine(e) {
|
|
13745
|
+
const details = e instanceof RequestError ? e.data?.details : undefined;
|
|
13746
|
+
const message = typeof details === "string" ? details : e instanceof Error ? e.message : String(e);
|
|
13747
|
+
return message.replace(/^Internal error: /, "").split(`
|
|
13748
|
+
`)[0] ?? message;
|
|
13749
|
+
}
|
|
13750
|
+
function sameLimits(a, b) {
|
|
13751
|
+
if (!a || !b)
|
|
13752
|
+
return a === b;
|
|
13753
|
+
const win = (x) => x ? `${x.used}@${x.resetsAt}` : "";
|
|
13754
|
+
return a.status === b.status && a.binding === b.binding && win(a.windows.five_hour) === win(b.windows.five_hour) && win(a.windows.seven_day) === win(b.windows.seven_day);
|
|
13755
|
+
}
|
|
13756
|
+
function same(a, b) {
|
|
13757
|
+
if (!a || !b)
|
|
13758
|
+
return a === b;
|
|
13759
|
+
return a.kind === b.kind && a.label === b.label && a.detail === b.detail && a.account?.email === b.account?.email && a.account?.organization === b.account?.organization && a.account?.plan === b.account?.plan;
|
|
13696
13760
|
}
|
|
13697
13761
|
|
|
13698
13762
|
// ../daemon/src/agent/registry.ts
|
|
13699
|
-
import { existsSync as
|
|
13763
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
13700
13764
|
import { homedir as homedir3 } from "os";
|
|
13701
|
-
import { dirname as dirname3, isAbsolute, join as
|
|
13765
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7, resolve as resolve3 } from "path";
|
|
13702
13766
|
|
|
13703
13767
|
// ../daemon/src/core/cloud.ts
|
|
13704
13768
|
var enabled = process.env.TOYON_CLOUD === "1";
|
|
@@ -14180,9 +14244,9 @@ function opencodeEnv() {
|
|
|
14180
14244
|
}
|
|
14181
14245
|
|
|
14182
14246
|
// ../daemon/src/agent/sandbox.ts
|
|
14183
|
-
import { existsSync as
|
|
14247
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync4 } from "fs";
|
|
14184
14248
|
import { homedir as homedir2, tmpdir } from "os";
|
|
14185
|
-
import { join as
|
|
14249
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
14186
14250
|
|
|
14187
14251
|
// ../daemon/src/core/paths.ts
|
|
14188
14252
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -14307,6 +14371,76 @@ function within2(path, root) {
|
|
|
14307
14371
|
return path === root || path.startsWith(root + sep);
|
|
14308
14372
|
}
|
|
14309
14373
|
|
|
14374
|
+
// ../daemon/src/agent/planDoc.ts
|
|
14375
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
14376
|
+
import { basename as basename2, isAbsolute, join as join5, relative, sep as sep2 } from "path";
|
|
14377
|
+
var PLANS_DIR = join5(".toyon", "plans");
|
|
14378
|
+
var PLAN_NAME = /^[A-Za-z0-9][\w.-]*\.md$/;
|
|
14379
|
+
function isPlanPath(rel) {
|
|
14380
|
+
return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
|
|
14381
|
+
}
|
|
14382
|
+
function ownPlanPath(cwd, file) {
|
|
14383
|
+
const dir = canonical(join5(cwd, PLANS_DIR));
|
|
14384
|
+
const abs = canonical(isAbsolute(file) ? file : join5(cwd, file));
|
|
14385
|
+
if (!within2(abs, dir) || relative(dir, abs).includes(sep2))
|
|
14386
|
+
return null;
|
|
14387
|
+
return PLAN_NAME.test(basename2(abs)) ? join5(PLANS_DIR, basename2(abs)) : null;
|
|
14388
|
+
}
|
|
14389
|
+
async function excludePlans(cwd) {
|
|
14390
|
+
await excludeFromGit(cwd, `${PLANS_DIR}/`);
|
|
14391
|
+
}
|
|
14392
|
+
function lastNumber(dir) {
|
|
14393
|
+
if (!existsSync7(dir))
|
|
14394
|
+
return 0;
|
|
14395
|
+
let max = 0;
|
|
14396
|
+
for (const name of readdirSync3(dir)) {
|
|
14397
|
+
const n = Number(/^(\d+)\.md$/.exec(name)?.[1]);
|
|
14398
|
+
if (n > max)
|
|
14399
|
+
max = n;
|
|
14400
|
+
}
|
|
14401
|
+
return max;
|
|
14402
|
+
}
|
|
14403
|
+
function sameAsLast(cwd, rel, markdown) {
|
|
14404
|
+
try {
|
|
14405
|
+
return readFileSync6(join5(cwd, rel), "utf8").trim() === markdown.trim();
|
|
14406
|
+
} catch (e) {
|
|
14407
|
+
log.warn(cwd, `could not read ${rel}`, e);
|
|
14408
|
+
return false;
|
|
14409
|
+
}
|
|
14410
|
+
}
|
|
14411
|
+
async function writePlanDoc(cwd, markdown) {
|
|
14412
|
+
const dir = join5(cwd, PLANS_DIR);
|
|
14413
|
+
const last = lastNumber(dir);
|
|
14414
|
+
if (last > 0) {
|
|
14415
|
+
const newest = join5(PLANS_DIR, `${last}.md`);
|
|
14416
|
+
if (sameAsLast(cwd, newest, markdown))
|
|
14417
|
+
return newest;
|
|
14418
|
+
}
|
|
14419
|
+
const rel = join5(PLANS_DIR, `${last + 1}.md`);
|
|
14420
|
+
try {
|
|
14421
|
+
mkdirSync4(dir, { recursive: true });
|
|
14422
|
+
writeFileSync3(join5(cwd, rel), markdown.endsWith(`
|
|
14423
|
+
`) ? markdown : `${markdown}
|
|
14424
|
+
`);
|
|
14425
|
+
} catch (e) {
|
|
14426
|
+
log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
|
|
14427
|
+
return null;
|
|
14428
|
+
}
|
|
14429
|
+
await excludePlans(cwd);
|
|
14430
|
+
return rel;
|
|
14431
|
+
}
|
|
14432
|
+
function planEdited(cwd, rel, proposed) {
|
|
14433
|
+
const file = join5(cwd, rel);
|
|
14434
|
+
if (!existsSync7(file))
|
|
14435
|
+
return false;
|
|
14436
|
+
try {
|
|
14437
|
+
return readFileSync6(file, "utf8").trim() !== proposed.trim();
|
|
14438
|
+
} catch (e) {
|
|
14439
|
+
log.warn(cwd, `could not read ${rel}`, e);
|
|
14440
|
+
return false;
|
|
14441
|
+
}
|
|
14442
|
+
}
|
|
14443
|
+
|
|
14310
14444
|
// ../daemon/src/agent/sandbox.ts
|
|
14311
14445
|
var CACHE_DIRS = [".bun", ".npm", ".cache", ".yarn", ".pnpm-store", "Library/Caches"].map((d) => resolve2(homedir2(), d));
|
|
14312
14446
|
var AGENT_SETTINGS = [".claude", ".codex", ".opencode", "opencode.json", "opencode.jsonc"];
|
|
@@ -14335,9 +14469,10 @@ async function prepareLaunch(cwd, spec) {
|
|
|
14335
14469
|
log.warn(cwd, `agent ${spec.id} runs without an OS sandbox`);
|
|
14336
14470
|
return { bounds, env: setup?.env ?? {} };
|
|
14337
14471
|
}
|
|
14338
|
-
var SETTINGS_REL =
|
|
14472
|
+
var SETTINGS_REL = join6(".claude", "settings.local.json");
|
|
14339
14473
|
function claudeLocalSettings(b) {
|
|
14340
14474
|
return {
|
|
14475
|
+
plansDirectory: PLANS_DIR,
|
|
14341
14476
|
sandbox: {
|
|
14342
14477
|
enabled: true,
|
|
14343
14478
|
failIfUnavailable: true,
|
|
@@ -14349,12 +14484,12 @@ function claudeLocalSettings(b) {
|
|
|
14349
14484
|
};
|
|
14350
14485
|
}
|
|
14351
14486
|
async function writeClaudeLocalSettings(cwd, b) {
|
|
14352
|
-
const file =
|
|
14353
|
-
|
|
14487
|
+
const file = join6(cwd, SETTINGS_REL);
|
|
14488
|
+
mkdirSync5(join6(cwd, ".claude"), { recursive: true });
|
|
14354
14489
|
let existing = {};
|
|
14355
|
-
if (
|
|
14490
|
+
if (existsSync8(file)) {
|
|
14356
14491
|
try {
|
|
14357
|
-
const parsed = JSON.parse(
|
|
14492
|
+
const parsed = JSON.parse(readFileSync7(file, "utf8"));
|
|
14358
14493
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
14359
14494
|
existing = parsed;
|
|
14360
14495
|
} catch (e) {
|
|
@@ -14367,13 +14502,20 @@ async function writeClaudeLocalSettings(cwd, b) {
|
|
|
14367
14502
|
const perms = existing.permissions && typeof existing.permissions === "object" ? existing.permissions : {};
|
|
14368
14503
|
const theirs = Array.isArray(perms.deny) ? perms.deny.filter((r) => typeof r === "string") : [];
|
|
14369
14504
|
const deny = uniq2([...theirs, ...ours.permissions.deny]);
|
|
14370
|
-
const next = {
|
|
14505
|
+
const next = {
|
|
14506
|
+
...existing,
|
|
14507
|
+
sandbox: ours.sandbox,
|
|
14508
|
+
permissions: { ...perms, ...ours.permissions, deny },
|
|
14509
|
+
plansDirectory: ours.plansDirectory
|
|
14510
|
+
};
|
|
14371
14511
|
const tmp = `${file}.tmp`;
|
|
14372
|
-
|
|
14512
|
+
writeFileSync4(tmp, `${JSON.stringify(next, null, 2)}
|
|
14373
14513
|
`);
|
|
14374
14514
|
renameSync(tmp, file);
|
|
14375
|
-
if (
|
|
14515
|
+
if (existsSync8(join6(cwd, ".git"))) {
|
|
14376
14516
|
await excludeFromGit(cwd, SETTINGS_REL);
|
|
14517
|
+
await excludePlans(cwd);
|
|
14518
|
+
}
|
|
14377
14519
|
}
|
|
14378
14520
|
|
|
14379
14521
|
// ../daemon/src/agent/registry.ts
|
|
@@ -14432,14 +14574,30 @@ var BUILTIN_AGENTS = [
|
|
|
14432
14574
|
systemPrompt: "prompt-prefix",
|
|
14433
14575
|
modes: { plan: "plan", build: "build" },
|
|
14434
14576
|
quickModel: opencodeQuickModel,
|
|
14435
|
-
loginHint: "OpenCode has no provider logged in; run
|
|
14577
|
+
loginHint: "OpenCode has no provider logged in; run opencode auth login in the terminal"
|
|
14436
14578
|
}
|
|
14437
14579
|
];
|
|
14580
|
+
function installErrorLine(stderr) {
|
|
14581
|
+
const lines = stderr.split(`
|
|
14582
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
14583
|
+
const first = lines.find((l) => l.startsWith("error:"))?.slice("error:".length).trim();
|
|
14584
|
+
const line = first || lines.at(-1) || "no output";
|
|
14585
|
+
if (UNTRUSTED_CA.test(line))
|
|
14586
|
+
return `${line}. A certificate bun does not trust, usually a proxy's: set NODE_EXTRA_CA_CERTS to its file and restart Toyon`;
|
|
14587
|
+
return line;
|
|
14588
|
+
}
|
|
14589
|
+
var UNTRUSTED_CA = /UNKNOWN_CERTIFICATE_VERIFICATION_ERROR|SELF_SIGNED_CERT|UNABLE_TO_GET_ISSUER_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/;
|
|
14438
14590
|
var bunInstall = async (dir) => {
|
|
14439
|
-
const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir
|
|
14440
|
-
|
|
14441
|
-
|
|
14591
|
+
const r = await run(process.execPath, ["install", "--no-progress", "--no-summary"], dir, {
|
|
14592
|
+
NO_COLOR: "1",
|
|
14593
|
+
FORCE_COLOR: "0"
|
|
14594
|
+
});
|
|
14595
|
+
return { ok: r.ok, err: r.ok ? "" : installErrorLine(r.err) };
|
|
14442
14596
|
};
|
|
14597
|
+
var RETRY_AFTER_MS = 30000;
|
|
14598
|
+
var RETRY_CAP_MS = 60 * 60000;
|
|
14599
|
+
var retryWindow = (retries) => Math.min(RETRY_AFTER_MS * 2 ** retries, RETRY_CAP_MS);
|
|
14600
|
+
var failedReason = (why) => `install failed: ${why}`;
|
|
14443
14601
|
function jsRuntime() {
|
|
14444
14602
|
if (process.env.TOYON_ACP_RUNTIME === "node") {
|
|
14445
14603
|
const node = Bun.which("node");
|
|
@@ -14455,15 +14613,18 @@ class AgentRegistry {
|
|
|
14455
14613
|
installer;
|
|
14456
14614
|
host;
|
|
14457
14615
|
sandbox;
|
|
14616
|
+
now;
|
|
14458
14617
|
specs = new Map;
|
|
14459
14618
|
installing = new Map;
|
|
14460
14619
|
installErrors = new Map;
|
|
14620
|
+
failures = new Map;
|
|
14461
14621
|
onChange = null;
|
|
14462
|
-
constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }) {
|
|
14622
|
+
constructor(specs, dir, installer = bunInstall, host = hostTraits(), sandbox = { problem: () => null }, now = Date.now) {
|
|
14463
14623
|
this.dir = dir;
|
|
14464
14624
|
this.installer = installer;
|
|
14465
14625
|
this.host = host;
|
|
14466
14626
|
this.sandbox = sandbox;
|
|
14627
|
+
this.now = now;
|
|
14467
14628
|
for (const s of specs)
|
|
14468
14629
|
this.specs.set(s.id, s);
|
|
14469
14630
|
}
|
|
@@ -14500,43 +14661,47 @@ class AgentRegistry {
|
|
|
14500
14661
|
const name = this.packageName(spec);
|
|
14501
14662
|
if (!name || spec.run.kind === "command")
|
|
14502
14663
|
return null;
|
|
14503
|
-
const pkgDir =
|
|
14664
|
+
const pkgDir = join7(this.dir, spec.id, "node_modules", name);
|
|
14504
14665
|
if (spec.run.kind === "npm-native") {
|
|
14505
|
-
const bin =
|
|
14506
|
-
return
|
|
14666
|
+
const bin = join7(pkgDir, spec.run.bin);
|
|
14667
|
+
return existsSync9(bin) ? bin : null;
|
|
14507
14668
|
}
|
|
14508
|
-
const pkgJson =
|
|
14509
|
-
if (!
|
|
14669
|
+
const pkgJson = join7(pkgDir, "package.json");
|
|
14670
|
+
if (!existsSync9(pkgJson))
|
|
14510
14671
|
return null;
|
|
14511
14672
|
try {
|
|
14512
|
-
const meta = JSON.parse(
|
|
14673
|
+
const meta = JSON.parse(readFileSync8(pkgJson, "utf8"));
|
|
14513
14674
|
const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
|
|
14514
14675
|
if (!rel)
|
|
14515
14676
|
return null;
|
|
14516
|
-
const script =
|
|
14517
|
-
return
|
|
14677
|
+
const script = join7(dirname3(pkgJson), rel);
|
|
14678
|
+
return existsSync9(script) ? script : null;
|
|
14518
14679
|
} catch (e) {
|
|
14519
14680
|
log.warn("agents", `cannot read ${pkgJson}`, e);
|
|
14520
14681
|
return null;
|
|
14521
14682
|
}
|
|
14522
14683
|
}
|
|
14523
14684
|
unavailable(spec) {
|
|
14524
|
-
return this.
|
|
14525
|
-
}
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
return
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
if (
|
|
14538
|
-
return
|
|
14539
|
-
|
|
14685
|
+
return this.status(spec).reason;
|
|
14686
|
+
}
|
|
14687
|
+
status(spec) {
|
|
14688
|
+
const boxed = spec.confinement === "none" ? null : this.sandbox.problem();
|
|
14689
|
+
if (spec.run.kind === "command") {
|
|
14690
|
+
const cmd = spec.run.command;
|
|
14691
|
+
const found = isAbsolute2(cmd) ? existsSync9(cmd) : Bun.which(cmd);
|
|
14692
|
+
return { reason: found ? boxed : `${cmd} not found on PATH` };
|
|
14693
|
+
}
|
|
14694
|
+
if (!this.packageName(spec))
|
|
14695
|
+
return { reason: `no build for ${this.host.platform} ${this.host.arch}` };
|
|
14696
|
+
if (this.installed(spec))
|
|
14697
|
+
return { reason: boxed };
|
|
14698
|
+
if (this.installing.has(spec.id))
|
|
14699
|
+
return { reason: "installing" };
|
|
14700
|
+
const failed = this.installErrors.get(spec.id);
|
|
14701
|
+
const missing = failed !== undefined ? failedReason(failed) : spec.onDemand ? "not installed" : "not installed yet";
|
|
14702
|
+
if (boxed)
|
|
14703
|
+
return { reason: failed !== undefined ? `${missing}; ${boxed}` : boxed };
|
|
14704
|
+
return { reason: missing, install: failed !== undefined ? { failed } : "missing" };
|
|
14540
14705
|
}
|
|
14541
14706
|
command(spec) {
|
|
14542
14707
|
const why = this.unavailable(spec);
|
|
@@ -14564,23 +14729,29 @@ class AgentRegistry {
|
|
|
14564
14729
|
const running = this.installing.get(id);
|
|
14565
14730
|
if (running)
|
|
14566
14731
|
return running;
|
|
14567
|
-
if (this.installed(spec) && this.installedVersion(spec) === spec.run.version)
|
|
14732
|
+
if (this.installed(spec) && this.installedVersion(spec) === spec.run.version) {
|
|
14733
|
+
this.installErrors.delete(id);
|
|
14734
|
+
this.failures.delete(id);
|
|
14568
14735
|
return Promise.resolve();
|
|
14736
|
+
}
|
|
14569
14737
|
const { version } = spec.run;
|
|
14570
|
-
const dir =
|
|
14738
|
+
const dir = join7(this.dir, id);
|
|
14571
14739
|
const p = (async () => {
|
|
14572
|
-
|
|
14573
|
-
|
|
14740
|
+
try {
|
|
14741
|
+
mkdirSync6(dir, { recursive: true });
|
|
14742
|
+
writeFileSync5(join7(dir, "package.json"), `${JSON.stringify({ name: `toyon-agent-${id}`, private: true, dependencies: { [pkg]: version } }, null, 2)}
|
|
14574
14743
|
`);
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
|
|
14744
|
+
log.info("agents", `installing ${pkg}@${version} for ${id}`);
|
|
14745
|
+
const r = await this.installer(dir, pkg, version);
|
|
14746
|
+
if (r.ok && this.installed(spec)) {
|
|
14747
|
+
this.installErrors.delete(id);
|
|
14748
|
+
this.failures.delete(id);
|
|
14749
|
+
log.info("agents", `${id} ready`);
|
|
14750
|
+
return;
|
|
14751
|
+
}
|
|
14752
|
+
this.fail(id, r.err || "no bin after install");
|
|
14753
|
+
} catch (e) {
|
|
14754
|
+
this.fail(id, e instanceof Error ? e.message : String(e));
|
|
14584
14755
|
}
|
|
14585
14756
|
})().finally(() => {
|
|
14586
14757
|
this.installing.delete(id);
|
|
@@ -14590,13 +14761,18 @@ class AgentRegistry {
|
|
|
14590
14761
|
this.onChange?.();
|
|
14591
14762
|
return p;
|
|
14592
14763
|
}
|
|
14764
|
+
fail(id, why) {
|
|
14765
|
+
this.installErrors.set(id, why);
|
|
14766
|
+
this.failures.set(id, { at: this.now(), retries: this.failures.get(id)?.retries ?? 0 });
|
|
14767
|
+
log.warn("agents", `${id}: ${failedReason(why)}`);
|
|
14768
|
+
}
|
|
14593
14769
|
installedVersion(spec) {
|
|
14594
14770
|
const name = this.packageName(spec);
|
|
14595
14771
|
if (!name)
|
|
14596
14772
|
return null;
|
|
14597
|
-
const pkgJson =
|
|
14773
|
+
const pkgJson = join7(this.dir, spec.id, "node_modules", name, "package.json");
|
|
14598
14774
|
try {
|
|
14599
|
-
return JSON.parse(
|
|
14775
|
+
return JSON.parse(readFileSync8(pkgJson, "utf8")).version ?? null;
|
|
14600
14776
|
} catch {
|
|
14601
14777
|
return null;
|
|
14602
14778
|
}
|
|
@@ -14609,22 +14785,37 @@ class AgentRegistry {
|
|
|
14609
14785
|
await this.install(id);
|
|
14610
14786
|
}
|
|
14611
14787
|
}
|
|
14788
|
+
async retryFailed() {
|
|
14789
|
+
if (this.installing.size > 0)
|
|
14790
|
+
return;
|
|
14791
|
+
for (const [id, entry] of this.failures) {
|
|
14792
|
+
if (this.now() - entry.at < retryWindow(entry.retries))
|
|
14793
|
+
continue;
|
|
14794
|
+
entry.retries += 1;
|
|
14795
|
+
await this.install(id);
|
|
14796
|
+
}
|
|
14797
|
+
}
|
|
14612
14798
|
adapters() {
|
|
14613
14799
|
const out = [];
|
|
14614
14800
|
for (const spec of this.list()) {
|
|
14615
14801
|
if (spec.run.kind === "command" || !this.packageName(spec))
|
|
14616
14802
|
continue;
|
|
14617
14803
|
const installed = this.installed(spec) ? this.installedVersion(spec) : null;
|
|
14618
|
-
const
|
|
14619
|
-
if (spec.onDemand && installed === null &&
|
|
14804
|
+
const failed = this.installErrors.get(spec.id);
|
|
14805
|
+
if (spec.onDemand && installed === null && failed === undefined)
|
|
14620
14806
|
continue;
|
|
14621
|
-
out.push({
|
|
14807
|
+
out.push({
|
|
14808
|
+
id: spec.id,
|
|
14809
|
+
installed,
|
|
14810
|
+
pinned: spec.run.version,
|
|
14811
|
+
...failed !== undefined ? { error: failedReason(failed) } : {}
|
|
14812
|
+
});
|
|
14622
14813
|
}
|
|
14623
14814
|
return out;
|
|
14624
14815
|
}
|
|
14625
14816
|
infos() {
|
|
14626
14817
|
return this.list().map((spec) => {
|
|
14627
|
-
const reason = this.
|
|
14818
|
+
const { reason, install } = this.status(spec);
|
|
14628
14819
|
return {
|
|
14629
14820
|
id: spec.id,
|
|
14630
14821
|
name: spec.name,
|
|
@@ -14632,8 +14823,10 @@ class AgentRegistry {
|
|
|
14632
14823
|
available: !reason,
|
|
14633
14824
|
...reason ? { reason } : {},
|
|
14634
14825
|
...this.installing.has(spec.id) ? { installing: true } : {},
|
|
14826
|
+
...install !== undefined ? { install } : {},
|
|
14635
14827
|
...spec.onDemand ? { onDemand: true } : {},
|
|
14636
|
-
sandboxed: spec.confinement !== "none"
|
|
14828
|
+
sandboxed: spec.confinement !== "none",
|
|
14829
|
+
loginHint: spec.loginHint
|
|
14637
14830
|
};
|
|
14638
14831
|
});
|
|
14639
14832
|
}
|
|
@@ -14701,7 +14894,7 @@ function managedSpecs(specs, managed) {
|
|
|
14701
14894
|
return managed.agents === null ? kept : kept.filter((s) => managed.agents?.includes(s.id));
|
|
14702
14895
|
}
|
|
14703
14896
|
function loadAgentRegistry(agentsFile, agentsDir, sandbox, managed = MANAGED_DEFAULTS) {
|
|
14704
|
-
const specs = managed.customAgents &&
|
|
14897
|
+
const specs = managed.customAgents && existsSync9(agentsFile) ? parseAgentsFile(readFileSync8(agentsFile, "utf8"), BUILTIN_AGENTS) : BUILTIN_AGENTS;
|
|
14705
14898
|
return new AgentRegistry(managedSpecs(specs, managed), agentsDir, undefined, undefined, sandbox);
|
|
14706
14899
|
}
|
|
14707
14900
|
|
|
@@ -14720,21 +14913,33 @@ function agentModeFor(spec, mode, available) {
|
|
|
14720
14913
|
function modeAfterPlan(optionName) {
|
|
14721
14914
|
return /manual/i.test(optionName) ? "ask" : "auto";
|
|
14722
14915
|
}
|
|
14916
|
+
function orderPlanOptions(options) {
|
|
14917
|
+
const rank = (id) => {
|
|
14918
|
+
if (id === "exit-plan-default")
|
|
14919
|
+
return 1;
|
|
14920
|
+
if (id.startsWith("exit-plan-clear-"))
|
|
14921
|
+
return 2;
|
|
14922
|
+
if (id.startsWith("exit-plan-"))
|
|
14923
|
+
return 0;
|
|
14924
|
+
return 3;
|
|
14925
|
+
};
|
|
14926
|
+
return [...options].sort((a, b) => rank(a.optionId) - rank(b.optionId));
|
|
14927
|
+
}
|
|
14723
14928
|
|
|
14724
14929
|
// ../daemon/src/agent/origins.ts
|
|
14725
|
-
import { existsSync as
|
|
14930
|
+
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
14726
14931
|
import { homedir as homedir4 } from "os";
|
|
14727
|
-
import { basename as
|
|
14932
|
+
import { basename as basename3, join as join8 } from "path";
|
|
14728
14933
|
function commandFolders(spec, cwd, home = homedir4()) {
|
|
14729
14934
|
if (spec.id === "claude") {
|
|
14730
14935
|
return [
|
|
14731
|
-
{ origin: "project", skills:
|
|
14732
|
-
{ origin: "user", skills:
|
|
14733
|
-
...pluginPaths(spec.meta).map((p) => ({ origin: "plugin", skills:
|
|
14936
|
+
{ origin: "project", skills: join8(cwd, ".claude", "skills"), commands: join8(cwd, ".claude", "commands") },
|
|
14937
|
+
{ origin: "user", skills: join8(home, ".claude", "skills"), commands: join8(home, ".claude", "commands") },
|
|
14938
|
+
...pluginPaths(spec.meta).map((p) => ({ origin: "plugin", skills: join8(p, "skills"), commands: join8(p, "commands") }))
|
|
14734
14939
|
];
|
|
14735
14940
|
}
|
|
14736
14941
|
if (spec.id === "codex")
|
|
14737
|
-
return [{ origin: "user", commands:
|
|
14942
|
+
return [{ origin: "user", commands: join8(home, ".codex", "prompts") }];
|
|
14738
14943
|
return [];
|
|
14739
14944
|
}
|
|
14740
14945
|
function readCommandOrigins(folders) {
|
|
@@ -14774,23 +14979,23 @@ function pluginPaths(meta) {
|
|
|
14774
14979
|
}
|
|
14775
14980
|
function skillNames(dir) {
|
|
14776
14981
|
return entries2(dir).flatMap((e) => {
|
|
14777
|
-
const file =
|
|
14778
|
-
if (!
|
|
14982
|
+
const file = join8(dir, e, "SKILL.md");
|
|
14983
|
+
if (!existsSync10(file))
|
|
14779
14984
|
return [];
|
|
14780
14985
|
return [frontMatterName(file) ?? e];
|
|
14781
14986
|
});
|
|
14782
14987
|
}
|
|
14783
14988
|
function commandNames(dir) {
|
|
14784
14989
|
return entries2(dir).flatMap((e) => {
|
|
14785
|
-
const path =
|
|
14990
|
+
const path = join8(dir, e);
|
|
14786
14991
|
if (e.endsWith(".md"))
|
|
14787
|
-
return [
|
|
14992
|
+
return [basename3(e, ".md")];
|
|
14788
14993
|
return isDir(path) ? commandNames(path) : [];
|
|
14789
14994
|
});
|
|
14790
14995
|
}
|
|
14791
14996
|
function frontMatterName(file) {
|
|
14792
14997
|
try {
|
|
14793
|
-
const text =
|
|
14998
|
+
const text = readFileSync9(file, "utf8");
|
|
14794
14999
|
if (!text.startsWith("---"))
|
|
14795
15000
|
return null;
|
|
14796
15001
|
const end = text.indexOf(`
|
|
@@ -14804,10 +15009,10 @@ function frontMatterName(file) {
|
|
|
14804
15009
|
}
|
|
14805
15010
|
}
|
|
14806
15011
|
function entries2(dir) {
|
|
14807
|
-
if (!
|
|
15012
|
+
if (!existsSync10(dir))
|
|
14808
15013
|
return [];
|
|
14809
15014
|
try {
|
|
14810
|
-
return
|
|
15015
|
+
return readdirSync4(dir).filter((e) => !e.startsWith("."));
|
|
14811
15016
|
} catch (e) {
|
|
14812
15017
|
log.warn("origins", `${dir} could not be listed`, e);
|
|
14813
15018
|
return [];
|
|
@@ -14851,13 +15056,13 @@ function unformatOutput(output) {
|
|
|
14851
15056
|
}
|
|
14852
15057
|
|
|
14853
15058
|
// ../daemon/src/agent/policy.ts
|
|
14854
|
-
import { isAbsolute as
|
|
15059
|
+
import { isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
|
|
14855
15060
|
|
|
14856
15061
|
// ../daemon/src/agent/acp/background.ts
|
|
14857
|
-
import { existsSync as
|
|
15062
|
+
import { existsSync as existsSync11, statSync as statSync4 } from "fs";
|
|
14858
15063
|
import { open as open2 } from "fs/promises";
|
|
14859
15064
|
import { homedir as homedir5 } from "os";
|
|
14860
|
-
import { basename as
|
|
15065
|
+
import { basename as basename4, dirname as dirname4, join as join9 } from "path";
|
|
14861
15066
|
var STARTED = /Command running in background with ID: (\S+)\. Output is being written to: (\S+)\./;
|
|
14862
15067
|
function backgroundStart(input, output) {
|
|
14863
15068
|
const flag = input && typeof input === "object" ? input.run_in_background : undefined;
|
|
@@ -14870,15 +15075,15 @@ function spawnDetached(output) {
|
|
|
14870
15075
|
return output.includes("Async agent launched successfully");
|
|
14871
15076
|
}
|
|
14872
15077
|
function claudeConfigDir() {
|
|
14873
|
-
return process.env.CLAUDE_CONFIG_DIR ||
|
|
15078
|
+
return process.env.CLAUDE_CONFIG_DIR || join9(homedir5(), ".claude");
|
|
14874
15079
|
}
|
|
14875
15080
|
function sessionLogFor(file, configDir = claudeConfigDir()) {
|
|
14876
15081
|
const tasks = dirname4(file);
|
|
14877
|
-
if (
|
|
15082
|
+
if (basename4(tasks) !== "tasks")
|
|
14878
15083
|
return null;
|
|
14879
15084
|
const session = dirname4(tasks);
|
|
14880
|
-
const path =
|
|
14881
|
-
return
|
|
15085
|
+
const path = join9(configDir, "projects", basename4(dirname4(session)), `${basename4(session)}.jsonl`);
|
|
15086
|
+
return existsSync11(path) ? path : null;
|
|
14882
15087
|
}
|
|
14883
15088
|
function taskEnd(text, toolId) {
|
|
14884
15089
|
const at = text.indexOf(`<tool-use-id>${toolId}</tool-use-id>`);
|
|
@@ -15091,14 +15296,16 @@ function isNetworkAsk(update) {
|
|
|
15091
15296
|
function isToolSearch(update) {
|
|
15092
15297
|
return update.name === TOOL_SEARCH || asRecord2(asRecord2(update._meta).claudeCode).toolName === TOOL_SEARCH;
|
|
15093
15298
|
}
|
|
15094
|
-
|
|
15299
|
+
var TOOL_SEARCH_OPENING = "loading a tool";
|
|
15300
|
+
function toolSearchTitle(rawInput) {
|
|
15095
15301
|
const query = asRecord2(rawInput).query;
|
|
15096
15302
|
if (typeof query !== "string")
|
|
15097
15303
|
return "";
|
|
15098
15304
|
const names = /^select:(.*)$/.exec(query.trim());
|
|
15099
15305
|
if (!names)
|
|
15100
|
-
return query.trim()
|
|
15101
|
-
|
|
15306
|
+
return `looked for a tool: ${query.trim()}`;
|
|
15307
|
+
const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
|
|
15308
|
+
return loaded ? `loaded ${loaded}` : "";
|
|
15102
15309
|
}
|
|
15103
15310
|
var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
|
|
15104
15311
|
function toyonTool(update) {
|
|
@@ -15122,7 +15329,7 @@ function heading(update) {
|
|
|
15122
15329
|
if (isNetworkAsk(update) && typeof host === "string")
|
|
15123
15330
|
return { name: "", title: host, kind: "fetch" };
|
|
15124
15331
|
if (isToolSearch(update)) {
|
|
15125
|
-
return { name: TOOL_SEARCH, title:
|
|
15332
|
+
return { name: TOOL_SEARCH, title: toolSearchTitle(update.rawInput) || TOOL_SEARCH_OPENING, kind: "other" };
|
|
15126
15333
|
}
|
|
15127
15334
|
const own = toyonTool(update);
|
|
15128
15335
|
if (own)
|
|
@@ -15254,7 +15461,7 @@ function mapUpdate(update, memos, tag, sink, background) {
|
|
|
15254
15461
|
});
|
|
15255
15462
|
}
|
|
15256
15463
|
const refined = { ...memo.held };
|
|
15257
|
-
const title = memo.name === TOOL_SEARCH ?
|
|
15464
|
+
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
15465
|
if (title && title !== memo.title)
|
|
15259
15466
|
refined.title = memo.title = title;
|
|
15260
15467
|
if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
|
|
@@ -15455,7 +15662,7 @@ function decide(req, bounds, cwd, mode = "auto", sandboxed = true) {
|
|
|
15455
15662
|
return { kind: "allow" };
|
|
15456
15663
|
}
|
|
15457
15664
|
for (const raw of paths) {
|
|
15458
|
-
const target = canonical(
|
|
15665
|
+
const target = canonical(isAbsolute3(raw) ? raw : resolve4(cwd, raw));
|
|
15459
15666
|
if (bounds.denyRead.some((d) => within2(target, d))) {
|
|
15460
15667
|
return {
|
|
15461
15668
|
kind: "reject",
|
|
@@ -15521,7 +15728,6 @@ var SYSTEM_APPEND = [
|
|
|
15521
15728
|
"`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
|
|
15522
15729
|
'Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, teardown (optional) runs once when a worktree is removed, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself; timeouts (optional) gives setup, check and commit each a ceiling as a duration like "30m" (ten minutes each when unset), for a suite that runs longer. A settings.local.json beside it (toyon.local.json at the root) holds one person\'s overrides and is never committed. Both files may carry comments and trailing commas; leave any you find.',
|
|
15523
15730
|
"Toyon runs those commands itself in every worktree, and the user watches the result live in a preview beside this chat, so never start a dev server or open a browser of your own to check your work. Each message says where this worktree's preview answers; fetch a page from there when you want to see it.",
|
|
15524
|
-
"To see whether a page renders without throwing, call the `preview` tool with its path when you have one: a browser the user has open loads it beside what they see and reports what it threw.",
|
|
15525
15731
|
"When you scaffold a project, write its .gitignore (dependencies, build output, local env files) before installing anything, so an install never leaves thousands of untracked files for the user to wade through or commit.",
|
|
15526
15732
|
"When you scaffold a project or change how it installs or starts, finish by updating the settings file Toyon already reads, or writing .toyon/settings.json when there is none, so the preview can run it."
|
|
15527
15733
|
].join(" ");
|
|
@@ -15534,7 +15740,6 @@ function failedRun(f) {
|
|
|
15534
15740
|
case "conflict":
|
|
15535
15741
|
return f.step;
|
|
15536
15742
|
case "preview":
|
|
15537
|
-
case "page":
|
|
15538
15743
|
return;
|
|
15539
15744
|
}
|
|
15540
15745
|
}
|
|
@@ -15550,8 +15755,6 @@ function fixPrompt(f) {
|
|
|
15550
15755
|
return commandFixPrompt(f.command);
|
|
15551
15756
|
case "preview":
|
|
15552
15757
|
return procFixPrompt(f.procs, f.log);
|
|
15553
|
-
case "page":
|
|
15554
|
-
return pageFixPrompt(f.errors);
|
|
15555
15758
|
}
|
|
15556
15759
|
}
|
|
15557
15760
|
var WHY_COMMAND_CHARS = 60;
|
|
@@ -15570,20 +15773,8 @@ function fixWhy(f) {
|
|
|
15570
15773
|
}
|
|
15571
15774
|
case "preview":
|
|
15572
15775
|
return "the dev server is not reachable";
|
|
15573
|
-
case "page":
|
|
15574
|
-
return "the preview threw";
|
|
15575
15776
|
}
|
|
15576
15777
|
}
|
|
15577
|
-
function pageFixPrompt(errors) {
|
|
15578
|
-
return [
|
|
15579
|
-
"Since your last edits the preview threw this in the user's browser:",
|
|
15580
|
-
"",
|
|
15581
|
-
...errors.map((e) => `- ${e}`),
|
|
15582
|
-
"",
|
|
15583
|
-
"Find the cause and fix it, inside this worktree. If your change did not cause it, say what did instead of changing anything."
|
|
15584
|
-
].join(`
|
|
15585
|
-
`);
|
|
15586
|
-
}
|
|
15587
15778
|
function failureContext(f) {
|
|
15588
15779
|
const run = failedRun(f);
|
|
15589
15780
|
if (!run)
|
|
@@ -15789,11 +15980,11 @@ ${a.text}
|
|
|
15789
15980
|
}
|
|
15790
15981
|
|
|
15791
15982
|
// ../daemon/src/agent/transcript.ts
|
|
15792
|
-
import { existsSync as
|
|
15983
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
15793
15984
|
import { appendFile } from "fs/promises";
|
|
15794
|
-
import { join as
|
|
15985
|
+
import { join as join10 } from "path";
|
|
15795
15986
|
function transcriptPathFor(transcriptsDir, worktreeId) {
|
|
15796
|
-
return
|
|
15987
|
+
return join10(transcriptsDir, `${worktreeId}.jsonl`);
|
|
15797
15988
|
}
|
|
15798
15989
|
function coalesce(entries) {
|
|
15799
15990
|
const out = [];
|
|
@@ -15847,7 +16038,7 @@ class Transcript {
|
|
|
15847
16038
|
this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
|
|
15848
16039
|
}
|
|
15849
16040
|
read() {
|
|
15850
|
-
return
|
|
16041
|
+
return existsSync12(this.path) ? parseTranscript(readFileSync10(this.path, "utf8"), this.tag) : [];
|
|
15851
16042
|
}
|
|
15852
16043
|
append(event) {
|
|
15853
16044
|
const entry = { seq: this.seq++, event };
|
|
@@ -16424,6 +16615,7 @@ class AcpSession {
|
|
|
16424
16615
|
this.queue = [];
|
|
16425
16616
|
this.steered = [];
|
|
16426
16617
|
this.queueChanged();
|
|
16618
|
+
this.cancelAsks("expired");
|
|
16427
16619
|
this.stop();
|
|
16428
16620
|
await this.dropConn("closing");
|
|
16429
16621
|
await this.log.flush();
|
|
@@ -16619,12 +16811,7 @@ class AcpSession {
|
|
|
16619
16811
|
this.emit({ type: "turn-end", stopReason: mapStopReason(res.stopReason), ts: Date.now() });
|
|
16620
16812
|
}
|
|
16621
16813
|
contextFor(item) {
|
|
16622
|
-
return ambientBlock([
|
|
16623
|
-
...item.context ?? [],
|
|
16624
|
-
this.d.preview?.(),
|
|
16625
|
-
item.asked === "page" ? undefined : this.d.pageErrors?.(),
|
|
16626
|
-
...this.landedSince()
|
|
16627
|
-
]);
|
|
16814
|
+
return ambientBlock([...item.context ?? [], this.d.preview?.(), this.d.pageErrors?.(), ...this.landedSince()]);
|
|
16628
16815
|
}
|
|
16629
16816
|
landedSince() {
|
|
16630
16817
|
const entries = this.log.entries;
|
|
@@ -16864,6 +17051,9 @@ class AcpSession {
|
|
|
16864
17051
|
}
|
|
16865
17052
|
if (params.update.sessionUpdate === "current_mode_update") {
|
|
16866
17053
|
live.modeId = params.update.currentModeId;
|
|
17054
|
+
const planId = agentModeFor(live.conn.spec, "plan", live.modeIds ?? undefined);
|
|
17055
|
+
if (planId && live.modeId === planId && this.mode() !== "plan")
|
|
17056
|
+
this.d.setMode?.("plan");
|
|
16867
17057
|
return;
|
|
16868
17058
|
}
|
|
16869
17059
|
if (params.update.sessionUpdate === "config_option_update") {
|
|
@@ -16935,8 +17125,9 @@ class AcpSession {
|
|
|
16935
17125
|
const plan = params.toolCall.kind === "switch_mode";
|
|
16936
17126
|
const parts = permissionParts(params);
|
|
16937
17127
|
const detail = parts.detail ?? "";
|
|
16938
|
-
const
|
|
16939
|
-
const
|
|
17128
|
+
const ownFile = plan ? stringField(params.toolCall.rawInput, "planFilePath") : undefined;
|
|
17129
|
+
const planPath = plan ? await this.d.onPlan?.(detail, ownFile) ?? null : null;
|
|
17130
|
+
const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
|
|
16940
17131
|
const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
|
|
16941
17132
|
return this.openAsk((id) => ({
|
|
16942
17133
|
type: "agent-permission",
|
|
@@ -17022,9 +17213,9 @@ class AcpSession {
|
|
|
17022
17213
|
const text = headers.length === 1 ? `with my answer to "${headers[0]}"` : "with my answers";
|
|
17023
17214
|
this.post(text, { attachments: followUp.attachments });
|
|
17024
17215
|
}
|
|
17025
|
-
cancelAsks() {
|
|
17216
|
+
cancelAsks(outcome = "cancelled") {
|
|
17026
17217
|
for (const ask of [...this.asks.values()])
|
|
17027
|
-
ask.settle(
|
|
17218
|
+
ask.settle(outcome);
|
|
17028
17219
|
}
|
|
17029
17220
|
syncStatus() {
|
|
17030
17221
|
if (this.status === "error")
|
|
@@ -17106,9 +17297,6 @@ class AcpSession {
|
|
|
17106
17297
|
this.noteProcess(conn.link, false);
|
|
17107
17298
|
}
|
|
17108
17299
|
}
|
|
17109
|
-
function isAuthRequired(e) {
|
|
17110
|
-
return e instanceof RequestError && e.code === -32000;
|
|
17111
|
-
}
|
|
17112
17300
|
function takesHttpMcp(init) {
|
|
17113
17301
|
const http = init.agentCapabilities?.mcpCapabilities?.http;
|
|
17114
17302
|
return http === true || typeof http === "object" && http !== null;
|
|
@@ -17185,66 +17373,6 @@ function sameCommands(a, b) {
|
|
|
17185
17373
|
return a.length === b.length && a.every((c, i) => c.name === b[i]?.name && c.description === b[i]?.description && c.hint === b[i]?.hint && c.origin === b[i]?.origin);
|
|
17186
17374
|
}
|
|
17187
17375
|
|
|
17188
|
-
// ../daemon/src/agent/planDoc.ts
|
|
17189
|
-
import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
17190
|
-
import { join as join10 } from "path";
|
|
17191
|
-
var PLANS_DIR = join10(".toyon", "plans");
|
|
17192
|
-
var PLAN_NAME = /^(\d+)\.md$/;
|
|
17193
|
-
function isPlanPath(rel) {
|
|
17194
|
-
return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
|
|
17195
|
-
}
|
|
17196
|
-
function lastNumber(dir) {
|
|
17197
|
-
if (!existsSync12(dir))
|
|
17198
|
-
return 0;
|
|
17199
|
-
let max = 0;
|
|
17200
|
-
for (const name of readdirSync4(dir)) {
|
|
17201
|
-
const n = Number(PLAN_NAME.exec(name)?.[1]);
|
|
17202
|
-
if (n > max)
|
|
17203
|
-
max = n;
|
|
17204
|
-
}
|
|
17205
|
-
return max;
|
|
17206
|
-
}
|
|
17207
|
-
function sameAsLast(cwd, rel, markdown) {
|
|
17208
|
-
try {
|
|
17209
|
-
return readFileSync10(join10(cwd, rel), "utf8").trim() === markdown.trim();
|
|
17210
|
-
} catch (e) {
|
|
17211
|
-
log.warn(cwd, `could not read ${rel}`, e);
|
|
17212
|
-
return false;
|
|
17213
|
-
}
|
|
17214
|
-
}
|
|
17215
|
-
async function writePlanDoc(cwd, markdown) {
|
|
17216
|
-
const dir = join10(cwd, PLANS_DIR);
|
|
17217
|
-
const last = lastNumber(dir);
|
|
17218
|
-
if (last > 0) {
|
|
17219
|
-
const newest = join10(PLANS_DIR, `${last}.md`);
|
|
17220
|
-
if (sameAsLast(cwd, newest, markdown))
|
|
17221
|
-
return newest;
|
|
17222
|
-
}
|
|
17223
|
-
const rel = join10(PLANS_DIR, `${last + 1}.md`);
|
|
17224
|
-
try {
|
|
17225
|
-
mkdirSync6(dir, { recursive: true });
|
|
17226
|
-
writeFileSync5(join10(cwd, rel), markdown.endsWith(`
|
|
17227
|
-
`) ? markdown : `${markdown}
|
|
17228
|
-
`);
|
|
17229
|
-
} catch (e) {
|
|
17230
|
-
log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
|
|
17231
|
-
return null;
|
|
17232
|
-
}
|
|
17233
|
-
await excludeFromGit(cwd, `${PLANS_DIR}/`);
|
|
17234
|
-
return rel;
|
|
17235
|
-
}
|
|
17236
|
-
function planEdited(cwd, rel, proposed) {
|
|
17237
|
-
const file = join10(cwd, rel);
|
|
17238
|
-
if (!existsSync12(file))
|
|
17239
|
-
return false;
|
|
17240
|
-
try {
|
|
17241
|
-
return readFileSync10(file, "utf8").trim() !== proposed.trim();
|
|
17242
|
-
} catch (e) {
|
|
17243
|
-
log.warn(cwd, `could not read ${rel}`, e);
|
|
17244
|
-
return false;
|
|
17245
|
-
}
|
|
17246
|
-
}
|
|
17247
|
-
|
|
17248
17376
|
// ../daemon/src/runtime/forward.ts
|
|
17249
17377
|
var POLL_MS2 = 100;
|
|
17250
17378
|
var HOLD_MS = 60000;
|
|
@@ -18458,8 +18586,8 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
18458
18586
|
onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
|
|
18459
18587
|
onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
|
|
18460
18588
|
beforeTurn: async () => d.turnStarting?.(wt.id),
|
|
18461
|
-
onAuth: (agentId, o) => d.
|
|
18462
|
-
onLimits: (agentId, limits) => d.
|
|
18589
|
+
onAuth: (agentId, o) => d.readiness?.observe(agentId, o),
|
|
18590
|
+
onLimits: (agentId, limits) => d.readiness?.observeLimits(agentId, limits),
|
|
18463
18591
|
seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
|
|
18464
18592
|
onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
|
|
18465
18593
|
commandOrigins: () => readCommandOrigins(commandFolders(spec(), wt.path)),
|
|
@@ -18468,13 +18596,14 @@ function defaultAgent(wt, d, preview, onProcess) {
|
|
|
18468
18596
|
if (d.state.setMode(wt.id, mode))
|
|
18469
18597
|
d.hub.emit("worktreesChanged");
|
|
18470
18598
|
},
|
|
18471
|
-
onPlan: async (markdown) => {
|
|
18599
|
+
onPlan: async (markdown, ownFile) => {
|
|
18472
18600
|
const w = d.state.worktree(wt.id);
|
|
18473
18601
|
if (w && !w.planned) {
|
|
18474
18602
|
w.planned = true;
|
|
18475
18603
|
d.state.save();
|
|
18476
18604
|
}
|
|
18477
|
-
const
|
|
18605
|
+
const own = ownFile ? ownPlanPath(wt.path, ownFile) : null;
|
|
18606
|
+
const path = own ?? (markdown.trim() ? await writePlanDoc(wt.path, markdown) : null);
|
|
18478
18607
|
const fresh = path ? d.state.worktree(wt.id) : undefined;
|
|
18479
18608
|
if (fresh && path && fresh.plan !== path) {
|
|
18480
18609
|
fresh.plan = path;
|
|
@@ -19504,7 +19633,7 @@ var LAND_ASK = [
|
|
|
19504
19633
|
"Line 2: blank",
|
|
19505
19634
|
"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
19635
|
"Line 4: blank",
|
|
19507
|
-
"Line 5: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
|
|
19636
|
+
"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
19637
|
"Line 6: blank",
|
|
19509
19638
|
"Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
|
|
19510
19639
|
].join(`
|
|
@@ -19512,17 +19641,25 @@ var LAND_ASK = [
|
|
|
19512
19641
|
var LAND_TURNS = 4;
|
|
19513
19642
|
var LAND_CHARS = 5000;
|
|
19514
19643
|
function landPrompt(i) {
|
|
19515
|
-
const
|
|
19516
|
-
|
|
19517
|
-
|
|
19644
|
+
const own = i.diffStat.trim();
|
|
19645
|
+
const committed = i.committedStat?.trim() ?? "";
|
|
19646
|
+
const head = [LAND_ASK, ""];
|
|
19647
|
+
if (own && i.earlier) {
|
|
19648
|
+
head.push(`Task, as the worktree is titled; background, since the branch committed or landed work before: ${clip(i.title, 200)}`);
|
|
19649
|
+
if (i.firstAsk)
|
|
19650
|
+
head.push(`First request here, which the earlier work may have answered: ${clip(i.firstAsk, 300)}`);
|
|
19651
|
+
} else {
|
|
19652
|
+
head.push(`Task: ${clip(i.title, 200)}`);
|
|
19653
|
+
if (i.firstAsk)
|
|
19654
|
+
head.push(`First request: ${clip(i.firstAsk, 300)}`);
|
|
19655
|
+
}
|
|
19518
19656
|
if (i.recentSubjects.length)
|
|
19519
19657
|
head.push(`Recent commit subjects:
|
|
19520
19658
|
${i.recentSubjects.map((s) => ` ${s}`).join(`
|
|
19521
19659
|
`)}`);
|
|
19522
19660
|
if (i.note?.trim())
|
|
19523
19661
|
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)"}`;
|
|
19662
|
+
const diff = diffBlock(own, committed);
|
|
19526
19663
|
let blocks = turnBlocks(i.turns);
|
|
19527
19664
|
const size = () => [...head, "", ...blocks, "", diff].join(`
|
|
19528
19665
|
|
|
@@ -19534,6 +19671,22 @@ ${i.diffStat.trim() || "(none)"}`;
|
|
|
19534
19671
|
|
|
19535
19672
|
`), diff].join(`
|
|
19536
19673
|
|
|
19674
|
+
`);
|
|
19675
|
+
}
|
|
19676
|
+
function diffBlock(own, committed) {
|
|
19677
|
+
if (!own && !committed)
|
|
19678
|
+
return `Diff summary:
|
|
19679
|
+
(none)`;
|
|
19680
|
+
const parts = [];
|
|
19681
|
+
if (own)
|
|
19682
|
+
parts.push(`What this commit will contain, which is all the message describes:
|
|
19683
|
+
${own}`);
|
|
19684
|
+
if (committed)
|
|
19685
|
+
parts.push(own ? `Already committed on this branch, context only, not what the message describes:
|
|
19686
|
+
${committed}` : `Nothing is uncommitted; the message is for the branch's commits as one:
|
|
19687
|
+
${committed}`);
|
|
19688
|
+
return parts.join(`
|
|
19689
|
+
|
|
19537
19690
|
`);
|
|
19538
19691
|
}
|
|
19539
19692
|
function turnBlocks(turns) {
|
|
@@ -19743,10 +19896,12 @@ function makePlanner(agents) {
|
|
|
19743
19896
|
}
|
|
19744
19897
|
|
|
19745
19898
|
// ../daemon/src/agent/transcriptForward.ts
|
|
19746
|
-
import { readdirSync as readdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
19899
|
+
import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
19747
19900
|
import { join as join12 } from "path";
|
|
19901
|
+
var FORWARD_STEP = "1";
|
|
19902
|
+
var MARK = '"page';
|
|
19748
19903
|
function forwardLine(line) {
|
|
19749
|
-
if (!line.includes(
|
|
19904
|
+
if (!line.includes(MARK))
|
|
19750
19905
|
return null;
|
|
19751
19906
|
let entry;
|
|
19752
19907
|
try {
|
|
@@ -19755,31 +19910,35 @@ function forwardLine(line) {
|
|
|
19755
19910
|
return null;
|
|
19756
19911
|
}
|
|
19757
19912
|
const e = entry.event;
|
|
19758
|
-
if (e?.type
|
|
19759
|
-
return
|
|
19760
|
-
|
|
19761
|
-
|
|
19913
|
+
if (e?.type === "page-error")
|
|
19914
|
+
return { drop: true };
|
|
19915
|
+
if (e?.type === "fix-asked" && e.kind === "page")
|
|
19916
|
+
return { line: JSON.stringify({ ...entry, event: { ...e, kind: "command" } }) };
|
|
19917
|
+
return null;
|
|
19762
19918
|
}
|
|
19763
19919
|
function forwardText(text) {
|
|
19764
19920
|
let changed = 0;
|
|
19765
|
-
const lines =
|
|
19766
|
-
|
|
19921
|
+
const lines = [];
|
|
19922
|
+
for (const line of text.split(`
|
|
19923
|
+
`)) {
|
|
19767
19924
|
const next = forwardLine(line);
|
|
19768
19925
|
if (next === null)
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
19926
|
+
lines.push(line);
|
|
19927
|
+
else {
|
|
19928
|
+
changed++;
|
|
19929
|
+
if ("line" in next)
|
|
19930
|
+
lines.push(next.line);
|
|
19931
|
+
}
|
|
19932
|
+
}
|
|
19773
19933
|
return { text: changed ? lines.join(`
|
|
19774
19934
|
`) : text, changed };
|
|
19775
19935
|
}
|
|
19776
|
-
function forwardTranscripts(
|
|
19936
|
+
function forwardTranscripts(dirs, stamp) {
|
|
19937
|
+
if (readStamp(stamp) === FORWARD_STEP)
|
|
19938
|
+
return;
|
|
19777
19939
|
let files = 0;
|
|
19778
19940
|
let rows = 0;
|
|
19779
|
-
for (const
|
|
19780
|
-
if (!name.endsWith(".jsonl"))
|
|
19781
|
-
continue;
|
|
19782
|
-
const path = join12(dir, name);
|
|
19941
|
+
for (const path of dirs.flatMap(transcriptsUnder)) {
|
|
19783
19942
|
const { text, changed } = forwardText(readFileSync11(path, "utf8"));
|
|
19784
19943
|
if (!changed)
|
|
19785
19944
|
continue;
|
|
@@ -19789,22 +19948,44 @@ function forwardTranscripts(dir) {
|
|
|
19789
19948
|
files++;
|
|
19790
19949
|
rows += changed;
|
|
19791
19950
|
}
|
|
19951
|
+
writeFileSync6(stamp, FORWARD_STEP);
|
|
19792
19952
|
if (files)
|
|
19793
|
-
log.info("transcripts", `
|
|
19953
|
+
log.info("transcripts", `moved ${rows} row(s) in ${files} transcript(s) past the preview-threw row`);
|
|
19954
|
+
}
|
|
19955
|
+
function readStamp(stamp) {
|
|
19956
|
+
try {
|
|
19957
|
+
return readFileSync11(stamp, "utf8").trim();
|
|
19958
|
+
} catch {
|
|
19959
|
+
return null;
|
|
19960
|
+
}
|
|
19961
|
+
}
|
|
19962
|
+
function transcriptsUnder(dir) {
|
|
19963
|
+
const found = [];
|
|
19964
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
19965
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
19966
|
+
found.push(join12(dir, entry.name));
|
|
19967
|
+
else if (entry.isDirectory()) {
|
|
19968
|
+
const inner = join12(dir, entry.name, "transcript.jsonl");
|
|
19969
|
+
if (existsSync14(inner))
|
|
19970
|
+
found.push(inner);
|
|
19971
|
+
}
|
|
19972
|
+
}
|
|
19973
|
+
return found;
|
|
19794
19974
|
}
|
|
19795
19975
|
|
|
19796
19976
|
// ../daemon/src/core/assets.ts
|
|
19797
|
-
import { existsSync as
|
|
19977
|
+
import { existsSync as existsSync15, readdirSync as readdirSync6, rmSync as rmSync3, statSync as statSync5 } from "fs";
|
|
19798
19978
|
import { join as join13, resolve as resolve5 } from "path";
|
|
19799
19979
|
function locateAssets(here, env = process.env) {
|
|
19800
|
-
const packaged =
|
|
19980
|
+
const packaged = existsSync15(join13(here, "shell", "index.html"));
|
|
19801
19981
|
return {
|
|
19802
19982
|
shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join13(here, "shell") : join13(here, "../../shell/dist")),
|
|
19803
19983
|
bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join13(here, "bridge.js") : join13(here, "../../bridge/dist/bridge.js")),
|
|
19804
19984
|
helperStub: packaged ? join13(here, "helper-stub") : join13(here, "../dist/helper-stub"),
|
|
19805
19985
|
cliEntry: packaged ? join13(here, "cli.js") : join13(here, "../../cli/src/cli.ts"),
|
|
19806
19986
|
sourceRoot: packaged ? null : resolve5(here, "../../.."),
|
|
19807
|
-
packageJson: packaged ? join13(here, "..", "package.json") : null
|
|
19987
|
+
packageJson: packaged ? join13(here, "..", "package.json") : null,
|
|
19988
|
+
changelog: packaged ? join13(here, "CHANGELOG.md") : resolve5(here, "../../../CHANGELOG.md")
|
|
19808
19989
|
};
|
|
19809
19990
|
}
|
|
19810
19991
|
var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
|
|
@@ -20263,7 +20444,7 @@ class KeepAwake {
|
|
|
20263
20444
|
}
|
|
20264
20445
|
|
|
20265
20446
|
// ../daemon/src/core/dialog.ts
|
|
20266
|
-
import { existsSync as
|
|
20447
|
+
import { existsSync as existsSync16 } from "fs";
|
|
20267
20448
|
import { homedir as homedir6 } from "os";
|
|
20268
20449
|
var PROMPTS = {
|
|
20269
20450
|
location: "Choose where the new project goes",
|
|
@@ -20281,7 +20462,7 @@ function folderDialog() {
|
|
|
20281
20462
|
let cancelled = false;
|
|
20282
20463
|
const ask = async (start, purpose) => {
|
|
20283
20464
|
const typed = start === "~" || start.startsWith("~/") ? `${homedir6()}${start.slice(1)}` : start;
|
|
20284
|
-
const from = typed.startsWith("/") &&
|
|
20465
|
+
const from = typed.startsWith("/") && existsSync16(typed) ? typed : homedir6();
|
|
20285
20466
|
cancelled = false;
|
|
20286
20467
|
const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
|
|
20287
20468
|
stdout: "pipe",
|
|
@@ -20358,9 +20539,9 @@ class GrantCodes {
|
|
|
20358
20539
|
|
|
20359
20540
|
// ../daemon/src/core/helper.ts
|
|
20360
20541
|
import { createHash as createHash3 } from "crypto";
|
|
20361
|
-
import { existsSync as
|
|
20542
|
+
import { existsSync as existsSync17 } from "fs";
|
|
20362
20543
|
import { chmod, copyFile as copyFile3, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
20363
|
-
import { basename as
|
|
20544
|
+
import { basename as basename5, dirname as dirname5, join as join14 } from "path";
|
|
20364
20545
|
var EXECUTABLE = "Toyon";
|
|
20365
20546
|
function helperWanted(where) {
|
|
20366
20547
|
return where.platform === "darwin" && !where.cloud && where.port === DAEMON_DEFAULT_PORT && where.home === undefined;
|
|
@@ -20439,7 +20620,7 @@ class Helper {
|
|
|
20439
20620
|
}
|
|
20440
20621
|
async write() {
|
|
20441
20622
|
const { spec } = this;
|
|
20442
|
-
if (!
|
|
20623
|
+
if (!existsSync17(spec.stub))
|
|
20443
20624
|
return "skipped";
|
|
20444
20625
|
const stubHash = createHash3("sha256").update(await readFile2(spec.stub)).digest("hex").slice(0, 16);
|
|
20445
20626
|
const { plist, script } = helperFiles(spec, stubHash);
|
|
@@ -20447,7 +20628,7 @@ class Helper {
|
|
|
20447
20628
|
const resources = join14(contents, "Resources");
|
|
20448
20629
|
const stub = join14(contents, "MacOS", EXECUTABLE);
|
|
20449
20630
|
const scriptFile = join14(resources, "launch.sh");
|
|
20450
|
-
if (
|
|
20631
|
+
if (existsSync17(stub) && existsSync17(scriptFile) && await readFile2(scriptFile, "utf8") === script)
|
|
20451
20632
|
return "kept";
|
|
20452
20633
|
await mkdir2(join14(contents, "MacOS"), { recursive: true });
|
|
20453
20634
|
await mkdir2(resources, { recursive: true });
|
|
@@ -20455,7 +20636,7 @@ class Helper {
|
|
|
20455
20636
|
await writeFile2(scriptFile, script, { mode: 493 });
|
|
20456
20637
|
await copyFile3(spec.stub, stub);
|
|
20457
20638
|
await chmod(stub, 493);
|
|
20458
|
-
if (!
|
|
20639
|
+
if (!existsSync17(join14(resources, "AppIcon.icns")))
|
|
20459
20640
|
await this.drawIcon(resources);
|
|
20460
20641
|
await this.run(["xattr", "-cr", spec.dir]);
|
|
20461
20642
|
if (!await this.run(["codesign", "--force", "--deep", "-s", "-", spec.dir])) {
|
|
@@ -20470,8 +20651,8 @@ class Helper {
|
|
|
20470
20651
|
const set = join14(tmp, "AppIcon.iconset");
|
|
20471
20652
|
await mkdir2(set, { recursive: true });
|
|
20472
20653
|
await this.run(["qlmanage", "-t", "-s", "1024", "-o", tmp, svg]);
|
|
20473
|
-
const big = join14(tmp, `${
|
|
20474
|
-
if (
|
|
20654
|
+
const big = join14(tmp, `${basename5(svg)}.png`);
|
|
20655
|
+
if (existsSync17(big)) {
|
|
20475
20656
|
for (const size of [16, 32, 64, 128, 256, 512, 1024]) {
|
|
20476
20657
|
await this.run(["sips", "-z", String(size), String(size), big, "--out", join14(set, `icon_${size}x${size}.png`)]);
|
|
20477
20658
|
}
|
|
@@ -20914,7 +21095,7 @@ class SelfWatch {
|
|
|
20914
21095
|
|
|
20915
21096
|
// ../daemon/src/core/state.ts
|
|
20916
21097
|
import { randomBytes as randomBytes6 } from "crypto";
|
|
20917
|
-
import { existsSync as
|
|
21098
|
+
import { existsSync as existsSync18, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync8 } from "fs";
|
|
20918
21099
|
var TRUSTED_MAX = 16;
|
|
20919
21100
|
var DAEMON_GROUPS = "daemon";
|
|
20920
21101
|
var GROUPS_SAVE_MS = 250;
|
|
@@ -20923,7 +21104,7 @@ var empty = { repos: [], worktrees: [], sessions: {} };
|
|
|
20923
21104
|
function loadState(paths) {
|
|
20924
21105
|
ensureDirs(paths);
|
|
20925
21106
|
const STATE_FILE = paths.stateFile;
|
|
20926
|
-
if (!
|
|
21107
|
+
if (!existsSync18(STATE_FILE))
|
|
20927
21108
|
return structuredClone(empty);
|
|
20928
21109
|
let raw;
|
|
20929
21110
|
try {
|
|
@@ -20975,7 +21156,7 @@ function saveState(paths, state) {
|
|
|
20975
21156
|
function loadOrCreateToken(paths) {
|
|
20976
21157
|
ensureDirs(paths);
|
|
20977
21158
|
const TOKEN_FILE = paths.tokenFile;
|
|
20978
|
-
if (
|
|
21159
|
+
if (existsSync18(TOKEN_FILE))
|
|
20979
21160
|
return readFileSync12(TOKEN_FILE, "utf8").trim();
|
|
20980
21161
|
const token = randomBytes6(32).toString("hex");
|
|
20981
21162
|
writeFileSync8(TOKEN_FILE, token, { mode: 384 });
|
|
@@ -21063,9 +21244,9 @@ class StateStore {
|
|
|
21063
21244
|
this.state.worktrees.push(wt);
|
|
21064
21245
|
this.save();
|
|
21065
21246
|
}
|
|
21066
|
-
removeWorktree(id) {
|
|
21247
|
+
removeWorktree(id, opts = {}) {
|
|
21067
21248
|
const wt = this.state.worktrees.find((w) => w.id === id);
|
|
21068
|
-
const repo = wt && wt.kind !== "spare" ? this.repo(wt.repoId) : undefined;
|
|
21249
|
+
const repo = wt && wt.kind !== "spare" && !opts.refused ? this.repo(wt.repoId) : undefined;
|
|
21069
21250
|
if (wt && repo)
|
|
21070
21251
|
repo.promptedAt = Math.max(repo.promptedAt ?? 0, sentAt(wt));
|
|
21071
21252
|
this.state.worktrees = this.state.worktrees.filter((w) => w.id !== id);
|
|
@@ -22010,7 +22191,7 @@ async function loadProjectTypescript(root) {
|
|
|
22010
22191
|
}
|
|
22011
22192
|
|
|
22012
22193
|
// ../daemon/src/drafts/store.ts
|
|
22013
|
-
import { existsSync as
|
|
22194
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13, renameSync as renameSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
22014
22195
|
var SAVE_DELAY_MS = 1000;
|
|
22015
22196
|
|
|
22016
22197
|
class DraftStore {
|
|
@@ -22147,7 +22328,7 @@ class DraftStore {
|
|
|
22147
22328
|
}
|
|
22148
22329
|
}
|
|
22149
22330
|
function readRecord(file) {
|
|
22150
|
-
if (!
|
|
22331
|
+
if (!existsSync19(file))
|
|
22151
22332
|
return [];
|
|
22152
22333
|
try {
|
|
22153
22334
|
const raw = JSON.parse(readFileSync13(file, "utf8"));
|
|
@@ -22404,10 +22585,10 @@ class ExecService {
|
|
|
22404
22585
|
|
|
22405
22586
|
// ../daemon/src/files/open.ts
|
|
22406
22587
|
import { stat as stat3 } from "fs/promises";
|
|
22407
|
-
import { basename as
|
|
22588
|
+
import { basename as basename6, resolve as resolve7 } from "path";
|
|
22408
22589
|
|
|
22409
22590
|
// ../daemon/src/repos/browse.ts
|
|
22410
|
-
import { existsSync as
|
|
22591
|
+
import { existsSync as existsSync20, statSync as statSync6 } from "fs";
|
|
22411
22592
|
import { readdir } from "fs/promises";
|
|
22412
22593
|
import { homedir as homedir8 } from "os";
|
|
22413
22594
|
import { dirname as dirname6, join as join16 } from "path";
|
|
@@ -22424,7 +22605,7 @@ function holdsNothing(names) {
|
|
|
22424
22605
|
}
|
|
22425
22606
|
async function describeFolder(abs) {
|
|
22426
22607
|
const path = collapseTilde(abs);
|
|
22427
|
-
if (
|
|
22608
|
+
if (existsSync20(join16(abs, ".git")))
|
|
22428
22609
|
return { path, kind: "project" };
|
|
22429
22610
|
try {
|
|
22430
22611
|
return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
|
|
@@ -22439,10 +22620,10 @@ function describe2(typed, parent) {
|
|
|
22439
22620
|
isDir = statSync6(typed).isDirectory();
|
|
22440
22621
|
} catch {}
|
|
22441
22622
|
return {
|
|
22442
|
-
exists:
|
|
22623
|
+
exists: existsSync20(typed),
|
|
22443
22624
|
isDir,
|
|
22444
|
-
isRepo: isDir &&
|
|
22445
|
-
parentExists:
|
|
22625
|
+
isRepo: isDir && existsSync20(join16(typed, ".git")),
|
|
22626
|
+
parentExists: existsSync20(parent)
|
|
22446
22627
|
};
|
|
22447
22628
|
}
|
|
22448
22629
|
async function browsePath(raw) {
|
|
@@ -22467,7 +22648,7 @@ async function browsePath(raw) {
|
|
|
22467
22648
|
if (!name.toLowerCase().startsWith(prefix))
|
|
22468
22649
|
continue;
|
|
22469
22650
|
const abs = join16(dir, name);
|
|
22470
|
-
entries.push({ path: collapseTilde(abs), name, isRepo:
|
|
22651
|
+
entries.push({ path: collapseTilde(abs), name, isRepo: existsSync20(join16(abs, ".git")) });
|
|
22471
22652
|
}
|
|
22472
22653
|
entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
|
|
22473
22654
|
return { entries: entries.slice(0, MAX_ENTRIES), target };
|
|
@@ -23291,6 +23472,9 @@ class OpenService {
|
|
|
23291
23472
|
throw new UserError(`${raw} is a folder: Toyon opens files`);
|
|
23292
23473
|
return this.openFile(raw, path, st);
|
|
23293
23474
|
}
|
|
23475
|
+
async where(raw) {
|
|
23476
|
+
return (await this.locate(raw)).path;
|
|
23477
|
+
}
|
|
23294
23478
|
async locate(raw) {
|
|
23295
23479
|
const path = canonical(resolve7(expandTilde(raw)));
|
|
23296
23480
|
const st = await stat3(path).catch((e) => {
|
|
@@ -23313,7 +23497,7 @@ class OpenService {
|
|
|
23313
23497
|
return hit ? { kind: "file", worktreeId: hit.id, path: path.slice(hit.root.length + 1) } : null;
|
|
23314
23498
|
}
|
|
23315
23499
|
async grant(path, size, mtimeMs) {
|
|
23316
|
-
const name =
|
|
23500
|
+
const name = basename6(path);
|
|
23317
23501
|
if (this.d.refused.some((r) => within2(path, canonical(resolve7(r))))) {
|
|
23318
23502
|
throw new UserError(`${name} is inside Toyon's own folder, which is not for editing`);
|
|
23319
23503
|
}
|
|
@@ -23390,13 +23574,70 @@ function parseAllowed(json) {
|
|
|
23390
23574
|
return null;
|
|
23391
23575
|
}
|
|
23392
23576
|
}
|
|
23393
|
-
var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest";
|
|
23577
|
+
var PR_VIEW_FIELDS = "number,url,title,state,mergedAt,reviewDecision,statusCheckRollup,mergeable,autoMergeRequest,headRefOid";
|
|
23394
23578
|
async function viewPr(repoPath, number) {
|
|
23395
23579
|
const r = await gh(["pr", "view", String(number), "--json", PR_VIEW_FIELDS], repoPath, 20000);
|
|
23396
23580
|
if (!r.ok || !r.out)
|
|
23397
23581
|
return null;
|
|
23398
23582
|
const parsed = parsePrView(r.out);
|
|
23399
|
-
|
|
23583
|
+
if (!parsed)
|
|
23584
|
+
return null;
|
|
23585
|
+
if (parsed.state === "open" && !parsed.preview) {
|
|
23586
|
+
const head = headOf(r.out);
|
|
23587
|
+
const preview = head ? await viewDeploymentPreview(repoPath, head) : null;
|
|
23588
|
+
if (preview)
|
|
23589
|
+
parsed.preview = preview;
|
|
23590
|
+
}
|
|
23591
|
+
return { ...parsed, at: Date.now() };
|
|
23592
|
+
}
|
|
23593
|
+
function headOf(json) {
|
|
23594
|
+
try {
|
|
23595
|
+
const o = JSON.parse(json);
|
|
23596
|
+
return typeof o.headRefOid === "string" && o.headRefOid ? o.headRefOid : null;
|
|
23597
|
+
} catch {
|
|
23598
|
+
return null;
|
|
23599
|
+
}
|
|
23600
|
+
}
|
|
23601
|
+
var DEPLOYMENTS_QUERY = `query($owner: String!, $name: String!, $oid: GitObjectID!) {
|
|
23602
|
+
repository(owner: $owner, name: $name) {
|
|
23603
|
+
object(oid: $oid) { ... on Commit {
|
|
23604
|
+
deployments(last: 10) { nodes { environment latestStatus { state environmentUrl } } }
|
|
23605
|
+
} }
|
|
23606
|
+
}
|
|
23607
|
+
}`;
|
|
23608
|
+
async function viewDeploymentPreview(repoPath, head) {
|
|
23609
|
+
const r = await gh([
|
|
23610
|
+
"api",
|
|
23611
|
+
"graphql",
|
|
23612
|
+
"-F",
|
|
23613
|
+
"owner={owner}",
|
|
23614
|
+
"-F",
|
|
23615
|
+
"name={repo}",
|
|
23616
|
+
"-f",
|
|
23617
|
+
`oid=${head}`,
|
|
23618
|
+
"-f",
|
|
23619
|
+
`query=${DEPLOYMENTS_QUERY}`
|
|
23620
|
+
], repoPath, 20000);
|
|
23621
|
+
if (!r.ok || !r.out)
|
|
23622
|
+
return null;
|
|
23623
|
+
return parseDeployments(r.out);
|
|
23624
|
+
}
|
|
23625
|
+
function parseDeployments(json) {
|
|
23626
|
+
let nodes;
|
|
23627
|
+
try {
|
|
23628
|
+
const o = JSON.parse(json);
|
|
23629
|
+
nodes = o.data?.repository?.object?.deployments?.nodes;
|
|
23630
|
+
} catch {
|
|
23631
|
+
return null;
|
|
23632
|
+
}
|
|
23633
|
+
if (!Array.isArray(nodes))
|
|
23634
|
+
return null;
|
|
23635
|
+
for (const d of [...nodes].reverse()) {
|
|
23636
|
+
const st = d?.latestStatus;
|
|
23637
|
+
if (st?.state === "SUCCESS" && isPreviewUrl(st.environmentUrl))
|
|
23638
|
+
return st.environmentUrl;
|
|
23639
|
+
}
|
|
23640
|
+
return null;
|
|
23400
23641
|
}
|
|
23401
23642
|
function parsePrView(json) {
|
|
23402
23643
|
let o;
|
|
@@ -23412,6 +23653,7 @@ function parsePrView(json) {
|
|
|
23412
23653
|
const checks = foldChecks(o.statusCheckRollup);
|
|
23413
23654
|
const mergeable = o.mergeable === "MERGEABLE" ? true : o.mergeable === "CONFLICTING" ? false : undefined;
|
|
23414
23655
|
const automerge = o.autoMergeRequest !== null && o.autoMergeRequest !== undefined;
|
|
23656
|
+
const preview = state === "open" ? previewOf3(o.statusCheckRollup) : undefined;
|
|
23415
23657
|
return {
|
|
23416
23658
|
number: o.number,
|
|
23417
23659
|
url: o.url,
|
|
@@ -23420,9 +23662,37 @@ function parsePrView(json) {
|
|
|
23420
23662
|
...review ? { review } : {},
|
|
23421
23663
|
...checks ? { checks } : {},
|
|
23422
23664
|
...mergeable !== undefined ? { mergeable } : {},
|
|
23423
|
-
...automerge ? { automerge } : {}
|
|
23665
|
+
...automerge ? { automerge } : {},
|
|
23666
|
+
...preview ? { preview } : {}
|
|
23424
23667
|
};
|
|
23425
23668
|
}
|
|
23669
|
+
function previewOf3(rollup) {
|
|
23670
|
+
if (!Array.isArray(rollup))
|
|
23671
|
+
return;
|
|
23672
|
+
for (const c of rollup) {
|
|
23673
|
+
const name = String(c.context ?? c.name ?? "");
|
|
23674
|
+
if (!/preview/i.test(name))
|
|
23675
|
+
continue;
|
|
23676
|
+
const word = String(c.conclusion ?? c.state ?? "").toUpperCase();
|
|
23677
|
+
if (word !== "SUCCESS")
|
|
23678
|
+
continue;
|
|
23679
|
+
const url = c.targetUrl ?? c.detailsUrl;
|
|
23680
|
+
if (isPreviewUrl(url))
|
|
23681
|
+
return url;
|
|
23682
|
+
}
|
|
23683
|
+
return;
|
|
23684
|
+
}
|
|
23685
|
+
function isPreviewUrl(url) {
|
|
23686
|
+
if (typeof url !== "string")
|
|
23687
|
+
return false;
|
|
23688
|
+
let host;
|
|
23689
|
+
try {
|
|
23690
|
+
host = new URL(url).hostname;
|
|
23691
|
+
} catch {
|
|
23692
|
+
return false;
|
|
23693
|
+
}
|
|
23694
|
+
return host !== "github.com" && !host.endsWith(".github.com");
|
|
23695
|
+
}
|
|
23426
23696
|
function foldChecks(rollup) {
|
|
23427
23697
|
if (!Array.isArray(rollup) || rollup.length === 0)
|
|
23428
23698
|
return;
|
|
@@ -23447,7 +23717,6 @@ function prNumberOf(url) {
|
|
|
23447
23717
|
|
|
23448
23718
|
// ../daemon/src/preview/errors.ts
|
|
23449
23719
|
var KEPT = 5;
|
|
23450
|
-
var ROWS_MAX = 3;
|
|
23451
23720
|
var TWICE_MS = 1000;
|
|
23452
23721
|
|
|
23453
23722
|
class PageErrorService {
|
|
@@ -23455,75 +23724,35 @@ class PageErrorService {
|
|
|
23455
23724
|
held = new Map;
|
|
23456
23725
|
constructor(d) {
|
|
23457
23726
|
this.d = d;
|
|
23458
|
-
d.hub.on("agent", (worktreeId, _seq, event) => this.heard(worktreeId, event));
|
|
23459
23727
|
d.hub.on("worktreesChanged", () => {
|
|
23460
23728
|
for (const id of this.held.keys())
|
|
23461
23729
|
if (!d.known(id))
|
|
23462
23730
|
this.held.delete(id);
|
|
23463
23731
|
});
|
|
23464
23732
|
}
|
|
23465
|
-
of(worktreeId) {
|
|
23466
|
-
let h = this.held.get(worktreeId);
|
|
23467
|
-
if (!h) {
|
|
23468
|
-
h = { recent: [], running: false, noted: new Set, rows: 0 };
|
|
23469
|
-
this.held.set(worktreeId, h);
|
|
23470
|
-
}
|
|
23471
|
-
return h;
|
|
23472
|
-
}
|
|
23473
|
-
heard(worktreeId, event) {
|
|
23474
|
-
const h = this.of(worktreeId);
|
|
23475
|
-
switch (event.type) {
|
|
23476
|
-
case "user-message":
|
|
23477
|
-
case "fix-asked":
|
|
23478
|
-
h.noted = new Set;
|
|
23479
|
-
h.rows = 0;
|
|
23480
|
-
break;
|
|
23481
|
-
case "turn-start":
|
|
23482
|
-
h.running = true;
|
|
23483
|
-
break;
|
|
23484
|
-
case "turn-end":
|
|
23485
|
-
case "agent-error":
|
|
23486
|
-
case "agent-auth-required":
|
|
23487
|
-
h.running = false;
|
|
23488
|
-
this.flush(worktreeId, h, event.ts);
|
|
23489
|
-
break;
|
|
23490
|
-
}
|
|
23491
|
-
}
|
|
23492
23733
|
report(worktreeId, message) {
|
|
23493
23734
|
if (!this.d.known(worktreeId)) {
|
|
23494
23735
|
log.debug("pages", `an error from unknown worktree ${worktreeId} was dropped`);
|
|
23495
23736
|
return;
|
|
23496
23737
|
}
|
|
23497
|
-
|
|
23738
|
+
let h = this.held.get(worktreeId);
|
|
23739
|
+
if (!h) {
|
|
23740
|
+
h = [];
|
|
23741
|
+
this.held.set(worktreeId, h);
|
|
23742
|
+
}
|
|
23498
23743
|
const now = this.d.now?.() ?? Date.now();
|
|
23499
|
-
const last = h.
|
|
23744
|
+
const last = h.at(-1);
|
|
23500
23745
|
if (last && last.message === message && now - last.ts < TWICE_MS)
|
|
23501
23746
|
return;
|
|
23502
|
-
h.
|
|
23503
|
-
if (h.
|
|
23504
|
-
h.
|
|
23505
|
-
if (!h.running)
|
|
23506
|
-
this.flush(worktreeId, h, now);
|
|
23507
|
-
}
|
|
23508
|
-
flush(worktreeId, h, ts) {
|
|
23509
|
-
const fresh = [...new Set(h.recent.map((e) => e.message))].filter((m) => !h.noted.has(m));
|
|
23510
|
-
if (fresh.length === 0 || h.rows >= ROWS_MAX)
|
|
23511
|
-
return;
|
|
23512
|
-
const agent = this.d.runtime.agentFor(worktreeId);
|
|
23513
|
-
if (!agent)
|
|
23514
|
-
return;
|
|
23515
|
-
for (const m of fresh)
|
|
23516
|
-
h.noted.add(m);
|
|
23517
|
-
h.rows++;
|
|
23518
|
-
agent.note({ type: "page-error", errors: fresh, ts });
|
|
23747
|
+
h.push({ message, ts: now });
|
|
23748
|
+
if (h.length > KEPT)
|
|
23749
|
+
h.splice(0, h.length - KEPT);
|
|
23519
23750
|
}
|
|
23520
23751
|
loaded(worktreeId) {
|
|
23521
|
-
|
|
23522
|
-
if (h)
|
|
23523
|
-
h.recent = [];
|
|
23752
|
+
this.held.delete(worktreeId);
|
|
23524
23753
|
}
|
|
23525
23754
|
recent(worktreeId) {
|
|
23526
|
-
return this.held.get(worktreeId)?.
|
|
23755
|
+
return this.held.get(worktreeId)?.map((e) => e.message) ?? [];
|
|
23527
23756
|
}
|
|
23528
23757
|
ambient(worktreeId) {
|
|
23529
23758
|
const errors = this.recent(worktreeId);
|
|
@@ -23535,122 +23764,6 @@ ${errors.map((e) => `- ${e}`).join(`
|
|
|
23535
23764
|
}
|
|
23536
23765
|
}
|
|
23537
23766
|
|
|
23538
|
-
// ../daemon/src/preview/render.ts
|
|
23539
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
23540
|
-
var WAIT_MS2 = 1e4;
|
|
23541
|
-
var PATH_MAX = 2048;
|
|
23542
|
-
var refused = (text) => ({ text, isError: true });
|
|
23543
|
-
|
|
23544
|
-
class RenderService {
|
|
23545
|
-
d;
|
|
23546
|
-
courier = () => false;
|
|
23547
|
-
pending = new Map;
|
|
23548
|
-
out = new Set;
|
|
23549
|
-
constructor(d) {
|
|
23550
|
-
this.d = d;
|
|
23551
|
-
}
|
|
23552
|
-
tool() {
|
|
23553
|
-
return {
|
|
23554
|
-
name: PREVIEW_TOOL,
|
|
23555
|
-
description: "See whether a page of this worktree's preview renders without throwing. Pass `path` (for example `/about`): a browser the user has open loads it beside what they are looking at and reports the uncaught errors and unhandled rejections it threw in the moment after load. With no path, reports what the preview has thrown since it last loaded, without rendering. It does not describe what the page looks like, and it is not a substitute for the user's own checks.",
|
|
23556
|
-
inputSchema: {
|
|
23557
|
-
type: "object",
|
|
23558
|
-
properties: {
|
|
23559
|
-
path: {
|
|
23560
|
-
type: "string",
|
|
23561
|
-
description: "the route to render, starting with `/`; absent to report without rendering"
|
|
23562
|
-
}
|
|
23563
|
-
},
|
|
23564
|
-
additionalProperties: false
|
|
23565
|
-
}
|
|
23566
|
-
};
|
|
23567
|
-
}
|
|
23568
|
-
async call(worktreeId, args) {
|
|
23569
|
-
const a = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
23570
|
-
if (a.path !== undefined && typeof a.path !== "string")
|
|
23571
|
-
return refused("`path` is a string: a route, like `/about`.");
|
|
23572
|
-
const path = a.path?.trim();
|
|
23573
|
-
if (path !== undefined && (!/^(?:\/(?![/\\])|[?#])/.test(path) || path.length > PATH_MAX)) {
|
|
23574
|
-
return refused("`path` is a route of the preview, starting with `/`, like `/about`.");
|
|
23575
|
-
}
|
|
23576
|
-
const standing = this.d.runtime.previewStanding(worktreeId);
|
|
23577
|
-
if (!standing)
|
|
23578
|
-
return refused("This project has nothing to run, so there is no page to render.");
|
|
23579
|
-
if (standing.status !== "running")
|
|
23580
|
-
return refused(previewContext(standing) ?? "The preview is not running.");
|
|
23581
|
-
if (path === undefined)
|
|
23582
|
-
return { text: this.held(worktreeId) };
|
|
23583
|
-
if (this.out.has(worktreeId))
|
|
23584
|
-
return refused("A render is already under way; call again when it has returned.");
|
|
23585
|
-
const result = await this.render(worktreeId, path);
|
|
23586
|
-
if (!result)
|
|
23587
|
-
return refused(`No browser has this worktree open right now, so nothing can render it. ${this.held(worktreeId)}`);
|
|
23588
|
-
if (!result.ok)
|
|
23589
|
-
return refused(`Could not render ${path}: ${result.reason ?? "the page did not report"}.`);
|
|
23590
|
-
return { text: this.report(path, result) };
|
|
23591
|
-
}
|
|
23592
|
-
render(worktreeId, path) {
|
|
23593
|
-
const id = randomUUID2();
|
|
23594
|
-
return new Promise((resolve) => {
|
|
23595
|
-
const wait = this.d.waitMs ?? WAIT_MS2;
|
|
23596
|
-
const timer = setTimeout(() => settle({
|
|
23597
|
-
worktreeId,
|
|
23598
|
-
id,
|
|
23599
|
-
ok: false,
|
|
23600
|
-
reason: `the page did not report within ${Math.round(wait / 1000)}s`,
|
|
23601
|
-
errors: []
|
|
23602
|
-
}), wait);
|
|
23603
|
-
const settle = (r) => {
|
|
23604
|
-
if (!this.pending.delete(id))
|
|
23605
|
-
return;
|
|
23606
|
-
clearTimeout(timer);
|
|
23607
|
-
this.out.delete(worktreeId);
|
|
23608
|
-
resolve(r);
|
|
23609
|
-
};
|
|
23610
|
-
this.pending.set(id, { worktreeId, settle });
|
|
23611
|
-
this.out.add(worktreeId);
|
|
23612
|
-
if (!this.courier(worktreeId, { t: "render", worktreeId, id, path }))
|
|
23613
|
-
settle(null);
|
|
23614
|
-
});
|
|
23615
|
-
}
|
|
23616
|
-
rendered(msg) {
|
|
23617
|
-
const p = this.pending.get(msg.id);
|
|
23618
|
-
if (!p || p.worktreeId !== msg.worktreeId) {
|
|
23619
|
-
log.debug("render", `an answer for no render out on ${msg.worktreeId} was dropped`);
|
|
23620
|
-
return;
|
|
23621
|
-
}
|
|
23622
|
-
p.settle(msg);
|
|
23623
|
-
}
|
|
23624
|
-
held(worktreeId) {
|
|
23625
|
-
const errors = this.d.pageErrors.recent(worktreeId);
|
|
23626
|
-
if (errors.length === 0)
|
|
23627
|
-
return "The preview has thrown nothing since it last loaded.";
|
|
23628
|
-
return `The preview has thrown since it last loaded:
|
|
23629
|
-
${errors.map((e) => `- ${e}`).join(`
|
|
23630
|
-
`)}`;
|
|
23631
|
-
}
|
|
23632
|
-
report(path, r) {
|
|
23633
|
-
const title = r.title ? ` (title "${r.title}")` : "";
|
|
23634
|
-
const landed = r.url ? pathOf(r.url) : null;
|
|
23635
|
-
const elsewhere = landed && landed !== path ? ` It landed on ${landed}.` : "";
|
|
23636
|
-
const head = `Rendered ${path} in the user's browser${title}.${elsewhere}`;
|
|
23637
|
-
if (r.errors.length === 0)
|
|
23638
|
-
return `${head} No errors in the moment after load.`;
|
|
23639
|
-
const n = r.errors.length;
|
|
23640
|
-
return `${head} ${n} ${n === 1 ? "error" : "errors"} in the moment after load:
|
|
23641
|
-
${r.errors.map((e) => `- ${e}`).join(`
|
|
23642
|
-
`)}`;
|
|
23643
|
-
}
|
|
23644
|
-
}
|
|
23645
|
-
function pathOf(url) {
|
|
23646
|
-
try {
|
|
23647
|
-
const u = new URL(url);
|
|
23648
|
-
return `${u.pathname}${u.search}`;
|
|
23649
|
-
} catch {
|
|
23650
|
-
return null;
|
|
23651
|
-
}
|
|
23652
|
-
}
|
|
23653
|
-
|
|
23654
23767
|
// ../daemon/src/runtime/setup.ts
|
|
23655
23768
|
var SETUP_RING = 64 * 1024;
|
|
23656
23769
|
var SETUP_COLS = 120;
|
|
@@ -23779,9 +23892,9 @@ class AfterLand {
|
|
|
23779
23892
|
}
|
|
23780
23893
|
|
|
23781
23894
|
// ../daemon/src/repos/registry.ts
|
|
23782
|
-
import { existsSync as
|
|
23895
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync7, rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
|
|
23783
23896
|
import { homedir as homedir10 } from "os";
|
|
23784
|
-
import { basename as
|
|
23897
|
+
import { basename as basename8, dirname as dirname8, join as join21 } from "path";
|
|
23785
23898
|
|
|
23786
23899
|
// ../daemon/src/git/base.ts
|
|
23787
23900
|
async function resolveBase(repoPath, defaultBranch, route) {
|
|
@@ -23792,7 +23905,7 @@ async function resolveBase(repoPath, defaultBranch, route) {
|
|
|
23792
23905
|
}
|
|
23793
23906
|
|
|
23794
23907
|
// ../daemon/src/repos/config.ts
|
|
23795
|
-
import { closeSync, existsSync as
|
|
23908
|
+
import { closeSync, existsSync as existsSync21, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync14, readSync, statSync as statSync7 } from "fs";
|
|
23796
23909
|
import { join as join18 } from "path";
|
|
23797
23910
|
|
|
23798
23911
|
// ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
@@ -25157,7 +25270,7 @@ function applyEdits(text, edits) {
|
|
|
25157
25270
|
|
|
25158
25271
|
// ../daemon/src/repos/config.ts
|
|
25159
25272
|
var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
|
|
25160
|
-
var present = (repoPath, place) => [place.shared, place.local].filter((rel) =>
|
|
25273
|
+
var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync21(join18(repoPath, rel)));
|
|
25161
25274
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
25162
25275
|
function parseSettings(text) {
|
|
25163
25276
|
const errors = [];
|
|
@@ -25242,7 +25355,7 @@ function configTarget(repoPath, made) {
|
|
|
25242
25355
|
function configBody(repoPath, rel, config) {
|
|
25243
25356
|
const place = PLACES.find((p) => p.local === rel);
|
|
25244
25357
|
const abs = place && join18(repoPath, place.shared);
|
|
25245
|
-
if (!abs || !
|
|
25358
|
+
if (!abs || !existsSync21(abs))
|
|
25246
25359
|
return config;
|
|
25247
25360
|
let shared;
|
|
25248
25361
|
try {
|
|
@@ -25290,7 +25403,7 @@ function detectConfig(repoPath) {
|
|
|
25290
25403
|
if (explicit)
|
|
25291
25404
|
log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
|
|
25292
25405
|
const pkgPath = join18(repoPath, "package.json");
|
|
25293
|
-
if (
|
|
25406
|
+
if (existsSync21(pkgPath)) {
|
|
25294
25407
|
const pkg = JSON.parse(readFileSync14(pkgPath, "utf8"));
|
|
25295
25408
|
const scripts = pkg.scripts ?? {};
|
|
25296
25409
|
const runner = detectRunner(repoPath, pkg.packageManager);
|
|
@@ -25312,7 +25425,7 @@ function detectConfig(repoPath) {
|
|
|
25312
25425
|
};
|
|
25313
25426
|
}
|
|
25314
25427
|
}
|
|
25315
|
-
if (
|
|
25428
|
+
if (existsSync21(join18(repoPath, "start.sh"))) {
|
|
25316
25429
|
return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
|
|
25317
25430
|
}
|
|
25318
25431
|
const assumed = assumeNothingToRun(repoPath);
|
|
@@ -25355,7 +25468,7 @@ function assumeNothingToRun(repoPath) {
|
|
|
25355
25468
|
return marker && !servesPage(repoPath) ? marker : undefined;
|
|
25356
25469
|
}
|
|
25357
25470
|
function markerIn(repoPath) {
|
|
25358
|
-
const named = NO_PAGE_MARKERS.find((f) =>
|
|
25471
|
+
const named = NO_PAGE_MARKERS.find((f) => existsSync21(join18(repoPath, f)));
|
|
25359
25472
|
if (named)
|
|
25360
25473
|
return named;
|
|
25361
25474
|
let entries;
|
|
@@ -25367,7 +25480,7 @@ function markerIn(repoPath) {
|
|
|
25367
25480
|
return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
25368
25481
|
}
|
|
25369
25482
|
function servesPage(repoPath) {
|
|
25370
|
-
if (WEB_FILES.some((f) =>
|
|
25483
|
+
if (WEB_FILES.some((f) => existsSync21(join18(repoPath, f))))
|
|
25371
25484
|
return true;
|
|
25372
25485
|
if (/^web:/m.test(head(join18(repoPath, "Procfile"))))
|
|
25373
25486
|
return true;
|
|
@@ -25404,7 +25517,7 @@ function mentions(text, name) {
|
|
|
25404
25517
|
return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
|
|
25405
25518
|
}
|
|
25406
25519
|
function head(path) {
|
|
25407
|
-
if (!
|
|
25520
|
+
if (!existsSync21(path))
|
|
25408
25521
|
return "";
|
|
25409
25522
|
try {
|
|
25410
25523
|
const fd = openSync2(path, "r");
|
|
@@ -25445,7 +25558,7 @@ function isLocalDatabase(url) {
|
|
|
25445
25558
|
}
|
|
25446
25559
|
function detectServices(repoPath) {
|
|
25447
25560
|
const out = {};
|
|
25448
|
-
const compose = COMPOSE_FILES.find((f) =>
|
|
25561
|
+
const compose = COMPOSE_FILES.find((f) => existsSync21(join18(repoPath, f)));
|
|
25449
25562
|
if (compose)
|
|
25450
25563
|
out.compose = compose;
|
|
25451
25564
|
for (const file of ENV_FILES) {
|
|
@@ -25484,21 +25597,21 @@ function inferPaths(cmd, repoPath, depth = 1) {
|
|
|
25484
25597
|
const t = tokens[i];
|
|
25485
25598
|
if (t === "cd") {
|
|
25486
25599
|
const dir = tokens[i + 1];
|
|
25487
|
-
if (dir && !dir.startsWith("-") &&
|
|
25600
|
+
if (dir && !dir.startsWith("-") && existsSync21(join18(repoPath, dir)))
|
|
25488
25601
|
add(dir);
|
|
25489
25602
|
continue;
|
|
25490
25603
|
}
|
|
25491
25604
|
if (t.startsWith("-") || t.startsWith("$"))
|
|
25492
25605
|
continue;
|
|
25493
25606
|
const mod = /^([A-Za-z_]\w*)(?:\.\w+)*:\w+$/.exec(t);
|
|
25494
|
-
if (mod?.[1] &&
|
|
25607
|
+
if (mod?.[1] && existsSync21(join18(repoPath, mod[1]))) {
|
|
25495
25608
|
add(mod[1]);
|
|
25496
25609
|
continue;
|
|
25497
25610
|
}
|
|
25498
25611
|
const rel = t.replace(/^\.\//, "");
|
|
25499
25612
|
if (rel.startsWith("/") || rel.startsWith("..") || rel.includes("="))
|
|
25500
25613
|
continue;
|
|
25501
|
-
if (!
|
|
25614
|
+
if (!existsSync21(join18(repoPath, rel)))
|
|
25502
25615
|
continue;
|
|
25503
25616
|
add(rel);
|
|
25504
25617
|
if (depth > 0 && rel.endsWith(".sh")) {
|
|
@@ -25525,9 +25638,9 @@ function procCommand(runner, script, body) {
|
|
|
25525
25638
|
return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
|
|
25526
25639
|
}
|
|
25527
25640
|
function detectPages(repoPath, scripts, runner) {
|
|
25528
|
-
if (!scripts.build || !
|
|
25641
|
+
if (!scripts.build || !existsSync21(join18(repoPath, "functions")))
|
|
25529
25642
|
return null;
|
|
25530
|
-
const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join18(repoPath, f)).find((f) =>
|
|
25643
|
+
const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join18(repoPath, f)).find((f) => existsSync21(f));
|
|
25531
25644
|
if (!cfg)
|
|
25532
25645
|
return null;
|
|
25533
25646
|
const out = readFileSync14(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
|
|
@@ -25536,17 +25649,17 @@ function detectPages(repoPath, scripts, runner) {
|
|
|
25536
25649
|
return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
|
|
25537
25650
|
}
|
|
25538
25651
|
function detectRunner(repoPath, packageManager) {
|
|
25539
|
-
if (
|
|
25652
|
+
if (existsSync21(join18(repoPath, "bun.lock")) || existsSync21(join18(repoPath, "bun.lockb")))
|
|
25540
25653
|
return "bun";
|
|
25541
|
-
if (
|
|
25654
|
+
if (existsSync21(join18(repoPath, "pnpm-lock.yaml")))
|
|
25542
25655
|
return "pnpm";
|
|
25543
|
-
if (
|
|
25656
|
+
if (existsSync21(join18(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
|
|
25544
25657
|
return "yarn";
|
|
25545
25658
|
return "npm";
|
|
25546
25659
|
}
|
|
25547
25660
|
|
|
25548
25661
|
// ../daemon/src/repos/create.ts
|
|
25549
|
-
import { existsSync as
|
|
25662
|
+
import { existsSync as existsSync22, realpathSync as realpathSync2, statSync as statSync8 } from "fs";
|
|
25550
25663
|
import { appendFile as appendFile2, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
25551
25664
|
import { homedir as homedir9 } from "os";
|
|
25552
25665
|
import { dirname as dirname7, join as join19, resolve as resolve8 } from "path";
|
|
@@ -25561,13 +25674,13 @@ function planProject(opts, home = homedir9()) {
|
|
|
25561
25674
|
const first = firstProjectsFolder(opts.parent, home);
|
|
25562
25675
|
const parent = first ?? resolveParent(opts.parent);
|
|
25563
25676
|
const dir = join19(parent, opts.name.trim());
|
|
25564
|
-
if (
|
|
25677
|
+
if (existsSync22(dir))
|
|
25565
25678
|
throw new UserError(`${dir} already exists`);
|
|
25566
25679
|
return { parent, dir, makeParent: first !== null };
|
|
25567
25680
|
}
|
|
25568
25681
|
function firstProjectsFolder(raw, home) {
|
|
25569
25682
|
const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
|
|
25570
|
-
if (typed !== join19(home, PROJECTS_FOLDER) ||
|
|
25683
|
+
if (typed !== join19(home, PROJECTS_FOLDER) || existsSync22(typed) || !existsSync22(home))
|
|
25571
25684
|
return null;
|
|
25572
25685
|
return join19(realpathSync2(home), PROJECTS_FOLDER);
|
|
25573
25686
|
}
|
|
@@ -25577,10 +25690,10 @@ function planInPlace(opts) {
|
|
|
25577
25690
|
throw new UserError(`${opts.name} is not a folder name`);
|
|
25578
25691
|
}
|
|
25579
25692
|
const typed = join19(resolveParent(opts.parent), name);
|
|
25580
|
-
if (!
|
|
25693
|
+
if (!existsSync22(typed) || !statSync8(typed).isDirectory())
|
|
25581
25694
|
throw new UserError(`${typed} is not a folder`);
|
|
25582
25695
|
const dir = realpathSync2(typed);
|
|
25583
|
-
if (
|
|
25696
|
+
if (existsSync22(join19(dir, ".git")))
|
|
25584
25697
|
throw new UserError(`${dir} is already a project; open it instead`);
|
|
25585
25698
|
return { parent: dirname7(dir), dir, makeParent: false };
|
|
25586
25699
|
}
|
|
@@ -25588,7 +25701,7 @@ function resolveParent(raw) {
|
|
|
25588
25701
|
const typed = expandTilde(raw.trim());
|
|
25589
25702
|
if (!typed.startsWith("/"))
|
|
25590
25703
|
throw new UserError(`${raw} is not a folder path`);
|
|
25591
|
-
if (!
|
|
25704
|
+
if (!existsSync22(typed))
|
|
25592
25705
|
throw new UserError(`${raw} does not exist; make it first`);
|
|
25593
25706
|
if (!statSync8(typed).isDirectory())
|
|
25594
25707
|
throw new UserError(`${raw} is not a folder`);
|
|
@@ -25705,8 +25818,8 @@ async function ok(p, what) {
|
|
|
25705
25818
|
}
|
|
25706
25819
|
|
|
25707
25820
|
// ../daemon/src/repos/watcher.ts
|
|
25708
|
-
import { existsSync as
|
|
25709
|
-
import { basename as
|
|
25821
|
+
import { existsSync as existsSync23, watch } from "fs";
|
|
25822
|
+
import { basename as basename7, join as join20 } from "path";
|
|
25710
25823
|
function watchDefaultBranch(repoPath, branch, onMove) {
|
|
25711
25824
|
const last = { local: null, upstream: null };
|
|
25712
25825
|
const read = async () => {
|
|
@@ -25770,11 +25883,11 @@ function watchDefaultBranch(repoPath, branch, onMove) {
|
|
|
25770
25883
|
let mid = null;
|
|
25771
25884
|
const armInner = () => {
|
|
25772
25885
|
inner?.close();
|
|
25773
|
-
inner =
|
|
25886
|
+
inner = existsSync23(mine) ? tryWatch(mine) : null;
|
|
25774
25887
|
};
|
|
25775
25888
|
const armMid = () => {
|
|
25776
25889
|
mid?.close();
|
|
25777
|
-
mid =
|
|
25890
|
+
mid = existsSync23(remotes) ? tryWatch(remotes, (_event, filename) => {
|
|
25778
25891
|
if (!filename || filename === remote.out)
|
|
25779
25892
|
armInner();
|
|
25780
25893
|
schedule();
|
|
@@ -25852,7 +25965,7 @@ function watchWorktreeDir(repoPath, onChange) {
|
|
|
25852
25965
|
}
|
|
25853
25966
|
function watchConfigFile(repoPath, onChange) {
|
|
25854
25967
|
const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
|
|
25855
|
-
const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) =>
|
|
25968
|
+
const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename7(rel)));
|
|
25856
25969
|
const dir = join20(repoPath, CONFIG_DIR);
|
|
25857
25970
|
let timer = null;
|
|
25858
25971
|
let outer = null;
|
|
@@ -25868,7 +25981,7 @@ function watchConfigFile(repoPath, onChange) {
|
|
|
25868
25981
|
const armInner = () => {
|
|
25869
25982
|
inner?.close();
|
|
25870
25983
|
inner = null;
|
|
25871
|
-
if (!
|
|
25984
|
+
if (!existsSync23(dir))
|
|
25872
25985
|
return;
|
|
25873
25986
|
try {
|
|
25874
25987
|
inner = watch(dir, (_event, filename) => {
|
|
@@ -25922,7 +26035,7 @@ class RepoRegistry {
|
|
|
25922
26035
|
}
|
|
25923
26036
|
async boot() {
|
|
25924
26037
|
const { state } = this.d;
|
|
25925
|
-
const gone = state.worktrees.filter((wt) => !
|
|
26038
|
+
const gone = state.worktrees.filter((wt) => !existsSync24(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
|
|
25926
26039
|
for (const wt of gone)
|
|
25927
26040
|
await this.d.worktrees.forgetGone(wt);
|
|
25928
26041
|
for (const repo of state.repos) {
|
|
@@ -25990,7 +26103,7 @@ class RepoRegistry {
|
|
|
25990
26103
|
return `${repo.name} is set up now`;
|
|
25991
26104
|
if (main.promptedAt || main.lastTurn)
|
|
25992
26105
|
return `${repo.name} has a chat now`;
|
|
25993
|
-
if (
|
|
26106
|
+
if (existsSync24(join21(repo.path, ".claude")))
|
|
25994
26107
|
return `an agent has already run in ${repo.name}`;
|
|
25995
26108
|
if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
|
|
25996
26109
|
return `${repo.name} has files in it now`;
|
|
@@ -26071,7 +26184,7 @@ class RepoRegistry {
|
|
|
26071
26184
|
}
|
|
26072
26185
|
async register(rawPath, made) {
|
|
26073
26186
|
const path = expandTilde(rawPath);
|
|
26074
|
-
if (!
|
|
26187
|
+
if (!existsSync24(path))
|
|
26075
26188
|
throw new UserError(`${path} does not exist`);
|
|
26076
26189
|
if (!await isGitRepo(path))
|
|
26077
26190
|
throw new UserError(`${path} is not a git repository`);
|
|
@@ -26085,7 +26198,7 @@ class RepoRegistry {
|
|
|
26085
26198
|
const repo = {
|
|
26086
26199
|
id: shortId(),
|
|
26087
26200
|
path: root,
|
|
26088
|
-
name:
|
|
26201
|
+
name: basename8(root),
|
|
26089
26202
|
defaultBranch: await defaultBranch(root),
|
|
26090
26203
|
config: detected.config,
|
|
26091
26204
|
configFile: configTarget(root, !!made),
|
|
@@ -26145,7 +26258,7 @@ class RepoRegistry {
|
|
|
26145
26258
|
throw new UserError(current.reason);
|
|
26146
26259
|
const was = configTarget(repo.path, !!repo.made);
|
|
26147
26260
|
const rel = configSibling(was, kind);
|
|
26148
|
-
const moved = rel !== was &&
|
|
26261
|
+
const moved = rel !== was && existsSync24(join21(repo.path, was));
|
|
26149
26262
|
if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
|
|
26150
26263
|
try {
|
|
26151
26264
|
rmSync4(join21(repo.path, was));
|
|
@@ -26261,7 +26374,7 @@ class RepoRegistry {
|
|
|
26261
26374
|
}
|
|
26262
26375
|
placeConfig(repo) {
|
|
26263
26376
|
const rel = configTarget(repo.path, !!repo.made);
|
|
26264
|
-
if (isLocalConfigFile(rel) &&
|
|
26377
|
+
if (isLocalConfigFile(rel) && existsSync24(join21(repo.path, rel))) {
|
|
26265
26378
|
fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
|
|
26266
26379
|
}
|
|
26267
26380
|
if (repo.configFile === rel)
|
|
@@ -27840,7 +27953,7 @@ function isAlive(pid) {
|
|
|
27840
27953
|
}
|
|
27841
27954
|
|
|
27842
27955
|
// ../daemon/src/runtime/bridge-script.ts
|
|
27843
|
-
import { existsSync as
|
|
27956
|
+
import { existsSync as existsSync25, readFileSync as readFileSync15, statSync as statSync9 } from "fs";
|
|
27844
27957
|
var LEARNED_MAX = 8;
|
|
27845
27958
|
|
|
27846
27959
|
class BridgeScript {
|
|
@@ -27870,7 +27983,7 @@ class BridgeScript {
|
|
|
27870
27983
|
this.cache = null;
|
|
27871
27984
|
}
|
|
27872
27985
|
get() {
|
|
27873
|
-
if (!
|
|
27986
|
+
if (!existsSync25(this.path))
|
|
27874
27987
|
return "// toyon bridge not built";
|
|
27875
27988
|
const s = statSync9(this.path);
|
|
27876
27989
|
const stamp = `${s.mtimeMs}:${s.size}`;
|
|
@@ -27903,7 +28016,7 @@ import { homedir as homedir12, hostname as hostname2 } from "os";
|
|
|
27903
28016
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
27904
28017
|
|
|
27905
28018
|
// ../daemon/src/agent/config.ts
|
|
27906
|
-
import { existsSync as
|
|
28019
|
+
import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
|
|
27907
28020
|
import { homedir as homedir11 } from "os";
|
|
27908
28021
|
import { join as join23 } from "path";
|
|
27909
28022
|
function agentConfigFiles(spec, repoPath, home = homedir11()) {
|
|
@@ -27911,7 +28024,7 @@ function agentConfigFiles(spec, repoPath, home = homedir11()) {
|
|
|
27911
28024
|
id,
|
|
27912
28025
|
label,
|
|
27913
28026
|
path,
|
|
27914
|
-
exists:
|
|
28027
|
+
exists: existsSync26(path)
|
|
27915
28028
|
});
|
|
27916
28029
|
if (spec.id === "claude") {
|
|
27917
28030
|
return [
|
|
@@ -27943,7 +28056,7 @@ function describeAgentConfig(spec, repoPath, home = homedir11()) {
|
|
|
27943
28056
|
return { agent: spec.id, files, servers };
|
|
27944
28057
|
}
|
|
27945
28058
|
function readJson(path) {
|
|
27946
|
-
if (!
|
|
28059
|
+
if (!existsSync26(path))
|
|
27947
28060
|
return null;
|
|
27948
28061
|
try {
|
|
27949
28062
|
const v = JSON.parse(readFileSync16(path, "utf8"));
|
|
@@ -27964,7 +28077,7 @@ function serversOf(raw, scope) {
|
|
|
27964
28077
|
});
|
|
27965
28078
|
}
|
|
27966
28079
|
function codexServers(path) {
|
|
27967
|
-
if (!
|
|
28080
|
+
if (!existsSync26(path))
|
|
27968
28081
|
return [];
|
|
27969
28082
|
const out = [];
|
|
27970
28083
|
let current = null;
|
|
@@ -28597,12 +28710,6 @@ var handlers = {
|
|
|
28597
28710
|
"page-loaded"(msg, _ctx, s) {
|
|
28598
28711
|
s.pageErrors.loaded(msg.worktreeId);
|
|
28599
28712
|
},
|
|
28600
|
-
"fix-page"(msg, _ctx, s) {
|
|
28601
|
-
s.fix.pressPage(msg.worktreeId);
|
|
28602
|
-
},
|
|
28603
|
-
rendered(msg, _ctx, s) {
|
|
28604
|
-
s.render.rendered(msg);
|
|
28605
|
-
},
|
|
28606
28713
|
async chat(msg, ctx, s) {
|
|
28607
28714
|
await sending(s, ctx, msg, msg.attachments, () => s.worktrees.send(msg.worktreeId, {
|
|
28608
28715
|
text: msg.text,
|
|
@@ -28859,6 +28966,15 @@ var handlers = {
|
|
|
28859
28966
|
reveal(msg, _ctx, s) {
|
|
28860
28967
|
s.files.reveal(msg.worktreeId, msg.path);
|
|
28861
28968
|
},
|
|
28969
|
+
async "reveal-path"(msg, _ctx, s) {
|
|
28970
|
+
s.files.revealPath(await s.opens.where(msg.path));
|
|
28971
|
+
},
|
|
28972
|
+
async "reveal-attachment"(msg, _ctx, s) {
|
|
28973
|
+
const path = await s.attachments.locate(msg.worktreeId, msg.file, (file) => s.worktrees.archivedAttachment(msg.worktreeId, file));
|
|
28974
|
+
if (!path)
|
|
28975
|
+
throw new UserError("that attachment is no longer on this machine");
|
|
28976
|
+
s.files.revealPath(path);
|
|
28977
|
+
},
|
|
28862
28978
|
async "discard-files"(msg, _ctx, s) {
|
|
28863
28979
|
try {
|
|
28864
28980
|
for (const path of msg.paths)
|
|
@@ -29015,9 +29131,12 @@ var handlers = {
|
|
|
29015
29131
|
s.files.revealPath(file.path);
|
|
29016
29132
|
},
|
|
29017
29133
|
async "agent-logout"(msg, _ctx, s) {
|
|
29018
|
-
await s.
|
|
29134
|
+
await s.readiness.logout(msg.agent);
|
|
29019
29135
|
s.hub.emit("agentsChanged");
|
|
29020
29136
|
},
|
|
29137
|
+
"agent-check"(msg, _ctx, s) {
|
|
29138
|
+
fireAndForget("agents", s.readiness.check(msg.agent), "agent check");
|
|
29139
|
+
},
|
|
29021
29140
|
"set-default-agent"(msg, _ctx, s) {
|
|
29022
29141
|
s.agents.require(msg.agent);
|
|
29023
29142
|
s.state.setDefaultAgent(msg.agent);
|
|
@@ -29140,7 +29259,7 @@ async function dispatch(msg, ctx, s) {
|
|
|
29140
29259
|
}
|
|
29141
29260
|
|
|
29142
29261
|
// ../daemon/src/server/http.ts
|
|
29143
|
-
import { existsSync as
|
|
29262
|
+
import { existsSync as existsSync27 } from "fs";
|
|
29144
29263
|
import { hostname } from "os";
|
|
29145
29264
|
import { join as join24 } from "path";
|
|
29146
29265
|
|
|
@@ -29365,6 +29484,20 @@ function createFetch(opts) {
|
|
|
29365
29484
|
const refused = await opts.restart(url.searchParams.has(RESTART_NOW));
|
|
29366
29485
|
return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
|
|
29367
29486
|
}
|
|
29487
|
+
if (url.pathname === "/changelog" && req.method === "GET") {
|
|
29488
|
+
if (!authed(req))
|
|
29489
|
+
return unauthorized();
|
|
29490
|
+
const text = await opts.changelog();
|
|
29491
|
+
if (text === null)
|
|
29492
|
+
return new Response("this copy of Toyon carries no release notes", { status: 404 });
|
|
29493
|
+
return new Response(text, {
|
|
29494
|
+
headers: {
|
|
29495
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
29496
|
+
"cache-control": NO_STORE2,
|
|
29497
|
+
"x-content-type-options": "nosniff"
|
|
29498
|
+
}
|
|
29499
|
+
});
|
|
29500
|
+
}
|
|
29368
29501
|
const paired = await opts.pairing(req, url, d);
|
|
29369
29502
|
if (paired)
|
|
29370
29503
|
return paired;
|
|
@@ -29431,7 +29564,7 @@ function createFetch(opts) {
|
|
|
29431
29564
|
return new Response("unauthorized", { status: 401 });
|
|
29432
29565
|
const id = url.pathname.slice("/uploads/".length);
|
|
29433
29566
|
const path = opts.attachments.uploads.path(id);
|
|
29434
|
-
if (!path || !
|
|
29567
|
+
if (!path || !existsSync27(path))
|
|
29435
29568
|
return new Response("not found", { status: 404 });
|
|
29436
29569
|
return new Response(Bun.file(path), {
|
|
29437
29570
|
headers: { "cache-control": IMMUTABLE, ...attachedHeaders(opts.attachments.uploads.imageType(id)) }
|
|
@@ -29444,9 +29577,14 @@ function createFetch(opts) {
|
|
|
29444
29577
|
const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
|
|
29445
29578
|
if (worktreeId && file)
|
|
29446
29579
|
await opts.attachments.whenWritten(worktreeId, file);
|
|
29447
|
-
const path = places.find((p) => !!p &&
|
|
29580
|
+
const path = places.find((p) => !!p && existsSync27(p));
|
|
29448
29581
|
if (!path)
|
|
29449
29582
|
return new Response("not found", { status: 404 });
|
|
29583
|
+
if (url.searchParams.has("where")) {
|
|
29584
|
+
return new Response(path, {
|
|
29585
|
+
headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }
|
|
29586
|
+
});
|
|
29587
|
+
}
|
|
29450
29588
|
return new Response(Bun.file(path), {
|
|
29451
29589
|
headers: { "cache-control": IMMUTABLE, ...attachedHeaders(drawnType(file ?? "")) }
|
|
29452
29590
|
});
|
|
@@ -29479,7 +29617,7 @@ function createFetch(opts) {
|
|
|
29479
29617
|
const rel = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
29480
29618
|
const file = join24(opts.shellDist, rel.replaceAll("..", ""));
|
|
29481
29619
|
const hashed = rel.startsWith("/assets/");
|
|
29482
|
-
if (
|
|
29620
|
+
if (existsSync27(file) && Bun.file(file).size > 0) {
|
|
29483
29621
|
if (rel === "/manifest.json") {
|
|
29484
29622
|
const { bar, ground } = opts.manifestColors();
|
|
29485
29623
|
const manifest = { ...await Bun.file(file).json(), theme_color: bar, background_color: ground };
|
|
@@ -29499,7 +29637,7 @@ function createFetch(opts) {
|
|
|
29499
29637
|
if (hashed)
|
|
29500
29638
|
return new Response("not found", { status: 404 });
|
|
29501
29639
|
const index = join24(opts.shellDist, "index.html");
|
|
29502
|
-
if (
|
|
29640
|
+
if (existsSync27(index))
|
|
29503
29641
|
return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE2 } });
|
|
29504
29642
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
29505
29643
|
}
|
|
@@ -29574,14 +29712,6 @@ function startServer(opts) {
|
|
|
29574
29712
|
}
|
|
29575
29713
|
};
|
|
29576
29714
|
const send = (ws, msg) => raw(ws, JSON.stringify(msg));
|
|
29577
|
-
s.render.courier = (worktreeId, msg) => {
|
|
29578
|
-
const shells = [...sockets].filter((ws) => ws.data.authed && !ws.data.preview);
|
|
29579
|
-
const ws = shells.find((w) => w.data.view === worktreeId) ?? shells.find((w) => w.data.subs.has(worktreeId));
|
|
29580
|
-
if (!ws)
|
|
29581
|
-
return false;
|
|
29582
|
-
send(ws, msg);
|
|
29583
|
-
return true;
|
|
29584
|
-
};
|
|
29585
29715
|
const broadcast = (msg) => {
|
|
29586
29716
|
const json = JSON.stringify(msg);
|
|
29587
29717
|
for (const ws of sockets)
|
|
@@ -29759,20 +29889,9 @@ function startServer(opts) {
|
|
|
29759
29889
|
};
|
|
29760
29890
|
s.hub.on("themesChanged", themesChanged);
|
|
29761
29891
|
themesChanged();
|
|
29762
|
-
const agentInfos = () => s.accounts.describe(s.agents.infos()).map((a) => {
|
|
29763
|
-
const models = s.state.cachedOptions(a.id, "model");
|
|
29764
|
-
const efforts = s.state.cachedOptions(a.id, "thought_level");
|
|
29765
|
-
const spec = s.agents.get(a.id);
|
|
29766
|
-
return {
|
|
29767
|
-
...a,
|
|
29768
|
-
...models.length > 0 ? { models } : {},
|
|
29769
|
-
...efforts.length > 0 ? { efforts } : {},
|
|
29770
|
-
...spec && !lacksQuickModel(spec, s.state) ? { quick: true } : {}
|
|
29771
|
-
};
|
|
29772
|
-
});
|
|
29773
29892
|
const agentsMsg = () => ({
|
|
29774
29893
|
t: "agents",
|
|
29775
|
-
agents:
|
|
29894
|
+
agents: s.readiness.describe(),
|
|
29776
29895
|
defaultAgent: s.agents.defaultId(s.state.defaultAgent),
|
|
29777
29896
|
agentChosen: s.state.defaultAgent !== undefined
|
|
29778
29897
|
});
|
|
@@ -29810,7 +29929,7 @@ function startServer(opts) {
|
|
|
29810
29929
|
trunks,
|
|
29811
29930
|
themes: s.themes.themes,
|
|
29812
29931
|
themePrefs: s.themes.prefs,
|
|
29813
|
-
agents:
|
|
29932
|
+
agents: s.readiness.describe(),
|
|
29814
29933
|
defaultAgent: s.agents.defaultId(s.state.defaultAgent),
|
|
29815
29934
|
agentChosen: s.state.defaultAgent !== undefined,
|
|
29816
29935
|
home: homedir12(),
|
|
@@ -29880,7 +29999,8 @@ function startServer(opts) {
|
|
|
29880
29999
|
restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
|
|
29881
30000
|
grants: opts.grants,
|
|
29882
30001
|
trusted: () => s.state.trustedOrigins,
|
|
29883
|
-
mcp: (req, id) => s.mcp.fetch(req, id)
|
|
30002
|
+
mcp: (req, id) => s.mcp.fetch(req, id),
|
|
30003
|
+
changelog: () => Bun.file(opts.changelog).text().catch(() => null)
|
|
29884
30004
|
}),
|
|
29885
30005
|
websocket: {
|
|
29886
30006
|
async open(ws) {
|
|
@@ -29897,6 +30017,7 @@ function startServer(opts) {
|
|
|
29897
30017
|
sockets.add(ws);
|
|
29898
30018
|
link(ws);
|
|
29899
30019
|
send(ws, await helloFrame());
|
|
30020
|
+
fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
|
|
29900
30021
|
for (const o of s.opens.takePending())
|
|
29901
30022
|
send(ws, openedFrame(o));
|
|
29902
30023
|
},
|
|
@@ -29991,9 +30112,9 @@ function startServer(opts) {
|
|
|
29991
30112
|
}
|
|
29992
30113
|
|
|
29993
30114
|
// ../daemon/src/themes/store.ts
|
|
29994
|
-
import { existsSync as
|
|
30115
|
+
import { existsSync as existsSync28, readdirSync as readdirSync8, readFileSync as readFileSync17, statSync as statSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
29995
30116
|
import { homedir as homedir13 } from "os";
|
|
29996
|
-
import { basename as
|
|
30117
|
+
import { basename as basename9, dirname as dirname9, join as join25, resolve as resolve9 } from "path";
|
|
29997
30118
|
var home = homedir13();
|
|
29998
30119
|
var defaultExtensionDirs = [
|
|
29999
30120
|
join25(home, ".vscode", "extensions"),
|
|
@@ -30078,7 +30199,7 @@ class ThemeStore {
|
|
|
30078
30199
|
if (json && typeof json === "object" && typeof json.include === "string") {
|
|
30079
30200
|
throw new ThemeImportError("this theme file uses `include`; drop the whole extension folder's theme into ~/.toyon/themes or install it in VS Code and rescan");
|
|
30080
30201
|
}
|
|
30081
|
-
const base = slug(
|
|
30202
|
+
const base = slug(basename9(name).replace(/\.(json|jsonc)$/i, ""));
|
|
30082
30203
|
const theme = vscodeToTheme(json, {
|
|
30083
30204
|
id: `file:${base}`,
|
|
30084
30205
|
name: json?.name ?? base,
|
|
@@ -30113,7 +30234,7 @@ function readVscodeTheme(file, depth = 0) {
|
|
|
30113
30234
|
return t;
|
|
30114
30235
|
}
|
|
30115
30236
|
function loadDir(dir) {
|
|
30116
|
-
if (!
|
|
30237
|
+
if (!existsSync28(dir))
|
|
30117
30238
|
return [];
|
|
30118
30239
|
const out = [];
|
|
30119
30240
|
for (const f of readdirSync8(dir)) {
|
|
@@ -30135,7 +30256,7 @@ function loadDir(dir) {
|
|
|
30135
30256
|
return out;
|
|
30136
30257
|
}
|
|
30137
30258
|
function discover(dir) {
|
|
30138
|
-
if (!
|
|
30259
|
+
if (!existsSync28(dir))
|
|
30139
30260
|
return [];
|
|
30140
30261
|
const out = [];
|
|
30141
30262
|
let entries;
|
|
@@ -30148,7 +30269,7 @@ function discover(dir) {
|
|
|
30148
30269
|
const extDir = join25(dir, name);
|
|
30149
30270
|
const pkgFile = join25(extDir, "package.json");
|
|
30150
30271
|
try {
|
|
30151
|
-
if (!statSync10(extDir).isDirectory() || !
|
|
30272
|
+
if (!statSync10(extDir).isDirectory() || !existsSync28(pkgFile))
|
|
30152
30273
|
continue;
|
|
30153
30274
|
const pkg = JSON.parse(readFileSync17(pkgFile, "utf8"));
|
|
30154
30275
|
const themes = pkg.contributes?.themes;
|
|
@@ -30157,7 +30278,7 @@ function discover(dir) {
|
|
|
30157
30278
|
const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
|
|
30158
30279
|
let nls = {};
|
|
30159
30280
|
const nlsFile = join25(extDir, "package.nls.json");
|
|
30160
|
-
if (
|
|
30281
|
+
if (existsSync28(nlsFile)) {
|
|
30161
30282
|
try {
|
|
30162
30283
|
nls = JSON.parse(readFileSync17(nlsFile, "utf8"));
|
|
30163
30284
|
} catch {}
|
|
@@ -30228,7 +30349,7 @@ var REFRESH_EVERY_MS = 60000;
|
|
|
30228
30349
|
var CHECK_EVERY_MS = 6 * 60 * 60000;
|
|
30229
30350
|
var FIRST_CHECK_MS = 2 * 60000;
|
|
30230
30351
|
var SETTLED_MS = 2 * 60000;
|
|
30231
|
-
var
|
|
30352
|
+
var RETRY_AFTER_MS2 = 24 * 60 * 60000;
|
|
30232
30353
|
var INSTALL_SETTLE_MS = 60000;
|
|
30233
30354
|
|
|
30234
30355
|
class UpdateService {
|
|
@@ -30380,7 +30501,7 @@ class UpdateService {
|
|
|
30380
30501
|
if (this.needsInstall() && this.d.command(target) === null)
|
|
30381
30502
|
return;
|
|
30382
30503
|
const failed = this.d.state.updateFailed;
|
|
30383
|
-
if (failed?.version === target && this.now() - failed.at <
|
|
30504
|
+
if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS2)
|
|
30384
30505
|
return;
|
|
30385
30506
|
if (this.d.busy()) {
|
|
30386
30507
|
this.busyAt = this.now();
|
|
@@ -30571,7 +30692,7 @@ class BackendShare {
|
|
|
30571
30692
|
|
|
30572
30693
|
// ../daemon/src/worktrees/cache.ts
|
|
30573
30694
|
import { createHash as createHash4 } from "crypto";
|
|
30574
|
-
import { existsSync as
|
|
30695
|
+
import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
|
|
30575
30696
|
import { mkdir as mkdir5, readdir as readdir2, rename, rm as rm4, stat as stat4, utimes, writeFile as writeFile3 } from "fs/promises";
|
|
30576
30697
|
import { join as join26 } from "path";
|
|
30577
30698
|
|
|
@@ -30604,7 +30725,7 @@ var STALE_MS = 30 * 24 * 3600000;
|
|
|
30604
30725
|
var TMP = ".tmp-";
|
|
30605
30726
|
var MANIFEST = "manifest.json";
|
|
30606
30727
|
function defaultTools(dir) {
|
|
30607
|
-
const has = (...files) => files.some((f) =>
|
|
30728
|
+
const has = (...files) => files.some((f) => existsSync29(join26(dir, f)));
|
|
30608
30729
|
const out = [];
|
|
30609
30730
|
if (has("bun.lock", "bun.lockb"))
|
|
30610
30731
|
out.push("bun --version");
|
|
@@ -30617,7 +30738,7 @@ function defaultTools(dir) {
|
|
|
30617
30738
|
return out;
|
|
30618
30739
|
}
|
|
30619
30740
|
function withSidecars(root, rel) {
|
|
30620
|
-
return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) =>
|
|
30741
|
+
return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync29(join26(root, n)));
|
|
30621
30742
|
}
|
|
30622
30743
|
var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
|
|
30623
30744
|
async function listEntries(familyDir) {
|
|
@@ -30633,7 +30754,7 @@ async function listEntries(familyDir) {
|
|
|
30633
30754
|
if (!m?.[1] || !m[2])
|
|
30634
30755
|
continue;
|
|
30635
30756
|
const dir = join26(familyDir, name);
|
|
30636
|
-
if (!
|
|
30757
|
+
if (!existsSync29(join26(dir, MANIFEST)))
|
|
30637
30758
|
continue;
|
|
30638
30759
|
out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
|
|
30639
30760
|
}
|
|
@@ -30671,7 +30792,7 @@ class ArtifactCache {
|
|
|
30671
30792
|
}
|
|
30672
30793
|
const restored = [];
|
|
30673
30794
|
for (const rel of policy.paths) {
|
|
30674
|
-
if (
|
|
30795
|
+
if (existsSync29(join26(wt.path, rel)))
|
|
30675
30796
|
continue;
|
|
30676
30797
|
const names = withSidecars(entry.dir, rel);
|
|
30677
30798
|
if (names.length === 0)
|
|
@@ -30712,7 +30833,7 @@ class ArtifactCache {
|
|
|
30712
30833
|
let tmp;
|
|
30713
30834
|
let slot;
|
|
30714
30835
|
try {
|
|
30715
|
-
const present = policy.paths.filter((rel) =>
|
|
30836
|
+
const present = policy.paths.filter((rel) => existsSync29(join26(wt.path, rel)));
|
|
30716
30837
|
if (present.length === 0)
|
|
30717
30838
|
return;
|
|
30718
30839
|
const family = await this.family(wt, repo, policy);
|
|
@@ -30996,8 +31117,7 @@ var RESPONSE = {
|
|
|
30996
31117
|
conflict: { does: "ask" },
|
|
30997
31118
|
check: { does: "ask", once: true },
|
|
30998
31119
|
command: { does: "offer" },
|
|
30999
|
-
preview: { does: "offer" }
|
|
31000
|
-
page: { does: "offer" }
|
|
31120
|
+
preview: { does: "offer" }
|
|
31001
31121
|
};
|
|
31002
31122
|
function lastAsked(entries) {
|
|
31003
31123
|
for (let i = entries.length - 1;i >= 0; i--) {
|
|
@@ -31015,7 +31135,7 @@ function askedFor(entries, toolId) {
|
|
|
31015
31135
|
function lastStop(entries) {
|
|
31016
31136
|
for (let i = entries.length - 1;i >= 0; i--) {
|
|
31017
31137
|
const e = entries[i].event;
|
|
31018
|
-
if (e.type === "usage" || e.type === "session-info"
|
|
31138
|
+
if (e.type === "usage" || e.type === "session-info")
|
|
31019
31139
|
continue;
|
|
31020
31140
|
if (e.type === "turn-end" && e.stopReason === "interrupted")
|
|
31021
31141
|
return { kind: "stopped" };
|
|
@@ -31025,21 +31145,6 @@ function lastStop(entries) {
|
|
|
31025
31145
|
}
|
|
31026
31146
|
return null;
|
|
31027
31147
|
}
|
|
31028
|
-
function lastPageErrors(entries) {
|
|
31029
|
-
const errors = [];
|
|
31030
|
-
for (let i = entries.length - 1;i >= 0; i--) {
|
|
31031
|
-
const e = entries[i].event;
|
|
31032
|
-
if (e.type === "page-error") {
|
|
31033
|
-
errors.unshift(...e.errors);
|
|
31034
|
-
continue;
|
|
31035
|
-
}
|
|
31036
|
-
if (e.type === "user-message" || e.type === "fix-asked")
|
|
31037
|
-
break;
|
|
31038
|
-
if (e.type === "tool-start" && isEditTool({ name: e.name, kind: e.kind }))
|
|
31039
|
-
break;
|
|
31040
|
-
}
|
|
31041
|
-
return errors;
|
|
31042
|
-
}
|
|
31043
31148
|
function failureOf(entries, toolId) {
|
|
31044
31149
|
let command;
|
|
31045
31150
|
for (const { event } of entries) {
|
|
@@ -31104,17 +31209,6 @@ class FixService {
|
|
|
31104
31209
|
if (!this.report(worktreeId, failure, { pressed: true }))
|
|
31105
31210
|
throw new UserError("nothing runs on main");
|
|
31106
31211
|
}
|
|
31107
|
-
pressPage(worktreeId) {
|
|
31108
|
-
this.d.state.requireWorktree(worktreeId);
|
|
31109
|
-
const agent = this.d.runtime.agentFor(worktreeId);
|
|
31110
|
-
if (!agent)
|
|
31111
|
-
throw new UserError("worktree still starting; try again in a moment");
|
|
31112
|
-
const errors = lastPageErrors(agent.transcript());
|
|
31113
|
-
if (errors.length === 0)
|
|
31114
|
-
throw new UserError("nothing the preview threw to fix");
|
|
31115
|
-
if (!this.report(worktreeId, { kind: "page", errors }, { pressed: true }))
|
|
31116
|
-
throw new UserError("nothing runs on main");
|
|
31117
|
-
}
|
|
31118
31212
|
goOn(worktreeId) {
|
|
31119
31213
|
const wt = this.d.state.requireWorktree(worktreeId);
|
|
31120
31214
|
if (isMain(wt))
|
|
@@ -31134,12 +31228,12 @@ class FixService {
|
|
|
31134
31228
|
}
|
|
31135
31229
|
|
|
31136
31230
|
// ../daemon/src/worktrees/handoff.ts
|
|
31137
|
-
import { randomUUID as
|
|
31231
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
31138
31232
|
var MESSAGE_MAX = 20000;
|
|
31139
31233
|
var TITLE_MAX2 = 200;
|
|
31140
31234
|
var PROJECT_MAX = 200;
|
|
31141
31235
|
var PER_TURN_MAX = 3;
|
|
31142
|
-
var
|
|
31236
|
+
var refused = (text) => ({ text, isError: true });
|
|
31143
31237
|
function openProposals(entries) {
|
|
31144
31238
|
const open = new Map;
|
|
31145
31239
|
for (const { event } of entries) {
|
|
@@ -31222,7 +31316,7 @@ class HandoffService {
|
|
|
31222
31316
|
const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
|
|
31223
31317
|
const shaped = a && typeof a.project === "string" && typeof a.message === "string" && (a.title === undefined || typeof a.title === "string");
|
|
31224
31318
|
if (!shaped)
|
|
31225
|
-
return
|
|
31319
|
+
return refused("handoff takes { project: string, message: string, title?: string }.");
|
|
31226
31320
|
return this.propose(worktreeId, { project: a.project, message: a.message, ...a.title ? { title: a.title } : {} }, "agent");
|
|
31227
31321
|
}
|
|
31228
31322
|
propose(worktreeId, p, by) {
|
|
@@ -31230,31 +31324,31 @@ class HandoffService {
|
|
|
31230
31324
|
if (!wt)
|
|
31231
31325
|
throw new UserError("that chat is gone");
|
|
31232
31326
|
if (p.project.length > PROJECT_MAX)
|
|
31233
|
-
return
|
|
31327
|
+
return refused(`The project name is over ${PROJECT_MAX} characters.`);
|
|
31234
31328
|
if ((p.title ?? "").length > TITLE_MAX2)
|
|
31235
|
-
return
|
|
31329
|
+
return refused(`The title is over ${TITLE_MAX2} characters; a few words.`);
|
|
31236
31330
|
if (p.message.length > MESSAGE_MAX) {
|
|
31237
|
-
return
|
|
31331
|
+
return refused(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
|
|
31238
31332
|
}
|
|
31239
31333
|
const target = this.resolve(worktreeId, p.project);
|
|
31240
31334
|
if ("refused" in target)
|
|
31241
|
-
return
|
|
31335
|
+
return refused(target.refused);
|
|
31242
31336
|
const message = p.message.trim();
|
|
31243
31337
|
if (!message)
|
|
31244
|
-
return
|
|
31338
|
+
return refused("Write the message the other project's agent should start from.");
|
|
31245
31339
|
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
31246
31340
|
const entries = agent.transcript();
|
|
31247
31341
|
if (openProposals(entries).length > 0) {
|
|
31248
|
-
return
|
|
31342
|
+
return refused("A handoff is already waiting for the person on this chat; finish your turn.");
|
|
31249
31343
|
}
|
|
31250
31344
|
if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
|
|
31251
|
-
return
|
|
31345
|
+
return refused("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
|
|
31252
31346
|
}
|
|
31253
31347
|
const title = p.title?.trim();
|
|
31254
31348
|
const { repo } = target;
|
|
31255
31349
|
agent.note({
|
|
31256
31350
|
type: "handoff-proposed",
|
|
31257
|
-
id:
|
|
31351
|
+
id: randomUUID2(),
|
|
31258
31352
|
repo: { id: repo.id, name: repo.name, path: repo.path },
|
|
31259
31353
|
message,
|
|
31260
31354
|
...title ? { title } : {},
|
|
@@ -31370,6 +31464,7 @@ function match(repos, q) {
|
|
|
31370
31464
|
// ../daemon/src/worktrees/landing.ts
|
|
31371
31465
|
var TAIL_CHARS = 400;
|
|
31372
31466
|
var DIFF_CHARS = 2000;
|
|
31467
|
+
var CONTEXT_CHARS2 = 800;
|
|
31373
31468
|
var CHECK_SLOTS = 2;
|
|
31374
31469
|
|
|
31375
31470
|
class LandingService {
|
|
@@ -31445,14 +31540,7 @@ class LandingService {
|
|
|
31445
31540
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
31446
31541
|
this.judging.set(worktreeId, turn.at);
|
|
31447
31542
|
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
|
-
});
|
|
31543
|
+
const prompt = landPrompt(await this.landInput(wt, repo));
|
|
31456
31544
|
let verdict = null;
|
|
31457
31545
|
try {
|
|
31458
31546
|
verdict = await this.d.judge(wt, prompt);
|
|
@@ -31525,6 +31613,25 @@ class LandingService {
|
|
|
31525
31613
|
return;
|
|
31526
31614
|
fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
|
|
31527
31615
|
}
|
|
31616
|
+
async landInput(wt, repo) {
|
|
31617
|
+
const base = baseOf(repo);
|
|
31618
|
+
const entries = this.d.transcript(wt.id);
|
|
31619
|
+
const [diff, subjects, committedAt] = await Promise.all([
|
|
31620
|
+
diffSummary(wt.id, wt.path, base),
|
|
31621
|
+
recentSubjects(wt.path, base),
|
|
31622
|
+
lastOwnCommitAt(wt.id, wt.path, base)
|
|
31623
|
+
]);
|
|
31624
|
+
const since = Math.max(committedAt, wt.lands?.at(-1)?.at ?? 0);
|
|
31625
|
+
return {
|
|
31626
|
+
title: wt.title,
|
|
31627
|
+
firstAsk: firstAskOf(entries),
|
|
31628
|
+
turns: turnsSince(entries, since),
|
|
31629
|
+
earlier: since > 0,
|
|
31630
|
+
diffStat: diff.own,
|
|
31631
|
+
committedStat: diff.committed,
|
|
31632
|
+
recentSubjects: subjects
|
|
31633
|
+
};
|
|
31634
|
+
}
|
|
31528
31635
|
async hasWork(wt) {
|
|
31529
31636
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
31530
31637
|
const files = await statusFiles(wt.path);
|
|
@@ -31547,14 +31654,14 @@ class LandingService {
|
|
|
31547
31654
|
};
|
|
31548
31655
|
this.judging.set(worktreeId, opts.at);
|
|
31549
31656
|
const started = Date.now();
|
|
31550
|
-
|
|
31657
|
+
const pending = { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept };
|
|
31658
|
+
this.d.worktrees.setLanding(worktreeId, pending);
|
|
31551
31659
|
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
31660
|
const command = repo.config.check?.trim();
|
|
31557
|
-
|
|
31661
|
+
let checkDone = false;
|
|
31662
|
+
const checked = async () => {
|
|
31663
|
+
if (!command)
|
|
31664
|
+
return { check: "none" };
|
|
31558
31665
|
const ceiling = timeoutFor(repo.config, "check");
|
|
31559
31666
|
this.d.runs.drop(worktreeId, "check");
|
|
31560
31667
|
const run = this.d.runs.begin(worktreeId, "check", {
|
|
@@ -31563,9 +31670,11 @@ class LandingService {
|
|
|
31563
31670
|
});
|
|
31564
31671
|
if (!await run.admitted)
|
|
31565
31672
|
return;
|
|
31566
|
-
if (!live())
|
|
31567
|
-
|
|
31568
|
-
|
|
31673
|
+
if (!live()) {
|
|
31674
|
+
run.finish(null);
|
|
31675
|
+
return;
|
|
31676
|
+
}
|
|
31677
|
+
const waited = Date.now() - started;
|
|
31569
31678
|
let r;
|
|
31570
31679
|
try {
|
|
31571
31680
|
r = await this.d.check(worktreeId, command, { quiet: opts.quiet, timeoutMs: ceiling, onSpawn: run.spawned });
|
|
@@ -31576,8 +31685,8 @@ class LandingService {
|
|
|
31576
31685
|
run.finish(r.exit);
|
|
31577
31686
|
if (!live())
|
|
31578
31687
|
return;
|
|
31579
|
-
|
|
31580
|
-
|
|
31688
|
+
const check = r.exit === 0 ? "pass" : "fail";
|
|
31689
|
+
let checkTail;
|
|
31581
31690
|
if (r.exit === "timeout") {
|
|
31582
31691
|
checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
|
|
31583
31692
|
`);
|
|
@@ -31585,19 +31694,14 @@ class LandingService {
|
|
|
31585
31694
|
checkTail = tail(r.text) || `exit ${r.exit}`;
|
|
31586
31695
|
else
|
|
31587
31696
|
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
|
-
});
|
|
31697
|
+
return { check, ran: r, waited, ...checkTail ? { checkTail } : {} };
|
|
31698
|
+
};
|
|
31699
|
+
const asked = async () => {
|
|
31700
|
+
if (!opts.ask || !this.d.judge)
|
|
31701
|
+
return { verdict: null, unanswered: false };
|
|
31702
|
+
const prompt = landPrompt({ ...await this.landInput(wt, repo), ...opts.note ? { note: opts.note } : {} });
|
|
31703
|
+
let verdict = null;
|
|
31704
|
+
let unanswered = false;
|
|
31601
31705
|
try {
|
|
31602
31706
|
verdict = await this.d.judge(wt, prompt);
|
|
31603
31707
|
} catch (e) {
|
|
@@ -31608,16 +31712,29 @@ class LandingService {
|
|
|
31608
31712
|
}
|
|
31609
31713
|
if (!live())
|
|
31610
31714
|
return;
|
|
31611
|
-
|
|
31715
|
+
if (!checkDone && verdict) {
|
|
31716
|
+
this.recapTurn(worktreeId, verdict);
|
|
31717
|
+
this.d.worktrees.setLanding(worktreeId, { ...pending, ...messageOf(verdict) });
|
|
31718
|
+
}
|
|
31719
|
+
return { verdict, unanswered };
|
|
31720
|
+
};
|
|
31721
|
+
const [checkResult, askResult] = await Promise.all([
|
|
31722
|
+
checked().finally(() => {
|
|
31723
|
+
checkDone = true;
|
|
31724
|
+
}),
|
|
31725
|
+
asked()
|
|
31726
|
+
]);
|
|
31727
|
+
if (!checkResult || !askResult || !live())
|
|
31728
|
+
return;
|
|
31729
|
+
const { check, checkTail, ran, waited = 0 } = checkResult;
|
|
31730
|
+
const { verdict, unanswered } = askResult;
|
|
31612
31731
|
const landing = {
|
|
31613
31732
|
at: opts.at,
|
|
31614
31733
|
check,
|
|
31615
31734
|
...checkTail ? { checkTail } : {},
|
|
31616
31735
|
ready: check !== "fail",
|
|
31617
31736
|
...kept,
|
|
31618
|
-
...verdict
|
|
31619
|
-
...verdict?.subject ? { subject: verdict.subject } : {},
|
|
31620
|
-
...verdict?.body ? { body: verdict.body } : {},
|
|
31737
|
+
...verdict ? messageOf(verdict) : {},
|
|
31621
31738
|
...unanswered ? { unanswered: true } : {},
|
|
31622
31739
|
fingerprint: await treeFingerprint(wt.path)
|
|
31623
31740
|
};
|
|
@@ -31627,14 +31744,18 @@ class LandingService {
|
|
|
31627
31744
|
if (!live())
|
|
31628
31745
|
return;
|
|
31629
31746
|
this.judging.delete(worktreeId);
|
|
31630
|
-
|
|
31631
|
-
|
|
31632
|
-
record.recap = { at: Date.now(), text: verdict.recap };
|
|
31747
|
+
if (verdict)
|
|
31748
|
+
this.recapTurn(worktreeId, verdict);
|
|
31633
31749
|
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
31750
|
this.d.worktrees.setLanding(worktreeId, landing);
|
|
31635
31751
|
if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
|
|
31636
31752
|
this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
|
|
31637
31753
|
}
|
|
31754
|
+
recapTurn(worktreeId, verdict) {
|
|
31755
|
+
const record = this.d.state.worktree(worktreeId)?.lastTurn;
|
|
31756
|
+
if (record && verdict.recap)
|
|
31757
|
+
record.recap = { at: Date.now(), text: verdict.recap };
|
|
31758
|
+
}
|
|
31638
31759
|
async sharedMigration(wt, repo) {
|
|
31639
31760
|
if (!repo.services || keepsCopiesApart(repo.config))
|
|
31640
31761
|
return;
|
|
@@ -31644,31 +31765,44 @@ class LandingService {
|
|
|
31644
31765
|
return "this changes migrations, and the worktrees share the database";
|
|
31645
31766
|
}
|
|
31646
31767
|
}
|
|
31768
|
+
function messageOf(verdict) {
|
|
31769
|
+
return {
|
|
31770
|
+
...!verdict.ready && verdict.why ? { why: verdict.why } : {},
|
|
31771
|
+
...verdict.subject ? { subject: verdict.subject } : {},
|
|
31772
|
+
...verdict.body ? { body: verdict.body } : {}
|
|
31773
|
+
};
|
|
31774
|
+
}
|
|
31647
31775
|
function tail(text) {
|
|
31648
31776
|
const t = text.replace(/\s+$/, "");
|
|
31649
31777
|
return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
|
|
31650
31778
|
}
|
|
31651
|
-
async function diffSummary(path, defaultBr) {
|
|
31779
|
+
async function diffSummary(id, path, defaultBr) {
|
|
31652
31780
|
const [base, dirty, untracked] = await Promise.all([
|
|
31653
31781
|
git(path, "merge-base", "HEAD", defaultBr),
|
|
31654
31782
|
git(path, "diff", "HEAD", "--stat"),
|
|
31655
31783
|
git(path, "ls-files", "--others", "--exclude-standard")
|
|
31656
31784
|
]);
|
|
31785
|
+
if (!base.ok)
|
|
31786
|
+
log.warn(id, `landing question: no merge base with ${defaultBr}: ${base.err.trim()}`);
|
|
31657
31787
|
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(`
|
|
31788
|
+
const own = [dirty.out ? `Uncommitted:
|
|
31789
|
+
${dirty.out}` : "", untracked.out ? `New files:
|
|
31790
|
+
${untracked.out}` : ""].filter(Boolean).join(`
|
|
31667
31791
|
|
|
31668
31792
|
`);
|
|
31669
|
-
return
|
|
31793
|
+
return { own: cap(own, DIFF_CHARS), committed: cap(committed?.out ?? "", CONTEXT_CHARS2) };
|
|
31794
|
+
}
|
|
31795
|
+
function cap(text, max) {
|
|
31796
|
+
return text.length > max ? `${text.slice(0, max)}
|
|
31670
31797
|
...` : text;
|
|
31671
31798
|
}
|
|
31799
|
+
async function lastOwnCommitAt(id, path, defaultBr) {
|
|
31800
|
+
const r = await git(path, "log", "-1", "--no-merges", "--format=%at", `${defaultBr}..HEAD`);
|
|
31801
|
+
if (!r.ok)
|
|
31802
|
+
log.warn(id, `landing question: could not read the branch's commits over ${defaultBr}: ${r.err.trim()}`);
|
|
31803
|
+
const seconds = Number(r.out.trim());
|
|
31804
|
+
return r.ok && Number.isFinite(seconds) ? seconds * 1000 : 0;
|
|
31805
|
+
}
|
|
31672
31806
|
async function recentSubjects(path, defaultBr) {
|
|
31673
31807
|
const r = await git(path, "log", "--format=%s", "-n", "5", defaultBr);
|
|
31674
31808
|
return r.ok ? r.out.split(`
|
|
@@ -31787,7 +31921,7 @@ async function mergedBranches(repoPath, defaultBr) {
|
|
|
31787
31921
|
|
|
31788
31922
|
// ../daemon/src/worktrees/discover.ts
|
|
31789
31923
|
import { createHash as createHash5 } from "crypto";
|
|
31790
|
-
import { basename as
|
|
31924
|
+
import { basename as basename10 } from "path";
|
|
31791
31925
|
|
|
31792
31926
|
// ../daemon/src/git/worktrees.ts
|
|
31793
31927
|
function parseWorktreeList(out) {
|
|
@@ -31876,7 +32010,7 @@ function subtractKnown(repoId, listed, known) {
|
|
|
31876
32010
|
rows.push({
|
|
31877
32011
|
id: discoveredId(g.path),
|
|
31878
32012
|
repoId,
|
|
31879
|
-
name: g.branch ??
|
|
32013
|
+
name: g.branch ?? basename10(g.path),
|
|
31880
32014
|
path: g.path,
|
|
31881
32015
|
...g.branch ? { branch: g.branch } : {},
|
|
31882
32016
|
...g.locked ? { locked: true } : {},
|
|
@@ -32038,7 +32172,7 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
32038
32172
|
}
|
|
32039
32173
|
|
|
32040
32174
|
// ../daemon/src/worktrees/service.ts
|
|
32041
|
-
import { cpSync, existsSync as
|
|
32175
|
+
import { cpSync, existsSync as existsSync34, readFileSync as readFileSync20, rmSync as rmSync8 } from "fs";
|
|
32042
32176
|
import { rm as rm5 } from "fs/promises";
|
|
32043
32177
|
import { dirname as dirname12, join as join30 } from "path";
|
|
32044
32178
|
|
|
@@ -32164,7 +32298,7 @@ async function dropLandRefs(repoPath, worktreeId) {
|
|
|
32164
32298
|
}
|
|
32165
32299
|
|
|
32166
32300
|
// ../daemon/src/git/land.ts
|
|
32167
|
-
import { existsSync as
|
|
32301
|
+
import { existsSync as existsSync30 } from "fs";
|
|
32168
32302
|
var STEP_TIMEOUT_MS = 10 * 60000;
|
|
32169
32303
|
function rowOf(r) {
|
|
32170
32304
|
return r.shown && r.toolId && r.command ? { toolId: r.toolId, command: r.command, text: r.text } : undefined;
|
|
@@ -32174,7 +32308,7 @@ var UNWATCHED = {
|
|
|
32174
32308
|
step() {},
|
|
32175
32309
|
git: (cwd, args) => stepRun(cwd, args)
|
|
32176
32310
|
};
|
|
32177
|
-
function
|
|
32311
|
+
function refused2(what, r, n = 200) {
|
|
32178
32312
|
if (r.exit === "timeout") {
|
|
32179
32313
|
const after = describeDuration(r.ceilingMs ?? STEP_TIMEOUT_MS);
|
|
32180
32314
|
return `${what} gave up after ${after}${r.shown ? "; what it printed is on the chat" : ""}`;
|
|
@@ -32185,7 +32319,7 @@ function stepRefused(what, r, n = 200) {
|
|
|
32185
32319
|
const row = typeof r.exit === "number" && r.hook ? rowOf(r) : undefined;
|
|
32186
32320
|
return {
|
|
32187
32321
|
ok: false,
|
|
32188
|
-
message:
|
|
32322
|
+
message: refused2(what, r, n),
|
|
32189
32323
|
...row && r.hook ? { hook: { ...row, name: r.hook } } : {}
|
|
32190
32324
|
};
|
|
32191
32325
|
}
|
|
@@ -32295,7 +32429,7 @@ async function midway(worktreePath) {
|
|
|
32295
32429
|
if (!r.ok)
|
|
32296
32430
|
return null;
|
|
32297
32431
|
const at = r.out.split(`
|
|
32298
|
-
`).findIndex((file) => file &&
|
|
32432
|
+
`).findIndex((file) => file && existsSync30(file));
|
|
32299
32433
|
return MIDWAY[at]?.[1] ?? null;
|
|
32300
32434
|
}
|
|
32301
32435
|
async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
|
|
@@ -32403,17 +32537,41 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
|
|
|
32403
32537
|
}
|
|
32404
32538
|
return { ok: true, message: `merged ${branch} into ${defaultBr}` };
|
|
32405
32539
|
}
|
|
32406
|
-
async function squashMessage(worktreePath, defaultBr, suggested) {
|
|
32540
|
+
async function squashMessage(worktreePath, defaultBr, suggested, typed = false) {
|
|
32407
32541
|
const n = await git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`);
|
|
32408
32542
|
if (n.out === "1") {
|
|
32543
|
+
if (typed && suggested)
|
|
32544
|
+
return suggested;
|
|
32409
32545
|
const one = await git(worktreePath, "log", "-1", "--format=%B");
|
|
32410
32546
|
if (one.ok && one.out.trim())
|
|
32411
32547
|
return one.out.trim();
|
|
32412
32548
|
}
|
|
32413
|
-
|
|
32414
|
-
|
|
32415
|
-
|
|
32416
|
-
|
|
32549
|
+
const log = await git(worktreePath, "log", "--reverse", "--no-merges", "--format=%s", `${defaultBr}..HEAD`);
|
|
32550
|
+
const subjects = log.out.split(`
|
|
32551
|
+
`).filter(Boolean);
|
|
32552
|
+
const [first = "", ...rest] = (suggested ?? subjects.at(-1) ?? "land").split(`
|
|
32553
|
+
`);
|
|
32554
|
+
const subject = first.trim();
|
|
32555
|
+
const others = subjects.at(-1) === subject ? subjects.slice(0, -1) : subjects;
|
|
32556
|
+
const body = [
|
|
32557
|
+
rest.join(`
|
|
32558
|
+
`).trim(),
|
|
32559
|
+
others.length ? `Squashed with the branch's earlier commits:
|
|
32560
|
+
${others.join(`
|
|
32561
|
+
`)}` : ""
|
|
32562
|
+
].filter(Boolean).join(`
|
|
32563
|
+
|
|
32564
|
+
`);
|
|
32565
|
+
return body ? `${subject}
|
|
32566
|
+
|
|
32567
|
+
${body}` : subject;
|
|
32568
|
+
}
|
|
32569
|
+
function splitMessage(text) {
|
|
32570
|
+
const [first = "", ...rest] = text.split(`
|
|
32571
|
+
`);
|
|
32572
|
+
const body = rest.join(`
|
|
32573
|
+
`).trim();
|
|
32574
|
+
return { subject: first.trim(), ...body ? { body } : {} };
|
|
32417
32575
|
}
|
|
32418
32576
|
async function fastForwardFetched(repoPath, defaultBr) {
|
|
32419
32577
|
const current = await git(repoPath, "branch", "--show-current");
|
|
@@ -32525,7 +32683,7 @@ async function fetchBase(worktreePath, repo, w = UNWATCHED) {
|
|
|
32525
32683
|
w.step(`fetching ${base}`);
|
|
32526
32684
|
const f = await w.git(worktreePath, ["fetch", "--quiet", tracked.remote, tracked.merge]);
|
|
32527
32685
|
if (!f.ok)
|
|
32528
|
-
return { ok: false, message:
|
|
32686
|
+
return { ok: false, message: refused2("fetch failed", f) };
|
|
32529
32687
|
return { ok: true, message: `fetched ${base}` };
|
|
32530
32688
|
}
|
|
32531
32689
|
async function pushBranch(worktreePath, branch, w = UNWATCHED) {
|
|
@@ -32615,7 +32773,7 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
32615
32773
|
}
|
|
32616
32774
|
|
|
32617
32775
|
// ../daemon/src/git/turn.ts
|
|
32618
|
-
import { copyFileSync, existsSync as
|
|
32776
|
+
import { copyFileSync, existsSync as existsSync31, rmSync as rmSync6 } from "fs";
|
|
32619
32777
|
import { join as join27 } from "path";
|
|
32620
32778
|
var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
|
|
32621
32779
|
var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
|
|
@@ -32641,7 +32799,7 @@ async function treeNow(wtPath, head, dirty) {
|
|
|
32641
32799
|
const tree = await run(GIT, ["write-tree"], wtPath, env);
|
|
32642
32800
|
return tree.ok && tree.out ? tree.out : null;
|
|
32643
32801
|
};
|
|
32644
|
-
if (!
|
|
32802
|
+
if (!existsSync31(index))
|
|
32645
32803
|
was = undefined;
|
|
32646
32804
|
const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
|
|
32647
32805
|
if (was && paths && was.head === head) {
|
|
@@ -32656,9 +32814,9 @@ async function treeNow(wtPath, head, dirty) {
|
|
|
32656
32814
|
}
|
|
32657
32815
|
}
|
|
32658
32816
|
synced.delete(wtPath);
|
|
32659
|
-
if (!
|
|
32817
|
+
if (!existsSync31(index)) {
|
|
32660
32818
|
const own = join27(index, "..", "index");
|
|
32661
|
-
if (
|
|
32819
|
+
if (existsSync31(own))
|
|
32662
32820
|
copyFileSync(own, index);
|
|
32663
32821
|
else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
|
|
32664
32822
|
return null;
|
|
@@ -32739,8 +32897,8 @@ async function dropTurnRefs(repoPath, worktreeId) {
|
|
|
32739
32897
|
}
|
|
32740
32898
|
|
|
32741
32899
|
// ../daemon/src/worktrees/archive.ts
|
|
32742
|
-
import { existsSync as
|
|
32743
|
-
import { basename as
|
|
32900
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync8, readdirSync as readdirSync9, readFileSync as readFileSync19, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
32901
|
+
import { basename as basename11, dirname as dirname11, join as join28 } from "path";
|
|
32744
32902
|
class WorktreeArchive {
|
|
32745
32903
|
dir;
|
|
32746
32904
|
records = new Map;
|
|
@@ -32749,7 +32907,7 @@ class WorktreeArchive {
|
|
|
32749
32907
|
mkdirSync8(dir, { recursive: true });
|
|
32750
32908
|
for (const id of readdirSync9(dir)) {
|
|
32751
32909
|
const file = this.filesOf(id).record;
|
|
32752
|
-
if (!
|
|
32910
|
+
if (!existsSync32(file))
|
|
32753
32911
|
continue;
|
|
32754
32912
|
try {
|
|
32755
32913
|
this.records.set(id, JSON.parse(readFileSync19(file, "utf8")));
|
|
@@ -32807,8 +32965,8 @@ class WorktreeArchive {
|
|
|
32807
32965
|
planFile(id, rel) {
|
|
32808
32966
|
if (!this.records.has(id) || !isPlanPath(rel))
|
|
32809
32967
|
return null;
|
|
32810
|
-
const file = join28(this.filesOf(id).plans,
|
|
32811
|
-
if (!
|
|
32968
|
+
const file = join28(this.filesOf(id).plans, basename11(rel));
|
|
32969
|
+
if (!existsSync32(file))
|
|
32812
32970
|
return null;
|
|
32813
32971
|
try {
|
|
32814
32972
|
return readFileSync19(file, "utf8");
|
|
@@ -32849,7 +33007,7 @@ function summarize(r, repoId, transcript) {
|
|
|
32849
33007
|
};
|
|
32850
33008
|
}
|
|
32851
33009
|
function lastUsage(transcript) {
|
|
32852
|
-
if (!
|
|
33010
|
+
if (!existsSync32(transcript))
|
|
32853
33011
|
return null;
|
|
32854
33012
|
const lines = readFileSync19(transcript, "utf8").split(`
|
|
32855
33013
|
`);
|
|
@@ -32865,7 +33023,7 @@ function lastUsage(transcript) {
|
|
|
32865
33023
|
return null;
|
|
32866
33024
|
}
|
|
32867
33025
|
function firstPrompt(transcript) {
|
|
32868
|
-
if (!
|
|
33026
|
+
if (!existsSync32(transcript))
|
|
32869
33027
|
return;
|
|
32870
33028
|
for (const line of readFileSync19(transcript, "utf8").split(`
|
|
32871
33029
|
`)) {
|
|
@@ -32880,7 +33038,7 @@ function firstPrompt(transcript) {
|
|
|
32880
33038
|
return;
|
|
32881
33039
|
}
|
|
32882
33040
|
function move(from, to) {
|
|
32883
|
-
if (!
|
|
33041
|
+
if (!existsSync32(from))
|
|
32884
33042
|
return;
|
|
32885
33043
|
mkdirSync8(dirname11(to), { recursive: true });
|
|
32886
33044
|
rmSync7(to, { recursive: true, force: true });
|
|
@@ -33051,8 +33209,8 @@ async function fillLandMarks(jobs, read, width = 8) {
|
|
|
33051
33209
|
}
|
|
33052
33210
|
|
|
33053
33211
|
// ../daemon/src/worktrees/spare.ts
|
|
33054
|
-
import { existsSync as
|
|
33055
|
-
import { basename as
|
|
33212
|
+
import { existsSync as existsSync33 } from "fs";
|
|
33213
|
+
import { basename as basename12, join as join29 } from "path";
|
|
33056
33214
|
class SparePool {
|
|
33057
33215
|
d;
|
|
33058
33216
|
inflight = new Map;
|
|
@@ -33071,7 +33229,7 @@ class SparePool {
|
|
|
33071
33229
|
if (!text.trim())
|
|
33072
33230
|
return;
|
|
33073
33231
|
const wt = this.d.state.worktree(boxId);
|
|
33074
|
-
if (wt?.kind !== "spare" || !
|
|
33232
|
+
if (wt?.kind !== "spare" || !existsSync33(wt.path))
|
|
33075
33233
|
return;
|
|
33076
33234
|
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
33077
33235
|
if (agent.runningAgent === null)
|
|
@@ -33135,9 +33293,18 @@ class SparePool {
|
|
|
33135
33293
|
const repoId = repo.id;
|
|
33136
33294
|
let cut = () => {};
|
|
33137
33295
|
const added = (async () => {
|
|
33138
|
-
if (
|
|
33296
|
+
if (existsSync33(wt.path))
|
|
33139
33297
|
return;
|
|
33140
|
-
|
|
33298
|
+
const run = this.d.runs.begin(wt.id, "setup", {
|
|
33299
|
+
timeoutMs: timeoutFor(repo.config, "setup"),
|
|
33300
|
+
stage: "checking out"
|
|
33301
|
+
});
|
|
33302
|
+
try {
|
|
33303
|
+
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", "--no-checkout", wt.path, baseOf(repo)));
|
|
33304
|
+
await withLock(`checkout:${repo.path}`, () => gitOrThrow(wt.path, "reset", "--hard"));
|
|
33305
|
+
} finally {
|
|
33306
|
+
run.finish(0);
|
|
33307
|
+
}
|
|
33141
33308
|
})();
|
|
33142
33309
|
const done = (async () => {
|
|
33143
33310
|
this.d.runtime.markSetup(wt.id, true);
|
|
@@ -33185,7 +33352,7 @@ class SparePool {
|
|
|
33185
33352
|
}
|
|
33186
33353
|
log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
|
|
33187
33354
|
this.d.carryDraft?.(wt.id, wt.repoId);
|
|
33188
|
-
if (
|
|
33355
|
+
if (existsSync33(wt.path)) {
|
|
33189
33356
|
await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
|
|
33190
33357
|
return;
|
|
33191
33358
|
}
|
|
@@ -33229,40 +33396,70 @@ class SparePool {
|
|
|
33229
33396
|
this.refreshing.set(repoId, run);
|
|
33230
33397
|
await run;
|
|
33231
33398
|
}
|
|
33232
|
-
async claim(repoId, title, worktreeId) {
|
|
33399
|
+
async claim(repoId, title, worktreeId, stamps, prepare) {
|
|
33233
33400
|
const wt = this.spareOf(repoId);
|
|
33234
33401
|
if (!wt)
|
|
33235
33402
|
return null;
|
|
33236
33403
|
if (worktreeId && wt.id !== worktreeId)
|
|
33237
33404
|
return null;
|
|
33238
33405
|
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);
|
|
33406
|
+
if (!wasReady && !worktreeId)
|
|
33407
|
+
return null;
|
|
33249
33408
|
const repo = this.d.state.requireRepo(repoId);
|
|
33250
|
-
const
|
|
33251
|
-
|
|
33409
|
+
const running = this.inflight.get(repoId);
|
|
33410
|
+
const was = { title: wt.title, branch: wt.branch, phase: wt.phase, lockfile: wt.lockfile };
|
|
33252
33411
|
wt.kind = "worktree";
|
|
33253
33412
|
delete wt.phase;
|
|
33254
33413
|
delete wt.lockfile;
|
|
33255
|
-
wt.branch =
|
|
33414
|
+
wt.branch = `toyon/${basename12(wt.path)}`;
|
|
33256
33415
|
wt.title = title;
|
|
33257
|
-
wt.unnamed = true;
|
|
33258
33416
|
wt.createdAt = Date.now();
|
|
33259
|
-
|
|
33260
|
-
|
|
33417
|
+
wt.promptedAt = wt.createdAt;
|
|
33418
|
+
Object.assign(wt, Object.fromEntries(Object.entries(stamps).filter(([, v]) => v !== undefined)));
|
|
33261
33419
|
const next = repo.needsSetup ? null : await this.record(repoId, repo);
|
|
33262
33420
|
this.d.state.save();
|
|
33263
33421
|
if (next)
|
|
33264
33422
|
fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
|
|
33265
|
-
|
|
33423
|
+
const settled = (async () => {
|
|
33424
|
+
try {
|
|
33425
|
+
await running?.added;
|
|
33426
|
+
running?.cut();
|
|
33427
|
+
if (!existsSync33(wt.path))
|
|
33428
|
+
throw new Error("the worktree's directory is gone");
|
|
33429
|
+
await this.refreshing.get(repoId);
|
|
33430
|
+
await prepare(wt);
|
|
33431
|
+
await withRepoLock(repo.path, async () => {
|
|
33432
|
+
await gitOrThrow(wt.path, "branch", wt.branch);
|
|
33433
|
+
await gitOrThrow(wt.path, "symbolic-ref", "HEAD", `refs/heads/${wt.branch}`);
|
|
33434
|
+
});
|
|
33435
|
+
if (wasReady)
|
|
33436
|
+
fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
|
|
33437
|
+
} catch (e) {
|
|
33438
|
+
await this.unclaim(wt, was, stamps, next, e);
|
|
33439
|
+
throw e;
|
|
33440
|
+
}
|
|
33441
|
+
})();
|
|
33442
|
+
return { wt, settled };
|
|
33443
|
+
}
|
|
33444
|
+
async unclaim(wt, was, stamps, next, e) {
|
|
33445
|
+
wt.kind = "spare";
|
|
33446
|
+
wt.title = was.title;
|
|
33447
|
+
wt.branch = was.branch;
|
|
33448
|
+
if (was.phase)
|
|
33449
|
+
wt.phase = was.phase;
|
|
33450
|
+
if (was.lockfile)
|
|
33451
|
+
wt.lockfile = was.lockfile;
|
|
33452
|
+
for (const k of Object.keys(stamps))
|
|
33453
|
+
delete wt[k];
|
|
33454
|
+
delete wt.promptedAt;
|
|
33455
|
+
if (!existsSync33(wt.path)) {
|
|
33456
|
+
await this.rollBack(wt, e);
|
|
33457
|
+
return;
|
|
33458
|
+
}
|
|
33459
|
+
this.d.state.save();
|
|
33460
|
+
this.d.hub.emit("worktreesChanged");
|
|
33461
|
+
if (next)
|
|
33462
|
+
await this.d.discard(next.id).catch((re) => log.warn(wt.repoId, "reserved spare removal failed", re));
|
|
33266
33463
|
}
|
|
33267
33464
|
}
|
|
33268
33465
|
|
|
@@ -33340,7 +33537,7 @@ class Trunk {
|
|
|
33340
33537
|
const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
|
|
33341
33538
|
this.noteFetch(repo.id, f);
|
|
33342
33539
|
if (!f.ok)
|
|
33343
|
-
return { ok: false, message:
|
|
33540
|
+
return { ok: false, message: refused2("fetch failed", f) };
|
|
33344
33541
|
this.d.invalidateCounts();
|
|
33345
33542
|
return this.follow(main.id);
|
|
33346
33543
|
}
|
|
@@ -33400,23 +33597,8 @@ class TurnService {
|
|
|
33400
33597
|
stoppedByHand = new Set;
|
|
33401
33598
|
constructor(d) {
|
|
33402
33599
|
this.d = d;
|
|
33403
|
-
this.boot();
|
|
33404
33600
|
d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
|
|
33405
33601
|
}
|
|
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
33602
|
onStatus(worktreeId, status) {
|
|
33421
33603
|
const prev = this.lastStatus.get(worktreeId) ?? "idle";
|
|
33422
33604
|
this.lastStatus.set(worktreeId, status);
|
|
@@ -33503,6 +33685,7 @@ var idsOf = (rows, trunks) => [
|
|
|
33503
33685
|
...rows.map((r) => r.id),
|
|
33504
33686
|
...Object.values(trunks).map((t) => t.id)
|
|
33505
33687
|
];
|
|
33688
|
+
var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
|
|
33506
33689
|
var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
|
|
33507
33690
|
`, 1)[0])).join(" ");
|
|
33508
33691
|
|
|
@@ -33517,6 +33700,8 @@ class WorktreeService {
|
|
|
33517
33700
|
archiving = new Map;
|
|
33518
33701
|
restoring = new Map;
|
|
33519
33702
|
going = new Set;
|
|
33703
|
+
arriving = new Set;
|
|
33704
|
+
firstPrompt = new Map;
|
|
33520
33705
|
counts;
|
|
33521
33706
|
handChecked = new Map;
|
|
33522
33707
|
naming = new Set;
|
|
@@ -33531,7 +33716,7 @@ class WorktreeService {
|
|
|
33531
33716
|
this.archive = new WorktreeArchive(d.paths.archiveDir);
|
|
33532
33717
|
this.counts = new Counts({
|
|
33533
33718
|
read: (id) => this.readCounts(id),
|
|
33534
|
-
going: (id) => this.going.has(id)
|
|
33719
|
+
going: (id) => this.going.has(id) || this.arriving.has(id)
|
|
33535
33720
|
});
|
|
33536
33721
|
this.trunk = new Trunk({
|
|
33537
33722
|
state: d.state,
|
|
@@ -33544,6 +33729,7 @@ class WorktreeService {
|
|
|
33544
33729
|
hub: d.hub,
|
|
33545
33730
|
runtime: d.runtime,
|
|
33546
33731
|
paths: d.paths,
|
|
33732
|
+
runs: this.runs,
|
|
33547
33733
|
setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
|
|
33548
33734
|
teardown: (wt, repo) => this.teardown(wt, repo),
|
|
33549
33735
|
discard: async (id) => {
|
|
@@ -33615,39 +33801,55 @@ class WorktreeService {
|
|
|
33615
33801
|
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
33616
33802
|
|
|
33617
33803
|
`) : 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);
|
|
33804
|
+
const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
|
|
33805
|
+
const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
|
|
33806
|
+
let carriedIn;
|
|
33807
|
+
let release;
|
|
33808
|
+
const stamps = {
|
|
33809
|
+
createdBy: opts.createdBy,
|
|
33810
|
+
agent,
|
|
33811
|
+
mode: opts.mode,
|
|
33812
|
+
model: opts.model,
|
|
33813
|
+
effort: opts.effort,
|
|
33814
|
+
variant,
|
|
33815
|
+
from: opts.from,
|
|
33816
|
+
profile,
|
|
33817
|
+
unnamed: given ? undefined : true
|
|
33818
|
+
};
|
|
33819
|
+
const claimed = await this.spare.claim(repoId, title, opts.worktreeId, stamps, async (wt) => {
|
|
33820
|
+
const rt = this.d.runtime.ensureAgent(wt);
|
|
33821
|
+
release = rt.agent.hold();
|
|
33644
33822
|
if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
|
|
33645
33823
|
await rt.agent.restart();
|
|
33646
|
-
|
|
33647
|
-
|
|
33648
|
-
|
|
33649
|
-
|
|
33650
|
-
|
|
33824
|
+
carriedIn = await this.carryMain(repo, wt, !!opts.carry);
|
|
33825
|
+
rt.agent.send(agentPrompt, { context: withCarry(context(wt.id), carriedIn), attachments });
|
|
33826
|
+
});
|
|
33827
|
+
if (claimed) {
|
|
33828
|
+
const { wt } = claimed;
|
|
33829
|
+
const rt = this.d.runtime.ensureAgent(wt);
|
|
33830
|
+
this.firstPrompt.set(wt.id, claimed.settled.then(() => {
|
|
33831
|
+
return;
|
|
33832
|
+
}, () => {
|
|
33833
|
+
return;
|
|
33834
|
+
}));
|
|
33835
|
+
this.d.hub.emit("worktreesChanged");
|
|
33836
|
+
try {
|
|
33837
|
+
await claimed.settled;
|
|
33838
|
+
} catch (e) {
|
|
33839
|
+
if (carriedIn && rt.agent.queueLength > 0)
|
|
33840
|
+
rt.agent.unqueue(0);
|
|
33841
|
+
release?.();
|
|
33842
|
+
throw e instanceof UserError ? e : new UserError(e instanceof Error ? e.message : String(e));
|
|
33843
|
+
} finally {
|
|
33844
|
+
this.firstPrompt.delete(wt.id);
|
|
33845
|
+
}
|
|
33846
|
+
release?.();
|
|
33847
|
+
if (restart)
|
|
33848
|
+
this.restartProcs(wt);
|
|
33849
|
+
this.nameOrAsk(wt, task, given, variant);
|
|
33850
|
+
if (carriedIn?.unmoved)
|
|
33851
|
+
throw new UserError(carriedIn.unmoved, { delivered: true });
|
|
33852
|
+
return wt;
|
|
33651
33853
|
}
|
|
33652
33854
|
if (!await isGitRepo(repo.path)) {
|
|
33653
33855
|
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.`);
|
|
@@ -33730,7 +33932,7 @@ class WorktreeService {
|
|
|
33730
33932
|
};
|
|
33731
33933
|
const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
|
|
33732
33934
|
const restored = (carried.ok ? carried.out.split(`
|
|
33733
|
-
`).filter(Boolean) : []).filter((p) => !
|
|
33935
|
+
`).filter(Boolean) : []).filter((p) => !existsSync34(join30(wt.path, p)));
|
|
33734
33936
|
const applied = await git(wt.path, "stash", "apply", sha);
|
|
33735
33937
|
if (applied.ok) {
|
|
33736
33938
|
const ref = await entry();
|
|
@@ -33770,7 +33972,7 @@ class WorktreeService {
|
|
|
33770
33972
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
33771
33973
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
33772
33974
|
let slug = branchSlug(title) || "branch";
|
|
33773
|
-
if (
|
|
33975
|
+
if (existsSync34(join30(this.d.paths.worktreesDir, repo.name, slug)))
|
|
33774
33976
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
33775
33977
|
const wtPath = join30(this.d.paths.worktreesDir, repo.name, slug);
|
|
33776
33978
|
await withRepoLock(repo.path, async () => {
|
|
@@ -33808,8 +34010,9 @@ class WorktreeService {
|
|
|
33808
34010
|
this.launch(wt, repo, repo.path);
|
|
33809
34011
|
return wt;
|
|
33810
34012
|
}
|
|
33811
|
-
async requireDiscovered(repoId, repoPath, target) {
|
|
33812
|
-
const
|
|
34013
|
+
async requireDiscovered(repoId, repoPath, target, except) {
|
|
34014
|
+
const known = this.d.state.worktrees.filter((w) => w.id !== except);
|
|
34015
|
+
const rows = await discoverIn(repoId, repoPath, known);
|
|
33813
34016
|
const found = rows.find((r) => canonical(r.path) === target);
|
|
33814
34017
|
if (!found)
|
|
33815
34018
|
throw new UserError("that worktree is gone, or Toyon already has it");
|
|
@@ -33824,38 +34027,50 @@ class WorktreeService {
|
|
|
33824
34027
|
const repoId = r.repoId;
|
|
33825
34028
|
const repo = this.d.state.requireRepo(repoId);
|
|
33826
34029
|
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
|
-
});
|
|
34030
|
+
const pushed = this.discoveredById(worktreeId);
|
|
34031
|
+
if (pushed?.locked)
|
|
34032
|
+
throw new UserError(heldBy(pushed));
|
|
34033
|
+
if (!r.branch)
|
|
34034
|
+
throw new UserError(`${r.name} is detached: check out a branch in it first`);
|
|
34035
|
+
const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((w) => isInside(target, canonical(w.path)));
|
|
34036
|
+
if (enclosing)
|
|
34037
|
+
throw new UserError(`${r.name} sits inside ${enclosing.path}, which Toyon already manages`);
|
|
34038
|
+
const wt = {
|
|
34039
|
+
id: shortId(),
|
|
34040
|
+
repoId,
|
|
34041
|
+
path: r.path,
|
|
34042
|
+
branch: r.branch,
|
|
34043
|
+
kind: "worktree",
|
|
34044
|
+
proxyPort: await allocateProxyPort(),
|
|
34045
|
+
title: r.name,
|
|
34046
|
+
createdAt: Date.now(),
|
|
34047
|
+
agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
|
|
34048
|
+
parked: true,
|
|
34049
|
+
...message ? { promptedAt: Date.now() } : {},
|
|
34050
|
+
...createdBy ? { createdBy } : {}
|
|
34051
|
+
};
|
|
34052
|
+
const { rt, release } = this.listAhead(wt, message);
|
|
33853
34053
|
this.invalidateDiscovered();
|
|
33854
|
-
this.
|
|
33855
|
-
|
|
33856
|
-
|
|
33857
|
-
|
|
33858
|
-
|
|
34054
|
+
this.d.hub.emit("worktreesChanged");
|
|
34055
|
+
try {
|
|
34056
|
+
await withRepoLock(repo.path, async () => {
|
|
34057
|
+
const found = await this.requireDiscovered(repoId, repo.path, target, wt.id);
|
|
34058
|
+
if (found.locked)
|
|
34059
|
+
throw new UserError(heldBy(found));
|
|
34060
|
+
if (!found.branch)
|
|
34061
|
+
throw new UserError(`${found.name} is detached: check out a branch in it first`);
|
|
34062
|
+
wt.branch = found.branch;
|
|
34063
|
+
});
|
|
34064
|
+
} catch (e) {
|
|
34065
|
+
await this.unlist(wt, !!message);
|
|
34066
|
+
this.invalidateDiscovered();
|
|
34067
|
+
this.d.hub.emit("worktreesChanged");
|
|
34068
|
+
throw e;
|
|
34069
|
+
}
|
|
34070
|
+
this.d.state.save();
|
|
34071
|
+
fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path, { setupCommands: false }), "setup + start");
|
|
34072
|
+
rt.agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
|
|
34073
|
+
release();
|
|
33859
34074
|
return wt;
|
|
33860
34075
|
}
|
|
33861
34076
|
async ownProcs(worktreeId, names) {
|
|
@@ -34025,7 +34240,7 @@ class WorktreeService {
|
|
|
34025
34240
|
}
|
|
34026
34241
|
async teardown(wt, repo) {
|
|
34027
34242
|
const cmds = repo.config.teardown ?? [];
|
|
34028
|
-
if (cmds.length === 0 || !
|
|
34243
|
+
if (cmds.length === 0 || !existsSync34(wt.path))
|
|
34029
34244
|
return;
|
|
34030
34245
|
for (const cmd of cmds) {
|
|
34031
34246
|
const code = await runSetup(cmd, wt.path, (line) => this.d.hub.emit("log", wt.id, "setup", line), worktreeEnv(wt, repo), { timeoutMs: timeoutFor(repo.config, "setup") });
|
|
@@ -34075,7 +34290,7 @@ class WorktreeService {
|
|
|
34075
34290
|
}
|
|
34076
34291
|
keepPlans(wt) {
|
|
34077
34292
|
const from = join30(wt.path, PLANS_DIR);
|
|
34078
|
-
if (!
|
|
34293
|
+
if (!existsSync34(from))
|
|
34079
34294
|
return;
|
|
34080
34295
|
try {
|
|
34081
34296
|
cpSync(from, this.plansScratch(wt.id), { recursive: true });
|
|
@@ -34269,79 +34484,126 @@ class WorktreeService {
|
|
|
34269
34484
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
34270
34485
|
if (rec.auto)
|
|
34271
34486
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
34272
|
-
const 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
|
-
});
|
|
34487
|
+
const path = existsSync34(old.path) ? join30(dirname12(old.path), freeSlot(dirname12(old.path)).dir) : old.path;
|
|
34298
34488
|
const { variant: _variant, ...rest } = old;
|
|
34299
34489
|
const wt = {
|
|
34300
34490
|
...rest,
|
|
34301
34491
|
repoId: repo.id,
|
|
34302
34492
|
path,
|
|
34303
|
-
branch,
|
|
34493
|
+
branch: old.branch,
|
|
34304
34494
|
proxyPort: await allocateProxyPort(),
|
|
34305
34495
|
promptedAt: Date.now(),
|
|
34306
34496
|
parked: true,
|
|
34307
34497
|
...createdBy ? { createdBy } : {}
|
|
34308
34498
|
};
|
|
34499
|
+
const chat = { ...this.chatFiles(old.id), plans: this.plansScratch(old.id) };
|
|
34309
34500
|
try {
|
|
34310
|
-
this.archive.take(old.id,
|
|
34501
|
+
this.archive.take(old.id, chat);
|
|
34311
34502
|
} catch (e) {
|
|
34312
34503
|
log.warn(old.id, "could not move its chat back out of the archive", e);
|
|
34504
|
+
throw new UserError(`could not move ${old.title}'s chat back out of the archive`);
|
|
34313
34505
|
}
|
|
34314
34506
|
if (rec.sessionId && path === old.path)
|
|
34315
34507
|
this.d.state.setSession(wt.id, rec.sessionId);
|
|
34508
|
+
this.arriving.add(wt.id);
|
|
34509
|
+
const { rt, release } = this.listAhead(wt, message);
|
|
34510
|
+
const run = this.runs.begin(wt.id, "setup", {
|
|
34511
|
+
timeoutMs: timeoutFor(repo.config, "setup"),
|
|
34512
|
+
stage: "checking out the branch"
|
|
34513
|
+
});
|
|
34514
|
+
this.d.hub.emit("worktreesChanged");
|
|
34515
|
+
this.d.hub.emit("archiveChanged", repo.id);
|
|
34516
|
+
let branch;
|
|
34517
|
+
try {
|
|
34518
|
+
branch = await withRepoLock(repo.path, () => this.checkOutArchived(repo, old, kept, path));
|
|
34519
|
+
} catch (e) {
|
|
34520
|
+
await this.takeBack(wt, rec, chat, run, !!message);
|
|
34521
|
+
throw e;
|
|
34522
|
+
}
|
|
34523
|
+
run.finish(0);
|
|
34524
|
+
this.arriving.delete(wt.id);
|
|
34525
|
+
wt.branch = branch;
|
|
34526
|
+
move(chat.plans, join30(path, PLANS_DIR));
|
|
34316
34527
|
const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
|
|
34317
34528
|
if (!dropped.ok)
|
|
34318
34529
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
34319
34530
|
this.counts.stale(wt.id);
|
|
34320
|
-
this.
|
|
34321
|
-
this.d.
|
|
34531
|
+
this.d.state.save();
|
|
34532
|
+
this.d.hub.emit("worktreesChanged");
|
|
34533
|
+
fireAndForget(wt.id, this.setupAndStart(wt, repo, repo.path), "setup + start");
|
|
34534
|
+
rt.agent.note({
|
|
34322
34535
|
type: "restored",
|
|
34323
34536
|
archivedAt: rec.archivedAt,
|
|
34324
34537
|
branch,
|
|
34325
34538
|
uncommitted: !!kept.snapshot,
|
|
34326
34539
|
ts: Date.now()
|
|
34327
34540
|
});
|
|
34328
|
-
|
|
34329
|
-
|
|
34330
|
-
this.d.hub.emit("archiveChanged", repo.id);
|
|
34541
|
+
release();
|
|
34542
|
+
this.d.hub.emit("filesChanged", wt.id);
|
|
34331
34543
|
return wt;
|
|
34332
34544
|
}
|
|
34545
|
+
async checkOutArchived(repo, old, kept, path) {
|
|
34546
|
+
if (!await commitOf(repo.path, archiveRef(old.id))) {
|
|
34547
|
+
throw new UserError(`${old.title}'s kept commits are gone from git`);
|
|
34548
|
+
}
|
|
34549
|
+
let name = old.branch;
|
|
34550
|
+
let create = true;
|
|
34551
|
+
const tip = await commitOf(repo.path, `refs/heads/${name}`);
|
|
34552
|
+
if (tip && hasOwnBranch(old)) {
|
|
34553
|
+
name = `${name}-${shortId().slice(0, 3)}`;
|
|
34554
|
+
} else if (tip) {
|
|
34555
|
+
if (tip !== kept.head && kept.snapshot) {
|
|
34556
|
+
throw new UserError(`${name} has moved since ${old.title} was archived: check it out in git instead`);
|
|
34557
|
+
}
|
|
34558
|
+
create = false;
|
|
34559
|
+
}
|
|
34560
|
+
try {
|
|
34561
|
+
await checkOutKept(repo.path, path, kept, { name, create });
|
|
34562
|
+
} catch (e) {
|
|
34563
|
+
await git(repo.path, "worktree", "remove", "--force", path);
|
|
34564
|
+
if (create)
|
|
34565
|
+
await git(repo.path, "branch", "-D", name);
|
|
34566
|
+
throw e;
|
|
34567
|
+
}
|
|
34568
|
+
return name;
|
|
34569
|
+
}
|
|
34570
|
+
listAhead(wt, message) {
|
|
34571
|
+
this.d.runtime.markSetup(wt.id, true);
|
|
34572
|
+
this.d.state.addWorktree(wt);
|
|
34573
|
+
const rt = this.d.runtime.ensureAgent(wt);
|
|
34574
|
+
const release = rt.agent.hold();
|
|
34575
|
+
if (message)
|
|
34576
|
+
rt.agent.send(message.text, { attachments: message.attachments });
|
|
34577
|
+
return { rt, release };
|
|
34578
|
+
}
|
|
34579
|
+
async takeBack(wt, rec, chat, run, queued) {
|
|
34580
|
+
run.finish(0);
|
|
34581
|
+
await this.unlist(wt, queued);
|
|
34582
|
+
this.arriving.delete(wt.id);
|
|
34583
|
+
this.archive.put(rec, chat);
|
|
34584
|
+
this.d.hub.emit("worktreesChanged");
|
|
34585
|
+
this.d.hub.emit("archiveChanged", wt.repoId);
|
|
34586
|
+
}
|
|
34587
|
+
async unlist(wt, queued) {
|
|
34588
|
+
const agent = this.d.runtime.agentFor(wt.id);
|
|
34589
|
+
if (queued && agent && agent.queueLength > 0)
|
|
34590
|
+
agent.unqueue(0);
|
|
34591
|
+
await this.d.runtime.stop(wt.id);
|
|
34592
|
+
this.d.state.removeWorktree(wt.id, { refused: true });
|
|
34593
|
+
releasePort(wt.proxyPort);
|
|
34594
|
+
}
|
|
34333
34595
|
async deleteArchived(archiveId) {
|
|
34334
34596
|
const rec = this.archive.get(archiveId);
|
|
34335
34597
|
if (!rec)
|
|
34336
34598
|
return;
|
|
34337
34599
|
const repo = this.repoOf(rec);
|
|
34338
34600
|
const repoPath = repo?.path ?? rec.repoPath;
|
|
34339
|
-
if (rec.kept &&
|
|
34601
|
+
if (rec.kept && existsSync34(repoPath)) {
|
|
34340
34602
|
const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
|
|
34341
34603
|
if (!r.ok)
|
|
34342
34604
|
log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
|
|
34343
34605
|
}
|
|
34344
|
-
if (rec.worktree.lands?.length &&
|
|
34606
|
+
if (rec.worktree.lands?.length && existsSync34(repoPath))
|
|
34345
34607
|
await dropLandRefs(repoPath, archiveId);
|
|
34346
34608
|
this.archive.delete(archiveId);
|
|
34347
34609
|
this.d.drafts?.drop(archiveId);
|
|
@@ -34450,8 +34712,8 @@ class WorktreeService {
|
|
|
34450
34712
|
}
|
|
34451
34713
|
async mirrorBaseIgnore(repo) {
|
|
34452
34714
|
const src = join30(repo.path, ".gitignore");
|
|
34453
|
-
const tracked =
|
|
34454
|
-
const mirror =
|
|
34715
|
+
const tracked = existsSync34(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
|
|
34716
|
+
const mirror = existsSync34(src) && !tracked;
|
|
34455
34717
|
const lines = mirror ? readFileSync20(src, "utf8").split(`
|
|
34456
34718
|
`).filter((l) => l.trim() !== "") : [];
|
|
34457
34719
|
await excludeBlock(repo.path, BASE_IGNORE, lines);
|
|
@@ -34466,8 +34728,10 @@ class WorktreeService {
|
|
|
34466
34728
|
}
|
|
34467
34729
|
const srcNm = join30(depsSource, "node_modules");
|
|
34468
34730
|
const dstNm = join30(wt.path, "node_modules");
|
|
34469
|
-
if (
|
|
34731
|
+
if (existsSync34(srcNm) && !existsSync34(dstNm)) {
|
|
34732
|
+
const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
|
|
34470
34733
|
const r = await cloneTree(srcNm, dstNm);
|
|
34734
|
+
run.finish(0);
|
|
34471
34735
|
if (r.ok)
|
|
34472
34736
|
this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
|
|
34473
34737
|
else
|
|
@@ -34475,7 +34739,7 @@ class WorktreeService {
|
|
|
34475
34739
|
}
|
|
34476
34740
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
34477
34741
|
const src = join30(depsSource, f);
|
|
34478
|
-
if (!
|
|
34742
|
+
if (!existsSync34(src) || existsSync34(join30(wt.path, f)))
|
|
34479
34743
|
continue;
|
|
34480
34744
|
const r = await run("cp", [src, join30(wt.path, f)], wt.path);
|
|
34481
34745
|
if (r.ok)
|
|
@@ -34746,9 +35010,10 @@ ${wt.landing.body}` : s;
|
|
|
34746
35010
|
const policy = landPolicy(repo.config);
|
|
34747
35011
|
const own = hasOwnBranch(wt);
|
|
34748
35012
|
const w = this.landWatch(wt);
|
|
34749
|
-
const
|
|
35013
|
+
const typed = message?.trim();
|
|
35014
|
+
const suggested = typed || (wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
|
|
34750
35015
|
|
|
34751
|
-
${wt.landing.body}` : wt.landing.subject : undefined;
|
|
35016
|
+
${wt.landing.body}` : wt.landing.subject : undefined);
|
|
34752
35017
|
const base = baseOf(repo);
|
|
34753
35018
|
if (policy.land === "pr") {
|
|
34754
35019
|
if (wt.pr?.state === "merged" && !prTaken(wt)) {
|
|
@@ -34792,13 +35057,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34792
35057
|
const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
|
|
34793
35058
|
return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
|
|
34794
35059
|
}
|
|
35060
|
+
const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
|
|
34795
35061
|
const result = await openPr({
|
|
34796
35062
|
worktreePath: wt.path,
|
|
34797
35063
|
branch: wt.branch,
|
|
34798
35064
|
defaultBr: repo.defaultBranch,
|
|
34799
35065
|
base,
|
|
34800
|
-
subject:
|
|
34801
|
-
body:
|
|
35066
|
+
subject: named?.subject,
|
|
35067
|
+
body: named?.body,
|
|
34802
35068
|
automerge: policy.automerge,
|
|
34803
35069
|
method: policy.merge
|
|
34804
35070
|
}, w);
|
|
@@ -34827,7 +35093,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34827
35093
|
if (!taken.ok)
|
|
34828
35094
|
return taken;
|
|
34829
35095
|
const mark = await landingMark(wt.path, base);
|
|
34830
|
-
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
|
|
35096
|
+
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
34831
35097
|
const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
|
|
34832
35098
|
if (!landed.ok)
|
|
34833
35099
|
return landed;
|
|
@@ -34878,7 +35144,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34878
35144
|
sha = await commitOf(wt.path, base) ?? "";
|
|
34879
35145
|
break;
|
|
34880
35146
|
}
|
|
34881
|
-
squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
|
|
35147
|
+
squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
34882
35148
|
const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
|
|
34883
35149
|
await this.headMoved(wt.id);
|
|
34884
35150
|
if (!built.ok || !built.sha)
|
|
@@ -34930,8 +35196,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34930
35196
|
async landedWhat(repo, wt, mark, opts) {
|
|
34931
35197
|
if (opts.pr?.title)
|
|
34932
35198
|
return [opts.pr.title];
|
|
34933
|
-
const subject = opts.squash
|
|
34934
|
-
`)[0]?.trim();
|
|
35199
|
+
const subject = opts.squash ? splitMessage(opts.squash).subject : "";
|
|
34935
35200
|
if (subject)
|
|
34936
35201
|
return [subject];
|
|
34937
35202
|
const read = await rangeSubjects(repo.path, mark.base, mark.tip);
|
|
@@ -34949,7 +35214,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
34949
35214
|
const r = await git(wt.path, "reset", "--keep", base);
|
|
34950
35215
|
if (!r.ok)
|
|
34951
35216
|
log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
|
|
34952
|
-
|
|
35217
|
+
this.counts.stale(wt.id);
|
|
35218
|
+
await this.counts.refresh([wt.id]);
|
|
34953
35219
|
}
|
|
34954
35220
|
async fetchBase(wt, repo, w) {
|
|
34955
35221
|
if (!baseIsRemote(repo))
|
|
@@ -35238,7 +35504,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35238
35504
|
await markTurn(wt.path, wt.id);
|
|
35239
35505
|
}
|
|
35240
35506
|
async gitStatus(worktreeId) {
|
|
35241
|
-
if (this.going.has(worktreeId))
|
|
35507
|
+
if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
|
|
35242
35508
|
return null;
|
|
35243
35509
|
const r = this.readable(worktreeId);
|
|
35244
35510
|
if (!r)
|
|
@@ -35281,7 +35547,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35281
35547
|
}
|
|
35282
35548
|
if (r.wt && isMain)
|
|
35283
35549
|
this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
|
|
35284
|
-
if (this.going.has(worktreeId))
|
|
35550
|
+
if (this.going.has(worktreeId) || this.arriving.has(worktreeId))
|
|
35285
35551
|
return null;
|
|
35286
35552
|
this.noteCounts(worktreeId, isMain, files.length, counts);
|
|
35287
35553
|
return { files, committed, ...turn ? { turn } : {}, head: head.ok ? head.out : undefined, ...counts };
|
|
@@ -35327,6 +35593,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
35327
35593
|
const wt = this.requireRun(worktreeId);
|
|
35328
35594
|
if (wt.kind === "main")
|
|
35329
35595
|
throw new UserError("nothing runs on main: the plus starts a worktree for it");
|
|
35596
|
+
await this.firstPrompt.get(worktreeId);
|
|
35330
35597
|
const agent = this.d.runtime.agentFor(worktreeId);
|
|
35331
35598
|
if (!agent)
|
|
35332
35599
|
throw new UserError("worktree still starting; try again in a moment");
|
|
@@ -35532,7 +35799,8 @@ var {
|
|
|
35532
35799
|
helperStub: HELPER_STUB,
|
|
35533
35800
|
cliEntry: CLI_ENTRY,
|
|
35534
35801
|
sourceRoot: SOURCE_ROOT,
|
|
35535
|
-
packageJson: PACKAGE_JSON
|
|
35802
|
+
packageJson: PACKAGE_JSON,
|
|
35803
|
+
changelog: CHANGELOG
|
|
35536
35804
|
} = locateAssets(here);
|
|
35537
35805
|
var pruned = pruneAssets(SHELL_DIST);
|
|
35538
35806
|
if (pruned > 0)
|
|
@@ -35540,7 +35808,7 @@ if (pruned > 0)
|
|
|
35540
35808
|
var managed = await loadManaged();
|
|
35541
35809
|
var paths = makePaths();
|
|
35542
35810
|
ensureDirs(paths);
|
|
35543
|
-
forwardTranscripts(paths.transcriptsDir);
|
|
35811
|
+
forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join31(paths.home, "transcripts-forwarded"));
|
|
35544
35812
|
var token = loadOrCreateToken(paths);
|
|
35545
35813
|
var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
|
|
35546
35814
|
var helperApp = join31(paths.home, DAEMON_FILES.helper);
|
|
@@ -35581,43 +35849,39 @@ await linuxSandbox.refresh();
|
|
|
35581
35849
|
var agents = loadAgentRegistry(paths.agentsFile, paths.agentsDir, linuxSandbox, managed.policy);
|
|
35582
35850
|
if (state.defaultAgent !== undefined && !agents.get(state.defaultAgent))
|
|
35583
35851
|
state.clearDefaultAgent();
|
|
35584
|
-
var
|
|
35852
|
+
var readiness2 = new AgentReadiness({
|
|
35585
35853
|
infos: () => agents.infos(),
|
|
35586
35854
|
require: (id) => agents.require(id),
|
|
35587
|
-
connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `
|
|
35855
|
+
connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `check:${spec.id}`),
|
|
35588
35856
|
cwd: paths.scratchDir,
|
|
35589
|
-
|
|
35857
|
+
options: (id, category) => state.cachedOptions(id, category),
|
|
35590
35858
|
learned: (id, category, choices) => {
|
|
35591
35859
|
if (state.setCachedOptions(id, category, choices))
|
|
35592
35860
|
hub.emit("agentsChanged");
|
|
35861
|
+
},
|
|
35862
|
+
quick: (id) => {
|
|
35863
|
+
const spec = agents.get(id);
|
|
35864
|
+
return !!spec && !lacksQuickModel(spec, state);
|
|
35593
35865
|
}
|
|
35594
35866
|
});
|
|
35867
|
+
readiness2.onChange = () => hub.emit("agentsChanged");
|
|
35595
35868
|
agents.onChange = () => {
|
|
35596
35869
|
hub.emit("agentsChanged");
|
|
35597
|
-
fireAndForget("agents",
|
|
35870
|
+
fireAndForget("agents", readiness2.checkMissing(), "read agent models");
|
|
35598
35871
|
};
|
|
35599
|
-
var accounts = new AgentAccounts({
|
|
35600
|
-
require: (id) => agents.require(id),
|
|
35601
|
-
connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `auth:${spec.id}`)
|
|
35602
|
-
});
|
|
35603
|
-
accounts.onChange = () => hub.emit("agentsChanged");
|
|
35604
35872
|
var uploads = new UploadStore(paths.uploadsDir);
|
|
35605
35873
|
var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
|
|
35606
35874
|
var mcpPort = port;
|
|
35607
35875
|
var tools = new ToolSet;
|
|
35608
35876
|
var mcp = new ToyonMcp({ url: (id) => `http://127.0.0.1:${mcpPort}/mcp/${id}`, version: package_default.version, tools });
|
|
35609
|
-
var pageErrors = new PageErrorService({
|
|
35610
|
-
hub,
|
|
35611
|
-
runtime: { agentFor: (id) => runtime.agentFor(id) },
|
|
35612
|
-
known: (id) => state.worktree(id) !== undefined
|
|
35613
|
-
});
|
|
35877
|
+
var pageErrors = new PageErrorService({ hub, known: (id) => state.worktree(id) !== undefined });
|
|
35614
35878
|
var runtime = new RuntimeRegistry({
|
|
35615
35879
|
hub,
|
|
35616
35880
|
state,
|
|
35617
35881
|
paths,
|
|
35618
35882
|
agents,
|
|
35619
35883
|
managed: managed.policy,
|
|
35620
|
-
|
|
35884
|
+
readiness: readiness2,
|
|
35621
35885
|
attachments: attachments2,
|
|
35622
35886
|
bridgeScript: () => bridge.get(),
|
|
35623
35887
|
pageErrors: (id) => pageErrors.ambient(id),
|
|
@@ -35660,8 +35924,6 @@ var handoff2 = new HandoffService({
|
|
|
35660
35924
|
create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts)
|
|
35661
35925
|
});
|
|
35662
35926
|
tools.add(handoff2);
|
|
35663
|
-
var render = new RenderService({ runtime, pageErrors });
|
|
35664
|
-
tools.add(render);
|
|
35665
35927
|
var turns = new TurnService({
|
|
35666
35928
|
state,
|
|
35667
35929
|
hub,
|
|
@@ -35776,6 +36038,7 @@ var {
|
|
|
35776
36038
|
port,
|
|
35777
36039
|
token,
|
|
35778
36040
|
shellDist: SHELL_DIST,
|
|
36041
|
+
changelog: CHANGELOG,
|
|
35779
36042
|
version: package_default.version,
|
|
35780
36043
|
noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
|
|
35781
36044
|
startLink: () => helper?.startLink() ?? null,
|
|
@@ -35819,12 +36082,11 @@ var {
|
|
|
35819
36082
|
landing,
|
|
35820
36083
|
fix,
|
|
35821
36084
|
pageErrors,
|
|
35822
|
-
render,
|
|
35823
36085
|
handoff: handoff2,
|
|
35824
36086
|
mcp,
|
|
35825
36087
|
themes,
|
|
35826
36088
|
agents,
|
|
35827
|
-
|
|
36089
|
+
readiness: readiness2,
|
|
35828
36090
|
attachments: attachments2,
|
|
35829
36091
|
self,
|
|
35830
36092
|
afterLand,
|
|
@@ -35855,7 +36117,7 @@ idle.boot();
|
|
|
35855
36117
|
runs.boot();
|
|
35856
36118
|
landing.boot();
|
|
35857
36119
|
fireAndForget("cache", cache.sweep(), "cache sweep");
|
|
35858
|
-
fireAndForget("agents", agents.installMissing().then(() =>
|
|
36120
|
+
fireAndForget("agents", agents.installMissing().then(() => readiness2.checkMissing()), "agent adapter install");
|
|
35859
36121
|
var repoArg = process.argv[2];
|
|
35860
36122
|
if (repoArg) {
|
|
35861
36123
|
try {
|