Only onPointerDown was wired, so dragging the knob/track only ever
committed the initial click position — nothing tracked the pointer
after that. Uses the Pointer Capture API (setPointerCapture on
pointerdown, onPointerMove while captured, release on pointerup) so
the value follows the cursor continuously during a drag, matching how
the legacy native <input type="range"> control behaves for free.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The track's visible line was only 2px tall, and pointerdown was bound
directly to that thin element, making it hard to grab. The hit area is
now 20px tall (a wrapping div) with the visible line rendered as a
thin decorative child, centered inside it — the ratio math only reads
left/width so click accuracy is unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a fast (120ms) CSS entrance animation for flat inspector accordion
group headers/body, gated to the group actually toggling (not derived
from remounting alone) to avoid a Chromium reflow quirk that otherwise
replays the animation on untouched collapsed siblings.
Collapsed group headers render in fixed, non-scrolling document flow
above and below the open group; only the open group's own content
scrolls, in a dedicated region. Also fixes the flat inspector footer's
missing background.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The flat inspector rendered its new Style/Grade groups AND the legacy
ColorGradingSection/StyleSections components a second time below them,
visibly doubling every control. Remove the now-redundant legacy render
call sites (and their now-unused imports) from PropertyPanelFlat.tsx;
those components stay intact for the legacy (flag-off) PropertyPanel.
FlatTextSection's multi-field branch (textFields.length > 1) now renders
FlatTextLayerList (Task 5) + the existing single-field FlatTextFieldEditor
for the active field, tracked via new local activeFieldKey state that
resyncs (useEffect) when the active field disappears from props. This
retires the legacy TextSection delegation entirely for that case; the
TextSection import is removed from propertyPanelFlatTextSection.tsx since
nothing else in the file referenced it.
Also updates propertyPanelSections.test.tsx and PropertyPanel.test.tsx,
which exercised/documented the old multi-field-falls-back-to-legacy-
TextSection behavior in comments and test titles — reworded to describe
the new flat path (assertions were already compatible and still pass).
Flag for reviewer: hideOwnHeading on the legacy TextSection component
(propertyPanelSections.tsx) was added in an earlier plan specifically for
this now-removed call site. It has no remaining consumer after this task
lands (PropertyPanel.tsx's legacy caller doesn't pass it). Left in place
per brief instruction — not deleting unilaterally, since that's a scope
decision for whoever reviews this task.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review proved the existing test didn't catch a broken stopPropagation
by temporarily removing it and confirming the suite still passed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of the pin-aware group list refactor flagged the test name
claiming the group "closes" on unpin — it doesn't assert that, and
structurally the group re-opens (togglePin never touches openGroupId).
Retitled to describe only the return-to-stack behavior actually tested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reads/writes the per-element-kind pinned-groups map added to
studioUiPreferences in the prior task, read-modify-writing the whole
map since writeStudioUiPreferences only shallow-merges top-level keys.
Also adds a not-busy click assertion for the Apply button so
onApplyToScope is exercised end-to-end, not just its disabled state.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also hoists the pointerdown-drag/reset-click test scaffold shared by the
new Roundness tests and the existing Contrast/Exposure tests into helpers
(findRowByText/dragSliderTrack/clickSliderReset), and exempts pre-existing,
branch-inherited fallow findings unrelated to this task (TextFieldEditor
complexity from earlier Text-inspector commits; test-scaffold duplication
across four flat-inspector-series test files from Plans 2-4) via
.fallowrc.jsonc, per this repo's established convention for line-shift/
inherited findings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of Task 6 flagged the exposure/other-key scale ternary as always
resolving to 100 on both arms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up to 684ec4e87: that fix corrected the seek target for Layout's
keyframe gutter via deriveElementTiming, but currentPct — which drives
KeyframeNavigation's diamond active/inactive state and prev/next arrow
targeting — still used PropertyPanel's naive elStart=0/elDuration=1
basis. For an element with animations but no explicit data-duration,
seeking to a keyframe's real absolute time no longer lit that
keyframe's diamond as active, and the prev/next arrows targeted the
wrong keyframes.
Thread currentTime into PropertyPanelFlat (swapping the now-redundant
currentPct prop 1-for-1, so PropertyPanel.tsx's line count is
unchanged) and recompute currentPct there from the same
deriveElementTiming basis already used for the seek fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Whole-plan coherence review (Plan 3a Layout + Plan 3b Motion) found that
Layout's keyframe gutter and Motion's Timing row independently derived an
element's start/duration and disagreed whenever an element had animations
but no explicit data-duration: Motion correctly inferred the range from the
element's GSAP tweens, while Layout's keyframe gutter fell back to a naive
`duration ?? 1`, so clicking a keyframe percentage in Layout could seek to a
different absolute time than what Motion's Timing row displayed.
Extract deriveElementTiming (propertyPanelFlatTimingDerivation.ts) as the
single shared basis both paths now consume: FlatTimingRow (Motion) and
PropertyPanelFlat's own elStart/elDuration (Layout's keyframe gutter and 3D
Transform block). PropertyPanelFlat now recomputes this basis itself from
its own element/gsapAnimations props instead of trusting the parent's naive
value, so PropertyPanel.tsx (and its legacy non-flat panel) is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Thread the Layout-group values through PropertyPanel -> PropertyPanelFlat
and add the third FlatGroup to the one-open/pin accordion (unconditional,
matching legacy Layout). Default-open Layout when neither Text nor Style
applies.
Fix the Flex double-render: the legacy StyleSections still renders its own
Flex Section, and the new flat Layout group renders its own LayoutFlexBlock.
Add an additive optional hideFlex prop to StyleSections and pass it on the
flat path so Flex renders exactly once (from the flat Layout group). Non-flat
callers omit it and are unchanged.
Extract the shared onLivePreviewProps closure into gsapLivePreview.ts (it was
duplicated inline in the legacy path) so PropertyPanel.tsx stays within the
600-LOC studio gate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
## What
First PR in a 6-PR stack migrating Studio's right-panel property inspector from an always-expanded stacked-sections layout to a "flat" one-open-at-a-time accordion. This PR lays the foundation: the `STUDIO_FLAT_INSPECTOR_ENABLED` feature flag, the accordion primitives (`FlatRow`, `FlatSegmentedRow`, `FlatGroup`, `PinnedZoneDivider`), the flat identity header/footer, and the first migrated group — Text.
Stack: #2120 (this) → #2121 (Style) → #2122 (Layout+Motion) → #2123 (Media) → #2124 (Grade) → #2125 (Pinning + multi-field Text).
## Why
The legacy inspector renders every applicable section expanded at once, which gets unwieldy as an element accumulates properties across style/layout/motion/media/grade. The flat redesign shows one section at a time (plus pinned sections), matching a design handoff mock.
## How
- `FlatGroup` owns the one-open accordion state (`openGroupId`/`onToggleOpen`) and pin affordance (`onTogglePin`), styled per the design mock.
- `FlatTextSection` is the first migrated group and the reference implementation every later group's task followed for the `isOpen`/`onToggleOpen`/`onTogglePin`/`summary` wiring pattern.
- Includes a same-PR bugfix (found via live browser testing, not caught by any automated test): the Text `FlatGroup` was rendering unconditionally regardless of element type (empty for non-text elements), and the multi-field fallback doubled the "Text" heading. Fixed by gating on `isTextEditableSelection` and adding a `hideOwnHeading` prop to the legacy `TextSection` fallback.
- Entirely gated behind `STUDIO_FLAT_INSPECTOR_ENABLED` (default off) — the legacy panel is untouched and remains the default for all users.
## Test plan
- Every primitive and the Text group have dedicated Vitest suites using real DOM events (click/pointerdown) with exact assertions, not shallow snapshots.
- Manually verified in Studio via live browser testing against the design mock (this is what caught the bugfix above).
- Full monorepo test suite green; `oxlint`/`oxfmt` clean; this repo's `fallow` complexity/duplication gate passes.
- [x] Unit tests added/updated
- [x] Manual testing performed
- [ ] Documentation updated (not applicable — internal Studio UI behind an off-by-default flag)
## What
Already-scaffolded HyperFrames projects pin an exact `hyperframes@X.Y.Z` in `package.json` scripts (`init.ts`), frozen at scaffold time — and the update-available notice is suppressed on non-TTY shells, exactly how agents invoke the CLI. So a large tail of projects sits on months-old versions, invisible and stuck, never seeing later render/router fixes.
The pin itself is deliberate (a video project should re-render identically across CLI versions), so this PR keeps it and instead gives projects a path off it:
1. **`rewriteProjectPinnedScripts` / `readPinnedHyperframesVersions`** (`packages/cli/src/utils/projectPin.ts`) — pure helpers that rewrite/read `hyperframes@<version>` pins in a `package.json` scripts object.
2. **`hyperframes upgrade --project [dir]`** — bumps a project's pinned scripts to npm-latest in one command (`--check` reports the delta without writing, `--json` for `{ changed, from, to, path }`).
3. **`printStalePinNotice`** — a throttled (once/24h), non-TTY-visible notice (unlike the existing update notice, which non-TTY shells suppress) that fires when the *current* project's pin is stale, pointing at `upgrade --project`.
4. **Skill instruction** (`skills/hyperframes-cli/SKILL.md` + reference) — tells agents to check for and bump a stale project pin via the **unpinned** `npx hyperframes@latest upgrade --project`.
5. **Scaffold templates** (`CLAUDE.md`/`AGENTS.md`) — new projects get the same guidance baked in from day one.
## Why
Only the global skill (piece 4) invoking the unpinned `npx hyperframes@latest upgrade --project` (piece 2) reaches projects that are *already* frozen on an old pin — a project pinned to an old CLI version never runs the new notice code (piece 3) or sees the new template text (piece 5). Those two are forward-only: they stop the bleed on projects scaffolded from here on, but the skill instruction is the only lever that reaches the existing backlog.
## How
`isSafeVersion` was extracted out of `updateCheck.ts` into its own `safeVersion.ts` module — `projectPin.ts` needs it and `updateCheck.ts` needs `projectPin.ts`'s `readPinnedHyperframesVersions`, so keeping `isSafeVersion` in `updateCheck.ts` created a circular import between the two files.
## Test plan
- [x] Unit tests added/updated (`projectPin.test.ts`, `upgrade.project.test.ts`, `updateCheck.stalepin.test.ts`) — TDD, all passing
- [x] Full `packages/cli` suite green (132 files / 1651 tests), `tsc --noEmit` clean, `bun run build` succeeds
- [x] Manual smoke test: `upgrade --project --check --json` reports the delta without writing; `upgrade --project` rewrites the pinned scripts in place
- [ ] Documentation updated — skill + scaffold templates updated in this PR; `CLAUDE.md`/`AGENTS.md` template parity verified with `diff -q`
Deferred (left for a separate decision, not in this PR): `npm deprecate hyperframes@"<0.7.53"` — reaches frozen projects with no skill loaded, but is a live, hard-to-reverse action against published packages that needs an explicit human call on cutoff version + message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Review round on PR #2442:
- miguel (blocker): the cross-file eligibility rule only guarded the
dom-edit tripwire; recordResolverParity and
recordAnimationResolverParity ran before wrongCompositionFile at
every cutover surface, so cross-file ops still emitted false
element_not_found (id present in the OTHER file's source passes the
runtime-node filter) and polluted the attempt denominator. The rule
now lives in one shared isCrossFileEdit guard applied by all three
entry points, wired with { targetPath, compositionPath } at all six
sdkCutover call sites (timing, timing-batch, gsap add/set/remove,
keyframe chokepoint, delete).
- Rames (race): the disk-truth read is now dispatched SYNCHRONOUSLY in
the same prologue as the miss check, before control returns to the
caller whose cutover persist writes the same file moments later — a
post-write read would see a remove op's target legitimately gone and
misclassify it as a genuine divergence. Sync reader throws become
rejections (IIFE), not exceptions into the swallow-all catch.
- Rames (parse failure): openComposition failure inside the disk check
now fails open as sourceReadFailed (unparseable source is not ground
truth), instead of the outer catch dropping the divergence event
entirely.
recordResolverParity's source check extracted to checkHfIdInSource
(complexity gate), mirroring checkAnimationIdOnDisk's error discipline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two resolver-shadow noise classes from production telemetry:
- Cross-file guard (0.7.41: 479 false element_not_found from ONE
session): the dom-edit tripwire ran for edits targeting a different
file than the session models. The cutover gates already decline these
(wrongCompositionFile); the tripwire now skips the same way — no
event, no attempt, since the op structurally cannot cut over.
- Stale-session disambiguation (0.7.48: 53 animation_not_found across
keyframe ops): the GSAP panel derives animationIds from the CURRENT
on-disk script every render, while the session's parsed id space
dates from the last reload. Position edits shift every
selector-method-position id, so panel ops landing before the reload
target ids the session has never seen. Parser id-space parity was
verified across legacy/acorn read/write paths (9 script shapes) —
the ids agree; the session is just behind. On a miss with a reader
wired, recordAnimationResolverParity now re-parses the on-disk file:
a hit there = stale session (suppress); a miss there = genuine
divergence, tagged diskChecked so the dashboard can trust the class.
Attempt-counter machinery moved to sdkResolverAttempts.ts (600-LOC
studio file gate); re-exported from sdkResolverShadow for API compat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flat inspector's Text FlatGroup rendered unconditionally, showing an
empty "Text" header for non-text elements (image, video, etc). Gate it on
isTextEditableSelection(element) so it disappears entirely when there's no
text to edit.
Also, the legacy multi-field TextSection (used as a fallback when an
element has 2+ text fields) rendered its own internal "Text" heading
nested inside the new flat Text FlatGroup, producing a doubled "Text"
heading. Add a hideOwnHeading prop to TextSection (default false, so its
other — legacy, non-flat — call site is unaffected) and pass it from
FlatTextSection's fallback path.
Miguel R4 blocker on #2359: my R3 fix at renderOrchestrator only updated
the observability copy, leaving the authoritative captureForceScreenshot
local at compileResult.forceScreenshot (false for auto→software). The
frameCapture side clamped its own local and correctly routed screenshot,
but downstream orchestrator code overwrote observability back to
beginframe from the still-false local at two sites:
- Parallel-stream label at renderOrchestrator.ts:2293 mis-labelled the
stream as 'beginframe' when actual capture was 'screenshot'.
- capture_strategy telemetry at renderOrchestrator.ts:2440-2450
overwrote the earlier observability correction, so the final
captureMode observation flipped back to 'beginframe' while the
engine actually captured screenshot.
Fix: extract the clamp into a caller-facing helper
applyConcreteGpuScreenshotClamp(current, resolvedGpuMode, cfg) that
returns the (possibly-promoted) new boolean. Callers assign it back to
their authoritative local, so routing + telemetry + strategy code read
one value.
Changes:
- packages/engine/src/config.ts: new exported
applyConcreteGpuScreenshotClamp; delegates to
shouldClampToScreenshotForConcreteGpu but computes the caller's
final value, not just the clamp decision. Reads the programmatic
opt-out from cfg.forceScreenshotExplicitlyOptedOut. Idempotent on
already-true input.
- packages/engine/src/index.ts: export the new helper.
- packages/engine/src/services/frameCapture.ts: replace the inline
OR expression with applyConcreteGpuScreenshotClamp.
- packages/producer/src/services/renderOrchestrator.ts: assign result
into the AUTHORITATIVE captureForceScreenshot local (was updating
only observability). Downstream parallel-stream label at :2293 and
capture_strategy telemetry at :2440-2450 now read the corrected
value.
Tests: 6 new caller-level cases for applyConcreteGpuScreenshotClamp
covering the exact matrix Miguel called out:
- resolved software + default false → promotes to true (screenshot)
- resolved software + programmatic opt-out → stays false (BeginFrame)
- resolved hardware + default false → stays false
- resolved software + already-true → stays true (idempotent)
- resolved software + env PRODUCER_FORCE_SCREENSHOT=false → stays false
- resolved software + undefined cfg → promotes to true (frameCapture path)
Local: 67/67 engine config tests pass (was 61). oxfmt clean.
Miguel R3 blocker on #2359: the runtime helper only checked the env opt-out
(PRODUCER_FORCE_SCREENSHOT=false), silently defeating the documented
programmatic escape hatch (overrides.forceScreenshot === false) on the
browserGpuMode:'auto' → software probe path. At the concrete-resolution
site the boolean forceScreenshot === false is ambiguous between default
and explicit opt-out — resolveConfig sees the provenance but the runtime
helper does not.
Fix: persist provenance on the resolved config.
- New INTERNAL EngineConfig field forceScreenshotExplicitlyOptedOut, set
by resolveConfig when EITHER env or programmatic explicit-false is
present. Purpose-documented in the type as 'not intended to be set by
callers'.
- shouldClampToScreenshotForConcreteGpu gains an opts.programmaticOptOut
parameter; returns false early when set. Env stays as the third arg
(backward compatibility with existing tests).
- frameCapture.ts and renderOrchestrator.ts pass
config.forceScreenshotExplicitlyOptedOut through at both call sites, so
the auto→software probe path preserves the same escape hatches as
literal browserGpuMode:'software'.
New tests: 5 additional cases across the helper (programmatic opt-out
alone; programmatic beats missing env) and resolveConfig provenance
(programmatic sets flag; env sets flag; neither leaves it undefined).
Local: 61/61 engine config tests pass (was 56).
Matches the isPathInside pattern in fileServer.ts. The helper is called from
webmAlphaCheck.ts's own webmAlphaAdvisory (same file), but fallow doesn't
count intra-file consumption or test-file imports. Suppress the false
positive rather than dropping the direct unit test — the 1/2/3-frame
byte-count gate + per-frame stride logic is load-bearing enough to warrant
independent tests, not just coverage through the outer advisory function.
Addresses Miguel's R1 blockers:
1. `browserGpuMode: "auto"` that runtime-probes to software slipped past the
`resolveConfig` clamp — that clamp only sees the pre-resolve string. Add
`shouldClampToScreenshotForConcreteGpu(resolvedGpuMode, currentForceScreenshot, env)`
in `packages/engine/src/config.ts` and apply it at BOTH concrete-resolution
sites:
- `packages/engine/src/services/frameCapture.ts`: downgrades `preMode`
from "beginframe" to "screenshot" when resolved GPU is software (respects
`PRODUCER_FORCE_SCREENSHOT=false` env opt-out), fixing the routing.
- `packages/producer/src/services/renderOrchestrator.ts`: updates
`captureObservability.forceScreenshot` (and thus `captureMode`) at the
same call site, fixing the observability truth on the auto → software
case.
2. New unit tests in `config.test.ts`:
- Documents the auto-branch gap (resolveConfig leaves auto as
forceScreenshot=false — the runtime companion closes it).
- 5 branch tests on `shouldClampToScreenshotForConcreteGpu` covering
software / hardware / already-forced / env-opt-out / non-"false" env
values.
Full suite: 56/56 pass.
Scope narrowing on Blocker 2: the distributed rendering path at
`packages/producer/src/services/distributed/plan.ts:753-754` and
`renderChunk.ts:462-466` explicitly hardcodes `browserGpuMode:"software",
forceScreenshot:false` post-resolveConfig and stays outside this PR's
invariant boundary. `compileStage` may still flip it to true for alpha
formats, but generic MP4 distributed renders on SwiftShader hosts remain
BeginFrame. That's a separate architectural cleanup (needs its own
behavior-change trace); the PR body now scopes the invariant to the
in-process CLI/orchestrator path.
Address Miguel's R1 blocker + Rames/Miga's testability nit:
- The `-frames:v 3` decode samples AT MOST 3 frames; a legitimate 1- or
2-frame WebM (256 or 512 bytes) was returned as `undefined` (probe
failure), silently skipping the advisory even when every available
pixel was opaque. Accept any positive whole-frame byte count ≤ 768
(multiples of 256), distinguishing successful short-EOF from partial/
malformed decode.
- Export `sampledAlphaIsFullyOpaque` and add 11 direct tests covering:
3/2/1-frame opaque decodes → true; transparent pixel at pos 0 or
final byte → false (guards the alpha-byte stride); non-frame-multiple
/ over-3-frame / zero byte counts → undefined; execFileSync throw →
undefined; findFFmpeg missing → undefined; and one args-shape guard
pinning the load-bearing `-c:v libvpx-vp9` before `-i` (without which
the default decoder silently discards VP9 alpha and the whole check
would false-positive on genuinely-transparent WebMs).
- Update advisory wording from "3 sampled decoded frames" to "every
sampled decoded pixel" so the message is honest for short WebMs.
18/18 tests pass locally under `vitest run`.
Review feedback on #2358: the batch-miss RENDER_FAILED in
runAssetImportMany (asset.ts) throws the same typed error as
client.ts's single-node renderNode, but wasn't labeled — so
cli_error.endpoint would silently come back undefined for the
flow that most heavily exercises /v1/images.
cli_error had no way to tell which figma REST call (images, files_nodes,
variables_local, styles, ...) actually hit RATE_LIMITED/FORBIDDEN/etc, so
the dashboard could see failures spike but not which call caused them.
FigmaClientError now carries a low-cardinality endpoint label (never the
raw fileKey/nodeId), threaded through to cli_error's endpoint property.
Extends webmAlphaCheck.ts (from #2044) with a pixel-level decode probe.
After the tag check passes, decodes 3 sampled frames via
`ffmpeg -c:v libvpx-vp9 -pix_fmt rgba -f rawvideo` at 8x8 and emits a
distinct advisory if every alpha byte reads 255.
#2044 detects the "tag absent" failure mode (ffprobe shows no
`alpha_mode` in stream tags). It doesn't catch a stricter case reported
on CLI 0.7.56 / Windows 11: ALPHA_MODE=1 present but BlockAdditional
alpha side data empty. Under current logic webmAlphaAdvisory sees
`alphaMode: true` and stays silent, so the render ships as opaque
without any signal.
The -metadata:s:v:0 alpha_mode=1 push is a muxer directive that some
ffmpeg builds write unconditionally, independent of whether libvpx-vp9
emitted the alpha plane. Tag presence is necessary but not sufficient
evidence of preserved alpha.
Advisory text names both possibilities (opaque composition OR silent
alpha drop) plus the concrete workaround (png-sequence + prores repack).
Fast path (no tag or missing tag) is unchanged. Probe adds ~1s per
WebM render only when the tag says alpha.
When `browserGpuMode === "software"`, set `forceScreenshot = true` in
`resolveConfig`. Explicit opt-outs (`PRODUCER_FORCE_SCREENSHOT=false`
or `overrides.forceScreenshot === false`) are honored.
This is defense-in-depth on top of the existing platform gates:
1. Linux + software (SwiftShader host) skips BeginFrame, avoiding the
compositor stall on shader-heavy frames under CPU raster (same
motivation as the closed PR #822).
2. `renderOrchestrator`'s reported `captureMode` field is derived from
`cfg.forceScreenshot ? "screenshot" : "beginframe"` — without this
clamp it misreports `"beginframe"` for the actual screenshot capture
on darwin + software.
3. Any new BeginFrame or drawElement entry point that forgets to gate
on GPU mode still routes to screenshot here.
Does NOT fix SwiftShader-on-darwin text-rasterization artifacts (an
ANGLE-SwiftShader issue on macOS text — the fix there is to use
`--browser-gpu`, which routes to `--use-angle=metal`).
Adopt the fake-timer pattern the sibling "recovers when a crashed reclaimer
leaves both lock directories" test in the same file already uses. Without
fake timers, if the dynamic `import("./manager.js")` beat between
`installFsMocks({ initialMtimeMs: Date.now() })` and the `withInstallLock`
call exceeds `staleMs` (50 ms on a busy shared runner with `vi.resetModules()`
per beforeEach), the new immediate-stale short-circuit added in #2328 fires
on iteration 1, breaks out before `waitedMs` reaches `waitNoticeMs=20`, and
no "Waiting for another hyperframes process" warn ever emits — the assertion
at `manager.test.ts:428` (`expected false to be true`) then fails.
Under fake timers, `Date.now()` is frozen at the mtime seed, so the lock
stays non-stale across the dynamic import and the wait-notice branch
observes real polling; `vi.advanceTimersByTimeAsync(staleMs + pollMs * 5)`
then drives the loop past both the wait-notice threshold and the stale
deadline so the reclaim + acquisition still resolves.
Test-only change; no production-code diff. Verified 27/27 in
`packages/cli/src/browser/manager.test.ts` under `vitest run`.
The `hyperframes feedback` convention only prompted for a free-text
`--comment` "with the failing composition pattern and what you tried".
Agents dutifully filed vague reports (blank CJK text, mid-run exit, 4K
timeout) with no error string, no failure-mode, and — critically — no
published composition, so none could be reproduced or root-caused.
Two additions to the CLI skill:
- Lead bug reports with `--file-issue` (+ `--dir`), which publishes a
minimal repro of the project to a public URL. A comment alone almost
never lets a maintainer reproduce; the composition is what does.
- Give the `--comment` a concrete bug checklist: exact error string
verbatim + whether output was produced / fell back / hard-exited; the
isolated trigger; exact command + HF_*/PRODUCER_* env; frame/timestamp +
visual defect. Drop the "repeat env" ask (the CLI already attaches it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveVideoCaptureBeyondViewport gated Chrome's beyond-viewport screenshot
path to hardware-GPU captures, to skip the full-surface software
re-rasterization tax. But without beyond-viewport, the viewport-bound
capture clips the bottom edge of any frame containing a native video
surface (the same #1094 tall-portrait guard the alpha capture paths already
hardcode) — leaving ~87 bottom rows black.
This hit two cohorts: software-GPU macOS/Linux hosts, and — worse — EVERY
distributed chunk render, which hardcodes browserGpuMode "software", so the
whole distributed fleet shipped video renders with a black bottom band.
Reporter confirmed forcing resolveVideoCaptureBeyondViewport=true fixes it.
Correct output wins over the software perf optimization: enable
beyond-viewport for any render with a native video surface, regardless of
GPU mode. Drops the now-vestigial browserGpuMode parameter (and its type)
and updates both call sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>