agent-yes 1.169.1 → 1.171.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SUPPORTED_CLIS-BMExykB1.js +8 -0
- package/dist/{SUPPORTED_CLIS-Bwg9VqLI.js → SUPPORTED_CLIS-D6MArV7A.js} +2 -2
- package/dist/{agent-yes.config-Bl08kMpl.js → agent-yes.config-ugPYTPOd.js} +2 -2
- package/dist/{agentYesHome-_eJa3DaX.js → agentYesHome-CtHb5b71.js} +1 -1
- package/dist/cli.js +10 -212
- package/dist/{configShared-CEyhl0WH.js → configShared-0MnIQ652.js} +1 -1
- package/dist/{e2e-BeKjLhmO.js → e2e-jb0Hp43q.js} +1 -1
- package/dist/{forkNested-UCKPEgSI.js → forkNested-DhJxa4q4.js} +2 -2
- package/dist/{globalPidIndex-DlmmJlO8.js → globalPidIndex-CoNr7tS8.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/notifyDaemon-BlGV0y4j.js +591 -0
- package/dist/{openBrowser-CCF1iuQK.js → openBrowser-ChR4llYa.js} +1 -1
- package/dist/{pidStore-BfoBWUjc.js → pidStore-BIvsBQ8X.js} +3 -3
- package/dist/pidStore-CLxwp0qI.js +5 -0
- package/dist/{reaper-C-eWAxIj.js → reaper-CWF2_ATd.js} +27 -3
- package/dist/{reaper-BQgDb85W.js → reaper-jfSmog15.js} +1 -1
- package/dist/{remotes-cx_GDFPj.js → remotes-BQMr4_En.js} +3 -3
- package/dist/{remotes-BmAPylU_.js → remotes-CC-4GuJb.js} +3 -3
- package/dist/{runningLock-BobVW1_A.js → runningLock-CNMl13dC.js} +1 -1
- package/dist/rustBinary-mYbyV0SD.js +228 -0
- package/dist/{schedule-CtVvSyaN.js → schedule-C5nUV0GO.js} +7 -7
- package/dist/{serve-CLmk5ujF.js → serve-ChcayCC3.js} +122 -26
- package/dist/{setup-CYbXOcpH.js → setup-CCQZ9tPB.js} +3 -3
- package/dist/{share-D_e2Fwiy.js → share-QregR8a_.js} +2 -2
- package/dist/spawnGate-BFhva-2F.js +5 -0
- package/dist/{spawnGate-DzPfa1PZ.js → spawnGate-IJDByl2U.js} +3 -3
- package/dist/subcommands-CIk1SSdW.js +9 -0
- package/dist/{subcommands--EUush_X.js → subcommands-CtWxwydR.js} +1049 -44
- package/dist/{tray-CWUpaZF4.js → tray-DXr7iK3E.js} +2 -2
- package/dist/{ts-Bbhf5JbN.js → ts-WHYKp13w.js} +8 -8
- package/dist/{versionChecker-BmWkWhcJ.js → versionChecker-BoPKPL9B.js} +2 -2
- package/dist/{webrtcLink-BG0Xc4-W.js → webrtcLink-B7REGtK2.js} +2 -2
- package/dist/{webrtcRemote-SybKurg9.js → webrtcRemote-Bx-eD_0I.js} +3 -3
- package/dist/{workspaceConfig-BC03X4Y1.js → workspaceConfig-BgqK-31W.js} +37 -6
- package/lab/ui/console-logic.js +125 -10
- package/lab/ui/index.html +340 -11
- package/lab/ui/landing.html +11 -1
- package/package.json +1 -1
- package/scripts/build-rs.ts +9 -5
- package/ts/badges.spec.ts +51 -1
- package/ts/badges.ts +35 -1
- package/ts/notifyDaemon.spec.ts +276 -0
- package/ts/notifyDaemon.ts +698 -0
- package/ts/notifyInbox.spec.ts +192 -0
- package/ts/notifyInbox.ts +294 -0
- package/ts/notifyRouter.spec.ts +355 -0
- package/ts/notifyRouter.ts +288 -0
- package/ts/notifyStore.spec.ts +308 -0
- package/ts/notifyStore.ts +439 -0
- package/ts/reaper.spec.ts +18 -1
- package/ts/reaper.ts +33 -1
- package/ts/rustBinary.ts +30 -0
- package/ts/serve.ts +194 -14
- package/ts/subcommands.spec.ts +341 -0
- package/ts/subcommands.ts +764 -29
- package/ts/workspaceConfig.spec.ts +48 -0
- package/ts/workspaceConfig.ts +46 -3
- package/dist/SUPPORTED_CLIS-DiL99G3j.js +0 -8
- package/dist/pidStore-5qyLX3Fh.js +0 -5
- package/dist/spawnGate-CQ1Il3Xk.js +0 -5
- package/dist/subcommands-1ZcaHYkb.js +0 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as getRunningAgentCount } from "./runningLock-
|
|
1
|
+
import { n as getRunningAgentCount } from "./runningLock-CNMl13dC.js";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
4
4
|
import { homedir } from "os";
|
|
@@ -187,4 +187,4 @@ async function startTray() {
|
|
|
187
187
|
|
|
188
188
|
//#endregion
|
|
189
189
|
export { ensureTray, startTray };
|
|
190
|
-
//# sourceMappingURL=tray-
|
|
190
|
+
//# sourceMappingURL=tray-DXr7iK3E.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
3
|
-
import { t as agentYesHome } from "./agentYesHome-
|
|
4
|
-
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-
|
|
5
|
-
import { t as PidStore } from "./pidStore-
|
|
6
|
-
import { i as readGlobalPids } from "./globalPidIndex-
|
|
7
|
-
import { n as register, r as sweep } from "./reaper-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-BoPKPL9B.js";
|
|
3
|
+
import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
|
|
4
|
+
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CNMl13dC.js";
|
|
5
|
+
import { t as PidStore } from "./pidStore-BIvsBQ8X.js";
|
|
6
|
+
import { i as readGlobalPids } from "./globalPidIndex-CoNr7tS8.js";
|
|
7
|
+
import { n as register, r as sweep } from "./reaper-CWF2_ATd.js";
|
|
8
8
|
import { arch, platform } from "process";
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
10
|
import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync } from "fs";
|
|
@@ -1084,7 +1084,7 @@ async function notifyWebhook(status, details, cwd = process.cwd()) {
|
|
|
1084
1084
|
|
|
1085
1085
|
//#endregion
|
|
1086
1086
|
//#region ts/index.ts
|
|
1087
|
-
const config = await import("./agent-yes.config-
|
|
1087
|
+
const config = await import("./agent-yes.config-ugPYTPOd.js").then((mod) => mod.default || mod);
|
|
1088
1088
|
const CLIS_CONFIG = config.clis;
|
|
1089
1089
|
/**
|
|
1090
1090
|
* Main function to run agent-cli with automatic yes/no responses
|
|
@@ -1824,4 +1824,4 @@ function sleep(ms) {
|
|
|
1824
1824
|
|
|
1825
1825
|
//#endregion
|
|
1826
1826
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1827
|
-
//# sourceMappingURL=ts-
|
|
1827
|
+
//# sourceMappingURL=ts-WHYKp13w.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.171.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-BoPKPL9B.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { u as parseSecret } from "./e2e-
|
|
1
|
+
import { u as parseSecret } from "./e2e-jb0Hp43q.js";
|
|
2
2
|
|
|
3
3
|
//#region ts/webrtcLink.ts
|
|
4
4
|
const SIGNAL_SUBPROTOCOL = "ay-signal-1";
|
|
@@ -42,4 +42,4 @@ function isWebrtcSpec(spec) {
|
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
export { isWebrtcSpec as n, parseWebrtcLink as r, SIGNAL_SUBPROTOCOL as t };
|
|
45
|
-
//# sourceMappingURL=webrtcLink-
|
|
45
|
+
//# sourceMappingURL=webrtcLink-B7REGtK2.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-
|
|
2
|
-
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-
|
|
1
|
+
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-jb0Hp43q.js";
|
|
2
|
+
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-B7REGtK2.js";
|
|
3
3
|
import { RTCPeerConnection } from "node-datachannel/polyfill";
|
|
4
4
|
|
|
5
5
|
//#region ts/webrtcRemote.ts
|
|
@@ -256,4 +256,4 @@ async function startWebrtcBridge(link) {
|
|
|
256
256
|
|
|
257
257
|
//#endregion
|
|
258
258
|
export { startWebrtcBridge };
|
|
259
|
-
//# sourceMappingURL=webrtcRemote-
|
|
259
|
+
//# sourceMappingURL=webrtcRemote-Bx-eD_0I.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as agentYesHome } from "./agentYesHome-
|
|
1
|
+
import { t as agentYesHome } from "./agentYesHome-CtHb5b71.js";
|
|
2
2
|
import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import path from "path";
|
|
@@ -80,8 +80,17 @@ function isProvisionAllowed(owner, repo) {
|
|
|
80
80
|
function getSpawnHook() {
|
|
81
81
|
const env = process.env.AGENT_YES_SPAWN_HOOK;
|
|
82
82
|
if (env && env.trim()) return env;
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
return configHookIfTrusted(readConfig().spawnHook);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Return a config-file hook value only when the config file is safe to trust as
|
|
87
|
+
* a source of code to run: not a symlink (can't be swapped out from under us),
|
|
88
|
+
* owned by us, and not group/world-writable (no other user can rewrite it). The
|
|
89
|
+
* env-var forms of these hooks bypass this — they come from the daemon's own
|
|
90
|
+
* environment, which is already trusted. Returns null when unset or guarded out.
|
|
91
|
+
*/
|
|
92
|
+
function configHookIfTrusted(value) {
|
|
93
|
+
if (!value || !value.trim()) return null;
|
|
85
94
|
if (process.platform !== "win32") try {
|
|
86
95
|
if (lstatSync(configPath()).isSymbolicLink()) return null;
|
|
87
96
|
const st = statSync(configPath());
|
|
@@ -90,13 +99,35 @@ function getSpawnHook() {
|
|
|
90
99
|
} catch {
|
|
91
100
|
return null;
|
|
92
101
|
}
|
|
93
|
-
return
|
|
102
|
+
return value;
|
|
94
103
|
}
|
|
95
104
|
/** Whether a usable {@link getSpawnHook} is configured (for read-only disclosure). */
|
|
96
105
|
function hasSpawnHook() {
|
|
97
106
|
return getSpawnHook() !== null;
|
|
98
107
|
}
|
|
99
108
|
/**
|
|
109
|
+
* A trusted, HOST-LOCAL shell hook run BEFORE a `fork`/`from` provisioning git
|
|
110
|
+
* op — "koho-style" (codehost-style) provisioning. Env `AGENT_YES_PROVISION_HOOK`
|
|
111
|
+
* overrides the config `provisionHook`; the config form is subject to the same
|
|
112
|
+
* tamper guard as {@link getSpawnHook} ({@link configHookIfTrusted}).
|
|
113
|
+
*
|
|
114
|
+
* Its purpose is to prepare the host for provisioning — most usefully to select
|
|
115
|
+
* the right git identity (e.g. `gh auth switch --user <who>`, keyed on the
|
|
116
|
+
* `KOHO_OWNER`/`KOHO_REPO` env the caller exports) before the clone/worktree/
|
|
117
|
+
* setup runs. When configured it is ALSO the provisioning gate: its exit code
|
|
118
|
+
* decides admission (0 = allow, non-zero = deny), overriding
|
|
119
|
+
* {@link getProvisionAllowlist}. Returns null when unset/guarded.
|
|
120
|
+
*/
|
|
121
|
+
function getProvisionHook() {
|
|
122
|
+
const env = process.env.AGENT_YES_PROVISION_HOOK;
|
|
123
|
+
if (env && env.trim()) return env;
|
|
124
|
+
return configHookIfTrusted(readConfig().provisionHook);
|
|
125
|
+
}
|
|
126
|
+
/** Whether a usable {@link getProvisionHook} is configured (for read-only disclosure). */
|
|
127
|
+
function hasProvisionHook() {
|
|
128
|
+
return getProvisionHook() !== null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
100
131
|
* Cap on concurrently-live agents admitted via `/api/spawn`. Env
|
|
101
132
|
* `AGENT_YES_MAX_AGENTS` overrides the config `maxAgents`. A non-positive,
|
|
102
133
|
* missing, or unparseable value means **unlimited** (returns undefined), which
|
|
@@ -161,5 +192,5 @@ function resolveSpawnCwd(input) {
|
|
|
161
192
|
}
|
|
162
193
|
|
|
163
194
|
//#endregion
|
|
164
|
-
export {
|
|
165
|
-
//# sourceMappingURL=workspaceConfig-
|
|
195
|
+
export { getSpawnHook as a, hasProvisionHook as c, resolveSpawnCwd as d, setWorkspaceRoot as f, getProvisionRoot as i, hasSpawnHook as l, getMinFreeMb as n, getSpawnWaitMs as o, getProvisionHook as r, getWorkspaceRoot as s, getMaxAgents as t, isProvisionAllowed as u };
|
|
196
|
+
//# sourceMappingURL=workspaceConfig-BgqK-31W.js.map
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -187,6 +187,18 @@ export function taskLabel(e) {
|
|
|
187
187
|
// id), so a newly-added server-side badge is never silently dropped.
|
|
188
188
|
export const BADGE_META = {
|
|
189
189
|
"goal-active": { label: "goal", title: "A /goal Stop-hook loop is active on this agent" },
|
|
190
|
+
"session-limit": {
|
|
191
|
+
label: "limit",
|
|
192
|
+
title: "Usage session limit hit — waiting for the reset time shown on screen",
|
|
193
|
+
},
|
|
194
|
+
retrying: {
|
|
195
|
+
label: "retry",
|
|
196
|
+
title: "Waiting for the API — the CLI is auto-retrying on its own backoff (no action needed)",
|
|
197
|
+
},
|
|
198
|
+
typing: {
|
|
199
|
+
label: "typing",
|
|
200
|
+
title: "The user is typing at this agent's terminal — ay send backs off until they pause",
|
|
201
|
+
},
|
|
190
202
|
};
|
|
191
203
|
|
|
192
204
|
// Status-flag chips ("badges") matched against the agent's screen — e.g. an
|
|
@@ -196,11 +208,16 @@ export function badgesFor(e) {
|
|
|
196
208
|
return (e.badges || []).map((id) => ({ id, ...(BADGE_META[id] || { label: id, title: id }) }));
|
|
197
209
|
}
|
|
198
210
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
211
|
+
// Time since the agent was last active ("12s" / "5m" / "3h") — measured from
|
|
212
|
+
// its last stdout write (last_active_at, the log file's mtime), so a long-lived
|
|
213
|
+
// but quiet agent reads as stale rather than "new". Falls back to started_at
|
|
214
|
+
// when the server hasn't stamped a last-active time (e.g. freshly spawned, no
|
|
215
|
+
// log yet). `now` is injectable so tests don't depend on the wall clock; the
|
|
216
|
+
// browser calls age(e) and gets Date.now().
|
|
201
217
|
export function age(e, now = Date.now()) {
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
const at = e.last_active_at ?? e.started_at;
|
|
219
|
+
if (!at) return "";
|
|
220
|
+
const s = Math.max(0, (now - at) / 1000);
|
|
204
221
|
if (s < 60) return Math.floor(s) + "s";
|
|
205
222
|
if (s < 3600) return Math.floor(s / 60) + "m";
|
|
206
223
|
return Math.floor(s / 3600) + "h";
|
|
@@ -331,7 +348,7 @@ function agentForestNodes(list) {
|
|
|
331
348
|
// flat entry list BEFORE layeredRows builds the room/peer/agent tree (which keeps
|
|
332
349
|
// the given order for siblings and first-seen order for room/peer groups). So
|
|
333
350
|
// sorting reorders roots and siblings without breaking the nesting.
|
|
334
|
-
export const SORT_MODES = ["state", "created", "identity"];
|
|
351
|
+
export const SORT_MODES = ["state", "active", "created", "identity"];
|
|
335
352
|
|
|
336
353
|
// Attention-first state ranking: someone scanning the fleet wants the agents that
|
|
337
354
|
// need them (needs_input) up top, then the wedged ones (stuck), then live work,
|
|
@@ -358,19 +375,29 @@ function gitWeight(e) {
|
|
|
358
375
|
return (g.changed || 0) + (g.ahead || 0) + (g.behind || 0) + (g.dirty ? 0.5 : 0);
|
|
359
376
|
}
|
|
360
377
|
|
|
378
|
+
// Last-active instant for an entry: its last stdout write (last_active_at),
|
|
379
|
+
// falling back to started_at when the server hasn't stamped one yet.
|
|
380
|
+
function lastActive(e) {
|
|
381
|
+
return e.last_active_at ?? e.started_at ?? 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
361
384
|
// Return a NEW array sorted for display per `mode` (default "state"):
|
|
362
385
|
// - "state": attention-first state, then git busyness, then newest.
|
|
386
|
+
// - "active": most recently active first (last_active_at desc).
|
|
363
387
|
// - "created": newest first (started_at desc).
|
|
364
388
|
// - "identity": user@host:owner/repo/branch (alphabetical).
|
|
365
389
|
// Every comparator falls back to newest-first so order is total & deterministic.
|
|
366
390
|
export function sortEntries(entries, mode = "state") {
|
|
367
391
|
const byNewest = (a, b) => (b.started_at || 0) - (a.started_at || 0);
|
|
392
|
+
const byActive = (a, b) => lastActive(b) - lastActive(a) || byNewest(a, b);
|
|
368
393
|
const cmp =
|
|
369
|
-
mode === "
|
|
370
|
-
?
|
|
371
|
-
: mode === "
|
|
372
|
-
?
|
|
373
|
-
:
|
|
394
|
+
mode === "active"
|
|
395
|
+
? byActive
|
|
396
|
+
: mode === "created"
|
|
397
|
+
? byNewest
|
|
398
|
+
: mode === "identity"
|
|
399
|
+
? (a, b) => fullIdent(a).localeCompare(fullIdent(b)) || byNewest(a, b)
|
|
400
|
+
: (a, b) => stateRank(a) - stateRank(b) || gitWeight(b) - gitWeight(a) || byNewest(a, b);
|
|
374
401
|
return entries.slice().sort(cmp);
|
|
375
402
|
}
|
|
376
403
|
|
|
@@ -465,6 +492,48 @@ function toAgentNode(n) {
|
|
|
465
492
|
return { kind: "agent", entry: n.entry, children: n.children.map(toAgentNode) };
|
|
466
493
|
}
|
|
467
494
|
|
|
495
|
+
// When the subagent trees are folded away, each surviving root agent shows a
|
|
496
|
+
// summary chip of the descendants hidden beneath it. Given the FULL layeredRows
|
|
497
|
+
// output, returns a Map keyed by the root agent's `entry` →
|
|
498
|
+
// { total, working, lastActive }:
|
|
499
|
+
// total = alive (non-exited) descendant subagents, at every depth
|
|
500
|
+
// working = those whose status is "active"
|
|
501
|
+
// lastActive = newest last_active_at (fallback started_at) among the alive
|
|
502
|
+
// descendants, or null when none is stamped
|
|
503
|
+
// Descendants roll up to their nearest folded-away ancestor's root, so a folded
|
|
504
|
+
// tree summarizes its whole subtree in one chip. Roots with no alive descendant
|
|
505
|
+
// aren't in the map (no chip). Pure so tests can exercise it without a DOM.
|
|
506
|
+
export function foldSummaries(rows) {
|
|
507
|
+
// Direct parent-agent entry → child agent entries (skip room/peer headers).
|
|
508
|
+
const kids = new Map();
|
|
509
|
+
for (const r of rows) {
|
|
510
|
+
if (r.kind !== "agent" || !r.parentEntry) continue;
|
|
511
|
+
const arr = kids.get(r.parentEntry) || [];
|
|
512
|
+
arr.push(r.entry);
|
|
513
|
+
kids.set(r.parentEntry, arr);
|
|
514
|
+
}
|
|
515
|
+
const summaries = new Map();
|
|
516
|
+
for (const r of rows) {
|
|
517
|
+
if (r.kind !== "agent" || r.parentEntry) continue; // roots only
|
|
518
|
+
const acc = { total: 0, working: 0, lastActive: null };
|
|
519
|
+
const stack = [...(kids.get(r.entry) || [])];
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
while (stack.length) {
|
|
522
|
+
const e = stack.pop();
|
|
523
|
+
if (seen.has(e)) continue; // guard a pathological parent cycle
|
|
524
|
+
seen.add(e);
|
|
525
|
+
for (const c of kids.get(e) || []) stack.push(c);
|
|
526
|
+
if (e.status === "exited") continue;
|
|
527
|
+
acc.total++;
|
|
528
|
+
if (e.status === "active") acc.working++;
|
|
529
|
+
const at = e.last_active_at ?? e.started_at;
|
|
530
|
+
if (at != null && (acc.lastActive == null || at > acc.lastActive)) acc.lastActive = at;
|
|
531
|
+
}
|
|
532
|
+
if (acc.total > 0) summaries.set(r.entry, acc);
|
|
533
|
+
}
|
|
534
|
+
return summaries;
|
|
535
|
+
}
|
|
536
|
+
|
|
468
537
|
// Next selection index when stepping the list by `dir` (+1 down / -1 up).
|
|
469
538
|
// No current selection (i<0) lands on the first (down) or last (up) row;
|
|
470
539
|
// otherwise clamps at the ends. Returns -1 for an empty list.
|
|
@@ -480,6 +549,52 @@ export function nextIndex(len, i, dir) {
|
|
|
480
549
|
// coordinate math lives here so it's unit-testable without a browser.
|
|
481
550
|
// ---------------------------------------------------------------------------
|
|
482
551
|
|
|
552
|
+
// ---- collaborative presence: peer focus + stdin flash ----------------------
|
|
553
|
+
// The left panel colour-codes who's on which agent: your own selection is blue
|
|
554
|
+
// (the .sel bar), every OTHER human peer's focus is yellow (.peerfocus), and a
|
|
555
|
+
// row pulses when its stdin was just written — independent of focus, since an
|
|
556
|
+
// `ay send` feeds an agent no one is looking at. These pure helpers own the
|
|
557
|
+
// bookkeeping so index.html only measures the DOM.
|
|
558
|
+
|
|
559
|
+
// Detect agents whose stdin just advanced. `seen` maps _key -> the last
|
|
560
|
+
// last_stdin_at we observed; given the current entries, return the _keys whose
|
|
561
|
+
// last_stdin_at is NEWER than what we'd seen (someone typed / `ay send` pushed),
|
|
562
|
+
// and fold the new values into `seen`. A key seen for the FIRST time never
|
|
563
|
+
// flashes (else every agent would pulse on initial load) — only a later bump
|
|
564
|
+
// does. Keys no longer present are pruned so `seen` can't grow without bound.
|
|
565
|
+
export function advanceStdinFlashes(entries, seen) {
|
|
566
|
+
const fresh = [];
|
|
567
|
+
const alive = new Set();
|
|
568
|
+
for (const e of entries) {
|
|
569
|
+
const key = e._key;
|
|
570
|
+
if (!key) continue;
|
|
571
|
+
alive.add(key);
|
|
572
|
+
const at = e.last_stdin_at;
|
|
573
|
+
if (typeof at !== "number") continue;
|
|
574
|
+
const prev = seen.get(key);
|
|
575
|
+
if (prev !== undefined && at > prev) fresh.push(key);
|
|
576
|
+
if (prev === undefined || at > prev) seen.set(key, at);
|
|
577
|
+
}
|
|
578
|
+
for (const key of [...seen.keys()]) if (!alive.has(key)) seen.delete(key);
|
|
579
|
+
return fresh;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Summarize fleet presence for the peers badge + per-row chips. `records` is one
|
|
583
|
+
// entry per OTHER viewer currently watching an agent: { key, viewer } (key = the
|
|
584
|
+
// composite _key of the agent they're on). Returns { total, byKey }: `total`
|
|
585
|
+
// distinct viewers (the "N peers" badge), `byKey` a Map of _key -> how many
|
|
586
|
+
// peers watch that agent (the yellow per-row chip + .peerfocus trigger).
|
|
587
|
+
export function focusSummary(records) {
|
|
588
|
+
const viewers = new Set();
|
|
589
|
+
const byKey = new Map();
|
|
590
|
+
for (const r of records) {
|
|
591
|
+
if (!r || !r.key || !r.viewer) continue;
|
|
592
|
+
viewers.add(r.viewer);
|
|
593
|
+
byKey.set(r.key, (byKey.get(r.key) || 0) + 1);
|
|
594
|
+
}
|
|
595
|
+
return { total: viewers.size, byKey };
|
|
596
|
+
}
|
|
597
|
+
|
|
483
598
|
// Stable per-viewer hue (0..359) for colour-coding peers' selections.
|
|
484
599
|
export function hashHue(s) {
|
|
485
600
|
let h = 0;
|