agent-yes 1.169.1 → 1.171.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 (61) hide show
  1. package/dist/SUPPORTED_CLIS-BMExykB1.js +8 -0
  2. package/dist/{SUPPORTED_CLIS-Bwg9VqLI.js → SUPPORTED_CLIS-D6MArV7A.js} +2 -2
  3. package/dist/{agent-yes.config-Bl08kMpl.js → agent-yes.config-ugPYTPOd.js} +2 -2
  4. package/dist/{agentYesHome-_eJa3DaX.js → agentYesHome-CtHb5b71.js} +1 -1
  5. package/dist/cli.js +10 -212
  6. package/dist/{configShared-CEyhl0WH.js → configShared-0MnIQ652.js} +1 -1
  7. package/dist/{e2e-BeKjLhmO.js → e2e-jb0Hp43q.js} +1 -1
  8. package/dist/{forkNested-UCKPEgSI.js → forkNested-DhJxa4q4.js} +2 -2
  9. package/dist/{globalPidIndex-DlmmJlO8.js → globalPidIndex-CoNr7tS8.js} +1 -1
  10. package/dist/index.js +4 -4
  11. package/dist/notifyDaemon-BlGV0y4j.js +591 -0
  12. package/dist/{openBrowser-CCF1iuQK.js → openBrowser-ChR4llYa.js} +1 -1
  13. package/dist/{pidStore-BfoBWUjc.js → pidStore-BIvsBQ8X.js} +3 -3
  14. package/dist/pidStore-CLxwp0qI.js +5 -0
  15. package/dist/{reaper-C-eWAxIj.js → reaper-CWF2_ATd.js} +27 -3
  16. package/dist/{reaper-BQgDb85W.js → reaper-jfSmog15.js} +1 -1
  17. package/dist/{remotes-cx_GDFPj.js → remotes-BQMr4_En.js} +3 -3
  18. package/dist/{remotes-BmAPylU_.js → remotes-CC-4GuJb.js} +3 -3
  19. package/dist/{runningLock-BobVW1_A.js → runningLock-CNMl13dC.js} +1 -1
  20. package/dist/rustBinary-mYbyV0SD.js +228 -0
  21. package/dist/{schedule-CtVvSyaN.js → schedule-C5nUV0GO.js} +7 -7
  22. package/dist/{serve-CLmk5ujF.js → serve-ChcayCC3.js} +122 -26
  23. package/dist/{setup-CYbXOcpH.js → setup-CCQZ9tPB.js} +3 -3
  24. package/dist/{share-D_e2Fwiy.js → share-QregR8a_.js} +2 -2
  25. package/dist/spawnGate-BFhva-2F.js +5 -0
  26. package/dist/{spawnGate-DzPfa1PZ.js → spawnGate-IJDByl2U.js} +3 -3
  27. package/dist/subcommands-CIk1SSdW.js +9 -0
  28. package/dist/{subcommands--EUush_X.js → subcommands-CtWxwydR.js} +1049 -44
  29. package/dist/{tray-CWUpaZF4.js → tray-DXr7iK3E.js} +2 -2
  30. package/dist/{ts-Bbhf5JbN.js → ts-WHYKp13w.js} +8 -8
  31. package/dist/{versionChecker-BmWkWhcJ.js → versionChecker-BoPKPL9B.js} +2 -2
  32. package/dist/{webrtcLink-BG0Xc4-W.js → webrtcLink-B7REGtK2.js} +2 -2
  33. package/dist/{webrtcRemote-SybKurg9.js → webrtcRemote-Bx-eD_0I.js} +3 -3
  34. package/dist/{workspaceConfig-BC03X4Y1.js → workspaceConfig-BgqK-31W.js} +37 -6
  35. package/lab/ui/console-logic.js +125 -10
  36. package/lab/ui/index.html +340 -11
  37. package/lab/ui/landing.html +11 -1
  38. package/package.json +1 -1
  39. package/scripts/build-rs.ts +9 -5
  40. package/ts/badges.spec.ts +51 -1
  41. package/ts/badges.ts +35 -1
  42. package/ts/notifyDaemon.spec.ts +276 -0
  43. package/ts/notifyDaemon.ts +698 -0
  44. package/ts/notifyInbox.spec.ts +192 -0
  45. package/ts/notifyInbox.ts +294 -0
  46. package/ts/notifyRouter.spec.ts +355 -0
  47. package/ts/notifyRouter.ts +288 -0
  48. package/ts/notifyStore.spec.ts +308 -0
  49. package/ts/notifyStore.ts +439 -0
  50. package/ts/reaper.spec.ts +18 -1
  51. package/ts/reaper.ts +33 -1
  52. package/ts/rustBinary.ts +30 -0
  53. package/ts/serve.ts +194 -14
  54. package/ts/subcommands.spec.ts +341 -0
  55. package/ts/subcommands.ts +764 -29
  56. package/ts/workspaceConfig.spec.ts +48 -0
  57. package/ts/workspaceConfig.ts +46 -3
  58. package/dist/SUPPORTED_CLIS-DiL99G3j.js +0 -8
  59. package/dist/pidStore-5qyLX3Fh.js +0 -5
  60. package/dist/spawnGate-CQ1Il3Xk.js +0 -5
  61. package/dist/subcommands-1ZcaHYkb.js +0 -9
package/lab/ui/index.html CHANGED
@@ -37,6 +37,9 @@
37
37
  --purple: #bc8cff;
38
38
  --pink: #f778ba;
39
39
  --red: #f85149;
40
+ /* Peer-focus yellow: a human collaborator (not you) is on this agent.
41
+ Distinct from your own blue (--accent) and the idle-dot amber. */
42
+ --peer: #e3b341;
40
43
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
41
44
  /* Safe-area insets — non-zero only in a standalone PWA on a notched /
42
45
  gesture-bar device (else 0, so these are harmless on desktop). Consumed
@@ -70,6 +73,7 @@
70
73
  --purple: #8250df;
71
74
  --pink: #bf3989;
72
75
  --red: #cf222e;
76
+ --peer: #bf8700;
73
77
  }
74
78
  }
75
79
  * {
@@ -420,6 +424,21 @@
420
424
  font-size: 11.5px;
421
425
  margin-top: 8px;
422
426
  }
427
+ .metaleft {
428
+ display: inline-flex;
429
+ align-items: center;
430
+ gap: 8px;
431
+ }
432
+ /* Peers badge beside the agent counter — how many other humans are in the
433
+ fleet right now. Yellow ("here") when a peer shares the agent you have
434
+ open, muted otherwise. Hidden entirely when you're alone. */
435
+ .peers {
436
+ color: var(--muted);
437
+ cursor: default;
438
+ }
439
+ .peers.here {
440
+ color: var(--peer);
441
+ }
423
442
  .connbadge {
424
443
  cursor: pointer;
425
444
  user-select: none;
@@ -630,6 +649,10 @@
630
649
  .omni-spawn .omni-title {
631
650
  color: var(--green);
632
651
  }
652
+ /* Destructive slash command (/kill) — red title, matching the ⋯-menu Force-kill. */
653
+ .omni-spawn.danger .omni-title {
654
+ color: var(--red);
655
+ }
633
656
  .omni-empty {
634
657
  padding: 14px;
635
658
  color: var(--muted);
@@ -814,6 +837,39 @@
814
837
  background: var(--panel);
815
838
  box-shadow: inset 3px 0 0 var(--accent);
816
839
  }
840
+ /* A human peer (not you) is watching this agent: yellow, mirrored on the
841
+ RIGHT edge so it can coexist with your own blue selection bar on the
842
+ left — a row you BOTH have open shows blue-left + yellow-right. */
843
+ .row.peerfocus {
844
+ box-shadow: inset -3px 0 0 var(--peer);
845
+ background: color-mix(in srgb, var(--peer) 7%, transparent);
846
+ }
847
+ .row.sel.peerfocus {
848
+ background: var(--panel);
849
+ box-shadow: inset 3px 0 0 var(--accent), inset -3px 0 0 var(--peer);
850
+ }
851
+ /* stdin just landed here (someone typed, or `ay send` pushed a command) —
852
+ pulse the row regardless of who (if anyone) is focused on it. Runs while
853
+ the .stdinflash class is present; index.html removes it after the window. */
854
+ .row.stdinflash {
855
+ animation: stdinpulse 0.6s ease-in-out infinite;
856
+ }
857
+ @keyframes stdinpulse {
858
+ 0%,
859
+ 100% {
860
+ background-color: transparent;
861
+ }
862
+ 50% {
863
+ background-color: color-mix(in srgb, var(--peer) 24%, transparent);
864
+ }
865
+ }
866
+ /* Per-row chip: how many peers are watching THIS agent (yellow, mono). */
867
+ .peerchip {
868
+ font-family: var(--mono);
869
+ font-size: 11px;
870
+ color: var(--peer);
871
+ flex: none;
872
+ }
817
873
  .r1 {
818
874
  display: flex;
819
875
  align-items: center;
@@ -891,6 +947,26 @@
891
947
  color: var(--green);
892
948
  border-color: #2a3a2a;
893
949
  }
950
+ /* Folded-subagent summary chip ("⊞ 3/5" = 3 working of 5 hidden subagents),
951
+ shown on a root agent row when the subagent trees are collapsed. Same pill
952
+ shape as .tasks; the ⊞ glyph echoes the fold toolbar button. Turns accent
953
+ when any hidden subagent is still working, so a collapsed tree with live
954
+ children stands out from a quiet one. Hover for the count + last-active. */
955
+ .subs {
956
+ font-family: var(--mono);
957
+ font-size: 10.5px;
958
+ color: var(--muted);
959
+ white-space: nowrap;
960
+ flex: none;
961
+ padding: 0 5px;
962
+ border: 1px solid var(--line);
963
+ border-radius: 8px;
964
+ cursor: pointer;
965
+ }
966
+ .subs.active {
967
+ color: var(--accent);
968
+ border-color: var(--accent);
969
+ }
894
970
  /* Status-flag chip ("badge") matched server-side against the agent's
895
971
  screen — e.g. an active /goal loop (ts/badges.ts). Same pill shape as
896
972
  .tasks but accent-colored so a matched flag stands out as a distinct
@@ -1368,8 +1444,12 @@
1368
1444
  />
1369
1445
  </div>
1370
1446
  <div class="meta">
1371
- <span id="count"></span>
1447
+ <span class="metaleft">
1448
+ <span id="count"></span>
1449
+ <span id="peers" class="peers" hidden></span>
1450
+ </span>
1372
1451
  <span class="metaright">
1452
+ <button id="foldbtn" class="viewbtn" title="fold subagent trees">⊞ subs</button>
1373
1453
  <button id="sortbtn" class="viewbtn" title="cycle sort order">⇅ state</button>
1374
1454
  <button id="viewbtn" class="viewbtn" title="toggle compact list">☰</button>
1375
1455
  <button id="newbtn" class="newbtn" title="spawn a new agent on this fleet">
@@ -1554,10 +1634,13 @@
1554
1634
  hasIdent,
1555
1635
  deviceCount,
1556
1636
  layeredRows,
1637
+ foldSummaries,
1557
1638
  sortEntries,
1558
1639
  SORT_MODES,
1559
1640
  taskLabel,
1560
1641
  badgesFor,
1642
+ advanceStdinFlashes,
1643
+ focusSummary,
1561
1644
  hashHue,
1562
1645
  selFromBottom,
1563
1646
  parseSel,
@@ -1584,6 +1667,17 @@
1584
1667
 
1585
1668
  let entries = [];
1586
1669
  let sel = null; // selected agent's composite key (room#pid)
1670
+ // Collaborative presence, fleet-wide: focusByKey maps an agent's _key ->
1671
+ // how many OTHER human peers are watching it (yellow bars + per-row chip);
1672
+ // peerTotal is the distinct-peer count for the header badge. stdinSeen +
1673
+ // flashUntil drive the stdin pulse: stdinSeen remembers each agent's last
1674
+ // last_stdin_at so we can spot a bump, flashUntil holds the deadline the
1675
+ // row keeps pulsing to. See advanceStdinFlashes/focusSummary (console-logic).
1676
+ let focusByKey = new Map();
1677
+ let peerTotal = 0;
1678
+ const stdinSeen = new Map();
1679
+ const flashUntil = new Map();
1680
+ const STDIN_FLASH_MS = 1200;
1587
1681
  let es = null; // live-tail subscription closer
1588
1682
  let term = null; // xterm.js Terminal rendering the raw PTY stream
1589
1683
  let fit = null;
@@ -2687,10 +2781,65 @@
2687
2781
  }
2688
2782
  renderPeerSelections();
2689
2783
  }
2784
+ // Fleet-wide presence: unlike pollPresence (only the SELECTED agent's host,
2785
+ // for the terminal overlay), this reads EVERY live source's blackboard so
2786
+ // the left panel can colour any agent a peer is watching. Each viewer
2787
+ // self-reports the pid it's on; we map that back to the source's composite
2788
+ // _key. Best-effort — a source that 404s (older host) contributes nothing.
2789
+ async function pollFleetPresence() {
2790
+ const recs = [];
2791
+ await Promise.all(
2792
+ [...sources.values()].map(async (s) => {
2793
+ if (!s.tx || !s.live) return;
2794
+ let all;
2795
+ try {
2796
+ all = await s.tx.fetchJSON("/api/presence");
2797
+ } catch {
2798
+ return;
2799
+ }
2800
+ if (!Array.isArray(all)) return;
2801
+ const keyOfPid = new Map((s.agents || []).map((e) => [String(e.pid), e._key]));
2802
+ for (const v of all) {
2803
+ if (!v || v.viewer === myViewerId) continue;
2804
+ const key = keyOfPid.get(String(v.agent));
2805
+ // Prefix the viewer id with its source so the same id on two hosts
2806
+ // still counts as two distinct people in the fleet total.
2807
+ if (key) recs.push({ key, viewer: s.id + ":" + v.viewer });
2808
+ }
2809
+ }),
2810
+ );
2811
+ const sum = focusSummary(recs);
2812
+ focusByKey = sum.byKey;
2813
+ peerTotal = sum.total;
2814
+ paintPeersBadge();
2815
+ renderList();
2816
+ }
2817
+ // "N peers" badge beside the agent counter. Yellow when a peer shares the
2818
+ // agent you have open ("same agent"); its tooltip breaks the count down by
2819
+ // agent so you can see who's where. Hidden when you're the only one here.
2820
+ function paintPeersBadge() {
2821
+ const el = $("peers");
2822
+ if (!el) return;
2823
+ if (!peerTotal) {
2824
+ el.hidden = true;
2825
+ el.removeAttribute("title");
2826
+ return;
2827
+ }
2828
+ el.hidden = false;
2829
+ el.textContent = `👤 ${peerTotal} peer${peerTotal === 1 ? "" : "s"}`;
2830
+ el.classList.toggle("here", !!(sel && focusByKey.get(sel)));
2831
+ const lines = [...focusByKey.entries()].map(([k, n]) => {
2832
+ const e = entries.find((x) => x._key === k);
2833
+ const name = e ? e.title || cliLabel(e) || ident(e) || `pid ${e.pid}` : k;
2834
+ return ` ${n} on ${name}`;
2835
+ });
2836
+ el.title = "peers watching agents in this fleet:\n" + lines.join("\n");
2837
+ }
2690
2838
  // 3s heartbeat (< the host's 12s TTL): refresh our presence + read others',
2691
2839
  // and follow the canonical grid size (resize+scale, never reflow) so all
2692
2840
  // viewers stay on one shared canvas and selections line up.
2693
2841
  setInterval(() => {
2842
+ pollFleetPresence(); // fleet-wide peer focus (independent of what's selected)
2694
2843
  if (!sel) return;
2695
2844
  sendPresence();
2696
2845
  pollPresence();
@@ -2701,6 +2850,20 @@
2701
2850
  .then(followAgentSize)
2702
2851
  .catch(() => {});
2703
2852
  }, 3000);
2853
+ // Fast pulse loop: drive the stdin flash on the STABLE list DOM (toggle a
2854
+ // class, no re-render) so a pulse begins/ends promptly regardless of the
2855
+ // list's own render cadence. Cheap — a no-op until something is flashing.
2856
+ setInterval(() => {
2857
+ if (!flashUntil.size) return;
2858
+ const now = Date.now();
2859
+ for (const [k, until] of [...flashUntil.entries()]) if (until <= now) flashUntil.delete(k);
2860
+ const listEl = $("list");
2861
+ if (!listEl) return;
2862
+ for (const el of listEl.querySelectorAll(".row[data-key]")) {
2863
+ const on = (flashUntil.get(el.getAttribute("data-key")) || 0) > now;
2864
+ el.classList.toggle("stdinflash", on);
2865
+ }
2866
+ }, 200);
2704
2867
 
2705
2868
  // Peer-selection overlay. The coordinate math (parseSel / selSegments /
2706
2869
  // hashHue) lives in console-logic.js (unit-tested); here we just measure the
@@ -2819,8 +2982,10 @@
2819
2982
  // Force — escalation a stuck agent can't ignore: a real SIGKILL of its process
2820
2983
  // GROUP, server-side via POST /api/kill. Use when /exit does nothing.
2821
2984
  const GRACEFUL_EXIT = { claude: "/exit", codex: "/exit", gemini: "/quit" };
2822
- async function stopAgent(force) {
2823
- const e = sel ? entries.find((x) => x._key === sel) : null;
2985
+ // `ent` targets a specific agent (the Cmd+K /kill · /restart commands pass the
2986
+ // omnibox anchor); the ⋯-menu buttons omit it and act on the selected agent.
2987
+ async function stopAgent(force, ent) {
2988
+ const e = ent || (sel ? entries.find((x) => x._key === sel) : null);
2824
2989
  if (!e) return;
2825
2990
  const who = `pid ${e.pid} ${cliLabel(e) || ident(e) || ""}`.trim();
2826
2991
  const tx = txFor(e),
@@ -2843,8 +3008,8 @@
2843
3008
  // Restart — stop (if live) + relaunch resuming the session, server-side via
2844
3009
  // POST /api/restart (the `ay restart` flow). The server runs it detached, so
2845
3010
  // it's decoupled from this agent's lifecycle — no prompt typed into the agent.
2846
- async function restartAgent() {
2847
- const e = sel ? entries.find((x) => x._key === sel) : null;
3011
+ async function restartAgent(ent) {
3012
+ const e = ent || (sel ? entries.find((x) => x._key === sel) : null);
2848
3013
  if (!e) return;
2849
3014
  const who = `pid ${e.pid} ${cliLabel(e) || ident(e) || ""}`.trim();
2850
3015
  if (!confirm(`Restart ${who}?\nStops it (if live) and relaunches resuming the session.`))
@@ -3191,6 +3356,14 @@
3191
3356
  // Compact list: one line per agent (dot + cli + title), persisted per device.
3192
3357
  let compactList = localStorage.getItem("ay.compactList") === "1";
3193
3358
 
3359
+ // Fold subagent trees: hide nested (sub)agent rows and roll each root's
3360
+ // hidden descendants into a summary chip. Folded BY DEFAULT (only "0" opts
3361
+ // out) so a busy fleet reads as a flat list of top-level agents at a glance.
3362
+ let foldSubs = localStorage.getItem("ay.foldSubs") !== "0";
3363
+ // Per-render Map(rootEntry → { total, working, lastActive }); rebuilt by
3364
+ // renderList whenever folding is on, consumed by foldChipHtml.
3365
+ let foldSum = null;
3366
+
3194
3367
  // Display sort order, cycled by the sort button and persisted per device.
3195
3368
  // Default "state" (attention-first state, then git busyness). Guard against
3196
3369
  // a stale/garbage stored value so we never start in an unknown mode.
@@ -3234,6 +3407,21 @@
3234
3407
  return `<span class="tasks${allDone ? " done" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
3235
3408
  }
3236
3409
 
3410
+ // Folded-subagent summary chip ("⊞ 3/5") for a root agent whose subagent
3411
+ // tree is collapsed — working/total of its hidden descendants, from the
3412
+ // per-render foldSum map. "" when folding is off or this row has no hidden
3413
+ // subagents. Clicking it expands the trees (see the list click handler).
3414
+ // The tooltip carries the recency the badge face omits ("last active 5s ago").
3415
+ function foldChipHtml(e) {
3416
+ const s = foldSum && foldSum.get(e);
3417
+ if (!s) return "";
3418
+ const rel = s.lastActive != null ? age({ last_active_at: s.lastActive }) : "";
3419
+ const tip =
3420
+ `${s.total} subagent${s.total === 1 ? "" : "s"} folded · ${s.working} working` +
3421
+ (rel ? ` · last active ${rel} ago` : "");
3422
+ return `<span class="subs${s.working ? " active" : ""}" data-fold-chip="1" title="${esc(tip)}">⊞ ${s.working}/${s.total}</span>`;
3423
+ }
3424
+
3237
3425
  // Status-flag chips ("badges") matched server-side against the agent's
3238
3426
  // screen (ts/badges.ts) — e.g. an active /goal loop. One small pill per
3239
3427
  // matched flag; "" when none matched. CSS class is "flag" (not "badge")
@@ -3260,13 +3448,39 @@
3260
3448
  return `<div class="ghead g-${r.kind}">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="gicon">${icon}</span><span class="glabel">${esc(r.label || "")}</span>${ctag}<span class="gkind">${r.kind}</span></div>`;
3261
3449
  }
3262
3450
 
3451
+ // Extra row classes for collaborative presence: a yellow bar when a human
3452
+ // peer is watching this agent, plus the stdin pulse while its input just
3453
+ // advanced. Empty in the solo case, so a single-user list is unchanged.
3454
+ function rowFlags(e) {
3455
+ const peers = focusByKey.get(e._key) || 0;
3456
+ const flash = (flashUntil.get(e._key) || 0) > Date.now();
3457
+ return (peers ? " peerfocus" : "") + (flash ? " stdinflash" : "");
3458
+ }
3459
+ // A yellow chip showing how many peers are watching this agent ("" if none).
3460
+ function peerChipHtml(e) {
3461
+ const n = focusByKey.get(e._key) || 0;
3462
+ return n
3463
+ ? `<span class="peerchip" title="${n} peer${n === 1 ? "" : "s"} watching this agent">👤${n}</span>`
3464
+ : "";
3465
+ }
3466
+
3263
3467
  function renderList() {
3264
3468
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
3265
3469
  // Build the full signalling-server > rooms > peers > agents > subagents
3266
3470
  // tree. Single-node room/peer layers fold away; ≥2 become ├ └ │ branches.
3267
- const rows = layeredRows(orderedEntries(toks));
3268
- const agentRows = rows.filter((r) => r.kind === "agent");
3471
+ const fullRows = layeredRows(orderedEntries(toks));
3472
+ const agentRows = fullRows.filter((r) => r.kind === "agent");
3473
+ // Fold: summarize each root's hidden descendants, then drop subagent rows
3474
+ // (any agent row with a parent agent). The count still reports every
3475
+ // matching agent, so folding never makes the fleet look smaller.
3476
+ foldSum = foldSubs ? foldSummaries(fullRows) : null;
3477
+ const rows = foldSubs
3478
+ ? fullRows.filter((r) => !(r.kind === "agent" && r.parentEntry))
3479
+ : fullRows;
3269
3480
  $("count").textContent = `${agentRows.length} / ${entries.length} agents`;
3481
+ paintPeersBadge(); // keep the peers badge in step with sel / the entry list
3482
+ $("foldbtn").textContent = foldSubs ? "⊞ subs" : "⊟ subs";
3483
+ $("foldbtn").classList.toggle("on", !foldSubs);
3270
3484
  $("viewbtn").classList.toggle("on", compactList);
3271
3485
  // Show the active sort mode on the button so it's self-documenting; the
3272
3486
  // default ("state") is also highlighted to hint it's the implicit order.
@@ -3287,15 +3501,17 @@
3287
3501
  const t = e.title || e.prompt || "";
3288
3502
  const id = compactIdent(e, ctx, 3, r.parentEntry);
3289
3503
  const cli = cliLabel(e);
3290
- return `<div class="row crow ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
3504
+ return `<div class="row crow ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">
3291
3505
  ${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}
3292
3506
  <span class="dot ${esc(e.status)}"></span>
3293
3507
  ${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
3294
3508
  ${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
3295
3509
  <span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
3510
+ ${foldChipHtml(e)}
3296
3511
  ${taskChipHtml(e)}
3297
3512
  ${badgeChipsHtml(e)}
3298
3513
  ${gitChipHtml(e)}
3514
+ ${peerChipHtml(e)}
3299
3515
  <span class="age">${age(e)}</span></div>`;
3300
3516
  })
3301
3517
  .join("") || `<div class="empty">no match</div>`;
@@ -3315,13 +3531,15 @@
3315
3531
  `<span class="rtag" data-k="${k}"><span style="opacity:.55">${k}:</span>${esc(v)}</span>`,
3316
3532
  )
3317
3533
  .join("");
3318
- return `<div class="row ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
3534
+ return `<div class="row ${e._key === sel ? "sel" : ""}${rowFlags(e)}" data-key="${esc(e._key)}">
3319
3535
  <div class="r1">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="dot ${esc(e.status)}"></span>
3320
3536
  <span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
3321
3537
  <span class="badge">pid ${e.pid}</span>
3538
+ ${foldChipHtml(e)}
3322
3539
  ${taskChipHtml(e)}
3323
3540
  ${badgeChipsHtml(e)}
3324
3541
  ${gitChipHtml(e)}
3542
+ ${peerChipHtml(e)}
3325
3543
  <span class="age">${age(e)}</span></div>
3326
3544
  ${e.title ? `<div class="rowtitle" title="${esc(e.title)}">${esc(e.title)}</div>` : ""}
3327
3545
  ${
@@ -3383,6 +3601,13 @@
3383
3601
  function mergeRender() {
3384
3602
  const srcs = [...sources.values()];
3385
3603
  entries = srcs.flatMap((s) => s.agents || []);
3604
+ // Spot agents whose stdin just advanced (last_stdin_at bumped since we
3605
+ // last saw them — someone typed, or `ay send` pushed input) and arm the
3606
+ // pulse. Runs before renderList so the flashing rows render pulsing; the
3607
+ // fast loop above stops them when the window lapses. Streamed deltas carry
3608
+ // the new mtime, so a silent `ay send` still trips the flash within ~1s.
3609
+ const until = Date.now() + STDIN_FLASH_MS;
3610
+ for (const k of advanceStdinFlashes(entries, stdinSeen)) flashUntil.set(k, until);
3386
3611
  // Badge: total agents + how many rooms are live. Red only when nothing
3387
3612
  // at all is reachable (no source answered).
3388
3613
  const roomSrcs = srcs.filter((s) => s.id !== LOCAL);
@@ -3680,6 +3905,14 @@
3680
3905
  }
3681
3906
 
3682
3907
  $("list").addEventListener("click", (ev) => {
3908
+ // Clicking a folded-subagent chip expands the trees instead of selecting
3909
+ // the row, so the hidden children it summarizes become reachable.
3910
+ if (ev.target.closest("[data-fold-chip]")) {
3911
+ foldSubs = false;
3912
+ localStorage.setItem("ay.foldSubs", "0");
3913
+ renderList();
3914
+ return;
3915
+ }
3683
3916
  const row = ev.target.closest(".row");
3684
3917
  if (row) select(row.dataset.key);
3685
3918
  });
@@ -3739,6 +3972,12 @@
3739
3972
  localStorage.setItem("ay.compactList", compactList ? "1" : "0");
3740
3973
  renderList();
3741
3974
  });
3975
+ // Fold / unfold every subagent tree (global toggle, persisted per device).
3976
+ $("foldbtn").addEventListener("click", () => {
3977
+ foldSubs = !foldSubs;
3978
+ localStorage.setItem("ay.foldSubs", foldSubs ? "1" : "0");
3979
+ renderList();
3980
+ });
3742
3981
  // Cycle the sort order: state → created → identity → state.
3743
3982
  $("sortbtn").addEventListener("click", () => {
3744
3983
  sortMode = SORT_MODES[(SORT_MODES.indexOf(sortMode) + 1) % SORT_MODES.length];
@@ -3781,9 +4020,10 @@
3781
4020
  // renders. Clamps at the ends, scrolls the row into view.
3782
4021
  function stepSelection(dir) {
3783
4022
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
3784
- // Must match renderList's order; step only over agent rows (skip headers).
4023
+ // Must match renderList's order; step only over agent rows (skip headers),
4024
+ // and skip folded-away subagents so nav lands only on visible rows.
3785
4025
  const shown = layeredRows(orderedEntries(toks))
3786
- .filter((r) => r.kind === "agent")
4026
+ .filter((r) => r.kind === "agent" && !(foldSubs && r.parentEntry))
3787
4027
  .map((r) => r.entry);
3788
4028
  if (!shown.length) return;
3789
4029
  const cur = shown.findIndex((e) => e._key === sel);
@@ -3889,6 +4129,66 @@
3889
4129
  return cur && cur.cwd && cur.git && cur.git.branch ? cur : null;
3890
4130
  }
3891
4131
 
4132
+ // Slash commands that act on the OPEN agent (the omnibox anchor), mirroring the
4133
+ // ⋯-menu recovery buttons. Typing "/" in the omnibox lists them; "/k" / "/r"
4134
+ // narrows. Each runs the very same handler the dropdown button does, so the
4135
+ // confirm prompt and server call are identical — /kill = Force-kill (SIGKILL),
4136
+ // /restart = Restart (resume). "/fork [topic]" is also offered here (built in
4137
+ // omniCommandRows, not this list, since it spawns rather than acts in place).
4138
+ const OMNI_COMMANDS = [
4139
+ {
4140
+ cmd: "/restart",
4141
+ icon: "↻",
4142
+ title: "Restart (resume)",
4143
+ desc: "stops it (if live) and relaunches resuming the session",
4144
+ run: (e) => restartAgent(e),
4145
+ },
4146
+ {
4147
+ cmd: "/kill",
4148
+ icon: "✕",
4149
+ title: "Force-kill (SIGKILL)",
4150
+ desc: "terminates the process immediately",
4151
+ danger: true,
4152
+ run: (e) => stopAgent(true, e),
4153
+ },
4154
+ ];
4155
+ // Build the command rows for a "/…"-prefixed query, targeting the anchor agent.
4156
+ // Empty when nothing is open (a command with no agent to act on is a no-op) so
4157
+ // the omnibox falls through to its normal search/spawn behaviour.
4158
+ function omniCommandRows(q) {
4159
+ const target = omniAnchor();
4160
+ if (!target) return [];
4161
+ // Match on the FIRST token so a command can carry args after it
4162
+ // (e.g. "/fork fix the auth bug" → cmd "fork", topic "fix the auth bug").
4163
+ const parts = q.slice(1).trim().split(/\s+/).filter(Boolean);
4164
+ const cmdTerm = (parts[0] || "").toLowerCase(); // command token after "/"
4165
+ const rows = OMNI_COMMANDS.filter((c) => c.cmd.slice(1).startsWith(cmdTerm)).map((c) => ({
4166
+ kind: "cmd",
4167
+ cmd: c,
4168
+ target,
4169
+ }));
4170
+ // "/fork [topic]" — a slash alias for the fork spawn row so fork is reachable
4171
+ // straight from the command palette (⌘K → "/fork"), not only via the row that
4172
+ // auto-appends to a search. It's a spawn (kind:"fork"), not a kind:"cmd" that
4173
+ // acts on an existing agent, so it's built here rather than in OMNI_COMMANDS —
4174
+ // omniActivate's fork branch (branch prompt → /api/spawn {fork}) handles it.
4175
+ // Needs a real fork source (the open agent with a live git branch); when the
4176
+ // anchor has none, forking is meaningless so the row is omitted.
4177
+ if ("fork".startsWith(cmdTerm)) {
4178
+ const src = omniForkSource();
4179
+ if (src)
4180
+ rows.push({
4181
+ kind: "fork",
4182
+ branch: forkSlug(parts.slice(1).join(" "), src.git.branch),
4183
+ srcBranch: src.git.branch,
4184
+ fromCwd: src.cwd,
4185
+ cli: src.cli,
4186
+ room: src._room,
4187
+ });
4188
+ }
4189
+ return rows;
4190
+ }
4191
+
3892
4192
  // Which agent the highlighted candidate should preview in the background: an
3893
4193
  // agent row → that agent; the spawn row / an empty list → the launch-time
3894
4194
  // agent (so the backdrop shows the spawn target's cwd).
@@ -3975,6 +4275,16 @@
3975
4275
  <div class="omni-sub">⏎ to choose a working dir &nbsp;·&nbsp; prompt: ${esc(q || "(empty)")}</div>
3976
4276
  </div></div>`;
3977
4277
  }
4278
+ if (r.kind === "cmd") {
4279
+ const t = r.target;
4280
+ const dz = r.cmd.danger ? " danger" : "";
4281
+ return `<div class="omni-row omni-spawn${dz}${sc}" data-i="${i}">
4282
+ <span class="dot ${esc(t.status)}"></span>
4283
+ <div class="omni-main">
4284
+ <div class="omni-title">${r.cmd.icon} ${esc(r.cmd.title)} <span style="opacity:.55">${esc(r.cmd.cmd)}</span></div>
4285
+ <div class="omni-sub">pid ${esc(String(t.pid))} ${esc(cliLabel(t) || ident(t) || "")} &nbsp;·&nbsp; ${esc(r.cmd.desc)}</div>
4286
+ </div></div>`;
4287
+ }
3978
4288
  const e = r.entry;
3979
4289
  const snip = r.snippet ? `<div class="omni-snip">…${r.snippet}…</div>` : "";
3980
4290
  return `<div class="omni-row${sc}" data-i="${i}">
@@ -3993,6 +4303,18 @@
3993
4303
 
3994
4304
  function runOmni() {
3995
4305
  const q = $("omni-input").value.trim();
4306
+ // A "/…" query is a slash command against the open agent, not a search —
4307
+ // show the matching command rows and skip the agent/spawn/tail machinery.
4308
+ if (q.startsWith("/")) {
4309
+ const cmds = omniCommandRows(q);
4310
+ if (cmds.length) {
4311
+ if (omniTailTimer) clearTimeout(omniTailTimer);
4312
+ omniRows = cmds;
4313
+ omniIdx = 0;
4314
+ renderOmni();
4315
+ return;
4316
+ }
4317
+ }
3996
4318
  const toks = q.split(/\s+/).filter(Boolean);
3997
4319
  // Tier 1 — instant title/identity match, ranked title-first then by recency.
3998
4320
  const hits = entries
@@ -4085,6 +4407,13 @@
4085
4407
  const q = $("omni-input").value.trim();
4086
4408
  const row = omniRows[omniIdx];
4087
4409
  const anchor = omniAnchor();
4410
+ // A slash command runs its ⋯-menu handler against the pinned target. Handled
4411
+ // before the spawn fallthrough so ⌘⏎ on a "/kill" row can't spawn instead.
4412
+ if (row && row.kind === "cmd") {
4413
+ closeOmni(false);
4414
+ await row.cmd.run(row.target);
4415
+ return;
4416
+ }
4088
4417
  // ⌘⏎ always = spawn-here (the fast default), regardless of the selected row.
4089
4418
  if (forceSpawn || (row && row.kind === "spawn")) {
4090
4419
  closeOmni(false); // committing — keep the bg as-is, spawnAndSelect takes over
@@ -214,7 +214,7 @@
214
214
  spawn agents from anywhere.
215
215
  </p>
216
216
  <div class="cta">
217
- <a class="btn" href="/w/">Open the console →</a>
217
+ <a class="btn" href="/w/" autofocus>Open the console →</a>
218
218
  <a class="btn ghost" href="https://github.com/snomiao/agent-yes">Star on GitHub</a>
219
219
  </div>
220
220
  <div class="install">
@@ -273,5 +273,15 @@ powershell -c "irm https://agent-yes.com/setup.ps1 | iex"</code></pre>
273
273
  </div>
274
274
  </footer>
275
275
  </div>
276
+ <script>
277
+ // Focus the "Open the console" CTA on load so keyboard/Enter users land
278
+ // straight on it. `autofocus` on <a> isn't honored everywhere, so back it
279
+ // up here — but skip on touch devices where focusing scroll-jumps the page.
280
+ (function () {
281
+ if (matchMedia("(hover: none)").matches) return;
282
+ var cta = document.querySelector(".hero .cta a.btn");
283
+ if (cta) cta.focus({ preventScroll: true });
284
+ })();
285
+ </script>
276
286
  </body>
277
287
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.169.1",
3
+ "version": "1.171.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -18,11 +18,15 @@ import path from "path";
18
18
  const repoRoot = path.resolve(import.meta.dirname, "..");
19
19
  const cargoHome = process.env.CARGO_HOME || path.join(os.homedir(), ".cargo");
20
20
 
21
- // Both binaries cargo touches: the build output and the installed copy.
22
- const targets = [
23
- path.join(repoRoot, "rs", "target", "release", "agent-yes.exe"),
24
- path.join(cargoHome, "bin", "agent-yes.exe"),
25
- ];
21
+ // Every binary cargo touches: the build output and the installed copy, for
22
+ // each [[bin]] in the package. Any of these can be locked by a live process,
23
+ // and cargo install refuses to overwrite an existing installed binary (even
24
+ // with --force it can't delete a locked one), so each must be moved aside.
25
+ const exeNames = ["agent-yes.exe", "ay-spawn-hidden.exe"];
26
+ const targets = exeNames.flatMap((exe) => [
27
+ path.join(repoRoot, "rs", "target", "release", exe),
28
+ path.join(cargoHome, "bin", exe),
29
+ ]);
26
30
 
27
31
  function moveLockedBinariesAside(): Array<{ original: string; aside: string }> {
28
32
  const moved: Array<{ original: string; aside: string }> = [];