agent-yes 1.108.0 → 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-BzuJvKuH.js → SUPPORTED_CLIS-C0AwpO1R.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-D4puI0b2.js → serve-AjtjceCu.js} +5 -5
- package/dist/{subcommands-4tanf24s.js → subcommands-B-1ABS7S.js} +1 -1
- package/dist/{subcommands-BLPtg1xN.js → subcommands-CPJDMI84.js} +2 -2
- package/dist/{ts-D_sIq4Yv.js → ts-BJvj36Z3.js} +2 -2
- package/dist/{versionChecker-osmGP6ly.js → versionChecker-DGSlVqgt.js} +2 -2
- package/lab/ui/console-logic.js +83 -0
- package/lab/ui/index.html +300 -126
- package/package.json +1 -1
- package/ts/workspaceConfig.spec.ts +4 -1
- package/dist/SUPPORTED_CLIS-B_gPkhav.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,10 +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 sel = null; // selected
|
|
811
|
+
let sel = null; // selected agent's composite key (room#pid)
|
|
794
812
|
let es = null; // live-tail subscription closer
|
|
795
813
|
let term = null; // xterm.js Terminal rendering the raw PTY stream
|
|
796
814
|
let fit = null;
|
|
@@ -1072,20 +1090,14 @@
|
|
|
1072
1090
|
: path;
|
|
1073
1091
|
};
|
|
1074
1092
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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 = {
|
|
1078
1097
|
async fetchJSON(path) {
|
|
1079
|
-
if (this.ch) return this.ch.fetchJSON(path);
|
|
1080
|
-
if (this.rtc) return JSON.parse((await this.rtc.req("GET", path)).text);
|
|
1081
1098
|
return (await fetch(withTok(path))).json();
|
|
1082
1099
|
},
|
|
1083
1100
|
async post(path, bodyObj) {
|
|
1084
|
-
if (this.ch) return this.ch.post(path, bodyObj);
|
|
1085
|
-
if (this.rtc) {
|
|
1086
|
-
const r = await this.rtc.req("POST", path, JSON.stringify(bodyObj));
|
|
1087
|
-
return { ok: r.status >= 200 && r.status < 300, text: r.text };
|
|
1088
|
-
}
|
|
1089
1101
|
const r = await fetch(withTok(path), {
|
|
1090
1102
|
method: "POST",
|
|
1091
1103
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1093,13 +1105,27 @@
|
|
|
1093
1105
|
});
|
|
1094
1106
|
return { ok: r.ok, text: await r.text() };
|
|
1095
1107
|
},
|
|
1096
|
-
// onText gets each parsed SSE data payload (same shape as the local EventSource path).
|
|
1097
1108
|
subscribe(path, onText, onOpen, onError) {
|
|
1098
|
-
|
|
1099
|
-
|
|
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) {
|
|
1100
1126
|
onOpen && onOpen();
|
|
1101
1127
|
let buf = "";
|
|
1102
|
-
return
|
|
1128
|
+
return rtc.subscribe(path, (raw) => {
|
|
1103
1129
|
buf += raw;
|
|
1104
1130
|
let i;
|
|
1105
1131
|
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
@@ -1113,14 +1139,66 @@
|
|
|
1113
1139
|
}
|
|
1114
1140
|
}
|
|
1115
1141
|
});
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
+
}
|
|
1124
1202
|
|
|
1125
1203
|
// Compact list: one line per agent (dot + cli + title), persisted per device.
|
|
1126
1204
|
let compactList = localStorage.getItem("ay.compactList") === "1";
|
|
@@ -1130,16 +1208,21 @@
|
|
|
1130
1208
|
const shown = entries.filter((e) => matches(e, toks));
|
|
1131
1209
|
$("count").textContent = `${shown.length} / ${entries.length} agents`;
|
|
1132
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;
|
|
1133
1216
|
if (compactList) {
|
|
1134
1217
|
$("list").innerHTML =
|
|
1135
1218
|
shown
|
|
1136
1219
|
.map((e) => {
|
|
1137
1220
|
const t = e.title || e.prompt || "";
|
|
1138
|
-
const id =
|
|
1221
|
+
const id = compactIdent(e, ctx);
|
|
1139
1222
|
const cli = cliLabel(e);
|
|
1140
|
-
return `<div class="row crow ${
|
|
1223
|
+
return `<div class="row crow ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
|
|
1141
1224
|
<span class="dot ${esc(e.status)}"></span>
|
|
1142
|
-
${id ? `<span class="cident" title="${esc(
|
|
1225
|
+
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
1143
1226
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
1144
1227
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
1145
1228
|
<span class="age">${age(e)}</span></div>`;
|
|
@@ -1150,13 +1233,16 @@
|
|
|
1150
1233
|
$("list").innerHTML =
|
|
1151
1234
|
shown
|
|
1152
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).
|
|
1153
1238
|
const tags = tagsFor(e)
|
|
1239
|
+
.filter(([k]) => k !== "host" || multiDevice)
|
|
1154
1240
|
.map(
|
|
1155
1241
|
([k, v]) =>
|
|
1156
1242
|
`<span class="rtag" data-k="${k}"><span style="opacity:.55">${k}:</span>${esc(v)}</span>`,
|
|
1157
1243
|
)
|
|
1158
1244
|
.join("");
|
|
1159
|
-
return `<div class="row ${
|
|
1245
|
+
return `<div class="row ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
|
|
1160
1246
|
<div class="r1"><span class="dot ${esc(e.status)}"></span>
|
|
1161
1247
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
1162
1248
|
<span class="badge">pid ${e.pid}</span>
|
|
@@ -1194,40 +1280,67 @@
|
|
|
1194
1280
|
} catch {}
|
|
1195
1281
|
}
|
|
1196
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
|
+
|
|
1197
1287
|
async function loadList() {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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)");
|
|
1204
1306
|
}
|
|
1307
|
+
renderRoomsIfOpen();
|
|
1205
1308
|
renderList();
|
|
1206
|
-
if (autoPid && entries.some((x) =>
|
|
1207
|
-
const
|
|
1309
|
+
if (autoPid && entries.some((x) => matchSel(x, autoPid))) {
|
|
1310
|
+
const tok = autoPid;
|
|
1208
1311
|
autoPid = null;
|
|
1312
|
+
const e = entries.find((x) => matchSel(x, tok));
|
|
1209
1313
|
// On a phone, a restored selection re-highlights the row but stays on
|
|
1210
1314
|
// the list — opening it would flip straight into the full-screen
|
|
1211
1315
|
// terminal (show-detail) and hide the list. An explicit ?pid= link is
|
|
1212
1316
|
// a deliberate jump, so it still opens. Desktop shows both panes, so
|
|
1213
1317
|
// there's nothing to scope — always open.
|
|
1214
1318
|
if (autoPidExplicit || window.innerWidth > 720) {
|
|
1215
|
-
select(
|
|
1319
|
+
select(e._key);
|
|
1216
1320
|
} else {
|
|
1217
|
-
sel =
|
|
1321
|
+
sel = e._key;
|
|
1218
1322
|
renderList();
|
|
1219
1323
|
}
|
|
1220
1324
|
}
|
|
1221
1325
|
}
|
|
1222
1326
|
|
|
1223
|
-
function select(
|
|
1224
|
-
|
|
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;
|
|
1225
1336
|
// Remember the selection so a refresh re-opens this agent (see boot/autoPid).
|
|
1226
1337
|
try {
|
|
1227
1338
|
localStorage.setItem("ay.sel", sel);
|
|
1228
1339
|
} catch {}
|
|
1229
|
-
|
|
1230
|
-
|
|
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);
|
|
1231
1344
|
renderList();
|
|
1232
1345
|
// Mobile: flip the single-column layout to the terminal ("detail") pane.
|
|
1233
1346
|
// Done BEFORE term.open below so xterm measures a visible container.
|
|
@@ -1267,15 +1380,15 @@
|
|
|
1267
1380
|
// feed it, so we just surface the latest title as the header name. Falls
|
|
1268
1381
|
// back to the cli name when the agent never sets one.
|
|
1269
1382
|
term.onTitleChange((t) => {
|
|
1270
|
-
if (sel ===
|
|
1383
|
+
if (sel === e._key && t && t.trim()) $("rname").textContent = t.trim();
|
|
1271
1384
|
});
|
|
1272
1385
|
// Adapt: drive the agent's PTY to the browser terminal size (POST
|
|
1273
1386
|
// /api/resize → winsize + SIGWINCH) so its TUI reflows to match what we
|
|
1274
1387
|
// render. Suppressed while we're merely adopting the agent's OWN size.
|
|
1275
1388
|
let adoptingAgentSize = false;
|
|
1276
1389
|
const pushSize = () => {
|
|
1277
|
-
if (term && sel && !adoptingAgentSize)
|
|
1278
|
-
|
|
1390
|
+
if (term && sel === e._key && !adoptingAgentSize)
|
|
1391
|
+
tx.post("/api/resize/" + encodeURIComponent(pid), {
|
|
1279
1392
|
cols: term.cols,
|
|
1280
1393
|
rows: term.rows,
|
|
1281
1394
|
}).catch(() => {});
|
|
@@ -1287,17 +1400,18 @@
|
|
|
1287
1400
|
// covers the UTF-8 mouse encoding (DECSET 1005). Verified end-to-end:
|
|
1288
1401
|
// a drag emits \x1b[<0;..M / \x1b[<32;..M / \x1b[<0;..m, wheel \x1b[<64/65..M.
|
|
1289
1402
|
const fwd = (d) => {
|
|
1290
|
-
if (sel
|
|
1403
|
+
if (sel === e._key)
|
|
1404
|
+
tx.post("/api/send", { keyword: pid, msg: d, code: "none" }).catch(() => {});
|
|
1291
1405
|
};
|
|
1292
1406
|
term.onData(fwd);
|
|
1293
1407
|
term.onBinary(fwd);
|
|
1294
1408
|
// Render the existing buffer at the AGENT's current width first so its
|
|
1295
1409
|
// wrapping is correct, instead of forcing our viewport width onto stale
|
|
1296
1410
|
// content. The user adapts to the window by resizing it (fit → push).
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1411
|
+
const selKey = e._key;
|
|
1412
|
+
tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
|
|
1299
1413
|
.then((sz) => {
|
|
1300
|
-
if (sel !==
|
|
1414
|
+
if (sel !== selKey || !term) return;
|
|
1301
1415
|
if (sz && sz.cols && sz.rows) {
|
|
1302
1416
|
adoptingAgentSize = true;
|
|
1303
1417
|
term.resize(sz.cols, sz.rows);
|
|
@@ -1320,8 +1434,8 @@
|
|
|
1320
1434
|
// viewer is pinned to the bottom, and cap the buffer so it can't grow forever.
|
|
1321
1435
|
$("livedot").className = "dot idle";
|
|
1322
1436
|
$("livetxt").textContent = "connecting…";
|
|
1323
|
-
const close =
|
|
1324
|
-
"/api/tail/" + encodeURIComponent(
|
|
1437
|
+
const close = tx.subscribe(
|
|
1438
|
+
"/api/tail/" + encodeURIComponent(pid) + "?raw=1",
|
|
1325
1439
|
(raw) => {
|
|
1326
1440
|
if (term) term.write(raw);
|
|
1327
1441
|
},
|
|
@@ -1339,7 +1453,7 @@
|
|
|
1339
1453
|
|
|
1340
1454
|
$("list").addEventListener("click", (ev) => {
|
|
1341
1455
|
const row = ev.target.closest(".row");
|
|
1342
|
-
if (row) select(row.dataset.
|
|
1456
|
+
if (row) select(row.dataset.key);
|
|
1343
1457
|
});
|
|
1344
1458
|
// Mobile back button: return to the list pane. The tail keeps streaming in the
|
|
1345
1459
|
// background (selection unchanged), so reopening the agent is instant.
|
|
@@ -1370,10 +1484,10 @@
|
|
|
1370
1484
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
1371
1485
|
const shown = entries.filter((e) => matches(e, toks));
|
|
1372
1486
|
if (!shown.length) return;
|
|
1373
|
-
const cur = shown.findIndex((e) =>
|
|
1487
|
+
const cur = shown.findIndex((e) => e._key === sel);
|
|
1374
1488
|
const next = shown[nextIndex(shown.length, cur, dir)];
|
|
1375
|
-
select(
|
|
1376
|
-
const row = $("list").querySelector('.row[data-
|
|
1489
|
+
select(next._key);
|
|
1490
|
+
const row = $("list").querySelector('.row[data-key="' + CSS.escape(next._key) + '"]');
|
|
1377
1491
|
if (row) row.scrollIntoView({ block: "nearest" });
|
|
1378
1492
|
}
|
|
1379
1493
|
// Alt+ArrowDown / Alt+ArrowUp cycles agents. Capture phase on window so it
|
|
@@ -1392,7 +1506,6 @@
|
|
|
1392
1506
|
|
|
1393
1507
|
// ---- rooms: localStorage cache + a manager you open by clicking the badge ----
|
|
1394
1508
|
const ROOMS_KEY = "ay.rooms";
|
|
1395
|
-
let curRoom = null;
|
|
1396
1509
|
const loadRooms = () => {
|
|
1397
1510
|
try {
|
|
1398
1511
|
return JSON.parse(localStorage.getItem(ROOMS_KEY) || "{}");
|
|
@@ -1428,57 +1541,79 @@
|
|
|
1428
1541
|
return "ch-" + (h >>> 0).toString(36).slice(0, 4).padStart(4, "0");
|
|
1429
1542
|
}
|
|
1430
1543
|
|
|
1431
|
-
function
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
try {
|
|
1440
|
-
Conn.ch.close();
|
|
1441
|
-
} catch {}
|
|
1442
|
-
Conn.ch = null;
|
|
1443
|
-
}
|
|
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);
|
|
1444
1552
|
}
|
|
1445
1553
|
|
|
1446
|
-
|
|
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) {
|
|
1447
1559
|
host = host || SIG_DEFAULT;
|
|
1448
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();
|
|
1449
1575
|
try {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
if (s === "closed") setConn("● room lost", "var(--red)");
|
|
1459
|
-
};
|
|
1460
|
-
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
|
+
};
|
|
1461
1584
|
await c.connect();
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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);
|
|
1465
1598
|
}
|
|
1466
|
-
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
const c = new RTCClient(host, room, token);
|
|
1470
|
-
c.onstate = (s) => {
|
|
1471
|
-
if (s === "failed" || s === "closed") setConn("● peer lost", "var(--red)");
|
|
1472
|
-
};
|
|
1473
|
-
try {
|
|
1474
|
-
await c.connect();
|
|
1475
|
-
Conn.rtc = c;
|
|
1599
|
+
s.live = true;
|
|
1476
1600
|
} catch (e) {
|
|
1477
|
-
|
|
1601
|
+
s.live = false;
|
|
1478
1602
|
}
|
|
1603
|
+
s.tried = true;
|
|
1604
|
+
renderRoomsIfOpen();
|
|
1479
1605
|
loadList();
|
|
1480
1606
|
}
|
|
1481
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
|
+
|
|
1482
1617
|
function parseRoomInput(s) {
|
|
1483
1618
|
s = s.trim();
|
|
1484
1619
|
const hash = s.indexOf("#");
|
|
@@ -1493,13 +1628,32 @@
|
|
|
1493
1628
|
return m ? { room: m[1], token: m[2], host: m[3] } : null;
|
|
1494
1629
|
}
|
|
1495
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
|
+
|
|
1496
1649
|
function renderRooms() {
|
|
1497
1650
|
const r = loadRooms();
|
|
1498
1651
|
const names = Object.keys(r).sort((a, b) => r[b].ts - r[a].ts);
|
|
1499
1652
|
const items = names.length
|
|
1500
1653
|
? names
|
|
1501
1654
|
.map(
|
|
1502
|
-
(n) => `<div class="ritem ${n
|
|
1655
|
+
(n) => `<div class="ritem ${sources.get(n)?.live ? "cur" : ""}">
|
|
1656
|
+
${roomStatus(n)}
|
|
1503
1657
|
<span class="rname" data-room="${esc(n)}">${esc(n)}</span>
|
|
1504
1658
|
<span class="rhost">${esc(r[n].host)}</span>
|
|
1505
1659
|
<span class="rx" data-del="${esc(n)}" title="forget">✕</span></div>`,
|
|
@@ -1529,17 +1683,26 @@
|
|
|
1529
1683
|
$("rooms").addEventListener("click", (ev) => {
|
|
1530
1684
|
const name = ev.target.closest(".rname");
|
|
1531
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.
|
|
1532
1688
|
const r = loadRooms()[name.dataset.room];
|
|
1533
1689
|
if (r) {
|
|
1534
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();
|
|
1535
1696
|
$("rooms").style.display = "none";
|
|
1536
1697
|
}
|
|
1537
1698
|
return;
|
|
1538
1699
|
}
|
|
1539
1700
|
const del = ev.target.closest(".rx");
|
|
1540
1701
|
if (del) {
|
|
1702
|
+
removeSource(del.dataset.del);
|
|
1541
1703
|
dropRoom(del.dataset.del);
|
|
1542
1704
|
renderRooms();
|
|
1705
|
+
loadList();
|
|
1543
1706
|
return;
|
|
1544
1707
|
}
|
|
1545
1708
|
if (ev.target.id === "roomadd") {
|
|
@@ -1587,16 +1750,33 @@
|
|
|
1587
1750
|
$("launch").style.display = "flex";
|
|
1588
1751
|
}
|
|
1589
1752
|
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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) {
|
|
1595
1770
|
await loadList();
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
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", {
|
|
1600
1780
|
cli: spec.cli || "claude",
|
|
1601
1781
|
cwd: spec.cwd || undefined,
|
|
1602
1782
|
prompt: spec.prompt || undefined,
|
|
@@ -1608,10 +1788,10 @@
|
|
|
1608
1788
|
for (let i = 0; i < 14; i++) {
|
|
1609
1789
|
await loadList();
|
|
1610
1790
|
const fresh = entries
|
|
1611
|
-
.filter((e) => !before.has(e.pid))
|
|
1791
|
+
.filter((e) => e._room === target.id && !before.has(e.pid))
|
|
1612
1792
|
.sort((a, b) => (b.started_at || 0) - (a.started_at || 0));
|
|
1613
1793
|
if (fresh.length) {
|
|
1614
|
-
select(fresh[0].
|
|
1794
|
+
select(fresh[0]._key);
|
|
1615
1795
|
return true;
|
|
1616
1796
|
}
|
|
1617
1797
|
await new Promise((r) => setTimeout(r, 800));
|
|
@@ -1624,7 +1804,7 @@
|
|
|
1624
1804
|
if (!r) return;
|
|
1625
1805
|
$("launch").style.display = "none";
|
|
1626
1806
|
await connectRoom(room, r.token, r.host);
|
|
1627
|
-
await spawnAndSelect(spec);
|
|
1807
|
+
await spawnAndSelect(spec, room);
|
|
1628
1808
|
}
|
|
1629
1809
|
|
|
1630
1810
|
$("launch").addEventListener("click", (ev) => {
|
|
@@ -1641,9 +1821,11 @@
|
|
|
1641
1821
|
// when there is one, prompt optional) → POST /api/spawn on this connection.
|
|
1642
1822
|
// Always allowed: the console already controls every running agent's stdin.
|
|
1643
1823
|
function showNew() {
|
|
1644
|
-
const here = entries.find((x) =>
|
|
1824
|
+
const here = entries.find((x) => x._key === sel);
|
|
1645
1825
|
const cwd = here?.cwd || "";
|
|
1646
|
-
const
|
|
1826
|
+
const target = spawnTarget();
|
|
1827
|
+
const where = target ? (target.id === LOCAL ? "local" : target.id) : "local";
|
|
1828
|
+
$("newform").dataset.room = target ? target.id : "";
|
|
1647
1829
|
$("newform").innerHTML = `<div class="lcard">
|
|
1648
1830
|
<div class="ltitle">New agent · ${esc(where)}</div>
|
|
1649
1831
|
<div class="nfield"><label>CLI</label><input id="nf-cli" value="claude" spellcheck="false" autocapitalize="off" /></div>
|
|
@@ -1668,7 +1850,7 @@
|
|
|
1668
1850
|
};
|
|
1669
1851
|
go.disabled = true;
|
|
1670
1852
|
go.textContent = "launching…";
|
|
1671
|
-
const ok = await spawnAndSelect(spec);
|
|
1853
|
+
const ok = await spawnAndSelect(spec, $("newform").dataset.room || undefined);
|
|
1672
1854
|
if (ok) {
|
|
1673
1855
|
$("newform").style.display = "none";
|
|
1674
1856
|
} else {
|
|
@@ -1694,6 +1876,7 @@
|
|
|
1694
1876
|
// boot: a launch URL opens the launcher; otherwise connect from the hash (then
|
|
1695
1877
|
// eat the token); a bare #room reconnects from the cached token; else local.
|
|
1696
1878
|
async function boot() {
|
|
1879
|
+
ensureLocalSource();
|
|
1697
1880
|
const raw = location.hash.replace(/^#/, "");
|
|
1698
1881
|
if (raw.startsWith("launch=")) {
|
|
1699
1882
|
let spec = null;
|
|
@@ -1702,8 +1885,8 @@
|
|
|
1702
1885
|
} catch {}
|
|
1703
1886
|
history.replaceState(null, document.title, location.pathname + location.search); // eat launch params
|
|
1704
1887
|
if (spec) showLaunch(spec);
|
|
1705
|
-
setConn("● local", "var(--muted)");
|
|
1706
1888
|
startPolling();
|
|
1889
|
+
connectAllRooms();
|
|
1707
1890
|
return;
|
|
1708
1891
|
}
|
|
1709
1892
|
// #k=<token> — local-mode auth from `ay serve --http`'s printed link.
|
|
@@ -1714,8 +1897,9 @@
|
|
|
1714
1897
|
} catch {}
|
|
1715
1898
|
// SECURITY: strip the token from the URL immediately.
|
|
1716
1899
|
history.replaceState(null, document.title, location.pathname + location.search);
|
|
1717
|
-
|
|
1900
|
+
ensureLocalSource();
|
|
1718
1901
|
startPolling();
|
|
1902
|
+
connectAllRooms();
|
|
1719
1903
|
return;
|
|
1720
1904
|
}
|
|
1721
1905
|
const h = decodeURIComponent(raw);
|
|
@@ -1749,23 +1933,13 @@
|
|
|
1749
1933
|
} else if (bare && loadRooms()[bare[1]]) {
|
|
1750
1934
|
const r = loadRooms()[bare[1]];
|
|
1751
1935
|
pending = { room: bare[1], token: r.token, host: r.host };
|
|
1752
|
-
} else if (!raw) {
|
|
1753
|
-
// No hash → reconnect to the last-used room (or the most recent saved
|
|
1754
|
-
// one), so opening agent-yes.com brings back your list automatically.
|
|
1755
|
-
const rooms = loadRooms();
|
|
1756
|
-
const names = Object.keys(rooms);
|
|
1757
|
-
if (names.length) {
|
|
1758
|
-
const last = localStorage.getItem("ay.lastRoom");
|
|
1759
|
-
const pick =
|
|
1760
|
-
last && rooms[last] ? last : names.sort((a, b) => rooms[b].ts - rooms[a].ts)[0];
|
|
1761
|
-
pending = { room: pick, token: rooms[pick].token, host: rooms[pick].host };
|
|
1762
|
-
}
|
|
1763
1936
|
}
|
|
1764
|
-
// Render the UI immediately and refresh on a timer; connect to
|
|
1765
|
-
//
|
|
1766
|
-
|
|
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.
|
|
1767
1940
|
startPolling();
|
|
1768
1941
|
if (pending) connectRoom(pending.room, pending.token, pending.host);
|
|
1942
|
+
connectAllRooms();
|
|
1769
1943
|
}
|
|
1770
1944
|
|
|
1771
1945
|
// ---- activity-gated polling + auto-reload on new deploy ----------------
|
package/package.json
CHANGED
|
@@ -57,7 +57,10 @@ describe("workspaceConfig", () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it("absolute path → used as-is", () => {
|
|
60
|
-
|
|
60
|
+
// path.resolve keeps a POSIX-absolute path on Unix but anchors it to the
|
|
61
|
+
// current drive on Windows (→ C:\tmp\elsewhere); compare against the same
|
|
62
|
+
// resolution the impl uses so the assertion holds on both (cf. "a/b" below).
|
|
63
|
+
expect(resolveSpawnCwd("/tmp/elsewhere")).toBe(path.resolve("/tmp/elsewhere"));
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
it("tilde path → home-based", () => {
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-D_sIq4Yv.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-osmGP6ly.js";
|
|
4
|
-
import "./pidStore-DBjlqzo8.js";
|
|
5
|
-
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BzuJvKuH.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|