toyon 0.10.0 → 0.11.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.
Files changed (27) hide show
  1. package/dist/cli.js +23 -23
  2. package/dist/daemon.js +1481 -440
  3. package/dist/shell/assets/{Editor-DL0q1Bu5.js → Editor-Cs_OeWSx.js} +3 -3
  4. package/dist/shell/assets/XTerm-Dc6Bk52B.js +58 -0
  5. package/dist/shell/assets/{cssMode-Et22hXpa.js → cssMode-D8uDYrHJ.js} +1 -1
  6. package/dist/shell/assets/{freemarker2-Clz20PcM.js → freemarker2-DlU9uDzS.js} +1 -1
  7. package/dist/shell/assets/{grammar-CPvdB3az.js → grammar-BSzyJU-q.js} +1 -1
  8. package/dist/shell/assets/{handlebars-B4nvXAx8.js → handlebars-B66kGi_7.js} +1 -1
  9. package/dist/shell/assets/{html-DPIGgBLK.js → html-D6juOeyd.js} +1 -1
  10. package/dist/shell/assets/{htmlMode-C2LaVuEX.js → htmlMode-CbTT-4ZV.js} +1 -1
  11. package/dist/shell/assets/index-B4KJ--q9.css +1 -0
  12. package/dist/shell/assets/index-Cj-6Xixu.js +268 -0
  13. package/dist/shell/assets/{javascript-CnwJfTbN.js → javascript-CHwG4FvH.js} +1 -1
  14. package/dist/shell/assets/{jsonMode-8znCTiqR.js → jsonMode-DzTEiaKX.js} +1 -1
  15. package/dist/shell/assets/{liquid-Bdm-1CUh.js → liquid-wddZX4zw.js} +1 -1
  16. package/dist/shell/assets/{lspLanguageFeatures-B1DMU7yH.js → lspLanguageFeatures-BlqDZ6dO.js} +1 -1
  17. package/dist/shell/assets/{mdx-Ktcqo9Jf.js → mdx-Dlz2LIv9.js} +1 -1
  18. package/dist/shell/assets/{python-QX--d8LR.js → python-DvkArqZY.js} +1 -1
  19. package/dist/shell/assets/{razor-C0LrXteG.js → razor-bDLQ0jQf.js} +1 -1
  20. package/dist/shell/assets/{tsMode-BtYCp6db.js → tsMode-B_NomV_y.js} +1 -1
  21. package/dist/shell/assets/{xml-BXbaQ9MG.js → xml-CBYJrQiV.js} +1 -1
  22. package/dist/shell/assets/{yaml-C6FhaU_C.js → yaml-DnmVK8S2.js} +1 -1
  23. package/dist/shell/index.html +2 -2
  24. package/package.json +1 -1
  25. package/dist/shell/assets/XTerm-C9YH4Lh2.js +0 -58
  26. package/dist/shell/assets/index-2QusTgw-.js +0 -262
  27. package/dist/shell/assets/index-TaJLoLSd.css +0 -1
package/dist/daemon.js CHANGED
@@ -4,6 +4,14 @@ import { rmSync as rmSync8, writeFileSync as writeFileSync10 } from "fs";
4
4
  import { dirname as dirname12 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
+ // ../shared/src/protocol/events.ts
8
+ var SHELL_TOOL = "shell";
9
+ var CHECK_TOOL = "check";
10
+ var TOOL_SEARCH = "ToolSearch";
11
+ var ASK_TOOL = "AskUserQuestion";
12
+ var TOYON_MCP_SERVER = "toyon";
13
+ var HANDOFF_TOOL = "handoff";
14
+
7
15
  // ../shared/src/agent-tools.ts
8
16
  var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
9
17
  var EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash"]);
@@ -16,6 +24,9 @@ var WRITTEN_KINDS = ["read", "edit", "delete", "move", "search", "execute", "fet
16
24
  function isWrittenKind(kind) {
17
25
  return WRITTEN_KINDS.includes(kind ?? "");
18
26
  }
27
+ function writingCall(call, input) {
28
+ return (isWrittenKind(call.kind) || call.name === ASK_TOOL) && emptyInput(input);
29
+ }
19
30
  function emptyInput(input) {
20
31
  if (!input || typeof input !== "object")
21
32
  return true;
@@ -271,6 +282,11 @@ function configSibling(rel, kind) {
271
282
  return place[kind];
272
283
  }
273
284
 
285
+ // ../shared/src/pair.ts
286
+ var PAIR_TTL_MS = 120000;
287
+ var pairLink = (host, code) => `https://${host}/#pair=${code}`;
288
+ var isTailnetName = (host) => host.endsWith(".ts.net");
289
+
274
290
  // ../shared/src/daemon.ts
275
291
  var DAEMON_FILES = {
276
292
  token: "token",
@@ -280,8 +296,18 @@ var DAEMON_FILES = {
280
296
  log: "daemon.log",
281
297
  remote: "remote.json"
282
298
  };
299
+ function machineLabel(hostname, remote) {
300
+ const of = (name) => name.split(".")[0]?.toLowerCase() ?? "";
301
+ if (remote && isTailnetName(remote.host))
302
+ return of(remote.host) || of(hostname);
303
+ return of(hostname) || "toyon";
304
+ }
305
+ var PREVIEW_GRANT_PARAM = "toyon_grant";
283
306
  var PREVIEW_PORTS = { from: 10001, to: 10008 };
284
307
  var DNS_NAME = /^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
308
+ function isLoopbackHost(host) {
309
+ return host === "127.0.0.1" || host === "::1" || host === "[::1]" || host === "localhost" || host.endsWith(".localhost");
310
+ }
285
311
  function isRemoteHost(name) {
286
312
  return DNS_NAME.test(name) && !/\.\d+$/.test(name) && name !== "localhost" && !name.endsWith(".localhost");
287
313
  }
@@ -6486,13 +6512,8 @@ function clipOutput(text) {
6486
6512
  ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6487
6513
  }
6488
6514
 
6489
- // ../shared/src/pair.ts
6490
- var PAIR_TTL_MS = 120000;
6491
- var pairLink = (host, code) => `https://${host}/#pair=${code}`;
6492
- var isTailnetName = (host) => host.endsWith(".ts.net");
6493
-
6494
6515
  // ../shared/src/protocol/limits.ts
6495
- var PROTOCOL_VERSION = 89;
6516
+ var PROTOCOL_VERSION = 93;
6496
6517
  var FILE_MAX_CHARS = 5000000;
6497
6518
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
6498
6519
  var IMAGE_MAX_EDGE = 2576;
@@ -6565,11 +6586,6 @@ var elementTraitsSchema = object({
6565
6586
  attrs: array(tuple([string2().max(40), string2().max(200)])).max(12)
6566
6587
  });
6567
6588
 
6568
- // ../shared/src/protocol/events.ts
6569
- var SHELL_TOOL = "shell";
6570
- var CHECK_TOOL = "check";
6571
- var TOOL_SEARCH = "ToolSearch";
6572
-
6573
6589
  // ../shared/src/protocol/ws.ts
6574
6590
  var id = string2().min(1).max(200);
6575
6591
  var permissionModeSchema = _enum(["auto", "ask", "plan"]);
@@ -6922,6 +6938,14 @@ var clientMsgSchema = discriminatedUnion("t", [
6922
6938
  answers: array(askAnswerSchema).max(8).optional()
6923
6939
  }),
6924
6940
  object({ t: literal("agent-decide"), worktreeId: id, askId: string2().max(64), choiceId: string2().max(200) }),
6941
+ object({
6942
+ t: literal("handoff-answer"),
6943
+ worktreeId: id,
6944
+ id: string2().max(64),
6945
+ go: boolean2(),
6946
+ note: string2().max(5000).optional()
6947
+ }),
6948
+ object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
6925
6949
  object({ t: literal("agent-logout"), agent: id }),
6926
6950
  object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
6927
6951
  object({ t: literal("reveal-agent-file"), agent: id, file: string2().max(64), repoId: id.optional() }),
@@ -8220,7 +8244,7 @@ function safeUser() {
8220
8244
  // ../daemon/package.json
8221
8245
  var package_default = {
8222
8246
  name: "@toyon/daemon",
8223
- version: "0.10.0",
8247
+ version: "0.11.0",
8224
8248
  private: true,
8225
8249
  type: "module",
8226
8250
  main: "src/index.ts",
@@ -12415,12 +12439,24 @@ class AgentAccounts {
12415
12439
  if (before.canLogout !== entry.canLogout || !same(before.status, entry.status))
12416
12440
  this.onChange?.();
12417
12441
  }
12442
+ observeLimits(agentId, limits) {
12443
+ const before = this.known.get(agentId) ?? { canLogout: false, status: undefined };
12444
+ const entry = { ...before, limits };
12445
+ this.known.set(agentId, entry);
12446
+ if (!sameLimits(before.limits, limits))
12447
+ this.onChange?.();
12448
+ }
12418
12449
  describe(infos) {
12419
12450
  return infos.map((info) => {
12420
12451
  const k = this.known.get(info.id);
12421
12452
  if (!k)
12422
12453
  return info;
12423
- return { ...info, ...k.status ? { auth: k.status } : {}, ...k.canLogout ? { canLogout: true } : {} };
12454
+ return {
12455
+ ...info,
12456
+ ...k.status ? { auth: k.status } : {},
12457
+ ...k.canLogout ? { canLogout: true } : {},
12458
+ ...k.limits ? { limits: k.limits } : {}
12459
+ };
12424
12460
  });
12425
12461
  }
12426
12462
  logout(agentId) {
@@ -12464,6 +12500,12 @@ class AgentAccounts {
12464
12500
  }
12465
12501
  }
12466
12502
  }
12503
+ function sameLimits(a, b) {
12504
+ if (!a || !b)
12505
+ return a === b;
12506
+ const win = (x) => x ? `${x.used}@${x.resetsAt}` : "";
12507
+ return a.status === b.status && a.binding === b.binding && win(a.windows.five_hour) === win(b.windows.five_hour) && win(a.windows.seven_day) === win(b.windows.seven_day);
12508
+ }
12467
12509
  function same(a, b) {
12468
12510
  if (!a || !b)
12469
12511
  return a === b;
@@ -12477,13 +12519,17 @@ import { Readable, Writable } from "stream";
12477
12519
  // ../daemon/src/runtime/kill.ts
12478
12520
  var KILL_GRACE_MS = 3000;
12479
12521
  var EXIT_GRACE_MS = 200;
12480
- async function killGroup(pid, exited) {
12522
+ async function killGroup(pid, exited, graceMs = KILL_GRACE_MS) {
12481
12523
  if (!signalGroup(pid, "SIGTERM"))
12482
12524
  return;
12483
- if (await within(exited, KILL_GRACE_MS))
12484
- return "SIGTERM";
12525
+ const deadline = Date.now() + graceMs;
12526
+ if (await within(exited, graceMs)) {
12527
+ await groupGone(pid, deadline);
12528
+ if (!groupAlive(pid))
12529
+ return "SIGTERM";
12530
+ }
12485
12531
  signalGroup(pid, "SIGKILL");
12486
- await within(exited, EXIT_GRACE_MS);
12532
+ await Promise.all([within(exited, EXIT_GRACE_MS), groupGone(pid, Date.now() + EXIT_GRACE_MS)]);
12487
12533
  return "SIGKILL";
12488
12534
  }
12489
12535
  async function killProcessGroup(child) {
@@ -12518,8 +12564,7 @@ function groupAlive(pgid) {
12518
12564
  return e.code !== "ESRCH";
12519
12565
  }
12520
12566
  }
12521
- function groupGone(pgid) {
12522
- const deadline = Date.now() + KILL_GRACE_MS + EXIT_GRACE_MS + 500;
12567
+ function groupGone(pgid, deadline = Date.now() + KILL_GRACE_MS + EXIT_GRACE_MS + 500) {
12523
12568
  return new Promise((resolve) => {
12524
12569
  const poll = () => {
12525
12570
  if (!groupAlive(pgid) || Date.now() >= deadline)
@@ -12949,6 +12994,303 @@ class LinuxSandbox {
12949
12994
  }
12950
12995
  }
12951
12996
 
12997
+ // ../daemon/src/agent/mcp.ts
12998
+ import { randomBytes as randomBytes2 } from "crypto";
12999
+
13000
+ // ../daemon/src/core/remote.ts
13001
+ import { createHmac, timingSafeEqual } from "crypto";
13002
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
13003
+ function loadRemote(file, env = process.env, managed = MANAGED_DEFAULTS) {
13004
+ if (env.TOYON_CLOUD === "1") {
13005
+ const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
13006
+ if (!isRemoteHost(host)) {
13007
+ throw new Error(`TOYON_CLOUD=1 needs TOYON_PUBLIC_HOST, the name the edge answers for; got "${host}"`);
13008
+ }
13009
+ const previews = env.TOYON_PREVIEWS ?? portPreviews(host);
13010
+ const why = checkPreviews(previews, host);
13011
+ if (why)
13012
+ throw new Error(`TOYON_PREVIEWS: ${why}`);
13013
+ return { host, previews, front: "edge" };
13014
+ }
13015
+ if (!existsSync(file))
13016
+ return null;
13017
+ if (managed.remote === "off") {
13018
+ log.warn("remote", `${file} is ignored: remote access is turned off by your organization's policy`);
13019
+ return null;
13020
+ }
13021
+ const view = parseRemote(readFileSync2(file, "utf8"));
13022
+ if (!view) {
13023
+ log.warn("remote", `${file} names no valid host or previews; remote access is off`);
13024
+ return null;
13025
+ }
13026
+ if (managed.remote === "tailscale" && !view.host.endsWith(".ts.net")) {
13027
+ log.warn("remote", `${file} names ${view.host}; your organization's policy allows a tailnet name only, so remote access is off`);
13028
+ return null;
13029
+ }
13030
+ return { ...view, front: "local" };
13031
+ }
13032
+ function sameSecret(given, want) {
13033
+ if (given === null)
13034
+ return false;
13035
+ const a = Buffer.from(given);
13036
+ const b = Buffer.from(want);
13037
+ return a.length === b.length && timingSafeEqual(a, b);
13038
+ }
13039
+ var isLoopbackPeer = (ip) => ip === "127.0.0.1" || ip === "::1" || ip.startsWith("::ffff:127.");
13040
+ function requestedAuthority(req) {
13041
+ const host = (req.headers.get("host") ?? "").toLowerCase();
13042
+ if (host.includes(":"))
13043
+ return host;
13044
+ const port = req.headers.get("x-forwarded-port");
13045
+ return port && /^\d{1,5}$/.test(port) && port !== "443" ? `${host}:${port}` : host;
13046
+ }
13047
+ var forbidden = (body = "forbidden") => ({ kind: "refused", response: new Response(body, { status: 403 }) });
13048
+ function toHttps(req, host) {
13049
+ const url = new URL(req.url);
13050
+ const location = `https://${host}${url.pathname}${url.search}`;
13051
+ return {
13052
+ kind: "refused",
13053
+ response: new Response(`open ${location} instead`, {
13054
+ status: 302,
13055
+ headers: { location, "cache-control": "no-store" }
13056
+ })
13057
+ };
13058
+ }
13059
+ function door(req, peer, remote, listener) {
13060
+ if (remote === null && listener === "preview")
13061
+ return { kind: "local" };
13062
+ if (remote?.front !== "edge" && !isLoopbackPeer(peer)) {
13063
+ const typed = requestedAuthority(req).replace(/:80$/, "");
13064
+ const browsing = req.method === "GET" || req.method === "HEAD";
13065
+ return remote && listener === "daemon" && browsing && typed === remote.host ? toHttps(req, remote.host) : forbidden();
13066
+ }
13067
+ const authority = requestedAuthority(req);
13068
+ const host = authority.split(":")[0] ?? "";
13069
+ if (isLoopbackHost(host)) {
13070
+ return remote?.front === "edge" ? forbidden() : { kind: "local" };
13071
+ }
13072
+ if (remote === null)
13073
+ return forbidden();
13074
+ const preview = matchPreview(remote.previews, authority);
13075
+ let worktreeId = null;
13076
+ if (listener === "daemon") {
13077
+ if (preview !== null && "id" in preview)
13078
+ worktreeId = preview.id;
13079
+ else if (preview !== null || host !== remote.host)
13080
+ return forbidden();
13081
+ } else if (preview === null || "id" in preview) {
13082
+ return forbidden();
13083
+ }
13084
+ if (req.headers.get("x-forwarded-proto") !== "https") {
13085
+ return forbidden(`${host} reaches Toyon over https only; the front must terminate TLS`);
13086
+ }
13087
+ if (listener === "preview")
13088
+ return { kind: "preview", worktreeId: null };
13089
+ return worktreeId === null ? { kind: "shell" } : { kind: "preview", worktreeId };
13090
+ }
13091
+ var PREVIEW_COOKIE = "toyon_preview";
13092
+ function previewGrant(token) {
13093
+ return createHmac("sha256", token).update("toyon preview grant").digest("hex");
13094
+ }
13095
+ function grantCookie(grant, name, opts = {}) {
13096
+ const site = opts.partitioned ? "SameSite=None; Partitioned" : "SameSite=Strict";
13097
+ return `${PREVIEW_COOKIE}=${grant}; Domain=${name}; Path=/; Max-Age=34560000; HttpOnly; Secure; ${site}`;
13098
+ }
13099
+ function takeGrant(cookie, grant) {
13100
+ let ok = false;
13101
+ const kept = [];
13102
+ for (const part of (cookie ?? "").split(";")) {
13103
+ const pair = part.trim();
13104
+ if (pair === "")
13105
+ continue;
13106
+ const eq = pair.indexOf("=");
13107
+ if (eq > 0 && pair.slice(0, eq) === PREVIEW_COOKIE) {
13108
+ if (sameSecret(pair.slice(eq + 1), grant))
13109
+ ok = true;
13110
+ continue;
13111
+ }
13112
+ kept.push(pair);
13113
+ }
13114
+ return { ok, rest: kept.length ? kept.join("; ") : null };
13115
+ }
13116
+ function passPreview(req, gate) {
13117
+ const pass = takeGrant(req.headers.get("cookie"), gate.grant);
13118
+ const url = new URL(req.url);
13119
+ const code = url.searchParams.get(PREVIEW_GRANT_PARAM);
13120
+ if (code !== null) {
13121
+ url.searchParams.delete(PREVIEW_GRANT_PARAM);
13122
+ const location = `${url.pathname}${url.search}`;
13123
+ if (!pass.ok && !gate.redeem(code))
13124
+ return { ok: false, response: refusedPreview() };
13125
+ const headers = { location, "cache-control": "no-store" };
13126
+ if (!pass.ok) {
13127
+ const partitioned = req.headers.get("sec-fetch-site") === "cross-site";
13128
+ headers["set-cookie"] = grantCookie(gate.grant, gate.host, { partitioned });
13129
+ }
13130
+ return { ok: false, response: new Response(null, { status: 302, headers }) };
13131
+ }
13132
+ if (!pass.ok)
13133
+ return { ok: false, response: refusedPreview() };
13134
+ const headers = new Headers(req.headers);
13135
+ if (pass.rest === null)
13136
+ headers.delete("cookie");
13137
+ else
13138
+ headers.set("cookie", pass.rest);
13139
+ return { ok: true, req: new Request(req, { headers }) };
13140
+ }
13141
+ function refusedPreview() {
13142
+ return new Response(`<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="3"></head>` + `<body>This preview opens from Toyon; open your Toyon link first.</body></html>`, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
13143
+ }
13144
+ function setsGrant(line) {
13145
+ return line.slice(0, line.indexOf("=")).trim() === PREVIEW_COOKIE;
13146
+ }
13147
+
13148
+ // ../daemon/src/agent/mcp.ts
13149
+ class ToolSet {
13150
+ providers = [];
13151
+ add(provider) {
13152
+ this.providers.push(provider);
13153
+ }
13154
+ list(worktreeId) {
13155
+ return this.providers.map((p) => p.tool(worktreeId));
13156
+ }
13157
+ async call(worktreeId, name, args) {
13158
+ const provider = this.providers.find((p) => p.tool(worktreeId).name === name);
13159
+ if (!provider)
13160
+ return { text: `unknown tool ${name}`, isError: true };
13161
+ return provider.call(worktreeId, args);
13162
+ }
13163
+ }
13164
+ var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
13165
+ var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
13166
+ var BODY_MAX_BYTES = 64 * 1024;
13167
+ var PARSE_ERROR = -32700;
13168
+ var INVALID_PARAMS = -32602;
13169
+ var METHOD_NOT_FOUND = -32601;
13170
+ var INTERNAL_ERROR = -32603;
13171
+ var rpcError = (id, code, message) => ({
13172
+ jsonrpc: "2.0",
13173
+ id,
13174
+ error: { code, message }
13175
+ });
13176
+ var asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : null;
13177
+
13178
+ class ToyonMcp {
13179
+ d;
13180
+ bearers = new Map;
13181
+ constructor(d) {
13182
+ this.d = d;
13183
+ }
13184
+ open(worktreeId) {
13185
+ const bearer = randomBytes2(32).toString("base64url");
13186
+ this.bearers.set(worktreeId, bearer);
13187
+ return {
13188
+ type: "http",
13189
+ name: TOYON_MCP_SERVER,
13190
+ url: this.d.url(worktreeId),
13191
+ headers: [{ name: "Authorization", value: `Bearer ${bearer}` }]
13192
+ };
13193
+ }
13194
+ close(worktreeId) {
13195
+ this.bearers.delete(worktreeId);
13196
+ }
13197
+ async fetch(req, worktreeId) {
13198
+ if (req.headers.get("origin") !== null)
13199
+ return new Response("forbidden", { status: 403 });
13200
+ if (req.method !== "POST")
13201
+ return new Response("method not allowed", { status: 405, headers: { allow: "POST" } });
13202
+ const bearer = this.bearers.get(worktreeId);
13203
+ if (!bearer || !sameSecret(req.headers.get("authorization"), `Bearer ${bearer}`)) {
13204
+ return new Response("unauthorized", { status: 401 });
13205
+ }
13206
+ const body = await readCapped(req, BODY_MAX_BYTES);
13207
+ if (body === null)
13208
+ return new Response("request body too large", { status: 413 });
13209
+ let parsed;
13210
+ try {
13211
+ parsed = JSON.parse(body);
13212
+ } catch {
13213
+ return Response.json(rpcError(null, PARSE_ERROR, "parse error"), { status: 400 });
13214
+ }
13215
+ const batch = Array.isArray(parsed);
13216
+ const messages = batch ? parsed : [parsed];
13217
+ const replies = [];
13218
+ for (const m of messages) {
13219
+ const reply = await this.handle(worktreeId, m);
13220
+ if (reply !== undefined)
13221
+ replies.push(reply);
13222
+ }
13223
+ if (replies.length === 0)
13224
+ return new Response(null, { status: 202 });
13225
+ return Response.json(batch ? replies : replies[0], {
13226
+ status: 200,
13227
+ headers: { "cache-control": "no-store" }
13228
+ });
13229
+ }
13230
+ async handle(worktreeId, raw) {
13231
+ const m = asRecord(raw);
13232
+ if (!m || typeof m.method !== "string")
13233
+ return rpcError(m?.id ?? null, PARSE_ERROR, "parse error");
13234
+ const method = m.method;
13235
+ const id = m.id;
13236
+ const params = asRecord(m.params) ?? {};
13237
+ if (method.startsWith("notifications/"))
13238
+ return;
13239
+ if (id === undefined)
13240
+ return;
13241
+ const ok = (result) => ({ jsonrpc: "2.0", id, result });
13242
+ switch (method) {
13243
+ case "initialize": {
13244
+ const asked = params.protocolVersion;
13245
+ const protocolVersion = typeof asked === "string" && PROTOCOL_VERSIONS.includes(asked) ? asked : DEFAULT_PROTOCOL_VERSION;
13246
+ return ok({
13247
+ protocolVersion,
13248
+ capabilities: { tools: { listChanged: false } },
13249
+ serverInfo: { name: TOYON_MCP_SERVER, version: this.d.version }
13250
+ });
13251
+ }
13252
+ case "ping":
13253
+ return ok({});
13254
+ case "tools/list": {
13255
+ const tools = this.d.tools.list(worktreeId);
13256
+ log.debug(worktreeId, `mcp: tools/list answered with ${tools.map((t) => t.name).join(", ") || "nothing"}`);
13257
+ return ok({ tools });
13258
+ }
13259
+ case "tools/call": {
13260
+ const name = params.name;
13261
+ if (typeof name !== "string" || !this.d.tools.list(worktreeId).some((t) => t.name === name)) {
13262
+ return rpcError(id, INVALID_PARAMS, `unknown tool ${typeof name === "string" ? name : ""}`.trim());
13263
+ }
13264
+ try {
13265
+ const r = await this.d.tools.call(worktreeId, name, params.arguments ?? {});
13266
+ return ok({ content: [{ type: "text", text: r.text }], ...r.isError ? { isError: true } : {} });
13267
+ } catch (e) {
13268
+ if (e instanceof UserError)
13269
+ return ok({ content: [{ type: "text", text: e.message }], isError: true });
13270
+ log.error(worktreeId, `mcp: ${name} failed`, e);
13271
+ return rpcError(id, INTERNAL_ERROR, "internal error");
13272
+ }
13273
+ }
13274
+ default:
13275
+ return rpcError(id, METHOD_NOT_FOUND, `method not found: ${method}`);
13276
+ }
13277
+ }
13278
+ }
13279
+ async function readCapped(req, max) {
13280
+ const body = req.body;
13281
+ if (!body)
13282
+ return "";
13283
+ const chunks = [];
13284
+ let total = 0;
13285
+ for await (const chunk of body) {
13286
+ total += chunk.byteLength;
13287
+ if (total > max)
13288
+ return null;
13289
+ chunks.push(chunk);
13290
+ }
13291
+ return Buffer.concat(chunks).toString("utf8");
13292
+ }
13293
+
12952
13294
  // ../daemon/src/agent/acp/options.ts
12953
13295
  var OPTION_FIELDS = { model: "model", thought_level: "effort" };
12954
13296
  var OPTION_CATEGORIES = Object.keys(OPTION_FIELDS);
@@ -13050,7 +13392,7 @@ class OptionProbe {
13050
13392
  }
13051
13393
 
13052
13394
  // ../daemon/src/agent/registry.ts
13053
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
13395
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
13054
13396
  import { homedir as homedir3 } from "os";
13055
13397
  import { dirname as dirname3, isAbsolute, join as join6, resolve as resolve3 } from "path";
13056
13398
 
@@ -13078,7 +13420,7 @@ var cloud = {
13078
13420
  // ../daemon/src/git/exec.ts
13079
13421
  import { spawn as spawn2 } from "child_process";
13080
13422
  import { createHash as createHash2 } from "crypto";
13081
- import { existsSync, readFileSync as readFileSync2 } from "fs";
13423
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
13082
13424
  import { join as join3 } from "path";
13083
13425
 
13084
13426
  // ../daemon/src/core/metrics.ts
@@ -13121,7 +13463,7 @@ function lockfileHash(dir) {
13121
13463
  const h = createHash2("sha1");
13122
13464
  for (const f of LOCKFILES) {
13123
13465
  try {
13124
- h.update(readFileSync2(join3(dir, f)));
13466
+ h.update(readFileSync3(join3(dir, f)));
13125
13467
  } catch {}
13126
13468
  }
13127
13469
  return h.digest("hex");
@@ -13138,7 +13480,7 @@ function resolveGit() {
13138
13480
  "/opt/homebrew/bin/git",
13139
13481
  "/usr/local/bin/git"
13140
13482
  ]) {
13141
- if (existsSync(p))
13483
+ if (existsSync2(p))
13142
13484
  return p;
13143
13485
  }
13144
13486
  return "git";
@@ -13441,7 +13783,7 @@ function confine(launch, b, extraWrite = [], platform = process.platform, which
13441
13783
  }
13442
13784
 
13443
13785
  // ../daemon/src/agent/native.ts
13444
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
13786
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
13445
13787
  var OS = { darwin: "darwin", linux: "linux", win32: "windows" };
13446
13788
  function nativePackage(pkg, host) {
13447
13789
  const os = OS[host.platform];
@@ -13464,14 +13806,14 @@ function hostTraits() {
13464
13806
  }
13465
13807
  function muslLoader() {
13466
13808
  try {
13467
- return existsSync2("/lib") && readdirSync2("/lib").some((f) => f.startsWith("ld-musl-"));
13809
+ return existsSync3("/lib") && readdirSync2("/lib").some((f) => f.startsWith("ld-musl-"));
13468
13810
  } catch {
13469
13811
  return false;
13470
13812
  }
13471
13813
  }
13472
13814
  function cpuFlags() {
13473
13815
  try {
13474
- return readFileSync3("/proc/cpuinfo", "utf8").match(/^flags\s*:(.*)$/m)?.[1]?.trim().split(/\s+/) ?? [];
13816
+ return readFileSync4("/proc/cpuinfo", "utf8").match(/^flags\s*:(.*)$/m)?.[1]?.trim().split(/\s+/) ?? [];
13475
13817
  } catch {
13476
13818
  return [];
13477
13819
  }
@@ -13534,7 +13876,7 @@ function opencodeEnv() {
13534
13876
  }
13535
13877
 
13536
13878
  // ../daemon/src/agent/sandbox.ts
13537
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "fs";
13879
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, writeFileSync as writeFileSync2 } from "fs";
13538
13880
  import { homedir as homedir2, tmpdir } from "os";
13539
13881
  import { join as join5, resolve as resolve2 } from "path";
13540
13882
 
@@ -13579,7 +13921,7 @@ function ensureDirs(p) {
13579
13921
  }
13580
13922
 
13581
13923
  // ../daemon/src/git/exclude.ts
13582
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync } from "fs";
13924
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync } from "fs";
13583
13925
  import { dirname } from "path";
13584
13926
  async function excludeFile(cwd) {
13585
13927
  const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
@@ -13592,7 +13934,7 @@ async function excludeFromGit(cwd, pattern) {
13592
13934
  const file = await excludeFile(cwd);
13593
13935
  if (!file)
13594
13936
  return;
13595
- const current = existsSync3(file) ? readFileSync4(file, "utf8") : "";
13937
+ const current = existsSync4(file) ? readFileSync5(file, "utf8") : "";
13596
13938
  if (current.split(`
13597
13939
  `).some((l) => l.trim() === pattern))
13598
13940
  return;
@@ -13608,7 +13950,7 @@ async function excludeBlock(cwd, name, patterns) {
13608
13950
  return;
13609
13951
  const open = `# toyon ${name}`;
13610
13952
  const close = `# toyon end ${name}`;
13611
- const current = existsSync3(file) ? readFileSync4(file, "utf8") : "";
13953
+ const current = existsSync4(file) ? readFileSync5(file, "utf8") : "";
13612
13954
  const lines = current.split(`
13613
13955
  `);
13614
13956
  const from = lines.indexOf(open);
@@ -13627,9 +13969,9 @@ async function excludeBlock(cwd, name, patterns) {
13627
13969
  }
13628
13970
  async function unexcludeFromGit(cwd, pattern) {
13629
13971
  const file = await excludeFile(cwd);
13630
- if (!file || !existsSync3(file))
13972
+ if (!file || !existsSync4(file))
13631
13973
  return;
13632
- const lines = readFileSync4(file, "utf8").split(`
13974
+ const lines = readFileSync5(file, "utf8").split(`
13633
13975
  `);
13634
13976
  const kept = lines.filter((l) => l.trim() !== pattern);
13635
13977
  if (kept.length === lines.length)
@@ -13639,12 +13981,12 @@ async function unexcludeFromGit(cwd, pattern) {
13639
13981
  }
13640
13982
 
13641
13983
  // ../daemon/src/agent/bounds.ts
13642
- import { existsSync as existsSync4, realpathSync } from "fs";
13984
+ import { existsSync as existsSync5, realpathSync } from "fs";
13643
13985
  import { basename, dirname as dirname2, resolve, sep } from "path";
13644
13986
  function canonical(p) {
13645
13987
  let probe = p;
13646
13988
  const tail = [];
13647
- while (!existsSync4(probe)) {
13989
+ while (!existsSync5(probe)) {
13648
13990
  const parent = dirname2(probe);
13649
13991
  if (parent === probe)
13650
13992
  return p;
@@ -13706,9 +14048,9 @@ async function writeClaudeLocalSettings(cwd, b) {
13706
14048
  const file = join5(cwd, SETTINGS_REL);
13707
14049
  mkdirSync4(join5(cwd, ".claude"), { recursive: true });
13708
14050
  let existing = {};
13709
- if (existsSync5(file)) {
14051
+ if (existsSync6(file)) {
13710
14052
  try {
13711
- const parsed = JSON.parse(readFileSync5(file, "utf8"));
14053
+ const parsed = JSON.parse(readFileSync6(file, "utf8"));
13712
14054
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
13713
14055
  existing = parsed;
13714
14056
  } catch (e) {
@@ -13726,7 +14068,7 @@ async function writeClaudeLocalSettings(cwd, b) {
13726
14068
  writeFileSync2(tmp, `${JSON.stringify(next, null, 2)}
13727
14069
  `);
13728
14070
  renameSync(tmp, file);
13729
- if (existsSync5(join5(cwd, ".git")))
14071
+ if (existsSync6(join5(cwd, ".git")))
13730
14072
  await excludeFromGit(cwd, SETTINGS_REL);
13731
14073
  }
13732
14074
 
@@ -13740,7 +14082,10 @@ var BUILTIN_AGENTS = [
13740
14082
  short: "Claude",
13741
14083
  builtin: true,
13742
14084
  run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.84.0", bin: "claude-agent-acp" },
13743
- ...cloud.enabled ? { env: { NO_BROWSER: "1" } } : {},
14085
+ env: {
14086
+ CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: "1",
14087
+ ...cloud.enabled ? { NO_BROWSER: "1" } : {}
14088
+ },
13744
14089
  terminalLogins: {
13745
14090
  "claude-login": { args: ["--cli", "auth", "login", "--claudeai"], env: { NO_BROWSER: "1" }, plan: true }
13746
14091
  },
@@ -13854,18 +14199,18 @@ class AgentRegistry {
13854
14199
  const pkgDir = join6(this.dir, spec.id, "node_modules", name);
13855
14200
  if (spec.run.kind === "npm-native") {
13856
14201
  const bin = join6(pkgDir, spec.run.bin);
13857
- return existsSync6(bin) ? bin : null;
14202
+ return existsSync7(bin) ? bin : null;
13858
14203
  }
13859
14204
  const pkgJson = join6(pkgDir, "package.json");
13860
- if (!existsSync6(pkgJson))
14205
+ if (!existsSync7(pkgJson))
13861
14206
  return null;
13862
14207
  try {
13863
- const meta = JSON.parse(readFileSync6(pkgJson, "utf8"));
14208
+ const meta = JSON.parse(readFileSync7(pkgJson, "utf8"));
13864
14209
  const rel = typeof meta.bin === "string" ? meta.bin : meta.bin?.[spec.run.bin];
13865
14210
  if (!rel)
13866
14211
  return null;
13867
14212
  const script = join6(dirname3(pkgJson), rel);
13868
- return existsSync6(script) ? script : null;
14213
+ return existsSync7(script) ? script : null;
13869
14214
  } catch (e) {
13870
14215
  log.warn("agents", `cannot read ${pkgJson}`, e);
13871
14216
  return null;
@@ -13885,7 +14230,7 @@ class AgentRegistry {
13885
14230
  return this.installErrors.get(spec.id) ?? (spec.onDemand ? "not installed" : "not installed yet");
13886
14231
  }
13887
14232
  const cmd = spec.run.command;
13888
- if (isAbsolute(cmd) ? existsSync6(cmd) : Bun.which(cmd))
14233
+ if (isAbsolute(cmd) ? existsSync7(cmd) : Bun.which(cmd))
13889
14234
  return null;
13890
14235
  return `${cmd} not found on PATH`;
13891
14236
  }
@@ -13947,7 +14292,7 @@ class AgentRegistry {
13947
14292
  return null;
13948
14293
  const pkgJson = join6(this.dir, spec.id, "node_modules", name, "package.json");
13949
14294
  try {
13950
- return JSON.parse(readFileSync6(pkgJson, "utf8")).version ?? null;
14295
+ return JSON.parse(readFileSync7(pkgJson, "utf8")).version ?? null;
13951
14296
  } catch {
13952
14297
  return null;
13953
14298
  }
@@ -13990,8 +14335,9 @@ class AgentRegistry {
13990
14335
  }
13991
14336
  }
13992
14337
  var ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
13993
- function parseCustomAgents(raw) {
13994
- const out = [];
14338
+ var isRecord2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
14339
+ function parseAgentsFile(raw, builtins) {
14340
+ const out = [...builtins];
13995
14341
  let parsed;
13996
14342
  try {
13997
14343
  parsed = JSON.parse(raw);
@@ -13999,7 +14345,7 @@ function parseCustomAgents(raw) {
13999
14345
  log.warn("agents", "agents.json is not valid JSON; ignoring it", e);
14000
14346
  return out;
14001
14347
  }
14002
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
14348
+ if (!isRecord2(parsed)) {
14003
14349
  log.warn("agents", "agents.json must be an object keyed by agent id; ignoring it");
14004
14350
  return out;
14005
14351
  }
@@ -14007,24 +14353,42 @@ function parseCustomAgents(raw) {
14007
14353
  const e = v ?? {};
14008
14354
  const str = (k) => typeof e[k] === "string" && e[k].trim() ? e[k] : undefined;
14009
14355
  const command = str("command");
14010
- const why = !ID_RE.test(id) ? "id must be lowercase letters, digits and dashes" : !command ? 'needs a non-empty "command"' : e.args !== undefined && !(Array.isArray(e.args) && e.args.every((a) => typeof a === "string")) ? '"args" must be an array of strings' : e.env !== undefined && !(e.env && typeof e.env === "object" && Object.values(e.env).every((x) => typeof x === "string")) ? '"env" must be an object of strings' : e.confinement !== undefined && (e.confinement === "claude-settings" || !CONFINEMENTS.includes(e.confinement)) ? '"confinement" must be "none", "adapter-sandbox" or "toyon-sandbox"' : null;
14356
+ const at = out.findIndex((s) => s.id === id);
14357
+ const tunes = !command && at >= 0 && out[at].builtin;
14358
+ const why = !ID_RE.test(id) ? "id must be lowercase letters, digits and dashes" : !command && !tunes ? 'needs a non-empty "command" (only a builtin is tuned without one)' : e.args !== undefined && !(Array.isArray(e.args) && e.args.every((a) => typeof a === "string")) ? '"args" must be an array of strings' : e.env !== undefined && !(isRecord2(e.env) && Object.values(e.env).every((x) => typeof x === "string")) ? '"env" must be an object of strings' : e.meta !== undefined && !isRecord2(e.meta) ? '"meta" must be an object' : e.confinement !== undefined && (e.confinement === "claude-settings" || !CONFINEMENTS.includes(e.confinement)) ? '"confinement" must be "none", "adapter-sandbox" or "toyon-sandbox"' : null;
14011
14359
  if (why) {
14012
14360
  log.warn("agents", `agents.json: skipping "${id}": ${why}`);
14013
14361
  continue;
14014
14362
  }
14015
- out.push({
14363
+ const env = e.env;
14364
+ const meta = e.meta;
14365
+ if (tunes) {
14366
+ const base = out[at];
14367
+ out[at] = {
14368
+ ...base,
14369
+ ...env ? { env: { ...base.env, ...env } } : {},
14370
+ ...meta ? { meta } : {}
14371
+ };
14372
+ continue;
14373
+ }
14374
+ const spec = {
14016
14375
  id,
14017
14376
  name: str("name") ?? id,
14018
14377
  builtin: false,
14019
14378
  run: { kind: "command", command, ...e.args ? { args: e.args } : {} },
14020
- ...e.env ? { env: e.env } : {},
14379
+ ...env ? { env } : {},
14380
+ ...meta ? { meta } : {},
14021
14381
  confinement: e.confinement ?? "none",
14022
14382
  systemPrompt: "prompt-prefix",
14023
14383
  ...str("mode") ? { mode: str("mode") } : {},
14024
14384
  ...str("planMode") ? { modes: { plan: str("planMode"), build: str("mode") } } : {},
14025
14385
  ...str("quickModel") ? { quickModel: str("quickModel") } : {},
14026
14386
  loginHint: str("loginHint") ?? `${str("name") ?? id} is not logged in`
14027
- });
14387
+ };
14388
+ if (at >= 0)
14389
+ out[at] = spec;
14390
+ else
14391
+ out.push(spec);
14028
14392
  }
14029
14393
  return out;
14030
14394
  }
@@ -14033,8 +14397,8 @@ function managedSpecs(specs, managed) {
14033
14397
  return managed.agents === null ? kept : kept.filter((s) => managed.agents?.includes(s.id));
14034
14398
  }
14035
14399
  function loadAgentRegistry(agentsFile, agentsDir, sandbox, managed = MANAGED_DEFAULTS) {
14036
- const custom = managed.customAgents && existsSync6(agentsFile) ? parseCustomAgents(readFileSync6(agentsFile, "utf8")) : [];
14037
- return new AgentRegistry(managedSpecs([...BUILTIN_AGENTS, ...custom], managed), agentsDir, undefined, undefined, sandbox);
14400
+ const specs = managed.customAgents && existsSync7(agentsFile) ? parseAgentsFile(readFileSync7(agentsFile, "utf8"), BUILTIN_AGENTS) : BUILTIN_AGENTS;
14401
+ return new AgentRegistry(managedSpecs(specs, managed), agentsDir, undefined, undefined, sandbox);
14038
14402
  }
14039
14403
 
14040
14404
  // ../daemon/src/agent/acp/session.ts
@@ -14184,6 +14548,7 @@ function pickOption(options, verdict) {
14184
14548
  var SYSTEM_APPEND = [
14185
14549
  "You are working inside a dedicated git worktree managed by Toyon.",
14186
14550
  "Make every change inside the current working directory and never modify files outside it. Reading files outside it is fine when the user points you there.",
14551
+ "When a change belongs in another project Toyon has open rather than in this worktree, do not edit outside it and do not stop at saying so: propose it with the `handoff` tool when you have one, naming the project and writing the message its agent will start from, with what is needed and why, how to reproduce it, and the paths here that depend on it; the user decides on a card, and once it is proposed you finish your turn.",
14187
14552
  "A write Toyon refuses (outside the worktree, or to an agent's own settings such as .claude/ or opencode.json) was refused by Toyon and not by the user, who was never asked. Report it as the boundary that stopped it, never as the user declining.",
14188
14553
  "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
14189
14554
  "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.",
@@ -14304,6 +14669,22 @@ function procDiagnosis(p) {
14304
14669
  return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
14305
14670
  return "It never answered on that port.";
14306
14671
  }
14672
+ function handoffPrompt(message, note, origin) {
14673
+ return [
14674
+ message,
14675
+ note ? `The person added, when they approved this: ${note}` : undefined,
14676
+ `This task was handed off from the worktree at ${origin.path} of the project ${origin.project}, on its branch ${origin.branch}. The message above was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`
14677
+ ].filter((p) => !!p).join(`
14678
+
14679
+ `);
14680
+ }
14681
+ function handoffAskPrompt(repo, text) {
14682
+ const own = text?.trim();
14683
+ return `The user wants to continue this work in ${repo.name} (${repo.path}), another project open in Toyon. Propose it with the handoff tool: project "${repo.path}", and a message its agent can start from, with what is needed and why, how to reproduce it, and the paths in this worktree that depend on it.` + (own ? ` The user's own words for what ${repo.name} should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
14684
+ }
14685
+ function handoffLandedContext(title, project, url) {
14686
+ return `The work this worktree handed off to ${project}, "${title}", has landed there${url ? ` (${url})` : ""}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
14687
+ }
14307
14688
  function ambientBlock(paragraphs) {
14308
14689
  const said = paragraphs.filter((p) => !!p);
14309
14690
  if (said.length === 0)
@@ -14411,7 +14792,7 @@ ${a.text}
14411
14792
  }
14412
14793
 
14413
14794
  // ../daemon/src/agent/transcript.ts
14414
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
14795
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
14415
14796
  import { appendFile } from "fs/promises";
14416
14797
  import { join as join7 } from "path";
14417
14798
  function transcriptPathFor(transcriptsDir, worktreeId) {
@@ -14469,7 +14850,7 @@ class Transcript {
14469
14850
  this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
14470
14851
  }
14471
14852
  read() {
14472
- return existsSync7(this.path) ? parseTranscript(readFileSync7(this.path, "utf8"), this.tag) : [];
14853
+ return existsSync8(this.path) ? parseTranscript(readFileSync8(this.path, "utf8"), this.tag) : [];
14473
14854
  }
14474
14855
  append(event) {
14475
14856
  const entry = { seq: this.seq++, event };
@@ -14567,7 +14948,7 @@ async function cleanUp(ctx, a, sessionId) {
14567
14948
  }
14568
14949
 
14569
14950
  // ../daemon/src/agent/acp/background.ts
14570
- import { existsSync as existsSync8, statSync as statSync3 } from "fs";
14951
+ import { existsSync as existsSync9, statSync as statSync3 } from "fs";
14571
14952
  import { open as open2 } from "fs/promises";
14572
14953
  import { homedir as homedir4 } from "os";
14573
14954
  import { basename as basename2, dirname as dirname4, join as join8 } from "path";
@@ -14591,7 +14972,7 @@ function sessionLogFor(file, configDir = claudeConfigDir()) {
14591
14972
  return null;
14592
14973
  const session = dirname4(tasks);
14593
14974
  const path = join8(configDir, "projects", basename2(dirname4(session)), `${basename2(session)}.jsonl`);
14594
- return existsSync8(path) ? path : null;
14975
+ return existsSync9(path) ? path : null;
14595
14976
  }
14596
14977
  function taskEnd(text, toolId) {
14597
14978
  const at = text.indexOf(`<tool-use-id>${toolId}</tool-use-id>`);
@@ -14900,25 +15281,25 @@ function unifiedDiff(oldText, newText, context = 3) {
14900
15281
 
14901
15282
  // ../daemon/src/agent/acp/map.ts
14902
15283
  function spawnOf(meta) {
14903
- const claude = asRecord(asRecord(meta).claudeCode);
15284
+ const claude = asRecord2(asRecord2(meta).claudeCode);
14904
15285
  const parentToolId = typeof claude.parentToolUseId === "string" ? claude.parentToolUseId : undefined;
14905
- const codex = asRecord(asRecord(meta).codex).subagent;
15286
+ const codex = asRecord2(asRecord2(meta).codex).subagent;
14906
15287
  const spawns = claude.toolName === "Agent" || claude.toolName === "Task";
14907
- const subagent = spawns || claude.subagent === true || Object.keys(asRecord(codex)).length > 0;
15288
+ const subagent = spawns || claude.subagent === true || Object.keys(asRecord2(codex)).length > 0;
14908
15289
  return { ...parentToolId ? { parentToolId } : {}, ...subagent ? { subagent: true } : {} };
14909
15290
  }
14910
- function asRecord(v) {
15291
+ function asRecord2(v) {
14911
15292
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
14912
15293
  }
14913
15294
  var NETWORK_ASK = "SandboxNetworkAccess";
14914
15295
  function isNetworkAsk(update) {
14915
- return update.name === NETWORK_ASK || asRecord(asRecord(update._meta).claudeCode).toolName === NETWORK_ASK;
15296
+ return update.name === NETWORK_ASK || asRecord2(asRecord2(update._meta).claudeCode).toolName === NETWORK_ASK;
14916
15297
  }
14917
15298
  function isToolSearch(update) {
14918
- return update.name === TOOL_SEARCH || asRecord(asRecord(update._meta).claudeCode).toolName === TOOL_SEARCH;
15299
+ return update.name === TOOL_SEARCH || asRecord2(asRecord2(update._meta).claudeCode).toolName === TOOL_SEARCH;
14919
15300
  }
14920
15301
  function toolSearchQuery(rawInput) {
14921
- const query = asRecord(rawInput).query;
15302
+ const query = asRecord2(rawInput).query;
14922
15303
  if (typeof query !== "string")
14923
15304
  return "";
14924
15305
  const names = /^select:(.*)$/.exec(query.trim());
@@ -14926,14 +15307,33 @@ function toolSearchQuery(rawInput) {
14926
15307
  return query.trim();
14927
15308
  return (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
14928
15309
  }
15310
+ var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
15311
+ function toyonTool(update) {
15312
+ const meta = asRecord2(asRecord2(update._meta).claudeCode).toolName;
15313
+ for (const spelled of [update.name ?? "", typeof meta === "string" ? meta : ""]) {
15314
+ const m = TOYON_TOOL.exec(spelled);
15315
+ if (m)
15316
+ return m[1];
15317
+ }
15318
+ return null;
15319
+ }
15320
+ function toyonTitle(tool, rawInput, title) {
15321
+ if (tool !== HANDOFF_TOOL)
15322
+ return title && toyonTool({ name: title }) === null ? title : tool;
15323
+ const project = asRecord2(rawInput).project;
15324
+ return typeof project === "string" && project.trim() ? `Continue in ${project.trim()}` : "Continue in another project";
15325
+ }
14929
15326
  function heading(update) {
14930
15327
  const name = typeof update.name === "string" ? update.name : "";
14931
- const host = asRecord(update.rawInput).host;
15328
+ const host = asRecord2(update.rawInput).host;
14932
15329
  if (isNetworkAsk(update) && typeof host === "string")
14933
15330
  return { name: "", title: host, kind: "fetch" };
14934
15331
  if (isToolSearch(update)) {
14935
15332
  return { name: TOOL_SEARCH, title: toolSearchQuery(update.rawInput) || TOOL_SEARCH, kind: "search" };
14936
15333
  }
15334
+ const own = toyonTool(update);
15335
+ if (own)
15336
+ return { name: own, title: toyonTitle(own, update.rawInput, update.title), kind: "other" };
14937
15337
  return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
14938
15338
  }
14939
15339
  function endOfAsk(req, res, memos) {
@@ -15014,7 +15414,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15014
15414
  content: update.content ?? [],
15015
15415
  rawOutput: update.rawOutput,
15016
15416
  ended: false,
15017
- writing: isWrittenKind(head.kind) && emptyInput(input),
15417
+ writing: writingCall(head, input),
15018
15418
  ...spawn.parentToolId ? { parent: spawn.parentToolId } : {},
15019
15419
  ...spawn.subagent ? { spawns: true } : {}
15020
15420
  };
@@ -15045,7 +15445,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15045
15445
  input,
15046
15446
  content: [],
15047
15447
  ended: false,
15048
- writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
15448
+ writing: writingCall({ name: update.name ?? "", kind: update.kind ?? undefined }, input),
15049
15449
  ...spawn.parentToolId ? { parent: spawn.parentToolId } : {},
15050
15450
  ...spawn.subagent ? { spawns: true } : {}
15051
15451
  };
@@ -15061,10 +15461,10 @@ function mapUpdate(update, memos, tag, sink, background) {
15061
15461
  });
15062
15462
  }
15063
15463
  const refined = { ...memo.held };
15064
- const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : update.title;
15464
+ const title = memo.name === TOOL_SEARCH ? toolSearchQuery(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
15065
15465
  if (title && title !== memo.title)
15066
15466
  refined.title = memo.title = title;
15067
- if (update.name && update.name !== memo.name)
15467
+ if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
15068
15468
  refined.name = memo.name = update.name;
15069
15469
  if (update.kind && update.kind !== memo.kind)
15070
15470
  refined.kind = memo.kind = update.kind;
@@ -15214,6 +15614,46 @@ function mapStopReason(r) {
15214
15614
  return r === "cancelled" ? "interrupted" : r;
15215
15615
  }
15216
15616
 
15617
+ // ../daemon/src/agent/acp/ratelimit.ts
15618
+ var RATE_LIMIT_META = "_claude/rateLimit";
15619
+ var STATUSES = new Set(["allowed", "allowed_warning", "rejected"]);
15620
+ var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : undefined;
15621
+ function window(v) {
15622
+ const w = v;
15623
+ if (!w || typeof w !== "object")
15624
+ return;
15625
+ const used = num(w.utilization);
15626
+ const resetsAt = num(w.resetsAt);
15627
+ if (used === undefined || resetsAt === undefined)
15628
+ return;
15629
+ return { used: Math.max(0, used), resetsAt: resetsAt * 1000 };
15630
+ }
15631
+ function parseRateLimit(meta, now) {
15632
+ const info = meta?.[RATE_LIMIT_META];
15633
+ if (!info || typeof info !== "object")
15634
+ return null;
15635
+ const status = info.status;
15636
+ if (!STATUSES.has(status))
15637
+ return null;
15638
+ const binding = typeof info.rateLimitType === "string" ? info.rateLimitType : undefined;
15639
+ const unified = info.unifiedWindows ?? undefined;
15640
+ const windows = {};
15641
+ const fiveHour = window(unified?.five_hour);
15642
+ const sevenDay = window(unified?.seven_day);
15643
+ if (fiveHour)
15644
+ windows.five_hour = fiveHour;
15645
+ if (sevenDay)
15646
+ windows.seven_day = sevenDay;
15647
+ if (!fiveHour && !sevenDay && (binding === "five_hour" || binding === "seven_day")) {
15648
+ const top = window({ utilization: info.utilization, resetsAt: info.resetsAt });
15649
+ if (top)
15650
+ windows[binding] = top;
15651
+ }
15652
+ if (!windows.five_hour && !windows.seven_day)
15653
+ return null;
15654
+ return { status, ...binding ? { binding } : {}, windows, at: now };
15655
+ }
15656
+
15217
15657
  // ../daemon/src/agent/acp/steering.ts
15218
15658
  var STEER_METHOD = "_session/steering";
15219
15659
  function supportsSteering(init) {
@@ -15258,6 +15698,7 @@ class AcpSession {
15258
15698
  tasks;
15259
15699
  asking = 0;
15260
15700
  asks = new Map;
15701
+ openHandoffs = new Set;
15261
15702
  log;
15262
15703
  seq;
15263
15704
  commandList = [];
@@ -15295,6 +15736,17 @@ class AcpSession {
15295
15736
  this.emit({ type: "tool-end", toolId, output: output.join(`
15296
15737
  `), isError: true });
15297
15738
  }
15739
+ for (const { event } of this.log.entries)
15740
+ this.trackHandoff(event);
15741
+ if (this.openHandoffs.size > 0)
15742
+ this.syncStatus();
15743
+ }
15744
+ trackHandoff(event) {
15745
+ if (event.type === "handoff-proposed")
15746
+ return this.openHandoffs.add(event.id).size > 0;
15747
+ if (event.type === "handoff" || event.type === "handoff-declined")
15748
+ return this.openHandoffs.delete(event.id);
15749
+ return false;
15298
15750
  }
15299
15751
  get commands() {
15300
15752
  return this.commandList;
@@ -15305,6 +15757,12 @@ class AcpSession {
15305
15757
  return;
15306
15758
  await this.warm();
15307
15759
  }
15760
+ modeChanged() {
15761
+ const live = this.live;
15762
+ if (!live || this.stopped)
15763
+ return;
15764
+ fireAndForget(this.d.worktreeId, this.applyMode(live), "mode change mid-session");
15765
+ }
15308
15766
  async warm() {
15309
15767
  if (this.stopped)
15310
15768
  return;
@@ -15318,6 +15776,9 @@ class AcpSession {
15318
15776
  get runningAgent() {
15319
15777
  return this.conn?.spec.id ?? null;
15320
15778
  }
15779
+ get mcpTools() {
15780
+ return this.conn ? this.conn.mcpServers.length > 0 : null;
15781
+ }
15321
15782
  pgid_ = null;
15322
15783
  get pgid() {
15323
15784
  return this.pgid_;
@@ -15376,6 +15837,8 @@ class AcpSession {
15376
15837
  }
15377
15838
  note(event) {
15378
15839
  this.emit(event);
15840
+ if (this.trackHandoff(event))
15841
+ this.syncStatus();
15379
15842
  }
15380
15843
  emit(event) {
15381
15844
  const entry = this.log.append(event);
@@ -15519,7 +15982,7 @@ class AcpSession {
15519
15982
  this.interrupted = false;
15520
15983
  this.steered = [];
15521
15984
  }
15522
- this.setStatus("idle");
15985
+ this.setStatus(this.openHandoffs.size > 0 ? "waiting" : "idle");
15523
15986
  } catch (e) {
15524
15987
  if (this.conn && (isAuthRequired(e) || this.rejectedCredential(e))) {
15525
15988
  const rejected = !isAuthRequired(e);
@@ -15690,7 +16153,23 @@ class AcpSession {
15690
16153
  this.emit({ type: "turn-end", stopReason: mapStopReason(res.stopReason), ts: Date.now() });
15691
16154
  }
15692
16155
  contextFor(item) {
15693
- return ambientBlock([...item.context ?? [], this.d.preview?.()]);
16156
+ return ambientBlock([...item.context ?? [], this.d.preview?.(), ...this.landedSince()]);
16157
+ }
16158
+ landedSince() {
16159
+ const entries = this.log.entries;
16160
+ const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked";
16161
+ let i = entries.length - 1;
16162
+ while (i >= 0 && !isMessage(entries[i].event))
16163
+ i--;
16164
+ const out = [];
16165
+ for (i--;i >= 0; i--) {
16166
+ const e = entries[i].event;
16167
+ if (isMessage(e))
16168
+ break;
16169
+ if (e.type === "handoff-landed")
16170
+ out.unshift(handoffLandedContext(e.title, e.repoName, e.url));
16171
+ }
16172
+ return out;
15694
16173
  }
15695
16174
  ensureConn(why) {
15696
16175
  if (this.conn)
@@ -15723,6 +16202,7 @@ class AcpSession {
15723
16202
  log.warn(this.d.worktreeId, "agent process exited while idle");
15724
16203
  this.conn = null;
15725
16204
  this.live = null;
16205
+ this.d.mcp?.close();
15726
16206
  this.cancelAsks();
15727
16207
  this.tasks.endAll("the agent stopped, and the command with it");
15728
16208
  this.endOwn("interrupted");
@@ -15749,6 +16229,7 @@ class AcpSession {
15749
16229
  deleteSupported: !!init.agentCapabilities?.sessionCapabilities?.delete,
15750
16230
  acceptsImages: init.agentCapabilities?.promptCapabilities?.image === true,
15751
16231
  steering: supportsSteering(init),
16232
+ mcpServers: takesHttpMcp(init) && this.d.mcp ? [this.d.mcp.open()] : [],
15752
16233
  side,
15753
16234
  commands
15754
16235
  };
@@ -15775,6 +16256,7 @@ class AcpSession {
15775
16256
  }
15776
16257
  async openLive(conn) {
15777
16258
  const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
16259
+ const meta = sessionMeta(conn.spec);
15778
16260
  let sessionId = this.d.getSessionId();
15779
16261
  let modes;
15780
16262
  let configOptions;
@@ -15784,9 +16266,9 @@ class AcpSession {
15784
16266
  const r = await conn.ctx.request(methods.agent.session.resume, {
15785
16267
  sessionId,
15786
16268
  cwd: this.d.cwd,
15787
- mcpServers: [],
16269
+ mcpServers: conn.mcpServers,
15788
16270
  additionalDirectories,
15789
- ...conn.spec.systemPrompt === "meta-append" ? { _meta: { systemPrompt: { append: SYSTEM_APPEND } } } : {}
16271
+ ...meta ? { _meta: meta } : {}
15790
16272
  });
15791
16273
  modes = r.modes;
15792
16274
  configOptions = r.configOptions;
@@ -15800,9 +16282,9 @@ class AcpSession {
15800
16282
  if (!resumed) {
15801
16283
  const r = await conn.ctx.request(methods.agent.session.new, {
15802
16284
  cwd: this.d.cwd,
15803
- mcpServers: [],
16285
+ mcpServers: conn.mcpServers,
15804
16286
  additionalDirectories,
15805
- ...conn.spec.systemPrompt === "meta-append" ? { _meta: { systemPrompt: { append: SYSTEM_APPEND } } } : {}
16287
+ ...meta ? { _meta: meta } : {}
15806
16288
  });
15807
16289
  sessionId = r.sessionId;
15808
16290
  modes = r.modes;
@@ -15903,12 +16385,23 @@ class AcpSession {
15903
16385
  if (!live || params.sessionId !== live.sessionId) {
15904
16386
  return log.debug(this.d.worktreeId, `acp: update for another session ${params.sessionId} dropped`);
15905
16387
  }
16388
+ if (params.update.sessionUpdate === "usage_update" && this.conn) {
16389
+ const limits = parseRateLimit(params.update._meta, Date.now());
16390
+ if (limits)
16391
+ this.d.onLimits?.(this.conn.spec.id, limits);
16392
+ }
15906
16393
  if (params.update.sessionUpdate === "current_mode_update") {
15907
16394
  live.modeId = params.update.currentModeId;
15908
16395
  return;
15909
16396
  }
15910
- if (params.update.sessionUpdate === "config_option_update")
16397
+ if (params.update.sessionUpdate === "config_option_update") {
16398
+ const before = live.options;
15911
16399
  this.absorb(live, params.update.configOptions);
16400
+ for (const [category, opt] of live.options) {
16401
+ if (before.get(category)?.current !== opt.current)
16402
+ this.d.onOptionChanged?.(category, opt.current);
16403
+ }
16404
+ }
15912
16405
  const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => this.tasks.start(toolId, start, input));
15913
16406
  this.forward(live, events);
15914
16407
  this.holdInput(live, params.update);
@@ -15968,14 +16461,16 @@ class AcpSession {
15968
16461
  }
15969
16462
  async askPermission(params) {
15970
16463
  const plan = params.toolCall.kind === "switch_mode";
15971
- const detail = permissionDetail(params);
16464
+ const parts = permissionParts(params);
16465
+ const detail = parts.detail ?? "";
15972
16466
  const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
15973
16467
  const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
15974
16468
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
15975
16469
  return this.openAsk((id) => ({
15976
16470
  type: "agent-permission",
15977
16471
  id,
15978
- title: params.toolCall.title ?? params.toolCall.name ?? "the agent needs a decision",
16472
+ title: parts.title,
16473
+ ...parts.command ? { command: parts.command } : {},
15979
16474
  ...detail ? { detail } : {},
15980
16475
  ...planPath ? { plan: planPath } : {},
15981
16476
  choices,
@@ -16056,7 +16551,8 @@ class AcpSession {
16056
16551
  syncStatus() {
16057
16552
  if (this.status === "error")
16058
16553
  return;
16059
- this.setStatus(this.asks.size > 0 ? "waiting" : this.running || this.own ? "working" : "idle");
16554
+ const waiting = this.asks.size > 0 || this.openHandoffs.size > 0;
16555
+ this.setStatus(waiting ? "waiting" : this.running || this.own ? "working" : "idle");
16060
16556
  }
16061
16557
  trackOwn(events) {
16062
16558
  if (!this.own) {
@@ -16123,6 +16619,7 @@ class AcpSession {
16123
16619
  const conn = this.conn;
16124
16620
  this.conn = null;
16125
16621
  this.live = null;
16622
+ this.d.mcp?.close();
16126
16623
  if (!conn)
16127
16624
  return;
16128
16625
  log.info(this.d.worktreeId, `agent ${conn.spec.id} stopped: ${why} (pid ${conn.link.pid ?? "unknown"})`);
@@ -16134,12 +16631,21 @@ class AcpSession {
16134
16631
  function isAuthRequired(e) {
16135
16632
  return e instanceof RequestError && e.code === -32000;
16136
16633
  }
16634
+ function takesHttpMcp(init) {
16635
+ const http = init.agentCapabilities?.mcpCapabilities?.http;
16636
+ return http === true || typeof http === "object" && http !== null;
16637
+ }
16137
16638
  var DETAIL_LINES = 80;
16138
- function permissionDetail(params) {
16639
+ function stringField(raw, key) {
16640
+ const v = raw && typeof raw === "object" ? raw[key] : undefined;
16641
+ return typeof v === "string" && v.trim() ? v : undefined;
16642
+ }
16643
+ function permissionParts(params) {
16644
+ const { toolCall } = params;
16139
16645
  const parts = [];
16140
- for (const c of params.toolCall.content ?? []) {
16646
+ for (const c of toolCall.content ?? []) {
16141
16647
  if (c.type === "content" && c.content.type === "text")
16142
- parts.push(c.content.text);
16648
+ parts.push({ text: true, md: c.content.text });
16143
16649
  else if (c.type === "diff") {
16144
16650
  const lines = c.newText.split(`
16145
16651
  `);
@@ -16147,27 +16653,37 @@ function permissionDetail(params) {
16147
16653
  `);
16148
16654
  const more = lines.length > DETAIL_LINES ? `
16149
16655
  \u2026 ${lines.length - DETAIL_LINES} more lines` : "";
16150
- parts.push(`\`${c.path}\`
16656
+ parts.push({ text: false, md: `\`${c.path}\`
16151
16657
 
16152
16658
  \`\`\`
16153
16659
  ${shown}${more}
16154
- \`\`\``);
16660
+ \`\`\`` });
16155
16661
  }
16156
16662
  }
16157
- if (parts.length > 0)
16158
- return parts.join(`
16663
+ const raw = toolCall.rawInput;
16664
+ const command = stringField(raw, "command");
16665
+ if (!command) {
16666
+ const detail = parts.map((p) => p.md).join(`
16667
+
16668
+ `) || stringField(raw, "plan") || "";
16669
+ return { title: toolCall.title ?? toolCall.name ?? "the agent needs a decision", ...detail ? { detail } : {} };
16670
+ }
16671
+ const same = (a, b) => a.trim() === b.trim();
16672
+ const prose = (s) => s && !same(s, command) && s !== toolCall.name ? s : undefined;
16673
+ const title = stringField(raw, "description") ?? parts.find((p) => p.text && prose(p.md))?.md ?? prose(toolCall.title ?? undefined) ?? "run this command?";
16674
+ const detail = parts.filter((p) => !(p.text && (same(p.md, title) || same(p.md, command)))).map((p) => p.md).join(`
16159
16675
 
16160
16676
  `);
16161
- const raw = params.toolCall.rawInput;
16162
- if (typeof raw?.plan === "string")
16163
- return raw.plan;
16164
- if (typeof raw?.command === "string")
16165
- return `\`\`\`sh
16166
- ${raw.command}
16167
- \`\`\``;
16168
- return "";
16677
+ return { title, command, ...detail ? { detail } : {} };
16169
16678
  }
16170
16679
  var REJECTED_CREDENTIAL_RE = /\b401\b|unauthorized|invalid[\s_-]?api[\s_-]?key|authentication[\s_-]?(error|failed)|api key (is )?(invalid|expired|incorrect)|incorrect api key|(token|credential)s? (have |has )?expired|expired (token|credential)|not (logged in|authenticated)/i;
16680
+ function sessionMeta(spec) {
16681
+ const meta = {
16682
+ ...spec.meta,
16683
+ ...spec.systemPrompt === "meta-append" ? { systemPrompt: { append: SYSTEM_APPEND } } : {}
16684
+ };
16685
+ return Object.keys(meta).length > 0 ? meta : undefined;
16686
+ }
16171
16687
  function terminalLogin(m) {
16172
16688
  if ("type" in m && m.type === "terminal")
16173
16689
  return { own: false, args: m.args ?? [] };
@@ -16192,27 +16708,41 @@ function sameCommands(a, b) {
16192
16708
  }
16193
16709
 
16194
16710
  // ../daemon/src/agent/planDoc.ts
16195
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
16711
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
16196
16712
  import { join as join9 } from "path";
16197
16713
  var PLANS_DIR = join9(".toyon", "plans");
16198
16714
  var PLAN_NAME = /^(\d+)\.md$/;
16199
16715
  function isPlanPath(rel) {
16200
16716
  return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
16201
16717
  }
16202
- function nextNumber(dir) {
16203
- if (!existsSync9(dir))
16204
- return 1;
16718
+ function lastNumber(dir) {
16719
+ if (!existsSync10(dir))
16720
+ return 0;
16205
16721
  let max = 0;
16206
16722
  for (const name of readdirSync3(dir)) {
16207
16723
  const n = Number(PLAN_NAME.exec(name)?.[1]);
16208
16724
  if (n > max)
16209
16725
  max = n;
16210
16726
  }
16211
- return max + 1;
16727
+ return max;
16728
+ }
16729
+ function sameAsLast(cwd, rel, markdown) {
16730
+ try {
16731
+ return readFileSync9(join9(cwd, rel), "utf8").trim() === markdown.trim();
16732
+ } catch (e) {
16733
+ log.warn(cwd, `could not read ${rel}`, e);
16734
+ return false;
16735
+ }
16212
16736
  }
16213
16737
  async function writePlanDoc(cwd, markdown) {
16214
16738
  const dir = join9(cwd, PLANS_DIR);
16215
- const rel = join9(PLANS_DIR, `${nextNumber(dir)}.md`);
16739
+ const last = lastNumber(dir);
16740
+ if (last > 0) {
16741
+ const newest = join9(PLANS_DIR, `${last}.md`);
16742
+ if (sameAsLast(cwd, newest, markdown))
16743
+ return newest;
16744
+ }
16745
+ const rel = join9(PLANS_DIR, `${last + 1}.md`);
16216
16746
  try {
16217
16747
  mkdirSync6(dir, { recursive: true });
16218
16748
  writeFileSync4(join9(cwd, rel), markdown.endsWith(`
@@ -16227,10 +16757,10 @@ async function writePlanDoc(cwd, markdown) {
16227
16757
  }
16228
16758
  function planEdited(cwd, rel, proposed) {
16229
16759
  const file = join9(cwd, rel);
16230
- if (!existsSync9(file))
16760
+ if (!existsSync10(file))
16231
16761
  return false;
16232
16762
  try {
16233
- return readFileSync8(file, "utf8").trim() !== proposed.trim();
16763
+ return readFileSync9(file, "utf8").trim() !== proposed.trim();
16234
16764
  } catch (e) {
16235
16765
  log.warn(cwd, `could not read ${rel}`, e);
16236
16766
  return false;
@@ -16548,6 +17078,26 @@ function orphansIn(ledger, rows, bootId, ledgerBootId, now) {
16548
17078
  }
16549
17079
  return out;
16550
17080
  }
17081
+ function strayProcs(rows, worktreesDir, livePaths) {
17082
+ const root = worktreesDir.endsWith("/") ? worktreesDir : `${worktreesDir}/`;
17083
+ const seen = new Set;
17084
+ const out = [];
17085
+ for (const r of rows) {
17086
+ if (r.ppid !== 1 || seen.has(r.pgid) || r.pgid === process.pid)
17087
+ continue;
17088
+ const at = r.command.indexOf(root);
17089
+ if (at === -1)
17090
+ continue;
17091
+ const [repoDir, wtDir] = r.command.slice(at + root.length).split(/\s/, 1)[0].split("/");
17092
+ if (!repoDir || !wtDir)
17093
+ continue;
17094
+ if (livePaths.has(`${root}${repoDir}/${wtDir}`))
17095
+ continue;
17096
+ seen.add(r.pgid);
17097
+ out.push({ worktreeId: "daemon", pgid: r.pgid, name: wtDir });
17098
+ }
17099
+ return out;
17100
+ }
16551
17101
  function strayAdapters(rows, agentsDir) {
16552
17102
  return rows.filter((r) => r.pid === r.pgid && r.ppid === 1 && r.command.includes(agentsDir));
16553
17103
  }
@@ -16683,131 +17233,17 @@ function expandEnv(env, vars) {
16683
17233
  return out;
16684
17234
  }
16685
17235
 
16686
- // ../daemon/src/core/remote.ts
16687
- import { createHmac, timingSafeEqual } from "crypto";
16688
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
16689
- function loadRemote(file, env = process.env, managed = MANAGED_DEFAULTS) {
16690
- if (env.TOYON_CLOUD === "1") {
16691
- const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
16692
- if (!isRemoteHost(host)) {
16693
- throw new Error(`TOYON_CLOUD=1 needs TOYON_PUBLIC_HOST, the name the edge answers for; got "${host}"`);
16694
- }
16695
- const previews = env.TOYON_PREVIEWS ?? portPreviews(host);
16696
- const why = checkPreviews(previews, host);
16697
- if (why)
16698
- throw new Error(`TOYON_PREVIEWS: ${why}`);
16699
- return { host, previews, front: "edge" };
16700
- }
16701
- if (!existsSync10(file))
16702
- return null;
16703
- if (managed.remote === "off") {
16704
- log.warn("remote", `${file} is ignored: remote access is turned off by your organization's policy`);
16705
- return null;
16706
- }
16707
- const view = parseRemote(readFileSync9(file, "utf8"));
16708
- if (!view) {
16709
- log.warn("remote", `${file} names no valid host or previews; remote access is off`);
16710
- return null;
16711
- }
16712
- if (managed.remote === "tailscale" && !view.host.endsWith(".ts.net")) {
16713
- log.warn("remote", `${file} names ${view.host}; your organization's policy allows a tailnet name only, so remote access is off`);
16714
- return null;
16715
- }
16716
- return { ...view, front: "local" };
16717
- }
16718
- function sameSecret(given, want) {
16719
- if (given === null)
16720
- return false;
16721
- const a = Buffer.from(given);
16722
- const b = Buffer.from(want);
16723
- return a.length === b.length && timingSafeEqual(a, b);
16724
- }
16725
- var isLoopbackPeer = (ip) => ip === "127.0.0.1" || ip === "::1" || ip.startsWith("::ffff:127.");
16726
- var isLoopbackHost = (h) => h === "127.0.0.1" || h === "localhost" || h.endsWith(".localhost");
16727
- function requestedAuthority(req) {
16728
- const host = (req.headers.get("host") ?? "").toLowerCase();
16729
- if (host.includes(":"))
16730
- return host;
16731
- const port = req.headers.get("x-forwarded-port");
16732
- return port && /^\d{1,5}$/.test(port) && port !== "443" ? `${host}:${port}` : host;
16733
- }
16734
- var forbidden = (body = "forbidden") => ({ kind: "refused", response: new Response(body, { status: 403 }) });
16735
- function door(req, peer, remote, listener) {
16736
- if (remote === null && listener === "preview")
16737
- return { kind: "local" };
16738
- if (remote?.front !== "edge" && !isLoopbackPeer(peer))
16739
- return forbidden();
16740
- const authority = requestedAuthority(req);
16741
- const host = authority.split(":")[0] ?? "";
16742
- if (isLoopbackHost(host)) {
16743
- return remote?.front === "edge" ? forbidden() : { kind: "local" };
16744
- }
16745
- if (remote === null)
16746
- return forbidden();
16747
- const preview = matchPreview(remote.previews, authority);
16748
- let worktreeId = null;
16749
- if (listener === "daemon") {
16750
- if (preview !== null && "id" in preview)
16751
- worktreeId = preview.id;
16752
- else if (preview !== null || host !== remote.host)
16753
- return forbidden();
16754
- } else if (preview === null || "id" in preview) {
16755
- return forbidden();
16756
- }
16757
- if (req.headers.get("x-forwarded-proto") !== "https") {
16758
- return forbidden(`${host} reaches Toyon over https only; the front must terminate TLS`);
16759
- }
16760
- if (listener === "preview")
16761
- return { kind: "preview", worktreeId: null };
16762
- return worktreeId === null ? { kind: "shell" } : { kind: "preview", worktreeId };
16763
- }
16764
- var PREVIEW_COOKIE = "toyon_preview";
16765
- function previewGrant(token) {
16766
- return createHmac("sha256", token).update("toyon preview grant").digest("hex");
16767
- }
16768
- function grantCookie(grant, name) {
16769
- return `${PREVIEW_COOKIE}=${grant}; Domain=${name}; Path=/; Max-Age=34560000; HttpOnly; Secure; SameSite=Strict`;
16770
- }
16771
- function takeGrant(cookie, grant) {
16772
- let ok = false;
16773
- const kept = [];
16774
- for (const part of (cookie ?? "").split(";")) {
16775
- const pair = part.trim();
16776
- if (pair === "")
16777
- continue;
16778
- const eq = pair.indexOf("=");
16779
- if (eq > 0 && pair.slice(0, eq) === PREVIEW_COOKIE) {
16780
- if (sameSecret(pair.slice(eq + 1), grant))
16781
- ok = true;
16782
- continue;
16783
- }
16784
- kept.push(pair);
16785
- }
16786
- return { ok, rest: kept.length ? kept.join("; ") : null };
16787
- }
16788
- function passPreview(req, grant) {
16789
- const pass = takeGrant(req.headers.get("cookie"), grant);
16790
- if (!pass.ok) {
16791
- return {
16792
- ok: false,
16793
- response: new Response(`<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="3"></head>` + `<body>This preview opens from Toyon; open your Toyon link first.</body></html>`, { status: 403, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } })
16794
- };
16795
- }
16796
- const headers = new Headers(req.headers);
16797
- if (pass.rest === null)
16798
- headers.delete("cookie");
16799
- else
16800
- headers.set("cookie", pass.rest);
16801
- return { ok: true, req: new Request(req, { headers }) };
16802
- }
16803
- function setsGrant(line) {
16804
- return line.slice(0, line.indexOf("=")).trim() === PREVIEW_COOKIE;
16805
- }
16806
-
16807
17236
  // ../daemon/src/runtime/proxy.ts
16808
17237
  var DIAL_TIMEOUT_MS = 5000;
16809
17238
  var frame = (ev) => typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
16810
17239
  var VITE_PING = "text/x-vite-ping";
17240
+ var ORIGIN_KEYED = "origin-agent-cluster";
17241
+ function placeholder(status) {
17242
+ return new Response(waitingPage(), {
17243
+ status,
17244
+ headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2", [ORIGIN_KEYED]: "?1" }
17245
+ });
17246
+ }
16811
17247
  function previewHandler(opts) {
16812
17248
  const hostPart = (t) => t.host.includes(":") ? `[${t.host}]` : t.host;
16813
17249
  return {
@@ -16825,12 +17261,8 @@ function previewHandler(opts) {
16825
17261
  let target = opts.getTarget();
16826
17262
  if (target == null && !machine && opts.ready)
16827
17263
  target = await opts.ready();
16828
- if (target == null) {
16829
- return new Response(waitingPage(), {
16830
- status: 503,
16831
- headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
16832
- });
16833
- }
17264
+ if (target == null)
17265
+ return placeholder(503);
16834
17266
  if (isUpgrade) {
16835
17267
  const proto = req.headers.get("sec-websocket-protocol") ?? undefined;
16836
17268
  const upstreamUrl = `ws://${hostPart(target)}:${target.port}${url.pathname}${url.search}`;
@@ -16856,20 +17288,18 @@ function previewHandler(opts) {
16856
17288
  redirect: "manual"
16857
17289
  });
16858
17290
  } catch {
16859
- return new Response(waitingPage(), {
16860
- status: 502,
16861
- headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
16862
- });
17291
+ return placeholder(502);
16863
17292
  }
16864
17293
  const html = (res.headers.get("content-type") ?? "").includes("text/html");
16865
17294
  const encoded = res.headers.has("content-encoding");
16866
17295
  const cookies = res.headers.getSetCookie();
16867
17296
  const grantSet = cookies.some(setsGrant);
16868
- if (!html && !encoded && !grantSet)
16869
- return res;
16870
17297
  const h = new Headers(res.headers);
16871
- h.delete("content-length");
16872
- h.delete("content-encoding");
17298
+ h.set(ORIGIN_KEYED, "?1");
17299
+ if (html || encoded) {
17300
+ h.delete("content-length");
17301
+ h.delete("content-encoding");
17302
+ }
16873
17303
  if (grantSet) {
16874
17304
  h.delete("set-cookie");
16875
17305
  for (const c of cookies)
@@ -16923,7 +17353,9 @@ function startProxy(opts) {
16923
17353
  return d.response;
16924
17354
  let admitted = req;
16925
17355
  if (d.kind === "preview") {
16926
- const pass = passPreview(req, opts.grant);
17356
+ if (!opts.gate)
17357
+ return new Response("forbidden", { status: 403 });
17358
+ const pass = passPreview(req, opts.gate);
16927
17359
  if (!pass.ok)
16928
17360
  return pass.response;
16929
17361
  admitted = pass.req;
@@ -17513,6 +17945,7 @@ var PREVIEW_WAKE_MS = 8000;
17513
17945
  var PREVIEW_POLL_MS = 100;
17514
17946
  var PROC_UP_MS = 60000;
17515
17947
  function defaultAgent(wt, d, preview, onProcess) {
17948
+ const mcp = d.mcp;
17516
17949
  const agent = new AcpSession({
17517
17950
  worktreeId: wt.id,
17518
17951
  cwd: wt.path,
@@ -17538,16 +17971,13 @@ function defaultAgent(wt, d, preview, onProcess) {
17538
17971
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
17539
17972
  beforeTurn: async () => d.turnStarting?.(wt.id),
17540
17973
  onAuth: (agentId, o) => d.accounts?.observe(agentId, o),
17974
+ onLimits: (agentId, limits) => d.accounts?.observeLimits(agentId, limits),
17541
17975
  seedCommands: () => d.state.cachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId),
17542
17976
  onCommandsLearned: (commands) => d.state.setCachedCommands(d.state.requireWorktree(wt.id).agent ?? "", wt.repoId, commands),
17543
17977
  mode: () => d.state.requireWorktree(wt.id).mode ?? DEFAULT_PERMISSION_MODE,
17544
17978
  setMode: (mode) => {
17545
- const w = d.state.requireWorktree(wt.id);
17546
- if (w.mode === mode)
17547
- return;
17548
- w.mode = mode;
17549
- d.state.save();
17550
- d.hub.emit("worktreesChanged");
17979
+ if (d.state.setMode(wt.id, mode))
17980
+ d.hub.emit("worktreesChanged");
17551
17981
  },
17552
17982
  onPlan: async (markdown) => {
17553
17983
  const w = d.state.worktree(wt.id);
@@ -17570,7 +18000,17 @@ function defaultAgent(wt, d, preview, onProcess) {
17570
18000
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
17571
18001
  if (d.state.setCachedOptions(agentId, category, choices))
17572
18002
  d.hub.emit("agentsChanged");
17573
- }
18003
+ },
18004
+ onOptionChanged: (category, value) => {
18005
+ const w = d.state.requireWorktree(wt.id);
18006
+ const field = OPTION_FIELDS[category];
18007
+ if (w[field] === value)
18008
+ return;
18009
+ w[field] = value;
18010
+ d.state.save();
18011
+ d.hub.emit("worktreesChanged");
18012
+ },
18013
+ ...mcp ? { mcp: { open: () => mcp.open(wt.id), close: () => mcp.close(wt.id) } } : {}
17574
18014
  });
17575
18015
  agent.onQueueChange = () => d.hub.emit("queue", wt.id, agent.queueItems);
17576
18016
  agent.onCommandsChange = (commands) => d.hub.emit("agentCommands", wt.id, commands);
@@ -17587,7 +18027,7 @@ function defaultProxy(wt, previewName, procs, d, wake) {
17587
18027
  port: wt.proxyPort,
17588
18028
  hostname: cloud.bindHost,
17589
18029
  remote: d.remote ?? null,
17590
- grant: d.grant ?? "",
18030
+ gate: d.gate ?? null,
17591
18031
  bridgeScript: d.bridgeScript,
17592
18032
  getTarget: () => previewTargetOf(procs, previewName),
17593
18033
  onRequest: wake.onRequest,
@@ -18296,6 +18736,11 @@ class RuntimeRegistry {
18296
18736
  const before = new Map(this.deps.state.groups(id).map((g) => [g.pgid, g]));
18297
18737
  const now = Date.now();
18298
18738
  const next = live.map((g) => ({ ...g, startedAt: before.get(g.pgid)?.startedAt ?? now }));
18739
+ const alive = this.deps.groupAlive ?? groupAlive;
18740
+ for (const [pgid, g] of before) {
18741
+ if (!live.some((l) => l.pgid === pgid) && alive(pgid))
18742
+ next.push(g);
18743
+ }
18299
18744
  this.deps.state.setGroups(id, next);
18300
18745
  }
18301
18746
  async reclaimOrphans() {
@@ -18310,6 +18755,13 @@ class RuntimeRegistry {
18310
18755
  seen.add(row.pgid);
18311
18756
  found.push({ worktreeId: "daemon", pgid: row.pgid, name: "adapter" });
18312
18757
  }
18758
+ const livePaths = new Set(state.worktrees.map((w) => w.path));
18759
+ for (const o of strayProcs(rows, this.deps.paths.worktreesDir, livePaths)) {
18760
+ if (seen.has(o.pgid))
18761
+ continue;
18762
+ seen.add(o.pgid);
18763
+ found.push(o);
18764
+ }
18313
18765
  const reclaim = this.deps.reclaim ?? reclaimGroup;
18314
18766
  await Promise.all(found.map(async (o) => {
18315
18767
  await reclaim(o.pgid);
@@ -18325,11 +18777,11 @@ class RuntimeRegistry {
18325
18777
  }
18326
18778
 
18327
18779
  // ../daemon/src/worktrees/naming.ts
18328
- import { randomBytes as randomBytes2 } from "crypto";
18780
+ import { randomBytes as randomBytes3 } from "crypto";
18329
18781
  import { existsSync as existsSync11 } from "fs";
18330
18782
  import { join as join10 } from "path";
18331
18783
  function shortId() {
18332
- return randomBytes2(5).toString("hex");
18784
+ return randomBytes3(5).toString("hex");
18333
18785
  }
18334
18786
  function freeSlot(parent) {
18335
18787
  for (;; ) {
@@ -18475,6 +18927,10 @@ function openAskOf(entries) {
18475
18927
  open.set(e.id, e.title);
18476
18928
  else if (e.type === "agent-ask-end")
18477
18929
  open.delete(e.id);
18930
+ else if (e.type === "handoff-proposed")
18931
+ open.set(e.id, `Continue in ${e.repo.name}?`);
18932
+ else if (e.type === "handoff" || e.type === "handoff-declined")
18933
+ open.delete(e.id);
18478
18934
  }
18479
18935
  const last = [...open.values()].at(-1)?.trim();
18480
18936
  return last ? clip(last) : undefined;
@@ -18519,6 +18975,7 @@ var LAND_SYSTEM = "You judge whether a coding task is finished, recap where it s
18519
18975
  var LAND_ASK = [
18520
18976
  "A coding agent just stopped in a git worktree. Decide whether the work is finished and ready to merge, or still in progress: a question the agent is waiting on, a step it said it would do next, or a part of the request it did not get to.",
18521
18977
  "The agent never commits: every change is left uncommitted for the user, and the agent saying so is normal, not a sign the work is unfinished.",
18978
+ "The replies are oldest first and the agent's last word is where the work stands now; it supersedes the earlier ones. A check, test or job an earlier reply said was still running is done if the last word says so, and is a step still to do only when the last word itself is waiting on it.",
18522
18979
  "Reply with exactly this shape and nothing else:",
18523
18980
  "Line 1: READY, or NOT READY: <what is still left to do, under 8 words, about the change itself and not the conversation>",
18524
18981
  "Line 2: blank",
@@ -18543,7 +19000,7 @@ ${i.recentSubjects.map((s) => ` ${s}`).join(`
18543
19000
  head.push(`Note from the user for the commit message, to be reflected in its wording: ${clip(i.note.trim(), 500)}`);
18544
19001
  const diff = `Diff summary:
18545
19002
  ${i.diffStat.trim() || "(none)"}`;
18546
- let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
19003
+ let blocks = turnBlocks(i.turns);
18547
19004
  const size = () => [...head, "", ...blocks, "", diff].join(`
18548
19005
 
18549
19006
  `).length;
@@ -18556,14 +19013,18 @@ ${i.diffStat.trim() || "(none)"}`;
18556
19013
 
18557
19014
  `);
18558
19015
  }
18559
- function turnBlock(t) {
19016
+ function turnBlocks(turns) {
19017
+ const newest = turns.slice(-LAND_TURNS);
19018
+ return newest.map((t, n) => turnBlock(t, n === newest.length - 1));
19019
+ }
19020
+ function turnBlock(t, last) {
18560
19021
  const lines = [];
18561
19022
  if (t.asks.length)
18562
19023
  lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
18563
19024
  if (t.fixes.length)
18564
19025
  lines.push(`Toyon, not the user, asked for a fix: ${clip(t.fixes.join(" / "), 400)}`);
18565
19026
  if (t.reply.trim())
18566
- lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
19027
+ lines.push(`${last ? "Agent's last word, where the work stands now" : "Agent ended with"}: ${clip(t.reply, 1000)}`);
18567
19028
  return lines.join(`
18568
19029
  `);
18569
19030
  }
@@ -18577,7 +19038,7 @@ function answerPrompt(i) {
18577
19038
  const head = [ANSWER_ASK, "", `Task: ${clip(i.title, 200)}`];
18578
19039
  if (i.firstAsk)
18579
19040
  head.push(`First request: ${clip(i.firstAsk, 300)}`);
18580
- let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
19041
+ let blocks = turnBlocks(i.turns);
18581
19042
  const size = () => [...head, "", ...blocks].join(`
18582
19043
 
18583
19044
  `).length;
@@ -19284,6 +19745,40 @@ function folderDialog() {
19284
19745
  };
19285
19746
  }
19286
19747
 
19748
+ // ../daemon/src/core/grants.ts
19749
+ import { randomBytes as randomBytes4 } from "crypto";
19750
+ class GrantCodes {
19751
+ live = [];
19752
+ ttlMs;
19753
+ max;
19754
+ now;
19755
+ constructor(opts = {}) {
19756
+ this.ttlMs = opts.ttlMs ?? 60000;
19757
+ this.max = opts.max ?? 32;
19758
+ this.now = opts.now ?? Date.now;
19759
+ }
19760
+ mint() {
19761
+ this.sweep();
19762
+ const code = randomBytes4(9).toString("base64url");
19763
+ this.live.push({ code, until: this.now() + this.ttlMs });
19764
+ if (this.live.length > this.max)
19765
+ this.live.splice(0, this.live.length - this.max);
19766
+ return { code, ms: this.ttlMs };
19767
+ }
19768
+ redeem(code) {
19769
+ this.sweep();
19770
+ const i = this.live.findIndex((g) => sameSecret(code, g.code));
19771
+ if (i < 0)
19772
+ return false;
19773
+ this.live.splice(i, 1);
19774
+ return true;
19775
+ }
19776
+ sweep() {
19777
+ const t = this.now();
19778
+ this.live = this.live.filter((g) => t < g.until);
19779
+ }
19780
+ }
19781
+
19287
19782
  // ../daemon/src/core/hub.ts
19288
19783
  class Hub {
19289
19784
  listeners = new Map;
@@ -19563,8 +20058,21 @@ class SelfWatch {
19563
20058
  }
19564
20059
 
19565
20060
  // ../daemon/src/core/state.ts
19566
- import { randomBytes as randomBytes3 } from "crypto";
20061
+ import { randomBytes as randomBytes5 } from "crypto";
19567
20062
  import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
20063
+ var TRUSTED_MAX = 16;
20064
+ function isShellOrigin(origin) {
20065
+ let url;
20066
+ try {
20067
+ url = new URL(origin);
20068
+ } catch {
20069
+ return false;
20070
+ }
20071
+ if (url.origin !== origin)
20072
+ return false;
20073
+ return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHost(url.hostname);
20074
+ }
20075
+ var DAEMON_GROUPS = "daemon";
19568
20076
  var GROUPS_SAVE_MS = 250;
19569
20077
  var SEEN_WORKTREES = 50;
19570
20078
  var empty = { repos: [], worktrees: [], sessions: {} };
@@ -19602,7 +20110,7 @@ function loadState(paths) {
19602
20110
  delete state.seen?.[id];
19603
20111
  }
19604
20112
  for (const id of Object.keys(state.groups ?? {})) {
19605
- if (!ids.has(id) && !id.startsWith("disc-"))
20113
+ if (!ids.has(id) && !id.startsWith("disc-") && id !== DAEMON_GROUPS)
19606
20114
  delete state.groups?.[id];
19607
20115
  }
19608
20116
  if (state.modelCache) {
@@ -19625,7 +20133,7 @@ function loadOrCreateToken(paths) {
19625
20133
  const TOKEN_FILE = paths.tokenFile;
19626
20134
  if (existsSync14(TOKEN_FILE))
19627
20135
  return readFileSync10(TOKEN_FILE, "utf8").trim();
19628
- const token = randomBytes3(32).toString("hex");
20136
+ const token = randomBytes5(32).toString("hex");
19629
20137
  writeFileSync5(TOKEN_FILE, token, { mode: 384 });
19630
20138
  return token;
19631
20139
  }
@@ -19716,8 +20224,12 @@ class StateStore {
19716
20224
  delete this.state.sessions[id];
19717
20225
  if (this.state.seen)
19718
20226
  delete this.state.seen[id];
19719
- if (this.state.groups)
20227
+ if (this.state.groups) {
20228
+ const left = this.state.groups[id] ?? [];
19720
20229
  delete this.state.groups[id];
20230
+ if (left.length > 0)
20231
+ this.state.groups[DAEMON_GROUPS] = [...this.state.groups[DAEMON_GROUPS] ?? [], ...left];
20232
+ }
19721
20233
  this.save();
19722
20234
  }
19723
20235
  groups(worktreeId) {
@@ -19767,6 +20279,16 @@ class StateStore {
19767
20279
  this.state.bootAt = id;
19768
20280
  this.save();
19769
20281
  }
20282
+ setMode(worktreeId, mode) {
20283
+ const wt = this.requireWorktree(worktreeId);
20284
+ if (wt.kind === "spare")
20285
+ throw new UserError("no mode for a spare worktree");
20286
+ if (wt.mode === mode)
20287
+ return false;
20288
+ wt.mode = mode;
20289
+ this.save();
20290
+ return true;
20291
+ }
19770
20292
  session(worktreeId) {
19771
20293
  return this.state.sessions[worktreeId];
19772
20294
  }
@@ -19840,6 +20362,17 @@ class StateStore {
19840
20362
  this.state.pairedAt = Date.now();
19841
20363
  this.save();
19842
20364
  }
20365
+ get trustedOrigins() {
20366
+ return (this.state.trustedOrigins ?? []).map((t) => t.origin);
20367
+ }
20368
+ trustOrigin(origin) {
20369
+ if (!isShellOrigin(origin))
20370
+ return;
20371
+ const list = (this.state.trustedOrigins ?? []).filter((t) => t.origin !== origin);
20372
+ list.push({ origin, at: Date.now() });
20373
+ this.state.trustedOrigins = list.slice(-TRUSTED_MAX);
20374
+ this.save();
20375
+ }
19843
20376
  save() {
19844
20377
  if (this.groupsTimer)
19845
20378
  clearTimeout(this.groupsTimer);
@@ -26048,10 +26581,10 @@ function servedLocally(origin) {
26048
26581
  }
26049
26582
 
26050
26583
  // ../daemon/src/server/ws.ts
26051
- import { homedir as homedir11 } from "os";
26584
+ import { homedir as homedir11, hostname as hostname2 } from "os";
26052
26585
 
26053
26586
  // ../daemon/src/core/pair.ts
26054
- import { randomBytes as randomBytes4 } from "crypto";
26587
+ import { randomBytes as randomBytes6 } from "crypto";
26055
26588
  class PairCodes {
26056
26589
  now;
26057
26590
  live = null;
@@ -26059,7 +26592,7 @@ class PairCodes {
26059
26592
  this.now = now;
26060
26593
  }
26061
26594
  mint() {
26062
- const code = randomBytes4(9).toString("base64url");
26595
+ const code = randomBytes6(9).toString("base64url");
26063
26596
  this.live = { code, until: this.now() + PAIR_TTL_MS };
26064
26597
  return { code, ms: PAIR_TTL_MS };
26065
26598
  }
@@ -26114,7 +26647,7 @@ async function readTailnetPhones(run = ((bin) => bin ? cli(bin) : null)(Bun.whic
26114
26647
  }
26115
26648
 
26116
26649
  // ../daemon/src/server/handlers.ts
26117
- import { randomBytes as randomBytes5 } from "crypto";
26650
+ import { randomBytes as randomBytes7 } from "crypto";
26118
26651
 
26119
26652
  // ../daemon/src/agent/config.ts
26120
26653
  import { existsSync as existsSync22, readFileSync as readFileSync14 } from "fs";
@@ -26831,7 +27364,7 @@ var handlers = {
26831
27364
  await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
26832
27365
  return;
26833
27366
  }
26834
- const group = randomBytes5(4).toString("hex");
27367
+ const group = randomBytes7(4).toString("hex");
26835
27368
  const made = await Promise.allSettled(Array.from({ length: of }, (_, i) => s.worktrees.create(msg.repoId, msg.prompt, {
26836
27369
  ...opts,
26837
27370
  ...i === 0 ? { worktreeId: msg.worktreeId } : {},
@@ -27180,6 +27713,14 @@ var handlers = {
27180
27713
  requireRun(s, msg.worktreeId);
27181
27714
  s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId });
27182
27715
  },
27716
+ async "handoff-answer"(msg, _ctx, s) {
27717
+ requireRun(s, msg.worktreeId);
27718
+ await s.handoff.answer(msg.worktreeId, msg.id, msg.go, msg.note);
27719
+ },
27720
+ async "handoff-ask"(msg, _ctx, s) {
27721
+ requireRun(s, msg.worktreeId);
27722
+ await s.handoff.ask(msg.worktreeId, msg.repoId, msg.text);
27723
+ },
27183
27724
  "agent-config"(msg, ctx, s) {
27184
27725
  const spec = s.agents.require(msg.agent);
27185
27726
  const repoPath = msg.repoId ? s.state.repo(msg.repoId)?.path ?? null : null;
@@ -27320,7 +27861,37 @@ async function dispatch(msg, ctx, s) {
27320
27861
 
27321
27862
  // ../daemon/src/server/http.ts
27322
27863
  import { existsSync as existsSync23 } from "fs";
27864
+ import { hostname } from "os";
27323
27865
  import { join as join21 } from "path";
27866
+
27867
+ // ../daemon/src/server/cors.ts
27868
+ var ALLOW_HEADERS = "authorization, content-type";
27869
+ var ALLOW_METHODS = "GET, POST";
27870
+ var MAX_AGE = "86400";
27871
+ function preflight(allowOrigin) {
27872
+ const res = new Response(null, { status: 204 });
27873
+ return withCors(res, allowOrigin);
27874
+ }
27875
+ function withCors(res, origin) {
27876
+ res.headers.set("access-control-allow-origin", origin);
27877
+ res.headers.set("access-control-allow-headers", ALLOW_HEADERS);
27878
+ res.headers.set("access-control-allow-methods", ALLOW_METHODS);
27879
+ res.headers.set("access-control-max-age", MAX_AGE);
27880
+ res.headers.append("vary", "Origin");
27881
+ return res;
27882
+ }
27883
+ function isLoopbackOrigin(origin) {
27884
+ try {
27885
+ const u = new URL(origin);
27886
+ if (u.protocol !== "http:" && u.protocol !== "https:")
27887
+ return false;
27888
+ return isLoopbackHost(u.hostname);
27889
+ } catch {
27890
+ return false;
27891
+ }
27892
+ }
27893
+
27894
+ // ../daemon/src/server/http.ts
27324
27895
  var IMMUTABLE = "private, max-age=31536000, immutable";
27325
27896
  var NO_STORE = "no-store";
27326
27897
  function attachedHeaders(imageType) {
@@ -27333,25 +27904,31 @@ function attachedHeaders(imageType) {
27333
27904
  function createFetch(opts) {
27334
27905
  const grant = previewGrant(opts.token);
27335
27906
  const remote = opts.remote;
27907
+ const gate = remote && { grant, host: remote.host, redeem: (code) => opts.grants.redeem(code) };
27336
27908
  const health = () => Response.json({
27337
27909
  ok: true,
27338
27910
  version: opts.version,
27339
27911
  pid: process.pid,
27340
27912
  branded: opts.branded(),
27341
27913
  remote: remote && { host: remote.host, previews: remote.previews },
27914
+ machine: machineLabel(hostname(), remote),
27342
27915
  managed: opts.managed,
27343
27916
  ...opts.metrics()
27344
27917
  });
27345
- return async function fetch2(req, srv) {
27346
- const url = new URL(req.url);
27347
- if (remote?.front === "edge" && url.pathname === "/health")
27348
- return health();
27349
- const d = door(req, srv.requestIP(req)?.address ?? "", remote, "daemon");
27350
- if (d.kind === "refused")
27351
- return d.response;
27918
+ const corsAllow = (req, url, d) => {
27919
+ const origin = req.headers.get("origin");
27920
+ if (!origin || d.kind !== "local" && d.kind !== "shell")
27921
+ return null;
27922
+ if (url.pathname === "/pair/redeem")
27923
+ return origin;
27924
+ if (opts.trusted().includes(origin))
27925
+ return origin;
27926
+ return d.kind === "local" && isLoopbackOrigin(origin) ? origin : null;
27927
+ };
27928
+ async function route(req, srv, url, d) {
27352
27929
  const remoteShell = d.kind === "shell";
27353
- if (d.kind === "preview" && d.worktreeId !== null) {
27354
- const pass = passPreview(req, grant);
27930
+ if (d.kind === "preview" && d.worktreeId !== null && gate) {
27931
+ const pass = passPreview(req, gate);
27355
27932
  if (!pass.ok)
27356
27933
  return pass.response;
27357
27934
  const handler = opts.preview(d.worktreeId);
@@ -27425,6 +28002,15 @@ function createFetch(opts) {
27425
28002
  const phones = remote && isTailnetName(remote.host) ? await opts.phones() : null;
27426
28003
  return Response.json(phones, { headers: { "cache-control": NO_STORE } });
27427
28004
  }
28005
+ if (url.pathname === "/preview-grant" && req.method === "POST") {
28006
+ if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
28007
+ return new Response("unauthorized", { status: 401 });
28008
+ }
28009
+ if (!remote)
28010
+ return new Response("previews here are not reached by name", { status: 400 });
28011
+ const body = opts.grants.mint();
28012
+ return Response.json(body, { headers: { "cache-control": NO_STORE } });
28013
+ }
27428
28014
  if (url.pathname === "/pair/redeem" && req.method === "POST") {
27429
28015
  if (!remote || !remoteShell)
27430
28016
  return new Response("not found", { status: 404 });
@@ -27432,7 +28018,7 @@ function createFetch(opts) {
27432
28018
  if (typeof body.code !== "string" || !opts.pair.redeem(body.code)) {
27433
28019
  return new Response("code expired", { status: 401 });
27434
28020
  }
27435
- opts.onPaired();
28021
+ opts.onPaired(req.headers.get("origin"));
27436
28022
  const reply = { token: opts.token };
27437
28023
  return Response.json(reply, {
27438
28024
  headers: { "cache-control": NO_STORE, "set-cookie": grantCookie(grant, remote.host) }
@@ -27560,6 +28146,28 @@ function createFetch(opts) {
27560
28146
  if (existsSync23(index))
27561
28147
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
27562
28148
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
28149
+ }
28150
+ return async function fetch2(req, srv) {
28151
+ const url = new URL(req.url);
28152
+ if (remote?.front === "edge" && url.pathname === "/health")
28153
+ return health();
28154
+ if (url.pathname.startsWith("/mcp/")) {
28155
+ const peer = srv.requestIP(req)?.address ?? "";
28156
+ if (!isLoopbackPeer(peer))
28157
+ return new Response("forbidden", { status: 403 });
28158
+ const worktreeId = url.pathname.slice("/mcp/".length);
28159
+ if (!worktreeId || worktreeId.includes("/"))
28160
+ return new Response("not found", { status: 404 });
28161
+ return opts.mcp(req, worktreeId);
28162
+ }
28163
+ const d = door(req, srv.requestIP(req)?.address ?? "", remote, "daemon");
28164
+ if (d.kind === "refused")
28165
+ return d.response;
28166
+ const allow = corsAllow(req, url, d);
28167
+ if (allow && req.method === "OPTIONS")
28168
+ return preflight(allow);
28169
+ const res = await route(req, srv, url, d);
28170
+ return res && allow ? withCors(res, allow) : res;
27563
28171
  };
27564
28172
  }
27565
28173
 
@@ -27688,22 +28296,20 @@ function startServer(opts) {
27688
28296
  agents: s.agents.adapters(),
27689
28297
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
27690
28298
  });
27691
- let statusesInFlight = false;
27692
- let statusesDirty = false;
28299
+ let statusesQueued = false;
27693
28300
  const worktreesChanged = () => {
27694
- if (statusesInFlight) {
27695
- statusesDirty = true;
28301
+ if (statusesQueued)
27696
28302
  return;
27697
- }
27698
- statusesInFlight = true;
27699
- fireAndForget("ws", (async () => {
27700
- do {
27701
- statusesDirty = false;
27702
- broadcast({ t: "worktrees", rows: await s.worktrees.rows(), trunks: await s.worktrees.trunks() });
27703
- } while (statusesDirty);
27704
- })().finally(() => {
27705
- statusesInFlight = false;
27706
- }), "worktrees broadcast");
28303
+ statusesQueued = true;
28304
+ setTimeout(() => {
28305
+ statusesQueued = false;
28306
+ try {
28307
+ const { rows, trunks } = s.worktrees.snapshot();
28308
+ broadcast({ t: "worktrees", rows, trunks });
28309
+ } catch (e) {
28310
+ log.warn("ws", "worktrees broadcast failed", e);
28311
+ }
28312
+ }, 0);
27707
28313
  };
27708
28314
  s.hub.on("worktreesChanged", worktreesChanged);
27709
28315
  s.hub.on("agentStatus", worktreesChanged);
@@ -27817,6 +28423,7 @@ function startServer(opts) {
27817
28423
  s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
27818
28424
  const helloFrame = async () => {
27819
28425
  await s.update.refresh();
28426
+ const { rows, trunks } = s.worktrees.snapshot();
27820
28427
  return {
27821
28428
  t: "hello",
27822
28429
  version,
@@ -27824,8 +28431,8 @@ function startServer(opts) {
27824
28431
  registry: await s.update.registry(),
27825
28432
  protocol: PROTOCOL_VERSION,
27826
28433
  repos: s.state.repos,
27827
- rows: await s.worktrees.rows({ quick: true }),
27828
- trunks: await s.worktrees.trunks({ quick: true }),
28434
+ rows,
28435
+ trunks,
27829
28436
  themes: s.themes.themes,
27830
28437
  themePrefs: s.themes.prefs,
27831
28438
  agents: agentInfos(),
@@ -27834,7 +28441,8 @@ function startServer(opts) {
27834
28441
  home: homedir11(),
27835
28442
  folderDialog: process.platform === "darwin" && !cloud.enabled,
27836
28443
  keepAwake: s.keepAwake.setting(),
27837
- remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
28444
+ remote: opts.remote,
28445
+ machine: machineLabel(hostname2(), opts.remote),
27838
28446
  paired: s.state.paired,
27839
28447
  gitIdentity: await s.repos.gitIdentity(),
27840
28448
  pending: s.repos.pending,
@@ -27874,11 +28482,21 @@ function startServer(opts) {
27874
28482
  restart: (now) => s.restarter.request({ now }),
27875
28483
  restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
27876
28484
  pair: new PairCodes,
27877
- onPaired: () => {
27878
- s.state.notePaired();
28485
+ grants: opts.grants,
28486
+ onPaired: (origin) => {
28487
+ const own = origin === null || origin === `https://${opts.remote?.host}`;
28488
+ if (own)
28489
+ s.state.notePaired();
28490
+ else {
28491
+ s.state.trustOrigin(origin);
28492
+ if (s.state.trustedOrigins.includes(origin))
28493
+ opts.onTrusted(origin);
28494
+ }
27879
28495
  broadcast({ t: "paired" });
27880
28496
  },
27881
- phones: () => readTailnetPhones()
28497
+ trusted: () => s.state.trustedOrigins,
28498
+ phones: () => readTailnetPhones(),
28499
+ mcp: (req, id) => s.mcp.fetch(req, id)
27882
28500
  }),
27883
28501
  websocket: {
27884
28502
  async open(ws) {
@@ -29060,6 +29678,245 @@ class FixService {
29060
29678
  }
29061
29679
  }
29062
29680
 
29681
+ // ../daemon/src/worktrees/handoff.ts
29682
+ import { randomUUID as randomUUID2 } from "crypto";
29683
+ var MESSAGE_MAX = 20000;
29684
+ var TITLE_MAX2 = 200;
29685
+ var PROJECT_MAX = 200;
29686
+ var PER_TURN_MAX = 3;
29687
+ var refused = (text) => ({ text, isError: true });
29688
+ function openProposals(entries) {
29689
+ const open = new Map;
29690
+ for (const { event } of entries) {
29691
+ if (event.type === "handoff-proposed")
29692
+ open.set(event.id, event);
29693
+ else if (event.type === "handoff" || event.type === "handoff-declined")
29694
+ open.delete(event.id);
29695
+ }
29696
+ return [...open.values()];
29697
+ }
29698
+ function proposedThisTurn(entries) {
29699
+ let n = 0;
29700
+ for (let i = entries.length - 1;i >= 0; i--) {
29701
+ const e = entries[i].event;
29702
+ if (e.type === "turn-start")
29703
+ break;
29704
+ if (e.type === "handoff-proposed")
29705
+ n++;
29706
+ }
29707
+ return n;
29708
+ }
29709
+
29710
+ class HandoffService {
29711
+ d;
29712
+ settling = new Set;
29713
+ constructor(d) {
29714
+ this.d = d;
29715
+ d.hub.on("landed", (worktreeId) => this.landed(worktreeId));
29716
+ }
29717
+ targets(worktreeId) {
29718
+ const own = this.d.state.worktree(worktreeId)?.repoId;
29719
+ return this.d.state.repos.filter((r) => r.id !== own);
29720
+ }
29721
+ resolve(worktreeId, project) {
29722
+ const q = project.trim();
29723
+ const others = this.targets(worktreeId);
29724
+ if (others.length === 0)
29725
+ return { refused: "No other project is open in Toyon, so there is nothing to hand off to." };
29726
+ const names = others.map((r) => r.name).join(", ");
29727
+ if (!q)
29728
+ return { refused: `Name the project to continue in. Open projects: ${names}.` };
29729
+ const found = match(others, q);
29730
+ if (found.length === 1)
29731
+ return { repo: found[0] };
29732
+ if (found.length > 1) {
29733
+ return {
29734
+ refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
29735
+ };
29736
+ }
29737
+ const wt = this.d.state.worktree(worktreeId);
29738
+ const own = wt ? this.d.state.repo(wt.repoId) : undefined;
29739
+ if (own && match([own], q).length === 1) {
29740
+ return {
29741
+ refused: `${own.name} is this worktree's own project; a handoff goes to another one. Open projects: ${names}.`
29742
+ };
29743
+ }
29744
+ return { refused: `No open project is called "${q}". Open projects: ${names}.` };
29745
+ }
29746
+ tool(worktreeId) {
29747
+ const names = this.targets(worktreeId).map((r) => r.name);
29748
+ const open = names.length ? `Open projects you can name: ${names.join(", ")}.` : "No other project is open right now.";
29749
+ return {
29750
+ name: HANDOFF_TOOL,
29751
+ description: `Propose continuing the current task in another project Toyon has open, when a change belongs there and not in this worktree. Nothing starts: a card with your message appears in this chat and the person decides. ${open}`,
29752
+ inputSchema: {
29753
+ type: "object",
29754
+ properties: {
29755
+ project: { type: "string", description: "The other project's name as listed, or its path." },
29756
+ message: {
29757
+ type: "string",
29758
+ description: "What that project's agent starts from: what is needed and why, how to reproduce it, and the paths in this worktree that depend on it. It is sent as that agent's first message."
29759
+ },
29760
+ title: { type: "string", description: "A short name for the new worktree, a few words." }
29761
+ },
29762
+ required: ["project", "message"],
29763
+ additionalProperties: false
29764
+ }
29765
+ };
29766
+ }
29767
+ async call(worktreeId, args) {
29768
+ const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
29769
+ const shaped = a && typeof a.project === "string" && typeof a.message === "string" && (a.title === undefined || typeof a.title === "string");
29770
+ if (!shaped)
29771
+ return refused("handoff takes { project: string, message: string, title?: string }.");
29772
+ return this.propose(worktreeId, { project: a.project, message: a.message, ...a.title ? { title: a.title } : {} }, "agent");
29773
+ }
29774
+ propose(worktreeId, p, by) {
29775
+ const wt = this.d.state.worktree(worktreeId);
29776
+ if (!wt)
29777
+ throw new UserError("that chat is gone");
29778
+ if (p.project.length > PROJECT_MAX)
29779
+ return refused(`The project name is over ${PROJECT_MAX} characters.`);
29780
+ if ((p.title ?? "").length > TITLE_MAX2)
29781
+ return refused(`The title is over ${TITLE_MAX2} characters; a few words.`);
29782
+ if (p.message.length > MESSAGE_MAX) {
29783
+ return refused(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
29784
+ }
29785
+ const target = this.resolve(worktreeId, p.project);
29786
+ if ("refused" in target)
29787
+ return refused(target.refused);
29788
+ const message = p.message.trim();
29789
+ if (!message)
29790
+ return refused("Write the message the other project's agent should start from.");
29791
+ const agent = this.d.runtime.ensureAgent(wt).agent;
29792
+ const entries = agent.transcript();
29793
+ if (openProposals(entries).length > 0) {
29794
+ return refused("A handoff is already waiting for the person on this chat; finish your turn.");
29795
+ }
29796
+ if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
29797
+ return refused("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
29798
+ }
29799
+ const title = p.title?.trim();
29800
+ const { repo } = target;
29801
+ agent.note({
29802
+ type: "handoff-proposed",
29803
+ id: randomUUID2(),
29804
+ repo: { id: repo.id, name: repo.name, path: repo.path },
29805
+ message,
29806
+ ...title ? { title } : {},
29807
+ by,
29808
+ mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
29809
+ ts: Date.now()
29810
+ });
29811
+ return { text: "Proposed to the person as a card; they decide. Finish your turn." };
29812
+ }
29813
+ async answer(worktreeId, id, go, note) {
29814
+ const wt = this.d.state.worktree(worktreeId);
29815
+ if (!wt)
29816
+ throw new UserError("that chat is gone");
29817
+ const agent = this.d.runtime.ensureAgent(wt).agent;
29818
+ const entries = agent.transcript();
29819
+ const proposed = entries.find(({ event }) => event.type === "handoff-proposed" && event.id === id)?.event;
29820
+ if (!proposed)
29821
+ throw new UserError("That card is not in this chat");
29822
+ if (!openProposals(entries).some((p) => p.id === id)) {
29823
+ return log.debug(worktreeId, `answer for a handoff that already closed (${id})`);
29824
+ }
29825
+ if (this.settling.has(id))
29826
+ return;
29827
+ if (!go) {
29828
+ agent.note({ type: "handoff-declined", id, ts: Date.now() });
29829
+ return;
29830
+ }
29831
+ this.settling.add(id);
29832
+ try {
29833
+ const repo = this.d.state.repo(proposed.repo.id);
29834
+ if (!repo)
29835
+ throw new UserError(`${proposed.repo.name} is no longer open in Toyon`);
29836
+ const origin = this.d.state.requireRepo(wt.repoId);
29837
+ const added = note?.trim() || undefined;
29838
+ const prompt = handoffPrompt(proposed.message, added, { path: wt.path, project: origin.name, branch: wt.branch });
29839
+ const made = await this.d.create(repo.id, prompt, {
29840
+ ...proposed.title ? { title: proposed.title } : {},
29841
+ mode: proposed.mode,
29842
+ from: { kind: "worktree", ref: wt.branch, origin: { id: wt.id, repoId: wt.repoId } }
29843
+ });
29844
+ agent.note({
29845
+ type: "handoff",
29846
+ id,
29847
+ worktreeId: made.id,
29848
+ repoId: repo.id,
29849
+ repoName: repo.name,
29850
+ title: made.title,
29851
+ ts: Date.now()
29852
+ });
29853
+ } catch (e) {
29854
+ const reason = e instanceof Error ? e.message : String(e);
29855
+ agent.note({ type: "handoff-declined", id, outcome: "failed", reason, ts: Date.now() });
29856
+ throw e;
29857
+ } finally {
29858
+ this.settling.delete(id);
29859
+ }
29860
+ }
29861
+ async ask(worktreeId, repoId, text) {
29862
+ const wt = this.d.state.requireWorktree(worktreeId);
29863
+ const repo = this.d.state.repo(repoId);
29864
+ if (!repo)
29865
+ throw new UserError("no such project");
29866
+ if (repo.id === wt.repoId)
29867
+ throw new UserError(`${repo.name} is this worktree's own project`);
29868
+ const agent = this.d.runtime.agentFor(worktreeId);
29869
+ if (!agent)
29870
+ throw new UserError("worktree still starting; try again in a moment");
29871
+ if (agent.mcpTools === null)
29872
+ await agent.warm();
29873
+ if (agent.mcpTools === true) {
29874
+ agent.send(handoffAskPrompt(repo, text), { asked: { kind: "handoff", why: `continue in ${repo.name}` } });
29875
+ return;
29876
+ }
29877
+ const own = text?.trim();
29878
+ if (!own)
29879
+ throw new UserError(`Write what ${repo.name} should do first`);
29880
+ const r = this.propose(worktreeId, { project: repo.path, message: own }, "person");
29881
+ if (r.isError)
29882
+ throw new UserError(r.text);
29883
+ }
29884
+ landed(worktreeId) {
29885
+ const wt = this.d.state.worktree(worktreeId);
29886
+ if (wt?.from?.kind !== "worktree")
29887
+ return;
29888
+ const origin = this.d.state.worktree(wt.from.origin.id);
29889
+ if (origin?.kind !== "worktree")
29890
+ return;
29891
+ const repo = this.d.state.repo(wt.repoId);
29892
+ if (!repo)
29893
+ return;
29894
+ this.d.runtime.ensureAgent(origin).agent.note({
29895
+ type: "handoff-landed",
29896
+ worktreeId: wt.id,
29897
+ repoName: repo.name,
29898
+ title: wt.title,
29899
+ ...wt.pr?.url ? { url: wt.pr.url } : {},
29900
+ ts: Date.now()
29901
+ });
29902
+ }
29903
+ }
29904
+ function match(repos, q) {
29905
+ const lower = q.toLowerCase();
29906
+ const ladder = [
29907
+ (r) => r.path === q,
29908
+ (r) => r.name === q,
29909
+ (r) => r.name.toLowerCase() === lower,
29910
+ (r) => r.name.toLowerCase().startsWith(lower)
29911
+ ];
29912
+ for (const rung of ladder) {
29913
+ const found = repos.filter(rung);
29914
+ if (found.length > 0)
29915
+ return found;
29916
+ }
29917
+ return [];
29918
+ }
29919
+
29063
29920
  // ../daemon/src/worktrees/landing.ts
29064
29921
  var TAIL_CHARS = 400;
29065
29922
  var DIFF_CHARS = 2000;
@@ -29866,7 +30723,7 @@ var UNWATCHED = {
29866
30723
  step() {},
29867
30724
  git: (cwd, args) => stepRun(cwd, args)
29868
30725
  };
29869
- function refused(what, r, n = 200) {
30726
+ function refused2(what, r, n = 200) {
29870
30727
  if (r.exit === "timeout") {
29871
30728
  const after = describeDuration(r.ceilingMs ?? STEP_TIMEOUT_MS);
29872
30729
  return `${what} gave up after ${after}${r.shown ? "; what it printed is on the chat" : ""}`;
@@ -29877,7 +30734,7 @@ function stepRefused(what, r, n = 200) {
29877
30734
  const row = typeof r.exit === "number" && r.hook ? rowOf(r) : undefined;
29878
30735
  return {
29879
30736
  ok: false,
29880
- message: refused(what, r, n),
30737
+ message: refused2(what, r, n),
29881
30738
  ...row && r.hook ? { hook: { ...row, name: r.hook } } : {}
29882
30739
  };
29883
30740
  }
@@ -30217,7 +31074,7 @@ async function fetchBase(worktreePath, repo, w = UNWATCHED) {
30217
31074
  w.step(`fetching ${base}`);
30218
31075
  const f = await w.git(worktreePath, ["fetch", "--quiet", tracked.remote, tracked.merge]);
30219
31076
  if (!f.ok)
30220
- return { ok: false, message: refused("fetch failed", f) };
31077
+ return { ok: false, message: refused2("fetch failed", f) };
30221
31078
  return { ok: true, message: `fetched ${base}` };
30222
31079
  }
30223
31080
  async function pushBranch(worktreePath, branch, w = UNWATCHED) {
@@ -30621,6 +31478,95 @@ class ArchivedGit {
30621
31478
  }
30622
31479
  }
30623
31480
 
31481
+ // ../daemon/src/worktrees/counts.ts
31482
+ var COUNTS_FLOOR_MS = 1e4;
31483
+ var same2 = (a, b) => a.ahead === b.ahead && a.behind === b.behind && a.unpushed === b.unpushed && a.dirty === b.dirty;
31484
+
31485
+ class Counts {
31486
+ d;
31487
+ cache = new Map;
31488
+ reading = new Map;
31489
+ marks = new Map;
31490
+ markAll = 0;
31491
+ constructor(d) {
31492
+ this.d = d;
31493
+ }
31494
+ get(id) {
31495
+ return this.cache.get(id)?.value;
31496
+ }
31497
+ stale(id) {
31498
+ if (id === undefined) {
31499
+ this.markAll++;
31500
+ for (const c of this.cache.values())
31501
+ c.at = 0;
31502
+ return;
31503
+ }
31504
+ this.marks.set(id, (this.marks.get(id) ?? 0) + 1);
31505
+ const c = this.cache.get(id);
31506
+ if (c)
31507
+ c.at = 0;
31508
+ }
31509
+ put(id, value) {
31510
+ return this.store(id, value);
31511
+ }
31512
+ refresh(ids) {
31513
+ const reads = [];
31514
+ for (const id of new Set(ids)) {
31515
+ const running = this.reading.get(id);
31516
+ if (running) {
31517
+ reads.push(running);
31518
+ continue;
31519
+ }
31520
+ const cached = this.cache.get(id);
31521
+ if (cached && Date.now() - cached.at < COUNTS_FLOOR_MS)
31522
+ continue;
31523
+ if (this.d.going(id))
31524
+ continue;
31525
+ reads.push(this.readOne(id));
31526
+ }
31527
+ if (!reads.length)
31528
+ return null;
31529
+ return Promise.all(reads).then((moved) => moved.some(Boolean));
31530
+ }
31531
+ async read(id) {
31532
+ this.stale(id);
31533
+ await this.refresh([id]);
31534
+ const c = this.cache.get(id);
31535
+ return c?.failed || !c?.value ? null : c.value;
31536
+ }
31537
+ markOf(id) {
31538
+ return (this.marks.get(id) ?? 0) + this.markAll;
31539
+ }
31540
+ readOne(id) {
31541
+ const read = (async () => {
31542
+ for (;; ) {
31543
+ const mark = this.markOf(id);
31544
+ let value = null;
31545
+ try {
31546
+ value = await this.d.read(id);
31547
+ } catch {}
31548
+ if (this.d.going(id))
31549
+ return false;
31550
+ if (this.markOf(id) !== mark)
31551
+ continue;
31552
+ if (value)
31553
+ return this.store(id, value);
31554
+ this.cache.set(id, { value: this.get(id), at: Date.now(), failed: true });
31555
+ return false;
31556
+ }
31557
+ })().finally(() => {
31558
+ this.reading.delete(id);
31559
+ });
31560
+ this.reading.set(id, read);
31561
+ return read;
31562
+ }
31563
+ store(id, value) {
31564
+ const prev = this.cache.get(id)?.value;
31565
+ this.cache.set(id, { value, at: Date.now() });
31566
+ return !prev || !same2(prev, value);
31567
+ }
31568
+ }
31569
+
30624
31570
  // ../daemon/src/worktrees/spare.ts
30625
31571
  import { existsSync as existsSync29 } from "fs";
30626
31572
  import { basename as basename9, join as join26 } from "path";
@@ -30909,7 +31855,7 @@ class Trunk {
30909
31855
  const f = await withFetchLock(repo.path, () => w.git(repo.path, ["fetch", "--quiet"]));
30910
31856
  this.noteFetch(repo.id, f);
30911
31857
  if (!f.ok)
30912
- return { ok: false, message: refused("fetch failed", f) };
31858
+ return { ok: false, message: refused2("fetch failed", f) };
30913
31859
  this.d.invalidateCounts();
30914
31860
  return this.follow(main.id);
30915
31861
  }
@@ -30937,14 +31883,13 @@ class Trunk {
30937
31883
  this.d.hub.emit("worktreesChanged");
30938
31884
  return ff;
30939
31885
  }
30940
- async all(opts = {}) {
30941
- const quick = opts.quick ? { missed: false } : null;
31886
+ all() {
30942
31887
  const out = {};
30943
- await Promise.all(this.d.state.repos.map(async (repo) => {
31888
+ for (const repo of this.d.state.repos) {
30944
31889
  const main = mainOf(this.d.state, repo.id);
30945
31890
  if (!main)
30946
- return;
30947
- const { behind, dirty } = await this.d.counts(main, repo.defaultBranch, quick);
31891
+ continue;
31892
+ const { behind, dirty } = this.d.counts(main) ?? {};
30948
31893
  const stale = this.stale.get(repo.id);
30949
31894
  const fetched = this.fetches.get(repo.id);
30950
31895
  out[repo.id] = {
@@ -30956,9 +31901,7 @@ class Trunk {
30956
31901
  ...fetched?.at ? { fetchedAt: fetched.at } : {},
30957
31902
  ...fetched?.failed ? { fetchFailed: fetched.failed } : {}
30958
31903
  };
30959
- }));
30960
- if (quick?.missed)
30961
- setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
31904
+ }
30962
31905
  return out;
30963
31906
  }
30964
31907
  }
@@ -31071,6 +32014,10 @@ function withCarry(context, c) {
31071
32014
  const line = c.moved ? `This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.` : `${c.branch} has ${files} the person chose to leave there. This worktree does not have them.`;
31072
32015
  return [...context ?? [], line];
31073
32016
  }
32017
+ var idsOf = (rows, trunks) => [
32018
+ ...rows.map((r) => r.id),
32019
+ ...Object.values(trunks).map((t) => t.id)
32020
+ ];
31074
32021
  var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
31075
32022
  `, 1)[0])).join(" ");
31076
32023
 
@@ -31082,21 +32029,29 @@ class WorktreeService {
31082
32029
  trunk;
31083
32030
  archive;
31084
32031
  archiving = new Map;
32032
+ restoring = new Map;
31085
32033
  going = new Set;
31086
- countsCache = new Map;
32034
+ counts;
31087
32035
  handChecked = new Map;
31088
32036
  naming = new Set;
31089
32037
  discoverCache = new Map;
32038
+ relisting = new Map;
32039
+ listMark = 0;
32040
+ pending = new Set;
31090
32041
  usage = new Map;
31091
32042
  constructor(d) {
31092
32043
  this.d = d;
31093
32044
  this.runs = d.runs ?? new RunService({ state: d.state, hub: d.hub });
31094
32045
  this.archive = new WorktreeArchive(d.paths.archiveDir);
32046
+ this.counts = new Counts({
32047
+ read: (id) => this.readCounts(id),
32048
+ going: (id) => this.going.has(id)
32049
+ });
31095
32050
  this.trunk = new Trunk({
31096
32051
  state: d.state,
31097
32052
  hub: d.hub,
31098
- counts: (main, defaultBranch, quick) => quick ? Promise.resolve(this.countsQuick(main.id, quick)) : this.counts(main.id, main.path, defaultBranch, false),
31099
- invalidateCounts: () => this.invalidateCounts()
32053
+ counts: (main) => this.counts.get(main.id),
32054
+ invalidateCounts: () => this.counts.stale()
31100
32055
  });
31101
32056
  this.spare = new SparePool({
31102
32057
  state: d.state,
@@ -31135,7 +32090,7 @@ class WorktreeService {
31135
32090
  this.usage.set(worktreeId, { used, size, ...cost !== undefined ? { cost } : {} });
31136
32091
  });
31137
32092
  d.hub.on("turnSettled", (worktreeId, turn) => {
31138
- this.countsCache.delete(worktreeId);
32093
+ this.counts.stale(worktreeId);
31139
32094
  if (turn.end === "done") {
31140
32095
  const wt = d.state.worktree(worktreeId);
31141
32096
  const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
@@ -31158,7 +32113,8 @@ class WorktreeService {
31158
32113
  const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
31159
32114
  const profile = this.checkProfile(repo, opts.profile);
31160
32115
  const task = taskText(prompt, attachments);
31161
- const base = titleFrom(task);
32116
+ const given = cleanTitle2(opts.title ?? "") || undefined;
32117
+ const base = given ?? titleFrom(task);
31162
32118
  const title = variant ? `${base} v${variant.index}` : base;
31163
32119
  if (opts.carry && variant && variant.of > 1) {
31164
32120
  throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
@@ -31172,6 +32128,10 @@ class WorktreeService {
31172
32128
  claimed.variant = variant;
31173
32129
  if (opts.createdBy)
31174
32130
  claimed.createdBy = opts.createdBy;
32131
+ if (given)
32132
+ delete claimed.unnamed;
32133
+ if (opts.from)
32134
+ claimed.from = opts.from;
31175
32135
  claimed.promptedAt = claimed.createdAt;
31176
32136
  claimed.agent = agent;
31177
32137
  if (opts.mode)
@@ -31191,7 +32151,7 @@ class WorktreeService {
31191
32151
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
31192
32152
  await rt.agent.restart();
31193
32153
  rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
31194
- this.scheduleNaming(claimed, task, variant);
32154
+ this.nameOrAsk(claimed, task, given, variant);
31195
32155
  if (carried.unmoved)
31196
32156
  throw new UserError(carried.unmoved, { delivered: true });
31197
32157
  return claimed;
@@ -31211,11 +32171,12 @@ class WorktreeService {
31211
32171
  kind: "worktree",
31212
32172
  proxyPort: await allocateProxyPort(),
31213
32173
  title,
31214
- unnamed: true,
32174
+ ...given ? {} : { unnamed: true },
31215
32175
  createdAt: Date.now(),
31216
32176
  promptedAt: Date.now(),
31217
32177
  agent,
31218
32178
  ...variant ? { variant } : {},
32179
+ ...opts.from ? { from: opts.from } : {},
31219
32180
  ...opts.createdBy ? { createdBy: opts.createdBy } : {},
31220
32181
  ...profile !== undefined ? { profile } : {},
31221
32182
  ...opts.mode ? { mode: opts.mode } : {},
@@ -31225,11 +32186,18 @@ class WorktreeService {
31225
32186
  const carried = await this.carryMain(repo, wt, !!opts.carry);
31226
32187
  this.launch(wt, repo, repo.path);
31227
32188
  this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
31228
- this.scheduleNaming(wt, task, variant);
32189
+ this.nameOrAsk(wt, task, given, variant);
31229
32190
  if (carried.unmoved)
31230
32191
  throw new UserError(carried.unmoved, { delivered: true });
31231
32192
  return wt;
31232
32193
  }
32194
+ nameOrAsk(wt, task, given, variant) {
32195
+ if (given === undefined) {
32196
+ this.scheduleNaming(wt, task, variant);
32197
+ return;
32198
+ }
32199
+ fireAndForget(wt.id, this.rename(wt.id, given), "branch for the given title");
32200
+ }
31233
32201
  async carryMain(repo, wt, move) {
31234
32202
  try {
31235
32203
  return await this.carryMainOrThrow(repo, wt, move);
@@ -31242,12 +32210,14 @@ class WorktreeService {
31242
32210
  async carryMainOrThrow(repo, wt, move) {
31243
32211
  const main = repo.defaultBranch;
31244
32212
  if (!move) {
31245
- const count = this.countsCache.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
32213
+ const count = this.counts.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
31246
32214
  return { branch: main, moved: false, count };
31247
32215
  }
31248
32216
  let count = 0;
32217
+ let left;
31249
32218
  const unmoved = await withRepoLock(repo.path, async () => {
31250
32219
  count = (await statusFiles(repo.path)).length;
32220
+ left = count;
31251
32221
  if (count === 0)
31252
32222
  return;
31253
32223
  const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
@@ -31258,6 +32228,7 @@ class WorktreeService {
31258
32228
  const sha = after.out.trim();
31259
32229
  if (!after.ok || before.ok && before.out.trim() === sha)
31260
32230
  return;
32231
+ left = 0;
31261
32232
  const entry = async () => {
31262
32233
  const list = await git(repo.path, "stash", "list", "--format=%H");
31263
32234
  const i = list.out.split(`
@@ -31281,11 +32252,15 @@ class WorktreeService {
31281
32252
  }
31282
32253
  const ref = await entry();
31283
32254
  const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
32255
+ left = back?.ok ? count : undefined;
31284
32256
  return back?.ok ? `the changes on ${main} did not apply in ${wt.title}, so they stayed on ${main}` : `the changes on ${main} did not apply in ${wt.title}; they are kept in git stash as "toyon: into ${wt.title}"`;
31285
32257
  });
31286
32258
  const mainWt = this.mainOf(repo.id);
31287
- if (mainWt)
31288
- this.countsCache.delete(mainWt.id);
32259
+ if (mainWt) {
32260
+ if (left !== undefined)
32261
+ this.counts.put(mainWt.id, { ...this.counts.get(mainWt.id), dirty: left });
32262
+ this.counts.stale(mainWt.id);
32263
+ }
31289
32264
  return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
31290
32265
  }
31291
32266
  launch(wt, repo, depsSource, opts = {}) {
@@ -31435,14 +32410,10 @@ class WorktreeService {
31435
32410
  this.restartProcs(wt, repo);
31436
32411
  }
31437
32412
  setMode(worktreeId, mode) {
31438
- const wt = this.d.state.requireWorktree(worktreeId);
31439
- if (wt.kind === "spare")
31440
- throw new UserError("no mode for a spare worktree");
31441
- if (wt.mode === mode)
32413
+ if (!this.d.state.setMode(worktreeId, mode))
31442
32414
  return;
31443
- wt.mode = mode;
31444
- this.d.state.save();
31445
32415
  this.d.hub.emit("worktreesChanged");
32416
+ this.d.runtime.agentFor(worktreeId)?.modeChanged();
31446
32417
  }
31447
32418
  setModel(worktreeId, model) {
31448
32419
  this.setOption(worktreeId, "model", model);
@@ -31726,6 +32697,18 @@ class WorktreeService {
31726
32697
  return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
31727
32698
  }
31728
32699
  async restore(archiveId, createdBy, message) {
32700
+ const running = this.restoring.get(archiveId);
32701
+ const live = running ? await running : this.d.state.worktree(archiveId);
32702
+ if (live) {
32703
+ if (message)
32704
+ await this.send(live.id, { text: message.text, clientId: createdBy, attachments: message.attachments });
32705
+ return live;
32706
+ }
32707
+ const done = this.bringBack(archiveId, createdBy, message).finally(() => this.restoring.delete(archiveId));
32708
+ this.restoring.set(archiveId, done);
32709
+ return done;
32710
+ }
32711
+ async bringBack(archiveId, createdBy, message) {
31729
32712
  const rec = this.archive.get(archiveId);
31730
32713
  if (!rec)
31731
32714
  throw new UserError("that archived worktree is gone");
@@ -31785,7 +32768,7 @@ class WorktreeService {
31785
32768
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
31786
32769
  if (!dropped.ok)
31787
32770
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
31788
- this.countsCache.delete(wt.id);
32771
+ this.counts.stale(wt.id);
31789
32772
  this.launch(wt, repo, repo.path);
31790
32773
  this.d.runtime.ensureAgent(wt).agent.note({
31791
32774
  type: "restored",
@@ -31897,8 +32880,7 @@ class WorktreeService {
31897
32880
  for (const w of sources)
31898
32881
  await this.discardWorktree(w.id);
31899
32882
  await this.refreshLanded(target);
31900
- this.countsCache.delete(target.id);
31901
- this.d.hub.emit("worktreesChanged");
32883
+ await this.headMoved(target.id);
31902
32884
  return { target, grafted: sources.map((w) => w.title) };
31903
32885
  }
31904
32886
  startPreview(worktreeId) {
@@ -32063,7 +33045,7 @@ ${wt.landing.body}` : s;
32063
33045
  const result = await commitWorktree(wt.path, message, this.landWatch(wt));
32064
33046
  if (result.ok) {
32065
33047
  await this.verdictSurvives(wt, { dropMessage: true });
32066
- this.headMoved(wt.id);
33048
+ await this.headMoved(wt.id);
32067
33049
  }
32068
33050
  return result;
32069
33051
  }
@@ -32165,6 +33147,7 @@ ${wt.landing.body}` : s;
32165
33147
  }
32166
33148
  noteLanded(wt, message, archiveIds) {
32167
33149
  this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, archiveIds, ts: Date.now() });
33150
+ this.d.hub.emit("landed", wt.id);
32168
33151
  }
32169
33152
  siblingIds(wt) {
32170
33153
  return siblingsOf(wt, this.d.state.worktrees).map((s) => s.id);
@@ -32232,11 +33215,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32232
33215
  const taken = await takeMainIn(wt.path, base, own, w);
32233
33216
  if (!taken.ok)
32234
33217
  return { result: taken };
32235
- this.headMoved(wt.id);
33218
+ await this.headMoved(wt.id);
32236
33219
  const pushed = await pushBranch(wt.path, wt.branch, w);
32237
33220
  if (!pushed.ok)
32238
33221
  return { result: pushed };
32239
- this.headMoved(wt.id);
33222
+ await this.headMoved(wt.id);
32240
33223
  this.setLanding(wt.id, undefined);
32241
33224
  const did = committed ? "committed and pushed" : "pushed";
32242
33225
  return { result: { ok: true, url: wt.pr.url, message: `${did}; PR #${wt.pr.number} has the new commits` } };
@@ -32248,7 +33231,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32248
33231
  const taken = await takeMainIn(wt.path, base, own, w);
32249
33232
  if (!taken.ok)
32250
33233
  return { result: taken };
32251
- this.headMoved(wt.id);
33234
+ await this.headMoved(wt.id);
32252
33235
  if ((await aheadBehind(wt.path, base)).ahead === 0) {
32253
33236
  const r = await this.prMergedOp(wt.id, mark, w);
32254
33237
  const on = `origin's ${repo.defaultBranch} has this work already`;
@@ -32329,7 +33312,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32329
33312
  const taken = await takeMainIn(wt.path, base, own, w);
32330
33313
  if (!taken.ok)
32331
33314
  return { result: taken };
32332
- this.headMoved(wt.id);
33315
+ await this.headMoved(wt.id);
32333
33316
  if ((await aheadBehind(wt.path, base)).ahead === 0) {
32334
33317
  already = true;
32335
33318
  sha = await commitOf(wt.path, base) ?? "";
@@ -32337,7 +33320,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32337
33320
  }
32338
33321
  const squash = method === "squash" ? await squashMessage(wt.path, base, suggested) : "";
32339
33322
  const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
32340
- this.headMoved(wt.id);
33323
+ await this.headMoved(wt.id);
32341
33324
  if (!built.ok || !built.sha)
32342
33325
  return { result: built };
32343
33326
  sha = built.sha;
@@ -32386,7 +33369,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32386
33369
  const r = await git(wt.path, "reset", "--keep", base);
32387
33370
  if (!r.ok)
32388
33371
  log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
32389
- this.headMoved(wt.id);
33372
+ await this.headMoved(wt.id);
32390
33373
  }
32391
33374
  async fetchBase(wt, repo, w) {
32392
33375
  if (!baseIsRemote(repo))
@@ -32453,7 +33436,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32453
33436
  }
32454
33437
  const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w, carry));
32455
33438
  if (taken.ok) {
32456
- this.headMoved(worktreeId);
33439
+ await this.headMoved(worktreeId);
32457
33440
  if (r.wt)
32458
33441
  await this.verdictSurvives(r.wt, { dropMessage: false });
32459
33442
  }
@@ -32469,10 +33452,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32469
33452
  const w = { step: (name) => this.step(worktreeId, name), git: UNWATCHED.git };
32470
33453
  return this.ship(worktreeId, "pull-main", async () => {
32471
33454
  const result = await this.trunk.pull(repo.id, w);
32472
- if (result.ok) {
32473
- this.invalidateCounts();
32474
- this.headMoved(worktreeId);
32475
- }
33455
+ if (result.ok && result.moved)
33456
+ await this.recountAll();
32476
33457
  return result;
32477
33458
  });
32478
33459
  }
@@ -32494,8 +33475,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32494
33475
  syncTrunk(repoId) {
32495
33476
  return this.trunk.sync(repoId);
32496
33477
  }
32497
- async trunks(opts = {}) {
32498
- const all = await this.trunk.all(opts);
33478
+ trunksNow() {
33479
+ const all = this.trunk.all();
32499
33480
  for (const t of Object.values(all)) {
32500
33481
  const shipping = this.ops.get(t.id);
32501
33482
  if (shipping)
@@ -32511,79 +33492,95 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32511
33492
  this.usage.set(worktreeId, found);
32512
33493
  return found ?? undefined;
32513
33494
  }
32514
- headMoved(worktreeId) {
32515
- this.countsCache.delete(worktreeId);
33495
+ async headMoved(worktreeId) {
33496
+ this.counts.stale(worktreeId);
33497
+ await this.counts.refresh([worktreeId]);
32516
33498
  this.d.hub.emit("worktreesChanged");
32517
33499
  }
33500
+ async recountAll() {
33501
+ this.counts.stale();
33502
+ await this.counts.refresh(this.countedIds());
33503
+ }
32518
33504
  invalidateCounts(worktreeId) {
32519
- if (worktreeId)
32520
- this.countsCache.delete(worktreeId);
32521
- else
32522
- this.countsCache.clear();
33505
+ this.counts.stale(worktreeId);
32523
33506
  }
32524
33507
  recount(repoId) {
32525
- this.countsCache.clear();
33508
+ this.counts.stale();
32526
33509
  this.d.hub.emit("repoTick", repoId);
32527
33510
  const repo = this.d.state.repo(repoId);
32528
33511
  if (repo && baseIsRemote(repo))
32529
33512
  fireAndForget(repoId, this.trunk.sync(repoId), "trunk sync");
32530
33513
  }
32531
33514
  invalidateDiscovered() {
32532
- this.discoverCache.clear();
33515
+ this.listMark++;
33516
+ for (const cached of this.discoverCache.values())
33517
+ cached.at = 0;
32533
33518
  }
32534
33519
  discoveredById(id) {
32535
- for (const { rows } of this.discoverCache.values()) {
32536
- const found = rows.find((r) => r.id === id);
32537
- if (found)
32538
- return found;
32539
- }
32540
- return null;
32541
- }
32542
- discoveredQuick(quick) {
32543
- return this.d.state.repos.flatMap((repo) => {
32544
- const cached = this.discoverCache.get(repo.id);
32545
- if (cached)
32546
- return cached.rows;
32547
- quick.missed = true;
32548
- return [];
32549
- });
33520
+ return this.foundCached().find((r) => r.id === id) ?? null;
32550
33521
  }
32551
33522
  async discovered() {
32552
- const perRepo = await Promise.all(this.d.state.repos.map(async (repo) => {
32553
- const cached = this.discoverCache.get(repo.id);
32554
- if (cached && Date.now() - cached.at < 1e4)
32555
- return cached.rows;
32556
- try {
32557
- const rows = await discoverIn(repo.id, repo.path, this.d.state.worktrees);
32558
- this.discoverCache.set(repo.id, { rows, at: Date.now() });
32559
- return rows;
32560
- } catch (e) {
32561
- log.warn(repo.id, "could not list this repo's worktrees", e);
32562
- return cached?.rows ?? [];
32563
- }
33523
+ await this.relist(this.d.state.repos.filter((repo) => this.listDue(repo.id)));
33524
+ return this.foundCached();
33525
+ }
33526
+ foundNow() {
33527
+ const due = this.d.state.repos.filter((repo) => this.listDue(repo.id));
33528
+ if (due.length)
33529
+ this.behind("discover", this.relist(due), "worktree list");
33530
+ return this.foundCached();
33531
+ }
33532
+ foundCached() {
33533
+ const owned = new Set(this.d.state.worktrees.flatMap((w) => [w.id, w.path]));
33534
+ return this.d.state.repos.flatMap((repo) => (this.discoverCache.get(repo.id)?.rows ?? []).filter((r) => !owned.has(r.id) && !owned.has(r.path)));
33535
+ }
33536
+ listDue(repoId) {
33537
+ const cached = this.discoverCache.get(repoId);
33538
+ return !cached || Date.now() - cached.at >= COUNTS_FLOOR_MS;
33539
+ }
33540
+ async relist(repos) {
33541
+ const moved = await Promise.all(repos.map((repo) => {
33542
+ const running = this.relisting.get(repo.id);
33543
+ if (running)
33544
+ return running;
33545
+ const listing = (async () => {
33546
+ const cached = this.discoverCache.get(repo.id);
33547
+ try {
33548
+ for (;; ) {
33549
+ const mark = this.listMark;
33550
+ const rows = await discoverIn(repo.id, repo.path, this.d.state.worktrees);
33551
+ if (mark !== this.listMark)
33552
+ continue;
33553
+ this.discoverCache.set(repo.id, { rows, at: Date.now() });
33554
+ return JSON.stringify(cached?.rows ?? []) !== JSON.stringify(rows);
33555
+ }
33556
+ } catch (e) {
33557
+ log.warn(repo.id, "could not list this repo's worktrees", e);
33558
+ return false;
33559
+ } finally {
33560
+ this.relisting.delete(repo.id);
33561
+ }
33562
+ })();
33563
+ this.relisting.set(repo.id, listing);
33564
+ return listing;
32564
33565
  }));
32565
- const rows = perRepo.flat();
32566
- this.d.runtime.pruneLooseShells(new Set(rows.map((r) => r.id)));
32567
- return rows;
32568
- }
32569
- async counts(id, path, base, countable) {
32570
- const cached = this.countsCache.get(id);
32571
- if (cached && Date.now() - cached.at < 1e4)
32572
- return cached;
32573
- if (this.going.has(id))
32574
- return cached ?? {};
32575
- try {
32576
- const ab = countable ? await aheadBehind(path, base) : await this.trunk.behind(id, path);
32577
- const dirty = (await statusFiles(path)).length;
32578
- const unpushed = await this.unpushed(id, path);
32579
- if (this.going.has(id))
32580
- return cached ?? {};
32581
- const fresh = { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty, at: Date.now() };
32582
- this.countsCache.set(id, fresh);
32583
- return fresh;
32584
- } catch {
32585
- return cached ?? {};
32586
- }
33566
+ this.d.runtime.pruneLooseShells(new Set(this.foundCached().map((r) => r.id)));
33567
+ const any = moved.some(Boolean);
33568
+ if (any)
33569
+ this.d.hub.emit("worktreesChanged");
33570
+ return any;
33571
+ }
33572
+ async readCounts(id) {
33573
+ const wt = this.d.state.worktree(id);
33574
+ const found = wt ? null : this.discoveredById(id);
33575
+ const row = wt ?? found;
33576
+ const repo = row && this.d.state.repo(row.repoId);
33577
+ if (!row || !repo)
33578
+ return null;
33579
+ const countable = wt ? !isMain(wt) : !!found?.branch;
33580
+ const ab = countable ? await aheadBehind(row.path, baseOf(repo)) : await this.trunk.behind(id, row.path);
33581
+ const dirty = (await statusFiles(row.path)).length;
33582
+ const unpushed = await this.unpushed(id, row.path);
33583
+ return { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty };
32587
33584
  }
32588
33585
  async unpushed(id, path) {
32589
33586
  if (this.d.state.worktree(id)?.pr?.state !== "open")
@@ -32591,12 +33588,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32591
33588
  return await aheadUpstream(path) ?? undefined;
32592
33589
  }
32593
33590
  async freshCounts(worktreeId) {
32594
- const wt = this.d.state.worktree(worktreeId);
32595
- const repo = wt && this.d.state.repo(wt.repoId);
32596
- if (!wt || !repo)
33591
+ if (this.going.has(worktreeId))
32597
33592
  return {};
32598
- this.countsCache.delete(worktreeId);
32599
- return this.counts(wt.id, wt.path, baseOf(repo), true);
33593
+ return await this.counts.read(worktreeId) ?? {};
32600
33594
  }
32601
33595
  readable(id) {
32602
33596
  const wt = this.d.state.worktree(id);
@@ -32687,14 +33681,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32687
33681
  }
32688
33682
  }
32689
33683
  noteCounts(id, main, dirty, ab) {
32690
- const prev = this.countsCache.get(id);
33684
+ const prev = this.counts.get(id);
32691
33685
  if (main && !prev)
32692
33686
  return;
32693
33687
  const ahead = main ? prev?.ahead : ab.ahead;
32694
33688
  const behind = main ? prev?.behind : ab.behind;
32695
33689
  const unpushed = main ? prev?.unpushed : ab.unpushed;
32696
- this.countsCache.set(id, { ahead, behind, unpushed, dirty, at: Date.now() });
32697
- if (prev?.dirty !== dirty || prev?.ahead !== ahead || prev?.behind !== behind || prev?.unpushed !== unpushed)
33690
+ if (this.counts.put(id, { ahead, behind, unpushed, dirty }))
32698
33691
  this.d.hub.emit("worktreesChanged");
32699
33692
  }
32700
33693
  async gitLog(worktreeId) {
@@ -32738,12 +33731,40 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32738
33731
  this.d.state.save();
32739
33732
  this.d.hub.emit("worktreesChanged");
32740
33733
  }
32741
- async rows(opts = {}) {
32742
- const quick = opts.quick ? { missed: false } : null;
32743
- const [owned, found] = await Promise.all([this.ownedRows(quick), this.foundRows(quick)]);
32744
- if (quick?.missed)
32745
- setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
32746
- return [...owned, ...found];
33734
+ snapshot() {
33735
+ const rows = [...this.ownedRows(), ...this.foundRows()];
33736
+ const trunks = this.trunksNow();
33737
+ const reading = this.counts.refresh(idsOf(rows, trunks));
33738
+ if (reading) {
33739
+ this.behind("counts", reading.then((moved) => {
33740
+ if (moved)
33741
+ this.d.hub.emit("worktreesChanged");
33742
+ }), "badge counts");
33743
+ }
33744
+ return { rows, trunks };
33745
+ }
33746
+ rows() {
33747
+ return this.snapshot().rows;
33748
+ }
33749
+ trunks() {
33750
+ return this.snapshot().trunks;
33751
+ }
33752
+ countedIds() {
33753
+ return idsOf([...this.ownedRows(), ...this.foundCached()], this.trunksNow());
33754
+ }
33755
+ behind(tag, read, what) {
33756
+ this.pending.add(read);
33757
+ const done = () => this.pending.delete(read);
33758
+ read.then(done, done);
33759
+ fireAndForget(tag, read, what);
33760
+ }
33761
+ async settled() {
33762
+ let waited = false;
33763
+ while (this.pending.size) {
33764
+ waited = true;
33765
+ await Promise.all(this.pending);
33766
+ }
33767
+ return waited;
32747
33768
  }
32748
33769
  mainOf(repoId) {
32749
33770
  return mainOf(this.d.state, repoId);
@@ -32751,24 +33772,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32751
33772
  leadSpareOf(repoId) {
32752
33773
  return this.spare.current(repoId)?.worktreeId ?? null;
32753
33774
  }
32754
- countsQuick(id, quick) {
32755
- const c = this.countsCache.get(id);
32756
- if (c)
32757
- return c;
32758
- quick.missed = true;
32759
- return {};
32760
- }
32761
33775
  isRow(wt) {
32762
33776
  if (wt.kind === "worktree")
32763
33777
  return true;
32764
33778
  const lead = this.leadSpareOf(wt.repoId);
32765
33779
  return wt.kind === "spare" ? lead === wt.id : lead === null;
32766
33780
  }
32767
- async ownedRows(quick) {
32768
- return Promise.all(this.d.state.worktrees.filter((wt) => this.isRow(wt)).map(async (wt) => {
33781
+ ownedRows() {
33782
+ return this.d.state.worktrees.filter((wt) => this.isRow(wt)).map((wt) => {
32769
33783
  const rt = this.d.runtime.get(wt.id);
32770
- const repo = this.d.state.requireRepo(wt.repoId);
32771
- const { ahead, behind, unpushed, dirty } = quick ? this.countsQuick(wt.id, quick) : await this.counts(wt.id, wt.path, baseOf(repo), !isMain(wt));
33784
+ const { ahead, behind, unpushed, dirty } = this.counts.get(wt.id) ?? {};
32772
33785
  return {
32773
33786
  id: wt.id,
32774
33787
  repoId: wt.repoId,
@@ -32791,13 +33804,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32791
33804
  sessionId: this.d.state.session(wt.id),
32792
33805
  shipping: this.ops.get(wt.id)
32793
33806
  };
32794
- }));
33807
+ });
32795
33808
  }
32796
- async foundRows(quick) {
32797
- const found = quick ? this.discoveredQuick(quick) : await this.discovered();
32798
- return Promise.all(found.map(async (f) => {
32799
- const repo = this.d.state.repo(f.repoId);
32800
- const { ahead, behind, dirty } = !repo ? {} : quick ? this.countsQuick(f.id, quick) : await this.counts(f.id, f.path, baseOf(repo), !!f.branch);
33809
+ foundRows() {
33810
+ return this.foundNow().map((f) => {
33811
+ const { ahead, behind, dirty } = this.counts.get(f.id) ?? {};
32801
33812
  return {
32802
33813
  ...f,
32803
33814
  procs: [],
@@ -32808,7 +33819,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
32808
33819
  dirty,
32809
33820
  shipping: this.ops.get(f.id)
32810
33821
  };
32811
- }));
33822
+ });
32812
33823
  }
32813
33824
  }
32814
33825
 
@@ -32919,6 +33930,12 @@ ensureDirs(paths);
32919
33930
  var token = loadOrCreateToken(paths);
32920
33931
  var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
32921
33932
  var remote = loadRemote(paths.remoteFile, process.env, managed.policy);
33933
+ var grants = new GrantCodes;
33934
+ var previewGate = remote && {
33935
+ grant: previewGrant(token),
33936
+ host: remote.host,
33937
+ redeem: (code) => grants.redeem(code)
33938
+ };
32922
33939
  if (remote && addressedByPort(remote.previews))
32923
33940
  pinProxyPorts(PREVIEW_PORTS);
32924
33941
  var state = new StateStore(paths);
@@ -32951,6 +33968,9 @@ var accounts = new AgentAccounts({
32951
33968
  accounts.onChange = () => hub.emit("agentsChanged");
32952
33969
  var uploads = new UploadStore(paths.uploadsDir);
32953
33970
  var attachments2 = new AttachmentStore(paths.attachmentsDir, uploads);
33971
+ var mcpPort = port;
33972
+ var tools = new ToolSet;
33973
+ var mcp = new ToyonMcp({ url: (id) => `http://127.0.0.1:${mcpPort}/mcp/${id}`, version: package_default.version, tools });
32954
33974
  var runtime = new RuntimeRegistry({
32955
33975
  hub,
32956
33976
  state,
@@ -32962,10 +33982,11 @@ var runtime = new RuntimeRegistry({
32962
33982
  bridgeScript: () => bridge.get(),
32963
33983
  turnStarting: (id) => worktrees.turnStarted(id),
32964
33984
  remote,
32965
- grant: previewGrant(token),
33985
+ gate: previewGate,
32966
33986
  viewed: (id) => idle.isViewed(id),
32967
33987
  shown: (id) => idle.isShown(id),
32968
- mainLeads: (repoId) => worktrees.spare.current(repoId) === null
33988
+ mainLeads: (repoId) => worktrees.spare.current(repoId) === null,
33989
+ mcp
32969
33990
  });
32970
33991
  var drafts = new DraftStore({
32971
33992
  file: paths.draftsFile,
@@ -32990,6 +34011,13 @@ var worktrees = new WorktreeService({
32990
34011
  fix
32991
34012
  });
32992
34013
  new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
34014
+ var handoff2 = new HandoffService({
34015
+ state,
34016
+ hub,
34017
+ runtime,
34018
+ create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts)
34019
+ });
34020
+ tools.add(handoff2);
32993
34021
  var turns = new TurnService({
32994
34022
  state,
32995
34023
  hub,
@@ -33094,12 +34122,18 @@ var keepAwake = new KeepAwake({
33094
34122
  mode: () => state.keepAwake,
33095
34123
  answerable: remote !== null
33096
34124
  });
33097
- var { branded, stop: stopServer } = startServer({
34125
+ var {
34126
+ server,
34127
+ branded,
34128
+ stop: stopServer
34129
+ } = startServer({
33098
34130
  port,
33099
34131
  token,
33100
34132
  shellDist: SHELL_DIST,
33101
34133
  version: package_default.version,
33102
34134
  noteShellOrigin: (origin) => bridge.learnShellOrigin(origin),
34135
+ onTrusted: () => refreshShellOrigins(),
34136
+ grants,
33103
34137
  remote,
33104
34138
  managed,
33105
34139
  services: {
@@ -33124,6 +34158,8 @@ var { branded, stop: stopServer } = startServer({
33124
34158
  prs,
33125
34159
  landing,
33126
34160
  fix,
34161
+ handoff: handoff2,
34162
+ mcp,
33127
34163
  themes,
33128
34164
  agents,
33129
34165
  accounts,
@@ -33136,17 +34172,22 @@ var { branded, stop: stopServer } = startServer({
33136
34172
  folderDialog: folderDialog()
33137
34173
  }
33138
34174
  });
33139
- bridge.setShellOrigins([
33140
- ...remote?.front === "edge" ? [] : [
33141
- `http://127.0.0.1:${port}`,
33142
- `http://localhost:${port}`,
33143
- `http://toyon.localhost:${port}`,
33144
- ...branded ? ["http://toyon.localhost"] : [],
33145
- `http://127.0.0.1:${SHELL_DEV_PORT}`,
33146
- `http://localhost:${SHELL_DEV_PORT}`
33147
- ],
33148
- ...remote ? [`https://${remote.host}`] : []
33149
- ]);
34175
+ mcpPort = server.port ?? port;
34176
+ function refreshShellOrigins() {
34177
+ bridge.setShellOrigins([
34178
+ ...remote?.front === "edge" ? [] : [
34179
+ `http://127.0.0.1:${port}`,
34180
+ `http://localhost:${port}`,
34181
+ `http://toyon.localhost:${port}`,
34182
+ ...branded ? ["http://toyon.localhost"] : [],
34183
+ `http://127.0.0.1:${SHELL_DEV_PORT}`,
34184
+ `http://localhost:${SHELL_DEV_PORT}`
34185
+ ],
34186
+ ...remote ? [`https://${remote.host}`] : [],
34187
+ ...state.trustedOrigins
34188
+ ]);
34189
+ }
34190
+ refreshShellOrigins();
33150
34191
  writeFileSync10(paths.pidFile, `${process.pid}
33151
34192
  `);
33152
34193
  var stopLagSampler = startLagSampler();