agent-yes 1.108.1 → 1.110.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.
@@ -0,0 +1,8 @@
1
+ import "./ts--IvDnRaR.js";
2
+ import "./logger-B9h0djqx.js";
3
+ import "./versionChecker-DSdFfx6l.js";
4
+ import "./pidStore-DBjlqzo8.js";
5
+ import "./globalPidIndex-yVd3mbsV.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dh1CAhi9.js";
7
+
8
+ export { SUPPORTED_CLIS };
@@ -1,8 +1,8 @@
1
- import { t as CLIS_CONFIG } from "./ts-DQiKBC0f.js";
1
+ import { t as CLIS_CONFIG } from "./ts--IvDnRaR.js";
2
2
 
3
3
  //#region ts/SUPPORTED_CLIS.ts
4
4
  const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
5
5
 
6
6
  //#endregion
7
7
  export { SUPPORTED_CLIS as t };
8
- //# sourceMappingURL=SUPPORTED_CLIS-CcreSTgo.js.map
8
+ //# sourceMappingURL=SUPPORTED_CLIS-Dh1CAhi9.js.map
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { n as logger } from "./logger-B9h0djqx.js";
3
- import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-BQULRcxP.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-DSdFfx6l.js";
4
4
  import { argv } from "process";
5
5
  import { execFileSync, spawn } from "child_process";
6
6
  import ms from "ms";
@@ -482,7 +482,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
482
482
  {
483
483
  const rawArg = process.argv[2];
484
484
  const isHelpFlag = rawArg === "-h" || rawArg === "--help";
485
- const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-BIz8LzhF.js");
485
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-D4eVn3eY.js");
486
486
  if (isHelpFlag && process.argv.length === 3) {
487
487
  cmdHelp();
488
488
  process.exit(0);
@@ -515,7 +515,7 @@ if (config.useRust) {
515
515
  }
516
516
  }
517
517
  if (rustBinary) {
518
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-atYBMnHG.js");
518
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-CGb1oNZd.js");
519
519
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
520
520
  if (config.verbose) {
521
521
  console.log(`[rust] Using binary: ${rustBinary}`);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DQiKBC0f.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts--IvDnRaR.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BQULRcxP.js";
3
+ import "./versionChecker-DSdFfx6l.js";
4
4
  import "./pidStore-DBjlqzo8.js";
5
5
  import "./globalPidIndex-yVd3mbsV.js";
6
6
 
@@ -1,11 +1,11 @@
1
- import "./ts-DQiKBC0f.js";
1
+ import "./ts--IvDnRaR.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BQULRcxP.js";
3
+ import "./versionChecker-DSdFfx6l.js";
4
4
  import "./pidStore-DBjlqzo8.js";
5
5
  import "./globalPidIndex-yVd3mbsV.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CcreSTgo.js";
6
+ import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dh1CAhi9.js";
7
7
  import "./remotes-C3xPRtfg.js";
8
- import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-DHPQVuWd.js";
8
+ import { c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, m as snapshotStatus, r as controlCodeFromName, u as readNotes } from "./subcommands-D3b7vKgY.js";
9
9
  import yargs from "yargs";
10
10
  import { mkdir, open, readFile, writeFile } from "fs/promises";
11
11
  import { homedir } from "os";
@@ -552,4 +552,4 @@ Options:
552
552
 
553
553
  //#endregion
554
554
  export { cmdServe };
555
- //# sourceMappingURL=serve-BrT33v_v.js.map
555
+ //# sourceMappingURL=serve-BmM1r9No.js.map
@@ -163,7 +163,7 @@ async function runSubcommand(argv) {
163
163
  case "restart": return await cmdRestart(rest);
164
164
  case "note": return await cmdNote(rest);
165
165
  case "serve": {
166
- const { cmdServe } = await import("./serve-BrT33v_v.js");
166
+ const { cmdServe } = await import("./serve-BmM1r9No.js");
167
167
  return cmdServe(rest);
168
168
  }
169
169
  case "setup": {
@@ -1595,4 +1595,4 @@ async function cmdStatus(rest) {
1595
1595
 
1596
1596
  //#endregion
1597
1597
  export { finalizedLines as a, listRecords as c, renderRawLog as d, resolveOne as f, writeToIpc as g, stopTipForCli as h, cursorAbs as i, matchKeyword as l, snapshotStatus as m, cmdHelp as n, isPidAlive as o, runSubcommand as p, controlCodeFromName as r, isSubcommand as s, GRACEFUL_EXIT_COMMANDS as t, readNotes as u };
1598
- //# sourceMappingURL=subcommands-DHPQVuWd.js.map
1598
+ //# sourceMappingURL=subcommands-D3b7vKgY.js.map
@@ -1,6 +1,6 @@
1
1
  import "./logger-B9h0djqx.js";
2
2
  import "./globalPidIndex-yVd3mbsV.js";
3
3
  import "./remotes-C3xPRtfg.js";
4
- import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-DHPQVuWd.js";
4
+ import { a as finalizedLines, c as listRecords, d as renderRawLog, f as resolveOne, g as writeToIpc, h as stopTipForCli, i as cursorAbs, l as matchKeyword, m as snapshotStatus, n as cmdHelp, o as isPidAlive, p as runSubcommand, r as controlCodeFromName, s as isSubcommand, t as GRACEFUL_EXIT_COMMANDS, u as readNotes } from "./subcommands-D3b7vKgY.js";
5
5
 
6
6
  export { cmdHelp, isSubcommand, runSubcommand };
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-BQULRcxP.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-DSdFfx6l.js";
3
3
  import { n as agentYesHome, t as PidStore } from "./pidStore-DBjlqzo8.js";
4
4
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
5
5
  import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
@@ -1714,4 +1714,4 @@ function sleep(ms) {
1714
1714
 
1715
1715
  //#endregion
1716
1716
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1717
- //# sourceMappingURL=ts-DQiKBC0f.js.map
1717
+ //# sourceMappingURL=ts--IvDnRaR.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.108.1";
10
+ var version = "1.110.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -221,4 +221,4 @@ async function displayVersion() {
221
221
 
222
222
  //#endregion
223
223
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
224
- //# sourceMappingURL=versionChecker-BQULRcxP.js.map
224
+ //# sourceMappingURL=versionChecker-DSdFfx6l.js.map
@@ -28,6 +28,80 @@ export function ident(e, cap) {
28
28
  return `${c(rb.repo)}/${c(rb.branch)}`;
29
29
  }
30
30
 
31
+ // ---- device-aware identity (multi-room) -----------------------------------
32
+ // When several machines' agents are aggregated into one list, an agent's full
33
+ // identity is user@host:owner/repo/branch. The device (user@host) comes from the
34
+ // codehost peer label on `_host`; the path (owner/repo/branch) from the cwd.
35
+
36
+ // Split a codehost device label into { user, host }. "sno@taka" → both parts;
37
+ // "taka" (no @) → host only; "" / missing → both empty (a local/unknown device).
38
+ export function deviceParts(host) {
39
+ if (!host) return { user: "", host: "" };
40
+ const at = String(host).indexOf("@");
41
+ return at >= 0
42
+ ? { user: String(host).slice(0, at), host: String(host).slice(at + 1) }
43
+ : { user: "", host: String(host) };
44
+ }
45
+
46
+ // The five identity fields, in display order, for one agent.
47
+ export function identFields(e) {
48
+ 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 };
51
+ }
52
+
53
+ const IDENT_ORDER = ["user", "host", "owner", "repo", "branch"];
54
+
55
+ // Precompute, over the whole shown list: which fields are uniform (identical for
56
+ // every agent — so they can be omitted) and whether any device info exists at
57
+ // all (if not, we render the legacy path-only identity, no user@host: prefix).
58
+ export function identContext(entries) {
59
+ const fields = entries.map(identFields);
60
+ const uniform = {};
61
+ for (const f of IDENT_ORDER) uniform[f] = new Set(fields.map((x) => x[f])).size <= 1;
62
+ const anyDevice = fields.some((x) => x.user || x.host);
63
+ return { uniform, anyDevice };
64
+ }
65
+
66
+ // Build an agent's compact identity against a precomputed identContext. Each
67
+ // field is clipped to `cap` chars (compact one-liner) and BLANKED when uniform
68
+ // across the list — but the separators (@ : / /) are kept so the string stays
69
+ // machine-parseable: e.g. all on one device → "@:age/mai", a mixed-device list →
70
+ // "sno@tak:age/mai". A purely local list (no devices anywhere) falls back to the
71
+ // legacy "own/rep/bra" with no device prefix.
72
+ export function compactIdent(e, ctx, cap = 3) {
73
+ const m = identFields(e);
74
+ 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")}`;
77
+ return ctx.anyDevice ? `${v("user")}@${v("host")}:${path}` : path;
78
+ }
79
+
80
+ // The full, uncapped identity for a hover title — every field shown, device
81
+ // prefix only when this agent actually has device info.
82
+ export function fullIdent(e) {
83
+ const m = identFields(e);
84
+ const path = `${m.owner}/${m.repo}/${m.branch}`;
85
+ return m.user || m.host ? `${m.user}@${m.host}:${path}` : path;
86
+ }
87
+
88
+ // True when a compact identity carries at least one real character (not just
89
+ // separators) — used to decide whether to render the identity span at all.
90
+ export function hasIdent(s) {
91
+ return /[^@:/]/.test(s || "");
92
+ }
93
+
94
+ // Count of distinct devices (user@host) present in the list. >1 means "not
95
+ // alone" → worth showing the device tag in the detailed view.
96
+ export function deviceCount(entries) {
97
+ const set = new Set();
98
+ for (const e of entries) {
99
+ const { user, host } = deviceParts(e._host);
100
+ if (user || host) set.add(user + "@" + host);
101
+ }
102
+ return set.size;
103
+ }
104
+
31
105
  // Derive codehost-style mnemonic tags from a cwd like .../ws/<owner>/<repo>/tree/<wt>.
32
106
  export function tagsFor(e) {
33
107
  const t = [];
@@ -63,6 +137,15 @@ export function matches(e, toks) {
63
137
  if (ci > 0) {
64
138
  const k = tok.slice(0, ci),
65
139
  v = tok.slice(ci + 1);
140
+ // room: / device: filter the aggregation by source and machine.
141
+ if (k === "room")
142
+ return String(e._room || "")
143
+ .toLowerCase()
144
+ .includes(v);
145
+ if (k === "device" || k === "dev")
146
+ return String(e._host || "")
147
+ .toLowerCase()
148
+ .includes(v);
66
149
  return tagsFor(e).some(([tk, tv]) => tk === k && tv.toLowerCase().includes(v));
67
150
  }
68
151
  return hay.toLowerCase().includes(tok);
package/lab/ui/index.html CHANGED
@@ -7,6 +7,8 @@
7
7
  <style>
8
8
  /* Palette borrowed from codehost (GitHub-dark) so the two feel like one system. */
9
9
  :root {
10
+ /* Adapt native form controls / scrollbars to the OS theme too. */
11
+ color-scheme: light dark;
10
12
  --bg: #0d1117;
11
13
  --panel: #161b22;
12
14
  --panel2: #1c2430;
@@ -22,6 +24,28 @@
22
24
  --red: #f85149;
23
25
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
24
26
  }
27
+ /* Light theme — GitHub-light, the daylight counterpart of the dark palette
28
+ above. prefers-color-scheme is re-evaluated by the browser the moment the
29
+ OS flips, so every var() consumer recolors in real time, no JS, no reload.
30
+ (The xterm canvas is the one exception — it's repainted from JS, see
31
+ termTheme() below.) */
32
+ @media (prefers-color-scheme: light) {
33
+ :root {
34
+ --bg: #ffffff;
35
+ --panel: #f6f8fa;
36
+ --panel2: #eaeef2;
37
+ --line: #d1d9e0;
38
+ --line2: #d8dee4;
39
+ --fg: #1f2328;
40
+ --muted: #59636e;
41
+ --accent: #0969da;
42
+ --green: #1a7f37;
43
+ --amber: #9a6700;
44
+ --purple: #8250df;
45
+ --pink: #bf3989;
46
+ --red: #cf222e;
47
+ }
48
+ }
25
49
  * {
26
50
  box-sizing: border-box;
27
51
  }
@@ -186,6 +210,19 @@
186
210
  .rooms .ritem.cur {
187
211
  box-shadow: inset 2px 0 0 var(--green);
188
212
  }
213
+ .rooms .rstat {
214
+ font-family: var(--mono);
215
+ font-size: 10.5px;
216
+ flex: none;
217
+ min-width: 18px;
218
+ }
219
+ .rooms .rstat.on {
220
+ color: var(--green);
221
+ }
222
+ .rooms .rstat.off {
223
+ color: var(--muted);
224
+ opacity: 0.6;
225
+ }
189
226
  .rooms .rname {
190
227
  font-family: var(--mono);
191
228
  color: var(--accent);
@@ -604,7 +641,7 @@
604
641
  min-height: 0;
605
642
  overflow: hidden;
606
643
  padding: 8px 10px;
607
- background: #0d1117;
644
+ background: var(--bg);
608
645
  }
609
646
  .log .xterm {
610
647
  height: 100%;
@@ -787,15 +824,61 @@
787
824
  age,
788
825
  matches,
789
826
  nextIndex,
827
+ identContext,
828
+ compactIdent,
829
+ fullIdent,
830
+ hasIdent,
831
+ deviceCount,
790
832
  } from "./console-logic.js";
791
833
 
792
834
  let entries = [];
793
- let wantRemote = false; // a room hash/cached room is in play — don't poll the cloud origin's (nonexistent) /api before the tunnel opens
794
- let sel = null; // selected keyword (pid as string)
835
+ let sel = null; // selected agent's composite key (room#pid)
795
836
  let es = null; // live-tail subscription closer
796
837
  let term = null; // xterm.js Terminal rendering the raw PTY stream
797
838
  let fit = null;
798
839
 
840
+ // xterm paints to a <canvas>, so unlike the CSS var() consumers it can't
841
+ // ride prefers-color-scheme on its own — its theme is a JS object. Mirror
842
+ // the OS preference here and re-apply it live when the system flips, so the
843
+ // terminal recolors in lockstep with the rest of the UI.
844
+ const prefersLight = window.matchMedia("(prefers-color-scheme: light)");
845
+ const termTheme = () =>
846
+ prefersLight.matches
847
+ ? {
848
+ // GitHub-light terminal palette. xterm's default ANSI slots are
849
+ // tuned for a dark background, so setting only bg/fg/cursor leaves
850
+ // white & bright-white SGR text (37/97) — common in CLI output —
851
+ // near-invisible on #ffffff. Remap the white slots to dark grays
852
+ // and darken the rest so every ANSI color stays legible on light.
853
+ background: "#ffffff",
854
+ foreground: "#1f2328",
855
+ cursor: "#1f2328",
856
+ selectionBackground: "#b6d6ff",
857
+ black: "#24292e",
858
+ red: "#cf222e",
859
+ green: "#116329",
860
+ yellow: "#4d2d00",
861
+ blue: "#0969da",
862
+ magenta: "#8250df",
863
+ cyan: "#1b7c83",
864
+ white: "#6e7781",
865
+ brightBlack: "#57606a",
866
+ brightRed: "#a40e26",
867
+ brightGreen: "#1a7f37",
868
+ brightYellow: "#633c01",
869
+ brightBlue: "#218bff",
870
+ brightMagenta: "#a475f9",
871
+ brightCyan: "#3192aa",
872
+ brightWhite: "#1f2328",
873
+ }
874
+ : { background: "#0d1117", foreground: "#c9d1d9", cursor: "#0d1117" };
875
+ // Exposed so the headless theme e2e can assert the light ANSI palette
876
+ // keeps the white slots dark (legible on a white terminal background).
877
+ window.__termTheme = termTheme;
878
+ prefersLight.addEventListener("change", () => {
879
+ if (term) term.options.theme = termTheme();
880
+ });
881
+
799
882
  const $ = (id) => document.getElementById(id);
800
883
  const esc = (s) =>
801
884
  String(s ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
@@ -1073,20 +1156,14 @@
1073
1156
  : path;
1074
1157
  };
1075
1158
 
1076
- const Conn = {
1077
- rtc: null, // RTCClient when remote (ay share), null when local
1078
- ch: null, // CodehostClient when viewing a codehost room
1159
+ // ---- transports: a uniform { fetchJSON, post, subscribe } over each wire ----
1160
+ // local (same-origin ay serve), an ay-share RTCClient, or a codehost room.
1161
+ // CodehostClient already exposes this shape, so it's used as a tx directly.
1162
+ const localTx = {
1079
1163
  async fetchJSON(path) {
1080
- if (this.ch) return this.ch.fetchJSON(path);
1081
- if (this.rtc) return JSON.parse((await this.rtc.req("GET", path)).text);
1082
1164
  return (await fetch(withTok(path))).json();
1083
1165
  },
1084
1166
  async post(path, bodyObj) {
1085
- if (this.ch) return this.ch.post(path, bodyObj);
1086
- if (this.rtc) {
1087
- const r = await this.rtc.req("POST", path, JSON.stringify(bodyObj));
1088
- return { ok: r.status >= 200 && r.status < 300, text: r.text };
1089
- }
1090
1167
  const r = await fetch(withTok(path), {
1091
1168
  method: "POST",
1092
1169
  headers: { "Content-Type": "application/json" },
@@ -1094,13 +1171,27 @@
1094
1171
  });
1095
1172
  return { ok: r.ok, text: await r.text() };
1096
1173
  },
1097
- // onText gets each parsed SSE data payload (same shape as the local EventSource path).
1098
1174
  subscribe(path, onText, onOpen, onError) {
1099
- if (this.ch) return this.ch.subscribe(path, onText, onOpen, onError);
1100
- if (this.rtc) {
1175
+ const ev = new EventSource(withTok(path));
1176
+ ev.onopen = () => onOpen && onOpen();
1177
+ ev.onmessage = (e) => onText(JSON.parse(e.data));
1178
+ ev.onerror = () => onError && onError();
1179
+ return () => ev.close();
1180
+ },
1181
+ };
1182
+ function rtcTx(rtc) {
1183
+ return {
1184
+ async fetchJSON(path) {
1185
+ return JSON.parse((await rtc.req("GET", path)).text);
1186
+ },
1187
+ async post(path, bodyObj) {
1188
+ const r = await rtc.req("POST", path, JSON.stringify(bodyObj));
1189
+ return { ok: r.status >= 200 && r.status < 300, text: r.text };
1190
+ },
1191
+ subscribe(path, onText, onOpen, onError) {
1101
1192
  onOpen && onOpen();
1102
1193
  let buf = "";
1103
- return this.rtc.subscribe(path, (raw) => {
1194
+ return rtc.subscribe(path, (raw) => {
1104
1195
  buf += raw;
1105
1196
  let i;
1106
1197
  while ((i = buf.indexOf("\n\n")) >= 0) {
@@ -1114,14 +1205,66 @@
1114
1205
  }
1115
1206
  }
1116
1207
  });
1117
- }
1118
- const ev = new EventSource(withTok(path));
1119
- ev.onopen = () => onOpen && onOpen();
1120
- ev.onmessage = (e) => onText(JSON.parse(e.data));
1121
- ev.onerror = () => onError && onError();
1122
- return () => ev.close();
1123
- },
1124
- };
1208
+ },
1209
+ };
1210
+ }
1211
+
1212
+ // ---- fleet: local + every saved room, all connected at once ------------
1213
+ // Each source contributes its agents to one merged list (tagged with the
1214
+ // owning source on `_room`/`_key`); per-agent ops route back via srcFor/txFor.
1215
+ // Live counts in the rooms panel come from each source's serverCount/devices.
1216
+ const LOCAL = "local";
1217
+ const sources = new Map(); // id -> { id, host, kind, tx, client, live, devices, serverCount }
1218
+ const srcFor = (e) => (e && sources.get(e._room)) || sources.get(LOCAL) || null;
1219
+ const txFor = (e) => srcFor(e)?.tx || localTx;
1220
+
1221
+ // The local source is only worth polling when this page is actually backed
1222
+ // by an ay serve: localhost, or served by `ay serve --http` (which leaves a
1223
+ // token), or when there are no rooms to fall back on. On the public origin
1224
+ // with rooms, skip it so we don't hammer a 404 every poll.
1225
+ function ensureLocalSource() {
1226
+ const isLocalhost = ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
1227
+ const hasToken = !!localStorage.getItem("ay.localToken");
1228
+ const enabled = isLocalhost || hasToken || Object.keys(loadRooms()).length === 0;
1229
+ if (enabled && !sources.has(LOCAL)) {
1230
+ sources.set(LOCAL, {
1231
+ id: LOCAL,
1232
+ host: "local",
1233
+ kind: "local",
1234
+ tx: localTx,
1235
+ client: null,
1236
+ live: false,
1237
+ tried: true, // no connect phase — polled directly
1238
+ devices: new Set(),
1239
+ serverCount: 0,
1240
+ });
1241
+ } else if (!enabled) {
1242
+ sources.delete(LOCAL);
1243
+ }
1244
+ }
1245
+
1246
+ // Pull one source's agent list, tagging each row with its origin + a
1247
+ // composite key (pids can collide across rooms). Updates the source's live
1248
+ // flag, device set, and server count for the rooms panel.
1249
+ async function listSource(s) {
1250
+ try {
1251
+ const arr = await s.tx.fetchJSON("/api/ls?all=1");
1252
+ s.live = true;
1253
+ s.devices = new Set();
1254
+ const out = (Array.isArray(arr) ? arr : []).map((e) => {
1255
+ const host = e._host || "";
1256
+ if (host) s.devices.add(host);
1257
+ return { ...e, _room: s.id, _key: s.id + "#" + e.pid, _host: host };
1258
+ });
1259
+ s.serverCount =
1260
+ s.kind === "ch" ? s.client?.hosts().length || 0 : s.devices.size || (s.live ? 1 : 0);
1261
+ return out;
1262
+ } catch {
1263
+ s.live = false;
1264
+ s.serverCount = 0;
1265
+ return [];
1266
+ }
1267
+ }
1125
1268
 
1126
1269
  // Compact list: one line per agent (dot + cli + title), persisted per device.
1127
1270
  let compactList = localStorage.getItem("ay.compactList") === "1";
@@ -1131,16 +1274,21 @@
1131
1274
  const shown = entries.filter((e) => matches(e, toks));
1132
1275
  $("count").textContent = `${shown.length} / ${entries.length} agents`;
1133
1276
  $("viewbtn").classList.toggle("on", compactList);
1277
+ // identContext blanks any field uniform across the shown list (so a
1278
+ // single-device fleet shows no device); multiDevice gates the detailed
1279
+ // host tag so it only appears when machines are actually mixed.
1280
+ const ctx = identContext(shown);
1281
+ const multiDevice = deviceCount(shown) > 1;
1134
1282
  if (compactList) {
1135
1283
  $("list").innerHTML =
1136
1284
  shown
1137
1285
  .map((e) => {
1138
1286
  const t = e.title || e.prompt || "";
1139
- const id = ident(e, true);
1287
+ const id = compactIdent(e, ctx);
1140
1288
  const cli = cliLabel(e);
1141
- return `<div class="row crow ${String(e.pid) === sel ? "sel" : ""}" data-pid="${e.pid}">
1289
+ return `<div class="row crow ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
1142
1290
  <span class="dot ${esc(e.status)}"></span>
1143
- ${id ? `<span class="cident" title="${esc(ident(e))}">${esc(id)}</span>` : ""}
1291
+ ${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
1144
1292
  ${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
1145
1293
  <span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
1146
1294
  <span class="age">${age(e)}</span></div>`;
@@ -1151,13 +1299,16 @@
1151
1299
  $("list").innerHTML =
1152
1300
  shown
1153
1301
  .map((e) => {
1302
+ // The host tag only earns its place when several machines are in
1303
+ // play; otherwise it's noise (every row would carry the same one).
1154
1304
  const tags = tagsFor(e)
1305
+ .filter(([k]) => k !== "host" || multiDevice)
1155
1306
  .map(
1156
1307
  ([k, v]) =>
1157
1308
  `<span class="rtag" data-k="${k}"><span style="opacity:.55">${k}:</span>${esc(v)}</span>`,
1158
1309
  )
1159
1310
  .join("");
1160
- return `<div class="row ${String(e.pid) === sel ? "sel" : ""}" data-pid="${e.pid}">
1311
+ return `<div class="row ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
1161
1312
  <div class="r1"><span class="dot ${esc(e.status)}"></span>
1162
1313
  <span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
1163
1314
  <span class="badge">pid ${e.pid}</span>
@@ -1195,48 +1346,67 @@
1195
1346
  } catch {}
1196
1347
  }
1197
1348
 
1349
+ // Match a stored/linked selection token against an entry: either the full
1350
+ // composite key (room#pid) or a bare pid (?pid= deep links, legacy ay.sel).
1351
+ const matchSel = (e, token) => e._key === token || String(e.pid) === String(token);
1352
+
1198
1353
  async function loadList() {
1199
- const remote = Conn.ch || Conn.rtc;
1200
- // A room was requested but its WebRTC tunnel isn't up yet: the cloud
1201
- // origin serves only this static page, so a same-origin /api/ls fetch
1202
- // here just 404s. Skip it connectRoom() drives loadList once the data
1203
- // channel opens (and again on every interval tick thereafter).
1204
- if (wantRemote && !remote) {
1205
- renderList();
1206
- return;
1207
- }
1208
- try {
1209
- entries = await Conn.fetchJSON("/api/ls?all=1");
1210
- setConn(remote ? "● " + (curRoom || "remote") : "● local", "var(--green)");
1211
- } catch (e) {
1212
- setConn(remote ? "● peer down" : "● ay serve down", "var(--red)");
1354
+ const srcs = [...sources.values()];
1355
+ const lists = await Promise.all(srcs.map(listSource));
1356
+ entries = lists.flat();
1357
+ // Badge: total agents + how many rooms are live. Red only when nothing
1358
+ // at all is reachable (no source answered).
1359
+ const roomSrcs = srcs.filter((s) => s.id !== LOCAL);
1360
+ const liveRooms = roomSrcs.filter((s) => s.live).length;
1361
+ const anyLive = srcs.some((s) => s.live);
1362
+ const connecting = roomSrcs.some((s) => !s.tried);
1363
+ const n = entries.length;
1364
+ if (!srcs.length) {
1365
+ setConn("● no fleet", "var(--muted)");
1366
+ } else if (!anyLive) {
1367
+ if (connecting) setConn("● connecting…", "var(--amber)");
1368
+ else setConn(roomSrcs.length ? "● rooms offline" : "● ay serve down", "var(--red)");
1369
+ } else {
1370
+ const roomBit = liveRooms ? ` · ${liveRooms} room${liveRooms === 1 ? "" : "s"}` : "";
1371
+ setConn(`● ${n} agent${n === 1 ? "" : "s"}${roomBit}`, "var(--green)");
1213
1372
  }
1373
+ renderRoomsIfOpen();
1214
1374
  renderList();
1215
- if (autoPid && entries.some((x) => String(x.pid) === String(autoPid))) {
1216
- const pid = autoPid;
1375
+ if (autoPid && entries.some((x) => matchSel(x, autoPid))) {
1376
+ const tok = autoPid;
1217
1377
  autoPid = null;
1378
+ const e = entries.find((x) => matchSel(x, tok));
1218
1379
  // On a phone, a restored selection re-highlights the row but stays on
1219
1380
  // the list — opening it would flip straight into the full-screen
1220
1381
  // terminal (show-detail) and hide the list. An explicit ?pid= link is
1221
1382
  // a deliberate jump, so it still opens. Desktop shows both panes, so
1222
1383
  // there's nothing to scope — always open.
1223
1384
  if (autoPidExplicit || window.innerWidth > 720) {
1224
- select(pid);
1385
+ select(e._key);
1225
1386
  } else {
1226
- sel = pid;
1387
+ sel = e._key;
1227
1388
  renderList();
1228
1389
  }
1229
1390
  }
1230
1391
  }
1231
1392
 
1232
- function select(pid) {
1233
- sel = String(pid);
1393
+ function select(keyOrPid) {
1394
+ const e =
1395
+ entries.find((x) => x._key === keyOrPid) ||
1396
+ entries.find((x) => String(x.pid) === String(keyOrPid));
1397
+ if (!e) {
1398
+ sel = String(keyOrPid);
1399
+ return;
1400
+ }
1401
+ sel = e._key;
1234
1402
  // Remember the selection so a refresh re-opens this agent (see boot/autoPid).
1235
1403
  try {
1236
1404
  localStorage.setItem("ay.sel", sel);
1237
1405
  } catch {}
1238
- const e = entries.find((x) => String(x.pid) === sel);
1239
- if (!e) return;
1406
+ // pid + tx are how we talk to the agent's own host; sel (composite) is
1407
+ // only for UI identity/highlight, since pids can collide across rooms.
1408
+ const pid = e.pid;
1409
+ const tx = txFor(e);
1240
1410
  renderList();
1241
1411
  // Mobile: flip the single-column layout to the terminal ("detail") pane.
1242
1412
  // Done BEFORE term.open below so xterm measures a visible container.
@@ -1262,7 +1432,7 @@
1262
1432
  scrollback: 5000,
1263
1433
  fontSize: 12,
1264
1434
  fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace',
1265
- theme: { background: "#0d1117", foreground: "#c9d1d9", cursor: "#0d1117" },
1435
+ theme: termTheme(),
1266
1436
  });
1267
1437
  fit = new FitAddon.FitAddon();
1268
1438
  term.loadAddon(fit);
@@ -1276,15 +1446,15 @@
1276
1446
  // feed it, so we just surface the latest title as the header name. Falls
1277
1447
  // back to the cli name when the agent never sets one.
1278
1448
  term.onTitleChange((t) => {
1279
- if (sel === String(e.pid) && t && t.trim()) $("rname").textContent = t.trim();
1449
+ if (sel === e._key && t && t.trim()) $("rname").textContent = t.trim();
1280
1450
  });
1281
1451
  // Adapt: drive the agent's PTY to the browser terminal size (POST
1282
1452
  // /api/resize → winsize + SIGWINCH) so its TUI reflows to match what we
1283
1453
  // render. Suppressed while we're merely adopting the agent's OWN size.
1284
1454
  let adoptingAgentSize = false;
1285
1455
  const pushSize = () => {
1286
- if (term && sel && !adoptingAgentSize)
1287
- Conn.post("/api/resize/" + encodeURIComponent(sel), {
1456
+ if (term && sel === e._key && !adoptingAgentSize)
1457
+ tx.post("/api/resize/" + encodeURIComponent(pid), {
1288
1458
  cols: term.cols,
1289
1459
  rows: term.rows,
1290
1460
  }).catch(() => {});
@@ -1296,17 +1466,18 @@
1296
1466
  // covers the UTF-8 mouse encoding (DECSET 1005). Verified end-to-end:
1297
1467
  // a drag emits \x1b[<0;..M / \x1b[<32;..M / \x1b[<0;..m, wheel \x1b[<64/65..M.
1298
1468
  const fwd = (d) => {
1299
- if (sel) Conn.post("/api/send", { keyword: sel, msg: d, code: "none" }).catch(() => {});
1469
+ if (sel === e._key)
1470
+ tx.post("/api/send", { keyword: pid, msg: d, code: "none" }).catch(() => {});
1300
1471
  };
1301
1472
  term.onData(fwd);
1302
1473
  term.onBinary(fwd);
1303
1474
  // Render the existing buffer at the AGENT's current width first so its
1304
1475
  // wrapping is correct, instead of forcing our viewport width onto stale
1305
1476
  // content. The user adapts to the window by resizing it (fit → push).
1306
- const selPid = sel;
1307
- Conn.fetchJSON("/api/size/" + encodeURIComponent(selPid))
1477
+ const selKey = e._key;
1478
+ tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
1308
1479
  .then((sz) => {
1309
- if (sel !== selPid || !term) return;
1480
+ if (sel !== selKey || !term) return;
1310
1481
  if (sz && sz.cols && sz.rows) {
1311
1482
  adoptingAgentSize = true;
1312
1483
  term.resize(sz.cols, sz.rows);
@@ -1329,8 +1500,8 @@
1329
1500
  // viewer is pinned to the bottom, and cap the buffer so it can't grow forever.
1330
1501
  $("livedot").className = "dot idle";
1331
1502
  $("livetxt").textContent = "connecting…";
1332
- const close = Conn.subscribe(
1333
- "/api/tail/" + encodeURIComponent(sel) + "?raw=1",
1503
+ const close = tx.subscribe(
1504
+ "/api/tail/" + encodeURIComponent(pid) + "?raw=1",
1334
1505
  (raw) => {
1335
1506
  if (term) term.write(raw);
1336
1507
  },
@@ -1348,7 +1519,7 @@
1348
1519
 
1349
1520
  $("list").addEventListener("click", (ev) => {
1350
1521
  const row = ev.target.closest(".row");
1351
- if (row) select(row.dataset.pid);
1522
+ if (row) select(row.dataset.key);
1352
1523
  });
1353
1524
  // Mobile back button: return to the list pane. The tail keeps streaming in the
1354
1525
  // background (selection unchanged), so reopening the agent is instant.
@@ -1379,10 +1550,10 @@
1379
1550
  const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
1380
1551
  const shown = entries.filter((e) => matches(e, toks));
1381
1552
  if (!shown.length) return;
1382
- const cur = shown.findIndex((e) => String(e.pid) === sel);
1553
+ const cur = shown.findIndex((e) => e._key === sel);
1383
1554
  const next = shown[nextIndex(shown.length, cur, dir)];
1384
- select(String(next.pid));
1385
- const row = $("list").querySelector('.row[data-pid="' + next.pid + '"]');
1555
+ select(next._key);
1556
+ const row = $("list").querySelector('.row[data-key="' + CSS.escape(next._key) + '"]');
1386
1557
  if (row) row.scrollIntoView({ block: "nearest" });
1387
1558
  }
1388
1559
  // Alt+ArrowDown / Alt+ArrowUp cycles agents. Capture phase on window so it
@@ -1401,7 +1572,6 @@
1401
1572
 
1402
1573
  // ---- rooms: localStorage cache + a manager you open by clicking the badge ----
1403
1574
  const ROOMS_KEY = "ay.rooms";
1404
- let curRoom = null;
1405
1575
  const loadRooms = () => {
1406
1576
  try {
1407
1577
  return JSON.parse(localStorage.getItem(ROOMS_KEY) || "{}");
@@ -1437,58 +1607,79 @@
1437
1607
  return "ch-" + (h >>> 0).toString(36).slice(0, 4).padStart(4, "0");
1438
1608
  }
1439
1609
 
1440
- function dropConn() {
1441
- if (Conn.rtc) {
1442
- try {
1443
- Conn.rtc.pc?.close();
1444
- } catch {}
1445
- Conn.rtc = null;
1446
- }
1447
- if (Conn.ch) {
1448
- try {
1449
- Conn.ch.close();
1450
- } catch {}
1451
- Conn.ch = null;
1452
- }
1610
+ function removeSource(room) {
1611
+ const s = sources.get(room);
1612
+ if (!s) return;
1613
+ try {
1614
+ s.client?.close?.();
1615
+ s.client?.pc?.close?.();
1616
+ } catch {}
1617
+ sources.delete(room);
1453
1618
  }
1454
1619
 
1455
- async function connectRoom(room, token, host) {
1620
+ // Add a room to the fleet and connect it — WITHOUT dropping the others, so
1621
+ // every saved room streams its agents at once. Idempotent: a room already
1622
+ // in the fleet just refreshes. The connection runs in the background; the
1623
+ // next poll picks up its agents once the tunnel is open.
1624
+ async function addRoomSource(room, token, host) {
1456
1625
  host = host || SIG_DEFAULT;
1457
- wantRemote = true; // from here on, loadList waits for the tunnel instead of hitting the origin
1458
1626
  saveRoom(room, token, host); // cache so the badge can list & reconnect later
1627
+ if (sources.has(room)) return;
1628
+ const s = {
1629
+ id: room,
1630
+ host,
1631
+ kind: host === CH_HOST ? "ch" : "rtc",
1632
+ tx: null,
1633
+ client: null,
1634
+ live: false,
1635
+ tried: false,
1636
+ devices: new Set(),
1637
+ serverCount: 0,
1638
+ };
1639
+ sources.set(room, s);
1640
+ renderRoomsIfOpen();
1459
1641
  try {
1460
- localStorage.setItem("ay.lastRoom", room); // reconnect here on a bare open
1461
- } catch {}
1462
- curRoom = room;
1463
- dropConn();
1464
- setConn("● connecting " + room + "…", "var(--amber)");
1465
- if (host === CH_HOST) {
1466
- const c = new CodehostClient(token);
1467
- c.onstate = (s) => {
1468
- if (s === "closed") setConn("● room lost", "var(--red)");
1469
- };
1470
- try {
1642
+ if (host === CH_HOST) {
1643
+ const c = new CodehostClient(token);
1644
+ c.onstate = (st) => {
1645
+ if (st === "closed") {
1646
+ s.live = false;
1647
+ renderRoomsIfOpen();
1648
+ }
1649
+ };
1650
+ await c.connect();
1651
+ s.client = c;
1652
+ s.tx = c;
1653
+ } else {
1654
+ const c = new RTCClient(host, room, token);
1655
+ c.onstate = (st) => {
1656
+ if (st === "failed" || st === "closed") {
1657
+ s.live = false;
1658
+ renderRoomsIfOpen();
1659
+ }
1660
+ };
1471
1661
  await c.connect();
1472
- Conn.ch = c;
1473
- } catch (e) {
1474
- setConn("● connect failed", "var(--red)");
1662
+ s.client = c;
1663
+ s.tx = rtcTx(c);
1475
1664
  }
1476
- loadList();
1477
- return;
1478
- }
1479
- const c = new RTCClient(host, room, token);
1480
- c.onstate = (s) => {
1481
- if (s === "failed" || s === "closed") setConn("● peer lost", "var(--red)");
1482
- };
1483
- try {
1484
- await c.connect();
1485
- Conn.rtc = c;
1665
+ s.live = true;
1486
1666
  } catch (e) {
1487
- setConn("● connect failed", "var(--red)");
1667
+ s.live = false;
1488
1668
  }
1669
+ s.tried = true;
1670
+ renderRoomsIfOpen();
1489
1671
  loadList();
1490
1672
  }
1491
1673
 
1674
+ // Connect to every saved room at once (called on boot).
1675
+ function connectAllRooms() {
1676
+ const r = loadRooms();
1677
+ for (const name of Object.keys(r)) addRoomSource(name, r[name].token, r[name].host);
1678
+ }
1679
+
1680
+ // Back-compat alias: a freshly pasted/linked room is added like any other.
1681
+ const connectRoom = (room, token, host) => addRoomSource(room, token, host);
1682
+
1492
1683
  function parseRoomInput(s) {
1493
1684
  s = s.trim();
1494
1685
  const hash = s.indexOf("#");
@@ -1503,13 +1694,32 @@
1503
1694
  return m ? { room: m[1], token: m[2], host: m[3] } : null;
1504
1695
  }
1505
1696
 
1697
+ // Re-render the rooms panel only if it's currently open (so live-count
1698
+ // updates from connect/poll land without forcing it open).
1699
+ function renderRoomsIfOpen() {
1700
+ if ($("rooms").style.display !== "none") renderRooms();
1701
+ }
1702
+
1703
+ // A room's live state: a room is reachable whenever we can join its
1704
+ // signaling server, so "live" really means "how many serving machines are
1705
+ // in it right now". The signaling layer can't tell us the count until we've
1706
+ // connected, so we show the source's last-known serverCount.
1707
+ function roomStatus(n) {
1708
+ const s = sources.get(n);
1709
+ if (!s) return `<span class="rstat off">○</span>`;
1710
+ if (!s.live) return `<span class="rstat off" title="offline">○</span>`;
1711
+ const c = s.serverCount;
1712
+ return `<span class="rstat on" title="${c} live ${c === 1 ? "server" : "servers"}">● ${c}</span>`;
1713
+ }
1714
+
1506
1715
  function renderRooms() {
1507
1716
  const r = loadRooms();
1508
1717
  const names = Object.keys(r).sort((a, b) => r[b].ts - r[a].ts);
1509
1718
  const items = names.length
1510
1719
  ? names
1511
1720
  .map(
1512
- (n) => `<div class="ritem ${n === curRoom ? "cur" : ""}">
1721
+ (n) => `<div class="ritem ${sources.get(n)?.live ? "cur" : ""}">
1722
+ ${roomStatus(n)}
1513
1723
  <span class="rname" data-room="${esc(n)}">${esc(n)}</span>
1514
1724
  <span class="rhost">${esc(r[n].host)}</span>
1515
1725
  <span class="rx" data-del="${esc(n)}" title="forget">✕</span></div>`,
@@ -1539,17 +1749,26 @@
1539
1749
  $("rooms").addEventListener("click", (ev) => {
1540
1750
  const name = ev.target.closest(".rname");
1541
1751
  if (name) {
1752
+ // Rooms are all connected at once now; clicking one filters the list to
1753
+ // it (and re-adds it if it was forgotten/offline), rather than switching.
1542
1754
  const r = loadRooms()[name.dataset.room];
1543
1755
  if (r) {
1544
1756
  connectRoom(name.dataset.room, r.token, r.host);
1757
+ $("q").value = "room:" + name.dataset.room;
1758
+ try {
1759
+ localStorage.setItem("ay.filter", $("q").value);
1760
+ } catch {}
1761
+ renderList();
1545
1762
  $("rooms").style.display = "none";
1546
1763
  }
1547
1764
  return;
1548
1765
  }
1549
1766
  const del = ev.target.closest(".rx");
1550
1767
  if (del) {
1768
+ removeSource(del.dataset.del);
1551
1769
  dropRoom(del.dataset.del);
1552
1770
  renderRooms();
1771
+ loadList();
1553
1772
  return;
1554
1773
  }
1555
1774
  if (ev.target.id === "roomadd") {
@@ -1597,16 +1816,33 @@
1597
1816
  $("launch").style.display = "flex";
1598
1817
  }
1599
1818
 
1600
- // Spawn a new agent on the CURRENT connection (local same-origin, or the
1601
- // connected remote fleet) and select it once it registers. Shared by the
1602
- // launch-URL flow and the "+ New agent" button. Returns false on a spawn
1603
- // error (the alert is already shown), true otherwise.
1604
- async function spawnAndSelect(spec) {
1819
+ // Pick the fleet to spawn on: an explicit roomId, else the selected agent's
1820
+ // source, else local, else any live source.
1821
+ function spawnTarget(roomId) {
1822
+ return (
1823
+ sources.get(roomId) ||
1824
+ srcFor(entries.find((e) => e._key === sel)) ||
1825
+ sources.get(LOCAL) ||
1826
+ [...sources.values()].find((s) => s.live) ||
1827
+ [...sources.values()][0] ||
1828
+ null
1829
+ );
1830
+ }
1831
+
1832
+ // Spawn a new agent on the chosen fleet and select it once it registers.
1833
+ // Shared by the launch-URL flow and the "+ New agent" button. Returns false
1834
+ // on a spawn error (the alert is already shown), true otherwise.
1835
+ async function spawnAndSelect(spec, roomId) {
1605
1836
  await loadList();
1606
- // Match by "newest agent that wasn't here before" — the spawn returns the
1607
- // wrapper pid, but the agent registers under the runtime's own pid.
1608
- const before = new Set(entries.map((e) => e.pid));
1609
- const res = await Conn.post("/api/spawn", {
1837
+ const target = spawnTarget(roomId);
1838
+ if (!target) {
1839
+ alert("no fleet to launch on connect a room or run `ay serve` locally");
1840
+ return false;
1841
+ }
1842
+ // Match by "newest agent that wasn't here before" ON THIS fleet — the
1843
+ // spawn returns the wrapper pid, but the agent registers under its own.
1844
+ const before = new Set(entries.filter((e) => e._room === target.id).map((e) => e.pid));
1845
+ const res = await target.tx.post("/api/spawn", {
1610
1846
  cli: spec.cli || "claude",
1611
1847
  cwd: spec.cwd || undefined,
1612
1848
  prompt: spec.prompt || undefined,
@@ -1618,10 +1854,10 @@
1618
1854
  for (let i = 0; i < 14; i++) {
1619
1855
  await loadList();
1620
1856
  const fresh = entries
1621
- .filter((e) => !before.has(e.pid))
1857
+ .filter((e) => e._room === target.id && !before.has(e.pid))
1622
1858
  .sort((a, b) => (b.started_at || 0) - (a.started_at || 0));
1623
1859
  if (fresh.length) {
1624
- select(fresh[0].pid);
1860
+ select(fresh[0]._key);
1625
1861
  return true;
1626
1862
  }
1627
1863
  await new Promise((r) => setTimeout(r, 800));
@@ -1634,7 +1870,7 @@
1634
1870
  if (!r) return;
1635
1871
  $("launch").style.display = "none";
1636
1872
  await connectRoom(room, r.token, r.host);
1637
- await spawnAndSelect(spec);
1873
+ await spawnAndSelect(spec, room);
1638
1874
  }
1639
1875
 
1640
1876
  $("launch").addEventListener("click", (ev) => {
@@ -1651,9 +1887,11 @@
1651
1887
  // when there is one, prompt optional) → POST /api/spawn on this connection.
1652
1888
  // Always allowed: the console already controls every running agent's stdin.
1653
1889
  function showNew() {
1654
- const here = entries.find((x) => String(x.pid) === sel);
1890
+ const here = entries.find((x) => x._key === sel);
1655
1891
  const cwd = here?.cwd || "";
1656
- const where = Conn.rtc ? curRoom || "remote fleet" : "local";
1892
+ const target = spawnTarget();
1893
+ const where = target ? (target.id === LOCAL ? "local" : target.id) : "local";
1894
+ $("newform").dataset.room = target ? target.id : "";
1657
1895
  $("newform").innerHTML = `<div class="lcard">
1658
1896
  <div class="ltitle">New agent · ${esc(where)}</div>
1659
1897
  <div class="nfield"><label>CLI</label><input id="nf-cli" value="claude" spellcheck="false" autocapitalize="off" /></div>
@@ -1678,7 +1916,7 @@
1678
1916
  };
1679
1917
  go.disabled = true;
1680
1918
  go.textContent = "launching…";
1681
- const ok = await spawnAndSelect(spec);
1919
+ const ok = await spawnAndSelect(spec, $("newform").dataset.room || undefined);
1682
1920
  if (ok) {
1683
1921
  $("newform").style.display = "none";
1684
1922
  } else {
@@ -1704,6 +1942,7 @@
1704
1942
  // boot: a launch URL opens the launcher; otherwise connect from the hash (then
1705
1943
  // eat the token); a bare #room reconnects from the cached token; else local.
1706
1944
  async function boot() {
1945
+ ensureLocalSource();
1707
1946
  const raw = location.hash.replace(/^#/, "");
1708
1947
  if (raw.startsWith("launch=")) {
1709
1948
  let spec = null;
@@ -1712,8 +1951,8 @@
1712
1951
  } catch {}
1713
1952
  history.replaceState(null, document.title, location.pathname + location.search); // eat launch params
1714
1953
  if (spec) showLaunch(spec);
1715
- setConn("● local", "var(--muted)");
1716
1954
  startPolling();
1955
+ connectAllRooms();
1717
1956
  return;
1718
1957
  }
1719
1958
  // #k=<token> — local-mode auth from `ay serve --http`'s printed link.
@@ -1724,8 +1963,9 @@
1724
1963
  } catch {}
1725
1964
  // SECURITY: strip the token from the URL immediately.
1726
1965
  history.replaceState(null, document.title, location.pathname + location.search);
1727
- setConn("● local", "var(--muted)");
1966
+ ensureLocalSource();
1728
1967
  startPolling();
1968
+ connectAllRooms();
1729
1969
  return;
1730
1970
  }
1731
1971
  const h = decodeURIComponent(raw);
@@ -1759,24 +1999,13 @@
1759
1999
  } else if (bare && loadRooms()[bare[1]]) {
1760
2000
  const r = loadRooms()[bare[1]];
1761
2001
  pending = { room: bare[1], token: r.token, host: r.host };
1762
- } else if (!raw) {
1763
- // No hash → reconnect to the last-used room (or the most recent saved
1764
- // one), so opening agent-yes.com brings back your list automatically.
1765
- const rooms = loadRooms();
1766
- const names = Object.keys(rooms);
1767
- if (names.length) {
1768
- const last = localStorage.getItem("ay.lastRoom");
1769
- const pick =
1770
- last && rooms[last] ? last : names.sort((a, b) => rooms[b].ts - rooms[a].ts)[0];
1771
- pending = { room: pick, token: rooms[pick].token, host: rooms[pick].host };
1772
- }
1773
2002
  }
1774
- // Render the UI immediately and refresh on a timer; connect to a room (if
1775
- // any) in the BACKGROUND so a dead/slow cached room never blanks the page.
1776
- if (!pending) setConn("● local", "var(--muted)");
1777
- else wantRemote = true; // remote room in play — loadList waits for the tunnel
2003
+ // Render the UI immediately and refresh on a timer; connect to every
2004
+ // saved room in the BACKGROUND so a dead/slow room never blanks the page,
2005
+ // and they all stream their agents into one list at once.
1778
2006
  startPolling();
1779
2007
  if (pending) connectRoom(pending.room, pending.token, pending.host);
2008
+ connectAllRooms();
1780
2009
  }
1781
2010
 
1782
2011
  // ---- activity-gated polling + auto-reload on new deploy ----------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.108.1",
3
+ "version": "1.110.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",
@@ -88,7 +88,8 @@
88
88
  "test": "vitest run",
89
89
  "test:coverage": "vitest run --coverage",
90
90
  "test:ui": "vitest run --config tests/ui-test/vitest.config.ts",
91
- "test:ui-dom": "vitest run --config tests/ui-dom/vitest.config.ts"
91
+ "test:ui-dom": "vitest run --config tests/ui-dom/vitest.config.ts",
92
+ "test:theme": "node tests/ui-test/theme.e2e.mjs"
92
93
  },
93
94
  "dependencies": {
94
95
  "@snomiao/bun-pty": "^0.3.4",
@@ -1,8 +0,0 @@
1
- import "./ts-DQiKBC0f.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./versionChecker-BQULRcxP.js";
4
- import "./pidStore-DBjlqzo8.js";
5
- import "./globalPidIndex-yVd3mbsV.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CcreSTgo.js";
7
-
8
- export { SUPPORTED_CLIS };