agent-yes 1.108.1 → 1.109.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SUPPORTED_CLIS-BlZnNglM.js +8 -0
- package/dist/{SUPPORTED_CLIS-CcreSTgo.js → SUPPORTED_CLIS-C0AwpO1R.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-BrT33v_v.js → serve-AjtjceCu.js} +5 -5
- package/dist/{subcommands-BIz8LzhF.js → subcommands-B-1ABS7S.js} +1 -1
- package/dist/{subcommands-DHPQVuWd.js → subcommands-CPJDMI84.js} +2 -2
- package/dist/{ts-DQiKBC0f.js → ts-BJvj36Z3.js} +2 -2
- package/dist/{versionChecker-BQULRcxP.js → versionChecker-DGSlVqgt.js} +2 -2
- package/lab/ui/console-logic.js +83 -0
- package/lab/ui/index.html +300 -137
- package/package.json +1 -1
- package/dist/SUPPORTED_CLIS-atYBMnHG.js +0 -8
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./ts-BJvj36Z3.js";
|
|
2
|
+
import "./logger-B9h0djqx.js";
|
|
3
|
+
import "./versionChecker-DGSlVqgt.js";
|
|
4
|
+
import "./pidStore-DBjlqzo8.js";
|
|
5
|
+
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0AwpO1R.js";
|
|
7
|
+
|
|
8
|
+
export { SUPPORTED_CLIS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-BJvj36Z3.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-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-C0AwpO1R.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-
|
|
3
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-DGSlVqgt.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-
|
|
485
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-B-1ABS7S.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-
|
|
518
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BlZnNglM.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-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-BJvj36Z3.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-DGSlVqgt.js";
|
|
4
4
|
import "./pidStore-DBjlqzo8.js";
|
|
5
5
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
6
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-BJvj36Z3.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-DGSlVqgt.js";
|
|
4
4
|
import "./pidStore-DBjlqzo8.js";
|
|
5
5
|
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0AwpO1R.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-
|
|
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-CPJDMI84.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-
|
|
555
|
+
//# sourceMappingURL=serve-AjtjceCu.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-
|
|
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-CPJDMI84.js";
|
|
5
5
|
|
|
6
6
|
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -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-
|
|
166
|
+
const { cmdServe } = await import("./serve-AjtjceCu.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-
|
|
1598
|
+
//# sourceMappingURL=subcommands-CPJDMI84.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-DGSlVqgt.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-
|
|
1717
|
+
//# sourceMappingURL=ts-BJvj36Z3.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.
|
|
10
|
+
var version = "1.109.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-
|
|
224
|
+
//# sourceMappingURL=versionChecker-DGSlVqgt.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -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
|
@@ -186,6 +186,19 @@
|
|
|
186
186
|
.rooms .ritem.cur {
|
|
187
187
|
box-shadow: inset 2px 0 0 var(--green);
|
|
188
188
|
}
|
|
189
|
+
.rooms .rstat {
|
|
190
|
+
font-family: var(--mono);
|
|
191
|
+
font-size: 10.5px;
|
|
192
|
+
flex: none;
|
|
193
|
+
min-width: 18px;
|
|
194
|
+
}
|
|
195
|
+
.rooms .rstat.on {
|
|
196
|
+
color: var(--green);
|
|
197
|
+
}
|
|
198
|
+
.rooms .rstat.off {
|
|
199
|
+
color: var(--muted);
|
|
200
|
+
opacity: 0.6;
|
|
201
|
+
}
|
|
189
202
|
.rooms .rname {
|
|
190
203
|
font-family: var(--mono);
|
|
191
204
|
color: var(--accent);
|
|
@@ -787,11 +800,15 @@
|
|
|
787
800
|
age,
|
|
788
801
|
matches,
|
|
789
802
|
nextIndex,
|
|
803
|
+
identContext,
|
|
804
|
+
compactIdent,
|
|
805
|
+
fullIdent,
|
|
806
|
+
hasIdent,
|
|
807
|
+
deviceCount,
|
|
790
808
|
} from "./console-logic.js";
|
|
791
809
|
|
|
792
810
|
let entries = [];
|
|
793
|
-
let
|
|
794
|
-
let sel = null; // selected keyword (pid as string)
|
|
811
|
+
let sel = null; // selected agent's composite key (room#pid)
|
|
795
812
|
let es = null; // live-tail subscription closer
|
|
796
813
|
let term = null; // xterm.js Terminal rendering the raw PTY stream
|
|
797
814
|
let fit = null;
|
|
@@ -1073,20 +1090,14 @@
|
|
|
1073
1090
|
: path;
|
|
1074
1091
|
};
|
|
1075
1092
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1093
|
+
// ---- transports: a uniform { fetchJSON, post, subscribe } over each wire ----
|
|
1094
|
+
// local (same-origin ay serve), an ay-share RTCClient, or a codehost room.
|
|
1095
|
+
// CodehostClient already exposes this shape, so it's used as a tx directly.
|
|
1096
|
+
const localTx = {
|
|
1079
1097
|
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
1098
|
return (await fetch(withTok(path))).json();
|
|
1083
1099
|
},
|
|
1084
1100
|
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
1101
|
const r = await fetch(withTok(path), {
|
|
1091
1102
|
method: "POST",
|
|
1092
1103
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1094,13 +1105,27 @@
|
|
|
1094
1105
|
});
|
|
1095
1106
|
return { ok: r.ok, text: await r.text() };
|
|
1096
1107
|
},
|
|
1097
|
-
// onText gets each parsed SSE data payload (same shape as the local EventSource path).
|
|
1098
1108
|
subscribe(path, onText, onOpen, onError) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1109
|
+
const ev = new EventSource(withTok(path));
|
|
1110
|
+
ev.onopen = () => onOpen && onOpen();
|
|
1111
|
+
ev.onmessage = (e) => onText(JSON.parse(e.data));
|
|
1112
|
+
ev.onerror = () => onError && onError();
|
|
1113
|
+
return () => ev.close();
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
function rtcTx(rtc) {
|
|
1117
|
+
return {
|
|
1118
|
+
async fetchJSON(path) {
|
|
1119
|
+
return JSON.parse((await rtc.req("GET", path)).text);
|
|
1120
|
+
},
|
|
1121
|
+
async post(path, bodyObj) {
|
|
1122
|
+
const r = await rtc.req("POST", path, JSON.stringify(bodyObj));
|
|
1123
|
+
return { ok: r.status >= 200 && r.status < 300, text: r.text };
|
|
1124
|
+
},
|
|
1125
|
+
subscribe(path, onText, onOpen, onError) {
|
|
1101
1126
|
onOpen && onOpen();
|
|
1102
1127
|
let buf = "";
|
|
1103
|
-
return
|
|
1128
|
+
return rtc.subscribe(path, (raw) => {
|
|
1104
1129
|
buf += raw;
|
|
1105
1130
|
let i;
|
|
1106
1131
|
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
@@ -1114,14 +1139,66 @@
|
|
|
1114
1139
|
}
|
|
1115
1140
|
}
|
|
1116
1141
|
});
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1142
|
+
},
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ---- fleet: local + every saved room, all connected at once ------------
|
|
1147
|
+
// Each source contributes its agents to one merged list (tagged with the
|
|
1148
|
+
// owning source on `_room`/`_key`); per-agent ops route back via srcFor/txFor.
|
|
1149
|
+
// Live counts in the rooms panel come from each source's serverCount/devices.
|
|
1150
|
+
const LOCAL = "local";
|
|
1151
|
+
const sources = new Map(); // id -> { id, host, kind, tx, client, live, devices, serverCount }
|
|
1152
|
+
const srcFor = (e) => (e && sources.get(e._room)) || sources.get(LOCAL) || null;
|
|
1153
|
+
const txFor = (e) => srcFor(e)?.tx || localTx;
|
|
1154
|
+
|
|
1155
|
+
// The local source is only worth polling when this page is actually backed
|
|
1156
|
+
// by an ay serve: localhost, or served by `ay serve --http` (which leaves a
|
|
1157
|
+
// token), or when there are no rooms to fall back on. On the public origin
|
|
1158
|
+
// with rooms, skip it so we don't hammer a 404 every poll.
|
|
1159
|
+
function ensureLocalSource() {
|
|
1160
|
+
const isLocalhost = ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
|
|
1161
|
+
const hasToken = !!localStorage.getItem("ay.localToken");
|
|
1162
|
+
const enabled = isLocalhost || hasToken || Object.keys(loadRooms()).length === 0;
|
|
1163
|
+
if (enabled && !sources.has(LOCAL)) {
|
|
1164
|
+
sources.set(LOCAL, {
|
|
1165
|
+
id: LOCAL,
|
|
1166
|
+
host: "local",
|
|
1167
|
+
kind: "local",
|
|
1168
|
+
tx: localTx,
|
|
1169
|
+
client: null,
|
|
1170
|
+
live: false,
|
|
1171
|
+
tried: true, // no connect phase — polled directly
|
|
1172
|
+
devices: new Set(),
|
|
1173
|
+
serverCount: 0,
|
|
1174
|
+
});
|
|
1175
|
+
} else if (!enabled) {
|
|
1176
|
+
sources.delete(LOCAL);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Pull one source's agent list, tagging each row with its origin + a
|
|
1181
|
+
// composite key (pids can collide across rooms). Updates the source's live
|
|
1182
|
+
// flag, device set, and server count for the rooms panel.
|
|
1183
|
+
async function listSource(s) {
|
|
1184
|
+
try {
|
|
1185
|
+
const arr = await s.tx.fetchJSON("/api/ls?all=1");
|
|
1186
|
+
s.live = true;
|
|
1187
|
+
s.devices = new Set();
|
|
1188
|
+
const out = (Array.isArray(arr) ? arr : []).map((e) => {
|
|
1189
|
+
const host = e._host || "";
|
|
1190
|
+
if (host) s.devices.add(host);
|
|
1191
|
+
return { ...e, _room: s.id, _key: s.id + "#" + e.pid, _host: host };
|
|
1192
|
+
});
|
|
1193
|
+
s.serverCount =
|
|
1194
|
+
s.kind === "ch" ? s.client?.hosts().length || 0 : s.devices.size || (s.live ? 1 : 0);
|
|
1195
|
+
return out;
|
|
1196
|
+
} catch {
|
|
1197
|
+
s.live = false;
|
|
1198
|
+
s.serverCount = 0;
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1125
1202
|
|
|
1126
1203
|
// Compact list: one line per agent (dot + cli + title), persisted per device.
|
|
1127
1204
|
let compactList = localStorage.getItem("ay.compactList") === "1";
|
|
@@ -1131,16 +1208,21 @@
|
|
|
1131
1208
|
const shown = entries.filter((e) => matches(e, toks));
|
|
1132
1209
|
$("count").textContent = `${shown.length} / ${entries.length} agents`;
|
|
1133
1210
|
$("viewbtn").classList.toggle("on", compactList);
|
|
1211
|
+
// identContext blanks any field uniform across the shown list (so a
|
|
1212
|
+
// single-device fleet shows no device); multiDevice gates the detailed
|
|
1213
|
+
// host tag so it only appears when machines are actually mixed.
|
|
1214
|
+
const ctx = identContext(shown);
|
|
1215
|
+
const multiDevice = deviceCount(shown) > 1;
|
|
1134
1216
|
if (compactList) {
|
|
1135
1217
|
$("list").innerHTML =
|
|
1136
1218
|
shown
|
|
1137
1219
|
.map((e) => {
|
|
1138
1220
|
const t = e.title || e.prompt || "";
|
|
1139
|
-
const id =
|
|
1221
|
+
const id = compactIdent(e, ctx);
|
|
1140
1222
|
const cli = cliLabel(e);
|
|
1141
|
-
return `<div class="row crow ${
|
|
1223
|
+
return `<div class="row crow ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
|
|
1142
1224
|
<span class="dot ${esc(e.status)}"></span>
|
|
1143
|
-
${id ? `<span class="cident" title="${esc(
|
|
1225
|
+
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
1144
1226
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
1145
1227
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
1146
1228
|
<span class="age">${age(e)}</span></div>`;
|
|
@@ -1151,13 +1233,16 @@
|
|
|
1151
1233
|
$("list").innerHTML =
|
|
1152
1234
|
shown
|
|
1153
1235
|
.map((e) => {
|
|
1236
|
+
// The host tag only earns its place when several machines are in
|
|
1237
|
+
// play; otherwise it's noise (every row would carry the same one).
|
|
1154
1238
|
const tags = tagsFor(e)
|
|
1239
|
+
.filter(([k]) => k !== "host" || multiDevice)
|
|
1155
1240
|
.map(
|
|
1156
1241
|
([k, v]) =>
|
|
1157
1242
|
`<span class="rtag" data-k="${k}"><span style="opacity:.55">${k}:</span>${esc(v)}</span>`,
|
|
1158
1243
|
)
|
|
1159
1244
|
.join("");
|
|
1160
|
-
return `<div class="row ${
|
|
1245
|
+
return `<div class="row ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
|
|
1161
1246
|
<div class="r1"><span class="dot ${esc(e.status)}"></span>
|
|
1162
1247
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
1163
1248
|
<span class="badge">pid ${e.pid}</span>
|
|
@@ -1195,48 +1280,67 @@
|
|
|
1195
1280
|
} catch {}
|
|
1196
1281
|
}
|
|
1197
1282
|
|
|
1283
|
+
// Match a stored/linked selection token against an entry: either the full
|
|
1284
|
+
// composite key (room#pid) or a bare pid (?pid= deep links, legacy ay.sel).
|
|
1285
|
+
const matchSel = (e, token) => e._key === token || String(e.pid) === String(token);
|
|
1286
|
+
|
|
1198
1287
|
async function loadList() {
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
setConn(
|
|
1211
|
-
}
|
|
1212
|
-
setConn(
|
|
1288
|
+
const srcs = [...sources.values()];
|
|
1289
|
+
const lists = await Promise.all(srcs.map(listSource));
|
|
1290
|
+
entries = lists.flat();
|
|
1291
|
+
// Badge: total agents + how many rooms are live. Red only when nothing
|
|
1292
|
+
// at all is reachable (no source answered).
|
|
1293
|
+
const roomSrcs = srcs.filter((s) => s.id !== LOCAL);
|
|
1294
|
+
const liveRooms = roomSrcs.filter((s) => s.live).length;
|
|
1295
|
+
const anyLive = srcs.some((s) => s.live);
|
|
1296
|
+
const connecting = roomSrcs.some((s) => !s.tried);
|
|
1297
|
+
const n = entries.length;
|
|
1298
|
+
if (!srcs.length) {
|
|
1299
|
+
setConn("● no fleet", "var(--muted)");
|
|
1300
|
+
} else if (!anyLive) {
|
|
1301
|
+
if (connecting) setConn("● connecting…", "var(--amber)");
|
|
1302
|
+
else setConn(roomSrcs.length ? "● rooms offline" : "● ay serve down", "var(--red)");
|
|
1303
|
+
} else {
|
|
1304
|
+
const roomBit = liveRooms ? ` · ${liveRooms} room${liveRooms === 1 ? "" : "s"}` : "";
|
|
1305
|
+
setConn(`● ${n} agent${n === 1 ? "" : "s"}${roomBit}`, "var(--green)");
|
|
1213
1306
|
}
|
|
1307
|
+
renderRoomsIfOpen();
|
|
1214
1308
|
renderList();
|
|
1215
|
-
if (autoPid && entries.some((x) =>
|
|
1216
|
-
const
|
|
1309
|
+
if (autoPid && entries.some((x) => matchSel(x, autoPid))) {
|
|
1310
|
+
const tok = autoPid;
|
|
1217
1311
|
autoPid = null;
|
|
1312
|
+
const e = entries.find((x) => matchSel(x, tok));
|
|
1218
1313
|
// On a phone, a restored selection re-highlights the row but stays on
|
|
1219
1314
|
// the list — opening it would flip straight into the full-screen
|
|
1220
1315
|
// terminal (show-detail) and hide the list. An explicit ?pid= link is
|
|
1221
1316
|
// a deliberate jump, so it still opens. Desktop shows both panes, so
|
|
1222
1317
|
// there's nothing to scope — always open.
|
|
1223
1318
|
if (autoPidExplicit || window.innerWidth > 720) {
|
|
1224
|
-
select(
|
|
1319
|
+
select(e._key);
|
|
1225
1320
|
} else {
|
|
1226
|
-
sel =
|
|
1321
|
+
sel = e._key;
|
|
1227
1322
|
renderList();
|
|
1228
1323
|
}
|
|
1229
1324
|
}
|
|
1230
1325
|
}
|
|
1231
1326
|
|
|
1232
|
-
function select(
|
|
1233
|
-
|
|
1327
|
+
function select(keyOrPid) {
|
|
1328
|
+
const e =
|
|
1329
|
+
entries.find((x) => x._key === keyOrPid) ||
|
|
1330
|
+
entries.find((x) => String(x.pid) === String(keyOrPid));
|
|
1331
|
+
if (!e) {
|
|
1332
|
+
sel = String(keyOrPid);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
sel = e._key;
|
|
1234
1336
|
// Remember the selection so a refresh re-opens this agent (see boot/autoPid).
|
|
1235
1337
|
try {
|
|
1236
1338
|
localStorage.setItem("ay.sel", sel);
|
|
1237
1339
|
} catch {}
|
|
1238
|
-
|
|
1239
|
-
|
|
1340
|
+
// pid + tx are how we talk to the agent's own host; sel (composite) is
|
|
1341
|
+
// only for UI identity/highlight, since pids can collide across rooms.
|
|
1342
|
+
const pid = e.pid;
|
|
1343
|
+
const tx = txFor(e);
|
|
1240
1344
|
renderList();
|
|
1241
1345
|
// Mobile: flip the single-column layout to the terminal ("detail") pane.
|
|
1242
1346
|
// Done BEFORE term.open below so xterm measures a visible container.
|
|
@@ -1276,15 +1380,15 @@
|
|
|
1276
1380
|
// feed it, so we just surface the latest title as the header name. Falls
|
|
1277
1381
|
// back to the cli name when the agent never sets one.
|
|
1278
1382
|
term.onTitleChange((t) => {
|
|
1279
|
-
if (sel ===
|
|
1383
|
+
if (sel === e._key && t && t.trim()) $("rname").textContent = t.trim();
|
|
1280
1384
|
});
|
|
1281
1385
|
// Adapt: drive the agent's PTY to the browser terminal size (POST
|
|
1282
1386
|
// /api/resize → winsize + SIGWINCH) so its TUI reflows to match what we
|
|
1283
1387
|
// render. Suppressed while we're merely adopting the agent's OWN size.
|
|
1284
1388
|
let adoptingAgentSize = false;
|
|
1285
1389
|
const pushSize = () => {
|
|
1286
|
-
if (term && sel && !adoptingAgentSize)
|
|
1287
|
-
|
|
1390
|
+
if (term && sel === e._key && !adoptingAgentSize)
|
|
1391
|
+
tx.post("/api/resize/" + encodeURIComponent(pid), {
|
|
1288
1392
|
cols: term.cols,
|
|
1289
1393
|
rows: term.rows,
|
|
1290
1394
|
}).catch(() => {});
|
|
@@ -1296,17 +1400,18 @@
|
|
|
1296
1400
|
// covers the UTF-8 mouse encoding (DECSET 1005). Verified end-to-end:
|
|
1297
1401
|
// a drag emits \x1b[<0;..M / \x1b[<32;..M / \x1b[<0;..m, wheel \x1b[<64/65..M.
|
|
1298
1402
|
const fwd = (d) => {
|
|
1299
|
-
if (sel
|
|
1403
|
+
if (sel === e._key)
|
|
1404
|
+
tx.post("/api/send", { keyword: pid, msg: d, code: "none" }).catch(() => {});
|
|
1300
1405
|
};
|
|
1301
1406
|
term.onData(fwd);
|
|
1302
1407
|
term.onBinary(fwd);
|
|
1303
1408
|
// Render the existing buffer at the AGENT's current width first so its
|
|
1304
1409
|
// wrapping is correct, instead of forcing our viewport width onto stale
|
|
1305
1410
|
// content. The user adapts to the window by resizing it (fit → push).
|
|
1306
|
-
const
|
|
1307
|
-
|
|
1411
|
+
const selKey = e._key;
|
|
1412
|
+
tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
|
|
1308
1413
|
.then((sz) => {
|
|
1309
|
-
if (sel !==
|
|
1414
|
+
if (sel !== selKey || !term) return;
|
|
1310
1415
|
if (sz && sz.cols && sz.rows) {
|
|
1311
1416
|
adoptingAgentSize = true;
|
|
1312
1417
|
term.resize(sz.cols, sz.rows);
|
|
@@ -1329,8 +1434,8 @@
|
|
|
1329
1434
|
// viewer is pinned to the bottom, and cap the buffer so it can't grow forever.
|
|
1330
1435
|
$("livedot").className = "dot idle";
|
|
1331
1436
|
$("livetxt").textContent = "connecting…";
|
|
1332
|
-
const close =
|
|
1333
|
-
"/api/tail/" + encodeURIComponent(
|
|
1437
|
+
const close = tx.subscribe(
|
|
1438
|
+
"/api/tail/" + encodeURIComponent(pid) + "?raw=1",
|
|
1334
1439
|
(raw) => {
|
|
1335
1440
|
if (term) term.write(raw);
|
|
1336
1441
|
},
|
|
@@ -1348,7 +1453,7 @@
|
|
|
1348
1453
|
|
|
1349
1454
|
$("list").addEventListener("click", (ev) => {
|
|
1350
1455
|
const row = ev.target.closest(".row");
|
|
1351
|
-
if (row) select(row.dataset.
|
|
1456
|
+
if (row) select(row.dataset.key);
|
|
1352
1457
|
});
|
|
1353
1458
|
// Mobile back button: return to the list pane. The tail keeps streaming in the
|
|
1354
1459
|
// background (selection unchanged), so reopening the agent is instant.
|
|
@@ -1379,10 +1484,10 @@
|
|
|
1379
1484
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
1380
1485
|
const shown = entries.filter((e) => matches(e, toks));
|
|
1381
1486
|
if (!shown.length) return;
|
|
1382
|
-
const cur = shown.findIndex((e) =>
|
|
1487
|
+
const cur = shown.findIndex((e) => e._key === sel);
|
|
1383
1488
|
const next = shown[nextIndex(shown.length, cur, dir)];
|
|
1384
|
-
select(
|
|
1385
|
-
const row = $("list").querySelector('.row[data-
|
|
1489
|
+
select(next._key);
|
|
1490
|
+
const row = $("list").querySelector('.row[data-key="' + CSS.escape(next._key) + '"]');
|
|
1386
1491
|
if (row) row.scrollIntoView({ block: "nearest" });
|
|
1387
1492
|
}
|
|
1388
1493
|
// Alt+ArrowDown / Alt+ArrowUp cycles agents. Capture phase on window so it
|
|
@@ -1401,7 +1506,6 @@
|
|
|
1401
1506
|
|
|
1402
1507
|
// ---- rooms: localStorage cache + a manager you open by clicking the badge ----
|
|
1403
1508
|
const ROOMS_KEY = "ay.rooms";
|
|
1404
|
-
let curRoom = null;
|
|
1405
1509
|
const loadRooms = () => {
|
|
1406
1510
|
try {
|
|
1407
1511
|
return JSON.parse(localStorage.getItem(ROOMS_KEY) || "{}");
|
|
@@ -1437,58 +1541,79 @@
|
|
|
1437
1541
|
return "ch-" + (h >>> 0).toString(36).slice(0, 4).padStart(4, "0");
|
|
1438
1542
|
}
|
|
1439
1543
|
|
|
1440
|
-
function
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
try {
|
|
1449
|
-
Conn.ch.close();
|
|
1450
|
-
} catch {}
|
|
1451
|
-
Conn.ch = null;
|
|
1452
|
-
}
|
|
1544
|
+
function removeSource(room) {
|
|
1545
|
+
const s = sources.get(room);
|
|
1546
|
+
if (!s) return;
|
|
1547
|
+
try {
|
|
1548
|
+
s.client?.close?.();
|
|
1549
|
+
s.client?.pc?.close?.();
|
|
1550
|
+
} catch {}
|
|
1551
|
+
sources.delete(room);
|
|
1453
1552
|
}
|
|
1454
1553
|
|
|
1455
|
-
|
|
1554
|
+
// Add a room to the fleet and connect it — WITHOUT dropping the others, so
|
|
1555
|
+
// every saved room streams its agents at once. Idempotent: a room already
|
|
1556
|
+
// in the fleet just refreshes. The connection runs in the background; the
|
|
1557
|
+
// next poll picks up its agents once the tunnel is open.
|
|
1558
|
+
async function addRoomSource(room, token, host) {
|
|
1456
1559
|
host = host || SIG_DEFAULT;
|
|
1457
|
-
wantRemote = true; // from here on, loadList waits for the tunnel instead of hitting the origin
|
|
1458
1560
|
saveRoom(room, token, host); // cache so the badge can list & reconnect later
|
|
1561
|
+
if (sources.has(room)) return;
|
|
1562
|
+
const s = {
|
|
1563
|
+
id: room,
|
|
1564
|
+
host,
|
|
1565
|
+
kind: host === CH_HOST ? "ch" : "rtc",
|
|
1566
|
+
tx: null,
|
|
1567
|
+
client: null,
|
|
1568
|
+
live: false,
|
|
1569
|
+
tried: false,
|
|
1570
|
+
devices: new Set(),
|
|
1571
|
+
serverCount: 0,
|
|
1572
|
+
};
|
|
1573
|
+
sources.set(room, s);
|
|
1574
|
+
renderRoomsIfOpen();
|
|
1459
1575
|
try {
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
if (s === "closed") setConn("● room lost", "var(--red)");
|
|
1469
|
-
};
|
|
1470
|
-
try {
|
|
1576
|
+
if (host === CH_HOST) {
|
|
1577
|
+
const c = new CodehostClient(token);
|
|
1578
|
+
c.onstate = (st) => {
|
|
1579
|
+
if (st === "closed") {
|
|
1580
|
+
s.live = false;
|
|
1581
|
+
renderRoomsIfOpen();
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1471
1584
|
await c.connect();
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1585
|
+
s.client = c;
|
|
1586
|
+
s.tx = c;
|
|
1587
|
+
} else {
|
|
1588
|
+
const c = new RTCClient(host, room, token);
|
|
1589
|
+
c.onstate = (st) => {
|
|
1590
|
+
if (st === "failed" || st === "closed") {
|
|
1591
|
+
s.live = false;
|
|
1592
|
+
renderRoomsIfOpen();
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
await c.connect();
|
|
1596
|
+
s.client = c;
|
|
1597
|
+
s.tx = rtcTx(c);
|
|
1475
1598
|
}
|
|
1476
|
-
|
|
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;
|
|
1599
|
+
s.live = true;
|
|
1486
1600
|
} catch (e) {
|
|
1487
|
-
|
|
1601
|
+
s.live = false;
|
|
1488
1602
|
}
|
|
1603
|
+
s.tried = true;
|
|
1604
|
+
renderRoomsIfOpen();
|
|
1489
1605
|
loadList();
|
|
1490
1606
|
}
|
|
1491
1607
|
|
|
1608
|
+
// Connect to every saved room at once (called on boot).
|
|
1609
|
+
function connectAllRooms() {
|
|
1610
|
+
const r = loadRooms();
|
|
1611
|
+
for (const name of Object.keys(r)) addRoomSource(name, r[name].token, r[name].host);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// Back-compat alias: a freshly pasted/linked room is added like any other.
|
|
1615
|
+
const connectRoom = (room, token, host) => addRoomSource(room, token, host);
|
|
1616
|
+
|
|
1492
1617
|
function parseRoomInput(s) {
|
|
1493
1618
|
s = s.trim();
|
|
1494
1619
|
const hash = s.indexOf("#");
|
|
@@ -1503,13 +1628,32 @@
|
|
|
1503
1628
|
return m ? { room: m[1], token: m[2], host: m[3] } : null;
|
|
1504
1629
|
}
|
|
1505
1630
|
|
|
1631
|
+
// Re-render the rooms panel only if it's currently open (so live-count
|
|
1632
|
+
// updates from connect/poll land without forcing it open).
|
|
1633
|
+
function renderRoomsIfOpen() {
|
|
1634
|
+
if ($("rooms").style.display !== "none") renderRooms();
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// A room's live state: a room is reachable whenever we can join its
|
|
1638
|
+
// signaling server, so "live" really means "how many serving machines are
|
|
1639
|
+
// in it right now". The signaling layer can't tell us the count until we've
|
|
1640
|
+
// connected, so we show the source's last-known serverCount.
|
|
1641
|
+
function roomStatus(n) {
|
|
1642
|
+
const s = sources.get(n);
|
|
1643
|
+
if (!s) return `<span class="rstat off">○</span>`;
|
|
1644
|
+
if (!s.live) return `<span class="rstat off" title="offline">○</span>`;
|
|
1645
|
+
const c = s.serverCount;
|
|
1646
|
+
return `<span class="rstat on" title="${c} live ${c === 1 ? "server" : "servers"}">● ${c}</span>`;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1506
1649
|
function renderRooms() {
|
|
1507
1650
|
const r = loadRooms();
|
|
1508
1651
|
const names = Object.keys(r).sort((a, b) => r[b].ts - r[a].ts);
|
|
1509
1652
|
const items = names.length
|
|
1510
1653
|
? names
|
|
1511
1654
|
.map(
|
|
1512
|
-
(n) => `<div class="ritem ${n
|
|
1655
|
+
(n) => `<div class="ritem ${sources.get(n)?.live ? "cur" : ""}">
|
|
1656
|
+
${roomStatus(n)}
|
|
1513
1657
|
<span class="rname" data-room="${esc(n)}">${esc(n)}</span>
|
|
1514
1658
|
<span class="rhost">${esc(r[n].host)}</span>
|
|
1515
1659
|
<span class="rx" data-del="${esc(n)}" title="forget">✕</span></div>`,
|
|
@@ -1539,17 +1683,26 @@
|
|
|
1539
1683
|
$("rooms").addEventListener("click", (ev) => {
|
|
1540
1684
|
const name = ev.target.closest(".rname");
|
|
1541
1685
|
if (name) {
|
|
1686
|
+
// Rooms are all connected at once now; clicking one filters the list to
|
|
1687
|
+
// it (and re-adds it if it was forgotten/offline), rather than switching.
|
|
1542
1688
|
const r = loadRooms()[name.dataset.room];
|
|
1543
1689
|
if (r) {
|
|
1544
1690
|
connectRoom(name.dataset.room, r.token, r.host);
|
|
1691
|
+
$("q").value = "room:" + name.dataset.room;
|
|
1692
|
+
try {
|
|
1693
|
+
localStorage.setItem("ay.filter", $("q").value);
|
|
1694
|
+
} catch {}
|
|
1695
|
+
renderList();
|
|
1545
1696
|
$("rooms").style.display = "none";
|
|
1546
1697
|
}
|
|
1547
1698
|
return;
|
|
1548
1699
|
}
|
|
1549
1700
|
const del = ev.target.closest(".rx");
|
|
1550
1701
|
if (del) {
|
|
1702
|
+
removeSource(del.dataset.del);
|
|
1551
1703
|
dropRoom(del.dataset.del);
|
|
1552
1704
|
renderRooms();
|
|
1705
|
+
loadList();
|
|
1553
1706
|
return;
|
|
1554
1707
|
}
|
|
1555
1708
|
if (ev.target.id === "roomadd") {
|
|
@@ -1597,16 +1750,33 @@
|
|
|
1597
1750
|
$("launch").style.display = "flex";
|
|
1598
1751
|
}
|
|
1599
1752
|
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1753
|
+
// Pick the fleet to spawn on: an explicit roomId, else the selected agent's
|
|
1754
|
+
// source, else local, else any live source.
|
|
1755
|
+
function spawnTarget(roomId) {
|
|
1756
|
+
return (
|
|
1757
|
+
sources.get(roomId) ||
|
|
1758
|
+
srcFor(entries.find((e) => e._key === sel)) ||
|
|
1759
|
+
sources.get(LOCAL) ||
|
|
1760
|
+
[...sources.values()].find((s) => s.live) ||
|
|
1761
|
+
[...sources.values()][0] ||
|
|
1762
|
+
null
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// Spawn a new agent on the chosen fleet and select it once it registers.
|
|
1767
|
+
// Shared by the launch-URL flow and the "+ New agent" button. Returns false
|
|
1768
|
+
// on a spawn error (the alert is already shown), true otherwise.
|
|
1769
|
+
async function spawnAndSelect(spec, roomId) {
|
|
1605
1770
|
await loadList();
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1771
|
+
const target = spawnTarget(roomId);
|
|
1772
|
+
if (!target) {
|
|
1773
|
+
alert("no fleet to launch on — connect a room or run `ay serve` locally");
|
|
1774
|
+
return false;
|
|
1775
|
+
}
|
|
1776
|
+
// Match by "newest agent that wasn't here before" ON THIS fleet — the
|
|
1777
|
+
// spawn returns the wrapper pid, but the agent registers under its own.
|
|
1778
|
+
const before = new Set(entries.filter((e) => e._room === target.id).map((e) => e.pid));
|
|
1779
|
+
const res = await target.tx.post("/api/spawn", {
|
|
1610
1780
|
cli: spec.cli || "claude",
|
|
1611
1781
|
cwd: spec.cwd || undefined,
|
|
1612
1782
|
prompt: spec.prompt || undefined,
|
|
@@ -1618,10 +1788,10 @@
|
|
|
1618
1788
|
for (let i = 0; i < 14; i++) {
|
|
1619
1789
|
await loadList();
|
|
1620
1790
|
const fresh = entries
|
|
1621
|
-
.filter((e) => !before.has(e.pid))
|
|
1791
|
+
.filter((e) => e._room === target.id && !before.has(e.pid))
|
|
1622
1792
|
.sort((a, b) => (b.started_at || 0) - (a.started_at || 0));
|
|
1623
1793
|
if (fresh.length) {
|
|
1624
|
-
select(fresh[0].
|
|
1794
|
+
select(fresh[0]._key);
|
|
1625
1795
|
return true;
|
|
1626
1796
|
}
|
|
1627
1797
|
await new Promise((r) => setTimeout(r, 800));
|
|
@@ -1634,7 +1804,7 @@
|
|
|
1634
1804
|
if (!r) return;
|
|
1635
1805
|
$("launch").style.display = "none";
|
|
1636
1806
|
await connectRoom(room, r.token, r.host);
|
|
1637
|
-
await spawnAndSelect(spec);
|
|
1807
|
+
await spawnAndSelect(spec, room);
|
|
1638
1808
|
}
|
|
1639
1809
|
|
|
1640
1810
|
$("launch").addEventListener("click", (ev) => {
|
|
@@ -1651,9 +1821,11 @@
|
|
|
1651
1821
|
// when there is one, prompt optional) → POST /api/spawn on this connection.
|
|
1652
1822
|
// Always allowed: the console already controls every running agent's stdin.
|
|
1653
1823
|
function showNew() {
|
|
1654
|
-
const here = entries.find((x) =>
|
|
1824
|
+
const here = entries.find((x) => x._key === sel);
|
|
1655
1825
|
const cwd = here?.cwd || "";
|
|
1656
|
-
const
|
|
1826
|
+
const target = spawnTarget();
|
|
1827
|
+
const where = target ? (target.id === LOCAL ? "local" : target.id) : "local";
|
|
1828
|
+
$("newform").dataset.room = target ? target.id : "";
|
|
1657
1829
|
$("newform").innerHTML = `<div class="lcard">
|
|
1658
1830
|
<div class="ltitle">New agent · ${esc(where)}</div>
|
|
1659
1831
|
<div class="nfield"><label>CLI</label><input id="nf-cli" value="claude" spellcheck="false" autocapitalize="off" /></div>
|
|
@@ -1678,7 +1850,7 @@
|
|
|
1678
1850
|
};
|
|
1679
1851
|
go.disabled = true;
|
|
1680
1852
|
go.textContent = "launching…";
|
|
1681
|
-
const ok = await spawnAndSelect(spec);
|
|
1853
|
+
const ok = await spawnAndSelect(spec, $("newform").dataset.room || undefined);
|
|
1682
1854
|
if (ok) {
|
|
1683
1855
|
$("newform").style.display = "none";
|
|
1684
1856
|
} else {
|
|
@@ -1704,6 +1876,7 @@
|
|
|
1704
1876
|
// boot: a launch URL opens the launcher; otherwise connect from the hash (then
|
|
1705
1877
|
// eat the token); a bare #room reconnects from the cached token; else local.
|
|
1706
1878
|
async function boot() {
|
|
1879
|
+
ensureLocalSource();
|
|
1707
1880
|
const raw = location.hash.replace(/^#/, "");
|
|
1708
1881
|
if (raw.startsWith("launch=")) {
|
|
1709
1882
|
let spec = null;
|
|
@@ -1712,8 +1885,8 @@
|
|
|
1712
1885
|
} catch {}
|
|
1713
1886
|
history.replaceState(null, document.title, location.pathname + location.search); // eat launch params
|
|
1714
1887
|
if (spec) showLaunch(spec);
|
|
1715
|
-
setConn("● local", "var(--muted)");
|
|
1716
1888
|
startPolling();
|
|
1889
|
+
connectAllRooms();
|
|
1717
1890
|
return;
|
|
1718
1891
|
}
|
|
1719
1892
|
// #k=<token> — local-mode auth from `ay serve --http`'s printed link.
|
|
@@ -1724,8 +1897,9 @@
|
|
|
1724
1897
|
} catch {}
|
|
1725
1898
|
// SECURITY: strip the token from the URL immediately.
|
|
1726
1899
|
history.replaceState(null, document.title, location.pathname + location.search);
|
|
1727
|
-
|
|
1900
|
+
ensureLocalSource();
|
|
1728
1901
|
startPolling();
|
|
1902
|
+
connectAllRooms();
|
|
1729
1903
|
return;
|
|
1730
1904
|
}
|
|
1731
1905
|
const h = decodeURIComponent(raw);
|
|
@@ -1759,24 +1933,13 @@
|
|
|
1759
1933
|
} else if (bare && loadRooms()[bare[1]]) {
|
|
1760
1934
|
const r = loadRooms()[bare[1]];
|
|
1761
1935
|
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
1936
|
}
|
|
1774
|
-
// Render the UI immediately and refresh on a timer; connect to
|
|
1775
|
-
//
|
|
1776
|
-
|
|
1777
|
-
else wantRemote = true; // remote room in play — loadList waits for the tunnel
|
|
1937
|
+
// Render the UI immediately and refresh on a timer; connect to every
|
|
1938
|
+
// saved room in the BACKGROUND so a dead/slow room never blanks the page,
|
|
1939
|
+
// and they all stream their agents into one list at once.
|
|
1778
1940
|
startPolling();
|
|
1779
1941
|
if (pending) connectRoom(pending.room, pending.token, pending.host);
|
|
1942
|
+
connectAllRooms();
|
|
1780
1943
|
}
|
|
1781
1944
|
|
|
1782
1945
|
// ---- activity-gated polling + auto-reload on new deploy ----------------
|
package/package.json
CHANGED
|
@@ -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 };
|