agent-yes 1.122.2 → 1.123.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 (56) hide show
  1. package/default.config.yaml +19 -0
  2. package/dist/{SUPPORTED_CLIS-BTu2brih.js → SUPPORTED_CLIS-B4O2cFlt.js} +2 -2
  3. package/dist/SUPPORTED_CLIS-DHkqGoNv.js +8 -0
  4. package/dist/{agent-yes.config-z-IPzH5U.js → agent-yes.config-D6ycMApr.js} +2 -65
  5. package/dist/cli.js +6 -6
  6. package/dist/configShared-C5QaNPnz.js +71 -0
  7. package/dist/{globalPidIndex-gZuTvTBs.js → globalPidIndex-C7r2m6s7.js} +19 -20
  8. package/dist/index.js +4 -4
  9. package/dist/pidStore-C4c2O15q.js +5 -0
  10. package/dist/{pidStore-B5vBu8Px.js → pidStore-CGKIhaJO.js} +5 -4
  11. package/dist/reaper-BLVA780B.js +3 -0
  12. package/dist/{reaper-Dj8R7ltI.js → reaper-BkjPN7mw.js} +24 -2
  13. package/dist/{remotes-CpGcTr7A.js → remotes-BRCDVnR7.js} +1 -1
  14. package/dist/{remotes-D2fqaRU8.js → remotes-D8GvSbhf.js} +1 -1
  15. package/dist/{schedule-DgRrdA_n.js → schedule-DULdIkU9.js} +7 -7
  16. package/dist/{serve-tn7ZetZs.js → serve-r_2v9EKc.js} +202 -58
  17. package/dist/{setup-dZhgpNse.js → setup-DHa6fX8M.js} +3 -3
  18. package/dist/{share-CksllWW-.js → share-YuM6-Q6A.js} +78 -4
  19. package/dist/{subcommands-D9BWZilr.js → subcommands-B13Kto-u.js} +647 -32
  20. package/dist/subcommands-Tv6AwUkD.js +7 -0
  21. package/dist/{tray-DjCIyakK.js → tray-BVnJLThD.js} +1 -1
  22. package/dist/{ts-CIf0uaR7.js → ts-DgukRoEI.js} +10 -7
  23. package/dist/{versionChecker-DjxKi4qe.js → versionChecker-BqOr1YqC.js} +2 -2
  24. package/dist/{workspaceConfig-XP2NEWmV.js → workspaceConfig-BJO4fzEn.js} +1 -1
  25. package/lab/ui/console-logic.js +222 -10
  26. package/lab/ui/icon.svg +5 -0
  27. package/lab/ui/index.html +689 -14
  28. package/lab/ui/landing.html +276 -0
  29. package/lab/ui/manifest.webmanifest +14 -0
  30. package/lab/ui/sw.js +56 -0
  31. package/package.json +5 -1
  32. package/ts/agentTree.spec.ts +92 -0
  33. package/ts/agentTree.ts +149 -0
  34. package/ts/configShared.ts +4 -0
  35. package/ts/globalPidIndex.ts +28 -20
  36. package/ts/idleWaiter.spec.ts +7 -1
  37. package/ts/index.ts +9 -0
  38. package/ts/lsWatch.spec.ts +61 -0
  39. package/ts/lsWatch.ts +94 -0
  40. package/ts/needsInput.spec.ts +55 -0
  41. package/ts/needsInput.ts +68 -0
  42. package/ts/pidStore.ts +3 -0
  43. package/ts/reaper.spec.ts +26 -2
  44. package/ts/reaper.ts +25 -0
  45. package/ts/resultEnvelope.spec.ts +43 -0
  46. package/ts/resultEnvelope.ts +88 -0
  47. package/ts/serve.ts +276 -41
  48. package/ts/share.ts +156 -3
  49. package/ts/subcommands.ts +0 -0
  50. package/ts/todoParse.spec.ts +68 -0
  51. package/ts/todoParse.ts +88 -0
  52. package/ts/utils.spec.ts +4 -1
  53. package/dist/SUPPORTED_CLIS-DcOKE9Nz.js +0 -8
  54. package/dist/pidStore-7y1cTcAE.js +0 -5
  55. package/dist/reaper-HqcUms2d.js +0 -3
  56. package/dist/subcommands-D8sHibKu.js +0 -6
@@ -0,0 +1,7 @@
1
+ import "./logger-B9h0djqx.js";
2
+ import "./globalPidIndex-C7r2m6s7.js";
3
+ import "./configShared-C5QaNPnz.js";
4
+ import "./remotes-D8GvSbhf.js";
5
+ import { _ as stopTipForCli, a as extractNeedsInput, c as isPidAlive, d as matchKeyword, f as readNotes, g as snapshotStatus, h as runSubcommand, i as cursorAbs, l as isSubcommand, m as resolveOne, n as cmdHelp, o as extractTaskCounts, p as renderRawLog, r as controlCodeFromName, s as finalizedLines, t as GRACEFUL_EXIT_COMMANDS, u as listRecords, v as writeToIpc } from "./subcommands-B13Kto-u.js";
6
+
7
+ export { cmdHelp, isSubcommand, runSubcommand };
@@ -187,4 +187,4 @@ async function startTray() {
187
187
 
188
188
  //#endregion
189
189
  export { ensureTray, startTray };
190
- //# sourceMappingURL=tray-DjCIyakK.js.map
190
+ //# sourceMappingURL=tray-BVnJLThD.js.map
@@ -1,10 +1,10 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-DjxKi4qe.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-BqOr1YqC.js";
3
3
  import { t as agentYesHome } from "./agentYesHome-BvaUOzCV.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
5
- import { t as PidStore } from "./pidStore-B5vBu8Px.js";
6
- import { i as readGlobalPids } from "./globalPidIndex-gZuTvTBs.js";
7
- import { n as sweep, t as register } from "./reaper-Dj8R7ltI.js";
5
+ import { t as PidStore } from "./pidStore-CGKIhaJO.js";
6
+ import { i as readGlobalPids } from "./globalPidIndex-C7r2m6s7.js";
7
+ import { n as register, r as sweep } from "./reaper-BkjPN7mw.js";
8
8
  import { arch, platform } from "process";
9
9
  import { execSync } from "child_process";
10
10
  import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync } from "fs";
@@ -1060,7 +1060,7 @@ async function notifyWebhook(status, details, cwd = process.cwd()) {
1060
1060
 
1061
1061
  //#endregion
1062
1062
  //#region ts/index.ts
1063
- const config = await import("./agent-yes.config-z-IPzH5U.js").then((mod) => mod.default || mod);
1063
+ const config = await import("./agent-yes.config-D6ycMApr.js").then((mod) => mod.default || mod);
1064
1064
  const CLIS_CONFIG = config.clis;
1065
1065
  /**
1066
1066
  * Main function to run agent-cli with automatic yes/no responses
@@ -1212,6 +1212,8 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
1212
1212
  } else logger.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
1213
1213
  sweep().catch(() => {});
1214
1214
  const ptyEnv = { ...env ?? process.env };
1215
+ const inheritedAyPid = Number(ptyEnv.AGENT_YES_PID);
1216
+ const parentPid = Number.isInteger(inheritedAyPid) && inheritedAyPid > 0 ? inheritedAyPid : void 0;
1215
1217
  ptyEnv.AGENT_YES_PID = String(process.pid);
1216
1218
  const ptyOptions = {
1217
1219
  name: "xterm-color",
@@ -1241,7 +1243,8 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
1241
1243
  args: cliArgs,
1242
1244
  prompt,
1243
1245
  cwd: workingDir,
1244
- wrapperPid: process.pid
1246
+ wrapperPid: process.pid,
1247
+ parentPid
1245
1248
  });
1246
1249
  } catch (error) {
1247
1250
  logger.warn(`[pidStore] Failed to register process ${shell.pid}:`, error);
@@ -1784,4 +1787,4 @@ function sleep(ms) {
1784
1787
 
1785
1788
  //#endregion
1786
1789
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1787
- //# sourceMappingURL=ts-CIf0uaR7.js.map
1790
+ //# sourceMappingURL=ts-DgukRoEI.js.map
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  //#region package.json
9
9
  var name = "agent-yes";
10
- var version = "1.122.2";
10
+ var version = "1.123.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -215,4 +215,4 @@ async function displayVersion() {
215
215
 
216
216
  //#endregion
217
217
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
218
- //# sourceMappingURL=versionChecker-DjxKi4qe.js.map
218
+ //# sourceMappingURL=versionChecker-BqOr1YqC.js.map
@@ -53,4 +53,4 @@ function resolveSpawnCwd(input) {
53
53
 
54
54
  //#endregion
55
55
  export { resolveSpawnCwd as n, setWorkspaceRoot as r, getWorkspaceRoot as t };
56
- //# sourceMappingURL=workspaceConfig-XP2NEWmV.js.map
56
+ //# sourceMappingURL=workspaceConfig-BJO4fzEn.js.map
@@ -14,9 +14,17 @@
14
14
  export const cliLabel = (e) => (e.cli && e.cli !== "claude" ? e.cli : "");
15
15
 
16
16
  // Parse owner/repo/branch from a cwd like .../ws/<owner>/<repo>/tree/<branch>.
17
+ // A cwd inside a git submodule keeps trailing path after the worktree branch
18
+ // (e.g. .../tree/share/lib/bot, where lib/bot is a submodule). The owner/repo/
19
+ // branch still describe the superproject worktree — git itself resolves a
20
+ // submodule cwd's identity to the superproject — so we surface the submodule's
21
+ // leaf dir as `sub` to keep nested repos distinguishable. `sub` is "" when the
22
+ // cwd is the worktree root.
17
23
  export function repoBranch(e) {
18
- const m = /\/([^/]+)\/([^/]+)\/tree\/([^/]+)/.exec(e.cwd || "");
19
- return m ? { owner: m[1], repo: m[2], branch: m[3] } : null;
24
+ const m = /\/([^/]+)\/([^/]+)\/tree\/([^/]+)(\/.*)?$/.exec(e.cwd || "");
25
+ if (!m) return null;
26
+ const sub = (m[4] || "").split("/").filter(Boolean).pop() || "";
27
+ return { owner: m[1], repo: m[2], branch: m[3], sub };
20
28
  }
21
29
 
22
30
  // Identity string for the left panel. cap=true → repo/branch each clipped to
@@ -25,7 +33,8 @@ export function ident(e, cap) {
25
33
  const rb = repoBranch(e);
26
34
  if (!rb) return "";
27
35
  const c = (s) => (cap && s.length > 3 ? s.slice(0, 3) : s);
28
- return `${c(rb.repo)}/${c(rb.branch)}`;
36
+ const sub = rb.sub ? `→${rb.sub}` : "";
37
+ return `${c(rb.repo)}/${c(rb.branch)}${sub}`;
29
38
  }
30
39
 
31
40
  // ---- device-aware identity (multi-room) -----------------------------------
@@ -46,11 +55,18 @@ export function deviceParts(host) {
46
55
  // The five identity fields, in display order, for one agent.
47
56
  export function identFields(e) {
48
57
  const d = deviceParts(e._host);
49
- const rb = repoBranch(e) || { owner: "", repo: "", branch: "" };
50
- return { user: d.user, host: d.host, owner: rb.owner, repo: rb.repo, branch: rb.branch };
58
+ const rb = repoBranch(e) || { owner: "", repo: "", branch: "", sub: "" };
59
+ return {
60
+ user: d.user,
61
+ host: d.host,
62
+ owner: rb.owner,
63
+ repo: rb.repo,
64
+ branch: rb.branch,
65
+ sub: rb.sub,
66
+ };
51
67
  }
52
68
 
53
- const IDENT_ORDER = ["user", "host", "owner", "repo", "branch"];
69
+ const IDENT_ORDER = ["user", "host", "owner", "repo", "branch", "sub"];
54
70
 
55
71
  // Precompute, over the whole shown list: which fields are uniform (identical for
56
72
  // every agent — so they can be omitted) and whether any device info exists at
@@ -69,11 +85,22 @@ export function identContext(entries) {
69
85
  // machine-parseable: e.g. all on one device → "@:age/mai", a mixed-device list →
70
86
  // "sno@tak:age/mai". A purely local list (no devices anywhere) falls back to the
71
87
  // legacy "own/rep/bra" with no device prefix.
72
- export function compactIdent(e, ctx, cap = 3) {
88
+ //
89
+ // `parent` is this row's tree parent entry (a subagent's superagent), when it
90
+ // has one. A field that matches the parent's is ALSO blanked: the nesting
91
+ // already conveys it, so a subagent in the same worktree as its parent shows
92
+ // only what differs — often just the submodule leaf (e.g. "//→bot"), or nothing
93
+ // at all (hidden by hasIdent) when it's the very same checkout.
94
+ export function compactIdent(e, ctx, cap = 3, parent = null) {
73
95
  const m = identFields(e);
96
+ const p = parent ? identFields(parent) : null;
74
97
  const clip = (s) => (cap && s.length > cap ? s.slice(0, cap) : s);
75
- const v = (f) => (ctx.uniform[f] ? "" : clip(m[f]));
76
- const path = `${v("owner")}/${v("repo")}/${v("branch")}`;
98
+ const blank = (f) => ctx.uniform[f] || (p != null && p[f] === m[f]);
99
+ const v = (f) => (blank(f) ? "" : clip(m[f]));
100
+ // Submodule leaf is shown in full (the finest-grain distinguisher) and joined
101
+ // with → rather than / so it reads as "inside that worktree".
102
+ const sub = blank("sub") ? "" : m.sub;
103
+ const path = `${v("owner")}/${v("repo")}/${v("branch")}${sub ? `→${sub}` : ""}`;
77
104
  return ctx.anyDevice ? `${v("user")}@${v("host")}:${path}` : path;
78
105
  }
79
106
 
@@ -81,7 +108,8 @@ export function compactIdent(e, ctx, cap = 3) {
81
108
  // prefix only when this agent actually has device info.
82
109
  export function fullIdent(e) {
83
110
  const m = identFields(e);
84
- const path = `${m.owner}/${m.repo}/${m.branch}`;
111
+ const sub = m.sub ? `→${m.sub}` : "";
112
+ const path = `${m.owner}/${m.repo}/${m.branch}${sub}`;
85
113
  return m.user || m.host ? `${m.user}@${m.host}:${path}` : path;
86
114
  }
87
115
 
@@ -108,6 +136,7 @@ export function tagsFor(e) {
108
136
  const rb = repoBranch(e);
109
137
  if (rb) {
110
138
  t.push(["repo", `${rb.owner}/${rb.repo}`], ["wt", rb.branch]);
139
+ if (rb.sub) t.push(["sub", rb.sub]); // submodule leaf, when cwd is nested
111
140
  }
112
141
  const cli = cliLabel(e);
113
142
  if (cli) t.push(["cli", cli]);
@@ -129,6 +158,15 @@ export function gitLabel(e) {
129
158
  return parts.join(" ");
130
159
  }
131
160
 
161
+ // Task-progress badge ("2/5") from the agent's parsed todo block (e.tasks =
162
+ // { done, total }, computed live in /api/ls). Empty string when no todo block was
163
+ // confidently detected — the badge is omitted entirely, never shown as "0/0".
164
+ export function taskLabel(e) {
165
+ const t = e.tasks;
166
+ if (!t || typeof t.total !== "number" || t.total <= 0) return "";
167
+ return `${t.done}/${t.total}`;
168
+ }
169
+
132
170
  // Human age of an agent ("12s" / "5m" / "3h"). `now` is injectable so tests
133
171
  // don't depend on the wall clock; the browser calls age(e) and gets Date.now().
134
172
  export function age(e, now = Date.now()) {
@@ -175,6 +213,180 @@ export function matches(e, toks) {
175
213
  });
176
214
  }
177
215
 
216
+ // Box-drawing rail prefix for a node given the "is-last-child?" flags of each
217
+ // ancestor (root→node). depth 0 → "". Shared by the agent forest and the layered
218
+ // room/peer tree so all rails line up: "│ ", " " for ancestors, "├ "/"└ " here.
219
+ function railPrefix(ancestorsLast) {
220
+ const depth = ancestorsLast.length;
221
+ let s = "";
222
+ for (let i = 0; i < depth - 1; i++) s += ancestorsLast[i] ? " " : "│ ";
223
+ if (depth > 0) s += ancestorsLast[depth - 1] ? "└ " : "├ ";
224
+ return s;
225
+ }
226
+
227
+ // Stable ordered grouping: [[key, items[]], ...] in first-seen key order.
228
+ function groupBy(arr, keyFn) {
229
+ const m = new Map();
230
+ for (const e of arr) {
231
+ const k = keyFn(e);
232
+ if (!m.has(k)) m.set(k, []);
233
+ m.get(k).push(e);
234
+ }
235
+ return [...m.entries()];
236
+ }
237
+
238
+ // Build the agent>subagent forest for ONE host's entries (pids are only unique
239
+ // per machine, so the caller must pre-scope by host). Primary link is the explicit
240
+ // spawn relationship (parent_pid === wrapper_pid). As a FALLBACK, an agent with no
241
+ // such parent whose cwd sits INSIDE another agent's cwd of the SAME worktree
242
+ // (owner/repo/branch) snaps under it — so a submodule/subdir agent nests under its
243
+ // superproject agent even without a parent_pid link. Returns root nodes
244
+ // { entry, children }; sibling/root order = input order. Cycles can't drop nodes:
245
+ // anything not reached from a root is appended as its own root.
246
+ function agentForestNodes(list) {
247
+ const byWrapper = new Map();
248
+ for (const e of list) if (e.wrapper_pid != null) byWrapper.set(e.wrapper_pid, e);
249
+ const nodeOf = new Map(list.map((e) => [e, { entry: e, children: [] }]));
250
+
251
+ const parentOf = new Map();
252
+ // 1) explicit spawn link.
253
+ for (const e of list) {
254
+ const p = e.parent_pid != null ? byWrapper.get(e.parent_pid) : null;
255
+ if (p && p !== e) parentOf.set(e, p);
256
+ }
257
+ // 2) cwd-containment fallback for the still-parentless. The closest ancestor
258
+ // (longest containing cwd) wins; same-worktree guard keeps an unrelated
259
+ // shared prefix (e.g. /Users/x/ws) from grouping strangers together.
260
+ for (const e of list) {
261
+ if (parentOf.has(e) || !e.cwd) continue;
262
+ const rb = repoBranch(e);
263
+ if (!rb) continue;
264
+ let best = null;
265
+ for (const c of list) {
266
+ if (c === e || !c.cwd || !e.cwd.startsWith(c.cwd + "/")) continue;
267
+ const crb = repoBranch(c);
268
+ if (!crb || crb.owner !== rb.owner || crb.repo !== rb.repo || crb.branch !== rb.branch)
269
+ continue;
270
+ if (!best || c.cwd.length > best.cwd.length) best = c;
271
+ }
272
+ if (best) parentOf.set(e, best);
273
+ }
274
+ // Attach, refusing any edge that would close a cycle (a pre-existing parent_pid
275
+ // cycle, or a pid/cwd mix) — such nodes stay roots.
276
+ const wouldCycle = (parent, child) => {
277
+ for (let cur = parent, i = 0; cur && i < list.length + 1; cur = parentOf.get(cur), i++)
278
+ if (cur === child) return true;
279
+ return false;
280
+ };
281
+ const roots = [];
282
+ for (const e of list) {
283
+ const p = parentOf.get(e);
284
+ if (p && p !== e && !wouldCycle(p, e)) nodeOf.get(p).children.push(nodeOf.get(e));
285
+ else roots.push(nodeOf.get(e));
286
+ }
287
+ // Cycle safety: collect nodes reachable from roots; append the rest as roots.
288
+ const seen = new Set();
289
+ const mark = (n) => {
290
+ if (seen.has(n)) return;
291
+ seen.add(n);
292
+ n.children.forEach(mark);
293
+ };
294
+ roots.forEach(mark);
295
+ for (const e of list) if (!seen.has(nodeOf.get(e))) roots.push(nodeOf.get(e));
296
+ return roots;
297
+ }
298
+
299
+ // Order entries as agent>subagent forests so a nested `ay` (one agent spawning
300
+ // another) renders indented under its parent. SCOPED PER HOST. Returns a NEW
301
+ // array in depth-first order; each entry is shallow-copied with `_branch` (a
302
+ // box-drawing tree prefix like "│ └ ") and `_depth`. A fleet with no nesting
303
+ // renders exactly as before (every row a root, empty `_branch`).
304
+ export function forestOrder(entries) {
305
+ const out = [];
306
+ for (const [, list] of groupBy(entries, (e) => e._host || "")) {
307
+ const seen = new Set();
308
+ const walk = (node, ancestorsLast) => {
309
+ if (seen.has(node)) return; // break a pathological parent_pid cycle
310
+ seen.add(node);
311
+ out.push(
312
+ Object.assign({}, node.entry, {
313
+ _branch: railPrefix(ancestorsLast),
314
+ _depth: ancestorsLast.length,
315
+ }),
316
+ );
317
+ node.children.forEach((c, i) =>
318
+ walk(c, ancestorsLast.concat(i === node.children.length - 1)),
319
+ );
320
+ };
321
+ for (const r of agentForestNodes(list)) walk(r, []);
322
+ }
323
+ return out;
324
+ }
325
+
326
+ // Build the full console hierarchy — signalling-server > rooms > peers(hosts) >
327
+ // agents > subagents — and flatten it into ordered render rows, VSCode-explorer
328
+ // style: a container layer (room/peer) with a single node in its scope is HIDDEN
329
+ // (its children float up a level); a layer with ≥2 siblings becomes a tree with
330
+ // ├ └ │ rails. Agents always get their own row, with their subagent forest nested
331
+ // beneath. The server layer is implicit (always one) so it never shows.
332
+ //
333
+ // Returns [{ kind:'room'|'peer'|'agent', label, entry, branch, depth }] in
334
+ // display order. Headers (room/peer) are non-selectable; only 'agent' rows are.
335
+ // A purely local fleet (one room, one unlabelled host) yields only agent rows —
336
+ // identical to forestOrder — so the common case stays a plain agent tree.
337
+ export function layeredRows(entries) {
338
+ const multiRoom = new Set(entries.map((e) => e._room || "")).size > 1;
339
+
340
+ // Container nodes carry a `kind`/`label` for headers; agent nodes carry an
341
+ // `entry`. A hidden layer simply isn't created — its agents/peers attach to the
342
+ // parent — which is what makes single-node layers vanish.
343
+ const roomNodes = [];
344
+ for (const [rid, re] of groupBy(entries, (e) => e._room || "")) {
345
+ const peerGroups = groupBy(re, (e) => e._host || "");
346
+ const multiPeer = peerGroups.length > 1;
347
+ const underRoom = [];
348
+ for (const [host, pe] of peerGroups) {
349
+ const agentNodes = agentForestNodes(pe).map(toAgentNode);
350
+ if (multiPeer && host)
351
+ underRoom.push({ kind: "peer", label: host, room: rid, host, children: agentNodes });
352
+ else underRoom.push(...agentNodes); // single/unlabelled peer hidden
353
+ }
354
+ if (multiRoom && rid) roomNodes.push({ kind: "room", label: rid, children: underRoom });
355
+ else roomNodes.push(...underRoom); // single room hidden
356
+ }
357
+
358
+ const rows = [];
359
+ const seen = new Set();
360
+ // parentAgent carries the nearest ancestor agent's entry down the walk so a
361
+ // subagent row knows its superagent — used to omit identity fields the tree
362
+ // nesting already conveys (see compactIdent). Room/peer headers are skipped.
363
+ const walk = (node, ancestorsLast, parentAgent) => {
364
+ if (seen.has(node)) return; // break a pathological parent_pid cycle
365
+ seen.add(node);
366
+ rows.push({
367
+ kind: node.kind,
368
+ label: node.label,
369
+ entry: node.entry,
370
+ room: node.room, // peer headers: (room,host) keys the connection-type cache
371
+ host: node.host,
372
+ parentEntry: node.kind === "agent" ? parentAgent : null,
373
+ branch: railPrefix(ancestorsLast),
374
+ depth: ancestorsLast.length,
375
+ });
376
+ const nextParent = node.kind === "agent" ? node.entry : parentAgent;
377
+ (node.children || []).forEach((c, i) =>
378
+ walk(c, ancestorsLast.concat(i === node.children.length - 1), nextParent),
379
+ );
380
+ };
381
+ for (const r of roomNodes) walk(r, [], null);
382
+ return rows;
383
+ }
384
+
385
+ // Wrap an agent forest node { entry, children } into a layered-tree node.
386
+ function toAgentNode(n) {
387
+ return { kind: "agent", entry: n.entry, children: n.children.map(toAgentNode) };
388
+ }
389
+
178
390
  // Next selection index when stepping the list by `dir` (+1 down / -1 up).
179
391
  // No current selection (i<0) lands on the first (down) or last (up) row;
180
392
  // otherwise clamps at the ends. Returns -1 for an empty list.
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="agent-yes">
2
+ <rect width="512" height="512" rx="104" fill="#0d1117" />
3
+ <path d="M132 268 l78 80 l170-186" fill="none" stroke="#3fb950" stroke-width="56"
4
+ stroke-linecap="round" stroke-linejoin="round" />
5
+ </svg>