Preview: transport engine-generated figures in the capture doc (bd-qbhp2cvv)#410
Merged
Merged
Conversation
Adds CaptureFile { path, contents_base64 } and a serde-defaulted
files: Vec<CaptureFile> to quarto_trace::EngineCapture, the carrier
for engine-generated supporting-file bytes (knitr/jupyter figures)
so preview replay can materialize them into the WASM VFS.
Wire-format compatible both ways: old captures deserialize (default),
and captures without files serialize without the key (skip_serializing_if),
byte-identical to the pre-field format. Pinned by three new tests in
quarto-trace/tests/roundtrip.rs. All EngineCapture literal sites
(tests/fixtures) updated with files: Vec::new().
Also switches this worktree's plan symlink/context to
claude-notes/plans/2026-07-23-preview-engine-supporting-files.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New quarto-core module engine/capture_files.rs: - collect_capture_files: resolves ExecuteResult.supporting_files entries (absolute or doc-relative; files or directories, walked recursively via SystemRuntime) against the doc dir, reads bytes, and returns CaptureFiles keyed by doc-relative forward-slash paths. Fail-soft: unreadable or outside-doc-dir entries warn and skip. Sorted listings keep capture bytes deterministic for the hash-keyed capture cache. - materialize_capture_files: the splice-time counterpart (wired in the next commit); validates paths stay under the doc dir since captures are remote data. EngineExecutionStage embeds the collected files in the EngineCapture aux payload, gated by the new PipelineObserver::wants_engine_capture_files() (default false — plain q2 render and trace observers pay zero extra I/O). Only the preview recorder's CaptureCollector opts in; its captures replay on machines (hub peers, browser VFS) that cannot see the recording disk — and the hub records in a temp dir deleted right after, so recording time is the only moment the bytes exist. Tests: 8 unit tests on collect/materialize edge cases + an end-to-end record_capture test with a figure-writing engine (written first, observed failing with files=[]). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CaptureSpliceStage now writes each capture's embedded supporting files (CaptureFile bytes from Phase 2) next to the live document before splicing, via ctx.runtime. In WASM that runtime IS the preview VFS — the exact place the SPA/hub-client image resolvers (assetWalker.ts blob URLs, iframePostProcessor.ts data URIs) read project-relative image srcs — so spliced engine output like  resolves without any TS changes. Doc dir = doc_ast.path.parent() (absolute in the WASM VFS), with a project-dir fallback for bare source names in native harnesses. Materialization is fail-soft and path-validated (no escape from the doc dir; captures are remote data). Test (written first, observed failing with NotFound): a render_qmd_to_html run over a temp-dir project with a capture whose files carry fake PNG bytes must both splice the image ref into the HTML and materialize the bytes at <docdir>/test_files/figure-html/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gzip_captures + capture_doc_size_warning (pure, unit-tested; threshold 10MB per plan decision) live in quarto_core::engine::capture_files. The three capture writers — quarto-preview's eager driver + re-execute handler and quarto-hub-provider's exec server — previously duplicated the serialize+gzip block; all now call the shared helper, so oversized capture docs (embedded figure bytes can inflate multi-plot docs) warn uniformly at every write site. Dependency cleanup: flate2 promoted to a regular dep of quarto-core (already in its graph via quarto-trace); dropped to dev-dep in quarto-hub-provider (only its integration test still gunzips). Full workspace suite: 10370 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion The Phase 4 flate2 dependency line had landed in quarto-core's native-only [target.'cfg(not(wasm32))'.dependencies] section, breaking the WASM build (capture_files.rs compiles on both targets). Moved to [dependencies]; flate2 builds cleanly on wasm32 (miniz_oxide) and the WASM client already uses it to gunzip captures. E2E (recorded in the plan): knitr repro verified in a real browser — capture embeds the 22KB ggplot PNG, preview renders it via a blob URL (672x480, loaded, no console errors). jupyter is blocked upstream: the engine emits only text reprs for matplotlib figures even under q2 render (no supporting files to transport) — filed bd-rwz8kwia, discovered-from bd-qbhp2cvv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
map_or_else in CaptureSpliceStage's doc-dir fallback and sort_unstable in the capture_files test, both flagged by the cargo xtask verify clippy leg. Full verify now passes end to end (Rust build + nextest, ts-packages, hub-client build:all + test:ci, q2-preview-spa build). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This was referenced Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Knitr/jupyter execution results that include images appeared in
q2 renderoutput but were broken images inq2 previewand the hub-client q2-preview format. Root cause: theEngineCapturerecorded for preview replay serializedExecuteResult.supporting_filesas paths only — the replaying machine (or browser VFS) can never see those files, and the hub even records captures in a temp dir that is deleted right after. The only code that turned those paths into bytes (copy_resources_to_output_dir) is native-render-only.Fix: carry the bytes inside the capture binary doc and materialize them into the (WASM) VFS at splice time, where the existing image resolvers already look.
CaptureFile { path, contents_base64 }+ serde-defaultedEngineCapture.files. Wire-compatible both ways (old captures deserialize; file-less captures serialize byte-identically).EngineExecutionStageembeds supporting-file bytes (recursive dir walk, doc-relative forward-slash paths, fail-soft), gated by newPipelineObserver::wants_engine_capture_files()— only the preview capture recorder opts in, soq2 renderpays zero extra I/O.CaptureSpliceStagematerializes embedded files next to the live doc viactx.runtime(in WASM: the preview VFS). Path-escape validation since captures are remote data. Zero TypeScript changes needed —assetWalker.ts/iframePostProcessor.tspick the files out of the VFS as-is.gzip_captureshelper (+ 10 MB size warning) replaces three duplicated gzip blocks (eager driver, re-execute handler, hub-provider exec server).End-to-end verification
ggplotcell): capture embeds the 22 KB PNG; in a real Chrome session the preview renders<img src="blob:…">at 672×480 with no console errors. Record inclaude-notes/plans/2026-07-23-preview-engine-supporting-files.md.q2 render(no supporting files to transport). Filed bd-rwz8kwia (discovered-from bd-qbhp2cvv); this transport is engine-agnostic and picks up jupyter figures once that engine gap is fixed.Test plan
render_qmd_to_html) — each behavior test written first and observed failing.cargo xtask verifypasses end to end (Rust + clippy -D warnings + ts-packages + hub-client build:all + test:ci + q2-preview-spa build).🤖 Generated with Claude Code