feat(studio): slideshow manifest persistence + panel helpers#1591
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
Review: slideshow manifest persistence + panel helpers
Clean, well-scoped PR. The split into three layers (pure transforms, persistence util, React hook) is well-reasoned and each layer earns its keep. A few observations:
1. Persistence design — solid
The <script type="application/hyperframes-slideshow+json"> island approach is the right call. It lives in the HTML document like a standard JSON-LD island, doesn't touch the SDK element tree, and the "strip all stale islands → insert one fresh island" strategy in persistSlideshowManifest is defensive without being over-engineered. The comment about <script> nodes having no hf-id clearly justifies why this bypasses setText dispatch.
The < / > escaping in buildSlideshowIslandHtml correctly prevents </script> breakout, and the round-trip through JSON.parse restores the original values. The test for this ("does NOT embed a literal </script> inside the JSON body") is well-crafted.
Exporting persistSdkSerialize from sdkCutover.ts with a clear ponytail comment explaining why (third caller — island write) is the right aperture. No unnecessary abstraction layer introduced.
2. Panel helpers — genuinely reusable
slideshowPanelHelpers.ts is 195 lines of pure functions with zero React, zero side-effects. This is the ideal shape for manifest transforms: each function takes a SlideshowManifest and returns a new one. They'll compose trivially with whatever state management lands later (Zustand, useReducer, etc.).
A few notes:
-
toggleMainLineSlide: Appending to the end on add is fine for now, but worth considering whether insert-at-cursor or insert-at-scene-timeline-position would be more intuitive once the UI is wired up. Not a blocker — just a future consideration. -
setSlideNotes: The auto-add behavior (creates a slide entry if one doesn't exist when setting notes) is a nice touch that prevents "set notes → nothing happened because slide wasn't added" confusion. -
pruneHotspots: Smart to cascade-delete hotspot references when deleting a sequence. The nested traversal through both main-line slides and sequence slides is thorough. -
addFragment: TheSet+ sort dedup is correct but worth noting:Setdeduplication uses strict equality, which is fine fornumber[]as long as fragments are always exact values (not floating-point approximations). If fragment times ever come from timeline scrubbing with sub-millisecond precision, you might want an epsilon-based dedup. Very minor — unlikely to bite in practice.
3. Data integrity — the important question
Corrupt/partial saves: The PR inherits persistSdkSerialize's write path, which is a single writeProjectFile call. If that write is interrupted (browser crash, tab close), you get a partial file. This is the same risk surface as every other SDK cutover path, so it's not new here — but since the slideshow manifest is authored data (notes, hotspots, branch sequences), data loss stings more than losing a style change. No blocker, but worth thinking about whether the save-queue's coalesce infra (which this PR correctly threads via coalesceKey) could also provide a write-ahead log or at least an in-memory rollback snapshot.
Migration path: parseSlideshowManifest does a minimal structural check (isManifest — is it an object with a slides array?) and then trusts the shape. If a future PR adds required fields to SlideshowManifest, old islands will parse but silently lack those fields. Consider a version field in the manifest now (even just version: 1) so future migrations can detect and upgrade. This is the cheapest thing to add early and the most painful thing to retrofit later.
No validation on persist: buildSlideshowIslandHtml serializes whatever SlideshowManifest it receives — it doesn't validate that sceneId values actually reference existing scenes, that hotspot targets reference existing sequences, etc. The parser side (resolveSlideshow) does validate these, which is good, but persisting an invalid manifest means the error surfaces on the next load, not at write time. Worth considering a validate-before-persist guard, at least in dev mode.
4. Integration with branching editor (#1582)
The branch-sequence helpers (createSequence, renameSequence, deleteSequence, assignToBranch) map cleanly to the SlideSequence type from core. The hotspot helpers (addHotspot, removeHotspot) reference sequences by ID, which is the right coupling level. The cascade-delete in deleteSequence (removing targeting hotspots) prevents dangling references.
One gap: there's no reorderBranchSlide equivalent for branch sequences — you can reorder main-line slides but not slides within a branch. Intentional deferral, or oversight?
5. Minor items
SceneInfointerface inslideshowPanelHelpers.ts(lines 10-15) is defined but never used in this PR. If it's consumed by the panel component in a later PR in the stack, that's fine — but if not, it's dead code.- The
fallow-ignore-next-line complexitycomment onreorderMainLineSlide— this function is straightforward (swap two array elements with bounds checking). Is fallow flagging it due to the early returns? Seems like a false positive worth noting in case the lint rule needs tuning. - The
fallow-ignore-next-line unused-exporton the re-export instudioHelpers.ts— is theCOMPOSITION_ROOT_OPEN_TAG_REre-export actually consumed anywhere, or is this just suppressing a lint warning for a genuinely unused export? - PR description is the default template with no sections filled in. For a 10-PR stack, even a one-liner per section ("Persistence layer for slideshow manifest; pure transform helpers for the panel") helps reviewers navigate.
Verdict
Well-structured, good test coverage for the persistence path (especially the </script> breakout and double-island collapse tests), clean separation of concerns. The main suggestion is adding a version field to the manifest now before it ships — everything else is minor or future-facing.
Would approve with the version field added (or a convincing argument for why it's unnecessary).
Review by Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
HEAD reviewed: bc2d796ad2de451bfb9181095c2c8de1a4a456fd
Verdict: Clean lower-half of the #1582 split — persistence layer is well-scoped, helpers are pure, tests are thorough. A few substantive concerns inherited from the parent monolith review; posted in full there (#1582) and on the UI half (#1592). Layering Miga's review.
Split verification — coverage of #1582
Compared gh pr diff 1582 vs gh pr diff 1591 + gh pr diff 1592 (the two split halves) file-by-file. Split is byte-identical, non-lossy:
- Per-file diffs match exactly between #1582 and the corresponding split half (all 11 files).
- Files in this PR (#1591):
slideshowPanelHelpers.ts,useSlideshowPersist.ts,sdkCutover.ts,setSlideshowManifest.ts,setSlideshowManifest.test.ts,studioHelpers.ts— 6 files, +488/-3. - Files in #1592:
App.tsx,StudioRightPanel.tsx,SlideshowPanel.tsx,SlideshowSubPanels.tsx,SlideshowPanel.test.ts— 5 files, +1448/-4. - 6+5 = 11 files = #1582's 11.
- 488+1448 = 1936 = #1582's 1936 additions (exact).
- 3+4 = 7 = #1582's 7 deletions (exact).
No file is in both halves. No file is missing from the union. The split is a mechanical separation, not a refactor — every helper, persist hook, and SDK-cutover export in #1591 is identical to the version in #1582.
Blockers
None.
Concerns (apply to this PR specifically)
1. persistSlideshowManifest skips the no-op gate that every sibling persistSdkSerialize caller applies — setSlideshowManifest.ts:62-78
Every other caller in sdkCutover.ts does if (after === before) return false before invoking persistSdkSerialize (e.g. sdkCutover.ts:198-199). The slideshow persist always replaces the island and writes, even when the resulting HTML is identical to current. Result: re-typed-same-key notes (post-debounce), toggle-then-untoggle a checkbox, reorder out of bounds — all produce a disk write, undo entry, and preview reload.
Fix: compare after === current immediately before calling persistSdkSerialize; bail if equal. Cheap, matches the sibling shape.
2. useSlideshowPersist reads originalContent per-call → race with concurrent writers — useSlideshowPersist.ts:39
const originalContent = await readProjectFile(path);
await persistSlideshowManifest({ manifest, sdkSession, originalContent, ... });The originalContent is the undo before baseline. Other cutover writers (e.g. sdkCutoverPersist) take originalContent from their caller, where it was snapshotted before sdkSession.dispatch(...) mutated state. Here, the read happens fresh every persist call. In the single-writer studio model this is fine, but if any DOM-edit or design-panel write lands between readProjectFile and writeProjectFile, the undo entry's before will be that interleaved write's contents, not what the user actually saw before they typed. The sdkSelfWriteRegistry (hooks/sdkSelfWriteRegistry.ts:11-30) suppresses self-echo reloads but doesn't serialize concurrent writers — the race is theoretical today but worth a comment in useSlideshowPersist documenting the single-writer assumption.
3. Manifest version field — concur with Miga
SlideshowManifest (packages/core/src/slideshow/slideshow.types.ts:3-6) has no version field. Adding version?: 1 now + threading into buildSlideshowIslandHtml costs ~3 lines and converts a future migration from "best-effort inference" to switch (manifest.version ?? 0). Cheapest to add before any author hand-edits a manifest in the wild. The fact that this PR introduces the persistence side of the island makes it the right moment to bake the version in.
4. Date.now() collisions silently drop user edits in helpers — slideshowPanelHelpers.ts:80, 158 (concur Miga with sharper consequence)
createSequence silently returns the manifest unchanged when an id already exists. addHotspot does the same. A double-click within 1ms (programmatic burst, fast trackpad double-tap) produces a duplicate seq-${Date.now()} ID; the second click is dropped with no UI feedback. The .catch(() => {}) in the call site doesn't help — the helper returns the unchanged manifest by design, not throwing. The ID generation lives in #1592's UI (SlideshowPanel.tsx:298, SlideshowSubPanels.tsx:385), but the silent-rejection helper contract lives here. Suggest at minimum a console.warn when the helper hits the duplicate-ID branch — converts a silent no-op into an observable signal.
5. pruneHotspots traversal — confirm intent on nested branch hotspots — slideshowPanelHelpers.ts:117-122
deleteSequence prunes hotspots from manifest.slides AND from seq.slides for every remaining sequence. Looks right. Worth a regression test that explicitly covers "delete sequence A; hotspots targeting A on a slide inside sequence B are pruned" — I see the helper tests cover the main-line cascade but didn't trace whether the cross-branch cascade has a dedicated test. (Tests live in #1592's test file at SlideshowPanel.test.ts — the test gate is on the wrong side of the split: pure-helper tests test the helpers shipped in this PR, but ride in the UI PR. See item 7.)
Nits
6. The "ponytail: third caller" comment is misleading — sdkCutover.ts:147
persistSdkSerialize has 5 callers internal to sdkCutover.ts (the various sdk*Persist flavors) plus the new external caller in setSlideshowManifest.ts. "Third caller" reads as if a public-API count was being tracked; the more accurate phrasing is "first external caller" or "first cross-module caller." Not load-bearing.
7. Helper unit tests ship in #1592, not here
The pure-function tests at SlideshowPanel.test.ts (460 lines) exercise the helpers introduced in this PR, but the test file lives in #1592 because it co-locates with the panel. If #1591 lands and #1592 stalls, the helpers are untested at merge. Two options:
- Move
slideshowPanelHelpers.test.tsinto #1591 (tests follow code). - Document that #1591 cannot merge independently of #1592 (Graphite already enforces this; just worth being explicit).
The persistence-side tests (setSlideshowManifest.test.ts, 141 lines) DO ship in #1591 — those are the ones that cover the </script> breakout and double-island collapse. Good.
8. PR description is the default template
For a 10-PR stack where the cross-compare relies on each PR's intent being explicit, the default ## What / ## Why / ## How / ## Test plan template makes review harder. A one-liner per section would let me confirm "this is the persistence layer; UI lives in #1592; ss-player-b base" without piecing it together from the branch names. (Same nit on #1592.)
Questions
9. Why the asymmetric slideshow lint rule from #1580 is silenced by safeParseManifest here?
safeParseManifest catches and warns on parse failure (SlideshowPanel.tsx:48), returning { slides: [] }. The lint rule from #1580 flags slideshow_invalid for malformed islands. If a malformed island opens in studio, the user sees the empty-slides state silently (no toast, no inline warning) and their next edit overwrites the malformed island with the new manifest — losing whatever the malformed content was trying to be. Intentional (user is editing; the malformed thing is being replaced anyway), or should studio surface the lint finding to the user before they edit?
What I didn't verify
- Whether the
coalesceKeyinrecordEditactually collapses to ONE undo entry per burst as the doc-comment claims, or N entries sharing a key. The PR description claims debounced-notes = "one undo entry per burst" — I trusted that. - Whether the
sdkSelfWriteRegistryhash window correctly identifies the slideshow persist write as a self-echo vs an external change. The persistSdkSerialize path callsmarkSelfWrite(targetPath, after)(sdkCutover.ts:160); the slideshow island write goes through the same path, so it should be tagged. But I didn't trace theuseSdkSession.ts:150"single writer" comment to confirm the slideshow path doesn't bypass it. - Concurrency with
writeProjectFileif it's not already serialized at the studio shell level. The single-writer assumption in (2) is the place to check.
Review by Rames D Jusso
Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stack-split from the original ss-player PR (#1581): the SlideshowController state machine (stack-based slide/fragment/branch navigation) and its tests. The <hyperframes-slideshow> web component follows in the next PR.
Stack-split from the original ss-player PR (#1581): the <hyperframes-slideshow> custom element (wraps <hyperframes-player>, drives the controller), presenter/audience BroadcastChannel sync, nav chrome, and the player scenes hook.
Stack-split from the original ss-studio PR (#1582): the data layer — setSlideshowManifest, the useSlideshowPersist hook, and panel helpers. The editor panel UI follows in the next PR.
86eea2b to
b2acf34
Compare
Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bc2d796 to
4c57d68
Compare
The base branch was changed.
Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(slideshow): address code-review findings #1580-1584 - player: bundle @hyperframes/core into the IIFE/global build (noExternal) - player: resolve audience mode from ?mode=audience URL query, not just attr - player: event-driven waitForScenes + loud failure when no slides resolve - player: scope window keydown so Space/Backspace don't hijack the host page - player: audience mirrors full position (branch + fragment) via syncTo - player: next() reveals remaining fragments even at slide end; enterBranch ignores empty sequences - core: harden extractScenes against null/non-object scene entries - core: strict manifest validation; error on inverted ranges & empty hotspot targets; dedup fragments - core/lint: accept data-end/timeline-derived scene durations (match runtime) - core+studio: share ISLAND_TYPE + island regex from @hyperframes/core/slideshow - studio: SlideList reflects manifest slide order; branch-slide authoring (notes/fragments/hotspots) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(player): slideshow fullscreen + presenter-view rework - fullscreen toggle in the nav chrome (button + 'F' key); standard Fullscreen API on the <hyperframes-slideshow> element, icon reflects state - presenter console: live slide on top, speaker-notes panel below, with the nav controls shown in-view; Present button hides once presenting (harness) - audience (viewer) window: chrome reduced to a fullscreen-only control, no nav - fix: audience / back() / backToMain() mirror stayed frozen on the first frame — a bare paused seek does not repaint some compositions. resumeSlide now plays a brief render-nudge (RENDER_NUDGE) past the target so the composition paints, then onTime pauses at the hold - refactor: extract reusable buildNavCluster() + wireChromeButtons(); rework buildPresenterLayout into the bottom notes panel - example: airbnb-deck presenter-test.html harness (Present button + 'F') Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(player): slideshow no auto-progress + presenter slide fits/pins - navigation jumps to a static frame instead of auto-playing the timeline: playTo() seeks to the hold (+ a brief RENDER_NUDGE to repaint) rather than sustaining playback, so slides hold until the user advances - presenter view: pin the live slide to the top and confine the player to the region above the notes panel, so the player CONTAINS the composition — the full slide stays visible (letterboxed) at any width and re-fits on resize; its bottom is no longer cut off by the notes panel - tests: seek targets updated for the render-nudge offset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): presenter nav flash, slide-1 boundary, branch buttons Three presenter-mode fixes from testing the airbnb deck: (1) navigation flash — seek to the exact target then play forward to repaint, instead of seeking backward (t-0.2) which painted the previous scene at boundaries; split hold into holdTarget (logical) and holdAt (target+nudge, clamped to slide.end). (2) slide-1 boundary — no-fragment slides rest at the slide midpoint, not slide.end. (3) presenter branch buttons — surface hotspots as buttons in the presenter console (the on-slide pill is lost in the letterboxed view). Also extract paintChrome() to dedupe the three chrome-render sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): stop presenter nav buttons flickering / dropping clicks The presenter elapsed clock called render() every second, which rebuilt the entire chrome (innerHTML) including the nav buttons — they flickered and any click landing mid-rebuild was lost. The 1s tick now updates only the elapsed text node; the nav buttons are rebuilt only on actual navigation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): CSP-safe nav hover, UUID editor ids, manifest version Addresses review feedback on the split stack: - CSP: replace the 8 inline onmouseover/onmouseout handlers on the nav buttons with a [data-hf-nav-cluster] button:hover CSS rule (injected once per document). No inline event handlers → works under strict CSP. - IDs: studio sequence/hotspot id generation used Date.now() (sub-ms collision on rapid clicks) — now crypto.randomUUID(). - Versioning: stamp version on the persisted manifest island (preserving an existing one); add the optional version field + SLIDESHOW_MANIFEST_VERSION to the core schema so future schema changes can migrate older islands. These live on the review-fixes tip (consistent with the stack's fixup-on-tip model); the touched code belongs to ss-player-b (#1590), ss-studio-a/b (#1591/#1592), and ss-core (#1580). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(ci): fix format + fallow gates for slideshow stack - .prettierignore: exclude generated demo compositions (registry/examples/**/*.html) from oxfmt — large video-pipeline output (GSAP/Three/WebGL), not hand-authored source. Was failing 'Format' repo-wide (pre-existing on main via #1584). - .fallowrc: exempt SlideshowPanel.tsx (health/complexity — section fan-out) and the slideshowPanelHelpers.ts / SlideshowPanel.test.ts parallel-structure clones (duplicates.ignore). File-level config, not inline comments — inline shifts line numbers and breaks fallow's inherited-finding fingerprint (per existing rc note). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(slideshow): address PR review + CodeQL findings - CodeQL #638 (parseSlideshow): complete the regex metachar escape in slideshowIslandRegex (was missing backslash); add JSDoc on the factory + lastIndex caveat (reviewer 5a/16). - CodeQL #639/#640 + review items 13/17: remove registry/examples/airbnb-deck/ presenter-test.html — a generated test harness (postMessage w/o origin check, proto-pollution) that was scope-creep into a fix PR and a 3rd duplicate island. Regenerate locally via the scratchpad script when testing. - Review item 15 (docs drift in skills/slideshow/SKILL.md): lint resolves scenes by data-composition-id only (not .clip[id]); fragments are valid INCLUSIVE of [start,end], not 'strictly inside'. IIFE bundles core confirmed (0 external @hyperframes/core refs in the slideshow global build). format/lint/fallow green. * feat(cli): add 'present' command — serve a deck in presenter mode hyperframes present [dir] starts a lightweight HTTP server, wraps the composition in <hyperframes-slideshow> with its island inlined, and opens the browser. A real HTTP origin is required for presenter mode: present() opens the audience window via window.open(?mode=audience) and the two sync over BroadcastChannel — neither works from file://. - New utils/compositionServer.ts factors the server scaffolding shared with 'play' (resolve runtime/player/slideshow bundles, inject runtime, asset content-types, bind to a free port); play.ts now uses it too. - Errors clearly if the deck has no slideshow island. - .fallowrc: exempt the play/present command entrypoints (validation + server wiring) and the per-command startup/logging block from the complexity / duplication gates. Verified end-to-end against registry/examples/airbnb-deck: server serves the wrapper + assets, the component binds and renders (counter 1 / 11). * fix(cli): present renders the deck (player sizing + self-driving serve) Two bugs caused a black slide area: - The <hyperframes-player> had no positioning, so its iframe collapsed to zero size — the (absolutely-positioned) chrome showed but the composition didn't. Add position:absolute; inset:0 (matches demo.html). - The composition was served with the engine runtime injected, which leaves its timelines engine-paused (blank). Slideshow decks self-drive their own timelines (like demo.html / the standalone harness), so serve them raw. Verified end-to-end on registry/examples/airbnb-deck: cover renders, Next advances 1/11 -> 2/11 and slide 2 paints. * fix(cli): present plays slideshow sound effects The composition (in the player's sandboxed iframe) posts { type: 'hf-sfx', name } to the parent on nav, but the iframe is autoplay-blocked — audio must play in the parent that owns the user gesture. Add the parent-side hf-sfx handler (the 4 standard clips advance/fragment/ branch-enter/back, served from the deck's sfx/ under /composition/sfx/), gesture-unlocked and mute-aware, in both presenter and audience windows. Verified: sfx serve 200 (audio/mpeg) and Next delivers [advance, fragment] to the parent handler. * feat(examples): softer mellow slideshow sfx for airbnb-deck Replace the aggressive percussive pops with gentle sine-tone cues (warm pitches C5/G4/E5/F4, 12ms attack + exponential decay, lowpassed) — advance/ fragment/branch-enter/back. Much lighter; fragment is the most subtle. * feat(examples): whoosh + sparkle slideshow sfx for airbnb-deck Replace the sine-tone cues with airy, designed sounds: - advance: a soft whoosh (band-limited pink noise, bell-shaped swell) - back: that whoosh reversed and darkened - fragment: a light sparkle (staggered high chime blips) - branch-enter: whoosh + a trailing sparkle (magical entry) * feat(examples): directional whoosh + richer branch-enter cue (airbnb-deck) - Going backward a slide now plays the reverse whoosh (back), not advance — the sfx logic detects nav direction by scene order instead of firing advance for every scene change. - branch-enter is now a more interesting magical cue: a faint whoosh + an ascending C5-E5-G5-C6 chime arpeggio + a trailing sparkle. Verified: next then prev fires [advance, fragment, back]; no page errors. * fix(cli): harden present sfx handler + mute-hover affordance (R2 review) Addresses Rames R2 items 19-21: - 20: the present audio handler reintroduced the CodeQL classes removed with presenter-test.html — add an origin check (same-origin composition iframe) and an own-property guard so a 'name' like __proto__ can't resolve to and mutate Object.prototype. - 21: assetContentType used a bare index lookup (ext='__proto__' -> prototype); guard with Object.hasOwn. - 19: the CSP hover rule erased the speaker button's muted color; add a higher-specificity [data-hf-muted] [data-hf-mute]:hover override. Verified: hf-sfx origin matches location.origin (guard passes), advance/fragment still fire, deck renders + advances. Items 14/18/22 deferred (minor, pre-existing). * fix(slideshow): address remaining R2 items (14/18/22) + re-remove harness - 14: resumeSlide now mirrors enterSlide — a no-fragment slide resumes at its midpoint (visible-at-rest), not frame-0; fragmented slides still resume to the saved fragment or slide.start. Added a dedicated test naming the heuristic. - 18: fullscreenchange swaps only the fullscreen glyph + aria (hoisted SVGs to module consts) instead of re-rendering the whole chrome. - 22: .prettierignore lists the specific generated demo compositions instead of blanket registry/examples/**/*.html, so hand-authored example HTML still formats. - presenter-test.html: a stray 54a4460 git add -A had re-added the deleted harness (reviving CodeQL #639/#640); remove it again. 106 slideshow tests pass; tsc/lint/fallow/format clean; deck still renders. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
miga-heygen
left a comment
There was a problem hiding this comment.
Re-review — 4c57d68 (post-feedback HEAD)
PR scope expanded: now includes the SlideshowController state machine and <hyperframes-slideshow> web component (2 new commits from the #1581 split), plus the original studio persistence layer. Reviewed the new code; focusing this comment on the original findings and whether they've been addressed.
Status of prior findings
1. Missing version field on SlideshowManifest — STILL OPEN
SlideshowManifest in @hyperframes/core/slideshow still has no version field. The type isn't touched in this PR, so it wasn't added. Both Rames and I flagged this. The argument remains: adding version?: 1 now (3 lines — type field + serialize it in buildSlideshowIslandHtml + thread into parseSlideshowManifest) converts future migrations from best-effort inference to a switch on a known field. The persistence island ships in this PR — once manifests are on disk in the wild, retrofitting a version field becomes a migration problem instead of a definition problem.
2. No reorderBranchSlide helper — STILL OPEN (acknowledged deferral)
reorderMainLineSlide exists but no equivalent for branch-sequence slides. If this is an intentional deferral for a later PR, a // TODO: or a brief note in the PR description would prevent this from being re-flagged.
3. SceneInfo interface defined but unused — STILL PRESENT
SceneInfo (slideshowPanelHelpers.ts:10-15) is exported but never referenced in any file in this PR. If it's consumed by the panel component in #1592, that's fine — but it rides in the wrong PR. Either move it to #1592 where its consumer lives, or add a comment noting which downstream PR consumes it.
4. No write-time validation of manifest integrity — STILL OPEN
buildSlideshowIslandHtml serializes whatever SlideshowManifest it receives. resolveSlideshow validates on the read side (and the new console.warn for manifest errors at hyperframes-slideshow.ts:885 is good), but a bad manifest can still be persisted and the error only surfaces on the next load. Not a blocker — the read-side validation is the critical path — but a validateBeforePersist guard (even dev-only via process.env.NODE_ENV) would catch issues at write time rather than next-session-open time.
5. Missing no-op gate in persistSlideshowManifest — STILL OPEN (Rames' finding)
persistSlideshowManifest (setSlideshowManifest.ts:62-78) still always strips + re-inserts the island and writes, even when the resulting HTML is byte-identical to current. Every other persistSdkSerialize caller does if (after === before) return before invoking the shared writer. The comment on persistSdkSerialize itself says "which also did the no-op check" — but this caller doesn't honor that contract.
The fix is ~2 lines: after computing after (line ~78), add if (after === current) return; before the persistSdkSerialize call. This prevents undo entries, disk writes, and preview reloads on re-typed-same-value notes, toggle-then-untoggle checkboxes, and out-of-bounds reorders.
6. Silent duplicate-ID rejection in helpers — STILL SILENT (Rames' finding #4)
createSequence and addHotspot silently return the manifest unchanged when a duplicate ID is passed. No console.warn was added. The ID generation lives in #1592 but the silent-rejection contract lives here. A console.warn on the duplicate-ID branch converts a silent no-op into an observable signal — useful for catching double-click / programmatic-burst race conditions.
7. useSlideshowPersist single-writer assumption undocumented — STILL UNDOCUMENTED (Rames' finding #2)
The originalContent = await readProjectFile(path) call happens fresh per persist, creating a theoretical race with concurrent writers. The coalesceKey mitigates rapid-fire writes but doesn't serialize against other cutover writers. A comment documenting the single-writer assumption (as Rames suggested) would prevent future developers from accidentally breaking this invariant.
New code observations (SlideshowController + web component)
The new player-side code is well-structured. A few notes:
-
Controller state machine — clean stack-based navigation with proper fragment handling. The
resumeSlidepath that preservesfragmentIndexonback()is the right design. Tests are thorough (574 lines covering linear nav, branching, fragment sequencing, edge cases). -
initInFlightre-entrancy guard — good. The generation counter (initGeneration) correctly handles disconnect-during-async-init. Thetry/finallyensuresinitInFlightis always cleared. -
waitForScenestimeout fallback — the 2500ms timeout with graceful[]fallback is sensible. Theabortedcallback parameter for early bail-out on disconnect is a nice touch. -
BroadcastChannel catch — the
catch {}atslideshowPresenter.tsfor unsupported environments is appropriate (feature detection, not error swallowing). -
Global
_keyframesInjectedflag — works for single-document use but would break if the element is used across documents (e.g. iframes). Not a concern for the current use case but worth noting if<hyperframes-slideshow>is ever embedded in a multi-document context.
Summary
| Finding | Status |
|---|---|
version field on manifest |
Open |
reorderBranchSlide |
Open (deferral OK if noted) |
SceneInfo unused |
Open |
| Write-time validation | Open (non-blocking) |
| No-op gate in persist | Open |
| Silent duplicate-ID rejection | Open |
| Single-writer assumption docs | Open |
The no-op gate (#5) and version field (#1) are the two I'd want addressed before merge. The rest are either non-blocking or can ride in follow-ups.
Re-review by Miga
* fix(slideshow): address split-PR review findings on #1585 Genuinely-open findings from the #1580/#1590/#1591/#1592 reviews (the rest were already fixed on this branch: CSP handlers, manifest version, UUID ids, float keys, presenter 1s-timer): core (#1580): - isManifest rejects a non-object/array manifest (e.g. [42,null]) explicitly - resolveSlideshow flags duplicate slideSequence ids instead of silent overwrite player (#1590): - present() window.open uses noopener,noreferrer (audience syncs via channel) - BroadcastChannel name is per-deck (keyed on pathname) to avoid same-origin cross-talk between decks - add observedAttributes + attributeChangedCallback so runtime sound/mode toggles re-render studio (#1591/#1592): - persistSlideshowManifest no-op gate (skip write when HTML is unchanged) - surface persist failures (console.error) instead of silent .catch(()=>{}) - confirm before deleting a branch sequence (data-loss + dangling hotspots) + tests for the collision + non-object-manifest rejection. 20 core / 106 player / 53 studio pass; tsc/lint/fmt/fallow clean; deck still renders. * fix(slideshow): finish remaining split-PR review findings The larger items from the #1580/#1590/#1591/#1592 reviews (the rest landed in #1585): core (#1580): - dedup isSceneLikeCompositionId — shared slideshow/sceneId.ts, used by both the lint rule and the runtime scene-window computation (no more mirror-and-drift) player (#1590 / #1592): - onKey: when multiple decks share a page, drop the unfocused-convenience so a key drives only the focused deck - slow-iframe recovery: if the scene timeline posts after the wait times out (empty scenes), re-init once so sceneId slides resolve instead of being dropped studio (#1591): - persistSlideshowManifest validates the built island round-trips before writing - reorderBranchSlide helper + BranchTree up/down controls (parallel to main-line reorder), with a branch-position indicator + tests for reorderBranchSlide. core 228 / player 106 / studio (panel) 46 pass; tsc/lint/fmt/fallow clean. * fix(player,cli): use fileURLToPath for path resolution (Windows CI) new URL(...).pathname yields a leading-slash drive path ("/D:/...") on Windows, which broke: - packages/player/vitest.config.ts — the @hyperframes/core/slideshow alias resolved to a nonexistent path, failing the player slideshow tests on the Windows render-verification CI (passed on macOS/Linux where pathname is clean) - packages/cli/src/utils/compositionServer.ts helperDir — same bug in the play/present bundle-path resolution fileURLToPath converts file:// URLs to correct OS paths on all platforms. Player slideshow tests pass; present serves + resolves bundles. * fix(producer): fileURLToPath for the renders dir (Windows) DEFAULT_RENDERS_DIR used new URL(import.meta.url).pathname, which is "/D:/..." on Windows and resolves to a bogus path — affects the Windows render pipeline. Last of the .pathname -> fileURLToPath fixes (repo-wide src sweep now clean).

What
Brief description of the change.
Why
Why is this change needed?
How
How was this implemented? Any notable design decisions?
Test plan
How was this tested?