fix: align Studio capture with preview#595
Conversation
jrusso1020
left a comment
There was a problem hiding this comment.
LGTM — single commit, two distinct bugs, two distinct tests. Clean PR shape after the bundling concerns from #591.
What's covered
Andy's sub-composition capture issue → "align Studio capture with preview"
Root cause: capture's preview-seek path only knew about window.__timelines (the GSAP timeline registry). Compositions exposing the newer window.__player runtime API never got seeked at capture time, so the captured frame was off-time. The new vite.thumbnail.ts:seekThumbnailPreview checks __player.seek first and falls back to __timelines.pause(t) only when __player isn't present. Returns "player" | "timelines" | "none" so failures are debuggable.
Tests in vite.thumbnail.test.ts cover both paths:
__player.seekpresent → it gets called,__timelines.pausedoes NOT, returns"player".- Only
__timelinespresent → all entries getpause(t), gsap ticker ticks, returns"timelines".
Bonus: the helper extraction also resolves the file-organization concern from #591 — instead of keeping seek logic inline in vite.config.ts, it now lives in a peer file (vite.thumbnail.ts) imported relatively, no Node-can't-load-.ts issue.
Names with spaces → projectRouting.ts
Root cause: App.tsx's useMountEffect did window.location.hash.match(/^#project\/([^/]+)/) and used the raw match, so #project/Notion%20Showcase produced projectId = "Notion%20Showcase" (encoded). Downstream frameCapture.buildFrameCaptureUrl then double-encoded that into Notion%2520Showcase for the API path. Capture endpoint received the wrong project id and 404'd.
parseProjectIdFromHash decodes properly, gracefully falls back on malformed %XX escapes, and buildProjectApiPath encodes exactly once. 11 test cases cover: legacy raw-space hash, malformed escapes, reserved chars (#/?), unicode round-trip, double-encoding-prevention, and end-to-end via buildFrameCaptureUrl.
Bonus seek-time fix in thumbnail.ts
Server-side: parseFloat(t || "0.5") || 0.5 was coercing t=0 to 0.5 (because 0 || 0.5 === 0.5). Fix uses Number.isFinite check so explicit t=0 is preserved. Cache version bumped v2 → v3 to invalidate stale cached thumbnails. Test "preserves an explicit zero seek time" added.
Symmetric proof
- Seek-time fix: reverted
thumbnail.tswhile keeping the new test → "preserves an explicit zero seek time" fails red (1/3 fail). Restore → 3/3 pass. - Project routing: removed
projectRouting.tswhile keeping its test → file load fails, 0 tests run. Restore → 11/11 pass.
Both fixes have tests that genuinely cover the bugs.
Tests
@hyperframes/studio269/269 (was 256, +13 from the new test files)@hyperframes/core602/602 (was 601, +1 from the new thumbnail-test case)
CI gate added: the new preview-regression.yml step runs the four relevant test files specifically, so future regressions on the capture-vs-preview alignment surface immediately.
One nit, non-blocking
Looking at hf#591's lessons re: bundled fixes — this PR does the right thing by keeping the two bugs in one commit but with distinct test files (vite.thumbnail.test.ts for sub-comp, projectRouting.test.ts for spaces, thumbnail.test.ts for the t=0 case). If anything, the t=0 fix probably warranted its own commit (it's third-bug-territory, not part of the two Miguel called out), but the test exists and the change is small/safe. Not worth blocking on.
— Review by Rames Jusso
Merge activity
|
Problem
Studio frame capture could fail for projects mounted outside the repo when the project id came from an encoded hash route. A project like
Notion Showcaseloaded as#project/Notion%20Showcase, but the capture URL encoded that already-encoded value again, producing/api/projects/Notion%2520Showcase/...and a 404.While validating the fix by seeking through the preview, capture also diverged from the visible player for nested compositions because the thumbnail route sought raw timelines instead of the same player seek path used by Studio preview.
What this fixes
#project/...routes and centralizes project hash/API path construction.%, reserved characters, and unicode.window.__player.seek(t)and only fall back to raw timeline seeking for standalone pages.t=0thumbnail requests instead of falling back to0.5seconds.Root cause
Studio treated the hash route segment as the canonical project id even when the browser had already percent-encoded it.
buildFrameCaptureUrlthen encoded that string again, so a decoded project directory name and the capture API path no longer matched.The preview/capture mismatch was a separate seek-path issue: the visible Studio preview seeks through the HyperFrames player, which maps global time into nested composition time. The capture route bypassed that layer and paused all registered timelines at the same global time.
The zero-second capture case came from parsing
twith a truthiness fallback, soparseFloat("0") || 0.5became0.5.Verification
Local checks
bun run --cwd packages/studio test -- vite.thumbnail.test.ts src/utils/projectRouting.test.ts src/utils/frameCapture.test.tsbun run --cwd packages/core test -- src/studio-api/routes/thumbnail.test.tsbunx oxfmt --check .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.tsbunx oxlint .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.tsbun run --cwd packages/studio typecheckbun run --cwd packages/core build:hyperframes-runtimebun run --cwd packages/core typecheckgit diff --checkPre-commit also reran lint, format, and typecheck successfully for the committed files.
Browser verification
Using
agent-browser, I mounted/Users/miguel07code/Downloads/Notion Showcaseinto Studio's project data and opened:Before the fix, Capture requested
/api/projects/Notion%2520Showcase/thumbnail/index.html?...and Studio showedCapture failed.After the fix, I sought the preview to
0s,2s,10s, and18s, captured each frame, and compared the visible preview crop against the capture output. The capture URLs all usedNotion%20Showcase, notNotion%2520Showcase, and no failure toast appeared.Mean pixel diffs for preview vs capture were:
0s:0.02s:0.864110s:0.349618s:0.2309The small non-zero diffs are raster/antialias-level differences after resizing the capture to the preview crop dimensions.
Notes
agent-browserrecording are local-only underqa-artifacts/capture-button/and are not committed.packages/studio/data/projects/and is not committed.