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/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) > Date.now();
4241
- return (peers ? " peerfocus" : "") + (flash ? " stdinflash" : "");
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-v2";
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
- // Only the same-origin /w/ shell; let everything else (CDN, signaling) be.
41
- if (url.origin !== self.location.origin || !url.pathname.startsWith("/w/")) return;
42
- e.respondWith(
43
- (async () => {
44
- try {
45
- const res = await fetch(req);
46
- if (res && res.ok) {
47
- const c = await caches.open(CACHE);
48
- c.put(req, res.clone());
49
- }
50
- return res;
51
- } catch {
52
- const cached = await caches.match(req);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.193.0",
3
+ "version": "1.195.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",