Skip to content

Remote code-execution provider for hub sessions (bd-sfet3264)#357

Open
cscheid wants to merge 30 commits into
mainfrom
feature/hub-execution-provider
Open

Remote code-execution provider for hub sessions (bd-sfet3264)#357
cscheid wants to merge 30 commits into
mainfrom
feature/hub-execution-provider

Conversation

@cscheid

@cscheid cscheid commented Jul 2, 2026

Copy link
Copy Markdown
Member

Integration branch for the remote code-execution provider epic (bd-sfet3264). Opened primarily to run CI on the branch and to review the latest work on top.

What this epic does

Lets a user with the q2 binary connect to a shared hub session, authenticate, and announce that this client will execute code. When a collaborator asks a document to run, the connected q2 provide-hub process runs the engines (knitr/jupyter) natively and deposits the results into the automerge session — so every player sees executed output, not just source. Phases 1–4 (capture consumption + clear, request/capability channel, Rust client peer + BearerDialer, execute-on-request) are implemented and cargo xtask verify-green; Phases 5–6 (retention/dedup, real provider-only authz, hardening) remain.

Full design + phase log: claude-notes/plans/2026-06-29-remote-execution-provider.md.

Latest change on top — merged preview status line (bd-yai4w8ly)

The preview pane showed two stacked status strips for code execution (executor/Run + capture/Clear). This collapses them into a single PreviewStatusBar: one row, a liveness dot, a precedence status label, and independently-gated Clear + Run/Re-run buttons ordered so Run stays pinned far right. Presentational component with 15 tests; full hub-client suite + strict build green.

  • Code: 0b13dbcb; changelog: c8251701; plan + knitr harness doc: 354d6964.

Verification

  • Rust + hub-client: cargo xtask verify-green at the epic phase checkpoints (see plan).
  • Status-line change specifically: strict npm run build:all + full npm run test:ci green; browser-verified end-to-end on the local Option-B harness (claude-notes/hub-execution-e2e/).

Known follow-ups

  • bd-gthycd33 — Jupyter engine output is not spliced into the hub-client preview (the capture arrives and the bar reads "Showing executed output", but the {python} cell renders as source). engine: knitr splices correctly ([1] 2, [1] 55), so the defect is jupyter-specific. Found while browser-verifying the status-line change; a separate agent will look at it.
  • Phase 5 (retention/dedup + provider-only authz via /auth/actor Bearer) and Phase 6 (reconnect/refresh, multi-executor claims) per the plan.

🤖 Generated with Claude Code

cscheid and others added 30 commits June 30, 2026 11:20
…-sfet3264 Phase 1)

Bring server-recorded engine execution output to the shared, persistent
hub-client editor. Until now only q2-preview's embedded SPA showed
executed code output; hub-client rendered source-only. This phase makes
hub-client a *consumer* of the existing automerge-native capture
transport (capture binary docs referenced by the IndexDocument's
CaptureRef sidecar), and adds the per-document "clear results"
affordance. No executor yet — that's Phase 3/4.

WASM (out-of-workspace crate):
- render_page_in_project_with_attribution gains a 4th capture_gz_json
  param. The inner render helpers already accepted both captures and
  attribution; the attribution entry simply hardcoded captures=Vec::new().
  Now it parses the gzipped EngineCapture[] (same wire format as
  render_page_for_preview) and threads it through, so executed output is
  spliced alongside attribution. render_page_in_project forwards None.

preview-runtime:
- renderPageInProjectWithAttribution wrapper + binding gain captureGzJson.
- New clearCapture(path) re-export.

quarto-sync-client:
- SyncClient.clearCapture(path): a pure CRDT map-key delete of the
  captures sidecar entry. Needs no executor and no server round-trip;
  removal syncs to every peer. Distinct from re-execute (replaces) and
  staleness (flags). The binary-doc bytes are left for separate server GC
  (samod has no document-delete API).

hub-client:
- App.tsx holds the captures sidecar (onCapturesChange) and threads it
  App -> Editor -> PreviewRouter -> ReactPreview (mirroring identities).
- ReactPreview fetches the active doc's capture bytes via getBinaryDocById
  (keyed on captureDocId, not content) and passes them to the render call;
  re-renders when the capture changes.
- ClearCaptureControl: a two-step inline confirmation (naming the
  collaborator-wide effect) shown only when the active doc has a capture.

Tests (TDD, RED->GREEN):
- captureSplice.wasm.test.ts: a real gzipped capture spliced through the
  real render entry; no-capture baseline; capture+attribution coexistence.
- ReactPreview.capture.integration.test.tsx: capture bytes reach the
  render call's 4th arg; no fetch when absent.
- ClearCaptureControl.integration.test.tsx: visibility + confirm/cancel.
- client.test.ts: clearCapture removes the entry, leaves siblings, no-op
  when absent.

Design + checklist: claude-notes/plans/2026-06-29-remote-execution-provider.md
The full in-browser E2E is folded into Phase 4 (faithful once the real
executor writes captures, avoiding throwaway capture-injection scaffolding).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fet3264)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fet3264 Phase 2)

Add the ephemeral, project-scoped channel for the remote-execution-provider
feature: a capability beacon (executor announces "online with engines X")
and an execute-now request, both riding Automerge ephemeral messaging on the
index DocHandle (so one channel reaches every peer regardless of the active
file). Per D2 this is the low-latency/non-persisted half; durable status
stays in the CaptureRef sidecar.

Scope (locked with user): the channel + API + capability detection only. No
executor produces beacons yet (Phase 4), so this is dormant in practice
today; the user-facing Run affordance and the claim/heartbeat/--force
protocol (D5) also land in Phase 4.

quarto-sync-client:
- SyncClient.getIndexHandle(): exposes the index DocHandle for project-scoped
  ephemeral messaging (mirrors the existing per-file getFileHandle surface).

preview-runtime:
- getIndexHandle() re-export.

hub-client:
- services/executionChannel.ts: the cross-language wire contract (the Rust
  executor mirrors it in Phase 4) — kind-discriminated, exec/-namespaced
  beacon + request messages — plus pure helpers (builders, parse/validate,
  applyBeacon/pruneExecutors with the 1.5x-interval staleness from D2), and
  a stateful createExecutionChannel that wires broadcast/subscribe + a prune
  timer. BEACON_INTERVAL_MS=3000, BEACON_TIMEOUT_MS=4500.
- hooks/useExecutionChannel.ts: starts/stops the channel with the
  connection/project, returns the live-executor set.
- App holds liveExecutors and passes executorsOnline to Editor, which shows
  a minimal read-only "Executor online" bar (the seam for Phase 4's Run UI).

Tests (TDD, RED->GREEN):
- client.test.ts: getIndexHandle null before connect, handle after.
- executionChannel.test.ts: wire format + pure helpers (13); stub-responder
  service tests against a fake DocHandle (5) — beacon appears/expires,
  request shape round-trips, self-beacon ignored, null when disconnected.
- useExecutionChannel.integration.test.tsx: beacon -> executor; teardown.

Plan + checklist: claude-notes/plans/2026-06-29-remote-execution-provider.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ange (bd-sfet3264)

Verified against the actual dep (quarto-dev git fork samod@q2 v0.9.0):
Repo::dial + the public Dialer trait + Transport::new let a BearerDialer
inject Authorization: Bearer per (re)connect with a fresh token; only the
~25-line ws_to_bytes mapping needs replicating in-crate. D1=C proceeds
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ir materialization (bd-sfet3264)

Auth-bridge hand-off uses the Node child's stdout/stdin pipes (cross-platform;
not a Unix-only extra fd): Rust q2 provide-hub spawns the Node auth helper,
which streams Bearer tokens on stdout. Executor materializes the project to a
fresh temp dir from the VFS per run. Adds the Phase 3 checklist (BearerDialer +
TokenSource, client-peer join+list with a dev token, then the subcommand + Node
auth bridge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bd-sfet3264 Phase 3A/3B)

New crate quarto-hub-provider — the Rust half of the hybrid (D1=C)
execution-provider: it joins a hub's automerge session as a samod client
peer over an authenticated websocket.

- BearerDialer (impl samod::Dialer): reimplements the small tungstenite
  connect path with an Authorization: Bearer header, fetching a fresh
  token from a TokenSource on every (re)connect (the spike-confirmed
  approach; no samod fork change). The ~25-line ws<->bytes mapping is
  replicated in-crate (inbound_to_bytes / outbound_to_ws) since samod's
  ws_to_bytes is private.
- TokenSource trait + StaticTokenSource (dev/test token before the Phase
  3C Node auth bridge).
- join_and_list_files: memory-storage Repo, dial(BearerDialer),
  established() with timeout, IndexDocument::load -> sorted file list.
  The narrow Phase 3 deliverable: prove the authenticated sync path
  before any execution.

Tests:
- 6 unit tests: ws message mapping (binary<->bytes, drop control frames,
  text is a protocol error) + the handshake request carries the Bearer
  header and rejects an illegal-byte token.
- integration: a bare samod acceptor behind a real tungstenite ws server
  + a seeded index doc; the provider connects over the real BearerDialer
  transport, syncs, and lists the files. Clippy -D warnings clean.

The authenticated-acceptance path (hub validates the JWT) is covered by
quarto-hub's auth_bearer tests and the upcoming Phase 3C real-binary run.

Plan: claude-notes/plans/2026-06-29-remote-execution-provider.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3264 Phase 3C)

Complete the hybrid (D1=C) auth path: a `q2 provide-hub` subcommand that
authenticates with the hub (via the existing OAuth machinery) and joins the
project's automerge session, then — for Phase 3 — lists the project's files.
Execution-on-request is Phase 4.

Auth bridge (cross-platform stdio, no Unix-only fd tricks):
- TS: new thin `auth-stream` entry in quarto-hub-mcp. `auth-stream/protocol.ts`
  is the transport-agnostic core (`runTokenStream`) — emits the initial Bearer
  on stdout, services `{"type":"refresh"}` from stdin, frames errors — and is
  unit-tested with stubs (7 cases). `auth-stream.ts` wires it to the real
  auth/* modules (CredentialStore + RefreshManager + AuthToolsState with a
  stub connectionManager; signs in on ReauthRequired). The Bearer is the OIDC
  id_token — byte-identical to what the hub validates.
- bundle: scripts/bundle.mjs factors shared esbuild options and emits a second
  entry, dist-bundle/auth-stream.mjs, riding the existing include_dir embed.

Rust:
- token_bridge.rs (NodeBridge): reuses quarto-mcp-launcher (node discovery +
  bundle extract + env injection; a few new pub use re-exports) to spawn
  `node auth-stream.mjs` with piped stdio (stderr inherited so the user sees
  the sign-in URL); a stdout-reader task parses token/error frames into a
  cache; impl TokenSource feeds the BearerDialer a fresh token per reconnect.
- provide_hub.rs + main.rs: the `q2 provide-hub <share-url|id> [--server]`
  subcommand → NodeBridge → join_and_list_files.

Tests:
- TS protocol (7), Rust frame parser (4), share-URL/server parsing (3).
- Integration: the NodeBridge spawns the *real* bundled helper with no creds
  and the bridge surfaces its error frame (ran, not skipped). Clippy -D
  warnings clean (async mutex on the helper stdin).

The interactive real-OAuth E2E (browser sign-in → list) is manual; the
automated coverage above stands in for it. Like q2 mcp, q2 provide-hub needs
the hub-mcp bundle built (cargo xtask build-hub-mcp-bundle).

Plan: claude-notes/plans/2026-06-29-remote-execution-provider.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sfet3264)

Architecture for the payoff phase: provider subscribes to the exec channel,
materializes the project to a temp dir from the VFS, runs record_capture_cached
(the native engine path), writes the capture binary doc + CaptureRef sidecar
(reusing quarto-preview/quarto-hub fns) so all peers see the output, and
broadcasts the capability beacon; hub-client gains the Run button. Open
decisions recorded: D4 v1 authz posture, D5 claims now/later, 4a/4b sub-split,
re-execution cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utor v1, always-fresh run (bd-sfet3264)

D4: execution restricted to the providing user's own per-project actor id by
default; --allow-all opens it to everyone with document access (noun: provider,
not owner). D5: single-executor v1 — two providers double-execute (documented);
true mutual exclusion needs server arbitration (Phase 6). Split 4a (execute +
write capture, scripted/testable) / 4b (Run UI). Always force a fresh run
(uncached record_capture) due to side effects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…264)

Add a prominent 'Known limitations (v1)' section: two providers on one project
double-execute (duplicate side effects); peer-to-peer mutual exclusion is
impossible without a server arbiter (Phase 6). Also notes capture-doc orphaning
(no GC yet) and the manual interactive-OAuth E2E. Accepted for v1 by the user.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hase 4a)

The native q2 provider now runs code on request. After joining a hub it
broadcasts an `exec/beacon` on the index DocHandle's ephemeral channel every
3s, and on an `exec/request` it materializes the project's VFS to a fresh temp
dir, runs the engines via the uncached `record_capture` (decision #4: always a
fresh run), and writes the result back as a capture binary doc + `CaptureRef`
sidecar — the transport the Phase 1 editor already consumes. Every piece reuses
Phases 1–3: the capture write-back mirrors quarto-preview's re_execute.rs.

New in crates/quarto-hub-provider:
- exec_channel.rs: Rust mirror of the TS execution wire format. `ExecMessage`
  is an internally-tagged enum (camelCase renames) encoded with ciborium — the
  browser's DocHandle.broadcast CBOR-encodes payloads (cbor-x, useRecords:false
  → standard CBOR maps), so ciborium interops byte-for-byte. A unit test
  asserts the CBOR shape is a map with the exact keys the TS parseExecMessage
  checks.
- materialize.rs: read-only VFS → temp-dir materializer (text via doc.text,
  binary via resource::read_binary_content), with a `..`/absolute path guard.
- execute.rs: `Provider` (Arc-shared) with a `run` loop = concurrent
  beacon-broadcast + ephemeral request-listen; `execute_document`
  (materialize → discover → record → write capture doc → set_capture);
  `AuthzPolicy` (AllowAll/Deny).
- join.rs refactored to expose `join()` returning the live (Repo,
  IndexDocument); `join_and_list_files` now builds on it.

Authorization (Phase 4a: mechanism-first, fail closed, per user 2026-07-01):
`q2 provide-hub` is fail-closed by default (connect + list + exit) and serves
requests only with `--allow-all`. The real provider-only default (gate on the
provider's own per-project actor id) needs the hub to accept a Bearer on
/auth/actor and lands in Phase 5; `AuthzPolicy` is the seam.

Verified end-to-end: a scripted integration test drives the real provider loop
against a real samod acceptor — editor broadcasts exec/request → provider writes
a capture that syncs back to the server (gunzipped to an EngineCapture with
engine_name == "test-passthrough"); a Deny companion writes nothing. Full
`cargo xtask verify` green (27 provider tests; workspace build/test; WASM
rebuild; hub-client tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sfet3264 Phase 4b)

The editor can now ask a connected `q2 provide-hub` executor to run a document.
When an executor's capability beacon is live and the active document has
executable cells, the preview pane shows a Run/Re-run control that broadcasts an
`exec/request` on the index handle's ephemeral channel; the provider executes
and writes the capture back, which the Phase 1 consumption path already splices
into the preview.

- executableCells.ts: `hasExecutableCells(content)` — gates the affordance
  (braced engine fences ```` ```{r} ````, not the dotted display-class form).
- useExecutionChannel now returns `{ executors, requestExecution }` (the channel
  is held in a ref so the callback is stable); App threads `requestExecution`
  to Editor.
- RunControl.tsx: presentational Run/Re-run affordance reflecting the durable
  CaptureRef status — disabled "Executing…" while a local pending flag or
  `state: running`; inline `lastError` on `state: error`; a "code changed" note
  on staleness. Pending clears when a new captureDocId arrives, on error, or
  after a 30s timeout (the ephemeral request may reach no executor). The UX
  mirrors q2-preview-spa's StaleCaptureOverlay but triggers via automerge, not
  loopback HTTP.
- Editor shows RunControl when `executorsOnline && hasExecutableCells(content)`,
  keeping the plain "Executor online" bar for non-executable docs.

Tests: unit +5 (executableCells), integration +9 (useExecutionChannel run
request/offline; RunControl states). Full hub-client suite green (unit 685 /
integration 95 / wasm 124); `npm run build:all` (strict tsc -b + vite) green.

No faithful browser E2E (same rationale as Phase 1G): a real click-through needs
a browser *and* a live provider against a shared hub (interactive OAuth + a
running executor), which can't be automated here. The scripted Phase 4a test
proves the provider half E2E; these tests prove the editor wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hase 4b)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-sfet3264)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e2e harness (bd-sfet3264)

`q2 provide-hub --token <bearer>` (or the QUARTO_HUB_TOKEN env) uses a
StaticTokenSource instead of spawning the interactive Node OAuth bridge. It's a
dev/testing hatch for a local, no-auth `q2 hub` — which ignores the bearer
entirely (server.rs: no auth_config ⇒ no credential check). The OAuth path is
unchanged when --token is absent. This is what StaticTokenSource's doc comment
always anticipated ("the dev --token path").

Add claude-notes/hub-execution-e2e/: a runnable local end-to-end harness for the
Run-button feature — an example project (hello.qmd with `engine: jupyter`), a
start-local-hub.sh helper that boots the no-auth hub and prints the index-doc id
+ the exact hub-client URL and provider command, and a README walkthrough.

Verified directly while building this: the no-auth hub answers /health with the
index-doc id; `q2 provide-hub --token dev` connects and lists files; and the
Jupyter engine executes 2+3→5 on this machine (via q2 render with
`engine: jupyter`, the same registry the provider uses). Full cargo xtask verify
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gotcha (bd-sfet3264)

The local q2-hub path only serves its own watched project, so creating a fresh
project in hub-client against a project-mode hub yields '0 file(s)' in the
provider. Restructure the walkthrough to lead with the simplest verified path
(hub-client on its default wss://sync.automerge.org + provider on the same, no
q2 hub), and document the project-mode caveat + the --no-project relay
alternative for the fully-local path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnostic + regression harness for the bug found during Phase 4 e2e: the samod
provider materializes 0 files for a document authored by a JS automerge-repo
peer (hub-client), so execution fails with "project discovery failed …".

- relay_sync.rs (2 self-contained tests, pass): samod peer B DOES sync a doc
  created by samod peer A through a bare relay AND a NeverAnnounce relay — so
  it's not the announce policy or the BearerDialer.
- sync_probe.rs (ignored; need network/a live doc): probe_live_doc_sync watches
  an existing doc's files map sync; probe_create_then_find shows two SAMOD peers
  sync fine via sync.automerge.org. Together they isolate the gap to
  JS-authored docs → the Rust samod peer (likely an automerge version/protocol
  mismatch: JS @automerge/automerge 3.2.6 / repo 2.5.6 vs Rust automerge 0.8.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…as Text (bd-bm0vaetl)

The provider's "0 file(s)" is NOT a sync failure (the doc syncs completely from a
JS automerge-repo peer to samod). hub-client (@automerge/automerge 3.2.6) stores
`files[path] = docId` as an automerge Text object, but quarto's Rust
IndexDocument::get_all_files reads values via ScalarValue::to_str(), which
returns None for Text — so a JS-authored files map reads empty.

- interop-repro/js-peer/{server.mjs,peer.mjs}: minimal JS automerge-repo sync
  server + create/read peer, independent of quarto-hub/hub-client.
- sync_probe.rs: probe_root_keys dumps the synced doc — shows
  `files=map{...=Object(Text)} value=Int(42)` (content DID sync; values are
  Text). probe_files_str_or_text validates the fix: reading each value as
  Str-or-Text (doc.text() for Text objects) recovers the ids.

Fix lives in crates/quarto-hub/src/index.rs (get_all_files/get_file: accept
Str or Text). Same class as the metadata-as-str lint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e (bd-bm0vaetl)

hub-client (@automerge/automerge 3.x) stores `files[path] = docId` string values
as automerge Text objects, not scalar strings. IndexDocument::get_all_files /
get_file read values with Value::to_str(), which returns None for Text — so a
project created in hub-client read as ZERO files and `q2 provide-hub` failed to
materialize anything ("project discovery failed: canonicalize No such file").

Add a `read_str_or_text` helper (scalar Str OR Text via doc.text()) and use it
in get_all_files/get_file. Rust-authored docs (project-mode hub, scalar Str)
keep working; hub-client docs now read. Same class as the metadata-as-str lint.
The document itself synced fine JS→Rust all along (an earlier theory that samod
couldn't sync JS docs was wrong).

Tests:
- index.rs: get_all_files_reads_text_valued_ids (Text + scalar entries).
- materialize.rs: materializes_a_js_authored_index_with_text_valued_ids.
- interop-repro/: minimal JS automerge-repo peer/server + the Rust probes that
  isolated the cause; e2e-run.mjs drives the whole loop (JS creates project →
  provider runs knitr → capture read back = STDOUT "1 2 3").

Verified end-to-end headlessly: a JS-authored project now lists its file, the
provider materializes + runs the knitr engine, writes the capture doc + sidecar,
and a JS peer reads the executed output (cat(1,2,3) → "1 2 3").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan to make hub-client's default format:html preview display server-recorded
engine captures (today only format:q2-preview does). Layered fix mapped across
WASM HTML branches, preview-runtime renderToHtml, and hub-client <Preview>;
CaptureSpliceStage inserts into the HTML pipeline unchanged. Awaiting review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…4uygha Phase 1)

Phase 1 of making hub-client's default `format: html` preview display
server-recorded engine captures (today only `format: q2-preview` does).

- HtmlRenderConfig gains a `captures: Vec<EngineCapture>` field + `with_captures`
  (default empty).
- Extract a shared `insert_capture_splice_stage` helper (the splice-insert +
  engine-stage rebuild-with-spliced-names, formerly inline in
  `build_q2_preview_pipeline_stages`) so the q2-preview and HTML capture paths
  can't drift — both must stay cell-aligned with `build_capture_pipeline_stages`.
- New `build_html_pipeline_stages_with_captures`; `render_qmd_to_html` uses it
  when captures are present (empty → unchanged builder, byte-identical).
- `RenderToHtmlRenderer::with_captures` threads the active page's captures into
  the website Pass-2 HTML render.

Cell alignment holds by construction: captures are recorded from this same HTML
stage list truncated at engine-execution.

Tests: pipeline.rs `render_qmd_to_html_splices_captures` (hand-built
`.cell`-wrapped capture, fictitious non-spawning engine, mirroring
captureSplice.wasm.test.ts); integration `render_to_html_captures.rs`
(multi-file project, ActivePage mode). Full quarto-core suite (2406) + clippy
green. WASM branches still to thread captures (Phase 2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…4uygha Phase 2)

Thread the already-parsed captures (they arrive via
render_page_in_project_with_attribution) into both HTML render branches, which
previously dropped them:
- render_single_doc_to_response `_ =>` arm builds the HtmlRenderConfig with
  `.with_captures(captures)`.
- render_project_active_page_to_response `_ =>` arm calls
  `renderer.with_captures(captures)` on RenderToHtmlRenderer.

No new wasm-bindgen signature — only the Rust branches change.

RED→GREEN WASM vitest captureSpliceHtml.wasm.test.ts: a `format: html` doc + a
gzipped capture → the marker appears in the rendered `html` (RED against the
pre-rebuild WASM, GREEN after build:wasm); no-capture ⇒ source-only. Full WASM
suite (126) green.

The TS convenience wrappers (renderToHtml/renderPageInProject) still need to
forward captureGzJson, and hub-client's <Preview> needs to fetch + pass them
(Phases 3–4) before this is user-visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-uy4uygha Phase 3)

RenderToHtmlOptions gains captureGzJson?; renderToHtmlInner forwards it into
renderPageInProject, which gains a captures param forwarding to the
already-capable renderPageInProjectWithAttribution. Pure pass-through so the
default HTML preview (Preview.tsx → renderToHtml) can carry captures to the
WASM HTML splice (Phase 2). tsc clean; preview-runtime tests (74) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…view (bd-uy4uygha Phase 4)

The default `format: html` preview (plain documents + every website page) now
displays server-recorded engine captures, so a document executed by a connected
`q2 provide-hub` shows its output — previously only `format: q2-preview` did,
which is why the output silently failed to appear for normal docs.

- New shared hook `useActiveCaptureBytes` (derive the active file's captureDocId
  from the sidecar, fetch the capture bytes via getBinaryDocById, keyed on the
  doc id). ReactPreview refactored to use it (removing its inline copy).
- Preview consumes it and threads `captureGzJson` into `renderToHtml`, with the
  bytes in the render-callback deps so a freshly-arrived capture re-renders.
- PreviewRouter now passes `captures` to <Preview> (it previously dropped it,
  routing captures only to ReactPreview / q2-preview).

Tests: useActiveCaptureBytes.integration (3), Preview.capture.integration (2,
mirroring the ReactPreview test); existing ReactPreview capture test still green
after the refactor. Full hub-client test:ci green (integration + wasm 126);
strict build:all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gha)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed e2e

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…us line (bd-yai4w8ly)

The preview pane showed two stacked status strips for code execution: the
executor/Run bar (RunControl / .executor-online-bar — "Executor online" +
Run/Re-run) and the capture bar (ClearCaptureControl — "Showing executed
output" + Clear results…). When an executor was online and a capture existed
they stacked as two differently-colored rows saying closely related things.

Replace all three with a single PreviewStatusBar that renders at most one row:
a green liveness dot (when an executor is online), one status label chosen by
precedence (Executing… → error → "Showing executed output" [· code changed] →
Executor online), and a right-aligned action group. Buttons are gated
independently — Clear whenever a capture exists, Run/Re-run whenever an
executor is online and the doc has executable cells — rendered [Clear] [Run]
so Run stays pinned to the far right across state transitions. The run-pending
snapshot/timeout and the two-step clear confirmation are preserved verbatim.

Verified end-to-end in a real browser against the local Option-B hub harness:
Run → "Showing executed output" (single row) → Clear confirm → source-only.

Plan: claude-notes/plans/2026-07-01-merge-preview-status-line.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… bug (bd-gthycd33)

Adds r-demo.qmd (engine: knitr) to the local e2e harness — a knitr companion to
the jupyter hello.qmd. Browser testing during bd-yai4w8ly confirmed knitr output
splices correctly into the hub-client preview (`[1] 2`, R version string,
`[1] 55`), while jupyter does not (source-only despite a capture arriving) —
filed as bd-gthycd33. Records that finding in the status-line plan's e2e notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Jul 2, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants