fix(slideshow): address code-review findings #1580-1584#1585
Conversation
ab3b968 to
c21951b
Compare
2880acf to
34f3591
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
Code Review — PR #1585
Reviewed the full diff (+925/−221 across 17 files). This is a dense, well-documented fixup PR. Findings organized by the five review axes requested.
1. Does it address the review findings from #1580–1584?
Yes — comprehensively. I can trace every claimed fix back to concrete code:
- Standalone build broken (tsup externalizing
@hyperframes/core): fixed vianoExternal: ["@hyperframes/core"]intsup.config.ts. One-liner, correct. - Audience mode never activating:
resolveMode()now falls back to?mode=audiencefromlocation.search. All five callsites that previously usedgetAttribute("mode")are updated. - Audience mirroring only
goToSlide(slideIndex): replaced withsyncTo(sequenceId, slideIndex, fragmentIndex)on the controller and the BroadcastChannel handler. Tests updated to verify full-position mirroring including branches. - Keyboard hijacking host page:
Space/Backspacegated behindfocusedcheck;ArrowRight/ArrowLeftstill work inambientmode (body focus or unfocused). Tests verify both states. next()skipping trailing fragments:atEndgate removed; test added for the specific "currentTime already at slide end" scenario.enterBranchsoft-lock on empty sequence: guard added (seq.slides.length === 0), test renamed and rewritten to verify no-op behavior.extractScenescrash on null entries: cast-free, null-guarded type predicate. Clean.- Strict manifest validation:
isManifestnow validatesslideSequencesas array and each element viaisSlideRef/isSlideSequence. Tests for non-arrayslideSequencesand malformed slides. - Inverted range not flagged:
endTime <= startTimeerror added toresolveSlide. Test covers it. - Empty hotspot target not flagged:
seq.slides.length === 0error. Test covers it. - Fragment dedup:
new SetinresolveSlide. Test verifies[2,1,2,1,3] → [1,2,3]. - Lint
parseTimingmismatch with runtime: now acceptsdata-end/data-hf-authored-endin addition todata-duration. Priority order mirrors what runtimetimeline.tsdoes. - Shared island constant:
SLIDESHOW_ISLAND_TYPE+slideshowIslandRegex()exported from core, consumed by studio'ssetSlideshowManifest.ts. Drift-prone duplication eliminated. - SlideList render order: now renders
manifest.slidesorder first (via map + sceneById lookup), then non-slide scenes. Uses aSetfor O(1) membership. - Branch-slide authoring:
mapSlidesInhelper threadssequenceIdthroughsetSlideNotes/addFragment/removeFragment/addHotspot/removeHotspot.BranchTreelets you select+edit branch slides. Panel threadsselectedSequenceIdeverywhere. Tests cover branch-scoped editing including the "don't auto-add to branch" guard. - Stable fragment keys:
key={t}→key={\frag-${i}`}`. Prevents React duplicate-key issues on duplicate times (which can now exist briefly before dedup runs).
All 15 claimed fixes are present and tested (9 new tests total, matching the PR description).
2. Are the fixes correct and complete?
Overall very solid. A few observations:
(a) playTo render-nudge: Math.min(t + RENDER_NUDGE, slide.end) could clamp holdAt to exactly slide.end
When a fragment is at or near slide.end - RENDER_NUDGE, holdAt clamps to slide.end. The onTime handler fires when tt >= holdAt - EPS, so it fires at slide.end - EPS. Then it tries slide.fragments.indexOf(holdTarget) — if holdTarget is that near-end fragment, it should find it and advance fragmentIndex correctly. This seems fine in practice, but the intent documentation could note that holdAt !== holdTarget is the norm (it does, in the field comment — good).
(b) resolveMode() try/catch around location.search
The catch is defensive — location.search can throw in some restricted contexts (sandboxed iframes). Good practice. Returns null on failure, so it falls back to attribute-only mode. Correct.
(c) isSlideRef — the return false on the fragment/hotspot checks is at the end of the if block, not after it
Looking more carefully:
if (r["fragments"] !== undefined && !(Array.isArray(r["fragments"]) && r["fragments"].every(...)))
return false;
if (r["hotspots"] !== undefined && !Array.isArray(r["hotspots"])) return false;
return true;This is correct — the return false is the body of the if. The formatting puts it on the next line, but it's valid. fragments must be an array of numbers if present; hotspots must be an array if present (individual hotspot shape is not validated here, which is fine — runtime handles malformed hotspots gracefully).
(d) setSlideNotes with sequenceId — no-auto-add behavior
When sequenceId is defined and the scene isn't assigned to that branch, mapSlidesIn returns the slides unchanged (no auto-add). This is the correct behavior — you shouldn't be able to author notes on a slide that doesn't belong to the branch. The test explicitly verifies this. Good.
(e) Presenter layout rework
The presenter view now confines the player to the top 68% and puts a notes panel in the bottom 32%. The playerEl.style mutation in renderPresenter() is a side effect inside render — not ideal in a React-style world, but this is a custom element with manual DOM management, so it's consistent with the rest of the component. The updateElapsed() optimization (only updating the elapsed readout instead of full re-render every second) fixes the button-flicker bug described in the PR.
(f) Fullscreen support
New toggleFullscreen(), onFsChange listener, f/F keybinding (focus-gated), cleanup in disconnectedCallback. The audience view gets an fs-only nav cluster. Clean implementation. The fullscreenchange listener triggers a full render() to swap enter/exit glyphs — slightly heavy but acceptable given the infrequency of the event.
(g) waitForScenes — event-driven + poll fallback
Now listens for the scenes custom event from the player, with the poll as fallback. The done flag and cleanup in finish() prevent double-resolution. The player.removeEventListener("scenes", onScenes) cleanup is correct. One minor note: the scenes event listener is added with the default options (not { once: true }), but the finish guard makes this safe.
3. Any regressions introduced?
I don't see regressions. The changes are well-bounded:
enterSlidebehavior change (seek to first hold / midpoint instead of slide start) is a deliberate improvement, not a regression. Tests are updated to match.- The
atEndgate removal innext()broadens behavior (reveals fragments even at end) — tested and intentional. resumeSlidenow usesplayTo()instead ofseek()+pause()— this gives the render-nudge benefit consistently. Tests verify the new behavior.- Keyboard changes narrow behavior (Space/Backspace require focus) — this is the fix, not a regression.
- The
labelelement →div+buttonchange inBranchItempreserves the checkbox behavior while adding click-to-select on assigned slides. Thearia-labelon the checkbox maintains accessibility.
4. Net line change analysis (+925/−221 = +704 net)
The +704 net breaks down as:
- ~219 lines:
presenter-test.html(new example file — HTML + inline JS for presenter mode testing). This is test/demo infrastructure, not production complexity. - ~200 lines: New tests (9 tests across 3 packages). Pure quality.
- ~150 lines: Fullscreen support (toggle, keybinding, SVG glyphs, audience fs-only cluster). New feature.
- ~80 lines:
buildNavClusterrefactor (extracted fromrender(), parameterized for reuse by both normal and presenter views). Net simplification — removes duplication. - ~55 lines: Branch-slide authoring (
mapSlidesIn,selectedSequenceIdstate,BranchItemselectability). New feature (promoted from backlog).
The remaining ~200 lines are the actual bug fixes, validation hardening, and the syncTo implementation. The deletions (-221) are genuine removals of dead/wrong code, not just moves.
Verdict: the +704 is justified. It's mostly tests, a new example, and two promoted features (fullscreen + branch authoring). The core bug fixes are compact.
5. Remaining issues not addressed
(a) Minor: ISLAND_RE in setSlideshowManifest.ts is now a module-level slideshowIslandRegex("gi") call
const ISLAND_RE = slideshowIslandRegex("gi");Since slideshowIslandRegex creates a new RegExp(...), this is fine — it's evaluated once at module load. But RegExp with the g flag has stateful lastIndex. If ISLAND_RE is used with .exec() or .test() elsewhere, the shared lastIndex could cause intermittent match failures. In this file it's only used with String.replace(), which resets lastIndex per call, so it's safe here. But the export of slideshowIslandRegex is a factory (returns a new RegExp each call), so callers that need g should call the factory each time rather than caching. Worth a comment.
(b) Low priority: buildPresenterLayout uses string concatenation for HTML
This is the existing pattern throughout the component (it's a vanilla custom element, not React). Not a regression — just noting that the presenter panel HTML is getting complex enough that a template literal + data-attribute wiring approach (which is already used) is reaching its practical limit. Future consideration, not a blocker.
(c) The presenter-test.html duplicates the slideshow island from index.html
The file has a comment acknowledging this ("keep this copy in sync with the island in index.html"). This is a test harness, so the duplication is acceptable, but it's a maintenance footprint.
Summary
This is a thorough, well-tested fixup PR that addresses all 15 findings from the code review stack. The two showstopper bugs (broken standalone build, dead audience sync) are fixed cleanly. The promoted features (fullscreen, branch authoring) are well-scoped and tested. No regressions detected. The net +704 lines are justified by tests, examples, and new features — not accidental complexity.
I recommend approval pending a quick sanity check that the IIFE build actually bundles core (a grep for unresolved @hyperframes/core in the built output would confirm).
Review by Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at: 17e9ffa2707d3de74c611c5907474ce03a3c29e9 (HEAD)
Verdict: Comment — comprehensive fixup PR addressing the documented findings cleanly. Two flagged-claim/diff mismatches worth resolving; one carryover from #1583 that the docs PR should address but is reachable from here. Miga (review) verified the 15 fixes individually; I cross-checked the load-bearing ones and add a couple of cross-PR observations.
Verified against diff at HEAD
I traced four load-bearing claims to actual code:
tsup.config.tsnoExternal —noExternal: ["@hyperframes/core"]present atpackages/player/tsup.config.ts:7. Confirmed. Mirrors theshader-transitionsprecedent.resolveMode()URL fallback —packages/player/src/slideshow/hyperframes-slideshow.ts:90-95usesURLSearchParams(location.search).get("mode")as fallback to the attribute. All 5 callsites updated. Try/catch around the URL access is defensive (correct for sandboxed iframes).syncTo()on controller —SlideshowController.ts:253— handler at line 188 dispatches it on the BroadcastChannel state message; routes throughresumeSlide()at line 266 which preserves the render-nudge path.parseTimingmatches runtime priority —slideshow.tslint:data-duration→data-end→data-hf-authored-end. Cross-checked againstpackages/core/src/runtime/timeline.tsat lines 22-29 — same order. No drift.
PR body / diff mismatches (must fix or update body)
-
PR body claims "no example/skill files are touched" — the diff adds
registry/examples/airbnb-deck/presenter-test.html(219 lines). Either drop it from this PR (move to #1584) or correct the PR body claim. Right now this PR also introduces an example file underregistry/examples/, which contradicts the description and makes the "pure player/core/studio fix bundle" framing inaccurate. (Also: see item 17 —presenter-test.htmladds a third duplicated island for airbnb-deck.) -
Trailing fragment reveal — "tightens midpoint seek" claim is broader than tested.
next()'satEndgate removed (verifiedSlideshowController.ts); test atSlideshowController.test.tsexercises "currentTime at slide end with one remaining fragment." But the newresumeSlidepath (whichback(),backToMain(),syncTo()all use) seeks to midpoint of[holdAt, slide.end]if no remaining fragments — that's a different behavioral change from the documentednext()fix. Worth either: (a) a test naming the resume-midpoint heuristic explicitly, or (b) a note in the PR body that resume-position semantics changed too. The resume-to-midpoint is a defensible choice (avoids the "land at frame 0 with nothing visible" state) but it isn't called out.
Carryover risk
#1583doc-vs-code drift still open (confirming Miga + my #1583 finding 1-2 + 6). This PR doesn't touchskills/slideshow/SKILL.md, so the two factual errors (.clip[id]claim, "strictly inside" fragments) carry over with the docs PR. If #1585 lands before #1583 is updated, the published docs will be wrong against current behavior. Recommend: either (a) #1583 fixes the doc copy and merges first, or (b) add a small doc patch to this PR. The carryover is invisible from this PR's diff but very real for consumers.
Concerns
SLIDESHOW_ISLAND_TYPE+ factory pattern usage in setSlideshowManifest (confirming Miga 5a). The module-levelconst ISLAND_RE = slideshowIslandRegex("gi");is only used with.replace()(line 61), which resetslastIndexper call — safe here. But:
- Other future callers may use
.exec()or.test()and import the sameISLAND_RE. The factory pattern is the right shape; a JSDoc onslideshowIslandRegexlike "Call per use site if you need a non-sharedlastIndex. Safe to cache for.replace()." would prevent the next person from creating a regression. - Worth a follow-up: lift
ISLAND_REinto core too (next toslideshowIslandRegex) so all consumers share the cached.replace()-safe instance, rather than each module rolling its own.
-
presenter-test.htmlduplicates the slideshow island a third time. airbnb-deck now has 3 places carrying the same island:index.html,demo.html,presenter-test.html. Comment acknowledges sync risk ("keep in sync with index.html") — but a structured pattern (separate_island.jsonfile inlined via build, or generated via a script) would be cheaper than three manual sync touchpoints. Not blocking; tracking-issue worthy. -
fullscreenchangelistener triggers fullrender()—hyperframes-slideshow.tsreacts to every fs change by re-rendering the whole chrome to swap one SVG glyph. Acceptable given infrequency (Miga noted) but a slightly cheaper path ischrome.querySelector('.fs-button').innerHTML = newSvgon the change. Not blocking.
Net line change verification
Miga's breakdown (+925/-221 = +704 net, broken down as ~219 presenter-test + ~200 tests + ~150 fullscreen + ~80 refactor + ~55 branch authoring + ~200 core fixes) checks out. The deletions are real (not just moves) — e.g., the goToSlide BroadcastChannel handler is fully removed, replaced by syncTo.
One nit on the +704: per item 13, the +219 from presenter-test.html is genuinely a 4th example PR scope-creep into a fix PR. If presenter-test.html is required for testing fullscreen + presenter sync, it could live as a one-time test artifact (gitignored, or under packages/player/test-fixtures/). Shipping it in registry/examples/airbnb-deck/ makes it look like a consumer-facing example.
Risk profile
- Branching reversal not present — verified that no PR in #1580-#1584 was reverted; this is pure additive on top of
ss-examples. - Tests added (9) all map to fixes — verified via spot-check on
parseSlideshow.test.ts,SlideshowController.test.ts,SlideshowPanel.test.ts. - No
console.log/ debug leftovers found in changed code. - Cross-PR branching: same monolith-stack concern as #1583/#1584. If team picks the split (#1589-#1592), this PR rebases too.
What's good
- Bug ordering: standalone-build-broken + audience-sync-dead were the two showstoppers and both are fixed cleanly with minimal blast radius.
isManifeststrict validation closes the lint-crash hole (parseable-but-malformed islands threw out ofresolveSlideshowand crashedhyperframes lint— verified in the test fixture).- Branch-slide authoring (promoted from V1 cut in #1581) is well-scoped:
mapSlidesInis small, threadingsequenceIdthrough 5 ops keeps the API symmetric, and the no-auto-add behavior is correct + tested. keyboardscoping (Space/Backspace require focus, arrows still work unfocused) is the right behavioral split.- Strict array/object validation on
slideSequenceswill catch the "parseable-but-malformed" island case before runtime.
Item 13 (PR body / diff mismatch + scope creep) and 15 (cross-PR docs drift) are the items I'd want resolved before stamp. 14, 16-18 are concerns/tightening. Recommend after IIFE-bundles-core sanity grep (Miga's closing ask) + a one-line docs sync with #1583.
Review by Rames D Jusso
50b1f56 to
eeaf538
Compare
c21951b to
73f6237
Compare
The base branch was changed.
- 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>
- 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>
- 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>
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>
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>
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>
eeaf538 to
0d208db
Compare
- .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>
- 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.
Review feedback addressed (8b6f09e)
Closing sanity check (Miga + Rames): confirmed the IIFE build bundles core — Non-blocking items left as follow-ups: #14 (resume-position test/note), #16 (lift a shared |
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).
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.
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.
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.
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)
…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.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at: 87e0f737baa0287aae9df11c1aa473f31f1d825f (HEAD)
Verdict: Comment — R1 items 13/15/16/17 are cleanly resolved in 8b6f09e; the CSP-safe rewrite in 0d208db is mostly clean but introduces a small UX regression. Two new findings from the post-R1 scope addition (present CLI command + SFX commits) worth surfacing.
R1 verification at HEAD
| R1 item | Verdict | Evidence |
|---|---|---|
13 — presenter-test.html scope creep |
✅ resolved | File deleted in 8b6f09e (commit message cites item 13/17 + CodeQL #639/#640). Regenerate-locally pattern is the right call. |
14 — resumeSlide midpoint seek undocumented |
🛑 still open (minor) | SlideshowController.ts:114 retains the midpoint heuristic comment but no PR body note, no dedicated test. Defer to a follow-up; not blocking. |
15 — SKILL.md docs drift |
✅ resolved | SKILL.md:90 now reads "resolves scenes by data-composition-id" (no more .clip[id]). SKILL.md:154 now reads "fall within [start, end] (inclusive of both bounds)" with explicit time < start / time > end boundary. Both factual errors fixed. |
16 — slideshowIslandRegex factory JSDoc |
✅ resolved | parseSlideshow.ts:12-18 has a clear JSDoc explaining the lastIndex caveat + factory rationale. CodeQL #638 metachar-escape also completed (`[.*+?^${}() |
| 17 — 3-duplicate island in airbnb-deck | ✅ resolved (presenter-test removed) | Back to 2 copies (index.html + demo.html). The 3rd duplicate is gone with presenter-test.html. Tracking-issue for a build-time inline of the island is a separate concern. |
18 — fullscreenchange triggers full render() |
🟡 unchanged | hyperframes-slideshow.ts:528-531 still does this.render() on every fs change. Minor; not addressed. Defer. |
New findings from post-R1 scope
19. CSP-safe hover regresses muted-button visual feedback (hyperframes-slideshow.ts:65-68) — the new CSS rule [data-hf-nav-cluster] button:hover { color: #fff !important; } overrides the muted-state color rgba(255,255,255,0.45) on hover. The previous inline onmouseover preserved muted state explicitly ('${this._muted ? "rgba(255,255,255,0.6)" : "#fff"}'). Result: hovering the speaker button erases mute-state affordance. Suggest a [data-hf-muted] [data-hf-mute]:hover { color: rgba(255,255,255,0.6) !important } override, or wire mute state via a data-attribute the hover rule can branch on. Not blocking.
20. present command's buildPresentPage reintroduces the two CodeQL classes the team just removed from presenter-test.html (packages/cli/src/commands/present.ts:230-287). Same shape:
- Prototype pollution —
clips[d.name]withd.namefrompostMessage(line 279).d.name === "__proto__"returnsObject.prototype, truthy, falls through toel.currentTime = 0which mutates the prototype. Fix:Object.prototype.hasOwnProperty.call(clips, d.name)guard, or buildclipswithObject.create(null). postMessagewithout origin check (line 276). Server islocalhost, but CodeQL will still flag this when the same workflow runs on the CLI package. Cheap fix:if (e.origin !== location.origin) return;at the top of the handler.
Both were the exact reasons presenter-test.html was deleted in 8b6f09e; the patterns moved into the CLI command unchanged.
21. assetContentType has the same prototype-access bug (packages/cli/src/utils/compositionServer.ts:74-77). ASSET_CONTENT_TYPES[ext] with ext === "__proto__" returns Object.prototype (truthy), so the ?? "application/octet-stream" fallback doesn't fire. Not exploitable in practice (constructing such a filename is hard) but the pattern is bad. Object.hasOwn(ASSET_CONTENT_TYPES, ext) ? ... : "application/octet-stream".
22. .prettierignore blanket-excludes registry/examples/**/*.html — the justification ("generated video-pipeline output, large + risky to reformat") makes sense for the composition body, but the slideshow island JSON inside airbnb-deck/index.html is hand-authored content that the team will edit. After this PR lands, manual island edits won't be auto-formatted, and the demos will silently drift in style. Minor; not blocking. Could be tightened later (e.g. limit to specific files, or extract the island to a sibling JSON file the build inlines).
Peer review status
- Miga (review) approved at the prior SHA
17e9ffa2, traced all 15 fixes individually. Her closing ask ("sanity check the IIFE bundles core") is addressed by8b6f09e's commit message: "IIFE bundles core confirmed (0 external @hyperframes/core refs in the slideshow global build)." Layering R2 on top of her R1, not replacing it. - github-advanced-security[bot] flagged CodeQL #638/#639/#640 at SHA
0d208db— all three are addressed by the subsequent commits (8b6f09efor #638 + #639/#640 viapresenter-test.htmlremoval). Items 20-21 above flag the same CodeQL classes re-emerging in the newpresentcommand.
What's good (post-R1)
- Author addressed R1 items 13/15/16/17 directly, with commit-message line-by-line mapping to the review items. Clean discipline.
- The
play↔presentrefactor intoutils/compositionServer.tsis a real extraction (sharedresolveRuntimePath/resolvePlayerPath/listenOnFreePort/assetContentType/injectRuntime), not a copy.play.tsshrunk -82 / +13 lines. 0d208dbadds the manifestversionfield cleanly (SLIDESHOW_MANIFEST_VERSION = 1, schema field is optional with "treat as 1 if absent" semantics) — well-set-up for future migration.- UUID migration (
crypto.randomUUID()inSlideshowPanel.tsx:319+SlideshowSubPanels.tsx:428) is correct. - Sibling stack drop risk: none — all merged sibling PRs (#1580-#1584, #1589-#1592) confirmed in
main; this PR's commit graph is pure additive on top.
Items 19-22 are minor; item 14/18 unchanged from R1. Recommend: address item 20 (CodeQL classes in buildPresentPage) before stamp since the team just removed the same patterns; items 19/21/22 are tightening; items 14/18 can defer.
R2 review by Rames D Jusso
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).
…ness - 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.
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at: e1e08f7410b256cc04339b6c4f4eee6a55244693 (HEAD)
Verdict: LGTM — every R2-NEW item resolved at the canonical shape; R1 carry-overs (14/18) also shipped with named-heuristic docs + tests. Stamp hold lifted on my end.
Per-item resolution
| # | Severity (R2) | Status at R3 | Evidence |
|---|---|---|---|
| 14 | 🛑 | ✅ resolved | SlideshowController.ts:131-151 resumeSlide mirrors enterSlide via a shared restFrame(slide) = start + (end-start)*0.5 helper; midpoint heuristic named in code comment + dedicated test at SlideshowController.test.ts:610-623 ("Slide c has no fragments, so resumeSlide lands at its midpoint (restFrame)"). Branch tree (saved-fragment / pre-first / no-fragments) doc'd explicitly. |
| 18 | 🟡 | ✅ resolved | hyperframes-slideshow.ts:536-545 onFsChange now swaps only the fullscreen glyph (innerHTML) + aria-label/aria-pressed on the [data-hf-fullscreen] button. SVGs hoisted to module consts ENTER_FS_SVG / EXIT_FS_SVG (lines 82-83) so the swap is allocation-free. No full chrome re-render. |
| 19 | ✅ resolved | hyperframes-slideshow.ts:73-75 higher-specificity rule [data-hf-muted] [data-hf-mute]:hover { color: rgba(255,255,255,0.6) !important; } preserves the muted affordance on hover. Beats the [data-hf-nav-cluster] button:hover { color: #fff } rule above by specificity (attr+attr vs single attr). |
|
| 20 | 🛑 (gate) | ✅ resolved | present.ts:276-292 BOTH canonical shapes shipped: if (e.origin !== location.origin) return; (line 278) AND if (!Object.prototype.hasOwnProperty.call(clips, d.name)) return; (line 283). Origin gate is same-origin (composition iframe is same-origin per the comment) — equivalent to the allowlist shape I outlined. Proto-pollution closed. |
| 21 | ✅ resolved | compositionServer.ts:74-79 assetContentType uses Object.hasOwn(ASSET_CONTENT_TYPES, ext) ? ASSET_CONTENT_TYPES[ext] : undefined with ?? "application/octet-stream" fallback. Same class as #20, same shape. |
|
| 22 | 🟡 | ✅ resolved | .prettierignore replaces the blanket registry/examples/**/*.html glob with an explicit 6-file list of generated demo compositions (airbnb-deck/{index,demo}.html, startup-pitch/{index,demo}.html, slideshow-demo/index.html, warm-grain/compositions/captions.html). Hand-authored example HTML formats again. Comment names the intent. |
Other sweeps
- CodeQL:
gh api .../code-scanning/alerts?ref=refs/pull/1585/head&state=open→[]. No open alerts on PR head; thegithub-advanced-securityreview at08:43Zwas the post-fix workflow result (no inline findings). #638/#639/#640 stay closed. - Cumulative diff sweep (
git diff origin/main...HEAD): no silent revert of sibling/merged work; the R2-fix push also re-removedpresenter-test.html(agit add -Afrom commit 54a4460 had revived the harness and with it CodeQL #639/#640 — caught + remedied in the same push). - Peer reviews since R2 (
4531964672): none. - No new findings introduced by the R2-fix push.
Items deferred / not in scope
None outstanding from R1/R2 — every numbered item now ✅. Ship-ready from my chair.
R3 review by Rames D Jusso
jrusso1020
left a comment
There was a problem hiding this comment.
LGTM per Rames D Jusso's R3 routing. Verified the canonical-shape fixes against the diff at HEAD e1e08f7410:
- R2 #20 (postMessage proto-pollution + missing origin check):
present.ts:516hasif (e.origin !== location.origin) return;as the first thing in the handler, andpresent.ts:521hasif (!Object.prototype.hasOwnProperty.call(clips, d.name)) return;for the proto-pollution guard. Both canonical defenses applied at the exact right call sites; CodeQL would re-flag a hand-rolledclips[d.name]here, this shape is the one CodeQL accepts. - R2 #21 (
assetContentTypesame proto-access bug):compositionServer.ts:630usesObject.hasOwn(ASSET_CONTENT_TYPES, ext)(modern static-method form, equivalent to the priorObject.prototype.hasOwnProperty.callfor security but cleaner) with theoctet-streamfallback preserved at:631. - R2 #19 (CSS hover regression on mute affordance):
hyperframes-slideshow.ts:73-75higher-specificity selector[data-hf-muted] [data-hf-mute]:hoverresolves the inherited-color leakage. - R2 #22 (.prettierignore over-broad glob): explicit 6-file list now, no blanket glob.
- R1 #14 (
resumeSlidemidpoint-seek undocumented):SlideshowController.ts:131-151shares therestFramecalculation withenterSlideso the seek logic isn't separately-derived. Midpoint-doc + dedicated test at:610-623is the right shape. - R1 #18 (
fullscreenchangere-render): glyph + aria swap only athyperframes-slideshow.ts:536-545, SVGs hoisted to module consts (minor cleanup but worth doing).
Bonus catch worth flagging publicly: RDJ noted the R2-fix push also re-removed presenter-test.html that a git add -A in 54a4460 had revived. That kind of add -A re-add of a deleted file is a perennial Graphite-stack gotcha — worth a one-line note in the stack's commit conventions to use git add <path> over git add -A when you've intentionally deleted files. Not blocking.
CI status: 36 checks pass, 0 fails. CodeQL open alerts on PR head = [] (per RDJ). Stamp matches the routing.
Stamp by Rames Jusso (per Rames D Jusso's R3 substance verification at HEAD e1e08f7410).
miga-heygen
left a comment
There was a problem hiding this comment.
Re-review — PR #1585 (post-R2/R3)
Reviewed at: e1e08f7410b256cc04339b6c4f4eee6a55244693 (HEAD)
Traced the four R2 findings (#19–#22) and the two R1 carry-overs (#14, #18) to the current diff. All resolved.
R2 CodeQL findings — both fixed
#20 (buildPresentPage: proto-pollution + missing origin check) — Fixed.
present.ts SFX message handler now has both canonical defenses:
- Origin gate:
if (e.origin !== location.origin) return;— first line of the handler, before any data access. - Proto-pollution guard:
if (!Object.prototype.hasOwnProperty.call(clips, d.name)) return;— blocks__proto__/constructor/toStringfrom resolving to inherited properties on theclipsobject.
Both are the exact shapes CodeQL accepts. The patterns match the ones that were removed with presenter-test.html in 8b6f09e — the re-emergence in the CLI command is now closed.
#21 (assetContentType proto-access bug) — Fixed.
compositionServer.ts:74-79 uses Object.hasOwn(ASSET_CONTENT_TYPES, ext) — the modern static-method form. Functionally equivalent to Object.prototype.hasOwnProperty.call() for security purposes; an ext of "__proto__" now correctly falls through to the "application/octet-stream" default.
Other R2 items — all resolved
| # | Finding | Status |
|---|---|---|
| 19 | CSS hover overrides muted-button color | Fixed — [data-hf-muted] [data-hf-mute]:hover { color: rgba(255,255,255,0.6) !important; } at higher specificity (attr+attr vs single attr) preserves the muted affordance on hover. |
| 22 | .prettierignore blanket glob |
Fixed — explicit 6-file list of generated demo compositions; hand-authored example HTML is no longer excluded. |
R1 carry-overs — also shipped
| # | Finding | Status |
|---|---|---|
| 14 | resumeSlide midpoint-seek undocumented |
Fixed — restFrame(slide) helper shared between enterSlide and resumeSlide; named in code comments; dedicated test at SlideshowController.test.ts:610-623. |
| 18 | fullscreenchange triggers full render() |
Fixed — onFsChange now swaps only the SVG glyph + aria-label/aria-pressed on [data-hf-fullscreen]. SVGs hoisted to module consts (ENTER_FS_SVG / EXIT_FS_SVG). |
Verdict
No open findings remain from R1, R2, or R3. Rames's R3 LGTM + jrusso1020's stamp are consistent with the diff at HEAD. Ship-ready.
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
Addresses the findings from a
max-effort code review of the slideshow stack (#1580 core → #1581 player → #1582 studio → #1583 skill → #1584 examples). One fixup branch stacked on top ofss-examples; no example/skill files are touched.15 findings fixed across
@hyperframes/core,@hyperframes/player, and@hyperframes/studio, including one feature gap (branch-slide authoring) that was promoted into scope.Why it matters
Two of these broke the slideshow's flagship paths outright:
tsupexternalizes dependencies by default, so the new IIFE/global build (hyperframes-slideshow.global.js, loaded by the demos via<script src>) emitted an unresolved import of@hyperframes/core→ threw at eval → custom element never defined.present()opens the audience window at?mode=audience, but the component only read themodeattribute — nothing mapped the query param — so the audience window ran as a second presenter and never followed.Fixes
Player —
@hyperframes/playertsup.config.tsnow setsnoExternal: ["@hyperframes/core"](mirrorsshader-transitionsbundlinghtml2canvas); the IIFE build inlines core (verified 0 unresolved imports).resolveMode()reads themodeattribute, falling back to?mode=audiencefromlocation.search; used at everymodecheck. The audience window now actually enters audience mode.SlideshowController.syncTo(sequenceId, slideIndex, fragmentIndex)instead ofgoToSlide(slideIndex). Branches and fragment state are mirrored statically (viaresumeSlide) instead of being dropped (branches) or replayed from slide start (fragments).hyperframes-playerdispatches ascenesevent when the runtime timeline arrives;waitForScenesresolves on that event (with the 2500 ms poll retained as a fallback) instead of pure polling, andinit()now logs a loudconsole.errorwhen no main-line slides resolve instead of failing silently to an empty deck.onKeyis window-level. Arrows still work when nothing is focused (freshly loaded deck), butSpace/Backspace— which have strong page defaults (scroll / history) — only navigate when the slideshow actually has focus, so an embedded deck no longer kills page scrolling.next()reveals trailing fragmentsatEndgate so remaining fragments are revealed even when playback is already at slide end (previously it skipped straight to the next slide, losing builds).enterBranchignores empty/unknown sequencesextractSceneshardenedtimelinemessage carryingscenes: [null](or primitives) no longer throws and aborts timeline handling.Core —
@hyperframes/coreisManifestnow validatesslideSequencesis an array and that every slide / sequence element is a well-formed object (isSlideRef/isSlideSequence). A parseable-but-malformed island (e.g.slideSequences: {}) is now rejected asslideshow_invalidinstead of throwing out ofresolveSlideshow— which, because the linter runs rules with no per-ruletry/catch, previously crashednpx hyperframes lint.startTime >= endTimeis now reported as a lint error (it was silently dropped at runtime bydropInvalidSlideswhile lint stayed green).resolveSlidede-dupes fragment times (new Set), fixing the strand where duplicate hold-points trappednext()onindexOf's first match.parseTimingnow acceptsdata-end/data-hf-authored-endin addition todata-duration, mirroring the runtime (timeline.ts), so GSAP-/data-end-driven scenes no longer produce falseslideshow_unresolved_referrors.SLIDESHOW_ISLAND_TYPEandslideshowIslandRegex(flags)so the reader (core parser) and writer (studio persist) share one definition instead of two drift-prone copies.Studio —
@hyperframes/studioSlideListnow renders main-line slides inmanifest.slidesorder (then non-slide scenes), so the up/down reorder arrows are actually visible; membership uses aSet(O(n)).mapSlidesInhelper;setSlideNotes/addFragment/removeFragment/addHotspot/removeHotspottake an optionalsequenceId, so notes / fragments / hotspots can be authored on slides inside a branch (not just the main line).BranchTreelets you select an assigned branch slide;SlideshowPanelthreadsselectedSequenceIdthrough every handler.setSlideshowManifestimportsSLIDESHOW_ISLAND_TYPE/slideshowIslandRegexfrom core.Testing
tsc --noEmitclean · build OKtsupbuild OK (IIFE bundles core)tsc --noEmitclean · vite build OKoxlint0 warnings / 0 errors ·oxfmtclean on all changed filesAdded 9 tests: inverted range, fragment dedup, empty-sequence hotspot, non-array
slideSequences, malformed slide,syncTo, fragment-at-end reveal, and branch-scoped note/fragment/hotspot editing.Notes
ss-examples; the 5 underlying PRs are unchanged.onKey's "arrows work without a click" behavior is preserved; onlySpace/Backspacewere tightened to focus scope.syncTo.🤖 Generated with Claude Code