agent-yes 1.122.2 → 1.123.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/default.config.yaml +19 -0
- package/dist/{SUPPORTED_CLIS-BTu2brih.js → SUPPORTED_CLIS-B4O2cFlt.js} +2 -2
- package/dist/SUPPORTED_CLIS-DHkqGoNv.js +8 -0
- package/dist/{agent-yes.config-z-IPzH5U.js → agent-yes.config-D6ycMApr.js} +2 -65
- package/dist/cli.js +6 -6
- package/dist/configShared-C5QaNPnz.js +71 -0
- package/dist/{globalPidIndex-gZuTvTBs.js → globalPidIndex-C7r2m6s7.js} +19 -20
- package/dist/index.js +4 -4
- package/dist/pidStore-C4c2O15q.js +5 -0
- package/dist/{pidStore-B5vBu8Px.js → pidStore-CGKIhaJO.js} +5 -4
- package/dist/reaper-BLVA780B.js +3 -0
- package/dist/{reaper-Dj8R7ltI.js → reaper-BkjPN7mw.js} +24 -2
- package/dist/{remotes-CpGcTr7A.js → remotes-BRCDVnR7.js} +1 -1
- package/dist/{remotes-D2fqaRU8.js → remotes-D8GvSbhf.js} +1 -1
- package/dist/{schedule-DgRrdA_n.js → schedule-DULdIkU9.js} +7 -7
- package/dist/{serve-tn7ZetZs.js → serve-r_2v9EKc.js} +202 -58
- package/dist/{setup-dZhgpNse.js → setup-DHa6fX8M.js} +3 -3
- package/dist/{share-CksllWW-.js → share-YuM6-Q6A.js} +78 -4
- package/dist/{subcommands-D9BWZilr.js → subcommands-B13Kto-u.js} +647 -32
- package/dist/subcommands-Tv6AwUkD.js +7 -0
- package/dist/{tray-DjCIyakK.js → tray-BVnJLThD.js} +1 -1
- package/dist/{ts-CIf0uaR7.js → ts-DgukRoEI.js} +10 -7
- package/dist/{versionChecker-DjxKi4qe.js → versionChecker-BqOr1YqC.js} +2 -2
- package/dist/{workspaceConfig-XP2NEWmV.js → workspaceConfig-BJO4fzEn.js} +1 -1
- package/lab/ui/console-logic.js +222 -10
- package/lab/ui/icon.svg +5 -0
- package/lab/ui/index.html +689 -14
- package/lab/ui/landing.html +276 -0
- package/lab/ui/manifest.webmanifest +14 -0
- package/lab/ui/sw.js +56 -0
- package/package.json +5 -1
- package/ts/agentTree.spec.ts +92 -0
- package/ts/agentTree.ts +149 -0
- package/ts/configShared.ts +4 -0
- package/ts/globalPidIndex.ts +28 -20
- package/ts/idleWaiter.spec.ts +7 -1
- package/ts/index.ts +9 -0
- package/ts/lsWatch.spec.ts +61 -0
- package/ts/lsWatch.ts +94 -0
- package/ts/needsInput.spec.ts +55 -0
- package/ts/needsInput.ts +68 -0
- package/ts/pidStore.ts +3 -0
- package/ts/reaper.spec.ts +26 -2
- package/ts/reaper.ts +25 -0
- package/ts/resultEnvelope.spec.ts +43 -0
- package/ts/resultEnvelope.ts +88 -0
- package/ts/serve.ts +276 -41
- package/ts/share.ts +156 -3
- package/ts/subcommands.ts +0 -0
- package/ts/todoParse.spec.ts +68 -0
- package/ts/todoParse.ts +88 -0
- package/ts/utils.spec.ts +4 -1
- package/dist/SUPPORTED_CLIS-DcOKE9Nz.js +0 -8
- package/dist/pidStore-7y1cTcAE.js +0 -5
- package/dist/reaper-HqcUms2d.js +0 -3
- package/dist/subcommands-D8sHibKu.js +0 -6
package/lab/ui/index.html
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agent-yes · console</title>
|
|
7
|
+
<!-- PWA: installable console under /w/ (manifest scope is /w/). The service
|
|
8
|
+
worker is network-first so it never serves a stale wire protocol — the
|
|
9
|
+
cache is only an offline-launch fallback. -->
|
|
10
|
+
<meta name="theme-color" content="#0d1117" />
|
|
11
|
+
<link rel="manifest" href="./manifest.webmanifest" />
|
|
12
|
+
<link rel="icon" href="./icon.svg" />
|
|
13
|
+
<link rel="apple-touch-icon" href="./icon.svg" />
|
|
14
|
+
<script>
|
|
15
|
+
if ("serviceWorker" in navigator) {
|
|
16
|
+
addEventListener("load", () => navigator.serviceWorker.register("./sw.js").catch(() => {}));
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
7
19
|
<style>
|
|
8
20
|
/* Palette borrowed from codehost (GitHub-dark) so the two feel like one system. */
|
|
9
21
|
:root {
|
|
@@ -124,6 +136,167 @@
|
|
|
124
136
|
h1 .tag {
|
|
125
137
|
color: var(--accent);
|
|
126
138
|
}
|
|
139
|
+
/* Connection-path pill in the header: at a glance, is the terminal you're
|
|
140
|
+
watching on the fast local/lan path or the slow relayed path? */
|
|
141
|
+
/* Per-peer connection-type pill in the terminal header — describes how this
|
|
142
|
+
viewer reaches the SELECTED agent's host (local/lan/wan/relay). */
|
|
143
|
+
.rhead .ctype {
|
|
144
|
+
position: relative;
|
|
145
|
+
font-size: 10px;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
letter-spacing: 0.02em;
|
|
148
|
+
align-self: center;
|
|
149
|
+
padding: 1px 7px;
|
|
150
|
+
border-radius: 999px;
|
|
151
|
+
border: 1px solid currentColor;
|
|
152
|
+
text-transform: uppercase;
|
|
153
|
+
opacity: 0.9;
|
|
154
|
+
cursor: help;
|
|
155
|
+
}
|
|
156
|
+
.ctype[hidden] {
|
|
157
|
+
display: none;
|
|
158
|
+
}
|
|
159
|
+
/* Hover popover with the connection debug readout. */
|
|
160
|
+
.ctype .ctip {
|
|
161
|
+
display: none;
|
|
162
|
+
position: absolute;
|
|
163
|
+
top: 150%;
|
|
164
|
+
right: 0;
|
|
165
|
+
z-index: 30;
|
|
166
|
+
min-width: 240px;
|
|
167
|
+
padding: 8px 10px;
|
|
168
|
+
background: var(--panel);
|
|
169
|
+
border: 1px solid var(--line);
|
|
170
|
+
border-radius: 8px;
|
|
171
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
172
|
+
color: var(--fg);
|
|
173
|
+
font-family: var(--mono);
|
|
174
|
+
font-size: 10.5px;
|
|
175
|
+
font-weight: 400;
|
|
176
|
+
line-height: 1.6;
|
|
177
|
+
letter-spacing: 0;
|
|
178
|
+
text-transform: none;
|
|
179
|
+
text-align: left;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
}
|
|
182
|
+
.ctype:hover .ctip {
|
|
183
|
+
display: block;
|
|
184
|
+
}
|
|
185
|
+
.ctip-h {
|
|
186
|
+
color: var(--muted);
|
|
187
|
+
margin-bottom: 5px;
|
|
188
|
+
white-space: normal;
|
|
189
|
+
}
|
|
190
|
+
.ctip-k {
|
|
191
|
+
display: inline-block;
|
|
192
|
+
width: 56px;
|
|
193
|
+
color: var(--muted);
|
|
194
|
+
}
|
|
195
|
+
/* ⋯ overflow menu in the terminal header + its dropdown. */
|
|
196
|
+
.rmenu {
|
|
197
|
+
position: relative;
|
|
198
|
+
margin-left: auto;
|
|
199
|
+
}
|
|
200
|
+
.rmenubtn {
|
|
201
|
+
background: none;
|
|
202
|
+
border: none;
|
|
203
|
+
color: var(--muted);
|
|
204
|
+
font-size: 16px;
|
|
205
|
+
line-height: 1;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
padding: 2px 6px;
|
|
208
|
+
border-radius: 6px;
|
|
209
|
+
}
|
|
210
|
+
.rmenubtn:hover {
|
|
211
|
+
background: var(--panel);
|
|
212
|
+
color: var(--fg);
|
|
213
|
+
}
|
|
214
|
+
.rmenupanel {
|
|
215
|
+
position: absolute;
|
|
216
|
+
top: 26px;
|
|
217
|
+
right: 0;
|
|
218
|
+
z-index: 15;
|
|
219
|
+
background: var(--panel);
|
|
220
|
+
border: 1px solid var(--line);
|
|
221
|
+
border-radius: 8px;
|
|
222
|
+
padding: 4px;
|
|
223
|
+
min-width: 140px;
|
|
224
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
|
225
|
+
}
|
|
226
|
+
.rmenupanel[hidden] {
|
|
227
|
+
display: none;
|
|
228
|
+
}
|
|
229
|
+
.rmenuitem {
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
gap: 7px;
|
|
233
|
+
padding: 5px 8px;
|
|
234
|
+
font-size: 12.5px;
|
|
235
|
+
border-radius: 6px;
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
user-select: none;
|
|
238
|
+
}
|
|
239
|
+
.rmenuitem:hover {
|
|
240
|
+
background: var(--bg);
|
|
241
|
+
}
|
|
242
|
+
.rmenuitem input {
|
|
243
|
+
margin: 0;
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
}
|
|
246
|
+
/* Action items (Stop / Force-kill) reuse the menu-item look as buttons. */
|
|
247
|
+
.rmenuact {
|
|
248
|
+
width: 100%;
|
|
249
|
+
background: none;
|
|
250
|
+
border: none;
|
|
251
|
+
text-align: left;
|
|
252
|
+
font: inherit;
|
|
253
|
+
color: var(--fg);
|
|
254
|
+
}
|
|
255
|
+
.rmenuact.danger {
|
|
256
|
+
color: var(--red);
|
|
257
|
+
}
|
|
258
|
+
.rmenusep {
|
|
259
|
+
height: 1px;
|
|
260
|
+
background: var(--line);
|
|
261
|
+
margin: 4px 2px;
|
|
262
|
+
}
|
|
263
|
+
/* Live performance HUD — opt-in via the ⋯ menu, hidden by default. */
|
|
264
|
+
.perfhud {
|
|
265
|
+
position: absolute;
|
|
266
|
+
top: 44px;
|
|
267
|
+
right: 12px;
|
|
268
|
+
z-index: 14;
|
|
269
|
+
background: rgba(13, 17, 23, 0.82);
|
|
270
|
+
color: var(--fg);
|
|
271
|
+
border: 1px solid var(--line);
|
|
272
|
+
border-radius: 8px;
|
|
273
|
+
padding: 7px 10px;
|
|
274
|
+
font-family: var(--mono);
|
|
275
|
+
font-size: 11px;
|
|
276
|
+
line-height: 1.5;
|
|
277
|
+
pointer-events: none;
|
|
278
|
+
white-space: pre;
|
|
279
|
+
backdrop-filter: blur(3px);
|
|
280
|
+
}
|
|
281
|
+
.perfhud[hidden] {
|
|
282
|
+
display: none;
|
|
283
|
+
}
|
|
284
|
+
.perfhud b {
|
|
285
|
+
color: var(--accent);
|
|
286
|
+
font-weight: 600;
|
|
287
|
+
}
|
|
288
|
+
/* Per-peer connection-type tag on peer-group headers in the tree. */
|
|
289
|
+
.ghead .ctag {
|
|
290
|
+
font-size: 9.5px;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
text-transform: uppercase;
|
|
293
|
+
letter-spacing: 0.02em;
|
|
294
|
+
margin-left: 6px;
|
|
295
|
+
padding: 0 5px;
|
|
296
|
+
border-radius: 999px;
|
|
297
|
+
border: 1px solid currentColor;
|
|
298
|
+
opacity: 0.85;
|
|
299
|
+
}
|
|
127
300
|
.sub {
|
|
128
301
|
color: var(--muted);
|
|
129
302
|
font-size: 12.5px;
|
|
@@ -516,6 +689,23 @@
|
|
|
516
689
|
.git.dirty {
|
|
517
690
|
color: var(--amber);
|
|
518
691
|
}
|
|
692
|
+
/* Task-progress badge ("2/5") parsed from the agent's todo block, sits next
|
|
693
|
+
to the git chip. Pill so it reads as a discrete counter; green when all
|
|
694
|
+
tasks are done. */
|
|
695
|
+
.tasks {
|
|
696
|
+
font-family: var(--mono);
|
|
697
|
+
font-size: 10.5px;
|
|
698
|
+
color: var(--muted);
|
|
699
|
+
white-space: nowrap;
|
|
700
|
+
flex: none;
|
|
701
|
+
padding: 0 5px;
|
|
702
|
+
border: 1px solid var(--line);
|
|
703
|
+
border-radius: 8px;
|
|
704
|
+
}
|
|
705
|
+
.tasks.done {
|
|
706
|
+
color: var(--green);
|
|
707
|
+
border-color: #2a3a2a;
|
|
708
|
+
}
|
|
519
709
|
.detail {
|
|
520
710
|
color: var(--muted);
|
|
521
711
|
font-size: 12.5px;
|
|
@@ -539,6 +729,46 @@
|
|
|
539
729
|
gap: 8px;
|
|
540
730
|
padding: 6px 18px;
|
|
541
731
|
}
|
|
732
|
+
/* Tree branch prefix for nested (sub)agents — monospace so the box-drawing
|
|
733
|
+
glyphs line up into continuous │ ├ └ rails across rows. */
|
|
734
|
+
.tbranch {
|
|
735
|
+
font-family: var(--mono);
|
|
736
|
+
color: var(--muted);
|
|
737
|
+
white-space: pre;
|
|
738
|
+
flex: none;
|
|
739
|
+
opacity: 0.7;
|
|
740
|
+
margin-right: -2px;
|
|
741
|
+
}
|
|
742
|
+
/* Room/peer group header rows (the upper layers of the tree). Non-selectable,
|
|
743
|
+
dimmer and smaller than agent rows so they read as containers, not agents. */
|
|
744
|
+
.ghead {
|
|
745
|
+
display: flex;
|
|
746
|
+
align-items: center;
|
|
747
|
+
gap: 6px;
|
|
748
|
+
padding: 5px 18px 2px;
|
|
749
|
+
font-size: 11.5px;
|
|
750
|
+
color: var(--muted);
|
|
751
|
+
user-select: none;
|
|
752
|
+
}
|
|
753
|
+
.ghead .gicon {
|
|
754
|
+
opacity: 0.6;
|
|
755
|
+
flex: none;
|
|
756
|
+
}
|
|
757
|
+
.ghead .glabel {
|
|
758
|
+
font-weight: 600;
|
|
759
|
+
color: var(--fg);
|
|
760
|
+
overflow: hidden;
|
|
761
|
+
text-overflow: ellipsis;
|
|
762
|
+
white-space: nowrap;
|
|
763
|
+
}
|
|
764
|
+
.ghead.g-room .glabel {
|
|
765
|
+
color: var(--accent);
|
|
766
|
+
}
|
|
767
|
+
.ghead .gkind {
|
|
768
|
+
font-size: 10px;
|
|
769
|
+
opacity: 0.45;
|
|
770
|
+
flex: none;
|
|
771
|
+
}
|
|
542
772
|
.crow .cident {
|
|
543
773
|
font-family: var(--mono);
|
|
544
774
|
font-size: 11.5px;
|
|
@@ -601,6 +831,7 @@
|
|
|
601
831
|
flex-direction: column;
|
|
602
832
|
min-width: 0;
|
|
603
833
|
min-height: 0;
|
|
834
|
+
position: relative; /* anchor for the absolutely-positioned perf HUD */
|
|
604
835
|
}
|
|
605
836
|
.rhead {
|
|
606
837
|
padding: 14px 20px;
|
|
@@ -811,12 +1042,32 @@
|
|
|
811
1042
|
<span class="live"
|
|
812
1043
|
><span class="dot" id="livedot"></span><span id="livetxt">connecting…</span></span
|
|
813
1044
|
>
|
|
1045
|
+
<span
|
|
1046
|
+
id="ctype"
|
|
1047
|
+
class="ctype"
|
|
1048
|
+
title="how your browser reaches this agent's host"
|
|
1049
|
+
hidden
|
|
1050
|
+
></span>
|
|
1051
|
+
<span class="rmenu">
|
|
1052
|
+
<button class="rmenubtn" id="rmenubtn" title="more" aria-label="more">⋯</button>
|
|
1053
|
+
<div class="rmenupanel" id="rmenupanel" hidden>
|
|
1054
|
+
<label class="rmenuitem"
|
|
1055
|
+
><input type="checkbox" id="perfToggle" /> <span>Perf HUD</span></label
|
|
1056
|
+
>
|
|
1057
|
+
<div class="rmenusep"></div>
|
|
1058
|
+
<button class="rmenuitem rmenuact" id="stopAgent" type="button">⏹ Stop agent</button>
|
|
1059
|
+
<button class="rmenuitem rmenuact danger" id="killAgent" type="button">
|
|
1060
|
+
✕ Force-kill (SIGKILL)
|
|
1061
|
+
</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
</span>
|
|
814
1064
|
</div>
|
|
815
1065
|
<div class="log" id="log">
|
|
816
1066
|
<div class="placeholder">
|
|
817
1067
|
← pick an agent to tail its log; type directly into the terminal
|
|
818
1068
|
</div>
|
|
819
1069
|
</div>
|
|
1070
|
+
<div class="perfhud" id="perfhud" hidden></div>
|
|
820
1071
|
</div>
|
|
821
1072
|
</div>
|
|
822
1073
|
|
|
@@ -842,6 +1093,8 @@
|
|
|
842
1093
|
fullIdent,
|
|
843
1094
|
hasIdent,
|
|
844
1095
|
deviceCount,
|
|
1096
|
+
layeredRows,
|
|
1097
|
+
taskLabel,
|
|
845
1098
|
} from "./console-logic.js";
|
|
846
1099
|
import {
|
|
847
1100
|
MARKER as E2E_MARKER,
|
|
@@ -1415,6 +1668,382 @@
|
|
|
1415
1668
|
const srcFor = (e) => (e && sources.get(e._room)) || sources.get(LOCAL) || null;
|
|
1416
1669
|
const txFor = (e) => srcFor(e)?.tx || localTx;
|
|
1417
1670
|
|
|
1671
|
+
// ---- connection-path classification (header [local/lan/wan/relay] pill) ----
|
|
1672
|
+
// Tells the user, at a glance, whether the terminal they're watching rides the
|
|
1673
|
+
// fast direct path or a relayed one — the difference between snappy and laggy.
|
|
1674
|
+
const isLoopbackIp = (ip) => ip === "::1" || /^127\./.test(ip);
|
|
1675
|
+
const isPrivateIp = (ip) =>
|
|
1676
|
+
/^10\./.test(ip) ||
|
|
1677
|
+
/^192\.168\./.test(ip) ||
|
|
1678
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(ip) ||
|
|
1679
|
+
/^169\.254\./.test(ip) || // IPv4 link-local
|
|
1680
|
+
/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip) || // CGNAT / Tailscale (100.64.0.0/10)
|
|
1681
|
+
/^fe80:/i.test(ip) || // IPv6 link-local
|
|
1682
|
+
/^f[cd][0-9a-f]{2}:/i.test(ip) || // IPv6 ULA (fc00::/7, incl. Tailscale fd7a:…)
|
|
1683
|
+
/\.local$/i.test(ip); // mDNS host candidate (browser-obfuscated LAN IP)
|
|
1684
|
+
// Inspect the selected ICE candidate pair and return the full picture for the
|
|
1685
|
+
// hover tooltip: kind, RTT, both candidates, a NAT-traversal hint, and the open
|
|
1686
|
+
// data channel. `kind` is classified by how we reach the REMOTE host: relay on
|
|
1687
|
+
// either end = TURN-proxied; otherwise the remote candidate's type/address
|
|
1688
|
+
// decides local/lan/wan. (The LOCAL end is often prflx/srflx even on a fast
|
|
1689
|
+
// private path — e.g. a Tailscale/CGNAT mesh shows local=prflx, remote=host
|
|
1690
|
+
// 100.x at ~1ms — so we key on the remote, not on both ends being `host`.)
|
|
1691
|
+
async function inspectConn(pc) {
|
|
1692
|
+
if (!pc || typeof pc.getStats !== "function") return null;
|
|
1693
|
+
try {
|
|
1694
|
+
const stats = await pc.getStats();
|
|
1695
|
+
let pairId = null;
|
|
1696
|
+
stats.forEach((s) => {
|
|
1697
|
+
if (s.type === "transport" && s.selectedCandidatePairId)
|
|
1698
|
+
pairId = s.selectedCandidatePairId;
|
|
1699
|
+
});
|
|
1700
|
+
let pair = null;
|
|
1701
|
+
stats.forEach((s) => {
|
|
1702
|
+
if (
|
|
1703
|
+
pairId
|
|
1704
|
+
? s.id === pairId
|
|
1705
|
+
: s.type === "candidate-pair" && s.state === "succeeded" && s.nominated
|
|
1706
|
+
)
|
|
1707
|
+
pair = s;
|
|
1708
|
+
});
|
|
1709
|
+
if (!pair) return null;
|
|
1710
|
+
let local = null,
|
|
1711
|
+
remote = null;
|
|
1712
|
+
stats.forEach((s) => {
|
|
1713
|
+
if (s.id === pair.localCandidateId) local = s;
|
|
1714
|
+
if (s.id === pair.remoteCandidateId) remote = s;
|
|
1715
|
+
});
|
|
1716
|
+
if (!local || !remote) return null;
|
|
1717
|
+
const lt = local.candidateType,
|
|
1718
|
+
rt = remote.candidateType,
|
|
1719
|
+
rip = remote.address || remote.ip || "";
|
|
1720
|
+
let kind, nat;
|
|
1721
|
+
if (lt === "relay" || rt === "relay") {
|
|
1722
|
+
kind = "relay";
|
|
1723
|
+
nat = "TURN-relayed — symmetric NAT or UDP blocked";
|
|
1724
|
+
} else if (rt === "host") {
|
|
1725
|
+
kind = isLoopbackIp(rip) ? "local" : !rip || isPrivateIp(rip) ? "lan" : "wan";
|
|
1726
|
+
nat =
|
|
1727
|
+
lt === "host"
|
|
1728
|
+
? "direct — both ends on-net"
|
|
1729
|
+
: "direct to host" + (lt === "srflx" || lt === "prflx" ? " (you behind NAT)" : "");
|
|
1730
|
+
} else {
|
|
1731
|
+
kind = "wan";
|
|
1732
|
+
nat = "STUN-reflexive P2P (NAT-traversed)";
|
|
1733
|
+
}
|
|
1734
|
+
const fmt = (c) => c.candidateType + "/" + (c.protocol || "?");
|
|
1735
|
+
return {
|
|
1736
|
+
kind,
|
|
1737
|
+
nat,
|
|
1738
|
+
rttMs:
|
|
1739
|
+
pair.currentRoundTripTime != null
|
|
1740
|
+
? Math.round(pair.currentRoundTripTime * 1000)
|
|
1741
|
+
: null,
|
|
1742
|
+
local:
|
|
1743
|
+
fmt(local) + (local.address || local.ip ? " " + (local.address || local.ip) : ""),
|
|
1744
|
+
remote: fmt(remote) + (rip ? " " + rip : ""),
|
|
1745
|
+
dc:
|
|
1746
|
+
[...stats.values()]
|
|
1747
|
+
.filter((v) => v.type === "data-channel" && v.state === "open")
|
|
1748
|
+
.map((v) => v.label)
|
|
1749
|
+
.join(",") || null,
|
|
1750
|
+
};
|
|
1751
|
+
} catch {
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
// Resolve the live RTCPeerConnection that carries a SPECIFIC agent. A codehost
|
|
1756
|
+
// room holds one peer per host (room.rtcs keyed by peerId); byPid maps the
|
|
1757
|
+
// agent's pid to its peerId, so each agent resolves to ITS host's connection —
|
|
1758
|
+
// a VPS agent reads wan while a tak.local agent in the same room reads lan.
|
|
1759
|
+
function pcForAgent(s, e) {
|
|
1760
|
+
if (!s) return null;
|
|
1761
|
+
if (s.kind === "rtc") return s.client?.pc || null; // share room: one host
|
|
1762
|
+
if (s.kind === "ch") {
|
|
1763
|
+
const peerId = s.client?.byPid?.get(String(e?.pid));
|
|
1764
|
+
return (peerId && s.client?.room?.rtcs?.get(peerId)?.pc) || null;
|
|
1765
|
+
}
|
|
1766
|
+
return null; // local ay-serve has no peer connection
|
|
1767
|
+
}
|
|
1768
|
+
// Two cache views, both filled from one classification pass:
|
|
1769
|
+
// - connKinds keyed by the true PEER (room + peerId) drives the header badge,
|
|
1770
|
+
// so two machines that happen to share a hostname never collide.
|
|
1771
|
+
// - hostKinds keyed by (room, host) drives the peer-group header tags, which
|
|
1772
|
+
// only ever render when hosts are already distinct.
|
|
1773
|
+
const hostKeyOf = (e) => (e?._room || "") + " " + (e?._host || "");
|
|
1774
|
+
function peerKeyOf(s, e) {
|
|
1775
|
+
if (!s || s.id === LOCAL || s.kind == null) return LOCAL;
|
|
1776
|
+
if (s.kind === "ch") {
|
|
1777
|
+
const peerId = s.client?.byPid?.get(String(e?.pid));
|
|
1778
|
+
return s.id + " " + (peerId || e?._host || "");
|
|
1779
|
+
}
|
|
1780
|
+
return s.id; // rtc share: one peer per room
|
|
1781
|
+
}
|
|
1782
|
+
const connKinds = new Map(); // peerKey -> "local" | "lan" | "wan" | "relay"
|
|
1783
|
+
const hostKinds = new Map(); // hostKey -> same (peer-header tags)
|
|
1784
|
+
const connInfo = new Map(); // peerKey -> full detail for the hover tooltip
|
|
1785
|
+
let connSig = "";
|
|
1786
|
+
// The signaling server backing a source: codehost rooms use the codehost
|
|
1787
|
+
// signal DO; agent-yes share rooms use their own host; local has none.
|
|
1788
|
+
const signalingOf = (s) =>
|
|
1789
|
+
!s || s.id === LOCAL || s.kind == null
|
|
1790
|
+
? null
|
|
1791
|
+
: s.kind === "ch"
|
|
1792
|
+
? "wss://signal.codehost.dev"
|
|
1793
|
+
: "wss://" + (s.host || SIG_DEFAULT);
|
|
1794
|
+
const CTYPE_COLORS = {
|
|
1795
|
+
local: "var(--green)",
|
|
1796
|
+
lan: "var(--green)",
|
|
1797
|
+
wan: "var(--amber)",
|
|
1798
|
+
relay: "var(--red)",
|
|
1799
|
+
};
|
|
1800
|
+
const CTYPE_TIPS = {
|
|
1801
|
+
local: "this machine (ay serve, local)",
|
|
1802
|
+
lan: "direct over the local network (fast)",
|
|
1803
|
+
wan: "direct over the internet, peer-to-peer (STUN — RTT-bound)",
|
|
1804
|
+
relay: "TURN-relayed — proxied through a server (slowest path)",
|
|
1805
|
+
};
|
|
1806
|
+
// A small colored pill for a connection kind — reused by the header badge and
|
|
1807
|
+
// every peer-group header in the tree.
|
|
1808
|
+
function connTagHtml(kind) {
|
|
1809
|
+
if (!kind) return "";
|
|
1810
|
+
return `<span class="ctag" style="color:${CTYPE_COLORS[kind]}" title="${esc(CTYPE_TIPS[kind])}">${kind}</span>`;
|
|
1811
|
+
}
|
|
1812
|
+
// Classify every distinct peer in the fleet, caching by (room,host). Because
|
|
1813
|
+
// each agent reads its own host's path, a mixed room (VPS + laptop) shows both
|
|
1814
|
+
// wan and lan at once — exactly what tells you a "local" agent is wrongly remote.
|
|
1815
|
+
async function refreshConnKinds() {
|
|
1816
|
+
// Probe each distinct peer once (not once per agent), keyed by peerKey.
|
|
1817
|
+
const reps = new Map(); // peerKey -> a representative entry for that peer
|
|
1818
|
+
for (const e of entries) {
|
|
1819
|
+
const k = peerKeyOf(srcFor(e), e);
|
|
1820
|
+
if (!reps.has(k)) reps.set(k, e);
|
|
1821
|
+
}
|
|
1822
|
+
connKinds.clear();
|
|
1823
|
+
connInfo.clear();
|
|
1824
|
+
await Promise.all(
|
|
1825
|
+
[...reps].map(async ([key, e]) => {
|
|
1826
|
+
const s = srcFor(e);
|
|
1827
|
+
if (!s || s.id === LOCAL || s.kind == null) {
|
|
1828
|
+
if (s && s.live) {
|
|
1829
|
+
connKinds.set(key, "local");
|
|
1830
|
+
connInfo.set(key, {
|
|
1831
|
+
kind: "local",
|
|
1832
|
+
nat: "this machine (ay serve)",
|
|
1833
|
+
signaling: null,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const info = await inspectConn(pcForAgent(s, e));
|
|
1839
|
+
if (info) {
|
|
1840
|
+
connKinds.set(key, info.kind);
|
|
1841
|
+
connInfo.set(key, { ...info, signaling: signalingOf(s) });
|
|
1842
|
+
}
|
|
1843
|
+
}),
|
|
1844
|
+
);
|
|
1845
|
+
// Project peer kinds onto (room,host) keys for the tree's peer headers.
|
|
1846
|
+
hostKinds.clear();
|
|
1847
|
+
for (const e of entries) {
|
|
1848
|
+
const kind = connKinds.get(peerKeyOf(srcFor(e), e));
|
|
1849
|
+
if (kind) hostKinds.set(hostKeyOf(e), kind);
|
|
1850
|
+
}
|
|
1851
|
+
paintHeaderBadge();
|
|
1852
|
+
// Repaint the list only when the peer-header tags actually changed — the poll
|
|
1853
|
+
// already re-renders on data deltas, so this avoids churning the DOM every tick.
|
|
1854
|
+
const sig = [...hostKinds]
|
|
1855
|
+
.sort()
|
|
1856
|
+
.map(([k, v]) => k + "=" + v)
|
|
1857
|
+
.join(";");
|
|
1858
|
+
if (sig !== connSig) {
|
|
1859
|
+
connSig = sig;
|
|
1860
|
+
renderList();
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
// The terminal-header pill shows how this viewer reaches the SELECTED agent's
|
|
1864
|
+
// host peer (local/lan/wan/relay) — per-peer, contextual to what's open. Reads
|
|
1865
|
+
// the cache only (kept sync so renders never await); hidden when no agent is
|
|
1866
|
+
// open (the whole rhead is hidden then anyway).
|
|
1867
|
+
function paintHeaderBadge() {
|
|
1868
|
+
const badge = $("ctype");
|
|
1869
|
+
if (!badge) return;
|
|
1870
|
+
const e = sel ? entries.find((x) => x._key === sel) : null;
|
|
1871
|
+
const key = e ? peerKeyOf(srcFor(e), e) : null;
|
|
1872
|
+
const kind = key ? connKinds.get(key) : null;
|
|
1873
|
+
if (!kind) {
|
|
1874
|
+
badge.hidden = true;
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
badge.hidden = false;
|
|
1878
|
+
badge.style.color = CTYPE_COLORS[kind] || "var(--muted)";
|
|
1879
|
+
badge.removeAttribute("title"); // the hover popover replaces the native title
|
|
1880
|
+
badge.innerHTML = esc(kind) + connTipHtml(kind, connInfo.get(key));
|
|
1881
|
+
}
|
|
1882
|
+
// The on-hover debug popover for the connection pill: latency, both ICE
|
|
1883
|
+
// candidates, NAT-traversal hint, the open data channel, and the signaling
|
|
1884
|
+
// server. Pure detail readout — the same getStats the classifier already ran.
|
|
1885
|
+
function connTipHtml(kind, info) {
|
|
1886
|
+
const rows = [];
|
|
1887
|
+
if (info?.rttMs != null) rows.push(["latency", info.rttMs + " ms RTT"]);
|
|
1888
|
+
if (info?.remote) rows.push(["remote", info.remote]);
|
|
1889
|
+
if (info?.local) rows.push(["local", info.local]);
|
|
1890
|
+
if (info?.nat) rows.push(["nat", info.nat]);
|
|
1891
|
+
if (info?.dc) rows.push(["channel", info.dc]);
|
|
1892
|
+
if (info?.signaling) rows.push(["signal", info.signaling]);
|
|
1893
|
+
const head = `<div class="ctip-h">${esc(CTYPE_TIPS[kind] || kind)}</div>`;
|
|
1894
|
+
const body = rows
|
|
1895
|
+
.map(([k, v]) => `<div><span class="ctip-k">${k}</span>${esc(String(v))}</div>`)
|
|
1896
|
+
.join("");
|
|
1897
|
+
return `<span class="ctip">${head}${body || '<div class="ctip-k">probing…</div>'}</span>`;
|
|
1898
|
+
}
|
|
1899
|
+
// Candidate pairs aren't known the instant a peer connects (ICE keeps probing)
|
|
1900
|
+
// and can change (relay fallback); a light poll keeps every pill honest.
|
|
1901
|
+
setInterval(refreshConnKinds, 2500);
|
|
1902
|
+
|
|
1903
|
+
// ---- live performance HUD (opt-in via the ⋯ menu, hidden by default) -----
|
|
1904
|
+
// Surfaces the metrics that actually move when the console feels laggy: render
|
|
1905
|
+
// fps, keystroke throughput (xterm onData), agent output rate (term.write),
|
|
1906
|
+
// and main-thread jank (long tasks). Counters accumulate over a 1s window;
|
|
1907
|
+
// the I/O hooks live in select() (perfNote on fwd / term.write).
|
|
1908
|
+
const perf = {
|
|
1909
|
+
frames: 0,
|
|
1910
|
+
inEvents: 0,
|
|
1911
|
+
inBytes: 0,
|
|
1912
|
+
outWrites: 0,
|
|
1913
|
+
outBytes: 0,
|
|
1914
|
+
jankMs: 0,
|
|
1915
|
+
echoMs: null,
|
|
1916
|
+
};
|
|
1917
|
+
let perfOn = false;
|
|
1918
|
+
let perfRaf = 0;
|
|
1919
|
+
// Keystroke→reflect latency: stamp the first un-echoed keystroke; the next
|
|
1920
|
+
// term.write that lands is its echo (the PTY round-trip). Accurate while
|
|
1921
|
+
// typing into an otherwise-quiet agent — the real UX number for a TUI.
|
|
1922
|
+
let pendingKeyTs = 0;
|
|
1923
|
+
function perfNote(kind, n) {
|
|
1924
|
+
if (!perfOn) return;
|
|
1925
|
+
if (kind === "in") {
|
|
1926
|
+
perf.inEvents++;
|
|
1927
|
+
perf.inBytes += n || 0;
|
|
1928
|
+
if (!pendingKeyTs) pendingKeyTs = performance.now();
|
|
1929
|
+
} else if (kind === "out") {
|
|
1930
|
+
perf.outWrites++;
|
|
1931
|
+
perf.outBytes += n || 0;
|
|
1932
|
+
if (pendingKeyTs) {
|
|
1933
|
+
perf.echoMs = Math.round(performance.now() - pendingKeyTs);
|
|
1934
|
+
pendingKeyTs = 0;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
function perfReset() {
|
|
1939
|
+
perf.frames =
|
|
1940
|
+
perf.inEvents =
|
|
1941
|
+
perf.inBytes =
|
|
1942
|
+
perf.outWrites =
|
|
1943
|
+
perf.outBytes =
|
|
1944
|
+
perf.jankMs =
|
|
1945
|
+
0;
|
|
1946
|
+
}
|
|
1947
|
+
function perfFrameLoop() {
|
|
1948
|
+
if (!perfOn) {
|
|
1949
|
+
perfRaf = 0;
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
perf.frames++;
|
|
1953
|
+
perfRaf = requestAnimationFrame(perfFrameLoop);
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
// Long tasks fire regardless of the HUD; we only sum them while it's on.
|
|
1957
|
+
new PerformanceObserver((l) => {
|
|
1958
|
+
if (perfOn) for (const e of l.getEntries()) perf.jankMs += e.duration;
|
|
1959
|
+
}).observe({ entryTypes: ["longtask"] });
|
|
1960
|
+
} catch {
|
|
1961
|
+
/* longtask API unavailable (Safari) — jank line just stays 0 */
|
|
1962
|
+
}
|
|
1963
|
+
const fmtBytes = (b) => (b >= 1024 ? (b / 1024).toFixed(1) + " KB" : Math.round(b) + " B");
|
|
1964
|
+
setInterval(() => {
|
|
1965
|
+
const el = $("perfhud");
|
|
1966
|
+
if (!perfOn || !el) {
|
|
1967
|
+
perfReset();
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const fps = perf.frames; // window is ~1s
|
|
1971
|
+
el.innerHTML =
|
|
1972
|
+
`<b>fps</b> ${fps}\n` +
|
|
1973
|
+
`<b>echo</b> ${perf.echoMs == null ? "—" : perf.echoMs + " ms"}\n` +
|
|
1974
|
+
`<b>keys</b> ${perf.inEvents}/s ${fmtBytes(perf.inBytes)}/s\n` +
|
|
1975
|
+
`<b>out</b> ${perf.outWrites}/s ${fmtBytes(perf.outBytes)}/s\n` +
|
|
1976
|
+
`<b>jank</b> ${Math.round(perf.jankMs)} ms/s`;
|
|
1977
|
+
perfReset(); // note: echoMs is preserved (last keystroke round-trip stays shown)
|
|
1978
|
+
}, 1000);
|
|
1979
|
+
function setPerfHud(on) {
|
|
1980
|
+
perfOn = on;
|
|
1981
|
+
const el = $("perfhud");
|
|
1982
|
+
if (el) el.hidden = !on;
|
|
1983
|
+
const cb = $("perfToggle");
|
|
1984
|
+
if (cb) cb.checked = on;
|
|
1985
|
+
try {
|
|
1986
|
+
localStorage.setItem("ay.perfHud", on ? "1" : "0");
|
|
1987
|
+
} catch {}
|
|
1988
|
+
if (on) {
|
|
1989
|
+
perfReset();
|
|
1990
|
+
if (!perfRaf) perfRaf = requestAnimationFrame(perfFrameLoop);
|
|
1991
|
+
if (el) el.textContent = "measuring…";
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
// Wire the ⋯ overflow menu and restore the saved HUD preference.
|
|
1995
|
+
// Recovery actions for the SELECTED agent, both confirmed first (destructive):
|
|
1996
|
+
// Stop — graceful: send the CLI's exit command (claude/codex /exit, gemini
|
|
1997
|
+
// /quit) + Enter over the existing /api/send stdin wire. (Ctrl+C / Ctrl+C×2
|
|
1998
|
+
// you can already just type — and some agents rely on that pattern — so
|
|
1999
|
+
// we don't reinvent it here.)
|
|
2000
|
+
// Force — escalation a stuck agent can't ignore: a real SIGKILL of its process
|
|
2001
|
+
// GROUP, server-side via POST /api/kill. Use when /exit does nothing.
|
|
2002
|
+
const GRACEFUL_EXIT = { claude: "/exit", codex: "/exit", gemini: "/quit" };
|
|
2003
|
+
async function stopAgent(force) {
|
|
2004
|
+
const e = sel ? entries.find((x) => x._key === sel) : null;
|
|
2005
|
+
if (!e) return;
|
|
2006
|
+
const who = `pid ${e.pid} ${cliLabel(e) || ident(e) || ""}`.trim();
|
|
2007
|
+
const tx = txFor(e),
|
|
2008
|
+
kw = String(e.pid);
|
|
2009
|
+
if (force) {
|
|
2010
|
+
if (!confirm(`Force-kill (SIGKILL) ${who}?\nThis terminates the process immediately.`))
|
|
2011
|
+
return;
|
|
2012
|
+
tx.post("/api/kill", { keyword: kw }).catch(() => {});
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const graceful = GRACEFUL_EXIT[e.cli] || "/exit";
|
|
2016
|
+
if (!confirm(`Stop ${who} — send "${graceful}"?`)) return;
|
|
2017
|
+
const send = (msg) =>
|
|
2018
|
+
tx.post("/api/send", { keyword: kw, msg, code: "none" }).catch(() => {});
|
|
2019
|
+
await send(graceful);
|
|
2020
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2021
|
+
await send("\r");
|
|
2022
|
+
}
|
|
2023
|
+
(function perfMenuBoot() {
|
|
2024
|
+
const btn = $("rmenubtn"),
|
|
2025
|
+
panel = $("rmenupanel"),
|
|
2026
|
+
cb = $("perfToggle");
|
|
2027
|
+
const closePanel = () => panel && (panel.hidden = true);
|
|
2028
|
+
if (btn && panel) {
|
|
2029
|
+
btn.addEventListener("click", (ev) => {
|
|
2030
|
+
ev.stopPropagation();
|
|
2031
|
+
panel.hidden = !panel.hidden;
|
|
2032
|
+
});
|
|
2033
|
+
document.addEventListener("click", (ev) => {
|
|
2034
|
+
if (!panel.hidden && !panel.contains(ev.target) && ev.target !== btn) closePanel();
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
if (cb) cb.addEventListener("change", () => setPerfHud(cb.checked));
|
|
2038
|
+
$("stopAgent")?.addEventListener("click", () => (closePanel(), stopAgent(false)));
|
|
2039
|
+
$("killAgent")?.addEventListener("click", () => (closePanel(), stopAgent(true)));
|
|
2040
|
+
let saved = false;
|
|
2041
|
+
try {
|
|
2042
|
+
saved = localStorage.getItem("ay.perfHud") === "1";
|
|
2043
|
+
} catch {}
|
|
2044
|
+
setPerfHud(saved);
|
|
2045
|
+
})();
|
|
2046
|
+
|
|
1418
2047
|
// The local source is only worth polling when this page is actually backed
|
|
1419
2048
|
// by an ay serve: localhost, or served by `ay serve --http` (which leaves a
|
|
1420
2049
|
// token), or when there are no rooms to fall back on. On the public origin
|
|
@@ -1545,28 +2174,62 @@
|
|
|
1545
2174
|
return `<span class="git${g.dirty ? " dirty" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
|
|
1546
2175
|
}
|
|
1547
2176
|
|
|
2177
|
+
// Task-progress badge ("2/5") parsed from the agent's todo block, shown next
|
|
2178
|
+
// to the git chip. Amber-ish when all done; omitted entirely when no block.
|
|
2179
|
+
function taskChipHtml(e) {
|
|
2180
|
+
const label = taskLabel(e);
|
|
2181
|
+
if (!label) return "";
|
|
2182
|
+
const t = e.tasks || {};
|
|
2183
|
+
const allDone = t.total > 0 && t.done >= t.total;
|
|
2184
|
+
const tip = `tasks: ${t.done || 0} done / ${t.total || 0} total`;
|
|
2185
|
+
return `<span class="tasks${allDone ? " done" : ""}" title="${esc(tip)}">${esc(label)}</span>`;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// A room/peer group header row. Non-selectable; carries the same tree rails
|
|
2189
|
+
// as agent rows so the hierarchy reads as one tree. The room/peer layers
|
|
2190
|
+
// only appear here when there are ≥2 of them (single ones fold away in
|
|
2191
|
+
// layeredRows), so a header always disambiguates something real.
|
|
2192
|
+
function headerHtml(r) {
|
|
2193
|
+
const icon = r.kind === "room" ? "▤" : "▣";
|
|
2194
|
+
// Peer headers carry their host's connection type (lan/wan/relay), so a
|
|
2195
|
+
// mixed room shows at a glance which hosts are local vs reached over WAN.
|
|
2196
|
+
const ctag =
|
|
2197
|
+
r.kind === "peer"
|
|
2198
|
+
? connTagHtml(hostKinds.get((r.room || "") + " " + (r.host || "")))
|
|
2199
|
+
: "";
|
|
2200
|
+
return `<div class="ghead g-${r.kind}">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="gicon">${icon}</span><span class="glabel">${esc(r.label || "")}</span>${ctag}<span class="gkind">${r.kind}</span></div>`;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
1548
2203
|
function renderList() {
|
|
1549
2204
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
1550
|
-
|
|
1551
|
-
|
|
2205
|
+
// Build the full signalling-server > rooms > peers > agents > subagents
|
|
2206
|
+
// tree. Single-node room/peer layers fold away; ≥2 become ├ └ │ branches.
|
|
2207
|
+
const rows = layeredRows(entries.filter((e) => matches(e, toks)));
|
|
2208
|
+
const agentRows = rows.filter((r) => r.kind === "agent");
|
|
2209
|
+
$("count").textContent = `${agentRows.length} / ${entries.length} agents`;
|
|
1552
2210
|
$("viewbtn").classList.toggle("on", compactList);
|
|
1553
2211
|
// identContext blanks any field uniform across the shown list (so a
|
|
1554
2212
|
// single-device fleet shows no device); multiDevice gates the detailed
|
|
1555
2213
|
// host tag so it only appears when machines are actually mixed.
|
|
1556
|
-
const
|
|
1557
|
-
const
|
|
2214
|
+
const shownEntries = agentRows.map((r) => r.entry);
|
|
2215
|
+
const ctx = identContext(shownEntries);
|
|
2216
|
+
const multiDevice = deviceCount(shownEntries) > 1;
|
|
1558
2217
|
if (compactList) {
|
|
1559
2218
|
$("list").innerHTML =
|
|
1560
|
-
|
|
1561
|
-
.map((
|
|
2219
|
+
rows
|
|
2220
|
+
.map((r) => {
|
|
2221
|
+
if (r.kind !== "agent") return headerHtml(r);
|
|
2222
|
+
const e = r.entry;
|
|
1562
2223
|
const t = e.title || e.prompt || "";
|
|
1563
|
-
const id = compactIdent(e, ctx);
|
|
2224
|
+
const id = compactIdent(e, ctx, 3, r.parentEntry);
|
|
1564
2225
|
const cli = cliLabel(e);
|
|
1565
2226
|
return `<div class="row crow ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
|
|
2227
|
+
${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}
|
|
1566
2228
|
<span class="dot ${esc(e.status)}"></span>
|
|
1567
2229
|
${hasIdent(id) ? `<span class="cident" title="${esc(fullIdent(e))}">${esc(id)}</span>` : ""}
|
|
1568
2230
|
${cli ? `<span class="cname">${esc(cli)}</span>` : ""}
|
|
1569
2231
|
<span class="ctitle ${e.title ? "" : "dim"}" title="${esc(t)}">${esc(t)}</span>
|
|
2232
|
+
${taskChipHtml(e)}
|
|
1570
2233
|
${gitChipHtml(e)}
|
|
1571
2234
|
<span class="age">${age(e)}</span></div>`;
|
|
1572
2235
|
})
|
|
@@ -1574,10 +2237,12 @@
|
|
|
1574
2237
|
return;
|
|
1575
2238
|
}
|
|
1576
2239
|
$("list").innerHTML =
|
|
1577
|
-
|
|
1578
|
-
.map((
|
|
2240
|
+
rows
|
|
2241
|
+
.map((r) => {
|
|
2242
|
+
if (r.kind !== "agent") return headerHtml(r);
|
|
2243
|
+
const e = r.entry;
|
|
1579
2244
|
// The host tag only earns its place when several machines are in
|
|
1580
|
-
// play
|
|
2245
|
+
// play AND it isn't already shown by a peer header above this row.
|
|
1581
2246
|
const tags = tagsFor(e)
|
|
1582
2247
|
.filter(([k]) => k !== "host" || multiDevice)
|
|
1583
2248
|
.map(
|
|
@@ -1586,9 +2251,10 @@
|
|
|
1586
2251
|
)
|
|
1587
2252
|
.join("");
|
|
1588
2253
|
return `<div class="row ${e._key === sel ? "sel" : ""}" data-key="${esc(e._key)}">
|
|
1589
|
-
<div class="r1"
|
|
2254
|
+
<div class="r1">${r.branch ? `<span class="tbranch">${esc(r.branch)}</span>` : ""}<span class="dot ${esc(e.status)}"></span>
|
|
1590
2255
|
<span class="name">${esc(cliLabel(e) || ident(e) || "agent")}</span>
|
|
1591
2256
|
<span class="badge">pid ${e.pid}</span>
|
|
2257
|
+
${taskChipHtml(e)}
|
|
1592
2258
|
${gitChipHtml(e)}
|
|
1593
2259
|
<span class="age">${age(e)}</span></div>
|
|
1594
2260
|
${e.title ? `<div class="rowtitle" title="${esc(e.title)}">${esc(e.title)}</div>` : ""}
|
|
@@ -1690,6 +2356,7 @@
|
|
|
1690
2356
|
return;
|
|
1691
2357
|
}
|
|
1692
2358
|
sel = e._key;
|
|
2359
|
+
paintHeaderBadge();
|
|
1693
2360
|
// Remember the selection so a refresh re-opens this agent (see boot/autoPid).
|
|
1694
2361
|
try {
|
|
1695
2362
|
localStorage.setItem("ay.sel", sel);
|
|
@@ -1796,8 +2463,10 @@
|
|
|
1796
2463
|
// keyword with 400 (pid arrives as a number from /api/ls JSON).
|
|
1797
2464
|
const kw = String(pid);
|
|
1798
2465
|
const fwd = (d) => {
|
|
1799
|
-
if (sel === e._key)
|
|
2466
|
+
if (sel === e._key) {
|
|
2467
|
+
perfNote("in", typeof d === "string" ? d.length : (d?.byteLength ?? 0));
|
|
1800
2468
|
tx.post("/api/send", { keyword: kw, msg: d, code: "none" }).catch(() => {});
|
|
2469
|
+
}
|
|
1801
2470
|
};
|
|
1802
2471
|
term.onData(fwd);
|
|
1803
2472
|
term.onBinary(fwd);
|
|
@@ -1837,7 +2506,10 @@
|
|
|
1837
2506
|
const close = tx.subscribe(
|
|
1838
2507
|
"/api/tail/" + encodeURIComponent(pid) + "?raw=1",
|
|
1839
2508
|
(raw) => {
|
|
1840
|
-
if (term)
|
|
2509
|
+
if (term) {
|
|
2510
|
+
term.write(raw);
|
|
2511
|
+
perfNote("out", raw?.length ?? 0);
|
|
2512
|
+
}
|
|
1841
2513
|
},
|
|
1842
2514
|
() => {
|
|
1843
2515
|
$("livedot").className = "dot active";
|
|
@@ -1882,7 +2554,10 @@
|
|
|
1882
2554
|
// renders. Clamps at the ends, scrolls the row into view.
|
|
1883
2555
|
function stepSelection(dir) {
|
|
1884
2556
|
const toks = $("q").value.trim().split(/\s+/).filter(Boolean);
|
|
1885
|
-
|
|
2557
|
+
// Must match renderList's order; step only over agent rows (skip headers).
|
|
2558
|
+
const shown = layeredRows(entries.filter((e) => matches(e, toks)))
|
|
2559
|
+
.filter((r) => r.kind === "agent")
|
|
2560
|
+
.map((r) => r.entry);
|
|
1886
2561
|
if (!shown.length) return;
|
|
1887
2562
|
const cur = shown.findIndex((e) => e._key === sel);
|
|
1888
2563
|
const next = shown[nextIndex(shown.length, cur, dir)];
|