Skip to content

fix(studio): the shared-row follow-ups - #3215

Merged
vanceingalls merged 421 commits into
mainfrom
wa-18i-row-fixes
Aug 13, 2026
Merged

fix(studio): the shared-row follow-ups#3215
vanceingalls merged 421 commits into
mainfrom
wa-18i-row-fixes

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

The follow-up fixes shared lane rows needed once they were real:

  • cap every clip bar on an expanded row, so a long clip cannot paint past it
  • stop a shared row's clips swallowing the pointer events the lane needs
  • mount the lanes on the row itself, so they scroll and collapse with it
  • re-analyse a carve when a voice it was listening to changes
  • expect what deleting a clip already wrote, rather than a stale snapshot

🤖 Generated with Claude Code

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid final slice. All five fixes are well-scoped, individually tested, and I found no correctness bugs.

No blocking issues.

The five fixes:

  1. Clip bar capping: clipBarHeight computed per-row, applied to ALL clips on expanded rows. Prevents waveform painting over envelopes.
  2. Pointer-event forwarding: Lane wrapper gets pointer-events-none; only the SVG and value inputs opt back in. Prevents clip bands from swallowing lane interactions.
  3. Lane mounting on the row: TimelineAutomationLaneSlot extracted from footer prop into a sibling div at row level. Fixes lane remount on selection change (which destroyed hover state and in-flight gestures).
  4. Carve re-analysis on voice deletion: survivingSources effect watches the timeline, prunes deleted voices from carve.sources, triggers re-analysis. Confirmed safe against infinite loops — the filter is idempotent, and analysis can't modify the voice list.
  5. diskContent for optimistic-concurrency: saveProjectFilesWithHistory gains diskContent so the write expectation matches what remove-element put on disk, not the pre-mutation baseline.

Non-blocking:

  • survivingSources.join(" ") uses space separator while candidateIds three lines below uses "\u0000". If an element ID ever contained a space, the effect could miss a real change. Element IDs are space-free by convention so near-zero risk, but inconsistent.
  • SPEC.md says "Status: SPEC — not implemented" but the system IS implemented across the preceding 8 PRs.
  • aria-controls multi-ID is correctly handled per WAI-ARIA spec.

LGTM — ship it.

Review by Miga

vanceingalls and others added 24 commits August 13, 2026 00:49
The 20s margin (already once widened for the same reason) is timing out in
CI's full-monorepo Test run — the resetModules()+fresh-import render this
test needs is uncached and competes with every other package's test suite
for the same worker pool, and the same test passes in well under 2s
standalone. Went to 45s rather than re-tuning to whatever number happens to
clear the current CI load, since that number moves every time CI gains a
package.
Two auto-apply effects both fire when sourceOptions.length === 1: the
multi-candidate effect only guards length === 0, so a single candidate
passes it too, and the single-candidate effect passes its own guard right
after — both compute the same sources list and both call setCarve, so the
common case (one narrator, one bed) triggered two decodes, two FFT runs, and
two concurrent attribute writes for one decision.

The multi-candidate effect now defers to its sibling for exactly one
candidate, which already has its own detailed handling for that case.

Review by Miga (PR #3213).
* feat(core): carve against every voice over a bed, always dynamically

A bed usually runs under a whole sequence — a narrator, an interview answer, a
second presenter — and carving against one of them left the others fighting it.
`source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's
clock before anything is measured. That is what keeps one analysis sufficient: the
chain is fixed, so there is no per-voice filter to switch between, and bands drawn
from all the speech there is with envelopes that rise wherever any of it happens
answer the actual question — where and when is speech masking this bed.

Summed rather than averaged: two people talking at once mask more than either
alone. Audio before the bed starts is dropped rather than folded in at zero, since
it plays over nothing and shifting it would put a cut where there is no voice.

`dynamic` is gone. A fixed depth thins the bed through every pause, and once both
have been heard there is no reason to want it, so every carve follows the speech.

Two helpers the panel and the headless script now share instead of each carrying a
copy — two definitions of "what does this name suggest" drift, and then the two
disagree about which track is the voice:

- `classifyAudioName` reads a track's kind from its id and filename together.
  `unknown` is deliberately common: treating an unrecognised name as "not a voice"
  would hide the one track somebody needs to pick.
- `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten
  duration counts as unbounded, not zero — refusing a clip whose length the
  composition leaves to the media would drop the commonest case there is.

Files written before this still load: a single `source` reads as a one-voice list,
a stored `dynamic` is ignored, and an absent attribute means the defaults whole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration

\b treats `_` as a word character, so \bbed\b never matched bed_01,
music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap —
an underscore-separated bed classified as "unknown" and could end up offered
as its own carve source. Replaced the short hints with a boundary that
actually excludes letters and digits on both sides.

clipsOverlap computed end = start + duration without guarding sign, so a
negative duration put end before start — an interval that does not describe
anything, and one specific case showed it silently dropping a real overlap
(a shorter, earlier broken end rejected a clip that genuinely contained the
point). Duration clamps to zero instead: a clip cannot un-play time, and a
zero-length clip at its start is the sane reading of "duration nobody wrote
down as positive."

Review by Miga (PR #3212).

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#3212 (accidentally squash-merged into this branch instead of main) changed
HfCarveSettings from a single `source` + `dynamic` toggle to a `sources`
list with dynamic mode removed outright — the multi-voice UI consumer that
goes with that shape lands in the very next PR, so this branch was left with
a type that no longer matched its own code.

Minimal port, not the multi-voice redesign that PR does properly: the
"Listen to" picker and analyse() treat sources[0] as the one voice this UI
still understands, and every dynamic-mode branch (the automated envelope
lanes, the toggle, the checkbox) is gone along with the field — a carve is
now always the static value the analysis computes, matching what the type
change made permanent. Test suite trimmed the same way: the automation-lane
and toggle tests covered behavior that no longer exists.
# Conflicts:
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx
#	packages/studio/src/components/editor/propertyPanelFxSection.tsx
# Conflicts:
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx
#	packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx
#	packages/studio/src/components/editor/propertyPanelFxSection.tsx
#	skills-manifest.json
#	skills/hyperframes-audio/SKILL.md
#	skills/hyperframes-audio/references/attributes.md
#	skills/hyperframes-audio/scripts/carve.mjs
Base automatically changed from wa-18h-shared-rows to main August 13, 2026 09:47
Fixes a pre-existing aria-owns mismatch surfaced by the merge: the
property-lane header cell used the combined caret id (clip lanes +
track automation lanes) while the row wrapper's aria-owns still used
the single un-combined id, so they never matched.
@github-actions

Copy link
Copy Markdown

Fallow audit report

Found 35 findings.

Duplication (30)
Severity Rule Location Description
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:120 Code clone group 1 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:156 Code clone group 1 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:391 Code clone group 2 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:431 Code clone group 2 (8 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:482 Code clone group 3 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:552 Code clone group 3 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:669 Code clone group 4 (15 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:682 Code clone group 5 (15 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:699 Code clone group 4 (15 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:712 Code clone group 5 (15 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:880 Code clone group 6 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:888 Code clone group 6 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:1061 Code clone group 7 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:1219 Code clone group 7 (6 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:1363 Code clone group 8 (11 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/components/editor/propertyPanelAudioFxGroup.test.tsx:1418 Code clone group 8 (11 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/useElementLifecycleOps.ts:115 Code clone group 9 (13 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/usePersistentEditHistory.test.ts:235 Code clone group 10 (11 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/hooks/useTimelineEditing.ts:412 Code clone group 9 (13 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.test.ts:586 Code clone group 11 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.test.ts:595 Code clone group 12 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.test.ts:633 Code clone group 11 (9 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/Timeline.test.ts:643 Code clone group 12 (7 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/TimelineAutomationLaneSlot.test.tsx:61 Code clone group 13 (19 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/player/components/automationLaneData.test.ts:211 Code clone group 13 (19 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/studioFileHistory.test.ts:106 Code clone group 14 (15 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/studioFileHistory.test.ts:136 Code clone group 15 (19 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/studioFileHistory.test.ts:170 Code clone group 15 (19 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/studioFileHistory.test.ts:175 Code clone group 14 (15 lines, 2 instances)
minor fallow/code-duplication packages/studio/src/utils/studioFileHistory.test.ts:208 Code clone group 10 (11 lines, 2 instances)
Health (5)
Severity Rule Location Description
major fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:202 'setCarve' has CRAP score 97.0 (threshold: 30.0, cyclomatic 19)
minor fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:275 'carvedAgainstBy' has CRAP score 37.1 (threshold: 30.0, cyclomatic 11)
major fallow/high-crap-score packages/studio/src/components/editor/propertyPanelAudioFxGroup.tsx:485 'analyse' has CRAP score 97.0 (threshold: 30.0, cyclomatic 19)
minor fallow/high-cognitive-complexity packages/studio/src/player/components/TimelineTrackHeader.tsx:409 'TimelineTrackHeader' has cognitive complexity 19 (threshold: 15)
minor fallow/high-crap-score packages/studio/src/utils/studioFileHistory.ts:77 '<arrow>' has CRAP score 31.6 (threshold: 30.0, cyclomatic 10)

Generated by fallow.

@vanceingalls
vanceingalls merged commit df57ad4 into main Aug 13, 2026
40 of 46 checks passed
@vanceingalls
vanceingalls deleted the wa-18i-row-fixes branch August 13, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants