(hub-client) Allow specifying sync server when creating new project#4
Merged
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
cscheid
added a commit
that referenced
this pull request
Apr 17, 2026
Closes out Phase 1 of the crossref plan.
- `crossref::codeblock_shorthand`: detect `#| label: <reftype>-..` in
`CodeBlock.text`, partition cell options into consumed (label,
<reftype>-cap/-scap/-alt) vs pass-through (echo, eval, engine-
specific), rewrite the text to drop consumed keys, and wrap the code
block in a `Div(#<label>)` scaffold with the caption paragraph.
Driven from `PreEngineSugaringStage` so the engine then sees plain
code blocks; the Div survives QMD round-trip (pinned by the Phase 0.7
fixture).
- `transforms::crossref_render` (finalization phase): converts
`CustomNode("FloatRefTarget")` with `ref_type=fig` into Pandoc's
native `Figure` so the HTML writer emits
`<figure><figcaption>Figure N: ...</figcaption></figure>`; other
ref_types become a `Div` wrapping content with a trailing numbered
caption paragraph. `CustomNode("CrossrefResolvedRef")` becomes a
`Link` with class `quarto-xref` pointing at `#<id>`; text is
"<Kind> <N>" for resolved refs or "?id?" for unresolved ones so the
failure is visible in output.
- `crossref::metadata` now uses `ConfigValue::as_plain_text` so
`crossref.custom` values parsed by pampa as `PandocInlines` resolve
to the underlying string (was reading as None before).
- `tests/crossref_fixtures.rs`: 11 integration fixtures parsing qmd
through pampa + running the crossref pipeline, asserting over the
resulting `CrossrefIndex` per plan success criterion #4 (structured
data, not HTML snapshots). Covers all four authoring shapes, counter
behavior, section paths, duplicate ids, unresolved refs,
@-disambiguation between crossref and citation prefixes, custom
categories via `crossref.custom`, and the code-block shorthand
end-to-end.
End-to-end `quarto render` on a hand-written qmd produces the expected
HTML: `<a href="#fig-foo" class="quarto-xref">Figure 1</a>` and
`<figure id="fig-foo"><p>Figure 1: <caption></p>...</figure>`.
Full workspace: 7362 tests pass; `cargo xtask verify` clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cscheid
added a commit
that referenced
this pull request
May 13, 2026
Drafts the engine-less CLI skeleton for `q2 preview` after bd-hfjj
landed the decomposition. Seven sub-tasks (A.0 through A.6) plus an
e2e smoke (A.7):
A.0 Lift hub-client/src/wasm-js-bridge/ to a new
@quarto/wasm-js-bridge workspace package; alias `/src/wasm-
js-bridge` in hub-client + q2-preview-spa + preview-renderer
tests. Resolves Q-A1 the right way (no duplication, no
cross-platform symlink fragility).
A.1 Stub `quarto preview` clap subcommand.
A.2 New crates/quarto-preview/ shell crate with the
quarto-trace-server-shaped include_dir! + build.rs
placeholder.
A.3 Fill in q2-preview-spa/src/main.tsx so the SPA actually
drives a render. <PreviewApp> stays local for now.
A.4 `cargo xtask build-q2-preview-spa` + wire into `build-all`.
A.5 End-to-end boot test (tempdir lifecycle, samod handshake,
URL-fragment carrier).
A.6 Always-visible "force re-render" button (epic resolution #4).
A.7 Playwright smoke (real-browser flow incl. DOM-stability
invariant).
Q-A3 source-code audit corrected an inherited claim: the epic and my
initial draft both said the SPA reads `indexDocId`/`wsUrl` from a
server-injected `<meta>` tag "the same trick share links use." Both
halves wrong. hub-client's share links use URL *fragments*
(`hub-client/src/App.tsx:272`, `utils/routing.ts`); there's no
meta-injection in the codebase. trace-server serves `index.html`
unmodified (`crates/quarto-trace-server/src/lib.rs:185-187`). Phase
A now uses the same URL-fragment pattern: the CLI opens
`http://<host>:<port>/#/preview/<indexDocId>`, the SPA reads
`window.location.hash`, the WebSocket URL derives from
`window.location`. No new server-side patterns introduced.
Q-A5 captures a future direction: persistent samod storage inside
`.quarto/preview/samod/` to amortize the "blank screen at boot" cost
on large projects. Phase A measures the cost; future phases act.
TDD: every sub-task is structured tests-first.
Status: approved 2026-05-13 (all open questions resolved); ready to
file beads sub-issues and begin A.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cscheid
added a commit
that referenced
this pull request
May 13, 2026
…hase A.6) Adds a small circular `↻` control overlaid on the top-right of the preview pane (`aria-label="Refresh preview"`). Click → PreviewApp bumps its `contentTick` so the render `useEffect` re-fires — reusing the one channel that the sync handler's `onFileContent` already uses, so there's a single path through the render pipeline regardless of who asks for the re-render. The button satisfies the epic's resolution #4 (force-refresh invariant): the user always has an escape hatch when the dep-graph misses a cross-doc relationship. Phase A has no dep-graph at all, so it's largely redundant today, but the affordance is part of the preview UX contract going forward; Phase C will extend the click handler to trigger server-side re-execution where applicable. Tests: - New integration test in `PreviewApp.integration.test.tsx` pins the contract at the runtime seam: click → at least one additional `renderPageForPreview` call. Match by accessible name rather than test-id so the UX label is part of the test. Manual smoke (Chrome DevTools): button rendered at (1876, 12) on a 1920x1080 viewport; click runs without console error; iframe content stays consistent; refresh affordance survives clicks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cscheid
added a commit
that referenced
this pull request
May 14, 2026
Adds the §C.1 integration test #3 the plan asks for: drive `quarto_preview::run_with_on_ready` (new) against a tempdir fixture with a `{test-passthrough}` code cell, wait for the sidecar entry to appear, then verify the linked binary doc round-trips back to a parseable `EngineCapture`. Surface changes: - New `quarto_preview::run_with_on_ready(config, extra_on_ready)` variant. Same lifecycle as `run`, plus an extra callback fired after the eager-capture driver has been enqueued. Tests use it with a oneshot channel to lift `Arc<HubContext>` out of the server task and poll the IndexDocument's sidecar through real samod reads — no duplicated wire-protocol code, no HTTP shim. `run` delegates to it with a no-op extra hook so the CLI is unchanged. - `capture_driver::read_capture_from_doc` promoted from `#[cfg(test)]` to plain `pub`. The on-the-wire format (gzipped JSON in a binary-doc envelope) is shared with the Phase C.4 WASM/SPA reader, so a public server-side reader is valuable for tests today and for any future inspection/debug surface. - `automerge` + `samod` moved from dev-dependencies to regular dependencies (versions pinned to match quarto-hub's). Required by the now-public `read_capture_from_doc`. Tests (2 new): - `tests/eager_capture.rs::eager_capture_populates_index_sidecar` — Spawn run_with_on_ready, oneshot the ctx out, poll `ctx.index().get_capture("doc.qmd")` up to 30 s, fetch the capture binary doc, verify `engine_name == "test-passthrough"`, `input_qmd` contains the cell body sentinel, and `result.markdown` contains the engine's passthrough comment. - `tests/eager_capture.rs::prose_only_doc_leaves_sidecar_empty` — Negative path (§C.1 integration #4): drive the full server against a prose-only fixture, sleep 2 s, assert `has_capture` is still false. End-to-end binary smoke (per CLAUDE.md "End-to-end verification before declaring success"): $ cargo run --bin q2 -- preview /tmp/c1-smoke --no-browser ... INFO quarto_hub::context: Discovered project files qmd_count=1 INFO quarto_hub::context: samod repo initialized INFO quarto_hub::index: Created new index document doc_id=... INFO quarto_hub::server: Hub server listening (project mode) addr=127.0.0.1:64574 INFO quarto_hub::server: Starting filesystem watcher (SIGTERM) INFO quarto_hub::server: Server shutting down... The eager-capture driver's on_ready hook fires inside `run_with_on_ready`; a prose-only doc produces no capture (verified by the integration test). Real-engine end-to-end (jupyter/knitr) was NOT verified this session — no R/Python runtime configured. The integration test substitutes a test-passthrough engine via PreviewConfig::engine_registry to exercise the full lifecycle deterministically. quarto-preview: 11/11 tests pass (5 unit + 4 smoke/boot + 2 new eager-capture). Plan: claude-notes/plans/2026-05-13-q2-preview-phase-c.md §C.1 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cscheid
added a commit
that referenced
this pull request
May 19, 2026
* docs(q2-preview): epic plan + beads (bd-kw93) for q2 preview workstream
Architecture: ephemeral local hub-client (samod + file watcher) serves
a pared-down React SPA that renders via the q2-preview format. Engines
run server-side via `engine: replay` and the EngineCapture flows
through automerge to the browser, which replays in WASM. No DOM
rebuild on edit — Bootstrap / MathJax / reveal.js state survives.
Phasing: A (skeleton + standalone SPA serving), B (broadened watcher +
dep-graph invalidation), C (record-on-demand engine + replay), D
(polish), E (stretch). All seven open questions resolved in the
2026-05-11 reviews — see plan §Resolutions from 2026-05-11 review #2.
Two follow-ups filed:
- bd-hfjj (epic, blocks bd-kw93): hub-client decomposition — shared
preview-pane package so hub-client and the preview SPA stay in
lockstep by construction.
- bd-56b0 (task, related to bd-kw93): cross-doc dependency channel
audit. Phase A's always-visible force-refresh button is the user
escape hatch until this lands.
Branch: feature/q2-preview. No code changes; this is the plan +
beads index update.
* docs(q2-preview): hub-client decomposition sub-epic plan (bd-hfjj)
Concretizes the bd-hfjj sub-epic of bd-kw93 with package boundaries
(two packages: @quarto/preview-renderer + @quarto/preview-runtime),
SPA location (top-level q2-preview-spa/), MVP scope (React preview
path only; Preview.tsx / slides / debug stay in hub-client), WASM
access (existing symlink + per-consumer alias), and a 7-phase
sequencing that keeps tests green across each move.
This branch holds both this sub-epic plan and the parent epic plan
(via the 03fc1238 ancestor commit). Parked off feature/q2-preview
while the other workstream there settles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(q2-preview): scaffold preview-renderer + preview-runtime workspaces (bd-hfjj Phase 1)
Creates two empty workspace packages mirroring the
ts-packages/quarto-sync-client pattern:
- `@quarto/preview-renderer` — pure React; jsdom-based integration
config; deps on react, react-dom, morphdom.
- `@quarto/preview-runtime` — WASM + automerge glue; both vitest
configs declare the `wasm-quarto-hub-client` alias pointing at
hub-client's existing symlink; deps on @automerge/automerge,
@automerge/automerge-repo, @quarto/quarto-sync-client,
@quarto/quarto-automerge-schema, web-tree-sitter.
Each package ships an `export {}` index, a trivial placeholder test
(green on both), and a small jsdom polyfill `test-utils/setup.ts` so
its integration config is self-contained. Both pass `test`,
`typecheck`, and `build`.
Phase 0 pre-flight is also captured in this commit:
- catalogue audit added customRegistry, atomicCustomNodes, and
utils/sourceInfo to the renderer move list (drift discovered by
grep against the current tree);
- test-helper placement section records dispositions
(mockSyncClient/mockWasm → runtime; visibility/setup/userSettings
stay in hub-client);
- plan status flipped from "draft — awaiting review" to "approved;
Phases 0–1 complete; Phase 2 next".
Hub-client probe imports (added to main.tsx, compiled cleanly through
both `tsc --noEmit` and `vite build`, then reverted) verified that
both packages resolve via npm's workspace symlinks + tsc's bundler
moduleResolution + vite's `source` condition. No changes to
hub-client landed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(q2-preview): move types + utils to preview-renderer (bd-hfjj Phase 2)
Moves 5 type modules and 7 utility modules out of hub-client into the
new @quarto/preview-renderer workspace, behind sub-path exports:
ts-packages/preview-renderer/src/types/
project.ts (+ test), diagnostic.ts, artifactPaths.ts,
sourceInfo.ts, intelligence.ts
ts-packages/preview-renderer/src/utils/
vfsPaths.ts (+ test),
iframeLinkHandlers.ts (+ integration test),
componentPath.ts (+ test),
stripAnsi.ts (+ test),
customRegistry.ts (+ test),
atomicCustomNodes.ts,
sourceInfo.ts (+ test)
`git mv` preserves history. All 64 importers across 44 hub-client
files are rewritten from `../<dir>/types/<m>` / `../<dir>/utils/<m>`
to `@quarto/preview-renderer/types/<m>` / `/utils/<m>`. The package's
exports map uses wildcard sub-paths because `types/diagnostic` and
`types/intelligence` both legitimately export `Diagnostic` /
`DiagnosticDetail` (the former @deprecated), and a single barrel
would force every importer to alias one side.
Two files originally scheduled for Phase 2 were deferred after audit:
- `components/ThemeContext.tsx` — no moving file consumes it, but it
imports `services/preferences/` (localStorage). The sound move is a
DI refactor, deferred until the SPA actually needs theme switching.
Tracked as follow-up `bd-hfjj-fu-theme`.
- `utils/iframePostProcessor.ts` — imports `vfsReadFile` /
`vfsReadBinaryFile` from `services/wasmRenderer`, which itself
moves in Phase 5. Moving the postprocessor with wasmRenderer then
is cleaner than adding a DI seam now. Its remaining internal import
of `./vfsPaths` is updated to `@quarto/preview-renderer/utils/vfsPaths`.
Hub-client's three vitest configs gain a
`@quarto/preview-renderer` → `ts-packages/preview-renderer/src` alias.
Vitest doesn't honor the package.json `source` condition the same way
Vite's prod build does on fresh clones; the alias matches the
existing sync-client / automerge-schema pattern.
preview-renderer adds `@quarto/quarto-automerge-schema` as a workspace
dep (required by `types/project`), and its own vitest.config.ts gains
the same alias so its tests resolve workspace deps to source.
Hub-client's test:ci, build:all, and `cargo xtask verify
--skip-rust-tests` all pass. Preview-renderer's unit (81 tests across
7 files) and integration (7 tests) suites pass.
Snapshot tests: no snapshot files changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(hub-client): changelog entry for bd-hfjj Phase 2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(q2-preview): move framework subtree to preview-renderer (bd-hfjj Phase 3)
Moves the entire components/render/framework/ subtree out of hub-client
into @quarto/preview-renderer:
ts-packages/preview-renderer/src/framework/
Ast.tsx, RegistryContext.tsx, dispatch.tsx,
customNode.ts (+ test), meta.ts (+ test),
plainText.ts (+ test), types.ts, index.ts
framework is exposed as a single barrel via `./framework` in
package.json's exports map (not a wildcard sub-path). Rationale: the
subtree mixes .tsx and .ts files, and Node's exports map can't
pattern-match both extensions in a single wildcard entry cleanly.
Every symbol re-exports through framework/index.ts already, so
consumers don't need direct sub-file imports.
107 imports across 60 hub-client files rewritten to
`from '@quarto/preview-renderer/framework'`. One dynamic
`await import('./framework')` in parity.integration.test.tsx also
caught and rewritten.
Framework's internal cross-dir imports — Phase 2 had rewritten
Ast/RegistryContext/dispatch to use self-package
`@quarto/preview-renderer/...` paths since their targets had moved
already — are converted to idiomatic relative paths
(`../types/sourceInfo`, `../utils/sourceInfo`, etc.) now that
framework itself lives inside the package.
preview-renderer's test suite now exercises 133 tests across 10 files
(adds customNode/meta/plainText tests). hub-client test:ci, build:all,
and `cargo xtask verify --skip-rust-tests` all green.
Snapshot tests: no snapshot files changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(hub-client): changelog entry for bd-hfjj Phase 3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(q2-preview): move services to preview-runtime (bd-hfjj Phase 5, swapped before Phase 4)
Moves the WASM-bridge / automerge-sync / user-grammar services out of
hub-client into the new @quarto/preview-runtime workspace, plus pulls
iframePostProcessor (deferred from Phase 2) into @quarto/preview-renderer
now that its `vfsReadFile` / `vfsReadBinaryFile` dependency lives in
runtime.
To preview-runtime:
src/wasmRenderer.ts (+ test)
src/automergeSync.ts (+ test)
src/userGrammar/Discovery.ts (renamed; + test)
src/userGrammar/Cache.ts (renamed; + test)
src/userGrammar/Highlight.ts (renamed; + WASM-init test)
src/test-utils/mockSyncClient.ts (test helper)
src/test-utils/mockWasm.ts (test helper)
src/wasm-quarto-hub-client.d.ts (mirrored from hub-client so the
package typechecks standalone)
src/vite-shims.d.ts (`*.wasm?url` and `/src/wasm-js-bridge/*.js`
ambient declarations)
To preview-renderer:
src/utils/iframePostProcessor.ts (+ unit and integration tests)
Phase reordering: the original plan had Phase 4 (q2-preview / iframe
wrappers / overlays) before Phase 5 (services). Swapping is *safer*:
most Phase-4 files import `services/wasmRenderer` or
`utils/iframePostProcessor`, so moving services first means the
remaining Phase-4 movers already point at @quarto/preview-runtime
when their turn comes. Hub-client's full test surface keeps validating
the renderer-side code throughout this swap, so any service regression
surfaces immediately. See "Phase ordering note" in the plan.
Re-decided 2026-05-13: `assetWalker.ts` does NOT move to preview-runtime.
It stays inside `q2-preview/` and moves with it in Phase 4. Rationale:
assetWalker is an AST walker that *uses* VFS, not a VFS service; moving
it to runtime would create a circular preview-runtime → preview-renderer
dependency via `vfsPaths`. The plan's prior signal ("the test moves
alongside since it tests the runtime function") wasn't load-bearing.
Cross-package wiring changes:
- preview-renderer gains @quarto/preview-runtime as a workspace dep
(iframePostProcessor imports `vfsReadFile`).
- preview-runtime gains @quarto/preview-renderer + @quarto/pandoc-types
+ @quarto/quarto-automerge-schema as workspace deps. The
preview-renderer dep is for the wire-format `Diagnostic` /
`RenderResponse` re-exports from `types/diagnostic`.
- preview-runtime/package.json adds sub-path exports
`./userGrammar/*` and `./test-utils/*`, plus a `test:wasm` script
(foundation for moving the wasm-test runner later if useful).
WASM build-time path resolution:
`wasmRenderer.ts`'s lazy `import('../wasm-js-bridge/sass.js')` was
rewritten to `'/src/wasm-js-bridge/sass.js'` — the same Vite-root
convention the Rust WASM module already uses via
`wasm-bindgen raw_module = "/src/wasm-js-bridge/sass.js"`. Every
consumer (hub-client today, the future q2-preview SPA) must host the
bridge files at `src/wasm-js-bridge/` and Vite resolves uniformly.
An ambient `declare module '/src/wasm-js-bridge/*.js'` shim in
`preview-runtime/src/vite-shims.d.ts` keeps tsc happy.
Hub-client import rewrites:
Two passes via `/tmp/rewrite-phase5-imports.py`:
- Pass 1: `<rel>/services/wasmRenderer`, `<rel>/services/automergeSync`,
`<rel>/utils/iframePostProcessor`, and the renamed userGrammar
paths.
- Pass 2: short-form intra-services imports (`./wasmRenderer`,
`./automergeSync`) that the first pass's pattern didn't cover.
Manually fixed three categories the regex couldn't:
- `vi.mock('./wasmRenderer', ...)` and `vi.mock('./automergeSync', ...)`
in test files (rewritten to mock '@quarto/preview-runtime').
- Inline `import('../services/automergeSync').ActorIdentity` in
Editor.tsx.
- `await import('./framework')` in parity.integration.test.tsx
(Phase 3 leftover that the framework rewrite caught here).
Hub-client's three vitest configs gain `@quarto/preview-runtime` aliases
to source (same pattern as `@quarto/preview-renderer`).
`Highlight.wasm.test.ts`'s `repoRoot` computation updated from
`../../..` to `../../../..` to account for the deeper directory.
All green:
- preview-runtime: 60 tests / 6 files, tsc build clean
- preview-renderer: 133 tests / 10 files (unit) + 8 integration,
tsc build clean
- hub-client: 525 unit tests, 173 integration tests, 79 wasm tests,
`build:all` clean
- cargo xtask verify --skip-rust-tests: all 9 steps pass
Snapshot tests: no snapshot files changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(hub-client): changelog entry for bd-hfjj Phase 5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(q2-preview): move q2-preview + iframe + overlays to preview-renderer (bd-hfjj Phase 4)
The biggest single move in the decomposition — 94 files touched. Lifts
the q2-preview format components, the iframe wrappers, and the
overlays out of hub-client and into @quarto/preview-renderer.
To preview-renderer/src/q2-preview/ (entire subtree):
AssetManifestContext, NoteNumberingContext, PreviewContext,
PreviewDocument (+ integration test), assetWalker (+ test),
dispatchers, entry (+ integration test), index.ts (barrel),
quartoClasses, registry (+ test), theoremEnvs, utils,
q2-preview.integration.test, custom-components.integration.test,
+ blocks/, inlines/, custom/ subtrees (each ~15-20 files).
To preview-renderer/src/iframe/:
Q2PreviewIframe (moved out of q2-preview/, + integration test),
MorphIframe, DoubleBufferedIframe.
To preview-renderer/src/overlays/:
PreviewErrorOverlay (+ integration test), PreviewStaticInfoViews.
`PreviewErrorOverlay` was DI-refactored as part of this move. Its
previous internal `usePreference('errorOverlayCollapsed')` call coupled
it to hub-client's localStorage layer; that wasn't viable inside the
shared package. The component now takes optional `collapsed` +
`onToggleCollapsed` props (controlled), with an internal
`useState(true)` fallback for the uncontrolled case. The two hub-client
call sites in `ReactPreview.tsx` and `Preview.tsx` wrap with
`usePreference` and pass the value down — same UX, same persistence,
cleaner boundary. The future q2-preview SPA can pass any state or
omit the props entirely.
Import wiring:
- Self-package `@quarto/preview-renderer/<X>` imports inside the
moved subtree were converted to depth-aware relative paths via
`/tmp/relativize-self-imports.py` (100 rewrites across 55 files).
Idiomatic and matches the Phase 3 framework treatment.
- `Q2PreviewIframe.tsx`'s `./assetWalker` import was rewritten to
`../q2-preview/assetWalker` (it moved out of q2-preview into
iframe/ as one of the few cross-subtree moves).
- Hub-client importers of moved files were rewritten via
`/tmp/rewrite-phase4-imports.py` (9 imports / 6 files in the
first pass, plus 2 more after the regex was broadened to handle
paths with intermediate `components/render/` segments).
- One `vi.mock('./q2-preview/Q2PreviewIframe', ...)` in
`ReactRenderer.integration.test.tsx` was retargeted to
`'@quarto/preview-renderer/iframe/Q2PreviewIframe'`.
Hub-client iframe-entry shim:
`hub-client/q2-preview.html` has a `<script type="module"
src="/src/components/render/q2-preview/entry.tsx">` tag — Vite
resolves that path relative to hub-client's project root. The real
entry now lives in preview-renderer; we keep the original path
stable by adding a one-line stub file at the hub-client location
that just re-imports `@quarto/preview-renderer/q2-preview/entry`.
`parity.integration.test.tsx`'s dynamic
`import('./q2-preview/entry')` also goes through this stub.
This required adding a specific `./q2-preview/entry` sub-path
export to `preview-renderer/package.json` (the q2-preview barrel
alone wouldn't expose `entry.tsx` since q2-preview/index.ts is the
barrel, not entry).
preview-renderer public API surface:
Top-level `src/index.ts` becomes a real barrel exposing
`Q2PreviewIframe`, `MorphIframe` (+ Handle), `DoubleBufferedIframe`
(+ Handle), `PreviewErrorOverlay`, `ErrorView` / `FallbackView` /
`NonQmdPlaceholderView`, plus a `export * from './q2-preview'` for
the rest of the q2-preview surface (Block, Inline, PreviewDocument,
previewRegistry, PreviewContext, AssetManifestContext,
buildAssetManifest, ManifestCacheEntry). Phase 6's q2-preview-spa
placeholder will use this barrel directly.
Test-config plumbing in `preview-renderer/vitest.integration.config.ts`:
- Workspace-package aliases: `@quarto/quarto-sync-client`,
`@quarto/preview-runtime`.
- `wasm-quarto-hub-client` → hub-client's symlink (JS shim resolves
at transform; the WASM binary itself isn't invoked).
- `/src/wasm-js-bridge` → hub-client's bridge dir, so
`wasmRenderer.ts`'s lazy `import('/src/wasm-js-bridge/sass.js')`
resolves at Vite's transform step. Scoped to that sub-tree (not
plain `/src`, which would also intercept test files' own
absolute paths).
All green:
- preview-renderer: 156 unit / 13 files + 129 integration / 9 files
- preview-runtime: 60 tests / 6 files (unchanged)
- hub-client: 525 unit + 58 integration + 79 wasm tests; build:all
clean.
- `cargo xtask verify --skip-rust-tests`: all 9 steps pass.
End-to-end UI smoke: NOT run this session (no browser available in
the worktree). Per CLAUDE.md §End-to-end verification — tests pass,
the real render path was not exercised in a browser. To verify,
`cd hub-client && npm run dev`, open a Quarto project in the
returned URL, and confirm the q2-preview format still renders.
Snapshot tests: no snapshot files changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(hub-client): changelog entry for bd-hfjj Phase 4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(q2-preview): q2-preview-spa skeleton + cargo xtask verify integration (bd-hfjj Phases 6 + 7)
Closes the bd-hfjj sub-epic. Phases 6 and 7 are mechanical follow-ups
to the big moves in Phases 2–5.
Phase 6 — q2-preview-spa skeleton:
Creates a new workspace package `q2-preview-spa/` (sibling to
hub-client/, trace-viewer/) that's the future host of the
`quarto preview` CLI command from bd-kw93. Today it's a 4-file
placeholder:
package.json — name, react/preview-renderer/preview-runtime deps
vite.config.ts — react + wasm plugins, source-condition aliases
tsconfig.{,app,node}.json — trace-viewer-shaped TS project refs
index.html — minimal shell
src/main.tsx — renders <PreviewErrorOverlay> with a placeholder
message, proving the cross-package boundary works.
Bundle audit (after `npm run build`):
- Total: 196K, 30 modules.
- grep'd for editor-only symbols (Monaco, GoogleOAuthProvider,
FileSidebar, ProjectSelector, automergeSync, createSyncClient,
monaco-editor): all zero. The §Crate/SPA invariant from the
epic is enforced by construction — editor code cannot be
transitively pulled in because the SPA only imports from
shared packages.
Caveat: the placeholder uses the *sub-path* overlay import
(`@quarto/preview-renderer/overlays/PreviewErrorOverlay`) rather
than the top-level barrel. Importing through the barrel pulls in
the q2-preview tree, which pulls in `@quarto/preview-runtime` and
its `/src/wasm-js-bridge/` consumer-provided files. The SPA
placeholder doesn't yet host those bridge files at its root;
Phase A of bd-kw93 will revisit when the SPA actually drives a
render. The current shape proves the cross-package boundary
works without paying that cost.
Phase 7 — cargo xtask verify integration + cleanup:
- TOTAL_STEPS bumped 9 → 11. Steps 10 (shared preview-* package
tests: preview-renderer unit + integration + preview-runtime
unit) and 11 (q2-preview-spa build) added. Skip flags added:
`--skip-shared-package-tests`, `--skip-q2-preview-spa-build`.
- `cargo xtask verify --skip-rust-tests` confirmed green across
all 11 steps.
- `hub-client/src/test-utils/index.ts` cleaned up: the mockSyncClient
and mockWasm re-exports were dead (those moved to preview-runtime
in Phase 5). The barrel now only carries `@testing-library/react`
helpers and `visibility.ts` exports.
- Audit confirmed no empty directories left behind in hub-client/src/;
the one-file q2-preview/entry.tsx stub there is intentional
(kept so q2-preview.html and parity.integration.test.tsx keep
their existing paths).
Also fixes two qmd-render-blocking characters in earlier Phase 4 +
Phase 2 changelog entries (`PreviewErrorOverlay`'s apostrophe and
the `(~50 files)` tilde — both confused Quarto's parser into
"Unclosed Single Quote" / "Unclosed Subscript"; rephrased to plain
text).
Plan flipped from "Phases 0–5 complete; Phase 6 next" to "COMPLETE".
The bd-hfjj sub-epic is closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(hub-client): changelog entry for bd-hfjj Phases 6+7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(q2-preview): Phase A plan (bd-kw93 sub-epic kickoff)
Drafts the engine-less CLI skeleton for `q2 preview` after bd-hfjj
landed the decomposition. Seven sub-tasks (A.0 through A.6) plus an
e2e smoke (A.7):
A.0 Lift hub-client/src/wasm-js-bridge/ to a new
@quarto/wasm-js-bridge workspace package; alias `/src/wasm-
js-bridge` in hub-client + q2-preview-spa + preview-renderer
tests. Resolves Q-A1 the right way (no duplication, no
cross-platform symlink fragility).
A.1 Stub `quarto preview` clap subcommand.
A.2 New crates/quarto-preview/ shell crate with the
quarto-trace-server-shaped include_dir! + build.rs
placeholder.
A.3 Fill in q2-preview-spa/src/main.tsx so the SPA actually
drives a render. <PreviewApp> stays local for now.
A.4 `cargo xtask build-q2-preview-spa` + wire into `build-all`.
A.5 End-to-end boot test (tempdir lifecycle, samod handshake,
URL-fragment carrier).
A.6 Always-visible "force re-render" button (epic resolution #4).
A.7 Playwright smoke (real-browser flow incl. DOM-stability
invariant).
Q-A3 source-code audit corrected an inherited claim: the epic and my
initial draft both said the SPA reads `indexDocId`/`wsUrl` from a
server-injected `<meta>` tag "the same trick share links use." Both
halves wrong. hub-client's share links use URL *fragments*
(`hub-client/src/App.tsx:272`, `utils/routing.ts`); there's no
meta-injection in the codebase. trace-server serves `index.html`
unmodified (`crates/quarto-trace-server/src/lib.rs:185-187`). Phase
A now uses the same URL-fragment pattern: the CLI opens
`http://<host>:<port>/#/preview/<indexDocId>`, the SPA reads
`window.location.hash`, the WebSocket URL derives from
`window.location`. No new server-side patterns introduced.
Q-A5 captures a future direction: persistent samod storage inside
`.quarto/preview/samod/` to amortize the "blank screen at boot" cost
on large projects. Phase A measures the cost; future phases act.
TDD: every sub-task is structured tests-first.
Status: approved 2026-05-13 (all open questions resolved); ready to
file beads sub-issues and begin A.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: file 7 Phase A sub-issues under bd-kw93
Files bd-0xmt (A.0 wasm-js-bridge), bd-yxqt (A.1+A.2 CLI + crate),
bd-o5wd (A.3 SPA main.tsx), bd-501n (A.4 xtask build), bd-mflk (A.5
boot test), bd-b5hf (A.6 force-refresh), bd-vpsy (A.7 Playwright)
as parent-child children of bd-kw93. Plan:
claude-notes/plans/2026-05-13-q2-preview-phase-a.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(wasm-js-bridge): lift to @quarto/wasm-js-bridge workspace package (bd-0xmt, Phase A.0)
First step of the q2-preview Phase A (bd-kw93). Moves the 4 JS bridge
modules + their .d.ts/.test companions out of hub-client/src/wasm-js-
bridge/ into a new @quarto/wasm-js-bridge workspace package. Each
consumer (hub-client today + q2-preview-spa later) aliases the
Vite-root path `/src/wasm-js-bridge/` to the package's src/ — the
Rust WASM module's `wasm-bindgen raw_module = "/src/wasm-js-bridge/
sass.js"` annotation is unchanged because resolution happens at
consumer build time. One copy in the workspace, no per-consumer
duplication, no symlinks.
Why this is A.0: Phase A.3 (filling in q2-preview-spa/src/main.tsx)
will boot the WASM module from the SPA's Vite root. Without this
move, the SPA would either need its own copy of the bridge files or
a symlink to hub-client's. Doing the lift now lets A.3 inherit a
clean answer.
Package details:
ts-packages/wasm-js-bridge/
package.json — `@quarto/wasm-js-bridge`, private, type module.
Deps `ejs`, `sass` (template.js + sass.js).
src/sass.js, sass.d.ts, cache.js, cache.d.ts, cache.test.ts,
fetch.js, template.js — git-mv'd from hub-client.
src/sass.test.ts (new) — pins the four public function names
(setVfsCallbacks, jsSassAvailable, jsSassCompilerName,
jsCompileSass). The Rust WASM module + wasmRenderer.ts both
reference these by name; renaming any silently breaks the
bridge at runtime. The TypeScript surface is `any`-typed
because sass.js is JS, so this guard test is the only
compile-or-test signal we have for the contract.
Consumer wiring:
- hub-client/vite.config.ts: alias `/src/wasm-js-bridge` →
`../ts-packages/wasm-js-bridge/src`. The leading `/` was
previously resolving via Vite project root to a directory that
no longer exists.
- hub-client/vitest.{,integration}.config.ts: inherit the alias
via mergeConfig from vite.config.ts (resolve.alias deep-merges
cleanly).
- hub-client/vitest.wasm.config.ts: still has `/src` →
`hub-client/src` for the WASM tests that need general /src/
paths; the more-specific `/src/wasm-js-bridge` alias from
vite.config.ts wins via longest-prefix match. Comment added.
- q2-preview-spa/vite.config.ts: same `/src/wasm-js-bridge` alias.
Not strictly required until A.3 fills in main.tsx with real WASM
init, but landing it now is cheap and means A.3 doesn't need
config changes.
- ts-packages/preview-renderer/vitest.integration.config.ts:
retarget the existing alias from `hub-client/src/wasm-js-bridge`
(now gone) to `../wasm-js-bridge/src` (within ts-packages).
- Three hub-client wasm tests (`smokeAll`, `themeCss`,
`themeFingerprint`) imported the bridge via the relative path
`../wasm-js-bridge/sass.js`. Rewritten to the Vite-root form
`/src/wasm-js-bridge/sass.js`, matching how wasmRenderer.ts
handles it. Now alias-driven, location-independent.
Hub-client cleanup:
- Removed `ejs` (^3.1.10), `sass` (^1.77.0), `@types/ejs`
(^3.1.5) from hub-client/package.json. The bridge files were
the only consumers; with the move complete, these npm deps
belong to @quarto/wasm-js-bridge instead.
Verification (all 11 cargo xtask verify steps pass):
- hub-client: 525 unit + 173 integration + 79 wasm tests
(test:wasm exercises the real sass compile through the bridge —
the strongest signal that the alias works end-to-end).
- preview-renderer: 156 unit + 129 integration tests.
- preview-runtime: 60 tests.
- q2-preview-spa: builds (placeholder still doesn't touch WASM).
- hub-client/dist/assets/sass-*.js confirmed to contain the bridge
code (grep'd for `dart-sass`, `setVfsCallbacks`, jsSassAvailable
function signature).
Snapshot tests: no snapshot files changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(hub-client): changelog entry for bd-0xmt (q2-preview Phase A.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-0xmt (q2-preview Phase A.0)
A.0 landed on branch beads/bd-0xmt-wasm-js-bridge-package
(commits ea1bb889 + 043f65e1). @quarto/wasm-js-bridge workspace
package now hosts the bridge files; hub-client + q2-preview-spa
alias /src/wasm-js-bridge to the package's src.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(xtask): add `switch-task` for in-place sub-task transitions
Two-pattern worktree workflow: `create-worktree` for parallel /
investigation work (fresh isolation; pays the npm install + cargo
cold-cache cost on purpose); `switch-task` for sequential
implementation work within an epic (reuses the current worktree's
warm caches by branch-switching in place).
`cargo xtask switch-task <bd-id> [--from <branch>]`:
1. If `--from <branch>` given: `git switch <branch>` + `git pull
--ff-only` so the topic branch starts from the latest tip
(siblings' merges land here first).
2. `git switch -c beads/<id>-<slug>` off the resulting HEAD.
3. `br update <id> --status in_progress` (skippable via
`--no-claim`).
4. Rewrites the `<!-- BEGIN/END WORKTREE CONTEXT -->` block in
`CLAUDE.local.md` so the next Claude Code session sees the new
sub-task's metadata (same template as create-worktree).
Reuses create_worktree's BeadsMetadata + derive_slug + build_section
+ update_claude_local_md helpers — the shape is the same; only the
git-side changes (no worktree add, just an in-place branch switch).
Companion changes to `.claude/rules/worktrees.md`:
- Documents the two patterns side-by-side at the top of the file so
Claude (or any contributor) picks the right command for the shape
of the work.
- New "Integration-line convention" section formalising that ready
sub-task work merges into a long-lived `feature/<name>` branch
(--no-ff so each sub-task is a single merge commit); `switch-task
--from <branch>` expects to find that integration line already
current.
- Adds the in-place sub-task pattern to the branch-naming section
(same `beads/<id>-<slug>` shape, no `.worktrees/` dir).
No change to existing commands; `cargo xtask verify` still 11/11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(xtask switch-task): branch off ref directly, don't switch first
Previous implementation did `git switch <from>` + `git pull --ff-only`
+ `git switch -c <new>`. Fails when `<from>` is already checked out
in another worktree — which is exactly the common case (main repo on
`feature/<epic>`, in-place sub-task work in a sibling worktree).
New shape: `git fetch origin <from>` (best-effort), then
`git switch -c <new> <from>` — the trailing start-point creates the
new branch at `<from>`'s tip without checking out `<from>` itself.
Worktree-friendly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(xtask switch-task): write CLAUDE.local.md to the current worktree, not the workspace root
The first end-to-end run of `switch-task` from inside a worktree
clobbered the *main repo's* `CLAUDE.local.md` instead of the
worktree's. Cause: switch_task used `create_worktree::repo_root()`,
which walks up to find `Cargo.toml` with `[workspace]`. For a
worktree of a workspace, that `[workspace]` declaration is shared
across all worktrees, so the walk lands on the main repo regardless
of which worktree the command is run from.
Add a `current_worktree_root()` helper that calls
`git rev-parse --show-toplevel` instead — that returns the
*invoking worktree's* top, which is the right anchor for the
per-worktree CLAUDE.local.md file. Use it in switch_task.
The bug only existed in `switch_task`; `create-worktree` is
unaffected because it explicitly constructs `repo_root.join(".worktrees")
.join(<leaf>)` and uses that for CLAUDE.local.md path, never the
caller's worktree root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: bd-yxqt → in_progress (Phase A.1+A.2 work begins)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(quarto-preview): CLI subcommand + new crate with embedded SPA (bd-yxqt, Phase A.1+A.2)
Two halves of the q2-preview phase-A skeleton:
A.1 — CLI surface
- Replaces the existing `Preview` clap stub (Q1-shaped placeholder
that returned NotImplemented) with the Q2 shape from
`claude-notes/plans/2026-05-13-q2-preview-phase-a.md` §A.1.
Drops the Q1-era flags (--render, --no-serve, --no-navigate,
--no-watch-inputs, --timeout, --log*, --quiet, --profile) since
Q2 always serves, always watches, and logs at the binary level.
Keeps [path] / --port / --host / --no-browser, adds --data-dir
(samod storage override), --preview-dir (SPA-from-disk override,
same shape as QUARTO_TRACE_VIEWER_DIR), --no-project (standalone
mode, Q5 resolution).
- Tests-first: `crates/quarto/tests/preview_cli.rs` pins the flag
set via `q2 preview --help` (3 tests). Started red (the
inherited Q1 flags failed the Phase-A flag-list assertion);
landed green after the replacement.
- `commands/preview.rs` grows a `PreviewArgs` struct carrying the
Phase-A flags. `execute()` is still a NotImplemented stub; the
fields are `#[allow(dead_code)]`-marked because A.5 (bd-mflk)
will consume them.
A.2 — quarto-preview crate
- New `crates/quarto-preview/`. Cargo.toml depends on quarto-hub,
include_dir, axum, tokio (the deps needed for the future
hub-layered router). Currently lib.rs is just the SPA shell;
A.5 layers the hub server on top.
- `build.rs` mirrors `quarto-trace-server/build.rs` exactly: looks
for `q2-preview-spa/dist/index.html`; if present, embeds that
directory via `include_dir!`; otherwise emits an OUT_DIR
placeholder with the same `<div id="root">` mount point (so the
smoke test works either way) plus a build warning telling the
user to run `cargo xtask build-q2-preview-spa` (added in A.4).
- `src/lib.rs` exposes `PreviewConfig` + `run(config)` + `router(
&config)`. `router()` is `pub` so the smoke test can exercise
the SPA-fallback chain without spinning the listener through
run()'s indefinite serve loop. `run()` itself uses
`axum::serve(...).with_graceful_shutdown(ctrl_c)`.
- Smoke tests (`tests/smoke.rs`, 2 tests):
1. `spa_root_serves_html_with_react_mount` — GET / returns
200 with the React mount point.
2. `unknown_path_falls_back_to_index_html` — GET on an
arbitrary path falls back to index.html (client-side
routing), matching what `q2-debug.html` /
`q2-preview.html` rely on in hub-client.
- Both run green; the bound port comes back via
`listener.local_addr()` so the tests are port-clash-free.
Verification: `cargo xtask verify --skip-hub-build --skip-hub-tests
--skip-trace-viewer-build --skip-trace-viewer-tests
--skip-shared-package-tests --skip-q2-preview-spa-build
--skip-treesitter-tests` green — Rust-only because the change
touches no Rust code that hub-client transitively consumes (no
quarto-core, no quarto-pandoc-types, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-yxqt (Phase A.1+A.2 complete)
q2 preview CLI surface + crates/quarto-preview shell crate landed
via merge commit. Ready: bd-o5wd (A.3 — fill in q2-preview-spa
main.tsx), bd-501n (A.4 — xtask build helper).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(q2-preview-spa): real boot path via PreviewApp + integration test (bd-o5wd, Phase A.3)
Replaces bd-hfjj Phase 6's placeholder main.tsx (which just rendered
`<PreviewErrorOverlay>` with static text to prove the package
boundary) with the actual SPA bootstrap: WASM init, samod connect,
first-.qmd selection, and `<Q2PreviewIframe>` driven off automerge.
New `src/PreviewApp.tsx`:
- Three boot states (loading / ready / error) tracked in a single
state object so the render branches stay declarative.
- Parses `window.location.hash` for `#/preview/<indexDocId>` per
Q-A3 of the Phase A plan. The CLI synthesises that fragment when
opening the browser; the SPA reads it client-side — no server
HTML rewriting needed.
- Derives the websocket URL from `window.location` (same host:port
serves SPA + samod ws), so there's a single source of truth for
where to connect.
- On mount: `initWasm()` → `setSyncHandlers()` → `connect(wsUrl,
indexDocId)`. Files come back from `connect()`'s resolved promise
AND via the `onFilesChange` handler (the runtime fires both).
- Active page = first `.qmd` in the project. URL-driven file
selection is a Phase-D polish item.
- `onFileContent` bumps a `contentTick` counter; a second
`useEffect` keyed on `[activeFile, contentTick]` re-runs
`renderPageInProject()` and feeds the resulting `ast_json` into
Q2PreviewIframe. That's the "edit re-renders without DOM
rebuild" promise of the q2-preview format.
- `setAst` on Q2PreviewIframe is a deliberate no-op for Phase A.
The iframe expects it because Phase 2 of q2-preview anticipated
a WYSIWYG round-trip into the .qmd source. Wiring that needs an
editor surface the SPA doesn't yet have.
New `src/PreviewApp.integration.test.tsx` (3 tests, all green):
1. Boot path: mocked runtime returns one .qmd; assert
Q2PreviewIframe receives `astJson` + `currentFilePath` matching
the mock.
2. Loading state: `connect()` held in a never-resolving promise;
assert "Initializing…" UI is visible *before* the iframe.
3. Connection error path: `connect()` throws; assert the message
surfaces through `<PreviewErrorOverlay>` and no iframe renders.
Test infra:
- `vitest.config.ts` + `vitest.integration.config.ts` mirroring
preview-renderer's pattern: node env for unit, jsdom for
integration. Workspace-package aliases (preview-renderer,
preview-runtime, quarto-automerge-schema, quarto-sync-client)
listed explicitly because vitest doesn't honour the `source`
exports condition on fresh clones.
- `src/test-utils/setup.ts`: jest-dom + fake-indexeddb polyfill +
minimal ResizeObserver shim. Mirrors hub-client's setup
(deliberate small duplication — see bd-hfjj's catalogue note).
Bookkeeping:
- q2-preview-spa now has its own `.gitignore` (dist/, node_modules,
*.tsbuildinfo) — matches `trace-viewer/` and `hub-client/`. The
prior placeholder dist/ that landed via bd-hfjj Phase 6 is
untracked accordingly.
- dev deps grow with jsdom + @testing-library/{react,jest-dom} +
vitest. `@quarto/quarto-automerge-schema` added as devDep for
the FileEntry import in the test.
Bundle audit (production build):
- Editor symbols (Monaco, GoogleOAuthProvider, FileSidebar,
ProjectSelector, monaco-editor): zero. The §invariant from the
epic still holds — SPA imports only from shared packages, editor
code can't transitively leak in.
- SPA-side code (Q2PreviewIframe, PreviewErrorOverlay,
renderPageInProject, "Initializing") present. The full WASM +
sass + automerge chain ships now (~40MB of dist incl. WASM
binary) — that's the cost of being a real preview host rather
than a placeholder.
`cargo xtask verify` 11/11 green (including the SPA build).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-o5wd (Phase A.3 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(xtask): `build-q2-preview-spa` + `build-all` chain (bd-501n, Phase A.4)
Mirror of `build-trace-viewer` for the q2-preview SPA. Two pieces:
- `cargo xtask build-q2-preview-spa` (new): runs `npm run build` in
`q2-preview-spa/`. Useful when only the SPA changed — the
`quarto-preview` crate's `include_dir!` picks up the fresh dist on
the next Rust compile.
- `cargo xtask build-all` extended: a new step between the
trace-viewer build and the Rust workspace build invokes
`npm run build` in `q2-preview-spa/` so a fresh-clone `build-all`
produces a `quarto` binary with the real SPA bundle embedded.
Skip with `--skip-q2-preview-spa-build`. Same `<name>_exists()`
guard as trace-viewer so the step is a no-op on branches that
predate bd-hfjj Phase 6's SPA scaffold.
Implementation mirrors `build_trace_viewer.rs` line-for-line —
project-root lookup, npm spawn, success check, banner. The
`BuildAllConfig` gains a `skip_q2_preview_spa_build` field plumbed
through the Command enum + match arm in main.rs.
Verify: `cargo xtask verify --skip-rust-tests` green (11/11).
`cargo xtask build-q2-preview-spa` runs cleanly. `cargo xtask
build-all --help` advertises the new flag. The end-to-end "fresh
clone" smoke (a full `cargo xtask build-all` from a clean checkout)
is exercised by CI rather than this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-501n (Phase A.4 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(quarto-preview): q2 preview boots, renders, and styles end-to-end (bd-mflk, Phase A.5)
Wires quarto_preview::run() to wrap the hub: SPA owns `/`, hub keeps
`/ws` + `/health` + `/api/*` + `/auth/*`. The CLI shim sets up an
ephemeral TempDir data_dir, probes a free port, prints the URL, and
hands off to quarto-hub's run_server_with() with a SPA-fallback
closure. quarto-hub gains three small composability seams:
`HubConfig::register_root_ws`, `StorageManager::new_with_data_dir`,
and `run_server_with<F>` accepting an optional router extender.
Driving the binary against a real fixture surfaced five gaps the
plan didn't anticipate; the plan file documents each with rationale
under "Status (bd-mflk, 2026-05-13)":
- PreviewApp fetches `index_document_id` from `/health` and
normalizes to the `automerge:<id>` form that
`@quarto/preview-runtime`'s `connect()` expects (the raw samod
form makes `repo.find()` hang). Replaces the URL-fragment
carrier originally chosen in Q-A3.
- New `#[wasm_bindgen] render_page_for_preview` entry point maps
the default-when-absent `html` format to `q2-preview` so a
bare-markdown file flows through the AST pipeline and returns
`ast_json`. Hub-client's `render_page_in_project` is unchanged.
- `quarto-sync-client.connect()` grows an optional `peerTimeoutMs`
(default 1ms preserved); the SPA passes 5000ms to avoid an
"Document … is unavailable" cold-start race when no IndexedDB
cache exists.
- Multi-entry Vite build (mirroring hub-client): adds
`q2-preview.html` + entry stub for the sandboxed renderer
iframe `Q2PreviewIframe` loads.
- `theme_fingerprint` threaded through PreviewApp →
`Q2PreviewIframe`, three-way (string / null / undefined) so the
iframe mints a blob URL for `/.quarto/project-artifacts/styles.css`
and posts `UPDATE_THEME`. Fixes the unstyled-DOM symptom.
Boot reliability: `q2-preview-spa/index.html` forces
`html/body/#root { height: 100% }` so the embedded iframe doesn't
collapse to its intrinsic content height.
Tests:
- New `crates/quarto-preview/tests/boot.rs` — in-process spawn of
`quarto_preview::run()`, asserts `/`, `/health`, SPA-fallback.
- Updated `crates/quarto-preview/tests/smoke.rs` — exercises
`extend_with_spa()` in isolation (3 cases).
- `q2-preview-spa/src/PreviewApp.integration.test.tsx` — 7 cases
covering fetch-then-connect chain, /health-failure surfacing,
docId normalization, and theme-fingerprint passthrough.
End-to-end verified per CLAUDE.md §End-to-end verification:
`q2 preview <tempdir>` → browser via Chrome DevTools shows
`<h1>Hello, q2 preview!</h1>` and both paragraphs rendered through
the AST iframe with Bootstrap typography applied; editing the
source `.qmd` on disk re-renders within ~2s via the file watcher
→ samod → SPA path.
`cargo xtask verify` green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-mflk (Phase A.5 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(q2-preview-spa): always-visible force-refresh button (bd-b5hf, Phase A.6)
Adds a small circular `↻` control overlaid on the top-right of the
preview pane (`aria-label="Refresh preview"`). Click → PreviewApp
bumps its `contentTick` so the render `useEffect` re-fires —
reusing the one channel that the sync handler's `onFileContent`
already uses, so there's a single path through the render pipeline
regardless of who asks for the re-render.
The button satisfies the epic's resolution #4 (force-refresh
invariant): the user always has an escape hatch when the dep-graph
misses a cross-doc relationship. Phase A has no dep-graph at all,
so it's largely redundant today, but the affordance is part of the
preview UX contract going forward; Phase C will extend the click
handler to trigger server-side re-execution where applicable.
Tests:
- New integration test in `PreviewApp.integration.test.tsx`
pins the contract at the runtime seam: click → at least one
additional `renderPageForPreview` call. Match by accessible
name rather than test-id so the UX label is part of the test.
Manual smoke (Chrome DevTools): button rendered at (1876, 12) on
a 1920x1080 viewport; click runs without console error; iframe
content stays consistent; refresh affordance survives clicks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-b5hf (Phase A.6 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(q2-preview-spa): Playwright E2E smoke for q2 preview (bd-vpsy, Phase A.7)
Adds a Playwright suite that drives the real `q2 preview` binary
end-to-end against a temp fixture and pins four behaviours:
1. Initial render contains the fixture heading + paragraph.
2. Editing the .qmd on disk re-renders the iframe within ~2s
(file watcher → samod → SPA round-trip).
3. The force-refresh button (bd-b5hf) is reachable + clickable.
4. DOM-stability invariant: the `<h1>` keeps the SAME DOM node
instance across an edit that doesn't touch the heading
(`===` reference equality via shared `window.__preEditH1`).
Plan-deviation note: §A.7 mentioned a `data-stable-id` attribute,
which doesn't actually exist in the rendered DOM. The intent there
was the React-reconciler-level invariant — when an edit doesn't
add/remove an element, React reuses the same DOM node — and the
test pins that directly on the heading.
Each test spins up its own `target/debug/q2` instance against a
fresh tempdir project (helpers/previewServer.ts spawns + parses
the printed launch URL). globalSetup just asserts the binary is
built so the failure mode is a copy-pasteable command rather than
a 90 s cargo-compile.
xtask wiring:
- New Step 12 in `cargo xtask verify --e2e` runs `npm run test:e2e`
from q2-preview-spa. Gated on `--e2e` so the default `verify`
stays fast and browser-free; skipped if q2-preview-spa/ doesn't
exist.
- `npm run test:e2e` (in q2-preview-spa) runs Playwright directly.
Verified locally: 4/4 pass under both `npm run test:e2e` and
`cargo xtask verify --skip-hub-build --skip-hub-tests --e2e`
(hub-client's own e2e suite is broken on this branch due to a
pre-existing `@quarto/quarto-sync-client/dist` resolution issue,
not introduced here).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-vpsy (Phase A.7 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* beads: file bd-u6ef (hub-client e2e fails: quarto-sync-client/dist resolution)
Discovered while wiring q2-preview-spa's Playwright suite (bd-vpsy).
Pre-existing on main; not introduced by Phase A work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* plan(q2-preview): Phase B file-watcher + remap broadening (bd-z529, bd-pf63, bd-91da)
claude-notes/plans/2026-05-13-q2-preview-phase-b.md lays out the
work to close the "everything that should trigger a re-render does"
loop:
- B.1 (bd-z529): broaden the FileWatcher allow-list to include
`_quarto.yml`, `_metadata.yml`, image extensions, and `.tsx`
(defer `_extensions/**` per plan §Q-B1). Plumb a WatchFilter
enum through HubConfig so hub keeps `.qmd`-only and
quarto-preview opts into broad.
- B.3 (bd-pf63): verify cross-doc + include-shortcode edits
propagate via the existing onFileContent → contentTick path.
Likely empirical / Playwright-only work.
- B.4 (bd-91da): acceptance Playwright spec pinning all three
Phase B scenarios.
bd-pf63 and bd-91da are `blocks`-gated on bd-z529 (the broader
watcher is the precondition for the cross-doc + acceptance work).
Phase A's `render_page_for_preview` (A.5.4c) already resolved
what the epic plan called Phase B item B.2 (format-html →
q2-preview remap), so it isn't a sub-task here. The plan
recommends dropping the proposed B.5 "opt-out of remap" knob
unless a real user complains.
Plan is plan-only — implementation in a later session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(quarto-hub): WatchFilter enum + PreviewBroad for q2 preview (bd-z529, Phase B.1)
Replaces the bare `is_qmd_file` check in `FileWatcher` with a `WatchFilter`
enum so `quarto hub` keeps its `.qmd`-only semantics while `q2 preview` can
opt into a broader allow-list.
`WatchFilter::PreviewBroad` accepts, in addition to `.qmd`:
- `_quarto.{yml,yaml}` and `_metadata.{yml,yaml}` (basename match)
- `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp` (extension match)
- `.tsx` (custom React components)
Both `.yml` and `.yaml` are matched because Quarto canonicalizes to `.yml`
but nothing prevents `.yaml`; silently dropping the alt would be a
surprising failure mode. `_extensions/**` is intentionally deferred per
plan Q-B1 — that channel needs gitignore-style ignore-patterns.
Wiring: new `HubConfig::watch_filter` field (default `QmdOnly`).
`quarto-preview::build_hub_config` overrides to `PreviewBroad`. Both
hub-CLI entry points spell out `Default::default()` so the narrow
semantics are explicit. `server::run_server_with` reads the filter
before the `HubConfig` move and threads it through `WatchConfig`.
Tests (`crates/quarto-hub/src/watch.rs`): 4 new predicate tests covering
both modes, accept cases, reject cases (including `.tsx.bak`,
`_quarto.yml.bak`, non-canonical YAML), and 2 new integration tests
that spawn a real `FileWatcher` and verify a `_quarto.yml` edit
surfaces only under `PreviewBroad`.
End-to-end verified through the binary: `q2 preview` boot log shows
`Started filesystem watcher … filter=PreviewBroad`, and a live edit
to `_quarto.yml` produced `DEBUG File change detected
…/_quarto.yml` followed by `Sync complete: filesystem → automerge
new_len=48`. Driving samod-side propagation to a browser re-render
remains Phase B.3 / B.4 work.
Plan: claude-notes/plans/2026-05-13-q2-preview-phase-b.md §B.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-z529 (Phase B.1 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(q2-preview-spa): cross-doc include-shortcode propagation (bd-pf63, Phase B.3)
Adds the empirical proof that editing an included `.qmd` re-renders
the includer in the live preview. Per Phase B plan §B.3 the verification
was expected to need zero production-code changes — confirmed: the
Phase A wiring (watcher → samod sync → `onFileContent` → VFS update →
`contentTick` bump → render useEffect) plus the B.1 broadened watcher
already covers the case.
- Generalise `q2-preview-spa/e2e/helpers/previewServer.ts` from a
single `fixtureQmd` to a multi-file `fixtureFiles: Array<{path,
content}>`, with nested-path mkdir. Migrates the one existing
callsite (`basic-preview.spec.ts`). B.4 will need the same shape.
- New `q2-preview-spa/e2e/include-shortcode.spec.ts`. Fixture:
`index.qmd` containing `{{< include x.qmd >}}` + `x.qmd` carrying a
unique sentinel. Two cases:
1. Initial render expands x.qmd into index.qmd (h1 from index, h2
and sentinel from x; would fail loudly if the SPA ever picked
x.qmd as the active page).
2. Editing x.qmd on disk surfaces the new sentinel in the includer
within 5 s (plan budget 2 s; matches `basic-preview.spec.ts`'s
CI ceiling).
All 6 q2-preview-spa Playwright tests pass (4 pre-existing + 2 new).
- Plan §B.3 rewritten to record the agreed approach (multi-file
fixture, strict include-shortcode scope, stop-and-report on red)
and tick the checklist. Empirical result: zero production-code
changes, contingency not triggered.
- `package-lock.json` drift correction — `npm install` synced entries
for `@playwright/test` and `@types/node` that were already declared
in `q2-preview-spa/package.json` but missing from the lockfile.
Unrelated to B.3 but harmless to include.
Verification:
- Direct: `npx playwright test` in q2-preview-spa → 6/6 pass.
- Indirect: `cargo xtask verify --skip-hub-build` → 12/12 steps green
(step 12 skipped without --e2e flag).
- `cargo xtask verify --e2e` is blocked at step 7 by the pre-existing
hub-client e2e failure tracked in bd-u6ef, which explicitly notes
the q2-preview-spa suite is unaffected. Not my fault to fix here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-pf63 (Phase B.3 complete)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(q2-preview-spa): config-file edit propagation (bd-mrx1, Phase B.4)
Adds the empirical proof that editing project-level and section-level
metadata files re-renders the active preview page with the new
metadata applied. Per Phase B plan §B.4 the verification was
expected to need zero production-code changes — confirmed: the
Phase A wiring (watcher → samod → onFileContent → contentTick →
render useEffect → MetadataMergeStage re-merge) plus B.1's
broadened watcher already cover both layers.
- New `q2-preview-spa/e2e/config-edits.spec.ts`. Fixture:
_quarto.yml (project title), posts/_metadata.yml (section
subtitle), posts/post1.qmd (no frontmatter, inherits both).
Three cases:
1. Initial render — both inherited values appear in the
rendered title-block. Load-bearing check that both
metadata layers are read on first render.
2. Editing _quarto.yml title — new title in DOM within 5 s;
section subtitle unchanged.
3. Editing posts/_metadata.yml subtitle — new subtitle in DOM
within 5 s; project title unchanged.
Each edit test cross-checks the OTHER layer to prevent an
over-eager edit trivially passing.
- Plan §B.4 rewritten with the agreed approach (single dual-
purpose fixture, criteria 1+2 only). Criterion 3 ("unrelated
sibling re-renders only on dep edge") is deferred to bd-0mji,
which also covers the Phase D dep-graph filter regression tests
once that lands.
Phase B closeout: all sub-tasks (B.1, B.3, B.4) land without
touching production rendering or sync code, matching the plan's
"verification, not implementation" framing.
Verification:
- Direct: `npx playwright test` in q2-preview-spa → 9/9 pass
(4 basic-preview + 2 include-shortcode + 3 config-edits).
- Indirect: `cargo xtask verify --skip-hub-build` → 12/12 green
(step 12 skipped without --e2e).
- `cargo xtask verify --e2e` is still blocked at step 7 by the
pre-existing hub-client e2e failure tracked in bd-u6ef.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-mrx1 (Phase B.4 complete), file bd-0mji follow-up
Also closes accidental duplicate bd-hytl (created by a stray
double-run during B.4 setup; no work done against it).
Phase B is now closed for verification work — all of B.1 / B.3 / B.4
have landed without touching production rendering or sync code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* plan(q2-preview): Phase C engine execution + replay (draft)
Drafts claude-notes/plans/2026-05-13-q2-preview-phase-c.md covering
the seven sub-tasks (C.1–C.7) the epic plan names for Phase C. Per
user direction, this commit is plan-only — sub-task bd-issues will
be filed in a follow-up session after plan review.
Highlights and decisions captured in the plan:
- Six Phase-C-specific open questions (Q-C1..Q-C6), each with a
recommended resolution: sidecar IndexDocument schema (additive,
no migration of existing `files` values); reuse the full
EngineExecutionStage pipeline for capture recording; whole-QMD
byte-equality for staleness (matches ReplayEngine's miss policy);
explicit `state` enum on the sidecar; reject concurrent
re-execute requests with 409.
- Explicit dependency order — C.3 (transport) blocks C.1 + C.4;
C.5 needs C.1 + C.2; C.6 + C.7 are parallelisable polish.
- Per-sub-task TDD test plans, seams (file:line for the existing
code), and acceptance criteria.
- Pre-flight investigation receipts so future sessions don't
re-derive the seams: EngineExecutionStage's existing capture
emission at line 256, EngineRegistry::with_replay, WASM
render_page_for_preview signature, IndexDocument Rust + TS
shapes, the file-watcher → sync_file hook, the
extend_with_spa router-extension point.
Phase-level decisions Q1–Q7 from the epic are reconfirmed as
settled and folded into the plan rather than re-litigated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads + plan: file Phase C sub-tasks (bd-kw93.1–bd-kw93.7)
Seven parent-child sub-tasks under bd-kw93 with blocks edges per the
dependency graph in the Phase C plan:
C.3 (bd-kw93.1) — schema + WASM signature foundational
C.1 (bd-kw93.2) — server eager capture blocked by C.3
C.4 (bd-kw93.3) — browser-side replay blocked by C.3
C.2 (bd-kw93.4) — staleness detection blocked by C.1
C.5 (bd-kw93.5) — stale-capture UX overlay blocked by C.4 + C.2
C.6 (bd-kw93.6) — preview.engine config blocked by C.5
C.7 (bd-kw93.7) — per-doc capture cache blocked by C.5
Plan updated to drop the bd-XXXX placeholders and reflect filed status.
bd-kw93.1 is the only Phase C item currently ready.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(automerge-schema): IndexDocument capture sidecar (V2) — bd-kw93.1
Phase C.3 schema half. Adds the typed `captures` sidecar that Phase C's
server eager-record (C.1) and browser-side replay (C.4) consume.
- New `CaptureRef` interface: `captureDocId` + optional `staleness`,
`state` (idle | running | error), `lastError`. Reserved as a string
enum (per plan §Q-C4) so future error/idle states grow without
another schema bump.
- `IndexDocument.captures?: Record<string, CaptureRef>` keyed by the
same path used in `files`. Sidecar approach (per plan §Q-C1) is
additive — no migration of existing `files` values.
- Bump `CURRENT_SCHEMA_VERSION` to 2.
- `migrateIndexDocument` now stages V0 → V1 → V2; V1-to-V2 is a
version bump only (no structural change — captures is absent until a
capture is recorded). Preserves any preexisting `captures` map.
- 6 new tests (V2 happy path, V1→V2 migration with identities preserved,
V0→V2 fast-path, V2 no-op, sidecar preserved across migration,
CaptureRef-all-fields type-shape check). 1 pre-existing test renamed
+ asserted as a forward migration rather than a v1 no-op.
Schema package: 47/47 tests, typecheck clean. Downstream consumer audit
shows the bump is invisible: `quarto-sync-client/src/client.ts:380`
runs `migrateIndexDocument(d)` without checking its return value; no
hub-client code imports the constant from this package.
Next on bd-kw93.1: sync-client wire-up, Rust IndexDocument mirror, WASM
`render_page_for_preview` signature widen.
Plan: claude-notes/plans/2026-05-13-q2-preview-phase-c.md §C.3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(sync-client): expose capture sidecar to consumers — bd-kw93.1
Phase C.3 sync-client wire-up. The IndexDocument's V2 captures sidecar
(landed in 2bdd6a5e) is now observable through the existing callback
shape — Phase C.4 (browser-side replay) will consume it from the SPA
without further plumbing.
- Re-export `CaptureRef` from `types.ts` so consumers can type-import
the sidecar shape with one import (mirrors how `FileEntry` is
surfaced).
- New `SyncClientCallbacks.onCapturesChange?: (captures: Record<string,
CaptureRef>) => void`. Optional — existing consumers compile
unchanged.
- `client.ts`: `getCapturesFromIndex` helper + `lastCaptures` tracking
+ `notifyCapturesIfChanged` diffing (JSON-equality, matching the
identities pattern). Fired on initial connect, on every index doc
change, and on createNewProject. Reset to `{}` on disconnect.
- `createNewProject` now writes `version: CURRENT_SCHEMA_VERSION`
(currently 2) into the new IndexDocument literal instead of a
hardcoded 1, so freshly-created projects are V2 from birth rather
than relying on migrateIndexDocument to bump them on next load.
- 2 new tests (empty-sidecar fire, populated-sidecar fire) modelled on
the existing identity tests.
sync-client tests: 68 → 70 passing; typecheck clean. Plan §C.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(quarto-hub): Rust IndexDocument capture sidecar mirror — bd-kw93.1
Phase C.3 Rust half. Adds a reader/writer for the same V2 capture sidecar
introduced in 2bdd6a5e (TS schema) so the server side can write
EngineCaptures from the file-watcher loop and the SPA can pick them up
through the existing samod sync — no extra channel.
Schema mirror:
- `CaptureState` enum (Idle | Running | Error) — matches the TS string
union; serialized as "idle" / "running" / "error" so the SPA can
read it without a discriminant.
- `CaptureRef` struct with `capture_doc_id` required + `staleness` /
`state` / `last_error` optional. Optional fields are omitted from the
automerge map when None, matching the TS shape and keeping forward-
compat with future fields.
- `CAPTURES_KEY = "captures"` on the ROOT map; nested per-path entries
each have their own ObjType::Map. Field names live in a private
`capture_field` module keyed to the TS interface.
`IndexDocument` methods:
- `get_all_captures()`, `get_capture(path)`, `has_capture(path)` —
return empty/None on V0/V1 docs that never had the key.
- `set_capture(path, &CaptureRef)` — creates the captures map lazily
(so V0/V1 docs migrate on first server write without an explicit
migration step). Delete-then-recreate the entry to avoid leaving
stale optional fields from a previous CaptureRef.
- `remove_capture(path)` — no-op when absent.
Tests: 7 new (initial empty, set+get roundtrip, all-fields-roundtrip
through a fresh DocHandle, overwrite-preserves-no-stale-fields, remove,
get_all returns multiple entries, files/captures independence). The last
test pins the v1 limitation called out in plan §Risks #3: removing a
file does NOT cascade-delete its capture sidecar; that's a caller
responsibility tracked separately.
quarto-hub: 186/186 tests pass; `RUSTFLAGS="-D warnings" cargo build -p
quarto-hub` clean. Plan §C.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* sync beads: close bd-kw93.1 (Phase C.3 schema complete)
C.3 sidecar schema is end-to-end: TS (2bdd6a5e), sync-client wire-up
(6a0af0e2), Rust mirror (804b8338). cargo xtask verify --skip-hub-build
all 12 steps green.
Also: bd-kw93.3 (C.4) description updated to absorb the WASM signature
widening originally listed under C.3 — doing it without the dispatch
logic (EngineRegistry::with_replay) would have been a half-finished
parameter, and C.4's test plan already exercises the dispatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* plan(q2-preview): Phase C Progress section, C.3 checked off
Top-level checklist tracks each sub-task. Records the WASM-widening
scope move from C.3 to C.4 (covered in bd-kw93.1's close reason and
bd-kw93.3's updated description, now also visible on the plan
itself).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(quarto-core): preview_record sub-pipeline for engine capture — bd-kw93.2
Phase C.1 first slice. Lands the engine-only sub-pipeline + capture
extraction that the q2 preview server will call from its file-watcher
loop to populate the IndexDocument capture sidecar.
`crates/quarto-core/src/engine/preview_record.rs`:
- New `record_capture(path, project, runtime, engine_registry) -> Result<Option<EngineCapture>>` entry point.
- Internal `CaptureCollector` observer wraps Arc<Mutex<Option<EngineCapture>>>, deserializes the ENGINE_CAPTURE_KIND aux payload that EngineExecutionStage already emits today (bd-45yw landed the emission seam).
- Sub-pipeline built by truncating `build_html_pipeline_stages_with_options` at the `engine-execution` stage — keeps the server-side record path in sync with the in-browser replay path automatically when new pre-engine stages are added.
- Optional engine_registry override is the test seam (passthrough engine registered under a non-"markdown" name to bypass the EngineExecutionStage short-circuit and trigger capture emission).
Returns `Ok(None)` when no capture is recorded — happens whenever the
resolved engine is `markdown` (its short-circuit at engine_execution.rs:191
skips both the execute() call and the aux event). Caller treats `None`
as "no capture needed" and omits the sidecar entry.
Tests (4 new, all passing):
- prose-only doc → None (default markdown engine path).
- doc wi…
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.
We may not want this enabled eventually, but useful when developing to be able to test against any
automerge-repocompliant backend e.g. one served by the autosync R package.