agent-yes 1.193.0 → 1.195.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-eK4A0Nql.js → SUPPORTED_CLIS-Bdj9qDRm.js} +2 -2
- package/dist/SUPPORTED_CLIS-Daig8XHX.js +8 -0
- package/dist/{agentShare-C2PSaiaa.js → agentShare-M2jaTMlH.js} +2 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-pJJzfvcJ.js → notifyDaemon-D5HwUMxH.js} +2 -2
- package/dist/{rustBinary-BEEvKYm_.js → rustBinary-B4pfibDJ.js} +2 -2
- package/dist/{schedule-BuWv8206.js → schedule-1oS-Yxxa.js} +4 -4
- package/dist/{serve-BrCvRObJ.js → serve-Ce708ALB.js} +44 -13
- package/dist/{setup-CL-giduA.js → setup-fTLT9Bf2.js} +2 -2
- package/dist/subcommands-CV5gqRKq.js +9 -0
- package/dist/{subcommands-CYAk7q_c.js → subcommands-CqB9zo8_.js} +305 -12
- package/dist/{ts-Cg0IpitM.js → ts-DDsICrj-.js} +2 -2
- package/dist/{versionChecker-C_ztEPMs.js → versionChecker-B8qEzYDu.js} +2 -2
- package/lab/ui/index.html +348 -4
- package/lab/ui/sw.js +101 -19
- package/package.json +1 -1
- package/ts/messageLog.spec.ts +145 -0
- package/ts/messageLog.ts +147 -0
- package/ts/serve.ts +60 -8
- package/ts/subcommands.ts +270 -5
- package/dist/SUPPORTED_CLIS-wAOAd_rx.js +0 -8
- package/dist/subcommands-B9-HhxcS.js +0 -9
package/lab/ui/index.html
CHANGED
|
@@ -392,6 +392,60 @@
|
|
|
392
392
|
padding-bottom: 10px;
|
|
393
393
|
margin-bottom: 4px;
|
|
394
394
|
}
|
|
395
|
+
/* P2P preview overlay — full-window iframe of the exposed app. */
|
|
396
|
+
.preview-overlay {
|
|
397
|
+
position: fixed;
|
|
398
|
+
inset: 0;
|
|
399
|
+
z-index: 70;
|
|
400
|
+
display: flex;
|
|
401
|
+
flex-direction: column;
|
|
402
|
+
background: var(--bg);
|
|
403
|
+
}
|
|
404
|
+
.preview-overlay[hidden] {
|
|
405
|
+
display: none;
|
|
406
|
+
}
|
|
407
|
+
.preview-bar {
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
gap: 10px;
|
|
411
|
+
padding: 6px 10px;
|
|
412
|
+
border-bottom: 1px solid var(--line);
|
|
413
|
+
background: var(--panel, var(--bg));
|
|
414
|
+
flex: 0 0 auto;
|
|
415
|
+
}
|
|
416
|
+
.preview-bar .preview-badge {
|
|
417
|
+
font-size: 11px;
|
|
418
|
+
font-weight: 600;
|
|
419
|
+
color: #3fb950;
|
|
420
|
+
border: 1px solid #3fb95055;
|
|
421
|
+
border-radius: 6px;
|
|
422
|
+
padding: 2px 7px;
|
|
423
|
+
white-space: nowrap;
|
|
424
|
+
}
|
|
425
|
+
.preview-bar .preview-title {
|
|
426
|
+
flex: 1;
|
|
427
|
+
min-width: 0;
|
|
428
|
+
overflow: hidden;
|
|
429
|
+
text-overflow: ellipsis;
|
|
430
|
+
white-space: nowrap;
|
|
431
|
+
opacity: 0.8;
|
|
432
|
+
font-size: 13px;
|
|
433
|
+
}
|
|
434
|
+
.preview-bar button {
|
|
435
|
+
border: 1px solid var(--line);
|
|
436
|
+
background: transparent;
|
|
437
|
+
color: var(--fg);
|
|
438
|
+
border-radius: 6px;
|
|
439
|
+
padding: 3px 9px;
|
|
440
|
+
cursor: pointer;
|
|
441
|
+
font-size: 13px;
|
|
442
|
+
}
|
|
443
|
+
.preview-frame {
|
|
444
|
+
flex: 1 1 auto;
|
|
445
|
+
border: 0;
|
|
446
|
+
width: 100%;
|
|
447
|
+
background: #fff;
|
|
448
|
+
}
|
|
395
449
|
/* Share modal — QR + link for a single-agent view-only share. */
|
|
396
450
|
.modal-backdrop {
|
|
397
451
|
position: fixed;
|
|
@@ -1079,6 +1133,50 @@
|
|
|
1079
1133
|
background-color: color-mix(in srgb, var(--peer) 24%, transparent);
|
|
1080
1134
|
}
|
|
1081
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
|
+
}
|
|
1082
1180
|
/* Per-row chip: how many peers are watching THIS agent (yellow, mono). */
|
|
1083
1181
|
.peerchip {
|
|
1084
1182
|
font-family: var(--mono);
|
|
@@ -1960,6 +2058,19 @@
|
|
|
1960
2058
|
</div>
|
|
1961
2059
|
</div>
|
|
1962
2060
|
|
|
2061
|
+
<!-- P2P preview: the exposed app rendered in-console over the machine's
|
|
2062
|
+
WebRTC tunnel (Service Worker proxies /w/p/<src>/<port>/*, no relay). -->
|
|
2063
|
+
<div class="preview-overlay" id="previewOverlay" hidden>
|
|
2064
|
+
<div class="preview-bar">
|
|
2065
|
+
<span class="preview-badge" title="served over a direct P2P WebRTC tunnel — traffic does not go through agent-yes.com">⚡ P2P · no relay</span>
|
|
2066
|
+
<span class="preview-title" id="previewTitle"></span>
|
|
2067
|
+
<button id="previewReload" type="button" title="reload">↻</button>
|
|
2068
|
+
<button id="previewPop" type="button" title="open in a new tab">↗</button>
|
|
2069
|
+
<button id="previewClose" type="button" title="close">✕</button>
|
|
2070
|
+
</div>
|
|
2071
|
+
<iframe class="preview-frame" id="previewFrame" title="preview"></iframe>
|
|
2072
|
+
</div>
|
|
2073
|
+
|
|
1963
2074
|
<!-- Cmd/Ctrl+K omnibox: search agents by title (instant) then output (tail),
|
|
1964
2075
|
or spawn a new agent in the highlighted agent's cwd with the typed prompt. -->
|
|
1965
2076
|
<div class="omni" id="omni" style="display: none">
|
|
@@ -2034,6 +2145,16 @@
|
|
|
2034
2145
|
const stdinSeen = new Map();
|
|
2035
2146
|
const flashUntil = new Map();
|
|
2036
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
|
|
2037
2158
|
let es = null; // live-tail subscription closer
|
|
2038
2159
|
let term = null; // xterm.js Terminal rendering the raw PTY stream
|
|
2039
2160
|
let fit = null;
|
|
@@ -2176,6 +2297,15 @@
|
|
|
2176
2297
|
started_at: a.startedAt,
|
|
2177
2298
|
}));
|
|
2178
2299
|
return {
|
|
2300
|
+
// Marker + raw tunnel access for the P2P preview: only a codehost
|
|
2301
|
+
// (`ch`) source can proxy a local port over its WebRTC DataChannel via
|
|
2302
|
+
// the daemon's `preview` plugin (/__codehost/port/<PORT>/*), so the
|
|
2303
|
+
// viewer's preview traffic stays peer-to-peer, off the edge relay.
|
|
2304
|
+
ch: true,
|
|
2305
|
+
peerId,
|
|
2306
|
+
previewFetch(port, method, path, init) {
|
|
2307
|
+
return chRoom.room.fetch(peerId, method, "/__codehost/port/" + port + path, init || {});
|
|
2308
|
+
},
|
|
2179
2309
|
async fetchJSON(path) {
|
|
2180
2310
|
let res;
|
|
2181
2311
|
try {
|
|
@@ -2432,6 +2562,43 @@
|
|
|
2432
2562
|
const sources = new Map(); // id -> { id, room, host, kind, tx, client, live, devices, serverCount }
|
|
2433
2563
|
const srcFor = (e) => (e && sources.get(e._src)) || sources.get(LOCAL) || null;
|
|
2434
2564
|
const txFor = (e) => srcFor(e)?.tx || localTx;
|
|
2565
|
+
|
|
2566
|
+
// Preview bridge: the Service Worker (which can't own the RTCPeerConnection)
|
|
2567
|
+
// forwards each /w/p/<src>/<port>/* request here; we run it over that
|
|
2568
|
+
// source's codehost tunnel (previewFetch → the daemon's `preview` plugin →
|
|
2569
|
+
// 127.0.0.1:<port>) and stream the response back through the message port.
|
|
2570
|
+
// This keeps the viewer's preview traffic peer-to-peer, off the edge relay.
|
|
2571
|
+
if ("serviceWorker" in navigator) {
|
|
2572
|
+
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
2573
|
+
const msg = event.data;
|
|
2574
|
+
if (!msg || msg.type !== "ay-preview-fetch") return;
|
|
2575
|
+
const port = event.ports[0];
|
|
2576
|
+
const tx = sources.get(msg.src)?.tx;
|
|
2577
|
+
if (!tx || !tx.ch || !tx.previewFetch) {
|
|
2578
|
+
port.postMessage({ type: "error", message: "no P2P preview for this machine" });
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const init = { headers: msg.headers };
|
|
2582
|
+
if (msg.body) init.body = new Uint8Array(msg.body);
|
|
2583
|
+
tx.previewFetch(msg.port, msg.method, msg.path, init)
|
|
2584
|
+
.then(async (res) => {
|
|
2585
|
+
const headers = {};
|
|
2586
|
+
res.headers.forEach((v, k) => (headers[k] = v));
|
|
2587
|
+
port.postMessage({ type: "head", status: res.status, statusText: res.statusText, headers });
|
|
2588
|
+
if (res.body) {
|
|
2589
|
+
const reader = res.body.getReader();
|
|
2590
|
+
for (;;) {
|
|
2591
|
+
const { done, value } = await reader.read();
|
|
2592
|
+
if (done) break;
|
|
2593
|
+
const buf = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
2594
|
+
port.postMessage({ type: "body", chunk: buf }, [buf]);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
port.postMessage({ type: "end" });
|
|
2598
|
+
})
|
|
2599
|
+
.catch((err) => port.postMessage({ type: "error", message: String(err) }));
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2435
2602
|
// Every machine belonging to a room (for an ay-share room, the room itself).
|
|
2436
2603
|
const roomSources = (name) => [...sources.values()].filter((s) => (s.room || s.id) === name);
|
|
2437
2604
|
const roomLive = (name) =>
|
|
@@ -3057,6 +3224,131 @@
|
|
|
3057
3224
|
}
|
|
3058
3225
|
}, 200);
|
|
3059
3226
|
|
|
3227
|
+
// ── inter-agent message traffic: row pulses + the wire between them ───────
|
|
3228
|
+
|
|
3229
|
+
/** Remaining life of a send, 1 at delivery → 0 once fully faded. */
|
|
3230
|
+
function sendAlpha(at) {
|
|
3231
|
+
return Math.max(0, Math.min(1, 1 - (Date.now() - at) / SEND_FADE_MS));
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// Poll every source's /api/edges and arm a pulse for each NEW send. Edge pids
|
|
3235
|
+
// are mapped to the `<src>#<pid>` row keys the list already uses, so a wire
|
|
3236
|
+
// never crosses sources (a daemon only ever sees its own agents). An older
|
|
3237
|
+
// daemon has no `sends` field — the console then simply shows no traffic.
|
|
3238
|
+
async function pollEdges() {
|
|
3239
|
+
const srcs = [...sources.values()].filter((s) => s.tx && s.live);
|
|
3240
|
+
const results = await Promise.allSettled(
|
|
3241
|
+
srcs.map(async (s) => {
|
|
3242
|
+
const body = await s.tx.fetchJSON("/api/edges");
|
|
3243
|
+
return (body?.sends ?? []).map((e) => ({
|
|
3244
|
+
by: `${s.id}#${e.by}`,
|
|
3245
|
+
target: `${s.id}#${e.target}`,
|
|
3246
|
+
at: e.at,
|
|
3247
|
+
kind: e.kind,
|
|
3248
|
+
}));
|
|
3249
|
+
}),
|
|
3250
|
+
);
|
|
3251
|
+
const edges = results.flatMap((p) => (p.status === "fulfilled" ? p.value : []));
|
|
3252
|
+
const now = Date.now();
|
|
3253
|
+
for (const e of edges) {
|
|
3254
|
+
const id = `${e.by}${e.target}${e.at}`;
|
|
3255
|
+
if (sendSeen.has(id)) continue; // already pulsed for this exact send
|
|
3256
|
+
sendSeen.add(id);
|
|
3257
|
+
// Don't flash a send that's already older than its own flash window (e.g.
|
|
3258
|
+
// the first poll after opening the console, which sees the whole minute).
|
|
3259
|
+
if (now - e.at > SEND_FLASH_MS) continue;
|
|
3260
|
+
sendFlashUntil.set(e.by, now + SEND_FLASH_MS);
|
|
3261
|
+
recvFlashUntil.set(e.target, now + SEND_FLASH_MS);
|
|
3262
|
+
}
|
|
3263
|
+
// Keep only wires still worth drawing; bound sendSeen to the live set so it
|
|
3264
|
+
// can't grow without limit over a long session.
|
|
3265
|
+
sendWires = edges.filter((e) => sendAlpha(e.at) > 0);
|
|
3266
|
+
if (sendSeen.size > 500) {
|
|
3267
|
+
const live = new Set(sendWires.map((e) => `${e.by}${e.target}${e.at}`));
|
|
3268
|
+
for (const id of [...sendSeen]) if (!live.has(id)) sendSeen.delete(id);
|
|
3269
|
+
}
|
|
3270
|
+
renderSendWires();
|
|
3271
|
+
}
|
|
3272
|
+
setInterval(() => void pollEdges(), 1500);
|
|
3273
|
+
|
|
3274
|
+
// Drive the send/recv row pulses on the stable list DOM (same trick as the
|
|
3275
|
+
// stdin flash above), and keep the wire fading. No-op when nothing's in
|
|
3276
|
+
// flight, so an idle console costs nothing.
|
|
3277
|
+
setInterval(() => {
|
|
3278
|
+
const now = Date.now();
|
|
3279
|
+
for (const [k, until] of [...sendFlashUntil.entries()])
|
|
3280
|
+
if (until <= now) sendFlashUntil.delete(k);
|
|
3281
|
+
for (const [k, until] of [...recvFlashUntil.entries()])
|
|
3282
|
+
if (until <= now) recvFlashUntil.delete(k);
|
|
3283
|
+
const listEl = $("list");
|
|
3284
|
+
if (listEl && (sendFlashUntil.size || recvFlashUntil.size)) {
|
|
3285
|
+
for (const el of listEl.querySelectorAll(".row[data-key]")) {
|
|
3286
|
+
const key = el.getAttribute("data-key");
|
|
3287
|
+
el.classList.toggle("sendflash", (sendFlashUntil.get(key) || 0) > now);
|
|
3288
|
+
el.classList.toggle("recvflash", (recvFlashUntil.get(key) || 0) > now);
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
if (sendWires.length) {
|
|
3292
|
+
sendWires = sendWires.filter((e) => sendAlpha(e.at) > 0);
|
|
3293
|
+
renderSendWires(); // redraw so the wire's opacity decays smoothly
|
|
3294
|
+
}
|
|
3295
|
+
}, 200);
|
|
3296
|
+
|
|
3297
|
+
/**
|
|
3298
|
+
* Draw the live send wires over the list: an S-curve in the left gutter from
|
|
3299
|
+
* the sender row's centre to the recipient's, opacity decaying with age.
|
|
3300
|
+
*
|
|
3301
|
+
* The SVG lives INSIDE #list (which scrolls), sized to scrollHeight, so the
|
|
3302
|
+
* wires scroll with their rows instead of needing a scroll listener. An
|
|
3303
|
+
* endpoint that isn't in the DOM right now — folded under a collapsed parent,
|
|
3304
|
+
* or filtered out by the search box — simply drops its wire.
|
|
3305
|
+
*/
|
|
3306
|
+
function renderSendWires() {
|
|
3307
|
+
const listEl = $("list");
|
|
3308
|
+
if (!listEl) return;
|
|
3309
|
+
let svg = listEl.querySelector(".wire-overlay");
|
|
3310
|
+
if (!sendWires.length) {
|
|
3311
|
+
if (svg) svg.remove();
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
if (!svg) {
|
|
3315
|
+
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
3316
|
+
svg.setAttribute("class", "wire-overlay");
|
|
3317
|
+
listEl.appendChild(svg);
|
|
3318
|
+
}
|
|
3319
|
+
const base = listEl.getBoundingClientRect();
|
|
3320
|
+
// Row centres in the list's own scrolled coordinate space.
|
|
3321
|
+
const yOf = (key) => {
|
|
3322
|
+
const el = listEl.querySelector(`.row[data-key="${CSS.escape(key)}"]`);
|
|
3323
|
+
if (!el) return null;
|
|
3324
|
+
const r = el.getBoundingClientRect();
|
|
3325
|
+
return r.top - base.top + listEl.scrollTop + r.height / 2;
|
|
3326
|
+
};
|
|
3327
|
+
svg.setAttribute("height", String(listEl.scrollHeight));
|
|
3328
|
+
svg.style.height = listEl.scrollHeight + "px";
|
|
3329
|
+
const paths = [];
|
|
3330
|
+
for (const e of sendWires) {
|
|
3331
|
+
const y1 = yOf(e.by);
|
|
3332
|
+
const y2 = yOf(e.target);
|
|
3333
|
+
if (y1 == null || y2 == null || y1 === y2) continue;
|
|
3334
|
+
const a = sendAlpha(e.at);
|
|
3335
|
+
const color = e.kind ? "245, 158, 11" : "63, 185, 80"; // amber = key/select, green = msg
|
|
3336
|
+
// Bulge left-to-right with the gap so a long hop is legible, capped so it
|
|
3337
|
+
// stays inside the gutter.
|
|
3338
|
+
const bulge = Math.min(34, 8 + Math.abs(y2 - y1) * 0.18);
|
|
3339
|
+
const x = 8;
|
|
3340
|
+
paths.push(
|
|
3341
|
+
`<path d="M ${x},${y1} C ${x + bulge},${y1} ${x + bulge},${y2} ${x},${y2}" ` +
|
|
3342
|
+
`fill="none" stroke="rgba(${color}, ${(0.2 + 0.8 * a).toFixed(3)})" ` +
|
|
3343
|
+
`stroke-width="${(1 + 1.6 * a).toFixed(2)}" stroke-dasharray="3 3" />` +
|
|
3344
|
+
// arrowhead at the recipient end — which way the message travelled
|
|
3345
|
+
`<circle cx="${x}" cy="${y2}" r="${(2 + 1.5 * a).toFixed(2)}" ` +
|
|
3346
|
+
`fill="rgba(${color}, ${(0.3 + 0.7 * a).toFixed(3)})" />`,
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
svg.innerHTML = paths.join("");
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3060
3352
|
// Peer-selection overlay. The coordinate math (parseSel / selSegments /
|
|
3061
3353
|
// hashHue) lives in console-logic.js (unit-tested); here we just measure the
|
|
3062
3354
|
// DOM and place the boxes. Drawn in OUR buffer rows, clipped to the viewport.
|
|
@@ -3397,6 +3689,23 @@
|
|
|
3397
3689
|
btn.textContent = orig;
|
|
3398
3690
|
}, 1200);
|
|
3399
3691
|
}
|
|
3692
|
+
// In-console P2P preview: an iframe served from /w/p/<src>/<port>/, which
|
|
3693
|
+
// the Service Worker proxies over the machine's WebRTC tunnel (no relay).
|
|
3694
|
+
function openPreview(src, port) {
|
|
3695
|
+
// Scope-relative so it works at both /w/ (agent-yes.com) and / (ay serve).
|
|
3696
|
+
const url = new URL("p/" + encodeURIComponent(src) + "/" + port + "/", location.href).pathname;
|
|
3697
|
+
const frame = $("previewFrame");
|
|
3698
|
+
if (!frame) return window.open(url, "_blank");
|
|
3699
|
+
$("previewTitle").textContent = "localhost:" + port;
|
|
3700
|
+
frame.src = url;
|
|
3701
|
+
$("previewOverlay").hidden = false;
|
|
3702
|
+
}
|
|
3703
|
+
function closePreview() {
|
|
3704
|
+
const ov = $("previewOverlay");
|
|
3705
|
+
if (ov) ov.hidden = true;
|
|
3706
|
+
const frame = $("previewFrame");
|
|
3707
|
+
if (frame) frame.src = "about:blank"; // tear down the app + its tunnel
|
|
3708
|
+
}
|
|
3400
3709
|
async function doExpose() {
|
|
3401
3710
|
const tx = exposePendingTx || localTx;
|
|
3402
3711
|
const port = exposePendingPort;
|
|
@@ -3435,7 +3744,7 @@
|
|
|
3435
3744
|
return;
|
|
3436
3745
|
}
|
|
3437
3746
|
if (Array.isArray(arr))
|
|
3438
|
-
for (const ex of arr) rows.push({ ...ex, _srcName: s.name || s.id, _tx: tx });
|
|
3747
|
+
for (const ex of arr) rows.push({ ...ex, _srcName: s.name || s.id, _src: s.id, _tx: tx });
|
|
3439
3748
|
}),
|
|
3440
3749
|
);
|
|
3441
3750
|
list.textContent = "";
|
|
@@ -3470,8 +3779,19 @@
|
|
|
3470
3779
|
meta.appendChild(host);
|
|
3471
3780
|
row.appendChild(meta);
|
|
3472
3781
|
|
|
3782
|
+
// Preview here: render the app P2P over this machine's WebRTC tunnel
|
|
3783
|
+
// (no edge relay), in an in-console iframe. Only offered for a codehost
|
|
3784
|
+
// (ch) source, which has the `preview` plugin.
|
|
3785
|
+
if (ex._tx && ex._tx.ch) {
|
|
3786
|
+
const preview = document.createElement("button");
|
|
3787
|
+
preview.className = "primary";
|
|
3788
|
+
preview.textContent = "Preview";
|
|
3789
|
+
preview.title = "view in-console over a direct P2P tunnel (no relay)";
|
|
3790
|
+
preview.onclick = () => openPreview(ex._src, ex.port);
|
|
3791
|
+
row.appendChild(preview);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3473
3794
|
const openBtn = document.createElement("button");
|
|
3474
|
-
openBtn.className = "primary";
|
|
3475
3795
|
openBtn.textContent = "Open";
|
|
3476
3796
|
// Re-mint a fresh claim so the owner (or a guest) gets a usable link.
|
|
3477
3797
|
openBtn.onclick = async () => {
|
|
@@ -3574,6 +3894,15 @@
|
|
|
3574
3894
|
$("exposeModal")?.addEventListener("click", (e) => {
|
|
3575
3895
|
if (e.target === $("exposeModal")) closeExposeModal();
|
|
3576
3896
|
});
|
|
3897
|
+
$("previewClose")?.addEventListener("click", closePreview);
|
|
3898
|
+
$("previewReload")?.addEventListener("click", () => {
|
|
3899
|
+
const f = $("previewFrame");
|
|
3900
|
+
if (f) f.src = f.src;
|
|
3901
|
+
});
|
|
3902
|
+
$("previewPop")?.addEventListener("click", () => {
|
|
3903
|
+
const f = $("previewFrame");
|
|
3904
|
+
if (f && f.src && f.src !== "about:blank") window.open(f.src, "_blank");
|
|
3905
|
+
});
|
|
3577
3906
|
})();
|
|
3578
3907
|
(function shareModalBoot() {
|
|
3579
3908
|
$("shareClose")?.addEventListener("click", closeShareModal);
|
|
@@ -4236,9 +4565,19 @@
|
|
|
4236
4565
|
// redundant — and it's the only row your own terminal's protocol replies
|
|
4237
4566
|
// could ever nudge, so this also keeps a resize from blinking your own row.
|
|
4238
4567
|
function rowFlags(e) {
|
|
4568
|
+
const now = Date.now();
|
|
4239
4569
|
const peers = focusByKey.get(e._key) || 0;
|
|
4240
|
-
const flash = e._key !== sel && (flashUntil.get(e._key) || 0) >
|
|
4241
|
-
|
|
4570
|
+
const flash = e._key !== sel && (flashUntil.get(e._key) || 0) > now;
|
|
4571
|
+
// Re-emit the message pulses too: renderList replaces rows wholesale, so a
|
|
4572
|
+
// class the fast loop toggled on would be lost on the next render otherwise.
|
|
4573
|
+
const sent = (sendFlashUntil.get(e._key) || 0) > now;
|
|
4574
|
+
const recv = (recvFlashUntil.get(e._key) || 0) > now;
|
|
4575
|
+
return (
|
|
4576
|
+
(peers ? " peerfocus" : "") +
|
|
4577
|
+
(flash ? " stdinflash" : "") +
|
|
4578
|
+
(sent ? " sendflash" : "") +
|
|
4579
|
+
(recv ? " recvflash" : "")
|
|
4580
|
+
);
|
|
4242
4581
|
}
|
|
4243
4582
|
// A yellow chip showing how many peers are watching this agent ("" if none).
|
|
4244
4583
|
function peerChipHtml(e) {
|
|
@@ -4303,6 +4642,7 @@
|
|
|
4303
4642
|
<span class="age">${age(e)}</span></div>`;
|
|
4304
4643
|
})
|
|
4305
4644
|
.join("") || `<div class="empty">no match</div>`;
|
|
4645
|
+
renderSendWires(); // rows were replaced — re-draw the overlay (see below)
|
|
4306
4646
|
return;
|
|
4307
4647
|
}
|
|
4308
4648
|
$("list").innerHTML =
|
|
@@ -4341,6 +4681,10 @@
|
|
|
4341
4681
|
</div>`;
|
|
4342
4682
|
})
|
|
4343
4683
|
.join("") || `<div class="empty">no match</div>`;
|
|
4684
|
+
// The innerHTML above replaced every row — and with them the wire overlay
|
|
4685
|
+
// (a child of #list). Re-draw it against the fresh rects, or an in-flight
|
|
4686
|
+
// wire would vanish on the next list render.
|
|
4687
|
+
renderSendWires();
|
|
4344
4688
|
}
|
|
4345
4689
|
|
|
4346
4690
|
// Restore the last filter so a refresh (incl. the auto-reload on a new
|
package/lab/ui/sw.js
CHANGED
|
@@ -6,7 +6,60 @@
|
|
|
6
6
|
// fallback when offline, which is what makes the installed app launchable with no
|
|
7
7
|
// network. WebSocket signaling and cross-origin CDN scripts are not GET fetches we
|
|
8
8
|
// own, so they pass straight through.
|
|
9
|
-
const CACHE = "agent-yes-w-
|
|
9
|
+
const CACHE = "agent-yes-w-v3";
|
|
10
|
+
// Preview proxy: <scope>p/<encSrc>/<port>/* renders a machine's local dev
|
|
11
|
+
// server INSIDE the console's existing WebRTC tunnel (peer-to-peer, off the
|
|
12
|
+
// edge relay). We can't own the RTCPeerConnection here, so we forward each
|
|
13
|
+
// request to the controlling page (which does), streaming the response back.
|
|
14
|
+
// Scope-relative so it works at both /w/ (agent-yes.com) and / (ay serve --http).
|
|
15
|
+
const BASE = new URL("./", self.location.href).pathname; // "/w/" or "/"
|
|
16
|
+
const PREVIEW = new RegExp("^" + BASE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "p/([^/]+)/(\\d{1,5})(/.*)?$");
|
|
17
|
+
|
|
18
|
+
async function pickClient() {
|
|
19
|
+
const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
|
|
20
|
+
// Prefer a real console tab (not the preview iframe itself).
|
|
21
|
+
return all.find((c) => !new URL(c.url).pathname.startsWith(BASE + "p/")) || all[0] || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function proxyPreview(request, src, port, rest) {
|
|
25
|
+
const client = await pickClient();
|
|
26
|
+
if (!client) return new Response("open the agent-yes console to preview", { status: 502 });
|
|
27
|
+
const headers = {};
|
|
28
|
+
request.headers.forEach((v, k) => (headers[k] = v));
|
|
29
|
+
const body =
|
|
30
|
+
request.method === "GET" || request.method === "HEAD"
|
|
31
|
+
? undefined
|
|
32
|
+
: new Uint8Array(await request.arrayBuffer());
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const mc = new MessageChannel();
|
|
35
|
+
let resolved = false;
|
|
36
|
+
mc.port1.onmessage = (ev) => {
|
|
37
|
+
const msg = ev.data;
|
|
38
|
+
if (msg.type === "head") {
|
|
39
|
+
const stream = new ReadableStream({
|
|
40
|
+
start(controller) {
|
|
41
|
+
mc.port1.onmessage = (e2) => {
|
|
42
|
+
const m2 = e2.data;
|
|
43
|
+
if (m2.type === "body") controller.enqueue(new Uint8Array(m2.chunk));
|
|
44
|
+
else if (m2.type === "end") controller.close();
|
|
45
|
+
else if (m2.type === "error") controller.error(new Error(m2.message));
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
resolved = true;
|
|
50
|
+
resolve(new Response(stream, { status: msg.status, statusText: msg.statusText, headers: msg.headers }));
|
|
51
|
+
} else if (msg.type === "error" && !resolved) {
|
|
52
|
+
resolved = true;
|
|
53
|
+
resolve(new Response("preview tunnel error: " + msg.message, { status: 502 }));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
client.postMessage(
|
|
57
|
+
{ type: "ay-preview-fetch", src, port: Number(port), method: request.method, path: rest, headers, body },
|
|
58
|
+
[mc.port2, ...(body ? [body.buffer] : [])],
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
10
63
|
const SHELL = [
|
|
11
64
|
"./",
|
|
12
65
|
"./index.html",
|
|
@@ -35,23 +88,52 @@ self.addEventListener("activate", (e) => {
|
|
|
35
88
|
|
|
36
89
|
self.addEventListener("fetch", (e) => {
|
|
37
90
|
const req = e.request;
|
|
38
|
-
if (req.method !== "GET") return;
|
|
39
91
|
const url = new URL(req.url);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return cached || (await caches.match("./index.html")) || Response.error();
|
|
54
|
-
}
|
|
55
|
-
})(),
|
|
56
|
-
);
|
|
92
|
+
if (url.origin !== self.location.origin) return; // CDN / signaling pass through
|
|
93
|
+
// Preview proxy (any method): a request is a preview request if its own path
|
|
94
|
+
// is under <base>p/<src>/<port>/ (the iframe navigation + relative URLs), OR
|
|
95
|
+
// it comes from a preview-iframe CLIENT (an app's absolute-path subresource
|
|
96
|
+
// like Vite's /@vite/client — which resolves to the origin root, not our
|
|
97
|
+
// prefix). Either way we proxy it to that machine's port over the tunnel.
|
|
98
|
+
const own = PREVIEW.exec(url.pathname);
|
|
99
|
+
if (own) {
|
|
100
|
+
const rest = (own[3] || "/") + url.search;
|
|
101
|
+
e.respondWith(proxyPreview(req, decodeURIComponent(own[1]), own[2], rest));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
e.respondWith(maybePreviewFromClient(e, req, url));
|
|
57
105
|
});
|
|
106
|
+
|
|
107
|
+
// If the request came from a preview iframe, proxy it to that iframe's port
|
|
108
|
+
// (appPath = the request's own path); otherwise fall back to the shell handler.
|
|
109
|
+
async function maybePreviewFromClient(e, req, url) {
|
|
110
|
+
const clientId = e.clientId || e.resultingClientId;
|
|
111
|
+
if (clientId) {
|
|
112
|
+
const client = await self.clients.get(clientId).catch(() => null);
|
|
113
|
+
const cm = client && PREVIEW.exec(new URL(client.url).pathname);
|
|
114
|
+
if (cm) {
|
|
115
|
+
return proxyPreview(req, decodeURIComponent(cm[1]), cm[2], url.pathname + url.search);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return shellFetch(req, url);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function shellFetch(req, url) {
|
|
122
|
+
const p = url.pathname;
|
|
123
|
+
// Pass through non-GET, the API (SSE/POST — never cache), and anything off the
|
|
124
|
+
// shell. Only cache the static console shell for offline launch.
|
|
125
|
+
const isApi = p === "/api" || p.startsWith("/api/");
|
|
126
|
+
if (req.method !== "GET" || isApi || !p.startsWith(BASE)) return fetch(req);
|
|
127
|
+
// Network-first for the same-origin shell; cache is the offline fallback.
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(req);
|
|
130
|
+
if (res && res.ok) {
|
|
131
|
+
const c = await caches.open(CACHE);
|
|
132
|
+
c.put(req, res.clone());
|
|
133
|
+
}
|
|
134
|
+
return res;
|
|
135
|
+
} catch {
|
|
136
|
+
const cached = await caches.match(req);
|
|
137
|
+
return cached || (await caches.match("./index.html")) || Response.error();
|
|
138
|
+
}
|
|
139
|
+
}
|
package/package.json
CHANGED