agent-yes 1.201.0 → 1.202.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.
Files changed (38) hide show
  1. package/default.config.yaml +8 -8
  2. package/dist/{SUPPORTED_CLIS-CnGjmp53.js → SUPPORTED_CLIS-Cc4YcXL_.js} +2 -2
  3. package/dist/SUPPORTED_CLIS-ChdPabFX.js +9 -0
  4. package/dist/{agentShare-lDreSLQE.js → agentShare-e4ErqbAZ.js} +2 -2
  5. package/dist/cli.js +4 -4
  6. package/dist/index.js +2 -2
  7. package/dist/{notifyDaemon-C4VzLmmO.js → notifyDaemon-Bo-RiQ2N.js} +2 -2
  8. package/dist/{rustBinary-CVZsX0za.js → rustBinary-Dd7-HOFy.js} +2 -2
  9. package/dist/{schedule-hdbXBwpG.js → schedule-Hu3krhNK.js} +4 -4
  10. package/dist/{serve-DEEesVYn.js → serve-D4B43UQC.js} +12 -12
  11. package/dist/{setup-DZbnDas9.js → setup-hUonzP6V.js} +2 -2
  12. package/dist/{subcommands-IwpLJA-n.js → subcommands-DQmW41_L.js} +1 -1
  13. package/dist/{subcommands-De-OHc0k.js → subcommands-DbiyHGKA.js} +53 -11
  14. package/dist/{ts-ZOEYyQAX.js → ts--ZOQIaTr.js} +2 -2
  15. package/dist/{versionChecker-CBtAasFe.js → versionChecker-D2egG8Wk.js} +2 -2
  16. package/dist/{ws-DLiI3C6R.js → ws-CygS4IEs.js} +2 -2
  17. package/lab/ui/console-logic.js +32 -2
  18. package/lab/ui/index.html +64 -20
  19. package/lab/ui/qrcode.js +548 -565
  20. package/lab/ui/room-client.js +70 -91
  21. package/lab/ui/sw.js +17 -7
  22. package/package.json +7 -7
  23. package/scripts/build-rgui.ts +3 -1
  24. package/ts/agentShare.spec.ts +5 -3
  25. package/ts/badges.spec.ts +84 -1
  26. package/ts/badges.ts +69 -5
  27. package/ts/notifyDaemon.spec.ts +2 -8
  28. package/ts/notifyDaemon.ts +32 -33
  29. package/ts/notifyInbox.spec.ts +2 -1
  30. package/ts/notifyRouter.spec.ts +7 -11
  31. package/ts/notifyStore.spec.ts +4 -1
  32. package/ts/notifyStore.ts +18 -14
  33. package/ts/serve.spec.ts +4 -6
  34. package/ts/serve.ts +69 -17
  35. package/ts/subcommands.ts +46 -18
  36. package/ts/ws.spec.ts +31 -7
  37. package/ts/ws.ts +15 -4
  38. package/dist/SUPPORTED_CLIS-BbWt8-ON.js +0 -9
@@ -32,11 +32,9 @@ class SignalingClient {
32
32
  this.open();
33
33
  }
34
34
  onWake = () => {
35
- if (this.closed)
36
- return;
35
+ if (this.closed) return;
37
36
  const state = this.ws?.readyState;
38
- if (state === 1)
39
- return;
37
+ if (state === 1) return;
40
38
  if (state === 0) {
41
39
  try {
42
40
  this.ws?.close();
@@ -92,7 +90,7 @@ class SignalingClient {
92
90
  type: "hello",
93
91
  role: this.opts.role,
94
92
  peerId: this.peerId,
95
- ...this.opts.meta ? { meta: this.opts.meta } : {}
93
+ ...(this.opts.meta ? { meta: this.opts.meta } : {}),
96
94
  };
97
95
  ws.send(JSON.stringify(hello));
98
96
  this.startHeartbeat();
@@ -105,10 +103,8 @@ class SignalingClient {
105
103
  } catch {
106
104
  return;
107
105
  }
108
- if (msg.type === "peers")
109
- this.opts.onPeers?.(msg.peers, msg.now);
110
- else if (msg.type === "signal")
111
- this.opts.onSignal?.(msg.from, msg.data);
106
+ if (msg.type === "peers") this.opts.onPeers?.(msg.peers, msg.now);
107
+ else if (msg.type === "signal") this.opts.onSignal?.(msg.from, msg.data);
112
108
  };
113
109
  ws.onclose = (ev) => {
114
110
  clearTimeout(connectTimer);
@@ -117,8 +113,7 @@ class SignalingClient {
117
113
  const ms = this.openedAt ? Date.now() - this.openedAt : 0;
118
114
  this.openedAt = 0;
119
115
  this.opts.onClose?.({ code: ev?.code ?? 0, reason: ev?.reason ?? "", ms });
120
- if (!this.closed)
121
- this.scheduleReconnect();
116
+ if (!this.closed) this.scheduleReconnect();
122
117
  };
123
118
  ws.onerror = () => {
124
119
  try {
@@ -156,8 +151,7 @@ class SignalingClient {
156
151
  this.clearReconnectTimer();
157
152
  this.reconnectTimer = setTimeout(() => {
158
153
  this.reconnectTimer = null;
159
- if (this.closed)
160
- return;
154
+ if (this.closed) return;
161
155
  if (this.hidden()) {
162
156
  this.dormant = true;
163
157
  return;
@@ -196,10 +190,7 @@ class SignalingClient {
196
190
  }
197
191
 
198
192
  // src/shared/rtc.ts
199
- var ICE_SERVERS = [
200
- "stun:stun.l.google.com:19302",
201
- "stun:stun1.l.google.com:19302"
202
- ];
193
+ var ICE_SERVERS = ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"];
203
194
  var CHANNEL_LABEL = "codehost";
204
195
  var BULK_CHANNEL_LABEL = "codehost-bulk";
205
196
 
@@ -212,14 +203,14 @@ class RtcClient {
212
203
  constructor(opts) {
213
204
  this.opts = opts;
214
205
  this.pc = new RTCPeerConnection({
215
- iceServers: ICE_SERVERS.map((urls) => ({ urls }))
206
+ iceServers: ICE_SERVERS.map((urls) => ({ urls })),
216
207
  });
217
208
  this.pc.onicecandidate = (ev) => {
218
209
  if (ev.candidate) {
219
210
  this.opts.sendSignal({
220
211
  kind: "candidate",
221
212
  candidate: ev.candidate.candidate,
222
- mid: ev.candidate.sdpMid ?? "0"
213
+ mid: ev.candidate.sdpMid ?? "0",
223
214
  });
224
215
  }
225
216
  };
@@ -242,8 +233,7 @@ class RtcClient {
242
233
  }
243
234
  async handleSignal(data) {
244
235
  const sig = data;
245
- if (!sig || typeof sig !== "object")
246
- return;
236
+ if (!sig || typeof sig !== "object") return;
247
237
  if (sig.kind === "answer") {
248
238
  await this.pc.setRemoteDescription({ type: "answer", sdp: sig.sdp });
249
239
  } else if (sig.kind === "candidate") {
@@ -265,29 +255,29 @@ class RtcClient {
265
255
  const stats = await this.pc.getStats();
266
256
  let pairId = null;
267
257
  stats.forEach((s) => {
268
- if (s.type === "transport" && s.selectedCandidatePairId)
269
- pairId = s.selectedCandidatePairId;
258
+ if (s.type === "transport" && s.selectedCandidatePairId) pairId = s.selectedCandidatePairId;
270
259
  });
271
260
  let pair = null;
272
261
  stats.forEach((s) => {
273
- if (pairId ? s.id === pairId : s.type === "candidate-pair" && s.state === "succeeded" && s.nominated) {
262
+ if (
263
+ pairId
264
+ ? s.id === pairId
265
+ : s.type === "candidate-pair" && s.state === "succeeded" && s.nominated
266
+ ) {
274
267
  pair = s;
275
268
  }
276
269
  });
277
- if (!pair)
278
- return null;
270
+ if (!pair) return null;
279
271
  const { localCandidateId, remoteCandidateId } = pair;
280
272
  let lan = true;
281
273
  let found = 0;
282
274
  stats.forEach((s) => {
283
275
  if (s.id === localCandidateId || s.id === remoteCandidateId) {
284
276
  found++;
285
- if (s.candidateType !== "host")
286
- lan = false;
277
+ if (s.candidateType !== "host") lan = false;
287
278
  }
288
279
  });
289
- if (found < 2)
290
- return null;
280
+ if (found < 2) return null;
291
281
  return lan ? "lan" : "p2p";
292
282
  } catch {
293
283
  return null;
@@ -310,15 +300,14 @@ class RtcClient {
310
300
  var FRAME_HEADER = 5;
311
301
  var MAX_FRAME = 64 * 1024;
312
302
  var MAX_CHUNK = MAX_FRAME - FRAME_HEADER;
313
- var enc = new TextEncoder;
314
- var dec = new TextDecoder;
303
+ var enc = new TextEncoder();
304
+ var dec = new TextDecoder();
315
305
  function encodeFrame(op, streamId, payload) {
316
306
  const len = payload?.byteLength ?? 0;
317
307
  const buf = new Uint8Array(5 + len);
318
308
  buf[0] = op;
319
309
  new DataView(buf.buffer).setUint32(1, streamId >>> 0, false);
320
- if (payload && len)
321
- buf.set(payload, 5);
310
+ if (payload && len) buf.set(payload, 5);
322
311
  return buf;
323
312
  }
324
313
  function encodeJson(op, streamId, obj) {
@@ -338,13 +327,12 @@ function payloadText(payload) {
338
327
  return dec.decode(payload);
339
328
  }
340
329
  function* chunk(body) {
341
- for (let off = 0;off < body.byteLength; off += MAX_CHUNK) {
330
+ for (let off = 0; off < body.byteLength; off += MAX_CHUNK) {
342
331
  yield body.slice(off, Math.min(off + MAX_CHUNK, body.byteLength));
343
332
  }
344
333
  }
345
334
  function concatBytes(parts) {
346
- if (parts.length === 1)
347
- return parts[0];
335
+ if (parts.length === 1) return parts[0];
348
336
  const total = parts.reduce((n, p) => n + p.byteLength, 0);
349
337
  const out = new Uint8Array(total);
350
338
  let off = 0;
@@ -364,18 +352,15 @@ function* wsMessageFrames(terminal, streamId, payload) {
364
352
  }
365
353
 
366
354
  class WsReassembler {
367
- pending = new Map;
355
+ pending = new Map();
368
356
  cont(streamId, payload) {
369
357
  const buf = this.pending.get(streamId);
370
- if (buf)
371
- buf.push(payload.slice());
372
- else
373
- this.pending.set(streamId, [payload.slice()]);
358
+ if (buf) buf.push(payload.slice());
359
+ else this.pending.set(streamId, [payload.slice()]);
374
360
  }
375
361
  finish(streamId, payload) {
376
362
  const buf = this.pending.get(streamId);
377
- if (!buf)
378
- return payload;
363
+ if (!buf) return payload;
379
364
  this.pending.delete(streamId);
380
365
  buf.push(payload);
381
366
  return concatBytes(buf);
@@ -390,11 +375,11 @@ class TunnelClient {
390
375
  transport;
391
376
  bulk;
392
377
  nextStreamId = 1;
393
- https = new Map;
394
- httpLane = new Map;
395
- wss = new Map;
396
- wsRx = new WsReassembler;
397
- textEncoder = new TextEncoder;
378
+ https = new Map();
379
+ httpLane = new Map();
380
+ wss = new Map();
381
+ wsRx = new WsReassembler();
382
+ textEncoder = new TextEncoder();
398
383
  constructor(transport, bulk = null) {
399
384
  this.transport = transport;
400
385
  this.bulk = bulk;
@@ -405,8 +390,7 @@ class TunnelClient {
405
390
  }
406
391
  failLane(lane, interactive) {
407
392
  for (const [streamId, waiter] of [...this.https]) {
408
- if ((this.httpLane.get(streamId) ?? this.transport) !== lane)
409
- continue;
393
+ if ((this.httpLane.get(streamId) ?? this.transport) !== lane) continue;
410
394
  this.https.delete(streamId);
411
395
  this.httpLane.delete(streamId);
412
396
  waiter.onError("tunnel closed");
@@ -421,7 +405,7 @@ class TunnelClient {
421
405
  }
422
406
  allocId() {
423
407
  const id = this.nextStreamId;
424
- this.nextStreamId = this.nextStreamId + 1 >>> 0 || 1;
408
+ this.nextStreamId = (this.nextStreamId + 1) >>> 0 || 1;
425
409
  return id;
426
410
  }
427
411
  onFrame(data) {
@@ -478,9 +462,12 @@ class TunnelClient {
478
462
  const stream = new ReadableStream({
479
463
  start: (c) => {
480
464
  controller = c;
481
- }
465
+ },
482
466
  });
483
- const reqHeaders = typeof DecompressionStream !== "undefined" ? { ...headers, "x-codehost-accept-gzip": "1" } : headers;
467
+ const reqHeaders =
468
+ typeof DecompressionStream !== "undefined"
469
+ ? { ...headers, "x-codehost-accept-gzip": "1" }
470
+ : headers;
484
471
  this.https.set(streamId, {
485
472
  onHead: (h) => {
486
473
  head = h;
@@ -491,11 +478,13 @@ class TunnelClient {
491
478
  resHeaders.delete("content-encoding");
492
479
  resHeaders.delete("content-length");
493
480
  }
494
- resolve(new Response(bodyStream, {
495
- status: h.status === 204 || h.status === 304 ? h.status : h.status,
496
- statusText: h.statusText,
497
- headers: resHeaders
498
- }));
481
+ resolve(
482
+ new Response(bodyStream, {
483
+ status: h.status === 204 || h.status === 304 ? h.status : h.status,
484
+ statusText: h.statusText,
485
+ headers: resHeaders,
486
+ }),
487
+ );
499
488
  },
500
489
  onBody: (b) => {
501
490
  try {
@@ -506,20 +495,21 @@ class TunnelClient {
506
495
  try {
507
496
  controller?.close();
508
497
  } catch {}
509
- if (!head)
510
- reject(new Error("stream ended before head"));
498
+ if (!head) reject(new Error("stream ended before head"));
511
499
  },
512
500
  onError: (msg) => {
513
501
  try {
514
502
  controller?.error(new Error(msg));
515
503
  } catch {}
516
- if (!head)
517
- reject(new Error(msg));
518
- }
504
+ if (!head) reject(new Error(msg));
505
+ },
519
506
  });
520
507
  const lane = this.bulk?.isOpen() ? this.bulk : this.transport;
521
508
  this.httpLane.set(streamId, lane);
522
- this.sendOn(lane, encodeJson(1 /* HttpReq */, streamId, { method, path, headers: reqHeaders }));
509
+ this.sendOn(
510
+ lane,
511
+ encodeJson(1 /* HttpReq */, streamId, { method, path, headers: reqHeaders }),
512
+ );
523
513
  if (body && body.byteLength) {
524
514
  for (const part of chunk(body))
525
515
  this.sendOn(lane, encodeFrame(2 /* HttpReqBody */, streamId, part));
@@ -537,21 +527,19 @@ class TunnelClient {
537
527
  this.send(f);
538
528
  },
539
529
  sendBin: (data) => {
540
- for (const f of wsMessageFrames(10 /* WsBin */, streamId, data))
541
- this.send(f);
530
+ for (const f of wsMessageFrames(10 /* WsBin */, streamId, data)) this.send(f);
542
531
  },
543
532
  close: (code, reason) => {
544
533
  this.send(encodeJson(11 /* WsClose */, streamId, { code, reason }));
545
534
  this.wss.delete(streamId);
546
- }
535
+ },
547
536
  };
548
537
  }
549
538
  send(frame) {
550
539
  this.sendOn(this.transport, frame);
551
540
  }
552
541
  sendOn(t, frame) {
553
- if (t.isOpen())
554
- t.send(frame);
542
+ if (t.isOpen()) t.send(frame);
555
543
  }
556
544
  get ready() {
557
545
  return this.transport.isOpen();
@@ -575,14 +563,13 @@ function rtcDataChannelTransport(channel) {
575
563
  },
576
564
  onFrame(cb) {
577
565
  channel.addEventListener("message", (ev) => {
578
- if (typeof ev.data === "string")
579
- return;
566
+ if (typeof ev.data === "string") return;
580
567
  cb(new Uint8Array(ev.data));
581
568
  });
582
569
  },
583
570
  onClose(cb) {
584
571
  channel.addEventListener("close", cb);
585
- }
572
+ },
586
573
  };
587
574
  }
588
575
 
@@ -600,9 +587,9 @@ var DIAL_FAIL_COOLDOWN_MS = 1e4;
600
587
  class CodehostRoom {
601
588
  peers = [];
602
589
  signaling;
603
- rtcs = new Map;
604
- tunnels = new Map;
605
- dialFailedAt = new Map;
590
+ rtcs = new Map();
591
+ tunnels = new Map();
592
+ dialFailedAt = new Map();
606
593
  closed = false;
607
594
  constructor(opts) {
608
595
  this.signaling = new SignalingClient({
@@ -615,7 +602,7 @@ class CodehostRoom {
615
602
  this.peers = peers.filter((p) => p.role === "server");
616
603
  opts.onPeers?.(this.peers);
617
604
  },
618
- onSignal: (from, data) => void this.rtcs.get(from)?.handleSignal(data)
605
+ onSignal: (from, data) => void this.rtcs.get(from)?.handleSignal(data),
619
606
  });
620
607
  this.signaling.connect();
621
608
  }
@@ -630,8 +617,7 @@ class CodehostRoom {
630
617
  }
631
618
  dial(peerId) {
632
619
  const existing = this.tunnels.get(peerId);
633
- if (existing)
634
- return existing;
620
+ if (existing) return existing;
635
621
  const failedAt = this.dialFailedAt.get(peerId);
636
622
  if (failedAt != null && Date.now() - failedAt < DIAL_FAIL_COOLDOWN_MS) {
637
623
  return Promise.reject(new Error("dial failed recently; cooling down"));
@@ -655,9 +641,8 @@ class CodehostRoom {
655
641
  },
656
642
  onClose: drop,
657
643
  onState: (state) => {
658
- if (state === "failed" || state === "disconnected")
659
- drop();
660
- }
644
+ if (state === "failed" || state === "disconnected") drop();
645
+ },
661
646
  });
662
647
  this.rtcs.set(peerId, rtc);
663
648
  rtc.start().catch((err) => {
@@ -674,11 +659,9 @@ class CodehostRoom {
674
659
  return dialing;
675
660
  }
676
661
  close() {
677
- if (this.closed)
678
- return;
662
+ if (this.closed) return;
679
663
  this.closed = true;
680
- for (const rtc of this.rtcs.values())
681
- rtc.close();
664
+ for (const rtc of this.rtcs.values()) rtc.close();
682
665
  this.rtcs.clear();
683
666
  this.tunnels.clear();
684
667
  this.signaling.close();
@@ -687,8 +670,4 @@ class CodehostRoom {
687
670
  function joinRoom(opts) {
688
671
  return new CodehostRoom(opts);
689
672
  }
690
- export {
691
- joinRoom,
692
- DEFAULT_SIGNAL_URL,
693
- CodehostRoom
694
- };
673
+ export { joinRoom, DEFAULT_SIGNAL_URL, CodehostRoom };
package/lab/ui/sw.js CHANGED
@@ -13,7 +13,9 @@ const CACHE = "agent-yes-w-v3";
13
13
  // request to the controlling page (which does), streaming the response back.
14
14
  // Scope-relative so it works at both /w/ (agent-yes.com) and / (ay serve --http).
15
15
  const BASE = new URL("./", self.location.href).pathname; // "/w/" or "/"
16
- const PREVIEW = new RegExp("^" + BASE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "p/([^/]+)/(\\d{1,5})(/.*)?$");
16
+ const PREVIEW = new RegExp(
17
+ "^" + BASE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "p/([^/]+)/(\\d{1,5})(/.*)?$",
18
+ );
17
19
 
18
20
  async function pickClient() {
19
21
  const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
@@ -53,8 +55,10 @@ async function proxyPreview(request, src, port, rest, allowInject) {
53
55
  mc.port1.onmessage = (e2) => {
54
56
  const m2 = e2.data;
55
57
  if (m2.type === "body") chunks.push(new Uint8Array(m2.chunk));
56
- else if (m2.type === "end") resolve(injectBootstrap(chunks, msg, headers, src, Number(port)));
57
- else if (m2.type === "error") resolve(new Response("preview error: " + m2.message, { status: 502 }));
58
+ else if (m2.type === "end")
59
+ resolve(injectBootstrap(chunks, msg, headers, src, Number(port)));
60
+ else if (m2.type === "error")
61
+ resolve(new Response("preview error: " + m2.message, { status: 502 }));
58
62
  };
59
63
  return;
60
64
  }
@@ -71,7 +75,15 @@ async function proxyPreview(request, src, port, rest, allowInject) {
71
75
  resolve(new Response(stream, { status: msg.status, statusText: msg.statusText, headers }));
72
76
  };
73
77
  client.postMessage(
74
- { type: "ay-preview-fetch", src, port: Number(port), method: request.method, path: rest, headers, body },
78
+ {
79
+ type: "ay-preview-fetch",
80
+ src,
81
+ port: Number(port),
82
+ method: request.method,
83
+ path: rest,
84
+ headers,
85
+ body,
86
+ },
75
87
  [mc.port2, ...(body ? [body.buffer] : [])],
76
88
  );
77
89
  });
@@ -99,9 +111,7 @@ function injectBootstrap(chunks, msg, headers, src, port) {
99
111
  JSON.stringify(port) +
100
112
  ");}catch(e){}})();</" +
101
113
  "script>";
102
- const html = /<head[^>]*>/i.test(raw)
103
- ? raw.replace(/<head[^>]*>/i, (m) => m + boot)
104
- : boot + raw;
114
+ const html = /<head[^>]*>/i.test(raw) ? raw.replace(/<head[^>]*>/i, (m) => m + boot) : boot + raw;
105
115
  headers.delete("content-security-policy");
106
116
  headers.delete("content-security-policy-report-only");
107
117
  headers.delete("content-length");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.201.0",
3
+ "version": "1.202.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -33,7 +33,9 @@
33
33
  "amp-yes": "./dist/amp-yes.js",
34
34
  "auggie-yes": "./dist/auggie-yes.js",
35
35
  "ay": "./dist/agent-yes.js",
36
+ "bash-yes": "./dist/bash-yes.js",
36
37
  "claude-yes": "./dist/claude-yes.js",
38
+ "cmd-yes": "./dist/cmd-yes.js",
37
39
  "codex-yes": "./dist/codex-yes.js",
38
40
  "copilot-yes": "./dist/copilot-yes.js",
39
41
  "cursor-yes": "./dist/cursor-yes.js",
@@ -45,10 +47,8 @@
45
47
  "openrouter-yes": "./dist/openrouter-yes.js",
46
48
  "orcy": "./dist/orcy.js",
47
49
  "pi-yes": "./dist/pi-yes.js",
48
- "qwen-yes": "./dist/qwen-yes.js",
49
- "bash-yes": "./dist/bash-yes.js",
50
- "cmd-yes": "./dist/cmd-yes.js",
51
- "powershell-yes": "./dist/powershell-yes.js"
50
+ "powershell-yes": "./dist/powershell-yes.js",
51
+ "qwen-yes": "./dist/qwen-yes.js"
52
52
  },
53
53
  "directories": {
54
54
  "doc": "docs"
@@ -137,6 +137,7 @@
137
137
  "@types/yargs": "^17.0.35",
138
138
  "@typescript/native-preview": "^7.0.0-dev.20260124.1",
139
139
  "@vitest/coverage-v8": "4.1.0",
140
+ "codehost": "^0.32.0",
140
141
  "husky": "^9.1.7",
141
142
  "lint-staged": "^16.2.7",
142
143
  "node-pty": "^1.1.0",
@@ -150,8 +151,7 @@
150
151
  "tsdown": "^0.20.3",
151
152
  "vitest": "4.1.0",
152
153
  "ws": "^8.20.0",
153
- "zod": "^3.23.0",
154
- "codehost": "^0.32.0"
154
+ "zod": "^3.23.0"
155
155
  },
156
156
  "peerDependencies": {
157
157
  "node-pty": "latest",
@@ -45,7 +45,9 @@ if (!existsSync(path.join(RGUI_DIR, "node_modules/d3-selection"))) {
45
45
  await $`bun install --cwd ${RGUI_DIR}`;
46
46
  }
47
47
 
48
- const outdir = process.argv[2] ? path.resolve(process.argv[2]) : path.join(root, "lab/ui/rgui/dist");
48
+ const outdir = process.argv[2]
49
+ ? path.resolve(process.argv[2])
50
+ : path.join(root, "lab/ui/rgui/dist");
49
51
  await mkdir(outdir, { recursive: true });
50
52
 
51
53
  const result = await Bun.build({
@@ -147,9 +147,11 @@ describe("scopedFetch — /api/ls/subscribe remove + teardown", () => {
147
147
  }
148
148
 
149
149
  it("forwards removes only for pids it forwarded (sibling removals stay hidden)", async () => {
150
- const res = await scopedFetch(SHARED, removalInner(), "r")(
151
- new Request("http://ay.local/api/ls/subscribe"),
152
- );
150
+ const res = await scopedFetch(
151
+ SHARED,
152
+ removalInner(),
153
+ "r",
154
+ )(new Request("http://ay.local/api/ls/subscribe"));
153
155
  const events = (await sse(res))
154
156
  .split("\n\n")
155
157
  .filter((e) => e.startsWith("data:"))
package/ts/badges.spec.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { badgeDef, BADGE_DEFS, matchBadges, TYPING_BADGE, type BadgeDef } from "./badges.ts";
2
+ import {
3
+ badgeDef,
4
+ badgeLabel,
5
+ BADGE_DEFS,
6
+ matchBadges,
7
+ TYPING_BADGE,
8
+ type BadgeDef,
9
+ } from "./badges.ts";
3
10
 
4
11
  describe("matchBadges", () => {
5
12
  it("matches goal-active when the /goal status line is on screen", () => {
@@ -78,6 +85,61 @@ describe("matchBadges", () => {
78
85
  });
79
86
  });
80
87
 
88
+ describe("dynamic footer counters", () => {
89
+ // Real footer lines captured from live agents (`ay tail`), singular and plural.
90
+ it("matches the shell counter and carries the footer text on the wire", () => {
91
+ expect(
92
+ matchBadges([
93
+ " ⏸ manual mode on · 1 shell · ctrl+t to hide tasks · ← for agents · ↓ to manage",
94
+ ]),
95
+ ).toEqual(["shells:1 shell"]);
96
+ expect(matchBadges([" ⏸ manual mode on · 4 shells · ↓ to manage"])).toEqual([
97
+ "shells:4 shells",
98
+ ]);
99
+ });
100
+
101
+ it("matches the monitor counter", () => {
102
+ expect(
103
+ matchBadges([" ⏸ manual mode on · 3 monitors · esc to interrupt · ↓ to manage"]),
104
+ ).toEqual(["monitors:3 monitors"]);
105
+ expect(matchBadges([" ⏸ manual mode on · 1 monitor · ↓ to manage"])).toEqual([
106
+ "monitors:1 monitor",
107
+ ]);
108
+ });
109
+
110
+ it("matches the background-agents counter (← N agents) at end of line", () => {
111
+ expect(matchBadges([" ⏸ manual mode on · ? for shortcuts · ← 3 agents"])).toEqual([
112
+ "bg-agents:3 agents",
113
+ ]);
114
+ });
115
+
116
+ it("does not light bg-agents on the plain '← for agents' hint (no count)", () => {
117
+ expect(matchBadges([" ⏸ manual mode on · ctrl+t to hide tasks · ← for agents"])).toEqual([]);
118
+ });
119
+
120
+ it("matches the PR chip", () => {
121
+ expect(matchBadges([" ⏸ manual mode on · PR #310"])).toEqual(["pr:PR #310"]);
122
+ expect(matchBadges([" ⏸ manual mode on · PR #229 · esc to interrupt"])).toEqual([
123
+ "pr:PR #229",
124
+ ]);
125
+ });
126
+
127
+ it("is anchored to footer chrome — prose mentions don't match", () => {
128
+ expect(
129
+ matchBadges([
130
+ "I opened 4 shells and 2 monitors while checking PR #310 today",
131
+ "there are 3 agents running",
132
+ ]),
133
+ ).toEqual([]);
134
+ });
135
+
136
+ it("can light several counters on one footer line", () => {
137
+ expect(
138
+ matchBadges([" ⏸ manual mode on · 2 shells · 3 monitors · PR #12 · ← 5 agents"]),
139
+ ).toEqual(["shells:2 shells", "monitors:3 monitors", "bg-agents:5 agents", "pr:PR #12"]);
140
+ });
141
+ });
142
+
81
143
  describe("badgeDef", () => {
82
144
  it("looks up a built-in definition by id", () => {
83
145
  expect(badgeDef("goal-active")).toBe(BADGE_DEFS[0]);
@@ -87,12 +149,33 @@ describe("badgeDef", () => {
87
149
  expect(badgeDef("does-not-exist")).toBeUndefined();
88
150
  });
89
151
 
152
+ it("resolves a dynamic id by the part before the ':'", () => {
153
+ expect(badgeDef("shells:4 shells")?.id).toBe("shells");
154
+ });
155
+
90
156
  it("resolves the time-derived typing badge even though it isn't in BADGE_DEFS", () => {
91
157
  expect(badgeDef("typing")).toBe(TYPING_BADGE);
92
158
  expect(BADGE_DEFS).not.toContain(TYPING_BADGE);
93
159
  });
94
160
  });
95
161
 
162
+ describe("badgeLabel", () => {
163
+ it("returns the static label for a plain id", () => {
164
+ expect(badgeLabel("goal-active")).toBe("goal");
165
+ expect(badgeLabel("typing")).toBe("typing");
166
+ });
167
+
168
+ it("substitutes the captured footer text into a dynamic label", () => {
169
+ expect(badgeLabel("shells:1 shell")).toBe("1 shell");
170
+ expect(badgeLabel("shells:4 shells")).toBe("4 shells");
171
+ expect(badgeLabel("pr:PR #310")).toBe("PR #310");
172
+ });
173
+
174
+ it("falls back to the raw id for an unknown badge", () => {
175
+ expect(badgeLabel("some-future-flag")).toBe("some-future-flag");
176
+ });
177
+ });
178
+
96
179
  describe("TYPING_BADGE", () => {
97
180
  it("is never produced by screen matching (its pattern can't match)", () => {
98
181
  // Presence is derived from the stdin-activity marker, not the rendered