From 2e3b9f7a399d8cce91c7a521e0a081b935252c41 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 16 Oct 2026 18:45:02 -0700 Subject: [PATCH 1/4] fix(skills): keep hoisted launch-video clips on top and apart in snapshot --- packages/cli/src/commands/snapshot.test.ts | 34 ++++++++++++++++++++++ packages/cli/src/commands/snapshot.ts | 25 +++++++++++++--- packages/engine/src/index.ts | 1 + 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/snapshot.test.ts b/packages/cli/src/commands/snapshot.test.ts index df0413aa021..f26176f050c 100644 --- a/packages/cli/src/commands/snapshot.test.ts +++ b/packages/cli/src/commands/snapshot.test.ts @@ -1027,3 +1027,37 @@ describe("snapshot composite recapture", () => { expect(finalCapture).toBeGreaterThan(recapture); }); }); + +describe("snapshot media identity", () => { + it("keys two videos that share an author id apart, as the render path does", async () => { + const project = mkdtempSync(join(tmpdir(), "hf-snapshot-dup-ids-")); + const clip = (src: string) => + ``; + writeFileSync( + join(project, "index.html"), + `
` + + `${clip("a.mp4")}${clip("b.mp4")}
`, + ); + snapshotState.openSettledPage.mockClear(); + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + try { + await expect( + snapshotCommand.run?.({ + args: { dir: project, at: "1", end: false, output: join(project, "out") }, + } as never), + ).rejects.toBeDefined(); + const [html, , options] = snapshotState.openSettledPage.mock.calls[0] as unknown as [ + string, + string, + { beforeNavigate?: unknown }, + ]; + expect(html).toContain('data-hf-render-id="clip"'); + expect(html).toContain('data-hf-render-id="clip__hf2"'); + expect(typeof options.beforeNavigate).toBe("function"); + } finally { + log.mockRestore(); + rmSync(project, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index e382eeabed6..0998967b97b 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -36,13 +36,17 @@ import { type ZoomTarget, } from "../capture/captureCompositionFrame.js"; import { + assignMediaRenderIds, isClipVisibleAt, isInClipWindow, + MEDIA_RENDER_ID_ATTR, readElementRateSpec, sourceTimeAt, timeAtSourceTime, type RateSpec, } from "@hyperframes/core"; +import { parseHTML } from "linkedom"; +import type { Page } from "puppeteer-core"; import { resolveProject } from "../utils/project.js"; import { definitiveEntryMismatchComposition, @@ -495,6 +499,18 @@ export function serveCapturePage(projectDir: string, html: string, proxy: boolea * Render key frames from a composition as PNG screenshots. * The agent can Read these to verify its output visually. */ +/** Two clips sharing an author id must stay two frames, keyed as the render path keys them. */ +export function withMediaRenderIds(html: string): string { + const { document } = parseHTML(html); + assignMediaRenderIds(document); + return document.toString(); +} + +async function installMediaRenderIdBridge(page: Page): Promise { + const engine = await import("@hyperframes/engine").catch(() => null); + await engine?.installMediaRenderIdBridge(page); +} + async function captureSnapshots( projectDir: string, opts: { @@ -541,7 +557,7 @@ async function captureSnapshots( // Localize fonts (embed remote @font-face as data URIs, matching the render // path) so snapshots render the real font instead of a fallback sans. - const html = await bundleWithLocalizedFonts(projectDir); + const html = withMediaRenderIds(await bundleWithLocalizedFonts(projectDir)); const server = await serveCapturePage(projectDir, html, opts.autoProxy); const savedPaths: string[] = []; @@ -555,6 +571,7 @@ async function captureSnapshots( renderReadyTimeoutMs: opts.timeout ?? 5000, renderReadyWarningSuffix: "snapshots may be inaccurate", browserGpuMode: opts.browserGpuMode, + beforeNavigate: installMediaRenderIdBridge, }); try { @@ -755,7 +772,7 @@ async function captureSnapshots( if (cameraExpr) await page.evaluate(cameraExpr); if (injectVideoFramesBatch && syncVideoFrameVisibility) { - const candidates = await page.evaluate(() => { + const candidates = await page.evaluate((renderIdAttr: string) => { const runtimeWindow = window as Window & { __hfResolveMediaStartSeconds?: (element: Element) => number; }; @@ -769,7 +786,7 @@ async function captureSnapshots( const rawDuration = parseFloat(v.dataset.duration ?? ""); const srcDur = Number.isFinite(v.duration) && v.duration > 0 ? v.duration : 0; return { - id: v.id, + id: v.getAttribute(renderIdAttr) || v.id, src: v.currentSrc || v.src, authoredStart, authoredRate: v.dataset.playbackRate, @@ -788,7 +805,7 @@ async function captureSnapshots( devicePixelRatio: window.devicePixelRatio, }; }); - }); + }, MEDIA_RENDER_ID_ATTR); const compositionDuration = duration; const active = candidates.flatMap((candidate) => { const start = resolveSnapshotVideoClipStart(candidate); diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index 9d5a4aac575..fab66385fb1 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -173,6 +173,7 @@ export { DOM_LAYER_MASK_STYLE_ID, type BeginFrameResult, } from "./services/screenshotService.js"; +export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js"; // ── Encoding ─────────────────────────────────────────────────────────────────── export { From f1b53260a1be4472457ba49dfa1e3faa3ff2462d Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 16 Oct 2026 19:01:18 -0700 Subject: [PATCH 2/4] refactor(cli): keep the snapshot render-id helper module-private --- packages/cli/src/commands/snapshot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index 0998967b97b..ba3e23a3808 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -500,7 +500,7 @@ export function serveCapturePage(projectDir: string, html: string, proxy: boolea * The agent can Read these to verify its output visually. */ /** Two clips sharing an author id must stay two frames, keyed as the render path keys them. */ -export function withMediaRenderIds(html: string): string { +function withMediaRenderIds(html: string): string { const { document } = parseHTML(html); assignMediaRenderIds(document); return document.toString(); From 2365c3c058b7663eb10980bbfbfa44db5ac4649c Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Fri, 16 Oct 2026 19:08:33 -0700 Subject: [PATCH 3/4] fix(cli): keep the snapshot command's doc comment on its function --- packages/cli/src/commands/snapshot.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index ba3e23a3808..2b693c8305e 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -495,10 +495,6 @@ export function serveCapturePage(projectDir: string, html: string, proxy: boolea return serveStaticProjectHtml(projectDir, html, undefined, [], proxy === true); } -/** - * Render key frames from a composition as PNG screenshots. - * The agent can Read these to verify its output visually. - */ /** Two clips sharing an author id must stay two frames, keyed as the render path keys them. */ function withMediaRenderIds(html: string): string { const { document } = parseHTML(html); @@ -511,6 +507,10 @@ async function installMediaRenderIdBridge(page: Page): Promise { await engine?.installMediaRenderIdBridge(page); } +/** + * Render key frames from a composition as PNG screenshots. + * The agent can Read these to verify its output visually. + */ async function captureSnapshots( projectDir: string, opts: { From b0f6d5afbe43d20bd9207232efdc1522abee79be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 22 Oct 2026 11:11:13 -0400 Subject: [PATCH 4/4] fix(engine): snapshot keeps an injected video frame visible when its render id differs --- .../src/services/screenshotService.test.ts | 28 +++++++++++++++++++ .../engine/src/services/screenshotService.ts | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/services/screenshotService.test.ts b/packages/engine/src/services/screenshotService.test.ts index 9fd1d9a0bb2..ea0725c6980 100644 --- a/packages/engine/src/services/screenshotService.test.ts +++ b/packages/engine/src/services/screenshotService.test.ts @@ -16,6 +16,7 @@ import { initTransparentBackground, DOM_LAYER_MASK_STYLE_ID, } from "./screenshotService.js"; +import { installMediaRenderIdBridge } from "./mediaRenderIdBridge.js"; // These DOM tests isolate layout/decode from the CDP transport, exercised in Chrome separately. vi.mock("./videoFrameTransport.js", () => ({ @@ -621,6 +622,33 @@ describe("video-frame injection respects ancestor visibility", () => { expect(seededImg.style.visibility).toBe("hidden"); }); + it("keeps a frame injected by render id visible through the sync, as snapshot calls them", async () => { + const { teardown, setup } = withGlobals( + setupHostHiddenScenario({}, { hostAttribute: "data-start" }), + ); + setup.video.setAttribute("data-hf-render-id", "pip__hf2"); + try { + await installMediaRenderIdBridge({ + evaluateOnNewDocument: async (fn: (...args: unknown[]) => unknown, ...args: unknown[]) => + fn(...args), + } as unknown as Page); + await injectVideoFramesBatch(passthroughPage(), [ + { + videoId: "pip__hf2", + dataUri: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=", + }, + ]); + await syncVideoFrameVisibility(passthroughPage(), ["pip__hf2"]); + } finally { + teardown(); + } + + const img = setup.video.nextElementSibling as HTMLElement | null; + expect(img?.classList.contains("__render_frame__")).toBe(true); + expect(img?.style.visibility).toBe("visible"); + }); + it("syncVideoFrameVisibility hides the replacement for ancestor-hidden actives", async () => { const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" })); const seededImg = setup.document.createElement("img"); diff --git a/packages/engine/src/services/screenshotService.ts b/packages/engine/src/services/screenshotService.ts index d702f8bf171..abf1bac548b 100644 --- a/packages/engine/src/services/screenshotService.ts +++ b/packages/engine/src/services/screenshotService.ts @@ -889,7 +889,7 @@ export async function syncVideoFrameVisibility( const img = video.nextElementSibling as HTMLElement | null; const hasImg = img && img.classList.contains("__render_frame__"); const ancestorHidden = isVisualAncestorHidden(video); - const visible = active.has(video.id) && !ancestorHidden; + const visible = active.has(window.__hfMediaId?.(video) || video.id) && !ancestorHidden; if (visible) { // Active video: show injected , hide native