Skip to content

feat(studio): audio gain on G, speed in clip names, additive selection and partner clamp (8/8) - #4821

Merged
vanceingalls merged 20 commits into
mainfrom
aov/08-gain-names-multiselect
Oct 2, 2026
Merged

vanceingalls merged 20 commits into
mainfrom
aov/08-gain-names-multiselect

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The Premiere polish pass:

  • clip names show speed as [150%] / [ramp]
  • one fx badge per clip
  • an Audio Gain dialog on G
  • Link for any unlinked video + audio pair
  • Shift/Cmd-click additive selection
  • audio kept inside its partner video while dragging and trimming
  • Look previews on hover
  • Unlink leaves nothing selected

Changes

  • studio:
    • [NN%] / [ramp] clip names, and a single fx badge in place of the per-tool and link badges.
    • AudioGainDialog on G (one undo step). Shortcuts are kept out of the modal, and it refuses while recording.
    • Link for exactly one unlinked video + one unlinked audio (same source → sync origin).
    • Shift/Cmd-click add to the selection.
    • An audio clip with a partner video (linked or same sync origin) can't move or trim outside the video span, and the drag actor holds at the clamp.
    • Look ▸ previews on hover and commits on click.
    • Unlink (menu, ⌘L, Alt-edit) clears the timeline selection.

Testing

Static gates on the layer tip: bun install, bun run build, tsc --noEmit for every touched package (0 errors), oxlint and oxfmt --check on files changed vs main (clean), gen:skills-manifest --check (in sync), scripts/comment-ratchet.mjs (ok).

  • Scoped vitest: sdk 574, studio 6434. All pass. This tip's tree equals the full feature branch.
  • Mutation check: removing the unlink clearSelection fails the new "unlink leaves nothing selected" test.

Notes

  • Root cause of "after unlinking, trimming the audio still moves the video": setResizingClip seeds the trim gesture from the whole selection. After Unlink both clips stayed selected, so upstream's multi-select group resize trimmed both. Unlink now clears the selection. Trimming with both clips deliberately selected still trims both, by design (upstream's restored multi-select group resize).
  • Studio's Shift-click time-range popover is replaced by Shift/Cmd additive selection (Premiere convention). Flagged for a product decision.
  • Part of the audio-on-video stack. See the bottom PR for the overview.

🤖 Generated with Claude Code

Before

before

After

after

Before: G does nothing and the sped-up clip reads Fast. After: G opens Audio Gain and the clip reads Fast [150%].

@vanceingalls
vanceingalls added this pull request to stack #4822 October 1, 2026 01:24

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed the #4821 delta at exact 49a5b9b0e2341e69d63d83eed3e65552b43aef23 against stacked #4820 ca563f775fcd9f0adba2c468f538f1dca6d6d037. Two inline holds: SDK linked-duration edits can push an offset audio partner past the video endpoint; and Studio's detached-audio containment runs before collision placement (and only for the grabbed element), so the final moved audio can escape the video. Current-head Comments is also red on five raised-comment-share files. #4820 and earlier stack layers remain held; this review is of this incremental delta only. No local browser, tests, merge, or release action. — Jerrai

Comment thread packages/sdk/src/engine/mutate.ts Outdated
const endsTogether =
Math.abs(pStart + (partner.duration ?? 0) - (gStart + (grabbed.duration ?? 0))) <
ALIGN_EPSILON_S;
if (edit.duration !== undefined && endsTogether) timing.duration = edit.duration;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When linked clips share an end but have different starts, copying the grabbed clip's duration moves the partner's end. Example: video [2,8] (duration 6) and audio [3,8] (duration 5), both linked. session.setTiming(video, {duration:4}) applies video [2,6] and then this sets audio duration 4 → [3,7], so the audio extends one second past its video even though this delta promises it stays inside. The new session test checks linked start movement; the duration test uses equal starts, which cannot catch the offset. Preserve the common end (derive partner duration from its own start and the new end) and add an unequal-start test.

const previewStart = video
? clampStartIntoBounds(floored, drag.element.duration, video)
: floored;
const placement = resolveDropPlacement(drag, clientY, previewStart, nextMove.track, ctx, group);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The detached-audio/video bound is applied to previewStart, but resolveDropPlacement can move it outside that bound to avoid another clip, and line 200 returns placement.start without reclamping or rejecting it. Concrete same-lane case: video [10,30], its audio [12,20] (duration 8), obstacle [20,30]; dragging audio toward 22 clamps to 22 (the last bounded start), but nearest-free placement prefers 30 over 12, and timelineClipDragCommit.ts:240–244 persists audio [30,38]. Also, the bound is computed only for drag.element, not audio carried as a passenger of a multi-selection; dragging selected text and audio moves the audio via resolveMultiSelection without its own bounds. Please constrain final placement and each held audio mover, and test collision and non-audio-grabbed multi-select cases.

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 49a5b9b to fb5c6b6 Compare October 1, 2026 05:46
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Edit accuracy: 953 passing here, 953 on the base branch

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • move-hold-xpercent-r30-root-z200: error / tracking 0.02, pressJump 0, drop 0, reload 0.04, render 0.03, undo true, teleport true / tracking 0.02, pressJump 0, drop 0, reload 0.04, render 0.03, undo true, teleport true

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from fb5c6b6 to a64a9ad Compare October 1, 2026 06:39

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-reviewed #4821 at exact a64a9ad1a50bb07930492362c083ba1a9d77f3f4 against current #4820. The unequal-start/shared-end duration repair and the Studio collision/passenger clamp address the previous examples; Comments and SDK/Studio checks are green. One boundary in the duration repair still breaks the linked containment promise (inline): a valid short video-duration edit can end before the audio starts, the computed partner duration becomes nonpositive and is silently skipped, leaving the old audio span beyond the video. Please define and test the crossing behavior. Current-head Studio/player captures and file-size checks also fail; no live UI or audio validation was performed. Other stack layers remain separately held. No merge or release action. — Jerrai

Comment thread packages/sdk/src/engine/mutate.ts Outdated
const partnerEnd = (partner.start ?? 0) + (partner.duration ?? 0);
if (Math.abs(partnerEnd - grabbedEnd) >= ALIGN_EPSILON_S) return undefined;
const duration = (edit.start ?? grabbedStart) + edit.duration - partnerStart;
return duration > 0 ? duration : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This repairs unequal-start durations only while the new shared endpoint remains after the partner's start. With linked video [2,8] and audio [3,8], setTiming(video,{duration:0.5}) makes the video [2,2.5], computes partner duration -0.5, and silently returns undefined; applySetTiming commits the video but leaves the audio [3,8] outside it. No SDK rejection runs before the first mutation. Please reject this edit atomically, remove/retime the partner under an explicit policy, or otherwise preserve the linked/containment invariant, with a regression at this crossing boundary.

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from a64a9ad to 73fc900 Compare October 1, 2026 08:20
@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 73fc900 to e8968fb Compare October 1, 2026 09:47

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At exact e8968fba6e0f41d34a6a1ba5898b466a57632723, the previous short-duration linked-partner defect is repaired by refusing a nonpositive resulting partner duration before mutation, and the new SDK regression covers it. A separate linked-timing defect remains: packages/sdk/src/engine/mutate.ts:320–329 stores grabbed baselines in grabbedByLink keyed only by the raw data-link ID. linkedPartnerIds in linkedTiming.ts:23–33 correctly scopes identical link IDs to their host composition, but the later baseline lookup discards that scope. If the root video/audio are both [2,8] and a nested composition's video/audio are both [0,4], all tagged lk-1, one setTiming targeting both videos with duration 0.5 records only the nested video's baseline for that key. The root audio's end 8 differs from nested grabbed end 4, so partnerDuration returns undefined; the root video ends at 2.5 while its linked audio remains [2,8]. Key the grabbed baseline by scoped group/target rather than link string alone, and add a two-composition same-link-ID multi-selection regression. Current-head SDK/file-size/Comments jobs ran green, but Studio and player captures ran red for missing Before/After evidence; both issues must be addressed (or evidence explicitly waived) before approval. No local tests/live playback/merge/release. — Jerrai

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-reviewed the #4821 delta at exact ddf395550503797608b89506c238d02e712c6282 against stacked #4820 fc172418abe4561eb1a8ea53c5c2c855b9873047. The scoped-baseline repair closes my prior two-composition/same-link-ID finding: grabbedBaselines now keys by host scope and link, and the two SDK tests exercise one multi-target edit plus atomic crossing refusal through the real session API. SDK/Studio tests, file-size, Comments and typecheck ran green. I did not run local tests or live Studio/audio validation.

No approval yet. The Studio and player captures check ran red: the PR body lacks Before/After sections with media, and no per-PR visual waiver has been given. Test (support) also ran red on 11 dark-theme contrast-baseline assertions; Studio: edit accuracy gate ran red on 27 rotate cases relative to the current main baseline (557→530). The stack base predates changes to both baseline files on main, and neither file is touched in this #4821 delta, which supports baseline drift rather than implicating this scoped-link fix—but these checks have not passed at this head. Rebase and rerun/resolve them before landing; provide the captures or explicit per-PR visual waiver for approval. The earlier CHANGES_REQUESTED review remains in force. Other stack-layer verdicts and release/pin gates are separate; no merge or release action. — Jerrai

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from ddf3955 to 9492558 Compare October 1, 2026 18:56
@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 9492558 to eeb2d91 Compare October 1, 2026 20:02

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed #4821 at eeb2d91 against #4820 e9f8992e. The previous root/nested reused-lk-1 baseline collision is repaired: grabbed windows are keyed by both nearest composition node and link, matching partner lookup. The inline/file-scope tests discriminate that path; main’s fade/hotkey/menu changes remain composed. No new #4821-specific blocker found, but parent #4818’s ambiguous SDK partner-ID hold keeps the stacked feature unready. This layer’s required Studio/player captures check is FAILED without Before/After evidence or waiver, so no approval. Edit-accuracy and Comments pass; no local tests or playback. — Jerrai

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from eeb2d91 to b37cdcf Compare October 1, 2026 22:27
vanceingalls added a commit that referenced this pull request Oct 1, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from b37cdcf to 9c5da19 Compare October 1, 2026 23:33

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At the final #4820-based head, mutate.ts computes linked timing before mutation and refuses nonpositive partner durations; grabbed baselines are keyed by composition scope and link. Session regressions cover root/child reused-link multi-target edits and atomic crossing refusal. The 68-file layer patch survives the rebase; three concurrent keyboard/typing-target edits are preserved, and captures pass. This closes my prior duration and scope holds, not pending CI or release gates. — Jerrai

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 9c5da19 to ac1616e Compare October 2, 2026 00:10

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exact ac1616e2 rebase review: all 68 layer paths and patch hunks match the old approved parent→head delta. The only concurrent layer-path edit is Studio index.ts; the additive WebMCP and audio-meter exports coexist without dropping either. The linked-timing grabbed baselines remain scoped by composition and link, and invalid partner duration is refused before mutation with the targeted regression retained. This is code approval for this layer only; #4815's viewport-performance hold and pending CI keep the stack from being called ready. No merge/release action. — Jerrai

@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from ac1616e to 0805245 Compare October 2, 2026 02:06
@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 0805245 to 2cb717e Compare October 2, 2026 02:12
Base automatically changed from aov/07-sync-origin-menu-groups to main October 2, 2026 06:37
vanceingalls and others added 20 commits October 2, 2026 06:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dges

Grey with no effects, white with any. The tooltip lists look, voice, other
fx, crop and ducking; a click opens the clip menu. The volume badge stays
when volume is off 100%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set Gain to, Adjust Gain by, Normalize Max Peak to, Normalize All Peaks to
and Normalize loudness to -16 LUFS, with the selection's peak amplitude read
from the peak map. Writes data-volume on every selected clip with sound as
one undo step. The clip menu's Normalize loudness becomes Audio Gain... (G).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… recording

Keys pressed inside an aria-modal dialog no longer reach the global hotkeys,
so Delete on the focused OK button cannot delete the clips the dialog edits.
The multi-clip write refuses during a recording like the single-clip one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Link no longer requires the same file and identical timing, matching Premiere.
Same-file pairs get a sync origin so a trimmed or moved audio shows its
offset; different files link without one. Merge back is disabled until the
pair is in sync. Linked moves keep the offset, and a trim only carries to a
partner whose edge is at the same time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A modifier click on a clip replaced the selection, so a video and an audio
could never both be selected and Link never appeared. Shift, Cmd and Ctrl
now toggle the clip (and its link partners unless Alt) in the selection;
Shift no longer turns a clip click into a time-range popover.

Right-clicking a lone unlinked video or audio offers Link when exactly one
opposite-kind clip from the same file sits on another track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dd in every host

A modifier click passed the toggle as an argument to an optional
onSelectElement call, so hosts without that handler (desktop) never ran it.

An audio clip linked to a video, or sharing its sync origin, now stays
inside that video's span while dragged or trimmed, Alt edits included,
unless the video moves or trims in the same gesture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dragged audio's ghost followed the pointer past the video's end while
the landing stayed clamped; it now stops where the clip will land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Unlink both clips stayed selected, so the next edge trim ran the
multi-select group resize and the former partner still followed. Unlink
from the menu, the shortcut or an Alt edit now leaves nothing selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… duration

Copying the grabbed clip's duration to a partner that starts later pushed the
partner's end past the clip both ended on. The partner's duration is now the
shared new end minus its own start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… placement too

The containment clamp ran before collision placement and only for the
grabbed clip, so a drop pushed past an obstacle, or audio carried along in a
multi-selection, could land outside its video. The allowed shift is now the
intersection over every moving audio, and a placement outside it is
rejected. Link partners are matched within their source file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ain dialog

Normalize moved from the clip menu into the Audio Gain dialog here, so the
volume-lane refusal and the saved/refused/failed checks go with it: a lane
owned clip is refused, and a failed single or multi-clip save rejects instead
of reporting success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s start

With linked video [2,8] and audio [3,8], setTiming(video, {duration: 0.5})
computed a partner duration of -0.5, skipped it, and committed the video alone,
leaving the audio outside its video. The partner edits are now planned before
any mutation; a non-positive partner duration throws ("Linked audio would start
after the new end — unlink or trim the audio first.") and can() reports
E_LINKED_PARTNER_CROSSED, so nothing is written and the undo stack is
untouched. In Studio the refusal fails the cutover (never declines into the
legacy path) and the timeline resize toast shows the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
planLinkedTiming stored each target's timing under its raw data-link id, while
linkedPartnerIds scopes link ids to their host composition. One setTiming naming
a root video and a nested video that both use lk-1 kept only the nested baseline,
so the root audio was skipped and left past its trimmed video. Baselines are now
keyed by host scope and link id, the same rule linkedPartnerIds uses, for both
the partner trim and the crossing refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…osition too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the aov/08-gain-names-multiselect branch from 8896bb3 to 258b833 Compare October 2, 2026 06:37

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fresh exact-head review at 258b833a87f059049494af915c9f079c72c6195c on merged-main 45b8e171e2792c89cc63b4d136de41eae6194b25: I compared the complete prior #4820-head→#4821-head layer (a8a9f431→8896bb3e) with the new main→head layer. Both have 20 commits, 68 paths and +2,392/−392 lines, with identical path/status/stats and zero normalized-hunk or missing-patch differences. All 68 changed-file blobs match the previously approved head; the merged base differs from the old parent in 17 paths, none overlapping this layer. Scoped link baselines, pre-mutation partner-crossing refusal and their tests therefore remain unchanged. Current-head CI still has running jobs with no failure observed; approval is code review only, not CI completion or merge/enqueue authorization. No local tests or live playback by this reviewer. — Jerrai

@vanceingalls
vanceingalls added this pull request to the merge queue Oct 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 2, 2026
@vanceingalls
vanceingalls added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 7090021 Oct 2, 2026
71 checks passed
@vanceingalls
vanceingalls deleted the aov/08-gain-names-multiselect branch October 2, 2026 07:29
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