refactor(studio): split oversized files and raise line limit to 600#1012
Conversation
jrusso1020
left a comment
There was a problem hiding this comment.
Verdict: COMMENT (would-be APPROVE)
Root-cause analysis is sound, fix is minimal and correctly scoped to the one site that needs it. Holding for James's stamp greenlight.
Root cause cross-checked against the issue's repro
Issue #1009 reports: 1080×1920 portrait video, position:absolute; inset:0; width:1080px; height:1920px, bottom 60-100px gap for the first ~37 seconds, macOS Chrome only, regression in v0.6.11 when enableBrowserPool: true became default. The new regression fixture (packages/producer/tests/portrait-video-fullbleed/src/index.html) matches the repro shape exactly. ✓
Tracing the fix:
- Pre-fix at
screenshotService.ts:436-454: usesvideo.offsetLeft/offsetTop/offsetWidth/offsetHeightwithgetBoundingClientRectonly as a fallback. The PR's claim thatoffset*can report stale compositor-dependent values on macOS Chrome under pooled browsers is plausible — these properties are computed from the rendering tree and can lag the CSS layout box when the compositor process is shared across workers and the video decoder is still settling. - Post-fix: uses
getBoundingClientRect()throughout. Returns CSS layout dimensions regardless of compositor state. Position computed asvideoRect.left - parentRect.leftto maintain the offsetParent-relative semantics the consumer expects.
The "fix at t≈37s" timing in the issue is consistent with the decoder-settles-then-offset-becomes-accurate theory.
Substantive observations
-
Linux CI can't reproduce the macOS-only bug — the regression fixture is a guard, not a proof.
Theportrait-video-fullbleedfixture renders correctly on Linux Chrome both pre-fix and post-fix because theoffset*-divergence bug doesn't manifest there. So the fixture validates "the new code path produces correct geometry for portrait full-bleed video on Linux" (a useful regression guard), but doesn't prove the fix addresses the macOS-specific compositor issue. The actual validation has to happen by running the fix against the reporter's repo (dannymarx/hyperframes-issue-repo) on macOS. The PR's test plan already flagsNeeds macOS verification— this is the load-bearing pre-merge check. Worth waiting on before stamping. -
Existing unit test doesn't exercise the divergence either.
screenshotService.test.ts:113-130mocks BOTHoffsetLeft/Top/Width/HeightANDgetBoundingClientRectto return identical values (0,0,1920,1080), then asserts the resulting<img>style matches. With identical mocks, both code paths produce the same output → test passes pre-fix and post-fix. Acceptable for regression safety, but again not a fix-proof. Adding a test that mocks divergent values (offsetHeight: 1820,rect.height: 1920) would actually validate that the post-fix code prefers the rect. Optional. -
offsetParentnull fallback semantics changed.
Whenvideo.offsetParent === null(e.g.,position:fixedvideo, or adisplay:noneancestor), the post-fix code falls back to{ left: 0, top: 0 }for the parent rect, making the replacement<img>viewport-positioned. Pre-fix,offsetLeft/Topalso returned 0 in this case (per spec — null offsetParent collapses offsets to 0), so the<img>landed at (0,0). Forposition:fixedvideo, post-fix is more correct (matches the video's actual viewport position); for hidden video, neither matters. Mostly an improvement, but a real behavior change worth being aware of. -
Transform inclusion is a behavior change too.
getBoundingClientRectincludes CSS transforms;offsetWidth/Heightdoes not. If a composition appliestransform: scale(0.5)to a<video>, pre-fix gave the un-transformed layout box (1080×1920 for the issue's video), post-fix gives the rendered size (540×960). For replacement-frame rendering, post-fix is more correct — we want the replacement to occupy the rendered space, not the un-transformed layout box. Worth a quick mental check on whether any hyperframes patterns scale<video>elements; grep didn't surface any, so this is probably theoretical. -
Math.roundis the right call but introduces sub-pixel rounding.
offsetWidthis integer by spec;Math.round(rect.width)could round 1919.4 → 1919, leaving a 1px gap at the bottom. The bug being reported is 60-100px, so this isn't the cause. But on high-DPR macOS displays where sub-pixel layout is common, this could surface as a faint 1px artifact on different compositions. Minor.Math.ceilfor width/height would be slightly more conservative; doesn't matter for the reported bug.
Sanity checks
- Parity check: grepped
packages/engine/src/services/,packages/producer/src/, andpackages/core/src/for any other site that usesvideo.offsetWidth/offsetHeightfor replacement geometry.screenshotService.tsis the only consumer. No parallel paths to update. ✓ - CI: 15 success / 2 in_progress / 13 cancelled (all the cancelled are superseded reruns of greens). No real failures.
- Mergeable_state:
blocked— waiting on reviews/checks.
Pre-merge ask
The most important verification isn't in this PR — it's running the fix against the reporter's repro on macOS. The fix is theoretically sound and the regression fixture catches future drift on the Linux path, but only a macOS run proves the bug is actually closed. Once Miguel (or someone with a macOS box) confirms the reporter's out.mp4 no longer has the bottom gap, stamp-ready.
— Rames Jusso
517adf6 to
b726727
Compare
0f6d91e to
375633f
Compare
375633f to
e8588b2
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
Verdict: COMMENT (would-be APPROVE with caveats)
Refactor is mostly sound. Net negative line count is a healthy sign and the splits look structurally clean. Holding for James's stamp greenlight; a few transparency / scope-framing observations below — none blocking.
Context note: branch / PR identity changed mid-stream
For future readers of git log: this PR was originally the engine fix for #1009 (SHA 9e1122fc, "fix(engine): use getBoundingClientRect for video frame replacement geometry") that I reviewed on 2026-05-21. Miguel subsequently verified the portrait-video bug isn't reproducible on v0.6.31 (using the reporter's repro repo on macOS), closed #1009 as not-reproducible, and rebased this branch onto a wholly different change — the Studio refactor. The PR title, body, and content are all completely new at the current head.
That's mechanically fine — branch reuse is normal — but it does mean my prior review on this PR-number is orphaned context. Future git log readers seeing the branch name fix/portrait-video-bottom-gap will be confused. Cosmetic, but consider closing this PR and reopening from a fresh branch if you want clean history.
Substantive observations
-
Scope creep relative to the PR description. The body says: "Simplified
App.tsx— reduced component size by cleaning up inline logic." That undersells a real refactor: the change extractsblockBaseOpts(memoized viauseMemo) andblockCallOpts(memoized viauseCallback) to consolidate the 3 inline-constructed options objects at the 3addBlockToProjectcall sites. That's a memoization refactor, not "cleanup of inline logic." Memoization changes React render-lifecycle semantics (downstream useCallback identities, memo'd deps, etc.). Worth a sentence in the PR body acknowledging the real shape.Quick check on correctness: the new
blockBaseOptsdeps[activeCompPath, fileManager.readProjectFile, fileManager.writeProjectFile, fileManager.refreshFileTree, editHistory.recordEdit, reloadPreview, showToast]cover all 7 fields of the new object — good.blockCallOptsreadspreviewIframeRef.current(ref — correctly omitted from deps) andusePlayerStore.getState().currentTime(synchronous, read at call time — correct semantics). I don't see a correctness bug, just a description-mismatch. -
File-size limit raise (500 → 600) lacks explicit justification. The change is real policy:
- Limit raised in both
.github/workflows/ci.yml:469andlefthook.yml. .filesize-allowlist(13 files) deleted entirely.- Pre-PR policy: "500 hard floor + per-file allowlist for known-exceptions."
- Post-PR policy: "600 hard ceiling, no allowlist mechanism."
That's a shift from "strict floor + escape valve" to "looser ceiling + no escape valve." Each shape is defensible; the PR doesn't say which empirical observation drove the 600. Was 600 picked because the largest file remaining after the splits is ~580 lines? Or was 500 always too aggressive? Worth a one-line rationale in the PR body so future maintainers know the basis. (And if the splits actually brought everything to <500, the question is whether to keep 500 + drop just the allowlist — even more conservative.)
- Limit raised in both
-
Some load-bearing comments deleted in
App.tsx. TheuseEffectforstudio_session_startlost its block comment:Fire once per browser tab session — sessionStorage-backed so HMR remounts, route changes, and any future StudioApp remount within the same tab don't refire
studio_session_start.has_projectlets us tell scratch-open from project-context-open.That's exactly the why-non-obvious shape CLAUDE.md asks comments to preserve. Without it, a future reader sees
hasFiredSessionStart() ?: markSessionStartFired()and might not realize the sessionStorage invariant + HMR-remount intent. Recommend restoring it (or moving it adjacent tomarkSessionStartFiredintelemetry/config). Same applies to the// ZERO store subscriptions — this hook never causes re-renders.comment removed fromuseTimelinePlayer.ts:65-66; that one documented a deliberate design choice that's load-bearing for understanding the hook's perf characteristics.
Sanity checks (no concerns)
NLELayout.test.tsremoval: legitimately stale. The test exercisedshouldDisableTimelineWhileCompositionLoading(x) => x— a literal identity function. The helper was inlined at its single call site (NLELayout.tsx:280) and the test removed. ✓timelineAssetDrop.test.ts-1+1: assertion tightened from'<img id="photo_asset"'(partial) to'<img id="photo_asset" data-start="0" data-duration="3" />'(full). The sourcetimelineAssetDrop.tsis unchanged (no diff), so this is tightening a loose existing test, not catching up to a behavior change. ✓- File splits structurally clean:
ShortcutsPanel.tsx(+277) andSpeedMenu.tsx(+83) extract fromPlayerControls.tsx(-362).manualEditsDomPatches.ts(+375) extracts frommanualEditsDom.ts(-340). Imports + type signatures land in the new files; the extracted logic appears intact. Worth a git-patch-id byte-equivalence check during the merge dry run if you want full rigor. useTimelinePlayer.ts-12 lines: pure comment + blank-line removal, no functional changes.- No engine/producer changes — confirmed via file list; consistent with PR body's "those will be a separate PR."
CI
23 success / 2 in_progress / 1 failure. The 1 failure is Fallow audit — same code-quality static-analysis noise we've seen on multiple PRs in this batch; not a real CI failure. The Fallow comment notes the refactor doesn't reduce per-function complexity (PlayerControls CRAP 268, App.tsx CRAP 756) — which is expected, since file splits don't change function cyclomatic complexity. A follow-up "decompose StudioApp further" PR would address that; not in this PR's scope.
Pre-merge asks (all documentation / transparency)
- Update the PR body to acknowledge the App.tsx memoization refactor.
- Add a one-line justification for the 500 → 600 limit raise.
- Consider restoring the sessionStorage/HMR comment block in
App.tsx(and the "ZERO store subscriptions" comment inuseTimelinePlayer.ts).
None block merge on merit. Holding for James's stamp greenlight.
— Rames Jusso
jrusso1020
left a comment
There was a problem hiding this comment.
Amending my prior review — Vai caught two real adds
Verified both items at the PR head:
1. projectId! non-null assertion (App.tsx:197 inside blockCallOpts factory)
const blockCallOpts = useCallback(
(blockName: string) => ({
...blockBaseOpts,
projectId: projectId!, // ← Vai's flag
blockName,
previewIframe: previewIframeRef.current,
currentTime: usePlayerStore.getState().currentTime,
timelineElements,
}),
[blockBaseOpts, projectId, timelineElements],
);Verified all 3 call sites guard at the caller (App.tsx:208/227/234 are all inside if (!projectId) return branches or follow such a guard), so this is runtime-safe today. But Vai's broader point stands: the ! strips the TS-level contract — a future caller who skips the guard would compile cleanly. Easy fix is to take projectId: string as an explicit parameter and have the callers pass it; the runtime guard already exists at every call site, so this is just hoisting the assertion into the type signature.
I read this code in my prior review and noted "no correctness bug" but didn't flag the ! as a smell. Vai's right that "safe today" ≠ "no review concern" when the assertion is enforcing what should be a type-level invariant.
2. Fallow audit CI exits red (30 findings, exit code 1)
I framed this as "Fallow noise we've seen on the batch — not a real CI failure." That was lazy framing. The job does exit 1, the PR's test plan claims "CI filesize check passes with new 600-line limit" without acknowledging the Fallow red, and the findings include items genuinely tied to this PR (the new manualEditsDomPatches.ts:100 buildBoxSizePatches CRAP 224.4 — that's the extracted function, not pre-existing on main).
Vai's framing is fairer: even if the findings are refactor-relocated false positives (the function complexity isn't introduced by the split, just moved), an explicit acknowledgment in the PR body — or an explicit fallow-ignore-with-reason on the load-bearing offenders — is the right shape. Shipping with red CI + silent test plan trains future readers to ignore red checks, which is the failure mode the test plan exists to prevent.
Updated stance
My prior verdict stands but Vai's two items are worth addressing pre-merge, alongside my three documentation asks. The corrected list of pre-merge asks:
- (Vai) Acknowledge the Fallow audit red in the PR body, OR resolve the load-bearing findings (
buildBoxSizePatchesCRAP 224.4 is the clear candidate — it's a new file, so afallow-ignore-next-line complexitywith a one-line reason is the cleanest fix). - (Vai) Replace
projectId: projectId!with an explicitprojectId: stringparameter toblockCallOpts. Hoists the runtime invariant into the type signature. - (Rames) Update PR body to acknowledge the App.tsx memoization refactor.
- (Rames) Add a one-line justification for the 500 → 600 limit raise.
- (Rames) Restore the sessionStorage/HMR comment block in
App.tsxand the "ZERO store subscriptions" comment inuseTimelinePlayer.ts.
None individually block merge, but #1 and #2 from Vai are closer to load-bearing than my doc asks. Aligning my framing with Vai's.
— Rames Jusso
e8588b2 to
d310ecf
Compare
Split PlayerControls.tsx into focused sub-components (SeekBar, WorkAreaOverlay, MuteButton, LoopButton, FullscreenButton, ShortcutsPanel, SpeedMenu) and extracted seek bar drag/progress tracking into useSeekBarDrag hook. Split manualEditsDom.ts patch-builder functions into manualEditsDomPatches.ts with data-driven helpers to reduce duplication and complexity. Extracted per-type reapply helpers from reapplyPositionEditsAfterSeek and factored out identity-matrix check from stripGsapTranslateFromTransform. Raised file-size limit from 500 to 600 lines, removed .filesize-allowlist.
d310ecf to
ea4d920
Compare
Summary
PlayerControls.tsxinto focused sub-components (SeekBar,WorkAreaOverlay,MuteButton,LoopButton,FullscreenButton,ShortcutsPanel,SpeedMenu) and extracted seek bar drag/progress tracking intouseSeekBarDraghookmanualEditsDom.tspatch builders intomanualEditsDomPatches.tswith data-driven helpers (collectInlineStyleOps,collectAttributeOps) to reduce duplication and complexitystripGsapTranslateFromTransform(extracted identity-matrix check) andreapplyPositionEditsAfterSeek(extracted per-type helpers).filesize-allowlistTest plan