toyon 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +64 -14
- package/dist/daemon.js +781 -190
- package/dist/shell/assets/{Editor-DAJni9OF.js → Editor-DL0q1Bu5.js} +161 -161
- package/dist/shell/assets/{XTerm-dug6M6yd.js → XTerm-C9YH4Lh2.js} +1 -1
- package/dist/shell/assets/{cssMode-B5iiwdXn.js → cssMode-Et22hXpa.js} +1 -1
- package/dist/shell/assets/{freemarker2-CdVcBNE9.js → freemarker2-Clz20PcM.js} +1 -1
- package/dist/shell/assets/{grammar-Cfxx1yRn.js → grammar-CPvdB3az.js} +1 -1
- package/dist/shell/assets/{handlebars-CW5EFZC2.js → handlebars-B4nvXAx8.js} +1 -1
- package/dist/shell/assets/{html-B8gP1xu-.js → html-DPIGgBLK.js} +1 -1
- package/dist/shell/assets/{htmlMode-C6I99y8u.js → htmlMode-C2LaVuEX.js} +1 -1
- package/dist/shell/assets/index-2QusTgw-.js +262 -0
- package/dist/shell/assets/index-TaJLoLSd.css +1 -0
- package/dist/shell/assets/{javascript-C_3_LFln.js → javascript-CnwJfTbN.js} +1 -1
- package/dist/shell/assets/{jsonMode-CD5xJEUk.js → jsonMode-8znCTiqR.js} +1 -1
- package/dist/shell/assets/{liquid-CJw93Zxn.js → liquid-Bdm-1CUh.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-wMR368iN.js → lspLanguageFeatures-B1DMU7yH.js} +1 -1
- package/dist/shell/assets/{mdx-BJ6yMgc3.js → mdx-Ktcqo9Jf.js} +1 -1
- package/dist/shell/assets/{python-BWGkxFUr.js → python-QX--d8LR.js} +1 -1
- package/dist/shell/assets/{razor-BV9pdU1A.js → razor-C0LrXteG.js} +1 -1
- package/dist/shell/assets/{tsMode-CN8OfYAX.js → tsMode-BtYCp6db.js} +1 -1
- package/dist/shell/assets/{xml-DBGLqvw7.js → xml-BXbaQ9MG.js} +1 -1
- package/dist/shell/assets/{yaml-dH0C6Dth.js → yaml-C6FhaU_C.js} +1 -1
- package/dist/shell/index.html +7 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-BRSL9cyD.css +0 -1
- package/dist/shell/assets/index-D_PsyNmp.js +0 -266
package/dist/daemon.js
CHANGED
|
@@ -58,6 +58,7 @@ function applyLog(lines, proc, line, retract = 0) {
|
|
|
58
58
|
return out;
|
|
59
59
|
return [...out, { proc, line }];
|
|
60
60
|
}
|
|
61
|
+
var KEEP_AWAKE_MODES = ["use", "always", "off"];
|
|
61
62
|
|
|
62
63
|
// ../shared/src/worktree-caps.ts
|
|
63
64
|
function isMain(wt) {
|
|
@@ -119,7 +120,7 @@ function archiveReason(wt, f) {
|
|
|
119
120
|
return null;
|
|
120
121
|
if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
|
|
121
122
|
return null;
|
|
122
|
-
const what = lost ? "
|
|
123
|
+
const what = lost ? "sibling landed" : wt.landed ? "landed" : "no changes";
|
|
123
124
|
return `${what}, ${UNOPENED}${duration(f.afterMs)}`;
|
|
124
125
|
}
|
|
125
126
|
var UNOPENED = "not opened in ";
|
|
@@ -6473,12 +6474,25 @@ function describeManaged(p) {
|
|
|
6473
6474
|
return out;
|
|
6474
6475
|
}
|
|
6475
6476
|
|
|
6477
|
+
// ../shared/src/output.ts
|
|
6478
|
+
var CONTEXT_CHARS = 6000;
|
|
6479
|
+
var HEAD_CHARS = 1500;
|
|
6480
|
+
function clipOutput(text) {
|
|
6481
|
+
if (text.length <= CONTEXT_CHARS)
|
|
6482
|
+
return text;
|
|
6483
|
+
const cut = text.length - CONTEXT_CHARS;
|
|
6484
|
+
return `${text.slice(0, HEAD_CHARS)}
|
|
6485
|
+
[${cut} characters cut here]
|
|
6486
|
+
${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
|
|
6487
|
+
}
|
|
6488
|
+
|
|
6476
6489
|
// ../shared/src/pair.ts
|
|
6477
6490
|
var PAIR_TTL_MS = 120000;
|
|
6478
6491
|
var pairLink = (host, code) => `https://${host}/#pair=${code}`;
|
|
6492
|
+
var isTailnetName = (host) => host.endsWith(".ts.net");
|
|
6479
6493
|
|
|
6480
6494
|
// ../shared/src/protocol/limits.ts
|
|
6481
|
-
var PROTOCOL_VERSION =
|
|
6495
|
+
var PROTOCOL_VERSION = 89;
|
|
6482
6496
|
var FILE_MAX_CHARS = 5000000;
|
|
6483
6497
|
var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
6484
6498
|
var IMAGE_MAX_EDGE = 2576;
|
|
@@ -6784,7 +6798,9 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6784
6798
|
object({
|
|
6785
6799
|
t: literal("adopt-worktree"),
|
|
6786
6800
|
clientId: string2().max(64).optional(),
|
|
6787
|
-
worktreeId: id
|
|
6801
|
+
worktreeId: id,
|
|
6802
|
+
message: object({ text: prose, attachments }).optional(),
|
|
6803
|
+
boxId
|
|
6788
6804
|
}),
|
|
6789
6805
|
object({ t: literal("git-status"), worktreeId: id }),
|
|
6790
6806
|
object({
|
|
@@ -6830,6 +6846,8 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6830
6846
|
object({ t: literal("design-scan"), worktreeId: id }),
|
|
6831
6847
|
object({ t: literal("discard-files"), worktreeId: id, paths: array(relPath).min(1).max(500) }),
|
|
6832
6848
|
object({ t: literal("judge"), worktreeId: id, note: string2().max(500).optional() }),
|
|
6849
|
+
object({ t: literal("fix-preview"), worktreeId: id, clientId: string2().max(64).optional() }),
|
|
6850
|
+
object({ t: literal("fix"), worktreeId: id, toolId: string2().min(1).max(200) }),
|
|
6833
6851
|
object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
|
|
6834
6852
|
object({ t: literal("stop-agent"), worktreeId: id }),
|
|
6835
6853
|
object({ t: literal("seen"), worktreeId: id }),
|
|
@@ -6888,6 +6906,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6888
6906
|
object({ t: literal("set-theme"), prefs: themePrefsSchema }),
|
|
6889
6907
|
object({ t: literal("zone"), tz: string2().max(100) }),
|
|
6890
6908
|
object({ t: literal("set-default-agent"), agent: id }),
|
|
6909
|
+
object({ t: literal("set-keep-awake"), mode: _enum(KEEP_AWAKE_MODES) }),
|
|
6891
6910
|
object({ t: literal("install-agent"), agent: id }),
|
|
6892
6911
|
object({
|
|
6893
6912
|
t: literal("agent-auth"),
|
|
@@ -8201,7 +8220,7 @@ function safeUser() {
|
|
|
8201
8220
|
// ../daemon/package.json
|
|
8202
8221
|
var package_default = {
|
|
8203
8222
|
name: "@toyon/daemon",
|
|
8204
|
-
version: "0.
|
|
8223
|
+
version: "0.10.0",
|
|
8205
8224
|
private: true,
|
|
8206
8225
|
type: "module",
|
|
8207
8226
|
main: "src/index.ts",
|
|
@@ -13214,8 +13233,9 @@ async function runLive(cmd, args, cwd, opts = {}) {
|
|
|
13214
13233
|
}
|
|
13215
13234
|
}
|
|
13216
13235
|
function watchHooks(stream, onHook) {
|
|
13236
|
+
let failed;
|
|
13217
13237
|
if (!stream)
|
|
13218
|
-
return;
|
|
13238
|
+
return () => failed;
|
|
13219
13239
|
const dec = new TextDecoder;
|
|
13220
13240
|
let buf = "";
|
|
13221
13241
|
const open = new Map;
|
|
@@ -13236,11 +13256,15 @@ function watchHooks(stream, onHook) {
|
|
|
13236
13256
|
if (ev.event === "child_start" && ev.child_class === "hook" && ev.hook_name) {
|
|
13237
13257
|
open.set(ev.child_id, ev.hook_name);
|
|
13238
13258
|
onHook(ev.hook_name);
|
|
13239
|
-
} else if (ev.event === "child_exit" && open.
|
|
13259
|
+
} else if (ev.event === "child_exit" && open.has(ev.child_id)) {
|
|
13260
|
+
if (ev.code)
|
|
13261
|
+
failed = open.get(ev.child_id);
|
|
13262
|
+
open.delete(ev.child_id);
|
|
13240
13263
|
onHook(open.size > 0 ? [...open.values()].pop() : undefined);
|
|
13241
13264
|
}
|
|
13242
13265
|
}
|
|
13243
13266
|
});
|
|
13267
|
+
return () => failed;
|
|
13244
13268
|
}
|
|
13245
13269
|
function runWatched(cmd, args, cwd, opts = {}) {
|
|
13246
13270
|
const started = Date.now();
|
|
@@ -13285,8 +13309,7 @@ function runWatched(cmd, args, cwd, opts = {}) {
|
|
|
13285
13309
|
};
|
|
13286
13310
|
if (child.pid)
|
|
13287
13311
|
opts.onSpawn?.(child.pid, kill);
|
|
13288
|
-
|
|
13289
|
-
watchHooks(child.stdio[3], opts.onHook);
|
|
13312
|
+
const failedHook = opts.onHook ? watchHooks(child.stdio[3], opts.onHook) : undefined;
|
|
13290
13313
|
if (opts.timeoutMs) {
|
|
13291
13314
|
timer = setTimeout(() => {
|
|
13292
13315
|
timedOut = true;
|
|
@@ -13322,7 +13345,8 @@ function runWatched(cmd, args, cwd, opts = {}) {
|
|
|
13322
13345
|
err: err.join("").trim(),
|
|
13323
13346
|
exit,
|
|
13324
13347
|
text,
|
|
13325
|
-
...timedOut && opts.timeoutMs ? { ceilingMs: opts.timeoutMs } : {}
|
|
13348
|
+
...timedOut && opts.timeoutMs ? { ceilingMs: opts.timeoutMs } : {},
|
|
13349
|
+
...failedHook?.() ? { hook: failedHook() } : {}
|
|
13326
13350
|
});
|
|
13327
13351
|
});
|
|
13328
13352
|
});
|
|
@@ -14051,6 +14075,12 @@ ${body}
|
|
|
14051
14075
|
return parts.join(`
|
|
14052
14076
|
`);
|
|
14053
14077
|
}
|
|
14078
|
+
function unformatOutput(output) {
|
|
14079
|
+
if (!output.startsWith("```\n"))
|
|
14080
|
+
return "";
|
|
14081
|
+
const close = output.lastIndexOf("\n```");
|
|
14082
|
+
return close < 4 ? "" : output.slice(4, close);
|
|
14083
|
+
}
|
|
14054
14084
|
|
|
14055
14085
|
// ../daemon/src/agent/policy.ts
|
|
14056
14086
|
import { isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
|
|
@@ -14159,6 +14189,7 @@ var SYSTEM_APPEND = [
|
|
|
14159
14189
|
"Never run `git commit` unless the user explicitly asks you to, and never offer to, through the question tool or otherwise; leave changes uncommitted for the user to review and commit themselves.",
|
|
14160
14190
|
"Keep the scope tight: do the asked task well, then stop. A follow-up the user might want later is a sentence in your reply, not a question.",
|
|
14161
14191
|
"When the work is done and the only thing left is whether to do something you recommend, do not end your turn on a prose offer waiting for a yes. Ask it with your question tool when you have one: one question, two options with the recommended one first, each saying what it changes, and typed text allowed so the answer can carry instructions. The card is the first thing the user reads, before any prose of yours, so the question itself states what you propose and why in a sentence or two; a question that is only the yes-or-no leaves them guessing what it is. Ask this way only where you would otherwise have stopped and waited; never to confirm work already asked for.",
|
|
14192
|
+
"An answer to a question of yours can carry typed text beside the option picked, and the text outranks the pick. Text that doubts the pick, asks you something or says the user cannot decide is not a yes: answer it in prose and change nothing until they have decided. Text that only adds detail to the pick is an instruction for carrying it out.",
|
|
14162
14193
|
"When referencing a file in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor, whether it is in this worktree or anywhere else on this machine. A folder, a URL route or a command is not a file: name it in backticks, never as a link.",
|
|
14163
14194
|
"`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
|
|
14164
14195
|
'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.',
|
|
@@ -14166,6 +14197,113 @@ var SYSTEM_APPEND = [
|
|
|
14166
14197
|
"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.",
|
|
14167
14198
|
"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."
|
|
14168
14199
|
].join(" ");
|
|
14200
|
+
function failedRun(f) {
|
|
14201
|
+
switch (f.kind) {
|
|
14202
|
+
case "hook":
|
|
14203
|
+
case "check":
|
|
14204
|
+
case "command":
|
|
14205
|
+
return f;
|
|
14206
|
+
case "conflict":
|
|
14207
|
+
return f.step;
|
|
14208
|
+
case "preview":
|
|
14209
|
+
return;
|
|
14210
|
+
}
|
|
14211
|
+
}
|
|
14212
|
+
function fixPrompt(f) {
|
|
14213
|
+
switch (f.kind) {
|
|
14214
|
+
case "hook":
|
|
14215
|
+
return hookFixPrompt(f.hook);
|
|
14216
|
+
case "conflict":
|
|
14217
|
+
return conflictFixPrompt(f.base, f.how, !!f.step);
|
|
14218
|
+
case "check":
|
|
14219
|
+
return checkFixPrompt(f.command);
|
|
14220
|
+
case "command":
|
|
14221
|
+
return commandFixPrompt(f.command);
|
|
14222
|
+
case "preview":
|
|
14223
|
+
return procFixPrompt(f.procs, f.log);
|
|
14224
|
+
}
|
|
14225
|
+
}
|
|
14226
|
+
var WHY_COMMAND_CHARS = 60;
|
|
14227
|
+
function fixWhy(f) {
|
|
14228
|
+
switch (f.kind) {
|
|
14229
|
+
case "hook":
|
|
14230
|
+
return `the ${f.hook} hook refused the ${f.hook.includes("push") ? "push" : "commit"}`;
|
|
14231
|
+
case "conflict":
|
|
14232
|
+
return f.how === "rebase" ? `the branch needs a rebase onto ${f.base}` : `the branch needs ${f.base} merged in`;
|
|
14233
|
+
case "check":
|
|
14234
|
+
return "the check failed";
|
|
14235
|
+
case "command": {
|
|
14236
|
+
const line = f.command.split(`
|
|
14237
|
+
`)[0] ?? "";
|
|
14238
|
+
return `\`${line.length > WHY_COMMAND_CHARS ? `${line.slice(0, WHY_COMMAND_CHARS)}...` : line}\` failed`;
|
|
14239
|
+
}
|
|
14240
|
+
case "preview":
|
|
14241
|
+
return "the dev server is not reachable";
|
|
14242
|
+
}
|
|
14243
|
+
}
|
|
14244
|
+
function failureContext(f) {
|
|
14245
|
+
const run = failedRun(f);
|
|
14246
|
+
if (!run)
|
|
14247
|
+
return;
|
|
14248
|
+
const shown = clipOutput(run.text.replace(/\n+$/, ""));
|
|
14249
|
+
const what = f.kind === "command" ? "The command that failed" : "What Toyon ran";
|
|
14250
|
+
return `${what}, and what it printed:
|
|
14251
|
+
$ ${run.command}${shown ? `
|
|
14252
|
+
${shown}` : `
|
|
14253
|
+
(it printed nothing)`}`;
|
|
14254
|
+
}
|
|
14255
|
+
function hookFixPrompt(hook) {
|
|
14256
|
+
return [
|
|
14257
|
+
`The ${hook} hook refused that, and what it printed is attached below.`,
|
|
14258
|
+
"Fix what it complains about, inside this worktree.",
|
|
14259
|
+
"Do not skip the hook, do not edit it or its configuration to get past it, and do not commit: Toyon runs the check when your turn ends, and the user lands from there."
|
|
14260
|
+
].join(" ");
|
|
14261
|
+
}
|
|
14262
|
+
function conflictFixPrompt(base, how, shown) {
|
|
14263
|
+
const ask = how === "rebase" ? `Rebase this branch onto ${base} and resolve the conflicts, keeping any uncommitted changes, then verify the app still works.` : `Merge ${base} into this branch, resolve the conflicts and commit the merge, then verify the app still works.`;
|
|
14264
|
+
return shown ? `${ask} What git printed when Toyon tried is attached below.` : ask;
|
|
14265
|
+
}
|
|
14266
|
+
function checkFixPrompt(command) {
|
|
14267
|
+
return [
|
|
14268
|
+
`The repo's check, \`${command}\`, failed, and what it printed is attached below.`,
|
|
14269
|
+
"Fix what it reports, inside this worktree.",
|
|
14270
|
+
"Do not weaken the check or its configuration to get past it. Toyon runs it again when your turn ends."
|
|
14271
|
+
].join(" ");
|
|
14272
|
+
}
|
|
14273
|
+
function commandFixPrompt(command) {
|
|
14274
|
+
return [
|
|
14275
|
+
`The user ran \`${command}\` and it failed; what it printed is attached below.`,
|
|
14276
|
+
"Find the cause and fix it, inside this worktree.",
|
|
14277
|
+
"If the cause is not in this worktree, say what it is instead of changing anything."
|
|
14278
|
+
].join(" ");
|
|
14279
|
+
}
|
|
14280
|
+
var PROC_TAIL = 40;
|
|
14281
|
+
function procFixPrompt(procs, log) {
|
|
14282
|
+
const bad = procs.filter((p) => p.status === "crashed" || p.status === "unreachable");
|
|
14283
|
+
const lines = bad.map((p) => `- \`${p.name}\`: \`${p.command}\`, started with PORT=${p.port}. ${procDiagnosis(p)}`);
|
|
14284
|
+
const tail = log.slice(-PROC_TAIL).map((l) => `[${l.proc}] ${l.line}`).join(`
|
|
14285
|
+
`);
|
|
14286
|
+
return [
|
|
14287
|
+
"The dev server in this worktree is not reachable, so the preview is empty.",
|
|
14288
|
+
"",
|
|
14289
|
+
...lines,
|
|
14290
|
+
"",
|
|
14291
|
+
tail ? `Last output:
|
|
14292
|
+
\`\`\`
|
|
14293
|
+
${tail}
|
|
14294
|
+
\`\`\`` : "It produced no output.",
|
|
14295
|
+
"",
|
|
14296
|
+
"Find the cause and fix it. The command must run in the foreground and listen on the port in the PORT environment variable, which Toyon sets differently for each worktree. If the tool takes its port from a flag instead (Vite does), make the app read PORT, for Vite `server.port: Number(process.env.PORT)` with `strictPort: true`, or add the flag to the start command in toyon's settings file. When your turn ends toyon restarts the process and checks again."
|
|
14297
|
+
].join(`
|
|
14298
|
+
`);
|
|
14299
|
+
}
|
|
14300
|
+
function procDiagnosis(p) {
|
|
14301
|
+
if (p.detail)
|
|
14302
|
+
return p.detail;
|
|
14303
|
+
if (p.status === "crashed")
|
|
14304
|
+
return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
|
|
14305
|
+
return "It never answered on that port.";
|
|
14306
|
+
}
|
|
14169
14307
|
function ambientBlock(paragraphs) {
|
|
14170
14308
|
const said = paragraphs.filter((p) => !!p);
|
|
14171
14309
|
if (said.length === 0)
|
|
@@ -14185,7 +14323,7 @@ function previewContext(p) {
|
|
|
14185
14323
|
line = "Toyon is setting this worktree up and starts the preview when that is done; the next message will say where it answers.";
|
|
14186
14324
|
break;
|
|
14187
14325
|
case "parked":
|
|
14188
|
-
line = "The preview is off:
|
|
14326
|
+
line = "The preview is off: Toyon has not started this worktree's dev servers, and starts them when you first write a file or when the user asks for them. Nothing answers until then.";
|
|
14189
14327
|
break;
|
|
14190
14328
|
case "starting":
|
|
14191
14329
|
line = `Toyon is starting the preview; it answers${at} once it is up.`;
|
|
@@ -14822,12 +14960,18 @@ function release(toolId, memo) {
|
|
|
14822
14960
|
memo.writing = false;
|
|
14823
14961
|
return memo.ended ? null : { type: "tool-update", toolId, ...held };
|
|
14824
14962
|
}
|
|
14963
|
+
function preempted(memos) {
|
|
14964
|
+
for (const memo of memos.values()) {
|
|
14965
|
+
if (!memo.ended && memo.writing && !memo.parent)
|
|
14966
|
+
memo.cut = true;
|
|
14967
|
+
}
|
|
14968
|
+
}
|
|
14825
14969
|
function abandoned(memos, parent, opening = false) {
|
|
14826
14970
|
const out = [];
|
|
14827
14971
|
for (const [toolId, memo] of memos) {
|
|
14828
14972
|
if (memo.ended || !memo.writing || memo.parent !== parent)
|
|
14829
14973
|
continue;
|
|
14830
|
-
if (opening && memo.held) {
|
|
14974
|
+
if (opening && memo.held && !memo.cut) {
|
|
14831
14975
|
const sent = release(toolId, memo);
|
|
14832
14976
|
if (sent)
|
|
14833
14977
|
out.push(sent);
|
|
@@ -15107,6 +15251,7 @@ class AcpSession {
|
|
|
15107
15251
|
held = false;
|
|
15108
15252
|
conn = null;
|
|
15109
15253
|
connecting = null;
|
|
15254
|
+
opening = null;
|
|
15110
15255
|
live = null;
|
|
15111
15256
|
reaper = null;
|
|
15112
15257
|
own = null;
|
|
@@ -15243,13 +15388,18 @@ class AcpSession {
|
|
|
15243
15388
|
send(text, opts = {}) {
|
|
15244
15389
|
if (this.stopped)
|
|
15245
15390
|
return log.warn(this.d.worktreeId, "send after close dropped");
|
|
15246
|
-
|
|
15247
|
-
ask.
|
|
15391
|
+
if (!opts.asked)
|
|
15392
|
+
for (const ask of [...this.asks.values()])
|
|
15393
|
+
ask.settle("skipped");
|
|
15248
15394
|
this.post(text, opts);
|
|
15249
15395
|
}
|
|
15250
15396
|
post(text, opts = {}) {
|
|
15251
|
-
const { context, attachments } = opts;
|
|
15397
|
+
const { context, attachments, asked } = opts;
|
|
15252
15398
|
const item = { text, context, ...attachments?.length ? { attachments } : {} };
|
|
15399
|
+
if (asked) {
|
|
15400
|
+
this.emit({ type: "fix-asked", text, ts: Date.now(), ...asked });
|
|
15401
|
+
item.recorded = { attachments: [] };
|
|
15402
|
+
}
|
|
15253
15403
|
this.d.attachments.uploads.hold(uploadIds(attachments));
|
|
15254
15404
|
if ((this.running || this.own) && !this.interrupted && !this.held && this.live?.conn.steering) {
|
|
15255
15405
|
return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
|
|
@@ -15305,7 +15455,10 @@ class AcpSession {
|
|
|
15305
15455
|
}
|
|
15306
15456
|
this.clearReaper();
|
|
15307
15457
|
log.warn(this.d.worktreeId, "steered message started a turn of the agent's own");
|
|
15458
|
+
return;
|
|
15308
15459
|
}
|
|
15460
|
+
if (this.live === live)
|
|
15461
|
+
preempted(live.tools);
|
|
15309
15462
|
}
|
|
15310
15463
|
unsteer(item) {
|
|
15311
15464
|
const at = this.steered.indexOf(item);
|
|
@@ -15609,8 +15762,18 @@ class AcpSession {
|
|
|
15609
15762
|
}
|
|
15610
15763
|
async ensureLive(why) {
|
|
15611
15764
|
const conn = await this.ensureConn(why);
|
|
15765
|
+
if (this.opening?.conn === conn)
|
|
15766
|
+
return this.opening.live;
|
|
15612
15767
|
if (this.live?.conn === conn)
|
|
15613
15768
|
return this.live;
|
|
15769
|
+
const live = this.openLive(conn).finally(() => {
|
|
15770
|
+
if (this.opening?.live === live)
|
|
15771
|
+
this.opening = null;
|
|
15772
|
+
});
|
|
15773
|
+
this.opening = { conn, live };
|
|
15774
|
+
return live;
|
|
15775
|
+
}
|
|
15776
|
+
async openLive(conn) {
|
|
15614
15777
|
const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
|
|
15615
15778
|
let sessionId = this.d.getSessionId();
|
|
15616
15779
|
let modes;
|
|
@@ -15651,7 +15814,7 @@ class AcpSession {
|
|
|
15651
15814
|
this.emit({ type: "session-info", sessionId, ...currentValues(options) });
|
|
15652
15815
|
this.learn(options);
|
|
15653
15816
|
const modeOption = modes ? null : readModeOption(configOptions);
|
|
15654
|
-
|
|
15817
|
+
const live = {
|
|
15655
15818
|
conn,
|
|
15656
15819
|
sessionId,
|
|
15657
15820
|
prefixPending: !resumed && conn.spec.systemPrompt === "prompt-prefix",
|
|
@@ -15661,13 +15824,14 @@ class AcpSession {
|
|
|
15661
15824
|
modeConfigId: modeOption?.id ?? null,
|
|
15662
15825
|
options
|
|
15663
15826
|
};
|
|
15664
|
-
|
|
15665
|
-
await this.
|
|
15666
|
-
await this.applyOption(
|
|
15827
|
+
this.live = live;
|
|
15828
|
+
await this.applyMode(live);
|
|
15829
|
+
await this.applyOption(live, "model");
|
|
15830
|
+
await this.applyOption(live, "thought_level");
|
|
15667
15831
|
const pushed = conn.commands.get(sessionId);
|
|
15668
15832
|
if (pushed)
|
|
15669
15833
|
this.setCommands(pushed);
|
|
15670
|
-
return
|
|
15834
|
+
return live;
|
|
15671
15835
|
}
|
|
15672
15836
|
mode() {
|
|
15673
15837
|
return this.d.mode?.() ?? DEFAULT_PERMISSION_MODE;
|
|
@@ -15767,7 +15931,7 @@ class AcpSession {
|
|
|
15767
15931
|
if (!memo || !held)
|
|
15768
15932
|
return;
|
|
15769
15933
|
const t = setTimeout(() => {
|
|
15770
|
-
if (this.live !== live || memo.held !== held || !(this.running || this.own))
|
|
15934
|
+
if (this.live !== live || memo.held !== held || memo.cut || !(this.running || this.own))
|
|
15771
15935
|
return;
|
|
15772
15936
|
const sent = release(toolId, memo);
|
|
15773
15937
|
if (sent)
|
|
@@ -17517,6 +17681,18 @@ class RuntimeRegistry {
|
|
|
17517
17681
|
return true;
|
|
17518
17682
|
return false;
|
|
17519
17683
|
}
|
|
17684
|
+
demand() {
|
|
17685
|
+
if (this.holds.size > 0)
|
|
17686
|
+
return "working";
|
|
17687
|
+
let waiting = false;
|
|
17688
|
+
for (const rt of this.runtimes.values()) {
|
|
17689
|
+
if (rt.agent.status === "working")
|
|
17690
|
+
return "working";
|
|
17691
|
+
if (rt.agent.status === "waiting")
|
|
17692
|
+
waiting = true;
|
|
17693
|
+
}
|
|
17694
|
+
return waiting ? "waiting" : null;
|
|
17695
|
+
}
|
|
17520
17696
|
markSetup(id, on) {
|
|
17521
17697
|
if (on)
|
|
17522
17698
|
this.settingUp.add(id);
|
|
@@ -18193,18 +18369,20 @@ function clip(text, max = CLIP) {
|
|
|
18193
18369
|
const line = text.replace(/\s+/g, " ").trim();
|
|
18194
18370
|
return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
|
|
18195
18371
|
}
|
|
18196
|
-
function slice(asks, ts) {
|
|
18197
|
-
return { asks, reply: "", edits: 0, toolErrors: 0, newestTs: ts };
|
|
18372
|
+
function slice(asks, fixes, ts) {
|
|
18373
|
+
return { asks, fixes, reply: "", edits: 0, toolErrors: 0, newestTs: ts };
|
|
18198
18374
|
}
|
|
18199
18375
|
function turnsSince(entries, seenAt) {
|
|
18200
18376
|
const turns = [];
|
|
18201
18377
|
let pending = [];
|
|
18378
|
+
let pendingFixes = [];
|
|
18202
18379
|
let open = null;
|
|
18203
18380
|
const writes = new Set;
|
|
18204
18381
|
const refusals = new Map;
|
|
18205
18382
|
const start = (ts) => {
|
|
18206
|
-
const t = slice(pending, ts);
|
|
18383
|
+
const t = slice(pending, pendingFixes, ts);
|
|
18207
18384
|
pending = [];
|
|
18385
|
+
pendingFixes = [];
|
|
18208
18386
|
turns.push(t);
|
|
18209
18387
|
return t;
|
|
18210
18388
|
};
|
|
@@ -18212,12 +18390,16 @@ function turnsSince(entries, seenAt) {
|
|
|
18212
18390
|
if (open && "ts" in e)
|
|
18213
18391
|
open.newestTs = Math.max(open.newestTs, e.ts);
|
|
18214
18392
|
switch (e.type) {
|
|
18215
|
-
case "user-message":
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
else
|
|
18219
|
-
pending.push(e.text);
|
|
18393
|
+
case "user-message": {
|
|
18394
|
+
const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
|
|
18395
|
+
(into?.asks ?? pending).push(e.text);
|
|
18220
18396
|
break;
|
|
18397
|
+
}
|
|
18398
|
+
case "fix-asked": {
|
|
18399
|
+
const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
|
|
18400
|
+
(into?.fixes ?? pendingFixes).push(e.why);
|
|
18401
|
+
break;
|
|
18402
|
+
}
|
|
18221
18403
|
case "turn-start":
|
|
18222
18404
|
open = start(e.ts);
|
|
18223
18405
|
break;
|
|
@@ -18378,6 +18560,8 @@ function turnBlock(t) {
|
|
|
18378
18560
|
const lines = [];
|
|
18379
18561
|
if (t.asks.length)
|
|
18380
18562
|
lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
|
|
18563
|
+
if (t.fixes.length)
|
|
18564
|
+
lines.push(`Toyon, not the user, asked for a fix: ${clip(t.fixes.join(" / "), 400)}`);
|
|
18381
18565
|
if (t.reply.trim())
|
|
18382
18566
|
lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
|
|
18383
18567
|
return lines.join(`
|
|
@@ -18617,93 +18801,8 @@ function pruneAssets(shellDist) {
|
|
|
18617
18801
|
return gone;
|
|
18618
18802
|
}
|
|
18619
18803
|
|
|
18620
|
-
// ../daemon/src/core/
|
|
18621
|
-
import {
|
|
18622
|
-
import { homedir as homedir5 } from "os";
|
|
18623
|
-
var PROMPTS = {
|
|
18624
|
-
location: "Choose where the new project goes",
|
|
18625
|
-
open: "Choose a project folder to open"
|
|
18626
|
-
};
|
|
18627
|
-
var SCRIPT = [
|
|
18628
|
-
"on run argv",
|
|
18629
|
-
"activate",
|
|
18630
|
-
"POSIX path of (choose folder with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))",
|
|
18631
|
-
"end run"
|
|
18632
|
-
];
|
|
18633
|
-
function folderDialog() {
|
|
18634
|
-
let open = null;
|
|
18635
|
-
let child = null;
|
|
18636
|
-
let cancelled = false;
|
|
18637
|
-
const ask = async (start, purpose) => {
|
|
18638
|
-
const typed = start === "~" || start.startsWith("~/") ? `${homedir5()}${start.slice(1)}` : start;
|
|
18639
|
-
const from = typed.startsWith("/") && existsSync13(typed) ? typed : homedir5();
|
|
18640
|
-
cancelled = false;
|
|
18641
|
-
const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
|
|
18642
|
-
stdout: "pipe",
|
|
18643
|
-
stderr: "pipe",
|
|
18644
|
-
stdin: "ignore"
|
|
18645
|
-
});
|
|
18646
|
-
child = p;
|
|
18647
|
-
const [out, err, code] = await Promise.all([
|
|
18648
|
-
new Response(p.stdout).text(),
|
|
18649
|
-
new Response(p.stderr).text(),
|
|
18650
|
-
p.exited
|
|
18651
|
-
]);
|
|
18652
|
-
child = null;
|
|
18653
|
-
if (code === 0)
|
|
18654
|
-
return out.trim().replace(/\/+$/, "") || "/";
|
|
18655
|
-
if (cancelled || err.includes("(-128)"))
|
|
18656
|
-
return null;
|
|
18657
|
-
throw new UserError(`the Finder dialog did not open: ${err.trim()}`);
|
|
18658
|
-
};
|
|
18659
|
-
return {
|
|
18660
|
-
choose(start, purpose) {
|
|
18661
|
-
if (process.platform !== "darwin") {
|
|
18662
|
-
return Promise.reject(new UserError("choosing a folder in Finder is only available on macOS"));
|
|
18663
|
-
}
|
|
18664
|
-
if (!open) {
|
|
18665
|
-
open = ask(start, purpose).finally(() => {
|
|
18666
|
-
open = null;
|
|
18667
|
-
});
|
|
18668
|
-
}
|
|
18669
|
-
return open;
|
|
18670
|
-
},
|
|
18671
|
-
cancel() {
|
|
18672
|
-
if (!child)
|
|
18673
|
-
return;
|
|
18674
|
-
cancelled = true;
|
|
18675
|
-
child.kill();
|
|
18676
|
-
}
|
|
18677
|
-
};
|
|
18678
|
-
}
|
|
18679
|
-
|
|
18680
|
-
// ../daemon/src/core/hub.ts
|
|
18681
|
-
class Hub {
|
|
18682
|
-
listeners = new Map;
|
|
18683
|
-
on(event, fn) {
|
|
18684
|
-
let set = this.listeners.get(event);
|
|
18685
|
-
if (!set) {
|
|
18686
|
-
set = new Set;
|
|
18687
|
-
this.listeners.set(event, set);
|
|
18688
|
-
}
|
|
18689
|
-
set.add(fn);
|
|
18690
|
-
return () => set?.delete(fn);
|
|
18691
|
-
}
|
|
18692
|
-
emit(event, ...args) {
|
|
18693
|
-
const set = this.listeners.get(event);
|
|
18694
|
-
if (!set)
|
|
18695
|
-
return;
|
|
18696
|
-
for (const fn of set) {
|
|
18697
|
-
try {
|
|
18698
|
-
const r = fn(...args);
|
|
18699
|
-
if (r instanceof Promise)
|
|
18700
|
-
r.catch((e) => log.error("hub", `async listener for ${String(event)} rejected`, e));
|
|
18701
|
-
} catch (e) {
|
|
18702
|
-
log.error("hub", `listener for ${String(event)} threw`, e);
|
|
18703
|
-
}
|
|
18704
|
-
}
|
|
18705
|
-
}
|
|
18706
|
-
}
|
|
18804
|
+
// ../daemon/src/core/awake.ts
|
|
18805
|
+
import { spawn as spawn3 } from "child_process";
|
|
18707
18806
|
|
|
18708
18807
|
// ../daemon/src/runtime/idle.ts
|
|
18709
18808
|
var LOCAL_SLEEP_MS = 2 * 60 * 60000;
|
|
@@ -19011,6 +19110,208 @@ function humanMs(ms) {
|
|
|
19011
19110
|
return `${Math.round(ms / 1000)} s`;
|
|
19012
19111
|
}
|
|
19013
19112
|
|
|
19113
|
+
// ../daemon/src/core/awake.ts
|
|
19114
|
+
var WAIT_MS = 30 * 60000;
|
|
19115
|
+
function idleSleepAssertion(raw, platform = process.platform) {
|
|
19116
|
+
if (raw === "off" || platform !== "darwin")
|
|
19117
|
+
return null;
|
|
19118
|
+
return () => {
|
|
19119
|
+
const child = spawn3("/usr/bin/caffeinate", ["-i", "-w", String(process.pid)], { stdio: "ignore" });
|
|
19120
|
+
child.on("error", (e) => log.warn("awake", "caffeinate did not start, so the Mac may sleep mid-turn", e));
|
|
19121
|
+
child.unref();
|
|
19122
|
+
return () => {
|
|
19123
|
+
child.kill();
|
|
19124
|
+
};
|
|
19125
|
+
};
|
|
19126
|
+
}
|
|
19127
|
+
|
|
19128
|
+
class KeepAwake {
|
|
19129
|
+
d;
|
|
19130
|
+
release = null;
|
|
19131
|
+
waitingAt = null;
|
|
19132
|
+
remote = 0;
|
|
19133
|
+
remoteLeftAt = null;
|
|
19134
|
+
timer = null;
|
|
19135
|
+
gone = false;
|
|
19136
|
+
waitMs;
|
|
19137
|
+
now;
|
|
19138
|
+
constructor(d) {
|
|
19139
|
+
this.d = d;
|
|
19140
|
+
this.waitMs = d.waitMs ?? WAIT_MS;
|
|
19141
|
+
this.now = d.now ?? Date.now;
|
|
19142
|
+
d.hub.on("agentStatus", (_id, status) => {
|
|
19143
|
+
if (status === "waiting")
|
|
19144
|
+
this.waitingAt = this.now();
|
|
19145
|
+
this.reconsider();
|
|
19146
|
+
});
|
|
19147
|
+
d.hub.on("holdsChanged", () => this.reconsider());
|
|
19148
|
+
d.hub.on("keepAwakeChanged", () => this.reconsider());
|
|
19149
|
+
this.reconsider();
|
|
19150
|
+
}
|
|
19151
|
+
setting() {
|
|
19152
|
+
return this.d.assert ? this.d.mode() : null;
|
|
19153
|
+
}
|
|
19154
|
+
remoteShells(n) {
|
|
19155
|
+
if (n === this.remote)
|
|
19156
|
+
return;
|
|
19157
|
+
if (n === 0)
|
|
19158
|
+
this.remoteLeftAt = this.now();
|
|
19159
|
+
this.remote = n;
|
|
19160
|
+
this.reconsider();
|
|
19161
|
+
}
|
|
19162
|
+
stop() {
|
|
19163
|
+
this.gone = true;
|
|
19164
|
+
this.disarm();
|
|
19165
|
+
this.hold(false);
|
|
19166
|
+
}
|
|
19167
|
+
reconsider() {
|
|
19168
|
+
if (this.gone || !this.d.assert)
|
|
19169
|
+
return;
|
|
19170
|
+
this.disarm();
|
|
19171
|
+
const mode = this.d.mode();
|
|
19172
|
+
const demand = this.d.demand();
|
|
19173
|
+
if (demand !== "waiting" || !this.d.answerable)
|
|
19174
|
+
this.waitingAt = null;
|
|
19175
|
+
else
|
|
19176
|
+
this.waitingAt ??= this.now();
|
|
19177
|
+
if (mode === "off") {
|
|
19178
|
+
this.hold(false);
|
|
19179
|
+
return;
|
|
19180
|
+
}
|
|
19181
|
+
if (mode === "always" || demand === "working" || this.remote > 0) {
|
|
19182
|
+
this.hold(true);
|
|
19183
|
+
return;
|
|
19184
|
+
}
|
|
19185
|
+
const since = Math.max(this.waitingAt ?? Number.NEGATIVE_INFINITY, this.remoteLeftAt ?? Number.NEGATIVE_INFINITY);
|
|
19186
|
+
const left = since + this.waitMs - this.now();
|
|
19187
|
+
if (left > 0) {
|
|
19188
|
+
this.hold(true);
|
|
19189
|
+
this.arm(left);
|
|
19190
|
+
return;
|
|
19191
|
+
}
|
|
19192
|
+
if (this.release && Number.isFinite(since)) {
|
|
19193
|
+
log.info("awake", `nobody has answered or connected for ${humanMs(this.waitMs)}; the machine may sleep again`);
|
|
19194
|
+
}
|
|
19195
|
+
this.hold(false);
|
|
19196
|
+
}
|
|
19197
|
+
hold(on) {
|
|
19198
|
+
if (on === (this.release !== null))
|
|
19199
|
+
return;
|
|
19200
|
+
if (on) {
|
|
19201
|
+
this.release = this.d.assert?.() ?? null;
|
|
19202
|
+
} else {
|
|
19203
|
+
this.release?.();
|
|
19204
|
+
this.release = null;
|
|
19205
|
+
}
|
|
19206
|
+
log.debug("awake", on ? "holding off idle sleep" : "idle sleep allowed again");
|
|
19207
|
+
}
|
|
19208
|
+
arm(ms) {
|
|
19209
|
+
const set = this.d.setTimer ?? ((fn, delay) => {
|
|
19210
|
+
const t = setTimeout(fn, delay);
|
|
19211
|
+
t.unref?.();
|
|
19212
|
+
return t;
|
|
19213
|
+
});
|
|
19214
|
+
this.timer = set(() => {
|
|
19215
|
+
this.timer = null;
|
|
19216
|
+
this.reconsider();
|
|
19217
|
+
}, ms);
|
|
19218
|
+
}
|
|
19219
|
+
disarm() {
|
|
19220
|
+
if (!this.timer)
|
|
19221
|
+
return;
|
|
19222
|
+
(this.d.clearTimer ?? ((t) => clearTimeout(t)))(this.timer);
|
|
19223
|
+
this.timer = null;
|
|
19224
|
+
}
|
|
19225
|
+
}
|
|
19226
|
+
|
|
19227
|
+
// ../daemon/src/core/dialog.ts
|
|
19228
|
+
import { existsSync as existsSync13 } from "fs";
|
|
19229
|
+
import { homedir as homedir5 } from "os";
|
|
19230
|
+
var PROMPTS = {
|
|
19231
|
+
location: "Choose where the new project goes",
|
|
19232
|
+
open: "Choose a project folder to open"
|
|
19233
|
+
};
|
|
19234
|
+
var SCRIPT = [
|
|
19235
|
+
"on run argv",
|
|
19236
|
+
"activate",
|
|
19237
|
+
"POSIX path of (choose folder with prompt (item 1 of argv) default location (POSIX file (item 2 of argv)))",
|
|
19238
|
+
"end run"
|
|
19239
|
+
];
|
|
19240
|
+
function folderDialog() {
|
|
19241
|
+
let open = null;
|
|
19242
|
+
let child = null;
|
|
19243
|
+
let cancelled = false;
|
|
19244
|
+
const ask = async (start, purpose) => {
|
|
19245
|
+
const typed = start === "~" || start.startsWith("~/") ? `${homedir5()}${start.slice(1)}` : start;
|
|
19246
|
+
const from = typed.startsWith("/") && existsSync13(typed) ? typed : homedir5();
|
|
19247
|
+
cancelled = false;
|
|
19248
|
+
const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
|
|
19249
|
+
stdout: "pipe",
|
|
19250
|
+
stderr: "pipe",
|
|
19251
|
+
stdin: "ignore"
|
|
19252
|
+
});
|
|
19253
|
+
child = p;
|
|
19254
|
+
const [out, err, code] = await Promise.all([
|
|
19255
|
+
new Response(p.stdout).text(),
|
|
19256
|
+
new Response(p.stderr).text(),
|
|
19257
|
+
p.exited
|
|
19258
|
+
]);
|
|
19259
|
+
child = null;
|
|
19260
|
+
if (code === 0)
|
|
19261
|
+
return out.trim().replace(/\/+$/, "") || "/";
|
|
19262
|
+
if (cancelled || err.includes("(-128)"))
|
|
19263
|
+
return null;
|
|
19264
|
+
throw new UserError(`the Finder dialog did not open: ${err.trim()}`);
|
|
19265
|
+
};
|
|
19266
|
+
return {
|
|
19267
|
+
choose(start, purpose) {
|
|
19268
|
+
if (process.platform !== "darwin") {
|
|
19269
|
+
return Promise.reject(new UserError("choosing a folder in Finder is only available on macOS"));
|
|
19270
|
+
}
|
|
19271
|
+
if (!open) {
|
|
19272
|
+
open = ask(start, purpose).finally(() => {
|
|
19273
|
+
open = null;
|
|
19274
|
+
});
|
|
19275
|
+
}
|
|
19276
|
+
return open;
|
|
19277
|
+
},
|
|
19278
|
+
cancel() {
|
|
19279
|
+
if (!child)
|
|
19280
|
+
return;
|
|
19281
|
+
cancelled = true;
|
|
19282
|
+
child.kill();
|
|
19283
|
+
}
|
|
19284
|
+
};
|
|
19285
|
+
}
|
|
19286
|
+
|
|
19287
|
+
// ../daemon/src/core/hub.ts
|
|
19288
|
+
class Hub {
|
|
19289
|
+
listeners = new Map;
|
|
19290
|
+
on(event, fn) {
|
|
19291
|
+
let set = this.listeners.get(event);
|
|
19292
|
+
if (!set) {
|
|
19293
|
+
set = new Set;
|
|
19294
|
+
this.listeners.set(event, set);
|
|
19295
|
+
}
|
|
19296
|
+
set.add(fn);
|
|
19297
|
+
return () => set?.delete(fn);
|
|
19298
|
+
}
|
|
19299
|
+
emit(event, ...args) {
|
|
19300
|
+
const set = this.listeners.get(event);
|
|
19301
|
+
if (!set)
|
|
19302
|
+
return;
|
|
19303
|
+
for (const fn of set) {
|
|
19304
|
+
try {
|
|
19305
|
+
const r = fn(...args);
|
|
19306
|
+
if (r instanceof Promise)
|
|
19307
|
+
r.catch((e) => log.error("hub", `async listener for ${String(event)} rejected`, e));
|
|
19308
|
+
} catch (e) {
|
|
19309
|
+
log.error("hub", `listener for ${String(event)} threw`, e);
|
|
19310
|
+
}
|
|
19311
|
+
}
|
|
19312
|
+
}
|
|
19313
|
+
}
|
|
19314
|
+
|
|
19014
19315
|
// ../daemon/src/core/idleExit.ts
|
|
19015
19316
|
var STOP_AFTER_MS = 30 * 60000;
|
|
19016
19317
|
function stopAfterFrom(raw, opts) {
|
|
@@ -19100,7 +19401,7 @@ class IdleExit {
|
|
|
19100
19401
|
}
|
|
19101
19402
|
|
|
19102
19403
|
// ../daemon/src/core/restart.ts
|
|
19103
|
-
import { spawn as
|
|
19404
|
+
import { spawn as spawn4 } from "child_process";
|
|
19104
19405
|
import { openSync } from "fs";
|
|
19105
19406
|
import { homedir as homedir6 } from "os";
|
|
19106
19407
|
function restartable(env = process.env) {
|
|
@@ -19121,7 +19422,7 @@ async function replacementRuns() {
|
|
|
19121
19422
|
}
|
|
19122
19423
|
function respawn(logFile) {
|
|
19123
19424
|
const logFd = openSync(logFile, "a");
|
|
19124
|
-
const child =
|
|
19425
|
+
const child = spawn4(process.execPath, process.argv.slice(1), {
|
|
19125
19426
|
detached: true,
|
|
19126
19427
|
stdio: ["ignore", logFd, logFd],
|
|
19127
19428
|
env: { ...process.env }
|
|
@@ -19506,6 +19807,16 @@ class StateStore {
|
|
|
19506
19807
|
delete this.state.defaultAgent;
|
|
19507
19808
|
this.save();
|
|
19508
19809
|
}
|
|
19810
|
+
get keepAwake() {
|
|
19811
|
+
return this.state.keepAwake ?? "use";
|
|
19812
|
+
}
|
|
19813
|
+
setKeepAwake(mode) {
|
|
19814
|
+
if (mode === "use")
|
|
19815
|
+
delete this.state.keepAwake;
|
|
19816
|
+
else
|
|
19817
|
+
this.state.keepAwake = mode;
|
|
19818
|
+
this.save();
|
|
19819
|
+
}
|
|
19509
19820
|
get theme() {
|
|
19510
19821
|
return this.state.theme;
|
|
19511
19822
|
}
|
|
@@ -20315,12 +20626,13 @@ class ExecService {
|
|
|
20315
20626
|
if (opts.quiet)
|
|
20316
20627
|
agent.note(start);
|
|
20317
20628
|
agent.note({ type: "tool-end", toolId, output: `could not run: ${reason}`, isError: true });
|
|
20318
|
-
return Promise.resolve({ exit: reason, text: "" });
|
|
20629
|
+
return Promise.resolve({ exit: reason, text: "", toolId });
|
|
20319
20630
|
}
|
|
20320
20631
|
const pid = proc.pid;
|
|
20321
20632
|
let killing;
|
|
20322
20633
|
const running = {
|
|
20323
20634
|
stop: () => {
|
|
20635
|
+
running.stopping = true;
|
|
20324
20636
|
killing ??= killGroup(pid, groupGone(pid)).then((signal) => {
|
|
20325
20637
|
if (signal)
|
|
20326
20638
|
running.signal = signal;
|
|
@@ -20337,7 +20649,8 @@ class ExecService {
|
|
|
20337
20649
|
this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
|
|
20338
20650
|
this.deps.runtime.noteGroup(worktreeId, `exec:${toolId}`, pid);
|
|
20339
20651
|
opts.onSpawn?.(pid, () => fireAndForget(worktreeId, running.stop(), `exec ${toolId} stopped`));
|
|
20340
|
-
|
|
20652
|
+
const kind = name === CHECK_TOOL ? "check" : "command";
|
|
20653
|
+
return this.collect(worktreeId, toolId, proc, running, agent, ceiling, kind, opts.quiet ? start : undefined);
|
|
20341
20654
|
}
|
|
20342
20655
|
async watch(worktreeId, command, run) {
|
|
20343
20656
|
const agent = this.agentFor(worktreeId);
|
|
@@ -20375,16 +20688,17 @@ class ExecService {
|
|
|
20375
20688
|
this.untrack(worktreeId, toolId);
|
|
20376
20689
|
}
|
|
20377
20690
|
if (!up && r.exit === 0)
|
|
20378
|
-
return { ...r, shown: false };
|
|
20691
|
+
return { ...r, shown: false, toolId };
|
|
20379
20692
|
if (!up)
|
|
20380
20693
|
agent.note(start);
|
|
20381
20694
|
agent.note({
|
|
20382
20695
|
type: "tool-end",
|
|
20383
20696
|
toolId,
|
|
20384
20697
|
output: formatOutput(text, r.exit, truncated, r.ceilingMs),
|
|
20385
|
-
isError: r.exit !== 0
|
|
20698
|
+
isError: r.exit !== 0,
|
|
20699
|
+
...r.hook && typeof r.exit === "number" && r.exit !== 0 ? { fixable: { kind: "hook", hook: r.hook } } : {}
|
|
20386
20700
|
});
|
|
20387
|
-
return { ...r, shown: true };
|
|
20701
|
+
return { ...r, shown: true, toolId };
|
|
20388
20702
|
}
|
|
20389
20703
|
async stop(worktreeId, toolId) {
|
|
20390
20704
|
const byTool = this.running.get(worktreeId);
|
|
@@ -20417,7 +20731,7 @@ class ExecService {
|
|
|
20417
20731
|
this.deps.runtime.release(worktreeId, `exec:${toolId}`);
|
|
20418
20732
|
this.deps.runtime.forgetGroup(worktreeId, pgid);
|
|
20419
20733
|
}
|
|
20420
|
-
async collect(worktreeId, toolId, proc, running, agent, ceiling, heldStart) {
|
|
20734
|
+
async collect(worktreeId, toolId, proc, running, agent, ceiling, kind, heldStart) {
|
|
20421
20735
|
let text = "";
|
|
20422
20736
|
let truncated = false;
|
|
20423
20737
|
const live = !heldStart;
|
|
@@ -20455,7 +20769,13 @@ class ExecService {
|
|
|
20455
20769
|
log.warn(worktreeId, "exec: reading output failed", e);
|
|
20456
20770
|
}
|
|
20457
20771
|
const pgid = proc.pid;
|
|
20458
|
-
const end = (how) => agent.note({
|
|
20772
|
+
const end = (how) => agent.note({
|
|
20773
|
+
type: "tool-end",
|
|
20774
|
+
toolId,
|
|
20775
|
+
output: formatOutput(text, how, truncated, ceiling),
|
|
20776
|
+
isError: how !== 0,
|
|
20777
|
+
...typeof how === "number" && how !== 0 && !running.stopping && !running.timedOut ? { fixable: { kind } } : {}
|
|
20778
|
+
});
|
|
20459
20779
|
const lingers = running.signal === undefined && groupAlive(pgid);
|
|
20460
20780
|
if (!lingers)
|
|
20461
20781
|
this.settle(worktreeId, toolId, pgid);
|
|
@@ -20466,18 +20786,18 @@ class ExecService {
|
|
|
20466
20786
|
}
|
|
20467
20787
|
if (lingers)
|
|
20468
20788
|
fireAndForget(worktreeId, this.linger(running, pgid).then(() => this.settle(worktreeId, toolId, pgid)), `exec ${toolId} left running`);
|
|
20469
|
-
return { exit: running.timedOut ? "timeout" : exit, text };
|
|
20789
|
+
return { exit: running.timedOut ? "timeout" : exit, text, toolId };
|
|
20470
20790
|
}
|
|
20471
20791
|
if (!lingers) {
|
|
20472
20792
|
end(running.timedOut ? "timeout" : exit);
|
|
20473
|
-
return { exit: running.timedOut ? "timeout" : exit, text };
|
|
20793
|
+
return { exit: running.timedOut ? "timeout" : exit, text, toolId };
|
|
20474
20794
|
}
|
|
20475
20795
|
agent.note({ type: "tool-update", toolId, background: true });
|
|
20476
20796
|
fireAndForget(worktreeId, this.linger(running, pgid).then(() => {
|
|
20477
20797
|
this.settle(worktreeId, toolId, pgid);
|
|
20478
20798
|
end(running.timedOut ? "timeout" : running.signal ?? exit);
|
|
20479
20799
|
}), `exec ${toolId} in the background`);
|
|
20480
|
-
return { exit, text };
|
|
20800
|
+
return { exit, text, toolId };
|
|
20481
20801
|
}
|
|
20482
20802
|
async linger(running, pgid) {
|
|
20483
20803
|
while (running.signal === undefined && groupAlive(pgid))
|
|
@@ -20586,7 +20906,7 @@ function looksBinary(text) {
|
|
|
20586
20906
|
}
|
|
20587
20907
|
|
|
20588
20908
|
// ../daemon/src/files/service.ts
|
|
20589
|
-
import { spawn as
|
|
20909
|
+
import { spawn as spawn5 } from "child_process";
|
|
20590
20910
|
import { stat as stat2, unlink } from "fs/promises";
|
|
20591
20911
|
|
|
20592
20912
|
// ../daemon/src/git/blame.ts
|
|
@@ -21285,7 +21605,7 @@ class FileService {
|
|
|
21285
21605
|
revealPath(target) {
|
|
21286
21606
|
if (process.platform !== "darwin")
|
|
21287
21607
|
throw new UserError("reveal is only available on macOS");
|
|
21288
|
-
const child =
|
|
21608
|
+
const child = spawn5("open", ["-R", target], { stdio: "ignore" });
|
|
21289
21609
|
child.on("error", () => {});
|
|
21290
21610
|
child.unref();
|
|
21291
21611
|
}
|
|
@@ -25750,6 +26070,49 @@ class PairCodes {
|
|
|
25750
26070
|
}
|
|
25751
26071
|
}
|
|
25752
26072
|
|
|
26073
|
+
// ../daemon/src/core/tailnet.ts
|
|
26074
|
+
var PHONE_OS = new Set(["android", "ios"]);
|
|
26075
|
+
function tailnetPhones(json) {
|
|
26076
|
+
let st;
|
|
26077
|
+
try {
|
|
26078
|
+
st = JSON.parse(json);
|
|
26079
|
+
} catch {
|
|
26080
|
+
return null;
|
|
26081
|
+
}
|
|
26082
|
+
if (st === null || typeof st !== "object")
|
|
26083
|
+
return null;
|
|
26084
|
+
const phones = [];
|
|
26085
|
+
for (const p of Object.values(st.Peer ?? {})) {
|
|
26086
|
+
if (!p || !PHONE_OS.has((p.OS ?? "").toLowerCase()))
|
|
26087
|
+
continue;
|
|
26088
|
+
const name = p.HostName || (p.DNSName ?? "").split(".")[0] || "a phone";
|
|
26089
|
+
phones.push({ name, ip: p.TailscaleIPs?.[0] ?? null });
|
|
26090
|
+
}
|
|
26091
|
+
return phones;
|
|
26092
|
+
}
|
|
26093
|
+
var PING_TIMEOUT = "2s";
|
|
26094
|
+
function cli(bin) {
|
|
26095
|
+
return async (args) => {
|
|
26096
|
+
try {
|
|
26097
|
+
const p = Bun.spawn([bin, ...args], { stdin: "ignore", stdout: "pipe", stderr: "ignore", timeout: 5000 });
|
|
26098
|
+
const out = await new Response(p.stdout).text();
|
|
26099
|
+
return { ok: await p.exited === 0, out };
|
|
26100
|
+
} catch {
|
|
26101
|
+
return { ok: false, out: "" };
|
|
26102
|
+
}
|
|
26103
|
+
};
|
|
26104
|
+
}
|
|
26105
|
+
var ping = (ip) => ["ping", "--until-direct=false", "--c", "1", "--timeout", PING_TIMEOUT, ip];
|
|
26106
|
+
async function readTailnetPhones(run = ((bin) => bin ? cli(bin) : null)(Bun.which("tailscale"))) {
|
|
26107
|
+
if (run === null)
|
|
26108
|
+
return null;
|
|
26109
|
+
const st = await run(["status", "--json"]);
|
|
26110
|
+
const listed = st.ok ? tailnetPhones(st.out) : null;
|
|
26111
|
+
if (listed === null)
|
|
26112
|
+
return null;
|
|
26113
|
+
return Promise.all(listed.map(async ({ name, ip }) => ({ name, online: ip !== null && (await run(ping(ip))).ok })));
|
|
26114
|
+
}
|
|
26115
|
+
|
|
25753
26116
|
// ../daemon/src/server/handlers.ts
|
|
25754
26117
|
import { randomBytes as randomBytes5 } from "crypto";
|
|
25755
26118
|
|
|
@@ -26540,8 +26903,10 @@ var handlers = {
|
|
|
26540
26903
|
async "delete-archived"(msg, _ctx, s) {
|
|
26541
26904
|
await s.worktrees.deleteArchived(msg.archiveId);
|
|
26542
26905
|
},
|
|
26543
|
-
async "adopt-worktree"(msg,
|
|
26544
|
-
await s
|
|
26906
|
+
async "adopt-worktree"(msg, ctx, s) {
|
|
26907
|
+
await sending(s, ctx, msg, msg.message?.attachments, async () => {
|
|
26908
|
+
await s.worktrees.adopt(msg.worktreeId, msg.clientId, msg.message);
|
|
26909
|
+
});
|
|
26545
26910
|
},
|
|
26546
26911
|
async "git-status"(msg, ctx, s) {
|
|
26547
26912
|
await gitStatus(s, ctx, msg.worktreeId);
|
|
@@ -26587,21 +26952,17 @@ var handlers = {
|
|
|
26587
26952
|
},
|
|
26588
26953
|
async land(msg, ctx, s) {
|
|
26589
26954
|
const { result } = await s.worktrees.land(msg.worktreeId, msg.message);
|
|
26590
|
-
const suggestion = !result.ok && result.conflict ? "Bring main into this branch and resolve the conflicts, then verify the app still works." : undefined;
|
|
26591
26955
|
if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
|
|
26592
26956
|
fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
|
|
26593
26957
|
}
|
|
26594
26958
|
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
|
|
26595
26959
|
url: result.url,
|
|
26596
|
-
...
|
|
26960
|
+
...result.asked ? { asked: true } : {}
|
|
26597
26961
|
}));
|
|
26598
26962
|
},
|
|
26599
26963
|
async "sync-main"(msg, ctx, s) {
|
|
26600
|
-
const { result
|
|
26601
|
-
|
|
26602
|
-
await notify(s, ctx, msg.worktreeId, prompt ? shipped(msg.worktreeId, false, `sync conflicts with ${base}: prompt prefilled in chat`, {
|
|
26603
|
-
suggestion: `Merge ${base} into this branch and resolve the conflicts, then verify the app still works.`
|
|
26604
|
-
}) : shipped(msg.worktreeId, result.ok, result.message));
|
|
26964
|
+
const { result } = await s.worktrees.sync(msg.worktreeId);
|
|
26965
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, result.asked ? { asked: true } : {}));
|
|
26605
26966
|
},
|
|
26606
26967
|
async "pull-main"(msg, ctx, s) {
|
|
26607
26968
|
const result = await s.worktrees.pull(msg.worktreeId);
|
|
@@ -26630,7 +26991,7 @@ var handlers = {
|
|
|
26630
26991
|
},
|
|
26631
26992
|
async commit(msg, ctx, s) {
|
|
26632
26993
|
const result = await s.worktrees.commit(msg.worktreeId, msg.message);
|
|
26633
|
-
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
26994
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, result.asked ? { asked: true } : {}));
|
|
26634
26995
|
},
|
|
26635
26996
|
async graft(msg, ctx, s) {
|
|
26636
26997
|
const { target } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
|
|
@@ -26705,6 +27066,12 @@ var handlers = {
|
|
|
26705
27066
|
async judge(msg, _ctx, s) {
|
|
26706
27067
|
await s.landing.judge(msg.worktreeId, msg.note);
|
|
26707
27068
|
},
|
|
27069
|
+
async "fix-preview"(msg, _ctx, s) {
|
|
27070
|
+
await s.worktrees.fixPreview(msg.worktreeId, msg.clientId);
|
|
27071
|
+
},
|
|
27072
|
+
fix(msg, _ctx, s) {
|
|
27073
|
+
s.fix.press(msg.worktreeId, msg.toolId);
|
|
27074
|
+
},
|
|
26708
27075
|
async "write-file"(msg, ctx, s) {
|
|
26709
27076
|
const head = { t: "file-written", worktreeId: msg.worktreeId, path: msg.path, seq: msg.seq };
|
|
26710
27077
|
try {
|
|
@@ -26835,6 +27202,10 @@ var handlers = {
|
|
|
26835
27202
|
s.state.setDefaultAgent(msg.agent);
|
|
26836
27203
|
s.hub.emit("agentsChanged");
|
|
26837
27204
|
},
|
|
27205
|
+
"set-keep-awake"(msg, _ctx, s) {
|
|
27206
|
+
s.state.setKeepAwake(msg.mode);
|
|
27207
|
+
s.hub.emit("keepAwakeChanged");
|
|
27208
|
+
},
|
|
26838
27209
|
"rescan-themes"(_msg, _ctx, s) {
|
|
26839
27210
|
s.themes.load();
|
|
26840
27211
|
s.hub.emit("themesChanged");
|
|
@@ -27010,7 +27381,8 @@ function createFetch(opts) {
|
|
|
27010
27381
|
dropped: new Set,
|
|
27011
27382
|
sent: 0,
|
|
27012
27383
|
bytes: 0,
|
|
27013
|
-
view: null
|
|
27384
|
+
view: null,
|
|
27385
|
+
...remoteShell ? { remote: true } : {}
|
|
27014
27386
|
};
|
|
27015
27387
|
const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
|
|
27016
27388
|
if (srv.upgrade(req, { data, headers }))
|
|
@@ -27046,6 +27418,13 @@ function createFetch(opts) {
|
|
|
27046
27418
|
const body = { code, url: pairLink(remote.host, code), ms };
|
|
27047
27419
|
return Response.json(body, { headers: { "cache-control": NO_STORE } });
|
|
27048
27420
|
}
|
|
27421
|
+
if (url.pathname === "/pair/phones" && req.method === "GET") {
|
|
27422
|
+
if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
|
|
27423
|
+
return new Response("unauthorized", { status: 401 });
|
|
27424
|
+
}
|
|
27425
|
+
const phones = remote && isTailnetName(remote.host) ? await opts.phones() : null;
|
|
27426
|
+
return Response.json(phones, { headers: { "cache-control": NO_STORE } });
|
|
27427
|
+
}
|
|
27049
27428
|
if (url.pathname === "/pair/redeem" && req.method === "POST") {
|
|
27050
27429
|
if (!remote || !remoteShell)
|
|
27051
27430
|
return new Response("not found", { status: 404 });
|
|
@@ -27166,6 +27545,13 @@ function createFetch(opts) {
|
|
|
27166
27545
|
const file = join21(opts.shellDist, rel.replaceAll("..", ""));
|
|
27167
27546
|
const hashed = rel.startsWith("/assets/");
|
|
27168
27547
|
if (existsSync23(file) && Bun.file(file).size > 0) {
|
|
27548
|
+
if (rel === "/manifest.json") {
|
|
27549
|
+
const { bar, ground } = opts.manifestColors();
|
|
27550
|
+
const manifest = { ...await Bun.file(file).json(), theme_color: bar, background_color: ground };
|
|
27551
|
+
return Response.json(manifest, {
|
|
27552
|
+
headers: { "content-type": "application/manifest+json", "cache-control": NO_STORE }
|
|
27553
|
+
});
|
|
27554
|
+
}
|
|
27169
27555
|
return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
|
|
27170
27556
|
}
|
|
27171
27557
|
if (hashed)
|
|
@@ -27202,13 +27588,17 @@ function startServer(opts) {
|
|
|
27202
27588
|
const { services: s, token, version } = opts;
|
|
27203
27589
|
const sockets = new Set;
|
|
27204
27590
|
const linked = new Set;
|
|
27591
|
+
const counted = () => {
|
|
27592
|
+
s.idleExit.clients(linked.size);
|
|
27593
|
+
s.keepAwake.remoteShells([...linked].filter((ws) => ws.data.remote || ws.data.preview).length);
|
|
27594
|
+
};
|
|
27205
27595
|
const link = (ws) => {
|
|
27206
27596
|
linked.add(ws);
|
|
27207
|
-
|
|
27597
|
+
counted();
|
|
27208
27598
|
};
|
|
27209
27599
|
const unlink = (ws) => {
|
|
27210
27600
|
if (linked.delete(ws))
|
|
27211
|
-
|
|
27601
|
+
counted();
|
|
27212
27602
|
};
|
|
27213
27603
|
const raw = (ws, json) => {
|
|
27214
27604
|
try {
|
|
@@ -27417,6 +27807,7 @@ function startServer(opts) {
|
|
|
27417
27807
|
agentChosen: s.state.defaultAgent !== undefined
|
|
27418
27808
|
});
|
|
27419
27809
|
s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
|
|
27810
|
+
s.hub.on("keepAwakeChanged", () => broadcast({ t: "keep-awake", mode: s.keepAwake.setting() }));
|
|
27420
27811
|
s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
|
|
27421
27812
|
s.hub.on("failed", (worktreeId, message) => broadcast({ t: "error", message, worktreeId }));
|
|
27422
27813
|
s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
|
|
@@ -27442,6 +27833,7 @@ function startServer(opts) {
|
|
|
27442
27833
|
agentChosen: s.state.defaultAgent !== undefined,
|
|
27443
27834
|
home: homedir11(),
|
|
27444
27835
|
folderDialog: process.platform === "darwin" && !cloud.enabled,
|
|
27836
|
+
keepAwake: s.keepAwake.setting(),
|
|
27445
27837
|
remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
|
|
27446
27838
|
paired: s.state.paired,
|
|
27447
27839
|
gitIdentity: await s.repos.gitIdentity(),
|
|
@@ -27474,6 +27866,10 @@ function startServer(opts) {
|
|
|
27474
27866
|
managed: { source: opts.managed.source, hash: opts.managed.hash },
|
|
27475
27867
|
preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
|
|
27476
27868
|
bootstrap: helloFrame,
|
|
27869
|
+
manifestColors: () => {
|
|
27870
|
+
const { surface0, surface1 } = s.themes.current().colors;
|
|
27871
|
+
return { bar: surface1, ground: surface0 };
|
|
27872
|
+
},
|
|
27477
27873
|
open: (path) => s.opens.open(path),
|
|
27478
27874
|
restart: (now) => s.restarter.request({ now }),
|
|
27479
27875
|
restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
|
|
@@ -27481,7 +27877,8 @@ function startServer(opts) {
|
|
|
27481
27877
|
onPaired: () => {
|
|
27482
27878
|
s.state.notePaired();
|
|
27483
27879
|
broadcast({ t: "paired" });
|
|
27484
|
-
}
|
|
27880
|
+
},
|
|
27881
|
+
phones: () => readTailnetPhones()
|
|
27485
27882
|
}),
|
|
27486
27883
|
websocket: {
|
|
27487
27884
|
async open(ws) {
|
|
@@ -28577,6 +28974,92 @@ class ChatSearch {
|
|
|
28577
28974
|
}
|
|
28578
28975
|
}
|
|
28579
28976
|
|
|
28977
|
+
// ../daemon/src/worktrees/fix.ts
|
|
28978
|
+
var RESPONSE = {
|
|
28979
|
+
hook: { does: "ask" },
|
|
28980
|
+
conflict: { does: "ask" },
|
|
28981
|
+
check: { does: "ask", once: true },
|
|
28982
|
+
command: { does: "offer" },
|
|
28983
|
+
preview: { does: "offer" }
|
|
28984
|
+
};
|
|
28985
|
+
function lastAsked(entries) {
|
|
28986
|
+
for (let i = entries.length - 1;i >= 0; i--) {
|
|
28987
|
+
const e = entries[i].event;
|
|
28988
|
+
if (e.type === "fix-asked")
|
|
28989
|
+
return e.kind;
|
|
28990
|
+
if (e.type === "user-message")
|
|
28991
|
+
return;
|
|
28992
|
+
}
|
|
28993
|
+
return;
|
|
28994
|
+
}
|
|
28995
|
+
function askedFor(entries, toolId) {
|
|
28996
|
+
return entries.some(({ event }) => event.type === "fix-asked" && event.toolId === toolId);
|
|
28997
|
+
}
|
|
28998
|
+
function failureOf(entries, toolId) {
|
|
28999
|
+
let command;
|
|
29000
|
+
for (const { event } of entries) {
|
|
29001
|
+
if (event.type === "tool-start" && event.toolId === toolId) {
|
|
29002
|
+
const input = event.input;
|
|
29003
|
+
command = typeof input?.command === "string" ? input.command : undefined;
|
|
29004
|
+
} else if (event.type === "tool-end" && event.toolId === toolId) {
|
|
29005
|
+
if (!event.fixable || command === undefined)
|
|
29006
|
+
return null;
|
|
29007
|
+
const run = { toolId, command, text: unformatOutput(event.output ?? "") };
|
|
29008
|
+
if (event.fixable.kind === "hook")
|
|
29009
|
+
return { kind: "hook", hook: event.fixable.hook ?? "git", ...run };
|
|
29010
|
+
return { kind: event.fixable.kind, ...run };
|
|
29011
|
+
}
|
|
29012
|
+
}
|
|
29013
|
+
return null;
|
|
29014
|
+
}
|
|
29015
|
+
|
|
29016
|
+
class FixService {
|
|
29017
|
+
d;
|
|
29018
|
+
constructor(d) {
|
|
29019
|
+
this.d = d;
|
|
29020
|
+
d.hub.on("checkFailed", (id, run) => {
|
|
29021
|
+
this.report(id, { kind: "check", ...run });
|
|
29022
|
+
});
|
|
29023
|
+
}
|
|
29024
|
+
report(worktreeId, failure, opts = {}) {
|
|
29025
|
+
const response = RESPONSE[failure.kind];
|
|
29026
|
+
if (response.does === "offer" && !opts.pressed)
|
|
29027
|
+
return false;
|
|
29028
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
29029
|
+
if (!wt || isMain(wt))
|
|
29030
|
+
return false;
|
|
29031
|
+
const agent = this.d.runtime.agentFor(worktreeId);
|
|
29032
|
+
if (!agent)
|
|
29033
|
+
return false;
|
|
29034
|
+
const entries = agent.transcript();
|
|
29035
|
+
if (response.once && !opts.pressed && lastAsked(entries) === failure.kind)
|
|
29036
|
+
return false;
|
|
29037
|
+
const toolId = failedRun(failure)?.toolId;
|
|
29038
|
+
if (toolId && askedFor(entries, toolId))
|
|
29039
|
+
return false;
|
|
29040
|
+
const context = failureContext(failure);
|
|
29041
|
+
agent.send(fixPrompt(failure), {
|
|
29042
|
+
asked: { kind: failure.kind, why: fixWhy(failure), ...toolId ? { toolId } : {} },
|
|
29043
|
+
...context ? { context: [context] } : {}
|
|
29044
|
+
});
|
|
29045
|
+
return true;
|
|
29046
|
+
}
|
|
29047
|
+
press(worktreeId, toolId) {
|
|
29048
|
+
this.d.state.requireWorktree(worktreeId);
|
|
29049
|
+
const agent = this.d.runtime.agentFor(worktreeId);
|
|
29050
|
+
if (!agent)
|
|
29051
|
+
throw new UserError("worktree still starting; try again in a moment");
|
|
29052
|
+
const entries = agent.transcript();
|
|
29053
|
+
const failure = failureOf(entries, toolId);
|
|
29054
|
+
if (!failure)
|
|
29055
|
+
throw new UserError("nothing to fix on that row");
|
|
29056
|
+
if (askedFor(entries, toolId))
|
|
29057
|
+
return;
|
|
29058
|
+
if (!this.report(worktreeId, failure, { pressed: true }))
|
|
29059
|
+
throw new UserError("nothing runs on main");
|
|
29060
|
+
}
|
|
29061
|
+
}
|
|
29062
|
+
|
|
28580
29063
|
// ../daemon/src/worktrees/landing.ts
|
|
28581
29064
|
var TAIL_CHARS = 400;
|
|
28582
29065
|
var DIFF_CHARS = 2000;
|
|
@@ -28590,6 +29073,7 @@ class LandingService {
|
|
|
28590
29073
|
constructor(d) {
|
|
28591
29074
|
this.d = d;
|
|
28592
29075
|
d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
|
|
29076
|
+
d.hub.on("messageRefused", (id, said) => fireAndForget(id, this.judge(id, `The commit-msg hook refused the last message, saying: ${said}`), "landing message after a refusal"));
|
|
28593
29077
|
d.hub.on("agentStatus", (id, status) => {
|
|
28594
29078
|
this.note(id, status);
|
|
28595
29079
|
if (status === "working")
|
|
@@ -28760,6 +29244,7 @@ class LandingService {
|
|
|
28760
29244
|
let check = "none";
|
|
28761
29245
|
let checkTail;
|
|
28762
29246
|
let waited = 0;
|
|
29247
|
+
let ran;
|
|
28763
29248
|
const command = repo.config.check?.trim();
|
|
28764
29249
|
if (command) {
|
|
28765
29250
|
const ceiling = timeoutFor(repo.config, "check");
|
|
@@ -28783,6 +29268,7 @@ class LandingService {
|
|
|
28783
29268
|
run.finish(r.exit);
|
|
28784
29269
|
if (!live())
|
|
28785
29270
|
return;
|
|
29271
|
+
ran = r;
|
|
28786
29272
|
check = r.exit === 0 ? "pass" : "fail";
|
|
28787
29273
|
if (r.exit === "timeout") {
|
|
28788
29274
|
checkTail = [`gave up after ${describeDuration(ceiling)}`, tail(r.text)].filter(Boolean).join(`
|
|
@@ -28838,6 +29324,8 @@ class LandingService {
|
|
|
28838
29324
|
record.recap = { at: Date.now(), text: verdict.recap };
|
|
28839
29325
|
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)` : ""}`);
|
|
28840
29326
|
this.d.worktrees.setLanding(worktreeId, landing);
|
|
29327
|
+
if (command && check === "fail" && ran && typeof ran.exit === "number" && !opts.quiet)
|
|
29328
|
+
this.d.hub.emit("checkFailed", worktreeId, { toolId: ran.toolId, command, text: ran.text });
|
|
28841
29329
|
}
|
|
28842
29330
|
async sharedMigration(wt, repo) {
|
|
28843
29331
|
if (!repo.services || keepsCopiesApart(repo.config))
|
|
@@ -29242,7 +29730,7 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
29242
29730
|
}
|
|
29243
29731
|
|
|
29244
29732
|
// ../daemon/src/worktrees/service.ts
|
|
29245
|
-
import { cpSync, existsSync as
|
|
29733
|
+
import { cpSync, existsSync as existsSync30, readFileSync as readFileSync18, rmSync as rmSync7 } from "fs";
|
|
29246
29734
|
import { rm as rm4 } from "fs/promises";
|
|
29247
29735
|
import { dirname as dirname11, join as join27 } from "path";
|
|
29248
29736
|
|
|
@@ -29368,7 +29856,11 @@ async function dropLandRefs(repoPath, worktreeId) {
|
|
|
29368
29856
|
}
|
|
29369
29857
|
|
|
29370
29858
|
// ../daemon/src/git/land.ts
|
|
29859
|
+
import { existsSync as existsSync26 } from "fs";
|
|
29371
29860
|
var STEP_TIMEOUT_MS = 10 * 60000;
|
|
29861
|
+
function rowOf(r) {
|
|
29862
|
+
return r.shown && r.toolId && r.command ? { toolId: r.toolId, command: r.command, text: r.text } : undefined;
|
|
29863
|
+
}
|
|
29372
29864
|
var stepRun = (cwd, args, onText, signal, opts = {}) => runWatched(GIT, args, cwd, { env: NO_PROMPT, onText, signal, ...opts, timeoutMs: opts.timeoutMs ?? STEP_TIMEOUT_MS });
|
|
29373
29865
|
var UNWATCHED = {
|
|
29374
29866
|
step() {},
|
|
@@ -29381,6 +29873,14 @@ function refused(what, r, n = 200) {
|
|
|
29381
29873
|
}
|
|
29382
29874
|
return `${what}: ${r.shown ? "what git and its hooks printed is on the chat" : r.err.slice(0, n)}`;
|
|
29383
29875
|
}
|
|
29876
|
+
function stepRefused(what, r, n = 200) {
|
|
29877
|
+
const row = typeof r.exit === "number" && r.hook ? rowOf(r) : undefined;
|
|
29878
|
+
return {
|
|
29879
|
+
ok: false,
|
|
29880
|
+
message: refused(what, r, n),
|
|
29881
|
+
...row && r.hook ? { hook: { ...row, name: r.hook } } : {}
|
|
29882
|
+
};
|
|
29883
|
+
}
|
|
29384
29884
|
async function commitWorktree(worktreePath, message, w = UNWATCHED) {
|
|
29385
29885
|
if ((await statusFiles(worktreePath)).length === 0)
|
|
29386
29886
|
return { ok: false, message: "nothing to commit" };
|
|
@@ -29388,7 +29888,7 @@ async function commitWorktree(worktreePath, message, w = UNWATCHED) {
|
|
|
29388
29888
|
w.step("committing");
|
|
29389
29889
|
const c = await w.git(worktreePath, ["commit", "-m", message]);
|
|
29390
29890
|
if (!c.ok)
|
|
29391
|
-
return
|
|
29891
|
+
return stepRefused("commit refused", c);
|
|
29392
29892
|
return { ok: true, message: `committed: ${message}` };
|
|
29393
29893
|
}
|
|
29394
29894
|
async function requireClean(worktreePath) {
|
|
@@ -29474,7 +29974,27 @@ async function putBack(worktreePath, defaultBr, aside, taken) {
|
|
|
29474
29974
|
message: `the uncommitted changes here conflict with ${defaultBr}${named}: commit them first, then sync`
|
|
29475
29975
|
};
|
|
29476
29976
|
}
|
|
29977
|
+
var MIDWAY = [
|
|
29978
|
+
["MERGE_HEAD", "merge"],
|
|
29979
|
+
["rebase-merge", "rebase"],
|
|
29980
|
+
["rebase-apply", "rebase"],
|
|
29981
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
29982
|
+
["REVERT_HEAD", "revert"]
|
|
29983
|
+
];
|
|
29984
|
+
async function midway(worktreePath) {
|
|
29985
|
+
const args = MIDWAY.flatMap(([file]) => ["--git-path", file]);
|
|
29986
|
+
const r = await git(worktreePath, "rev-parse", "--path-format=absolute", ...args);
|
|
29987
|
+
if (!r.ok)
|
|
29988
|
+
return null;
|
|
29989
|
+
const at = r.out.split(`
|
|
29990
|
+
`).findIndex((file) => file && existsSync26(file));
|
|
29991
|
+
return MIDWAY[at]?.[1] ?? null;
|
|
29992
|
+
}
|
|
29477
29993
|
async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
|
|
29994
|
+
const unfinished = await midway(worktreePath);
|
|
29995
|
+
if (unfinished) {
|
|
29996
|
+
return { ok: false, message: `a ${unfinished} is unfinished here: finish or abort it first` };
|
|
29997
|
+
}
|
|
29478
29998
|
if (!carry) {
|
|
29479
29999
|
const cErr = await requireClean(worktreePath);
|
|
29480
30000
|
if (cErr)
|
|
@@ -29489,8 +30009,19 @@ async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = f
|
|
|
29489
30009
|
const taken = await bringIn(worktreePath, defaultBr, own, behind, w);
|
|
29490
30010
|
return aside ? putBack(worktreePath, defaultBr, aside, taken) : taken;
|
|
29491
30011
|
}
|
|
30012
|
+
async function requireLinear(worktreePath, base) {
|
|
30013
|
+
const merges = await git(worktreePath, "rev-list", "--merges", "--count", `${base}..HEAD`);
|
|
30014
|
+
if (!merges.ok || merges.out === "0")
|
|
30015
|
+
return null;
|
|
30016
|
+
return {
|
|
30017
|
+
ok: false,
|
|
30018
|
+
conflict: "rebase",
|
|
30019
|
+
message: `the branch holds a merge commit, which landing by rebase would put on ${base}: ask the agent to rebase onto ${base} first`
|
|
30020
|
+
};
|
|
30021
|
+
}
|
|
29492
30022
|
async function bringIn(worktreePath, defaultBr, own, behind, w) {
|
|
29493
|
-
|
|
30023
|
+
const merges = own ? await git(worktreePath, "rev-list", "--merges", "--count", `${defaultBr}..HEAD`) : null;
|
|
30024
|
+
if (own && merges?.out === "0") {
|
|
29494
30025
|
w.step(`rebasing onto ${defaultBr}`);
|
|
29495
30026
|
const r = await w.git(worktreePath, ["rebase", defaultBr]);
|
|
29496
30027
|
if (!r.ok) {
|
|
@@ -29498,10 +30029,12 @@ async function bringIn(worktreePath, defaultBr, own, behind, w) {
|
|
|
29498
30029
|
const files = overwritten(r.text);
|
|
29499
30030
|
if (files.length)
|
|
29500
30031
|
return { ok: false, message: untrackedInTheWay(defaultBr, files) };
|
|
29501
|
-
const ask = `rebase onto ${defaultBr} conflicts: ask the agent to
|
|
30032
|
+
const ask = `rebase onto ${defaultBr} conflicts: ask the agent to rebase onto ${defaultBr} and resolve them`;
|
|
30033
|
+
const step = rowOf(r);
|
|
29502
30034
|
return {
|
|
29503
30035
|
ok: false,
|
|
29504
|
-
conflict:
|
|
30036
|
+
conflict: "rebase",
|
|
30037
|
+
...step ? { step } : {},
|
|
29505
30038
|
message: r.shown ? `${ask}; the conflicts are on the chat` : `${ask} (${r.text.slice(0, 200)})`
|
|
29506
30039
|
};
|
|
29507
30040
|
}
|
|
@@ -29548,7 +30081,7 @@ async function landLocally(worktreePath, branch, repoPath, defaultBr, method, me
|
|
|
29548
30081
|
const c = await w.git(repoPath, ["commit", "-m", message]);
|
|
29549
30082
|
if (!c.ok) {
|
|
29550
30083
|
await git(repoPath, "reset", "--hard", "HEAD");
|
|
29551
|
-
return
|
|
30084
|
+
return stepRefused("squash commit refused", c);
|
|
29552
30085
|
}
|
|
29553
30086
|
return { ok: true, message: `squashed ${branch} onto ${defaultBr}` };
|
|
29554
30087
|
}
|
|
@@ -29607,7 +30140,7 @@ async function landingCommit(worktreePath, branch, base, method, message, own, w
|
|
|
29607
30140
|
const c = await w.git(worktreePath, ["commit", "-m", message]);
|
|
29608
30141
|
if (!c.ok) {
|
|
29609
30142
|
await git(worktreePath, "reset", "--soft", head.out);
|
|
29610
|
-
return
|
|
30143
|
+
return stepRefused("squash commit refused", c);
|
|
29611
30144
|
}
|
|
29612
30145
|
const sha = await git(worktreePath, "rev-parse", "HEAD");
|
|
29613
30146
|
return { ok: true, sha: sha.out, message: `squashed ${branch} onto ${base}` };
|
|
@@ -29620,15 +30153,15 @@ async function landingCommit(worktreePath, branch, base, method, message, own, w
|
|
|
29620
30153
|
w.step(`squashing onto ${base}`);
|
|
29621
30154
|
const s = await w.git(worktreePath, ["merge", "--squash", branch]);
|
|
29622
30155
|
if (!s.ok)
|
|
29623
|
-
return
|
|
30156
|
+
return stepRefused("squash refused", s);
|
|
29624
30157
|
const c = await w.git(worktreePath, ["commit", "-m", message]);
|
|
29625
30158
|
if (!c.ok)
|
|
29626
|
-
return
|
|
30159
|
+
return stepRefused("squash commit refused", c);
|
|
29627
30160
|
} else {
|
|
29628
30161
|
w.step(`merging into ${base}`);
|
|
29629
30162
|
const m = await w.git(worktreePath, ["merge", "--no-ff", "-m", `Merge branch '${branch}'`, branch]);
|
|
29630
30163
|
if (!m.ok)
|
|
29631
|
-
return
|
|
30164
|
+
return stepRefused("merge refused", m);
|
|
29632
30165
|
}
|
|
29633
30166
|
const sha = await git(worktreePath, "rev-parse", "HEAD");
|
|
29634
30167
|
return { ok: true, sha: sha.out, message: `${method === "squash" ? "squashed" : "merged"} ${branch} onto ${base}` };
|
|
@@ -29655,15 +30188,17 @@ async function pushLanding(worktreePath, sha, defaultBr, w = UNWATCHED) {
|
|
|
29655
30188
|
if (p.ok)
|
|
29656
30189
|
return { ok: true, message: `pushed ${defaultBr} to ${tracked.remote}` };
|
|
29657
30190
|
const moved = /\[rejected\]|fetch first|non-fast-forward|cannot lock ref/.test(p.err);
|
|
29658
|
-
return moved ? { ok: false, moved: true, message: `${defaultBr} on ${tracked.remote} moved while landing` } :
|
|
30191
|
+
return moved ? { ok: false, moved: true, message: `${defaultBr} on ${tracked.remote} moved while landing` } : stepRefused("push failed", p, 300);
|
|
29659
30192
|
}
|
|
29660
30193
|
async function mergeFailure(cwd, m, conflictMessage) {
|
|
29661
30194
|
const conflict = /CONFLICT|Automatic merge failed/.test(m.text);
|
|
29662
30195
|
if (conflict) {
|
|
29663
30196
|
await git(cwd, "merge", "--abort");
|
|
30197
|
+
const step = rowOf(m);
|
|
29664
30198
|
return {
|
|
29665
30199
|
ok: false,
|
|
29666
|
-
conflict:
|
|
30200
|
+
conflict: "merge",
|
|
30201
|
+
...step ? { step } : {},
|
|
29667
30202
|
message: m.shown ? `${conflictMessage}; the conflicts are on the chat` : `${conflictMessage} (${m.text.slice(0, 200)})`
|
|
29668
30203
|
};
|
|
29669
30204
|
}
|
|
@@ -29694,7 +30229,7 @@ async function pushBranch(worktreePath, branch, w = UNWATCHED) {
|
|
|
29694
30229
|
push = await w.git(worktreePath, args);
|
|
29695
30230
|
}
|
|
29696
30231
|
if (!push.ok)
|
|
29697
|
-
return
|
|
30232
|
+
return stepRefused("push failed", push, 300);
|
|
29698
30233
|
return { ok: true, message: `pushed ${branch}` };
|
|
29699
30234
|
}
|
|
29700
30235
|
async function goneFromOrigin(worktreePath, branch) {
|
|
@@ -29766,7 +30301,7 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
29766
30301
|
}
|
|
29767
30302
|
|
|
29768
30303
|
// ../daemon/src/git/turn.ts
|
|
29769
|
-
import { copyFileSync, existsSync as
|
|
30304
|
+
import { copyFileSync, existsSync as existsSync27, rmSync as rmSync5 } from "fs";
|
|
29770
30305
|
import { join as join24 } from "path";
|
|
29771
30306
|
var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
|
|
29772
30307
|
var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
|
|
@@ -29792,7 +30327,7 @@ async function treeNow(wtPath, head, dirty) {
|
|
|
29792
30327
|
const tree = await run(GIT, ["write-tree"], wtPath, env);
|
|
29793
30328
|
return tree.ok && tree.out ? tree.out : null;
|
|
29794
30329
|
};
|
|
29795
|
-
if (!
|
|
30330
|
+
if (!existsSync27(index))
|
|
29796
30331
|
was = undefined;
|
|
29797
30332
|
const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
|
|
29798
30333
|
if (was && paths && was.head === head) {
|
|
@@ -29807,9 +30342,9 @@ async function treeNow(wtPath, head, dirty) {
|
|
|
29807
30342
|
}
|
|
29808
30343
|
}
|
|
29809
30344
|
synced.delete(wtPath);
|
|
29810
|
-
if (!
|
|
30345
|
+
if (!existsSync27(index)) {
|
|
29811
30346
|
const own = join24(index, "..", "index");
|
|
29812
|
-
if (
|
|
30347
|
+
if (existsSync27(own))
|
|
29813
30348
|
copyFileSync(own, index);
|
|
29814
30349
|
else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
|
|
29815
30350
|
return null;
|
|
@@ -29890,7 +30425,7 @@ async function dropTurnRefs(repoPath, worktreeId) {
|
|
|
29890
30425
|
}
|
|
29891
30426
|
|
|
29892
30427
|
// ../daemon/src/worktrees/archive.ts
|
|
29893
|
-
import { existsSync as
|
|
30428
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync17, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
|
|
29894
30429
|
import { basename as basename8, dirname as dirname10, join as join25 } from "path";
|
|
29895
30430
|
class WorktreeArchive {
|
|
29896
30431
|
dir;
|
|
@@ -29900,7 +30435,7 @@ class WorktreeArchive {
|
|
|
29900
30435
|
mkdirSync8(dir, { recursive: true });
|
|
29901
30436
|
for (const id of readdirSync8(dir)) {
|
|
29902
30437
|
const file = this.filesOf(id).record;
|
|
29903
|
-
if (!
|
|
30438
|
+
if (!existsSync28(file))
|
|
29904
30439
|
continue;
|
|
29905
30440
|
try {
|
|
29906
30441
|
this.records.set(id, JSON.parse(readFileSync17(file, "utf8")));
|
|
@@ -29950,7 +30485,7 @@ class WorktreeArchive {
|
|
|
29950
30485
|
if (!this.records.has(id) || !isPlanPath(rel))
|
|
29951
30486
|
return null;
|
|
29952
30487
|
const file = join25(this.filesOf(id).plans, basename8(rel));
|
|
29953
|
-
if (!
|
|
30488
|
+
if (!existsSync28(file))
|
|
29954
30489
|
return null;
|
|
29955
30490
|
try {
|
|
29956
30491
|
return readFileSync17(file, "utf8");
|
|
@@ -29991,7 +30526,7 @@ function summarize(r, repoId, transcript) {
|
|
|
29991
30526
|
};
|
|
29992
30527
|
}
|
|
29993
30528
|
function lastUsage(transcript) {
|
|
29994
|
-
if (!
|
|
30529
|
+
if (!existsSync28(transcript))
|
|
29995
30530
|
return null;
|
|
29996
30531
|
const lines = readFileSync17(transcript, "utf8").split(`
|
|
29997
30532
|
`);
|
|
@@ -30007,7 +30542,7 @@ function lastUsage(transcript) {
|
|
|
30007
30542
|
return null;
|
|
30008
30543
|
}
|
|
30009
30544
|
function firstPrompt(transcript) {
|
|
30010
|
-
if (!
|
|
30545
|
+
if (!existsSync28(transcript))
|
|
30011
30546
|
return;
|
|
30012
30547
|
for (const line of readFileSync17(transcript, "utf8").split(`
|
|
30013
30548
|
`)) {
|
|
@@ -30022,7 +30557,7 @@ function firstPrompt(transcript) {
|
|
|
30022
30557
|
return;
|
|
30023
30558
|
}
|
|
30024
30559
|
function move(from, to) {
|
|
30025
|
-
if (!
|
|
30560
|
+
if (!existsSync28(from))
|
|
30026
30561
|
return;
|
|
30027
30562
|
mkdirSync8(dirname10(to), { recursive: true });
|
|
30028
30563
|
rmSync6(to, { recursive: true, force: true });
|
|
@@ -30087,7 +30622,7 @@ class ArchivedGit {
|
|
|
30087
30622
|
}
|
|
30088
30623
|
|
|
30089
30624
|
// ../daemon/src/worktrees/spare.ts
|
|
30090
|
-
import { existsSync as
|
|
30625
|
+
import { existsSync as existsSync29 } from "fs";
|
|
30091
30626
|
import { basename as basename9, join as join26 } from "path";
|
|
30092
30627
|
class SparePool {
|
|
30093
30628
|
d;
|
|
@@ -30107,7 +30642,7 @@ class SparePool {
|
|
|
30107
30642
|
if (!text.trim())
|
|
30108
30643
|
return;
|
|
30109
30644
|
const wt = this.d.state.worktree(boxId);
|
|
30110
|
-
if (wt?.kind !== "spare" || !
|
|
30645
|
+
if (wt?.kind !== "spare" || !existsSync29(wt.path))
|
|
30111
30646
|
return;
|
|
30112
30647
|
const agent = this.d.runtime.ensureAgent(wt).agent;
|
|
30113
30648
|
if (agent.runningAgent === null)
|
|
@@ -30171,7 +30706,7 @@ class SparePool {
|
|
|
30171
30706
|
const repoId = repo.id;
|
|
30172
30707
|
let cut = () => {};
|
|
30173
30708
|
const added = (async () => {
|
|
30174
|
-
if (
|
|
30709
|
+
if (existsSync29(wt.path))
|
|
30175
30710
|
return;
|
|
30176
30711
|
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, baseOf(repo)));
|
|
30177
30712
|
})();
|
|
@@ -30219,7 +30754,7 @@ class SparePool {
|
|
|
30219
30754
|
}
|
|
30220
30755
|
log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
|
|
30221
30756
|
this.d.carryDraft?.(wt.id, wt.repoId);
|
|
30222
|
-
if (
|
|
30757
|
+
if (existsSync29(wt.path)) {
|
|
30223
30758
|
await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
|
|
30224
30759
|
return;
|
|
30225
30760
|
}
|
|
@@ -30276,7 +30811,7 @@ class SparePool {
|
|
|
30276
30811
|
const running = this.inflight.get(repoId);
|
|
30277
30812
|
await running?.added.catch(() => {});
|
|
30278
30813
|
running?.cut();
|
|
30279
|
-
if (wt.kind !== "spare" || !
|
|
30814
|
+
if (wt.kind !== "spare" || !existsSync29(wt.path))
|
|
30280
30815
|
return null;
|
|
30281
30816
|
}
|
|
30282
30817
|
await this.refreshing.get(repoId);
|
|
@@ -30513,6 +31048,7 @@ function stopOf(prev, next) {
|
|
|
30513
31048
|
// ../daemon/src/worktrees/service.ts
|
|
30514
31049
|
var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
|
|
30515
31050
|
var BASE_IGNORE = "base .gitignore";
|
|
31051
|
+
var REFUSAL_CHARS = 400;
|
|
30516
31052
|
async function deleteTree(path) {
|
|
30517
31053
|
try {
|
|
30518
31054
|
await rm4(path, { recursive: true, force: true });
|
|
@@ -30730,7 +31266,7 @@ class WorktreeService {
|
|
|
30730
31266
|
};
|
|
30731
31267
|
const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
|
|
30732
31268
|
const restored = (carried.ok ? carried.out.split(`
|
|
30733
|
-
`).filter(Boolean) : []).filter((p) => !
|
|
31269
|
+
`).filter(Boolean) : []).filter((p) => !existsSync30(join27(wt.path, p)));
|
|
30734
31270
|
const applied = await git(wt.path, "stash", "apply", sha);
|
|
30735
31271
|
if (applied.ok) {
|
|
30736
31272
|
const ref = await entry();
|
|
@@ -30766,7 +31302,7 @@ class WorktreeService {
|
|
|
30766
31302
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
30767
31303
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
30768
31304
|
let slug = branchSlug(title) || "branch";
|
|
30769
|
-
if (
|
|
31305
|
+
if (existsSync30(join27(this.d.paths.worktreesDir, repo.name, slug)))
|
|
30770
31306
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
30771
31307
|
const wtPath = join27(this.d.paths.worktreesDir, repo.name, slug);
|
|
30772
31308
|
await withRepoLock(repo.path, async () => {
|
|
@@ -30811,7 +31347,7 @@ class WorktreeService {
|
|
|
30811
31347
|
throw new UserError("that worktree is gone, or Toyon already has it");
|
|
30812
31348
|
return found;
|
|
30813
31349
|
}
|
|
30814
|
-
async adopt(worktreeId, createdBy) {
|
|
31350
|
+
async adopt(worktreeId, createdBy, message) {
|
|
30815
31351
|
const r = this.readable(worktreeId);
|
|
30816
31352
|
if (!r)
|
|
30817
31353
|
throw new UserError("that worktree is gone");
|
|
@@ -30840,12 +31376,18 @@ class WorktreeService {
|
|
|
30840
31376
|
title: found.name,
|
|
30841
31377
|
createdAt: Date.now(),
|
|
30842
31378
|
agent: this.d.agents.require(this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id,
|
|
31379
|
+
parked: true,
|
|
31380
|
+
...message ? { promptedAt: Date.now() } : {},
|
|
30843
31381
|
...createdBy ? { createdBy } : {}
|
|
30844
31382
|
};
|
|
30845
31383
|
return rec;
|
|
30846
31384
|
});
|
|
30847
31385
|
this.invalidateDiscovered();
|
|
30848
31386
|
this.launch(wt, repo, repo.path, { setupCommands: false });
|
|
31387
|
+
const { agent } = this.d.runtime.ensureAgent(wt);
|
|
31388
|
+
agent.note({ type: "adopted", branch: wt.branch, ts: Date.now() });
|
|
31389
|
+
if (message)
|
|
31390
|
+
agent.send(message.text, { attachments: message.attachments });
|
|
30849
31391
|
return wt;
|
|
30850
31392
|
}
|
|
30851
31393
|
async ownProcs(worktreeId, names) {
|
|
@@ -31015,7 +31557,7 @@ class WorktreeService {
|
|
|
31015
31557
|
}
|
|
31016
31558
|
async teardown(wt, repo) {
|
|
31017
31559
|
const cmds = repo.config.teardown ?? [];
|
|
31018
|
-
if (cmds.length === 0 || !
|
|
31560
|
+
if (cmds.length === 0 || !existsSync30(wt.path))
|
|
31019
31561
|
return;
|
|
31020
31562
|
for (const cmd of cmds) {
|
|
31021
31563
|
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") });
|
|
@@ -31065,7 +31607,7 @@ class WorktreeService {
|
|
|
31065
31607
|
}
|
|
31066
31608
|
keepPlans(wt) {
|
|
31067
31609
|
const from = join27(wt.path, PLANS_DIR);
|
|
31068
|
-
if (!
|
|
31610
|
+
if (!existsSync30(from))
|
|
31069
31611
|
return;
|
|
31070
31612
|
try {
|
|
31071
31613
|
cpSync(from, this.plansScratch(wt.id), { recursive: true });
|
|
@@ -31196,7 +31738,7 @@ class WorktreeService {
|
|
|
31196
31738
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
31197
31739
|
if (rec.auto)
|
|
31198
31740
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
31199
|
-
const path =
|
|
31741
|
+
const path = existsSync30(old.path) ? join27(dirname11(old.path), freeSlot(dirname11(old.path)).dir) : old.path;
|
|
31200
31742
|
const branch = await withRepoLock(repo.path, async () => {
|
|
31201
31743
|
if (!await commitOf(repo.path, archiveRef(old.id))) {
|
|
31202
31744
|
throw new UserError(`${old.title}'s kept commits are gone from git`);
|
|
@@ -31263,12 +31805,12 @@ class WorktreeService {
|
|
|
31263
31805
|
return;
|
|
31264
31806
|
const repo = this.repoOf(rec);
|
|
31265
31807
|
const repoPath = repo?.path ?? rec.repoPath;
|
|
31266
|
-
if (rec.kept &&
|
|
31808
|
+
if (rec.kept && existsSync30(repoPath)) {
|
|
31267
31809
|
const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
|
|
31268
31810
|
if (!r.ok)
|
|
31269
31811
|
log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
|
|
31270
31812
|
}
|
|
31271
|
-
if (rec.worktree.lands?.length &&
|
|
31813
|
+
if (rec.worktree.lands?.length && existsSync30(repoPath))
|
|
31272
31814
|
await dropLandRefs(repoPath, archiveId);
|
|
31273
31815
|
this.archive.delete(archiveId);
|
|
31274
31816
|
this.d.drafts?.drop(archiveId);
|
|
@@ -31378,8 +31920,8 @@ class WorktreeService {
|
|
|
31378
31920
|
}
|
|
31379
31921
|
async mirrorBaseIgnore(repo) {
|
|
31380
31922
|
const src = join27(repo.path, ".gitignore");
|
|
31381
|
-
const tracked =
|
|
31382
|
-
const mirror =
|
|
31923
|
+
const tracked = existsSync30(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
|
|
31924
|
+
const mirror = existsSync30(src) && !tracked;
|
|
31383
31925
|
const lines = mirror ? readFileSync18(src, "utf8").split(`
|
|
31384
31926
|
`).filter((l) => l.trim() !== "") : [];
|
|
31385
31927
|
await excludeBlock(repo.path, BASE_IGNORE, lines);
|
|
@@ -31394,7 +31936,7 @@ class WorktreeService {
|
|
|
31394
31936
|
}
|
|
31395
31937
|
const srcNm = join27(depsSource, "node_modules");
|
|
31396
31938
|
const dstNm = join27(wt.path, "node_modules");
|
|
31397
|
-
if (
|
|
31939
|
+
if (existsSync30(srcNm) && !existsSync30(dstNm)) {
|
|
31398
31940
|
const r = await cloneTree(srcNm, dstNm);
|
|
31399
31941
|
if (r.ok)
|
|
31400
31942
|
this.d.hub.emit("log", wt.id, "setup", `deps via ${r.how} in ${r.ms}ms`);
|
|
@@ -31403,7 +31945,7 @@ class WorktreeService {
|
|
|
31403
31945
|
}
|
|
31404
31946
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
31405
31947
|
const src = join27(depsSource, f);
|
|
31406
|
-
if (!
|
|
31948
|
+
if (!existsSync30(src) || existsSync30(join27(wt.path, f)))
|
|
31407
31949
|
continue;
|
|
31408
31950
|
const r = await run("cp", [src, join27(wt.path, f)], wt.path);
|
|
31409
31951
|
if (r.ok)
|
|
@@ -31536,13 +32078,15 @@ ${wt.landing.body}` : s;
|
|
|
31536
32078
|
const handle = commit ? this.runs.begin(wt.id, "commit", { timeoutMs: ceiling }) : null;
|
|
31537
32079
|
const run = (onText, signal) => stepRun(cwd, args, onText, signal, {
|
|
31538
32080
|
timeoutMs: ceiling,
|
|
31539
|
-
...handle ? { onSpawn: handle.spawned
|
|
32081
|
+
...handle ? { onSpawn: handle.spawned } : {},
|
|
32082
|
+
onHook: (hook) => handle?.stage(hook ? `${hook} hook` : undefined)
|
|
31540
32083
|
});
|
|
31541
32084
|
const watched = async () => {
|
|
31542
32085
|
if (!this.d.watch)
|
|
31543
32086
|
return run(() => {});
|
|
32087
|
+
const command = `${where} ${commandLine(args)}`;
|
|
31544
32088
|
try {
|
|
31545
|
-
return await this.d.watch(wt.id,
|
|
32089
|
+
return { ...await this.d.watch(wt.id, command, run), command };
|
|
31546
32090
|
} catch (e) {
|
|
31547
32091
|
log.warn(wt.id, `landing step runs unwatched: ${e instanceof Error ? e.message : e}`);
|
|
31548
32092
|
return run(() => {});
|
|
@@ -31580,7 +32124,35 @@ ${wt.landing.body}` : s;
|
|
|
31580
32124
|
});
|
|
31581
32125
|
if (out.result.ok && out.archiveIds)
|
|
31582
32126
|
this.noteLanded(wt, out.result.message, out.archiveIds);
|
|
31583
|
-
return out;
|
|
32127
|
+
return { ...out, result: this.fixShip(wt, out.result, baseOf(repo)) };
|
|
32128
|
+
}
|
|
32129
|
+
fixShip(wt, result, base) {
|
|
32130
|
+
if (result.ok)
|
|
32131
|
+
return result;
|
|
32132
|
+
if (result.hook?.name === "commit-msg") {
|
|
32133
|
+
this.d.hub.emit("messageRefused", wt.id, result.hook.text.trim().slice(-REFUSAL_CHARS));
|
|
32134
|
+
return { ...result, message: "the commit-msg hook refused the message: another is being written" };
|
|
32135
|
+
}
|
|
32136
|
+
const { hook } = result;
|
|
32137
|
+
const failure = hook ? { kind: "hook", hook: hook.name, toolId: hook.toolId, command: hook.command, text: hook.text } : result.conflict ? { kind: "conflict", base, how: result.conflict, ...result.step ? { step: result.step } : {} } : null;
|
|
32138
|
+
return failure && this.d.fix.report(wt.id, failure) ? { ...result, asked: true } : result;
|
|
32139
|
+
}
|
|
32140
|
+
async fixPreview(worktreeId, clientId) {
|
|
32141
|
+
const wt = this.d.state.requireWorktree(worktreeId);
|
|
32142
|
+
const failure = {
|
|
32143
|
+
kind: "preview",
|
|
32144
|
+
procs: this.d.runtime.get(worktreeId)?.procs?.states() ?? [],
|
|
32145
|
+
log: this.d.runtime.recentLogs(worktreeId)
|
|
32146
|
+
};
|
|
32147
|
+
if (!isLead(wt)) {
|
|
32148
|
+
if (!this.d.fix.report(worktreeId, failure, { pressed: true }))
|
|
32149
|
+
throw new UserError("worktree still starting; try again in a moment");
|
|
32150
|
+
return;
|
|
32151
|
+
}
|
|
32152
|
+
await this.create(wt.repoId, fixPrompt(failure), {
|
|
32153
|
+
...clientId ? { createdBy: clientId } : {},
|
|
32154
|
+
...isProvisional(wt) ? { worktreeId } : {}
|
|
32155
|
+
});
|
|
31584
32156
|
}
|
|
31585
32157
|
async movedMidTurn(wt) {
|
|
31586
32158
|
const status = this.d.runtime.agentFor(wt.id)?.status;
|
|
@@ -31709,11 +32281,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
31709
32281
|
if (committed && !committed.ok)
|
|
31710
32282
|
return committed;
|
|
31711
32283
|
committedHere = committed !== null;
|
|
32284
|
+
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
32285
|
+
const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
|
|
32286
|
+
if (merged)
|
|
32287
|
+
return merged;
|
|
31712
32288
|
const taken = await takeMainIn(wt.path, base, own, w);
|
|
31713
32289
|
if (!taken.ok)
|
|
31714
32290
|
return taken;
|
|
31715
32291
|
const mark = await landingMark(wt.path, base);
|
|
31716
|
-
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
31717
32292
|
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
|
|
31718
32293
|
const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
|
|
31719
32294
|
if (!landed.ok)
|
|
@@ -31744,6 +32319,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
31744
32319
|
return { result: { ok: false, message: `nothing to land: no commits ahead of ${base}` } };
|
|
31745
32320
|
let sha = "";
|
|
31746
32321
|
let already = false;
|
|
32322
|
+
const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
|
|
32323
|
+
if (merged)
|
|
32324
|
+
return { result: merged };
|
|
31747
32325
|
for (let attempt = 0;; attempt++) {
|
|
31748
32326
|
const fetched = await this.fetchBase(wt, repo, w);
|
|
31749
32327
|
if (!fetched.ok)
|
|
@@ -31881,7 +32459,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
31881
32459
|
}
|
|
31882
32460
|
return taken;
|
|
31883
32461
|
});
|
|
31884
|
-
return { result, base: r.base };
|
|
32462
|
+
return { result: r.wt ? this.fixShip(r.wt, result, r.base) : result, base: r.base };
|
|
31885
32463
|
}
|
|
31886
32464
|
async pull(worktreeId) {
|
|
31887
32465
|
const wt = this.d.state.requireWorktree(worktreeId);
|
|
@@ -31903,7 +32481,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
31903
32481
|
const m = message.trim();
|
|
31904
32482
|
if (!m)
|
|
31905
32483
|
throw new UserError("commit message required");
|
|
31906
|
-
|
|
32484
|
+
const base = baseOf(this.d.state.requireRepo(wt.repoId));
|
|
32485
|
+
return this.fixShip(wt, await this.ship(wt.id, "commit", () => this.commitRecorded(wt, m)), base);
|
|
31907
32486
|
}
|
|
31908
32487
|
setEmpty(wt, empty) {
|
|
31909
32488
|
if (wt.empty === empty)
|
|
@@ -32397,6 +32976,7 @@ var drafts = new DraftStore({
|
|
|
32397
32976
|
var runs = new RunService({ state, hub });
|
|
32398
32977
|
var exec = new ExecService({ state, runtime, shipping: (id) => worktrees.shippingOf(id) });
|
|
32399
32978
|
var cache = new ArtifactCache({ paths, state, hub });
|
|
32979
|
+
var fix = new FixService({ state, hub, runtime });
|
|
32400
32980
|
var worktrees = new WorktreeService({
|
|
32401
32981
|
state,
|
|
32402
32982
|
hub,
|
|
@@ -32406,7 +32986,8 @@ var worktrees = new WorktreeService({
|
|
|
32406
32986
|
drafts,
|
|
32407
32987
|
cache,
|
|
32408
32988
|
watch: (id, command, run) => exec.watch(id, command, run),
|
|
32409
|
-
runs
|
|
32989
|
+
runs,
|
|
32990
|
+
fix
|
|
32410
32991
|
});
|
|
32411
32992
|
new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
|
|
32412
32993
|
var turns = new TurnService({
|
|
@@ -32506,6 +33087,13 @@ var idleExit = new IdleExit({
|
|
|
32506
33087
|
},
|
|
32507
33088
|
afterMs: stopAfter
|
|
32508
33089
|
});
|
|
33090
|
+
var keepAwake = new KeepAwake({
|
|
33091
|
+
hub,
|
|
33092
|
+
demand: () => runtime.demand(),
|
|
33093
|
+
assert: idleSleepAssertion(process.env.TOYON_KEEP_AWAKE),
|
|
33094
|
+
mode: () => state.keepAwake,
|
|
33095
|
+
answerable: remote !== null
|
|
33096
|
+
});
|
|
32509
33097
|
var { branded, stop: stopServer } = startServer({
|
|
32510
33098
|
port,
|
|
32511
33099
|
token,
|
|
@@ -32527,6 +33115,7 @@ var { branded, stop: stopServer } = startServer({
|
|
|
32527
33115
|
runtime,
|
|
32528
33116
|
idle,
|
|
32529
33117
|
idleExit,
|
|
33118
|
+
keepAwake,
|
|
32530
33119
|
exec,
|
|
32531
33120
|
runs,
|
|
32532
33121
|
refs,
|
|
@@ -32534,6 +33123,7 @@ var { branded, stop: stopServer } = startServer({
|
|
|
32534
33123
|
drafts,
|
|
32535
33124
|
prs,
|
|
32536
33125
|
landing,
|
|
33126
|
+
fix,
|
|
32537
33127
|
themes,
|
|
32538
33128
|
agents,
|
|
32539
33129
|
accounts,
|
|
@@ -32606,6 +33196,7 @@ async function shutdown(signal, opts = {}) {
|
|
|
32606
33196
|
shuttingDown = true;
|
|
32607
33197
|
log.info("daemon", `${signal}: stopping dev servers`);
|
|
32608
33198
|
idleExit.stop();
|
|
33199
|
+
keepAwake.stop();
|
|
32609
33200
|
idle.shutdown();
|
|
32610
33201
|
landing.stop();
|
|
32611
33202
|
runs.stopWatching();
|