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.
- package/dist/SUPPORTED_CLIS-CGb1oNZd.js +8 -0
- package/dist/{SUPPORTED_CLIS-CcreSTgo.js → SUPPORTED_CLIS-Dh1CAhi9.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-BrT33v_v.js → serve-BmM1r9No.js} +5 -5
- package/dist/{subcommands-DHPQVuWd.js → subcommands-D3b7vKgY.js} +2 -2
- package/dist/{subcommands-BIz8LzhF.js → subcommands-D4eVn3eY.js} +1 -1
- package/dist/{ts-DQiKBC0f.js → ts--IvDnRaR.js} +2 -2
- package/dist/{versionChecker-BQULRcxP.js → versionChecker-DSdFfx6l.js} +2 -2
- package/lab/ui/console-logic.js +83 -0
- package/lab/ui/index.html +368 -139
- package/package.json +3 -2
- package/dist/SUPPORTED_CLIS-atYBMnHG.js +0 -8
|
@@ -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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
1
|
+
import "./ts--IvDnRaR.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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-
|
|
224
|
+
//# sourceMappingURL=versionChecker-DSdFfx6l.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
|
@@ -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:
|
|
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
|
|
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) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
@@ -1073,20 +1156,14 @@
|
|
|
1073
1156
|
: path;
|
|
1074
1157
|
};
|
|
1075
1158
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
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
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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 =
|
|
1287
|
+
const id = compactIdent(e, ctx);
|
|
1140
1288
|
const cli = cliLabel(e);
|
|
1141
|
-
return `<div class="row crow ${
|
|
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(
|
|
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 ${
|
|
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
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
setConn(
|
|
1211
|
-
}
|
|
1212
|
-
setConn(
|
|
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) =>
|
|
1216
|
-
const
|
|
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(
|
|
1385
|
+
select(e._key);
|
|
1225
1386
|
} else {
|
|
1226
|
-
sel =
|
|
1387
|
+
sel = e._key;
|
|
1227
1388
|
renderList();
|
|
1228
1389
|
}
|
|
1229
1390
|
}
|
|
1230
1391
|
}
|
|
1231
1392
|
|
|
1232
|
-
function select(
|
|
1233
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
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:
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
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
|
|
1307
|
-
|
|
1477
|
+
const selKey = e._key;
|
|
1478
|
+
tx.fetchJSON("/api/size/" + encodeURIComponent(pid))
|
|
1308
1479
|
.then((sz) => {
|
|
1309
|
-
if (sel !==
|
|
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 =
|
|
1333
|
-
"/api/tail/" + encodeURIComponent(
|
|
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.
|
|
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) =>
|
|
1553
|
+
const cur = shown.findIndex((e) => e._key === sel);
|
|
1383
1554
|
const next = shown[nextIndex(shown.length, cur, dir)];
|
|
1384
|
-
select(
|
|
1385
|
-
const row = $("list").querySelector('.row[data-
|
|
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
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
setConn("● connect failed", "var(--red)");
|
|
1662
|
+
s.client = c;
|
|
1663
|
+
s.tx = rtcTx(c);
|
|
1475
1664
|
}
|
|
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;
|
|
1665
|
+
s.live = true;
|
|
1486
1666
|
} catch (e) {
|
|
1487
|
-
|
|
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
|
|
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
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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].
|
|
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) =>
|
|
1890
|
+
const here = entries.find((x) => x._key === sel);
|
|
1655
1891
|
const cwd = here?.cwd || "";
|
|
1656
|
-
const
|
|
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
|
-
|
|
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
|
|
1775
|
-
//
|
|
1776
|
-
|
|
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.
|
|
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 };
|