agent-yes 1.194.0 → 1.196.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SUPPORTED_CLIS-Ca2ffDRy.js +8 -0
- package/dist/{SUPPORTED_CLIS--WH2XfM2.js → SUPPORTED_CLIS-CxLCQtAU.js} +2 -2
- package/dist/{agentShare-BEXN8bNT.js → agentShare-Bi95Xy0S.js} +2 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-Deisyrbn.js → notifyDaemon-BdOeFIVl.js} +2 -2
- package/dist/{rustBinary-cTIMKGQp.js → rustBinary-D_ifX2Vd.js} +2 -2
- package/dist/{schedule-MQZjDaY6.js → schedule-rvMgc0tA.js} +4 -4
- package/dist/{serve-hA23xPpg.js → serve-D0-LoTfw.js} +43 -13
- package/dist/{setup-C6grPf1R.js → setup-BC1VjTOf.js} +2 -2
- package/dist/subcommands-Cz8xpc5X.js +9 -0
- package/dist/{subcommands-Ba0V9EEH.js → subcommands-s74ViJcI.js} +305 -12
- package/dist/{ts-6JcbtJh1.js → ts-CP17kUI1.js} +2 -2
- package/dist/{versionChecker-DDNKcQRx.js → versionChecker-DTssk7cU.js} +2 -2
- package/lab/ui/index.html +292 -2
- package/lab/ui/room-client.js +544 -388
- package/lab/ui/sw.js +71 -19
- package/package.json +1 -1
- package/ts/messageLog.spec.ts +145 -0
- package/ts/messageLog.ts +147 -0
- package/ts/serve.ts +55 -8
- package/ts/subcommands.ts +270 -5
- package/dist/SUPPORTED_CLIS-NfXUfiWY.js +0 -8
- package/dist/subcommands-LP1Z8g-h.js +0 -9
package/lab/ui/index.html
CHANGED
|
@@ -1133,6 +1133,50 @@
|
|
|
1133
1133
|
background-color: color-mix(in srgb, var(--peer) 24%, transparent);
|
|
1134
1134
|
}
|
|
1135
1135
|
}
|
|
1136
|
+
/* An inter-agent message just travelled between two rows (`ay send`/`key`/
|
|
1137
|
+
`select`, from /api/edges `sends`). Both ends pulse so the exchange reads
|
|
1138
|
+
as a pair: green at the SENDER, amber at the RECIPIENT — the same hues the
|
|
1139
|
+
/r/ forest uses for its send wire. Distinct from .stdinflash (which fires
|
|
1140
|
+
for ANY stdin, including a human typing) so agent→agent traffic stands out. */
|
|
1141
|
+
.row.sendflash {
|
|
1142
|
+
animation: sendpulse 0.7s ease-in-out infinite;
|
|
1143
|
+
}
|
|
1144
|
+
.row.recvflash {
|
|
1145
|
+
animation: recvpulse 0.7s ease-in-out infinite;
|
|
1146
|
+
}
|
|
1147
|
+
@keyframes sendpulse {
|
|
1148
|
+
0%,
|
|
1149
|
+
100% {
|
|
1150
|
+
background-color: transparent;
|
|
1151
|
+
}
|
|
1152
|
+
50% {
|
|
1153
|
+
background-color: color-mix(in srgb, var(--green) 22%, transparent);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
@keyframes recvpulse {
|
|
1157
|
+
0%,
|
|
1158
|
+
100% {
|
|
1159
|
+
background-color: transparent;
|
|
1160
|
+
}
|
|
1161
|
+
50% {
|
|
1162
|
+
background-color: color-mix(in srgb, var(--amber) 22%, transparent);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
/* The wire itself: an SVG laid over the (scrolling) list, drawn from the row
|
|
1166
|
+
rects. Inside .list so it scrolls with the rows; pointer-events off so it
|
|
1167
|
+
never eats a click meant for a row. */
|
|
1168
|
+
.list {
|
|
1169
|
+
position: relative;
|
|
1170
|
+
}
|
|
1171
|
+
.wire-overlay {
|
|
1172
|
+
position: absolute;
|
|
1173
|
+
left: 0;
|
|
1174
|
+
top: 0;
|
|
1175
|
+
width: 100%;
|
|
1176
|
+
pointer-events: none;
|
|
1177
|
+
overflow: visible;
|
|
1178
|
+
z-index: 3;
|
|
1179
|
+
}
|
|
1136
1180
|
/* Per-row chip: how many peers are watching THIS agent (yellow, mono). */
|
|
1137
1181
|
.peerchip {
|
|
1138
1182
|
font-family: var(--mono);
|
|
@@ -2101,6 +2145,16 @@
|
|
|
2101
2145
|
const stdinSeen = new Map();
|
|
2102
2146
|
const flashUntil = new Map();
|
|
2103
2147
|
const STDIN_FLASH_MS = 1200;
|
|
2148
|
+
// Inter-agent message traffic (from /api/edges `sends`). A send is an EVENT,
|
|
2149
|
+
// not a standing relation: both ends pulse and the wire between them fades
|
|
2150
|
+
// out over SEND_FADE_MS. `sendSeen` dedupes by (by,target,at) so re-polling
|
|
2151
|
+
// the same still-in-window edge doesn't re-arm the flash forever.
|
|
2152
|
+
const sendFlashUntil = new Map(); // sender key → until (ms)
|
|
2153
|
+
const recvFlashUntil = new Map(); // receiver key → until (ms)
|
|
2154
|
+
const sendSeen = new Set();
|
|
2155
|
+
let sendWires = []; // [{by, target, at, kind}] — live wires, keys are <src>#<pid>
|
|
2156
|
+
const SEND_FLASH_MS = 2000;
|
|
2157
|
+
const SEND_FADE_MS = 20000; // wire fully faded this long after delivery
|
|
2104
2158
|
let es = null; // live-tail subscription closer
|
|
2105
2159
|
let term = null; // xterm.js Terminal rendering the raw PTY stream
|
|
2106
2160
|
let fit = null;
|
|
@@ -2252,6 +2306,10 @@
|
|
|
2252
2306
|
previewFetch(port, method, path, init) {
|
|
2253
2307
|
return chRoom.room.fetch(peerId, method, "/__codehost/port/" + port + path, init || {});
|
|
2254
2308
|
},
|
|
2309
|
+
// Open a WS to the previewed port over the tunnel (Vite HMR etc.).
|
|
2310
|
+
previewWs(port, path, protocols, handlers) {
|
|
2311
|
+
return chRoom.room.openWs(peerId, "/__codehost/port/" + port + path, protocols, handlers);
|
|
2312
|
+
},
|
|
2255
2313
|
async fetchJSON(path) {
|
|
2256
2314
|
let res;
|
|
2257
2315
|
try {
|
|
@@ -2545,6 +2603,98 @@
|
|
|
2545
2603
|
.catch((err) => port.postMessage({ type: "error", message: String(err) }));
|
|
2546
2604
|
});
|
|
2547
2605
|
}
|
|
2606
|
+
|
|
2607
|
+
// A window.WebSocket replacement, bound to a preview source+port, that
|
|
2608
|
+
// tunnels over the codehost DataChannel (previewWs → the daemon's port WS)
|
|
2609
|
+
// instead of the network. The preview iframe's injected bootstrap installs
|
|
2610
|
+
// it as window.WebSocket so a dev server's HMR socket goes P2P too.
|
|
2611
|
+
// previewWs is async (it dials the peer), so sends before open are queued.
|
|
2612
|
+
window.__ayMakeWS = function (src, port) {
|
|
2613
|
+
return class PreviewWebSocket {
|
|
2614
|
+
static CONNECTING = 0;
|
|
2615
|
+
static OPEN = 1;
|
|
2616
|
+
static CLOSING = 2;
|
|
2617
|
+
static CLOSED = 3;
|
|
2618
|
+
CONNECTING = 0;
|
|
2619
|
+
OPEN = 1;
|
|
2620
|
+
CLOSING = 2;
|
|
2621
|
+
CLOSED = 3;
|
|
2622
|
+
constructor(url, protocols) {
|
|
2623
|
+
this.url = String(url);
|
|
2624
|
+
this.readyState = 0; // CONNECTING
|
|
2625
|
+
this.binaryType = "blob";
|
|
2626
|
+
this.protocol = "";
|
|
2627
|
+
this._listeners = {};
|
|
2628
|
+
this._handle = null;
|
|
2629
|
+
this._queue = [];
|
|
2630
|
+
const tx = sources.get(src)?.tx;
|
|
2631
|
+
const u = new URL(this.url, location.href);
|
|
2632
|
+
const protoList = protocols ? (Array.isArray(protocols) ? protocols : [protocols]) : undefined;
|
|
2633
|
+
if (!tx || !tx.previewWs) return void this._fail();
|
|
2634
|
+
tx.previewWs(port, u.pathname + u.search, protoList, {
|
|
2635
|
+
onOpenAck: (ok, proto) => {
|
|
2636
|
+
if (!ok) return this._fail();
|
|
2637
|
+
this.readyState = 1;
|
|
2638
|
+
this.protocol = proto || "";
|
|
2639
|
+
this._dispatch("open", {});
|
|
2640
|
+
for (const m of this._queue) this._rawSend(m);
|
|
2641
|
+
this._queue = [];
|
|
2642
|
+
},
|
|
2643
|
+
onText: (t) => this._dispatch("message", { data: t }),
|
|
2644
|
+
onBin: (d) => this._dispatch("message", { data: this._wrap(d) }),
|
|
2645
|
+
onClose: (code, reason) => {
|
|
2646
|
+
this.readyState = 3;
|
|
2647
|
+
this._dispatch("close", { code, reason, wasClean: code === 1000 });
|
|
2648
|
+
},
|
|
2649
|
+
})
|
|
2650
|
+
.then((h) => {
|
|
2651
|
+
this._handle = h;
|
|
2652
|
+
if (this.readyState === 3 && this._closeReq) h.close(this._closeReq.code, this._closeReq.reason);
|
|
2653
|
+
})
|
|
2654
|
+
.catch(() => this._fail());
|
|
2655
|
+
}
|
|
2656
|
+
_wrap(d) {
|
|
2657
|
+
return this.binaryType === "arraybuffer"
|
|
2658
|
+
? d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength)
|
|
2659
|
+
: new Blob([d]);
|
|
2660
|
+
}
|
|
2661
|
+
_rawSend(data) {
|
|
2662
|
+
const h = this._handle;
|
|
2663
|
+
if (!h) return;
|
|
2664
|
+
if (typeof data === "string") h.sendText(data);
|
|
2665
|
+
else if (data instanceof Blob) data.arrayBuffer().then((b) => h.sendBin(new Uint8Array(b)));
|
|
2666
|
+
else if (ArrayBuffer.isView(data)) h.sendBin(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
2667
|
+
else h.sendBin(new Uint8Array(data));
|
|
2668
|
+
}
|
|
2669
|
+
send(data) {
|
|
2670
|
+
if (this.readyState === 0) this._queue.push(data);
|
|
2671
|
+
else if (this.readyState === 1) this._rawSend(data);
|
|
2672
|
+
}
|
|
2673
|
+
close(code, reason) {
|
|
2674
|
+
if (this.readyState === 3) return;
|
|
2675
|
+
this.readyState = 3;
|
|
2676
|
+
if (this._handle) this._handle.close(code, reason);
|
|
2677
|
+
else this._closeReq = { code, reason };
|
|
2678
|
+
this._dispatch("close", { code: code || 1000, reason: reason || "", wasClean: true });
|
|
2679
|
+
}
|
|
2680
|
+
_fail() {
|
|
2681
|
+
this.readyState = 3;
|
|
2682
|
+
this._dispatch("error", {});
|
|
2683
|
+
this._dispatch("close", { code: 1006, reason: "tunnel open failed", wasClean: false });
|
|
2684
|
+
}
|
|
2685
|
+
addEventListener(t, l) {
|
|
2686
|
+
(this._listeners[t] = this._listeners[t] || new Set()).add(l);
|
|
2687
|
+
}
|
|
2688
|
+
removeEventListener(t, l) {
|
|
2689
|
+
this._listeners[t]?.delete(l);
|
|
2690
|
+
}
|
|
2691
|
+
_dispatch(t, init) {
|
|
2692
|
+
const ev = { type: t, target: this, ...init };
|
|
2693
|
+
this["on" + t]?.call(this, ev);
|
|
2694
|
+
this._listeners[t]?.forEach((l) => l.call(this, ev));
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
};
|
|
2548
2698
|
// Every machine belonging to a room (for an ay-share room, the room itself).
|
|
2549
2699
|
const roomSources = (name) => [...sources.values()].filter((s) => (s.room || s.id) === name);
|
|
2550
2700
|
const roomLive = (name) =>
|
|
@@ -3170,6 +3320,131 @@
|
|
|
3170
3320
|
}
|
|
3171
3321
|
}, 200);
|
|
3172
3322
|
|
|
3323
|
+
// ── inter-agent message traffic: row pulses + the wire between them ───────
|
|
3324
|
+
|
|
3325
|
+
/** Remaining life of a send, 1 at delivery → 0 once fully faded. */
|
|
3326
|
+
function sendAlpha(at) {
|
|
3327
|
+
return Math.max(0, Math.min(1, 1 - (Date.now() - at) / SEND_FADE_MS));
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// Poll every source's /api/edges and arm a pulse for each NEW send. Edge pids
|
|
3331
|
+
// are mapped to the `<src>#<pid>` row keys the list already uses, so a wire
|
|
3332
|
+
// never crosses sources (a daemon only ever sees its own agents). An older
|
|
3333
|
+
// daemon has no `sends` field — the console then simply shows no traffic.
|
|
3334
|
+
async function pollEdges() {
|
|
3335
|
+
const srcs = [...sources.values()].filter((s) => s.tx && s.live);
|
|
3336
|
+
const results = await Promise.allSettled(
|
|
3337
|
+
srcs.map(async (s) => {
|
|
3338
|
+
const body = await s.tx.fetchJSON("/api/edges");
|
|
3339
|
+
return (body?.sends ?? []).map((e) => ({
|
|
3340
|
+
by: `${s.id}#${e.by}`,
|
|
3341
|
+
target: `${s.id}#${e.target}`,
|
|
3342
|
+
at: e.at,
|
|
3343
|
+
kind: e.kind,
|
|
3344
|
+
}));
|
|
3345
|
+
}),
|
|
3346
|
+
);
|
|
3347
|
+
const edges = results.flatMap((p) => (p.status === "fulfilled" ? p.value : []));
|
|
3348
|
+
const now = Date.now();
|
|
3349
|
+
for (const e of edges) {
|
|
3350
|
+
const id = `${e.by}${e.target}${e.at}`;
|
|
3351
|
+
if (sendSeen.has(id)) continue; // already pulsed for this exact send
|
|
3352
|
+
sendSeen.add(id);
|
|
3353
|
+
// Don't flash a send that's already older than its own flash window (e.g.
|
|
3354
|
+
// the first poll after opening the console, which sees the whole minute).
|
|
3355
|
+
if (now - e.at > SEND_FLASH_MS) continue;
|
|
3356
|
+
sendFlashUntil.set(e.by, now + SEND_FLASH_MS);
|
|
3357
|
+
recvFlashUntil.set(e.target, now + SEND_FLASH_MS);
|
|
3358
|
+
}
|
|
3359
|
+
// Keep only wires still worth drawing; bound sendSeen to the live set so it
|
|
3360
|
+
// can't grow without limit over a long session.
|
|
3361
|
+
sendWires = edges.filter((e) => sendAlpha(e.at) > 0);
|
|
3362
|
+
if (sendSeen.size > 500) {
|
|
3363
|
+
const live = new Set(sendWires.map((e) => `${e.by}${e.target}${e.at}`));
|
|
3364
|
+
for (const id of [...sendSeen]) if (!live.has(id)) sendSeen.delete(id);
|
|
3365
|
+
}
|
|
3366
|
+
renderSendWires();
|
|
3367
|
+
}
|
|
3368
|
+
setInterval(() => void pollEdges(), 1500);
|
|
3369
|
+
|
|
3370
|
+
// Drive the send/recv row pulses on the stable list DOM (same trick as the
|
|
3371
|
+
// stdin flash above), and keep the wire fading. No-op when nothing's in
|
|
3372
|
+
// flight, so an idle console costs nothing.
|
|
3373
|
+
setInterval(() => {
|
|
3374
|
+
const now = Date.now();
|
|
3375
|
+
for (const [k, until] of [...sendFlashUntil.entries()])
|
|
3376
|
+
if (until <= now) sendFlashUntil.delete(k);
|
|
3377
|
+
for (const [k, until] of [...recvFlashUntil.entries()])
|
|
3378
|
+
if (until <= now) recvFlashUntil.delete(k);
|
|
3379
|
+
const listEl = $("list");
|
|
3380
|
+
if (listEl && (sendFlashUntil.size || recvFlashUntil.size)) {
|
|
3381
|
+
for (const el of listEl.querySelectorAll(".row[data-key]")) {
|
|
3382
|
+
const key = el.getAttribute("data-key");
|
|
3383
|
+
el.classList.toggle("sendflash", (sendFlashUntil.get(key) || 0) > now);
|
|
3384
|
+
el.classList.toggle("recvflash", (recvFlashUntil.get(key) || 0) > now);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
if (sendWires.length) {
|
|
3388
|
+
sendWires = sendWires.filter((e) => sendAlpha(e.at) > 0);
|
|
3389
|
+
renderSendWires(); // redraw so the wire's opacity decays smoothly
|
|
3390
|
+
}
|
|
3391
|
+
}, 200);
|
|
3392
|
+
|
|
3393
|
+
/**
|
|
3394
|
+
* Draw the live send wires over the list: an S-curve in the left gutter from
|
|
3395
|
+
* the sender row's centre to the recipient's, opacity decaying with age.
|
|
3396
|
+
*
|
|
3397
|
+
* The SVG lives INSIDE #list (which scrolls), sized to scrollHeight, so the
|
|
3398
|
+
* wires scroll with their rows instead of needing a scroll listener. An
|
|
3399
|
+
* endpoint that isn't in the DOM right now — folded under a collapsed parent,
|
|
3400
|
+
* or filtered out by the search box — simply drops its wire.
|
|
3401
|
+
*/
|
|
3402
|
+
function renderSendWires() {
|
|
3403
|
+
const listEl = $("list");
|
|
3404
|
+
if (!listEl) return;
|
|
3405
|
+
let svg = listEl.querySelector(".wire-overlay");
|
|
3406
|
+
if (!sendWires.length) {
|
|
3407
|
+
if (svg) svg.remove();
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
if (!svg) {
|
|
3411
|
+
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3412
|
+
svg.setAttribute("class", "wire-overlay");
|
|
3413
|
+
listEl.appendChild(svg);
|
|
3414
|
+
}
|
|
3415
|
+
const base = listEl.getBoundingClientRect();
|
|
3416
|
+
// Row centres in the list's own scrolled coordinate space.
|
|
3417
|
+
const yOf = (key) => {
|
|
3418
|
+
const el = listEl.querySelector(`.row[data-key="${CSS.escape(key)}"]`);
|
|
3419
|
+
if (!el) return null;
|
|
3420
|
+
const r = el.getBoundingClientRect();
|
|
3421
|
+
return r.top - base.top + listEl.scrollTop + r.height / 2;
|
|
3422
|
+
};
|
|
3423
|
+
svg.setAttribute("height", String(listEl.scrollHeight));
|
|
3424
|
+
svg.style.height = listEl.scrollHeight + "px";
|
|
3425
|
+
const paths = [];
|
|
3426
|
+
for (const e of sendWires) {
|
|
3427
|
+
const y1 = yOf(e.by);
|
|
3428
|
+
const y2 = yOf(e.target);
|
|
3429
|
+
if (y1 == null || y2 == null || y1 === y2) continue;
|
|
3430
|
+
const a = sendAlpha(e.at);
|
|
3431
|
+
const color = e.kind ? "245, 158, 11" : "63, 185, 80"; // amber = key/select, green = msg
|
|
3432
|
+
// Bulge left-to-right with the gap so a long hop is legible, capped so it
|
|
3433
|
+
// stays inside the gutter.
|
|
3434
|
+
const bulge = Math.min(34, 8 + Math.abs(y2 - y1) * 0.18);
|
|
3435
|
+
const x = 8;
|
|
3436
|
+
paths.push(
|
|
3437
|
+
`<path d="M ${x},${y1} C ${x + bulge},${y1} ${x + bulge},${y2} ${x},${y2}" ` +
|
|
3438
|
+
`fill="none" stroke="rgba(${color}, ${(0.2 + 0.8 * a).toFixed(3)})" ` +
|
|
3439
|
+
`stroke-width="${(1 + 1.6 * a).toFixed(2)}" stroke-dasharray="3 3" />` +
|
|
3440
|
+
// arrowhead at the recipient end — which way the message travelled
|
|
3441
|
+
`<circle cx="${x}" cy="${y2}" r="${(2 + 1.5 * a).toFixed(2)}" ` +
|
|
3442
|
+
`fill="rgba(${color}, ${(0.3 + 0.7 * a).toFixed(3)})" />`,
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
svg.innerHTML = paths.join("");
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3173
3448
|
// Peer-selection overlay. The coordinate math (parseSel / selSegments /
|
|
3174
3449
|
// hashHue) lives in console-logic.js (unit-tested); here we just measure the
|
|
3175
3450
|
// DOM and place the boxes. Drawn in OUR buffer rows, clipped to the viewport.
|
|
@@ -4386,9 +4661,19 @@
|
|
|
4386
4661
|
// redundant — and it's the only row your own terminal's protocol replies
|
|
4387
4662
|
// could ever nudge, so this also keeps a resize from blinking your own row.
|
|
4388
4663
|
function rowFlags(e) {
|
|
4664
|
+
const now = Date.now();
|
|
4389
4665
|
const peers = focusByKey.get(e._key) || 0;
|
|
4390
|
-
const flash = e._key !== sel && (flashUntil.get(e._key) || 0) >
|
|
4391
|
-
|
|
4666
|
+
const flash = e._key !== sel && (flashUntil.get(e._key) || 0) > now;
|
|
4667
|
+
// Re-emit the message pulses too: renderList replaces rows wholesale, so a
|
|
4668
|
+
// class the fast loop toggled on would be lost on the next render otherwise.
|
|
4669
|
+
const sent = (sendFlashUntil.get(e._key) || 0) > now;
|
|
4670
|
+
const recv = (recvFlashUntil.get(e._key) || 0) > now;
|
|
4671
|
+
return (
|
|
4672
|
+
(peers ? " peerfocus" : "") +
|
|
4673
|
+
(flash ? " stdinflash" : "") +
|
|
4674
|
+
(sent ? " sendflash" : "") +
|
|
4675
|
+
(recv ? " recvflash" : "")
|
|
4676
|
+
);
|
|
4392
4677
|
}
|
|
4393
4678
|
// A yellow chip showing how many peers are watching this agent ("" if none).
|
|
4394
4679
|
function peerChipHtml(e) {
|
|
@@ -4453,6 +4738,7 @@
|
|
|
4453
4738
|
<span class="age">${age(e)}</span></div>`;
|
|
4454
4739
|
})
|
|
4455
4740
|
.join("") || `<div class="empty">no match</div>`;
|
|
4741
|
+
renderSendWires(); // rows were replaced — re-draw the overlay (see below)
|
|
4456
4742
|
return;
|
|
4457
4743
|
}
|
|
4458
4744
|
$("list").innerHTML =
|
|
@@ -4491,6 +4777,10 @@
|
|
|
4491
4777
|
</div>`;
|
|
4492
4778
|
})
|
|
4493
4779
|
.join("") || `<div class="empty">no match</div>`;
|
|
4780
|
+
// The innerHTML above replaced every row — and with them the wire overlay
|
|
4781
|
+
// (a child of #list). Re-draw it against the fresh rects, or an in-flight
|
|
4782
|
+
// wire would vanish on the next list render.
|
|
4783
|
+
renderSendWires();
|
|
4494
4784
|
}
|
|
4495
4785
|
|
|
4496
4786
|
// Restore the last filter so a refresh (incl. the auto-reload on a new
|