Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
221 commits
Select commit Hold shift + click to select a range
0d5abd2
feat(core): audio FX registry
vanceingalls Aug 1, 2026
8e0f63d
fix(core): stop declaring knobs that move nothing
vanceingalls Aug 4, 2026
f596128
feat(core): Web Audio graphs for the FX registry
vanceingalls Aug 1, 2026
c3291f2
fix(core): reverb level, phaser wiring, per-channel dynamics, one-pol…
vanceingalls Aug 4, 2026
3c33413
feat(engine): render audio FX in an OfflineAudioContext
vanceingalls Aug 1, 2026
6562969
feat(core): voiceover carve analysis
vanceingalls Aug 1, 2026
2fc689b
fix(engine): keep the FX render 16-bit, stereo, and correctly sized
vanceingalls Aug 4, 2026
17c654d
fix(producer): report an FX render failure as an audio error
vanceingalls Aug 4, 2026
535146b
test(engine): skip the browser FX render cases when there is no browser
vanceingalls Aug 12, 2026
38a9f6b
chore(engine): suppress the temp-file alert with the reason it is safe
vanceingalls Aug 12, 2026
f810040
feat(core): register the audio-fx-rack canary at 0%
vanceingalls Aug 12, 2026
80adff9
feat(studio): audio FX panel generated from the registry
vanceingalls Aug 1, 2026
92eb6e3
feat(studio): show the Audio FX section on audio tracks
vanceingalls Aug 1, 2026
aa172fb
refactor(studio): lift audioFxSummary out of PropertyPanelFlat
vanceingalls Aug 12, 2026
e3e3a41
feat(studio): put the audio FX rack behind its canary
vanceingalls Aug 12, 2026
153d0d3
feat(core,studio): hear the FX chain in preview, and run the carve an…
vanceingalls Aug 1, 2026
7e85bfb
fix(studio,core): stop parameter drags from restarting playback
vanceingalls Aug 1, 2026
78bce4d
fix(core): register FX worklets before building nodes that need them
vanceingalls Aug 2, 2026
486be53
refactor(studio): split the FX node row out of FxSection
vanceingalls Aug 2, 2026
fcf1a88
refactor(studio): lift the audio FX group out of PropertyPanelFlat
vanceingalls Aug 12, 2026
ab88116
feat(core): automation envelope model for audio tracks
vanceingalls Aug 2, 2026
d80cfde
feat(core): expose the AudioParams behind automatable FX knobs
vanceingalls Aug 2, 2026
e2153f3
feat(core): play automation envelopes in preview
vanceingalls Aug 2, 2026
ca93344
feat(engine): bake automation envelopes into the render
vanceingalls Aug 2, 2026
8b77ca0
fix(core): make the volume lane audible in preview
vanceingalls Aug 4, 2026
e84747c
fix(core): apply chain edits to the running graph
vanceingalls Aug 4, 2026
c000ecc
feat(studio): the geometry and plumbing behind automation lanes
vanceingalls Aug 4, 2026
85649d6
fix(studio): make automationLaneData reviewable, and evict one entry …
vanceingalls Aug 4, 2026
7bf71e8
fix(studio): carry the audio FX attributes onto every timeline row
vanceingalls Aug 4, 2026
4580abc
feat(studio): the automation lane itself
vanceingalls Aug 4, 2026
00a7031
fix(studio): let a track disclose its automation without a tween
vanceingalls Aug 4, 2026
be84bac
feat(studio): automate a parameter without reloading the preview
vanceingalls Aug 4, 2026
5295a35
fix(studio): stop the last two automation writes reloading the preview
vanceingalls Aug 4, 2026
4e94ad5
feat(studio): automate and un-automate each effect parameter
vanceingalls Aug 4, 2026
7554e95
feat(studio): only offer voiceover carve when there is a voice to car…
vanceingalls Aug 4, 2026
bb345bf
test(studio): cover carve visibility through the real element
vanceingalls Aug 4, 2026
47c147e
fix(studio): keep FX panel writes from clobbering each other
vanceingalls Aug 4, 2026
48c5dde
feat(engine): let an FX tail decay instead of cutting it at the clip
vanceingalls Aug 4, 2026
01cb7c0
feat(studio): curve, snap and type a value in an automation lane
vanceingalls Aug 4, 2026
046ec25
feat(studio): pure range ops for automation lane selections
vanceingalls Aug 5, 2026
800e579
fix(studio): budget replaceRange's inner points before capping, not a…
vanceingalls Aug 5, 2026
d59d7b5
feat(studio): automation selection slice
vanceingalls Aug 5, 2026
90195e4
feat(studio): drag-select a time range on an automation lane
vanceingalls Aug 5, 2026
cd036dc
feat(studio): delete an automation selection from the keyboard
vanceingalls Aug 5, 2026
a12cae4
test(studio): cover the automation selection stale-target guard
vanceingalls Aug 5, 2026
db2200e
feat(studio): ramp, swell and dip generators for automation selections
vanceingalls Aug 5, 2026
71a0538
fix(studio): let an automation range keep Delete from the clip
vanceingalls Aug 5, 2026
33ac531
chore: suppress unused export for automationShapes (consumed upstack)
vanceingalls Aug 5, 2026
af6f447
feat(studio): simplify dense automation runs
vanceingalls Aug 5, 2026
f899dae
chore: add automationSimplify to fallow complexity ignore
vanceingalls Aug 5, 2026
c9a1fd1
feat(studio): internal clipboard for automation ranges
vanceingalls Aug 5, 2026
6cd3798
feat(studio): copy and paste automation ranges across lanes
vanceingalls Aug 5, 2026
6964474
feat(studio): shape and simplify menu on an automation selection
vanceingalls Aug 5, 2026
e949398
fix(studio): repair the automation paste path and finish the key arbi…
vanceingalls Aug 6, 2026
bb9dd61
feat(engine): render audio FX in an OfflineAudioContext
vanceingalls Aug 1, 2026
e275c04
feat(core): voiceover carve analysis
vanceingalls Aug 1, 2026
a7d30a2
fix(engine): keep the FX render 16-bit, stereo, and correctly sized
vanceingalls Aug 4, 2026
f75e5a8
fix(producer): report an FX render failure as an audio error
vanceingalls Aug 4, 2026
384fdc4
test(engine): skip the browser FX render cases when there is no browser
vanceingalls Aug 12, 2026
7fdf4db
chore(engine): suppress the temp-file alert with the reason it is safe
vanceingalls Aug 12, 2026
7d79370
test(engine): probe ffmpeg and Chrome instead of assuming them
vanceingalls Aug 12, 2026
956ae2f
feat(core): register the audio-fx-rack canary at 0%
vanceingalls Aug 12, 2026
33961e6
feat(studio): audio FX panel generated from the registry
vanceingalls Aug 1, 2026
c3f2591
feat(studio): show the Audio FX section on audio tracks
vanceingalls Aug 1, 2026
33100d2
refactor(studio): lift audioFxSummary out of PropertyPanelFlat
vanceingalls Aug 12, 2026
d5794bf
feat(core,studio): hear the FX chain in preview, and run the carve an…
vanceingalls Aug 1, 2026
a2eabd2
fix(studio,core): stop parameter drags from restarting playback
vanceingalls Aug 1, 2026
550812a
feat(studio): put the audio FX rack behind its canary
vanceingalls Aug 12, 2026
cfc1215
fix(core): register FX worklets before building nodes that need them
vanceingalls Aug 2, 2026
cfafa22
refactor(studio): split the FX node row out of FxSection
vanceingalls Aug 2, 2026
cbf77bc
feat(core): automation envelope model for audio tracks
vanceingalls Aug 2, 2026
c2846a9
refactor(studio): lift the audio FX group out of PropertyPanelFlat
vanceingalls Aug 12, 2026
75057e2
feat(core): expose the AudioParams behind automatable FX knobs
vanceingalls Aug 2, 2026
380ed3d
feat(core): play automation envelopes in preview
vanceingalls Aug 2, 2026
db00f0d
fix(core): make the volume lane audible in preview
vanceingalls Aug 4, 2026
0f25e9c
feat(engine): bake automation envelopes into the render
vanceingalls Aug 2, 2026
33bb99f
fix(core): apply chain edits to the running graph
vanceingalls Aug 4, 2026
d9df8a1
fix(studio): drop the FX panel's dead __testables export
vanceingalls Aug 12, 2026
d21dfed
merge: pull in the FX panel dead-export fix
vanceingalls Aug 12, 2026
27fb8a8
merge: pull in the FX panel dead-export fix
vanceingalls Aug 12, 2026
43ca71e
merge: pull in the FX panel dead-export fix
vanceingalls Aug 12, 2026
c3399cd
merge: pull in the FX panel dead-export fix
vanceingalls Aug 12, 2026
7c410fd
fix(core,studio): clear the remaining Fallow audit findings on the FX…
vanceingalls Aug 12, 2026
1a2969a
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
05af338
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
ae55ede
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
3418148
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
439ba6c
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
f4b84cf
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
494d431
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
fec1957
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
3533460
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
7d72507
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
f2ac945
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
5bcf5ab
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
7e0fbad
merge: pull in the fallow-audit fixes
vanceingalls Aug 12, 2026
55b20e6
Merge remote-tracking branch 'origin/main' into wa-4-fx-panel
vanceingalls Aug 12, 2026
6d11410
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
0d0d73d
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
644f9bf
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
ada3272
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
4816a1b
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
9328aee
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
1829d4c
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
9bedde7
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
2fb9588
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
a99d6d6
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
34f0546
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
80070ab
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
788adde
merge: pull in the main-catchup merge
vanceingalls Aug 12, 2026
dad13c8
fix(ci): allowlist the build-script consolidation in the no-main-dele…
vanceingalls Aug 12, 2026
7069323
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
e1011a8
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
3e2c259
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
556afc7
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
b9c7684
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
82f8d20
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
fe1ef0c
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
46efb15
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
dc951b9
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
4f1974e
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
e14bd85
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
79f02ec
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
06150a9
merge: pull in the deletion-guard fix
vanceingalls Aug 12, 2026
6edd737
Merge remote-tracking branch 'origin/main' into wa-2-fx-preview
vanceingalls Aug 12, 2026
5ee0b17
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
6e3c8d1
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
fe19f99
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
0c16352
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
cc70cd1
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
df8d168
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
943e8d7
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7da9de5
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
31f4862
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e830e08
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b82bb88
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
dd81d79
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7cf032d
fix(core): dedupe the wet/dry mix math between delayFeedback and chor…
vanceingalls Aug 12, 2026
0d92b36
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
9f6e04c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
f951af6
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
43b03b6
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
4b068fb
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
ff6db14
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
c9c535c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
b0998a8
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
edff77f
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
652fea0
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
9cb9a76
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
830a21c
merge: pull in the wet/dry dedupe fix
vanceingalls Aug 12, 2026
13d13b9
Merge remote-tracking branch 'origin/main' into wa-3-automation-model
vanceingalls Aug 12, 2026
e98a651
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
be2fcbc
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
ed9f61e
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
17b6003
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
830d887
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
bd9083d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
2a722ef
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7f4f249
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e79620a
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
c8b53f6
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
f5b428b
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7ae0fd4
Merge remote-tracking branch 'origin/main' into wa-5-automation-runti…
vanceingalls Aug 12, 2026
b28e620
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
199dbeb
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
150e8f4
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
6f378b0
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
908d909
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
8870d86
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
a697226
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
1b2109d
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
e214048
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
76fbfcb
fix(core): remove the build-audio-fx-runtime.ts stray resurrected by …
vanceingalls Aug 12, 2026
0467055
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
a664bb8
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
4fe82ee
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
c96b429
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
16a471c
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
182af24
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
1bfdf40
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
6bcd100
merge: pull in the stray-file cleanup
vanceingalls Aug 12, 2026
a4699e4
Merge remote-tracking branch 'origin/main' into wa-10-lane-component
vanceingalls Aug 12, 2026
7359e64
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
dac4f9e
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
44ec112
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b886ae4
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
006027f
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
b8e5e83
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
c44f776
merge: pull in the main-merge conflict fix
vanceingalls Aug 12, 2026
7bd053a
fix(studio): pull TimelineLanes under the 600-line cap
vanceingalls Aug 12, 2026
6232f24
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
c6f1db7
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
f99bf5e
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
43407fe
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
2936314
merge: pull in the file-size fix
vanceingalls Aug 12, 2026
c03fd4a
fix(studio): split the extracted pointerdown handler under the CRAP t…
vanceingalls Aug 13, 2026
1a5bac7
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
a9f33b6
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
0cfd6fd
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
b35c9d8
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
afcbd9e
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
3a38cc2
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
a259d14
merge: pull in the CRAP-score fix
vanceingalls Aug 13, 2026
0bdfaca
Merge remote-tracking branch 'origin/main' into wa-11-panel-writes
vanceingalls Aug 13, 2026
76703a8
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
919e588
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
d938c79
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
d1222df
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
fe0e70e
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
6815d46
merge: pull in the PR #3025 conflict fix
vanceingalls Aug 13, 2026
cf02c1e
fix(studio): drop the unused DomEditSelection import in PropertyPanel…
vanceingalls Aug 13, 2026
50e48e1
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
144ae1a
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
4f9317e
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
86ace73
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
e4b4734
merge: pull in the unused-import fix
vanceingalls Aug 13, 2026
0f4ec28
Merge remote-tracking branch 'origin/main' into wa-15-lane-selection
vanceingalls Aug 13, 2026
f4e0584
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
92069b5
merge: pull in the PR #3050 conflict fix
vanceingalls Aug 13, 2026
ae71729
Merge remote-tracking branch 'origin/main' into wa-16-lane-shapes
vanceingalls Aug 13, 2026
cb920a4
merge: pull in the PR #3055 conflict fix
vanceingalls Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@
"withLane",
],
},
// automationShapes is part of the audio-automation stack: its consumer is
// the UI layer that uses shape generators one PR upstack, so a per-PR audit
// diffing against the merge base sees these as unused. Consumed for real once
// the stack merges; safe to drop this entry then.
{
"file": "packages/studio/src/player/components/automationShapes.ts",
"exports": ["AUTOMATION_SHAPES"],
},
// automationSimplify is part of the audio-automation stack: its consumer is
// the UI layer one PR upstack, so a per-PR audit diffing against the merge
// base sees these as unused. Consumed for real once the stack merges; safe
// to drop this entry then.
{
"file": "packages/studio/src/player/components/automationSimplify.ts",
"exports": ["simplifyPoints"],
},
// propertyPanelAutomation is the shared reader for both panel sections; the
// FX group that consumes these two lands one PR upstack, so a per-PR audit
// against the merge base sees them as unused.
Expand Down Expand Up @@ -747,6 +763,11 @@
"packages/parsers/src/gsapParser.ts",
// htmlParser.ts has pre-existing complexity (moved from packages/core).
"packages/parsers/src/htmlParser.ts",
// automationSimplify.ts: Ramer–Douglas–Peucker algorithm inherently requires
// nested loops and stack-based control flow (12 cyclomatic / 20 cognitive);
// this complexity is by design and not refactorable. Consumed by the UI
// layer one PR upstack in the audio-automation feature stack.
"packages/studio/src/player/components/automationSimplify.ts",
// studio-server files: pre-existing complexity (moved from packages/core/src/studio-api/).
// files.ts: executeGsapMutationRecast/Acorn are CRITICAL; excluded as files.ts
// was already in health.ignore at the old path (packages/core/src/studio-api/routes/files.ts).
Expand Down
7 changes: 7 additions & 0 deletions packages/studio/src/contexts/DomEditContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ export function useDomEditSelectionContext(): DomEditSelectionValue {
return ctx;
}

/** Optional counterpart to useDomEditActionsContextOptional — same reason: the
* player package's own components mount outside a provider in standalone and
* test trees, where "no dom-edit selection" is the correct answer. */
export function useDomEditSelectionContextOptional(): DomEditSelectionValue | null {
return useContext(DomEditSelectionContext);
}

/** @deprecated Prefer useDomEditActionsContext or useDomEditSelectionContext. */
export function useDomEditContext(): DomEditValue {
return { ...useDomEditActionsContext(), ...useDomEditSelectionContext() };
Expand Down
87 changes: 86 additions & 1 deletion packages/studio/src/hooks/useAppHotkeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { dispatchPlainKey } from "./useAppHotkeys";
import { dispatchModifierKey, dispatchPlainKey } from "./useAppHotkeys";
import { usePlayerStore } from "../player/store/playerStore";
import { clearAutomationClipboard, copyRange } from "../player/components/automationClipboard";
import { VOLUME_RANGE } from "@hyperframes/core/audio-automation";
import type { TimelineElement } from "../player/store/timelineElement";

/** Minimal valid fixture — TimelineElement only requires these five fields. */
Expand Down Expand Up @@ -38,7 +40,11 @@ function callbacks() {
const press = (key: string) =>
new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });

const chord = (key: string) =>
new KeyboardEvent("keydown", { key, metaKey: true, bubbles: true, cancelable: true });

afterEach(() => {
clearAutomationClipboard();
usePlayerStore.getState().clearAutomationSelection();
usePlayerStore.setState({
elements: [],
Expand Down Expand Up @@ -111,3 +117,82 @@ describe("dispatchPlainKey — Delete arbitration", () => {
expect(e.defaultPrevented).toBe(true);
});
});

describe("dispatchModifierKey — Cmd+C/Cmd+V arbitration", () => {
const clip: TimelineElement = {
id: "bgm",
key: "bgm",
tag: "audio",
start: 0,
duration: 6,
track: 0,
};

it("lets the clip clipboard have Cmd+C when no automation range is active", () => {
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
const cb = callbacks();
dispatchModifierKey(chord("c"), "c", cb);
expect(cb.handleCopy).toHaveBeenCalled();
});

it("keeps Cmd+C from the clip clipboard when an automation range is active", () => {
// Both clipboards arming on one press double-wrote and toasted "Copied clip".
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
usePlayerStore.getState().setAutomationSelection({
elementKey: "bgm",
target: "volume",
t0: 1,
t1: 3,
});
const cb = callbacks();
const e = chord("c");
expect(dispatchModifierKey(e, "c", cb)).toBe(true);
expect(cb.handleCopy).not.toHaveBeenCalled();
// No preventDefault: the automation handler downstream still needs the key.
expect(e.defaultPrevented).toBe(false);
});

it("lets the clip clipboard have Cmd+V when the automation clipboard is empty", () => {
// Nothing to paste means nothing to claim — the clip paste should still run.
clearAutomationClipboard();
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
usePlayerStore.getState().setAutomationSelection({
elementKey: "bgm",
target: "volume",
t0: 1,
t1: 3,
});
const cb = callbacks();
dispatchModifierKey(chord("v"), "v", cb);
expect(cb.handlePaste).toHaveBeenCalled();
});

it("keeps Cmd+V from duplicating the clip while an automation paste is pending", () => {
clearAutomationClipboard();
copyRange(
null,
{
target: "volume",
points: [
{ t: 1, v: 1 },
{ t: 3, v: 0.25 },
],
},
VOLUME_RANGE,
1,
3,
);
usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
usePlayerStore.getState().setAutomationSelection({
elementKey: "bgm",
target: "volume",
t0: 1,
t1: 3,
});
const cb = callbacks();
const e = chord("v");
dispatchModifierKey(e, "v", cb);
expect(cb.handlePaste).not.toHaveBeenCalled();
expect(e.defaultPrevented).toBe(false);
});
});
21 changes: 19 additions & 2 deletions packages/studio/src/hooks/useAppHotkeys.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useCallback, useEffect, useRef } from "react";
import { automationOwnsKey } from "./useAutomationSelectionKeyboard";
import { usePlayerStore } from "../player";
import type { TimelineElement } from "../player";
import type { DomEditSelection } from "../components/editor/domEditing";
import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
import { isTypingTarget } from "../utils/typingTarget";
import { isEditableTarget } from "../utils/timelineDiscovery";
import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
import { canSplitElement } from "../utils/timelineElementSplit";
import { trackStudioEvent } from "../utils/studioTelemetry";
Expand Down Expand Up @@ -158,7 +160,14 @@ interface HotkeyCallbacks {
showToast: (message: string, tone?: "error" | "info") => void;
}

function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks): boolean {
/** Exported for tests, like dispatchPlainKey below: lets the Cmd+C/Cmd+V
* arbitration between an automation range and the clip clipboard be asserted
* without standing up the whole hook. */
export function dispatchModifierKey(
event: KeyboardEvent,
key: string,
cb: HotkeyCallbacks,
): boolean {
if (
!shouldIgnoreHistoryShortcut(event.target) &&
handleUndoRedoKey(
Expand Down Expand Up @@ -195,7 +204,15 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
return true;
}

if (!event.shiftKey && !event.altKey && !isTypingTarget(event.target)) {
if (!event.shiftKey && !event.altKey && !isEditableTarget(event.target)) {
// An active automation range owns Cmd+C/Cmd+V, the same way it owns Delete
// below. This listener is on window/capture and runs before
// useAutomationSelectionKeyboard's document/capture handler, so without
// this the clip clipboard also claimed the key: Cmd+V duplicated the clip
// while the automation paste wrote the same file, and Cmd+C armed both
// clipboards and toasted "Copied clip". Return without preventDefault so
// the downstream handler still sees the key.
if (automationOwnsKey(event)) return true;
if (key === "c") {
if (cb.handleCopy()) {
event.preventDefault();
Expand Down
Loading
Loading