agent-yes 1.169.1 → 1.170.1
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-Bwg9VqLI.js → SUPPORTED_CLIS-ByppVvWO.js} +2 -2
- package/dist/SUPPORTED_CLIS-C_c097RZ.js +8 -0
- 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-D5jtpCAR.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-DsO0IlNq.js +228 -0
- package/dist/{schedule-CtVvSyaN.js → schedule-ChTPIcXf.js} +7 -7
- package/dist/{serve-CLmk5ujF.js → serve-De1gAWNI.js} +120 -26
- package/dist/{setup-CYbXOcpH.js → setup-nlMqyEPB.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--EUush_X.js → subcommands-BFlpV23b.js} +1049 -44
- package/dist/subcommands-DLDpwrfG.js +9 -0
- package/dist/{tray-CWUpaZF4.js → tray-DXr7iK3E.js} +2 -2
- package/dist/{ts-Bbhf5JbN.js → ts-DsxejviJ.js} +8 -8
- package/dist/{versionChecker-BmWkWhcJ.js → versionChecker-Czk3aHqy.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 +79 -10
- package/lab/ui/index.html +174 -8
- 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 +180 -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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./logger-CDIsZ-Pp.js";
|
|
2
|
+
import "./globalPidIndex-CoNr7tS8.js";
|
|
3
|
+
import "./configShared-0MnIQ652.js";
|
|
4
|
+
import { A as resolveOne, B as writeKeysPaced, C as matchKeyword, D as renderLogTailLines, E as readPtysize, F as snapshotStatus, I as stdinActivityPath, L as stopTipForCli, M as resolveResumeArgs, N as restartHintLines, O as renderRawLog, P as runSubcommand, R as submitAndConfirm, S as listRecords, T as readNotes, V as writeToIpc, _ as isPidAlive, a as cmdHelp, b as isUserTyping, c as deriveLiveState, d as extractMenu, f as extractNeedsInput, g as isExitRequest, h as isAgentStuck, i as backoffWhileTyping, j as resolveReadWindow, k as renderRawLogLines, l as deriveLiveStatus, m as finalizedLines, n as READ_PAGE_DEFAULT, o as controlCodeFromName, p as extractTaskCounts, r as TYPING_WINDOW_MS, s as cursorAbs, t as GRACEFUL_EXIT_COMMANDS, u as extractBadges, v as isSlashCommand, w as menuSelectKeys, x as lastStdinAt, y as isSubcommand, z as waitForLogQuiet } from "./subcommands-BFlpV23b.js";
|
|
5
|
+
import "./e2e-jb0Hp43q.js";
|
|
6
|
+
import "./webrtcLink-B7REGtK2.js";
|
|
7
|
+
import "./remotes-CC-4GuJb.js";
|
|
8
|
+
|
|
9
|
+
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -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-Czk3aHqy.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-DsxejviJ.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.170.1";
|
|
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-Czk3aHqy.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.
|
package/lab/ui/index.html
CHANGED
|
@@ -630,6 +630,10 @@
|
|
|
630
630
|
.omni-spawn .omni-title {
|
|
631
631
|
color: var(--green);
|
|
632
632
|
}
|
|
633
|
+
/* Destructive slash command (/kill) — red title, matching the ⋯-menu Force-kill. */
|
|
634
|
+
.omni-spawn.danger .omni-title {
|
|
635
|
+
color: var(--red);
|
|
636
|
+
}
|
|
633
637
|
.omni-empty {
|
|
634
638
|
padding: 14px;
|
|
635
639
|
color: var(--muted);
|
|
@@ -891,6 +895,26 @@
|
|
|
891
895
|
color: var(--green);
|
|
892
896
|
border-color: #2a3a2a;
|
|
893
897
|
}
|
|
898
|
+
/* Folded-subagent summary chip ("⊞ 3/5" = 3 working of 5 hidden subagents),
|
|
899
|
+
shown on a root agent row when the subagent trees are collapsed. Same pill
|
|
900
|
+
shape as .tasks; the ⊞ glyph echoes the fold toolbar button. Turns accent
|
|
901
|
+
when any hidden subagent is still working, so a collapsed tree with live
|
|
902
|
+
children stands out from a quiet one. Hover for the count + last-active. */
|
|
903
|
+
.subs {
|
|
904
|
+
font-family: var(--mono);
|
|
905
|
+
font-size: 10.5px;
|
|
906
|
+
color: var(--muted);
|
|
907
|
+
white-space: nowrap;
|
|
908
|
+
flex: none;
|
|
909
|
+
padding: 0 5px;
|
|
910
|
+
border: 1px solid var(--line);
|
|
911
|
+
border-radius: 8px;
|
|
912
|
+
cursor: pointer;
|
|
913
|
+
}
|
|
914
|
+
.subs.active {
|
|
915
|
+
color: var(--accent);
|
|
916
|
+
border-color: var(--accent);
|
|
917
|
+
}
|
|
894
918
|
/* Status-flag chip ("badge") matched server-side against the agent's
|
|
895
919
|
screen — e.g. an active /goal loop (ts/badges.ts). Same pill shape as
|
|
896
920
|
.tasks but accent-colored so a matched flag stands out as a distinct
|
|
@@ -1370,6 +1394,7 @@
|
|
|
1370
1394
|
<div class="meta">
|
|
1371
1395
|
<span id="count"></span>
|
|
1372
1396
|
<span class="metaright">
|
|
1397
|
+
<button id="foldbtn" class="viewbtn" title="fold subagent trees">⊞ subs</button>
|
|
1373
1398
|
<button id="sortbtn" class="viewbtn" title="cycle sort order">⇅ state</button>
|
|
1374
1399
|
<button id="viewbtn" class="viewbtn" title="toggle compact list">☰</button>
|
|
1375
1400
|
<button id="newbtn" class="newbtn" title="spawn a new agent on this fleet">
|
|
@@ -1554,6 +1579,7 @@
|
|
|
1554
1579
|
hasIdent,
|
|
1555
1580
|
deviceCount,
|
|
1556
1581
|
layeredRows,
|
|
1582
|
+
foldSummaries,
|
|
1557
1583
|
sortEntries,
|
|
1558
1584
|
SORT_MODES,
|
|
1559
1585
|
taskLabel,
|
|
@@ -2819,8 +2845,10 @@
|
|
|
2819
2845
|
// Force — escalation a stuck agent can't ignore: a real SIGKILL of its process
|
|
2820
2846
|
// GROUP, server-side via POST /api/kill. Use when /exit does nothing.
|
|
2821
2847
|
const GRACEFUL_EXIT = { claude: "/exit", codex: "/exit", gemini: "/quit" };
|
|
2822
|
-
|
|
2823
|
-
|
|
2848
|
+
// `ent` targets a specific agent (the Cmd+K /kill · /restart commands pass the
|
|
2849
|
+
// omnibox anchor); the ⋯-menu buttons omit it and act on the selected agent.
|
|
2850
|
+
async function stopAgent(force, ent) {
|
|
2851
|
+
const e = ent || (sel ? entries.find((x) => x._key === sel) : null);
|
|
2824
2852
|
if (!e) return;
|
|
2825
2853
|
const who = `pid ${e.pid} ${cliLabel(e) || ident(e) || ""}`.trim();
|
|
2826
2854
|
const tx = txFor(e),
|
|
@@ -2843,8 +2871,8 @@
|
|
|
2843
2871
|
// Restart — stop (if live) + relaunch resuming the session, server-side via
|
|
2844
2872
|
// POST /api/restart (the `ay restart` flow). The server runs it detached, so
|
|
2845
2873
|
// it's decoupled from this agent's lifecycle — no prompt typed into the agent.
|
|
2846
|
-
async function restartAgent() {
|
|
2847
|
-
const e = sel ? entries.find((x) => x._key === sel) : null;
|
|
2874
|
+
async function restartAgent(ent) {
|
|
2875
|
+
const e = ent || (sel ? entries.find((x) => x._key === sel) : null);
|
|
2848
2876
|
if (!e) return;
|
|
2849
2877
|
const who = `pid ${e.pid} ${cliLabel(e) || ident(e) || ""}`.trim();
|
|
2850
2878
|
if (!confirm(`Restart ${who}?\nStops it (if live) and relaunches resuming the session.`))
|
|
@@ -3191,6 +3219,14 @@
|
|
|
3191
3219
|
// Compact list: one line per agent (dot + cli + title), persisted per device.
|
|
3192
3220
|
let compactList = localStorage.getItem("ay.compactList") === "1";
|
|
3193
3221
|
|
|
3222
|
+
// Fold subagent trees: hide nested (sub)agent rows and roll each root's
|
|
3223
|
+
// hidden descendants into a summary chip. Folded BY DEFAULT (only "0" opts
|
|
3224
|
+
// out) so a busy fleet reads as a flat list of top-level agents at a glance.
|
|
3225
|
+
let foldSubs = localStorage.getItem("ay.foldSubs") !== "0";
|
|
3226
|
+
// Per-render Map(rootEntry → { total, working, lastActive }); rebuilt by
|
|
3227
|
+
// renderList whenever folding is on, consumed by foldChipHtml.
|
|
3228
|
+
let foldSum = null;
|
|
3229
|
+
|
|
3194
3230
|
// Display sort order, cycled by the sort button and persisted per device.
|
|
3195
3231
|
// Default "state" (attention-first state, then git busyness). Guard against
|
|
3196
3232
|
// a stale/garbage stored value so we never start in an unknown mode.
|
|
@@ -3234,6 +3270,21 @@
|
|
|
3234
3270
|
return `<span class="tasks${allDone ? " done" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
|
|
3235
3271
|
}
|
|
3236
3272
|
|
|
3273
|
+
// Folded-subagent summary chip ("⊞ 3/5") for a root agent whose subagent
|
|
3274
|
+
// tree is collapsed — working/total of its hidden descendants, from the
|
|
3275
|
+
// per-render foldSum map. "" when folding is off or this row has no hidden
|
|
3276
|
+
// subagents. Clicking it expands the trees (see the list click handler).
|
|
3277
|
+
// The tooltip carries the recency the badge face omits ("last active 5s ago").
|
|
3278
|
+
function foldChipHtml(e) {
|
|
3279
|
+
const s = foldSum && foldSum.get(e);
|
|
3280
|
+
if (!s) return "";
|
|
3281
|
+
const rel = s.lastActive != null ? age({ last_active_at: s.lastActive }) : "";
|
|
3282
|
+
const tip =
|
|
3283
|
+
`${s.total} subagent${s.total === 1 ? "" : "s"} folded · ${s.working} working` +
|
|
3284
|
+
(rel ? ` · last active ${rel} ago` : "");
|
|
3285
|
+
return `<span class="subs${s.working ? " active" : ""}" data-fold-chip="1" title="${esc(tip)}">⊞ ${s.working}/${s.total}</span>`;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3237
3288
|
// Status-flag chips ("badges") matched server-side against the agent's
|
|
3238
3289
|
// screen (ts/badges.ts) — e.g. an active /goal loop. One small pill per
|
|
3239
3290
|
// matched flag; "" when none matched. CSS class is "flag" (not "badge")
|
|
@@ -3264,9 +3315,18 @@
|
|
|
3264
3315
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
3265
3316
|
// Build the full signalling-server > rooms > peers > agents > subagents
|
|
3266
3317
|
// tree. Single-node room/peer layers fold away; ≥2 become ├ └ │ branches.
|
|
3267
|
-
const
|
|
3268
|
-
const agentRows =
|
|
3318
|
+
const fullRows = layeredRows(orderedEntries(toks));
|
|
3319
|
+
const agentRows = fullRows.filter((r) => r.kind === "agent");
|
|
3320
|
+
// Fold: summarize each root's hidden descendants, then drop subagent rows
|
|
3321
|
+
// (any agent row with a parent agent). The count still reports every
|
|
3322
|
+
// matching agent, so folding never makes the fleet look smaller.
|
|
3323
|
+
foldSum = foldSubs ? foldSummaries(fullRows) : null;
|
|
3324
|
+
const rows = foldSubs
|
|
3325
|
+
? fullRows.filter((r) => !(r.kind === "agent" && r.parentEntry))
|
|
3326
|
+
: fullRows;
|
|
3269
3327
|
$("count").textContent = `${agentRows.length} / ${entries.length} agents`;
|
|
3328
|
+
$("foldbtn").textContent = foldSubs ? "⊞ subs" : "⊟ subs";
|
|
3329
|
+
$("foldbtn").classList.toggle("on", !foldSubs);
|
|
3270
3330
|
$("viewbtn").classList.toggle("on", compactList);
|
|
3271
3331
|
// Show the active sort mode on the button so it's self-documenting; the
|
|
3272
3332
|
// default ("state") is also highlighted to hint it's the implicit order.
|
|
@@ -3293,6 +3353,7 @@
|
|
|
3293
3353
|
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
3294
3354
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
3295
3355
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
3356
|
+
${foldChipHtml(e)}
|
|
3296
3357
|
${taskChipHtml(e)}
|
|
3297
3358
|
${badgeChipsHtml(e)}
|
|
3298
3359
|
${gitChipHtml(e)}
|
|
@@ -3319,6 +3380,7 @@
|
|
|
3319
3380
|
<div class="r1">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="dot ${esc(e.status)}"></span>
|
|
3320
3381
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
3321
3382
|
<span class="badge">pid ${e.pid}</span>
|
|
3383
|
+
${foldChipHtml(e)}
|
|
3322
3384
|
${taskChipHtml(e)}
|
|
3323
3385
|
${badgeChipsHtml(e)}
|
|
3324
3386
|
${gitChipHtml(e)}
|
|
@@ -3680,6 +3742,14 @@
|
|
|
3680
3742
|
}
|
|
3681
3743
|
|
|
3682
3744
|
$("list").addEventListener("click", (ev) => {
|
|
3745
|
+
// Clicking a folded-subagent chip expands the trees instead of selecting
|
|
3746
|
+
// the row, so the hidden children it summarizes become reachable.
|
|
3747
|
+
if (ev.target.closest("[data-fold-chip]")) {
|
|
3748
|
+
foldSubs = false;
|
|
3749
|
+
localStorage.setItem("ay.foldSubs", "0");
|
|
3750
|
+
renderList();
|
|
3751
|
+
return;
|
|
3752
|
+
}
|
|
3683
3753
|
const row = ev.target.closest(".row");
|
|
3684
3754
|
if (row) select(row.dataset.key);
|
|
3685
3755
|
});
|
|
@@ -3739,6 +3809,12 @@
|
|
|
3739
3809
|
localStorage.setItem("ay.compactList", compactList ? "1" : "0");
|
|
3740
3810
|
renderList();
|
|
3741
3811
|
});
|
|
3812
|
+
// Fold / unfold every subagent tree (global toggle, persisted per device).
|
|
3813
|
+
$("foldbtn").addEventListener("click", () => {
|
|
3814
|
+
foldSubs = !foldSubs;
|
|
3815
|
+
localStorage.setItem("ay.foldSubs", foldSubs ? "1" : "0");
|
|
3816
|
+
renderList();
|
|
3817
|
+
});
|
|
3742
3818
|
// Cycle the sort order: state → created → identity → state.
|
|
3743
3819
|
$("sortbtn").addEventListener("click", () => {
|
|
3744
3820
|
sortMode = SORT_MODES[(SORT_MODES.indexOf(sortMode) + 1) % SORT_MODES.length];
|
|
@@ -3781,9 +3857,10 @@
|
|
|
3781
3857
|
// renders. Clamps at the ends, scrolls the row into view.
|
|
3782
3858
|
function stepSelection(dir) {
|
|
3783
3859
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
3784
|
-
// Must match renderList's order; step only over agent rows (skip headers)
|
|
3860
|
+
// Must match renderList's order; step only over agent rows (skip headers),
|
|
3861
|
+
// and skip folded-away subagents so nav lands only on visible rows.
|
|
3785
3862
|
const shown = layeredRows(orderedEntries(toks))
|
|
3786
|
-
.filter((r) => r.kind === "agent")
|
|
3863
|
+
.filter((r) => r.kind === "agent" && !(foldSubs && r.parentEntry))
|
|
3787
3864
|
.map((r) => r.entry);
|
|
3788
3865
|
if (!shown.length) return;
|
|
3789
3866
|
const cur = shown.findIndex((e) => e._key === sel);
|
|
@@ -3889,6 +3966,66 @@
|
|
|
3889
3966
|
return cur && cur.cwd && cur.git && cur.git.branch ? cur : null;
|
|
3890
3967
|
}
|
|
3891
3968
|
|
|
3969
|
+
// Slash commands that act on the OPEN agent (the omnibox anchor), mirroring the
|
|
3970
|
+
// ⋯-menu recovery buttons. Typing "/" in the omnibox lists them; "/k" / "/r"
|
|
3971
|
+
// narrows. Each runs the very same handler the dropdown button does, so the
|
|
3972
|
+
// confirm prompt and server call are identical — /kill = Force-kill (SIGKILL),
|
|
3973
|
+
// /restart = Restart (resume). "/fork [topic]" is also offered here (built in
|
|
3974
|
+
// omniCommandRows, not this list, since it spawns rather than acts in place).
|
|
3975
|
+
const OMNI_COMMANDS = [
|
|
3976
|
+
{
|
|
3977
|
+
cmd: "/restart",
|
|
3978
|
+
icon: "↻",
|
|
3979
|
+
title: "Restart (resume)",
|
|
3980
|
+
desc: "stops it (if live) and relaunches resuming the session",
|
|
3981
|
+
run: (e) => restartAgent(e),
|
|
3982
|
+
},
|
|
3983
|
+
{
|
|
3984
|
+
cmd: "/kill",
|
|
3985
|
+
icon: "✕",
|
|
3986
|
+
title: "Force-kill (SIGKILL)",
|
|
3987
|
+
desc: "terminates the process immediately",
|
|
3988
|
+
danger: true,
|
|
3989
|
+
run: (e) => stopAgent(true, e),
|
|
3990
|
+
},
|
|
3991
|
+
];
|
|
3992
|
+
// Build the command rows for a "/…"-prefixed query, targeting the anchor agent.
|
|
3993
|
+
// Empty when nothing is open (a command with no agent to act on is a no-op) so
|
|
3994
|
+
// the omnibox falls through to its normal search/spawn behaviour.
|
|
3995
|
+
function omniCommandRows(q) {
|
|
3996
|
+
const target = omniAnchor();
|
|
3997
|
+
if (!target) return [];
|
|
3998
|
+
// Match on the FIRST token so a command can carry args after it
|
|
3999
|
+
// (e.g. "/fork fix the auth bug" → cmd "fork", topic "fix the auth bug").
|
|
4000
|
+
const parts = q.slice(1).trim().split(/\s+/).filter(Boolean);
|
|
4001
|
+
const cmdTerm = (parts[0] || "").toLowerCase(); // command token after "/"
|
|
4002
|
+
const rows = OMNI_COMMANDS.filter((c) => c.cmd.slice(1).startsWith(cmdTerm)).map((c) => ({
|
|
4003
|
+
kind: "cmd",
|
|
4004
|
+
cmd: c,
|
|
4005
|
+
target,
|
|
4006
|
+
}));
|
|
4007
|
+
// "/fork [topic]" — a slash alias for the fork spawn row so fork is reachable
|
|
4008
|
+
// straight from the command palette (⌘K → "/fork"), not only via the row that
|
|
4009
|
+
// auto-appends to a search. It's a spawn (kind:"fork"), not a kind:"cmd" that
|
|
4010
|
+
// acts on an existing agent, so it's built here rather than in OMNI_COMMANDS —
|
|
4011
|
+
// omniActivate's fork branch (branch prompt → /api/spawn {fork}) handles it.
|
|
4012
|
+
// Needs a real fork source (the open agent with a live git branch); when the
|
|
4013
|
+
// anchor has none, forking is meaningless so the row is omitted.
|
|
4014
|
+
if ("fork".startsWith(cmdTerm)) {
|
|
4015
|
+
const src = omniForkSource();
|
|
4016
|
+
if (src)
|
|
4017
|
+
rows.push({
|
|
4018
|
+
kind: "fork",
|
|
4019
|
+
branch: forkSlug(parts.slice(1).join(" "), src.git.branch),
|
|
4020
|
+
srcBranch: src.git.branch,
|
|
4021
|
+
fromCwd: src.cwd,
|
|
4022
|
+
cli: src.cli,
|
|
4023
|
+
room: src._room,
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
return rows;
|
|
4027
|
+
}
|
|
4028
|
+
|
|
3892
4029
|
// Which agent the highlighted candidate should preview in the background: an
|
|
3893
4030
|
// agent row → that agent; the spawn row / an empty list → the launch-time
|
|
3894
4031
|
// agent (so the backdrop shows the spawn target's cwd).
|
|
@@ -3975,6 +4112,16 @@
|
|
|
3975
4112
|
<div class="omni-sub">⏎ to choose a working dir · prompt: ${esc(q || "(empty)")}</div>
|
|
3976
4113
|
</div></div>`;
|
|
3977
4114
|
}
|
|
4115
|
+
if (r.kind === "cmd") {
|
|
4116
|
+
const t = r.target;
|
|
4117
|
+
const dz = r.cmd.danger ? " danger" : "";
|
|
4118
|
+
return `<div class="omni-row omni-spawn${dz}${sc}" data-i="${i}">
|
|
4119
|
+
<span class="dot ${esc(t.status)}"></span>
|
|
4120
|
+
<div class="omni-main">
|
|
4121
|
+
<div class="omni-title">${r.cmd.icon} ${esc(r.cmd.title)} <span style="opacity:.55">${esc(r.cmd.cmd)}</span></div>
|
|
4122
|
+
<div class="omni-sub">pid ${esc(String(t.pid))} ${esc(cliLabel(t) || ident(t) || "")} · ${esc(r.cmd.desc)}</div>
|
|
4123
|
+
</div></div>`;
|
|
4124
|
+
}
|
|
3978
4125
|
const e = r.entry;
|
|
3979
4126
|
const snip = r.snippet ? `<div class="omni-snip">…${r.snippet}…</div>` : "";
|
|
3980
4127
|
return `<div class="omni-row${sc}" data-i="${i}">
|
|
@@ -3993,6 +4140,18 @@
|
|
|
3993
4140
|
|
|
3994
4141
|
function runOmni() {
|
|
3995
4142
|
const q = $("omni-input").value.trim();
|
|
4143
|
+
// A "/…" query is a slash command against the open agent, not a search —
|
|
4144
|
+
// show the matching command rows and skip the agent/spawn/tail machinery.
|
|
4145
|
+
if (q.startsWith("/")) {
|
|
4146
|
+
const cmds = omniCommandRows(q);
|
|
4147
|
+
if (cmds.length) {
|
|
4148
|
+
if (omniTailTimer) clearTimeout(omniTailTimer);
|
|
4149
|
+
omniRows = cmds;
|
|
4150
|
+
omniIdx = 0;
|
|
4151
|
+
renderOmni();
|
|
4152
|
+
return;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
3996
4155
|
const toks = q.split(/\s+/).filter(Boolean);
|
|
3997
4156
|
// Tier 1 — instant title/identity match, ranked title-first then by recency.
|
|
3998
4157
|
const hits = entries
|
|
@@ -4085,6 +4244,13 @@
|
|
|
4085
4244
|
const q = $("omni-input").value.trim();
|
|
4086
4245
|
const row = omniRows[omniIdx];
|
|
4087
4246
|
const anchor = omniAnchor();
|
|
4247
|
+
// A slash command runs its ⋯-menu handler against the pinned target. Handled
|
|
4248
|
+
// before the spawn fallthrough so ⌘⏎ on a "/kill" row can't spawn instead.
|
|
4249
|
+
if (row && row.kind === "cmd") {
|
|
4250
|
+
closeOmni(false);
|
|
4251
|
+
await row.cmd.run(row.target);
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4088
4254
|
// ⌘⏎ always = spawn-here (the fast default), regardless of the selected row.
|
|
4089
4255
|
if (forceSpawn || (row && row.kind === "spawn")) {
|
|
4090
4256
|
closeOmni(false); // committing — keep the bg as-is, spawnAndSelect takes over
|
package/lab/ui/landing.html
CHANGED
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
spawn agents from anywhere.
|
|
215
215
|
</p>
|
|
216
216
|
<div class="cta">
|
|
217
|
-
<a class="btn" href="/w/">Open the console →</a>
|
|
217
|
+
<a class="btn" href="/w/" autofocus>Open the console →</a>
|
|
218
218
|
<a class="btn ghost" href="https://github.com/snomiao/agent-yes">Star on GitHub</a>
|
|
219
219
|
</div>
|
|
220
220
|
<div class="install">
|
|
@@ -273,5 +273,15 @@ powershell -c "irm https://agent-yes.com/setup.ps1 | iex"</code></pre>
|
|
|
273
273
|
</div>
|
|
274
274
|
</footer>
|
|
275
275
|
</div>
|
|
276
|
+
<script>
|
|
277
|
+
// Focus the "Open the console" CTA on load so keyboard/Enter users land
|
|
278
|
+
// straight on it. `autofocus` on <a> isn't honored everywhere, so back it
|
|
279
|
+
// up here — but skip on touch devices where focusing scroll-jumps the page.
|
|
280
|
+
(function () {
|
|
281
|
+
if (matchMedia("(hover: none)").matches) return;
|
|
282
|
+
var cta = document.querySelector(".hero .cta a.btn");
|
|
283
|
+
if (cta) cta.focus({ preventScroll: true });
|
|
284
|
+
})();
|
|
285
|
+
</script>
|
|
276
286
|
</body>
|
|
277
287
|
</html>
|
package/package.json
CHANGED