BotsKit — Control Room · all axes, all options

One decision forks the whole system: can our widget iframe into the page you're on? Yes → Axis A (in-page). No → Axis B (render in a window we own, stream pixels back). Each path's trust surface is one small file you audit once; everything behind it updates live without re-trusting. Drag a bookmarklet to your bar, or click Simulate to feel the real experience right here.

⚠ DEV control room — intentionally unsandboxed so Simulate runs the real anchors (popups · iframes · LLM). Not a shipped page.

The fork (one runtime question)

Host page CSP frame-src allows our collar iframe? → Axis A · bookmarklet.exit. Blocked? → Axis B · bookmarklet.stream (host_base render + pixel stream + input replay). No silent fallback — you pick (or a CSP probe hints).
Served from botskit/service_api at 127.0.0.1:8800 — this server roots at caprpc/, so every link on this page (and in the map) resolves. The Axis-B-only server serve.py :8123 roots at stream_frames/ and can't serve this page.

Axis AB — Combined (one drag, both modes)

Tries A's iframe collar first; falls back to B's pop-around streamer on hostile-CSP hosts. Build-stitched from the two single-axis sources — both bodies live in full inside one IIFE, so the audit surface is just "A + B + a small orchestration shim".

What you trust

… LOC
bookmarklet.exit_stream.src.js
No dedup. Audit the stitch as one file, or audit A and B separately and trust build_books.py to combine them faithfully. Hub URLs are baked at sync time — the file is self-contained.

What you experience

A first → B on CSP block
Iframe collar in-page when the host CSP allows it; otherwise the cross-origin pop-around streamer takes over. Switch is automatic — a frame-src CSP violation OR a 1700 ms handshake timeout triggers fallback.
AB — Exit then Stream experimental
Tries axis-A in-page iframe collar; if the host CSP frame-src blocks it OR the bc.v0 handshake doesn't return in 1700ms, falls back to axis-B iii.b cross-origin pop-around streamer.
▶ AB — Exit then Stream
drag me to your bookmarks bar — or:
Stitched by build_books.py sync from bookmarklet.exit.src.js + bookmarklet.poparound_xo.src.js. DO NOT hand-edit the stitched file — edit the canonical sources and re-run sync.
Read the stitched trust anchor
loading…

Axis A — Exit (in-page)

Fires when the host page's CSP lets our collar iframe load. Native paint, no fidelity gap. This one reached a full release + deploy chain.

What you trust

… LOC
bookmarklet.exit.src.js
Audit once · frozen at install · a change forces a re-drag.

What you experience

collar → chat uApp
In-page panel: nested collar + uApp iframes. The chat, capabilities, and updates all live behind the collar — no re-trust on update.
BotsKit · Exit shipped
in-page collar injected via bookmarklet — fires when the host page's CSP allows our iframe.
▶ BotsKit · Exit
drag me to your bookmarks bar — or:
Read the trust anchor (the whole surface)
loading…

Axis B — Stream (host_base + pixels)

Fires when CSP blocks our iframe. We render the real collar+widget in a host_base the page can't gate, stream PNG frames to a canvas overlay, and replay your input. The widget you see is pixels; the trust still lives in this one anchor.

What you trust

… LOC
bookmarklet.stream.src.js
Two parts: a dumb frame pipe + the bc.v0 control core (§CAPS, grant-chip gated). Page data leaves only for a cap you grant, only to our own host_base.

What you experience

streamed chat overlay
Same chat brain as Axis A — the canonical chat.core.js lives in axis-B's release (no _core/ shared dir; build_books.py would sync the other way if A ever needed it). Rendered in the host_base and streamed back; read-page rides bc.v0; the LLM loads only when you Send (idle = zero egress).
i — local window shipped
popup of our origin, host keeps focus. The shipped default.
▶ BotsKit · Stream (i)
drag me to your bookmarks bar — or:
ii — regular hub tab experimental
opens in a new tab; the tab steals focus until you switch back. Renders the widget at tile size (~560x720 centered) rather than full-bleed.
▶ BotsKit · Stream (ii)
drag me to your bookmarks bar — or:
iii.b — pop-around (T11c cross-origin via window.name) experimental
Cross-origin variant: first click opens clone with a unique window.name (prefix _pa_xo_), navigates this tab to hub. Second click on the named clone tab adopts via window.opener (the hub) — sync, no probe, cross-origin compatible.
▶ BotsKit · Stream (iii.b)
drag me to your bookmarks bar — or:
iii.c — pop-around (T12 anchor-click) experimental
T12 verified Chrome 147. Anchor-click sibling of T11c — opens clone via synthetic <a target=_pa_anchor_xxx rel=opener>.click() instead of window.open(). Pros: bypasses popup blockers that pattern-match window.open(). Cons: no WindowProxy returned → no keepBehind safety net, the 50 ms self-nav is doing all the focus-swap work. Second-click adoption via window.opener works the same as iii.b/d (the named target preserves opener).
▶ BotsKit · Stream (iii.c)
drag me to your bookmarks bar — or:
iii.d — pop-around (T11d re-click handshake) experimental
Cross-origin variant: clone window.name uses prefix _pa_rc_; URL carries cloneName + pingPlain=1 for a future hub→clone auxiliary channel. Second-click adoption via window.opener — same shape as iii.b.
▶ BotsKit · Stream (iii.d)
drag me to your bookmarks bar — or:
iv — remote server render opt-in
renderer-server: NOT built in v1 (separate conversation). Surfaced for trust — no drag.
Only the trust anchor changes per option — the seam lives in the anchor's router object, substituted by build_books.py from build_books.toml.
🧪 bridge experiments (host-only, not in trust anchor)
Why this exists. When a streamed uApp (e.g. puter chat) calls window.open() for OAuth, the host_base lacks user activation (synthetic clicks from {t:'inject'} don't grant it) and the popup is blocked. The fix is a relay: host_base's window.open is patched to forward to the HOST page, which still has transient activation from the recent canvas click.
How to use. Tick the box, click Simulate here on variant i, then in the streamed chat trigger sign-in. Watch the log below for relay events. If a real auth tab opens at the host page level (not the host_base), the bridge works.
bridge experiment idle.

⚡ Frame rate & liveness A1 shipped in anchor

The stream runs at ~8 fps (tick 120 ms) while the host_base window is visible on screen. If you minimize or fully cover it, the browser throttles its setTimeout to ~1 fps — frames crawl. That's a browser policy for hidden pages, not our rate.

Every option to fight it, by family — the only question that matters is the middle column:

optionback to ~8 fps?status
A1 — foreground page drives a postMessage tick (capture on message receipt, not the hidden timer) YESshipped in anchor
A2 — server / WebSocket pushes the tick YESopt-in (needs server)
B — in-page tricks (MessageChannel self-loop, Web Worker timer, AudioContext / muted video keepalive) partial / no softens floor only
Service Worker as a clock NO debunked — event-driven, killed ~30 s idle, no DOM
C — WebRTC peer / server render (browser-to-browser; nothing local paints) YESescape hatch (surfaced)
D — just keep the window visible / PiP YESship today

Shipped: A1 now lives in the anchor — it drives a foreground {t:'tick'} each animation frame, so the stream holds ~8 fps even when the host_base is minimized. Proven by test_external_tick_beats_clamp and a manual minimized run (1→8 fps). Users re-drag once to pick it up. Full analysis + the AudioContext myth-buster + the rasterization gate: stream_frames/docs/dev_frame_liveness.md.

Read the trust anchor (the whole surface)
loading…

Optimizations — tune Axis B (a click away)

The renderer/transport are settled; these harnesses tune what's left. Each is an experiment, not product — findings live in the linked dev_*.md. Full map: readme_unified_bookmark.md.

⚡ Frame rate & liveness solved (A1)

Background tabs throttle setTimeout to ~1 fps. A foreground tick drives capture so the stream holds ~8 fps even minimized.

In the harness: click Simulate background throttle to watch fps drop to ~1 and snap back to ~8.

🔍 Stream quality / sharpness scale knob open

Renderer is settled (SVG-fo + lossless PNG). The blur is a devicePixelRatio issue — DPR only ever lost over the video encoder, never over PNG. This re-tests it.

Scale × format knobs, stacked vs the live-DOM reference. On a HiDPI screen 2×/3× + PNG should beat 1×.
control room ready.