VS Code + Colab frontends, with kernel-side scene replay (#281) - #293
VS Code + Colab frontends, with kernel-side scene replay (#281)#293sspickle wants to merge 21 commits into
Conversation
In hosts that never run vpython's injected JavaScript and expose no Comm channel to third-party renderers (VS Code notebooks), speak the whole protocol over the kernel's tornado websocket and announce the port via an application/vnd.vpython.v1+json display output for the VPython VS Code extension to render. Auto-detected via VSCODE_PID/VSCODE_CWD; VPYTHON_FRONTEND=ws|jupyter overrides both ways. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Colab output frames run our JS but no websocket reaches the kernel VM (port proxy rejects programmatic connections from the sandboxed iframe); Colab's google.colab.kernel.comms shim is fully duplex. Mirror image of with_wsfrontend: the whole protocol rides an ipykernel Comm. No blocking handshake (CommSender buffers until ack), comm-open retried post_execute, and rate() self-clocks the flush (_direct_trigger) since a blocked kernel never sees the browser's pacing triggers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
…instead Their _wait spins for a browser reply that Colab's comm channel can only deliver between cells: a guaranteed hang. Fail loudly (issue #281 style). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Re-display the output frame after two unconnected cells; expose show() for manual recovery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Cell-boundary retries race the async bootstrap; an idle-loop task lands the open+ack+attach within ~1s of any idle moment once the JS is ready. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
The ack for open N arrives during kernel idle — exactly when the retry loop was closing comm N to open N+1, dropping every ack it provoked. Leave prior opens alone; the ack that lands picks its comm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
…ms.open) Only the browser knows when its bootstrap has finished loading, so let IT open the comm: the kernel registers a passive target before displaying the bootstrap, and the JS calls google.colab.kernel.comms.open once its libraries are up. Kernel-initiated retries remain as fallback for shims without comms.open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Notebooks saved with outputs replay old bootstrap iframes on reopen; the zombies call comms.open at the new kernel and fight the live frame for the scene. Opens must carry the current session's nonce or are closed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Colab silently declines to activate large inline scripts in display output (small inline scripts and script-src tags run fine — bisected live). Ship glowcomm_colab.js from the same jsDelivr media/ dir as GlowScript; the inline part is a one-line boot call carrying cdn+nonce. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
A 1 Hz stream of kernel-initiated comm_opens correlates exactly with Colab silently dropping display_data (the bootstrap box vanished from import and post_execute alike). The browser-initiated handshake makes kernel-side retries obsolete anyway; the per-cell post_execute fallback remains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
The bootstrap box now comes from an explicit wc.show() cell or from the post_execute self-heal (which fires on the first unconnected cell). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Colab strips script-src attributes in display HTML (and skips large inline scripts), but a small inline script may insert external scripts freely. Verified end-to-end live: render -> boot -> comms.open -> connect -> flush (connected True, backlog flushed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Frontends are ephemeral (Colab re-renders output frames on scroll; VS Code can evict outputs; pages reload). The kernel now journals every constructor cmd and every attribute ever touched (SceneJournal, hooked in appendcmd/addattr/delete), and on EVERY attach both senders send reset + all constructors + current attr values instead of a one-shot backlog flush. A new wire cmd 'reset' tells glowcomm_host to destroy() the old scene first. Any frontend instance can now rebuild the world from nothing — re-renders and reconnects become first-class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Browsers cache jsDelivr responses for days; a CDN purge cannot reach disk caches. The session nonce forces one fresh fetch per session for glowcomm_colab.js and glowcomm_host.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
scene = canvas() constructs with _canvas_constructing=True, which suppresses the frontend import — so the canvas constructor cmd lands in the pending updates buffer BEFORE the journal exists, and replays were missing their canvas (observed: journal had objects but no canvas; every replayed object failed to construct). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
…journal
They share the object's idx; journal them separately and replay them
after the constructors. Observed live: canvas replaced by a {'title'}
cmd -> every replay was canvasless.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
canvas enriches its cmd dict AFTER appendcmd; a record-time copy ships a bare canvas and glow renders its dim default stage. Store the live reference; copy at read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
Canvas construction emits: canvas ctor -> lights='empty_list' follow-up (wiping glow's built-in default lights) -> two distant_light ctors (the standard lighting). The journal replayed all constructors first and all follow-ups last, so the lights wipe ran AFTER the standard lights were recreated, deleting them — replayed scenes rendered ambient-only (dim). The journal now keeps one emission-ordered log of ctor refs and follow-up cmds and replays it verbatim after reset. Wire format unchanged; no frontend JS change needed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
|
Folded the Colab work into this PR rather than stacking a second one: the scene-replay architecture (journal + replay-on-attach) spans both new frontends — it changes Colab is verified working end-to-end in a real notebook (browser-initiated comm handshake, session-nonce zombie-frame protection, scene replay at full brightness across output-frame re-renders). Demo: https://colab.research.google.com/github/vpython/vscode-vpython/blob/main/colab/vpython-colab-demo.ipynb |
Random ports are fine locally, but remote forwards and their visibility are per-port state that every kernel restart invalidates. Pinning lets a devcontainer forward and expose the port once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR
|
GitHub Codespaces now works too — desktop AND browser VS Code, zero config. The companion extension (vpython/vscode-vpython v0.1.7) gained an extension-host side that relays the wire protocol between the output webview and the kernel's tornado websocket over VS Code renderer messaging — the host dials |
What this is
The kernel half of VPython support for VS Code notebooks and Google Colab (#281), plus the scene-replay architecture that makes both reliable. With this branch, VPython works in three hosts:
with_notebook.pywith_wsfrontend.pywith_colab.pyStatus: working end-to-end in both new hosts. Scenes render live — objects, animation via
rate(), zoom/orbit interaction, textures;text()with fonts in VS Code — verified on macOS with VS Code 1.133 and in real Colab (including full-brightness scene replay across output-frame re-renders).VS Code: websocket-only mode
Under VS Code (auto-detected via
VSCODE_PID/VSCODE_CWD, overridable withVPYTHON_FRONTEND=ws|jupyter), vpython skips the classic Comm + nbextension machinery and speaks the entire wire protocol over the tornado websocket it already runs, announcing the port with a custom-MIME display output:The companion renderer extension — https://github.com/vpython/vscode-vpython — picks that up, loads GlowScript +
glowcomm_host.jsin the notebook output webview, connects, and drives the standard trigger ping-pong.Why websocket-only: VS Code's Jupyter extension deprecated third-party access to raw kernel messaging (the ipywidgets comm mechanism), so the classic Comm downlink is unreachable from a renderer extension — but a webview can open a WebSocket to the kernel's tornado server directly (both directions verified in a spike). Moving the downlink onto the websocket the kernel already runs makes the whole protocol renderer-reachable with no Jupyter plumbing at all.
Colab: comm-only mode
Colab is the mirror image: output frames are sandboxed cross-origin iframes that cannot reach the kernel's tornado port (the port-proxy authenticates with Google cookies that don't flow there), but Colab shims Jupyter comms (
google.colab.kernel.comms). Sowith_colab.pyruns the whole protocol over a comm instead.Colab's platform quirks shaped the design (each was found empirically):
comms.openwhen its frame has actually loaded. (Kernel-initiated opens race Colab's async iframe lifecycle; kernel-side retry loops make Colab drop display outputs.) A per-session nonce freezes out zombie frames from saved outputs.import, so the scene box is shown by an explicitwc.show()call.<script>/script src=in display HTML are silently neutralized; a tiny inline injector loadsglowcomm_colab.js(and GlowScript) from jsDelivr instead.compound/text/extrusion/scene.pause/waitfor/pickraise a clearNotImplementedErrorrather than deadlocking;rate()self-clocks its flushes.Demo notebook: vpython-colab-demo.ipynb.
Scene replay — the reliability architecture
Both new hosts have ephemeral frontends: Colab re-renders output frames on scroll, VS Code can evict webviews, pages reload. The kernel is the only durable holder of the scene, so it now journals it (
_scene_journal.py— every constructor cmd and every attribute ever touched, in emission order) and every frontend attach replays: a wireresetcmd (added toglowcomm_host.js), then every cmd in its original order, then the current value of every dirty attribute. Both senders (CommSender,WsSender) share the same replay path (_frontend_replay.py).Emission order matters — canvas construction emits its constructor, then a
lights='empty_list'wipe of glow's built-in defaults, then the two standarddistant_lights; replaying out of order deletes the lights and dims the scene (found and fixed during live Colab testing).Changes
vpython/with_wsfrontend.py,vpython/_wssender.py(new): websocket-only bootstrap + kernel→browser sender — buffers packages until a renderer connects, then write-through, marshaling writes onto the tornado thread; replays on reconnect.vpython/with_colab.py,vpython/_commsender.py(new): comm-only bootstrap (passive target, nonce,show(), self-heal) + comm sender with the same buffer/flush/replay contract.vpython/_scene_journal.py,vpython/_frontend_replay.py(new): pure scene journal + replay builder.vpython/vpython_libraries/glowcomm_host.js,glowcomm_colab.js(new): host-agnostic frontend runtime (from Pyodide/wasm support: pure-Python wheel, worker transport, GlowScript host factory #291's factoring, plus theresetcmd) and the Colab bootstrap.vpython/_notebook_helpers.py: three-way frontend detection (_use_ws_frontend(),_is_colab()).vpython/vpython.py: frontend selection,GlowWidget(sender_override=...), journal hooks inappendcmd/addattr/delete, classic per-canvas HTML suppressed when a frontend owns the container.vpython/rate_control.py: direct-trigger flush for hosts whose browser triggers can't reach a blocked kernel.test_scene_journal.py,test_colabfrontend.py,test_wsfrontend.py— suite: 38 passed.Not in scope yet
Remote/WSL kernels (needs VS Code port forwarding), one scene container per notebook, widgets/pause/waitfor in ws mode (same deferrals as glowcomm_host), a kernel pump to lift Colab's mid-cell-reply limits.
Draft while the extension hardens — review welcome. Companion PRs: #292 (merged — the loud import timeout these modes gracefully degrade to), #291 (packaging;
glowcomm_host.jsoriginates there).🤖 Generated with Claude Code
https://claude.ai/code/session_01G7y9rTA1r8r8EhEnPSQenR