Skip to content

feat(editor): bulk barline locks + band-aware Tempo Map selection - #291

Merged
byrongamatos merged 1 commit into
mainfrom
feat/editor-tempo-bulk-locks
Jul 16, 2026
Merged

feat(editor): bulk barline locks + band-aware Tempo Map selection#291
byrongamatos merged 1 commit into
mainfrom
feat/editor-tempo-bulk-locks

Conversation

@ChrisBeWithYou

Copy link
Copy Markdown
Contributor

What this is

PR 3 of the #285 re-cut (stacked on #290#289 → main; retarget as parents merge; CI fires on retarget to main — local gates green). The marquee-reliability and bulk-lock features, rebuilt on main's existing multi-select infrastructure.

The features

  • Bulk locks, one undo. TempoLockCmd makes lock/unlock a first-class exec/rollback command: select any number of barlines (marquee, Shift-click, Ctrl/Cmd-click, Ctrl/Cmd+A — uncapped) and the S key / toolbar button / right-click menu applies one uniform state (lock all unless every one is already locked) as one undoable edit. Entries re-resolve by time so undo survives a grid rebuild.
  • sessionNeutral history flag (new, in history.js): lock toggles belong in Undo but are an editor preference, not pack data — they now skip markSessionDirty, so locking barlines never creates a false Save prompt. _afterEdit(cmd) threads the command through exec/undo/redo.
  • Band-aware selection. The marker lane splits into a top handle band (direct edits: forgiving 8px pole grabs, sub-beat rubato) and the lane body below (marquee territory) — while the thin full-height barline keeps a precise 2.5px drag target anywhere, and holding a selection modifier restores the forgiving width. Hover mirrors the exact same tolerance so the cursor never promises a grab the click would route to the marquee.
  • Ctrl/Cmd-click toggles individual poles in/out of the selection (returns a new Set — no shared-reference mutation). Marquee finalize routes through a pure selector that focuses the drag end (forward drag → last hit, reverse → first).
  • Group-aware chrome: the toolbar lock button's face derives from the same plan the verb executes ("Lock 12 barlines"), the inspector label shows "N barlines selected · Shift-click extends · Ctrl/Cmd-click toggles", and right-clicking outside the selection collapses to the clicked barline before showing verbs.

What was already on main (not re-ported)

The marquee drag machinery, Shift-click range extend, Ctrl/Cmd+A select-all, Escape-clears, and group drag all landed in the earlier multi-select wave — this PR layers planners + the undo command + the hit-band refinement on top.

Tests

8 new cases in tests/tempo_multiselect.test.mjs: the handle band, the dual grab tolerance, marquee replace/extend + drag-end focus, non-mutating Ctrl-toggle, the uniform lock plan (mixed → lock-all; all → unlock-all; single-focus fallback), the no-cap contract (512 barlines), the button-face pure, and the full exec → one-undo → redo round-trip asserting S.sessionDirty === false. (The shared test env's fake elements gained no-op classList — the signpost cue path needed it.)

Gates: 182 JS suites green, pytest 291/2 skipped, lint 0 errors / 3 baseline warnings.

Remaining from the salvage queue

Tracks lanes UI → engine/meters (evaluate).

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 04d52c23-75c8-4c8b-9bac-0921a8012268

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-tempo-bulk-locks

Comment @coderabbitai help to get the list of available commands.

@ChrisBeWithYou
ChrisBeWithYou force-pushed the feat/editor-metronome-guide branch from cce7ca2 to 95b0219 Compare July 16, 2026 03:54
@ChrisBeWithYou
ChrisBeWithYou force-pushed the feat/editor-tempo-bulk-locks branch from a8235fa to e0c109c Compare July 16, 2026 03:54
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
…t tree

Makes the track-session tree (from the foundation PR) a first-class UI:
a resizable header column beside the timeline, one header cell per lane
the canvas draws — both surfaces read the SAME pure geometry
(_trackSessionLaneLayoutPure / fitted heights), so they can never drift.

Per row: inline rename, drag reorder + folders, lane-height resize,
mute/solo/volume (the SAME canonical S.partMix the mixer panel owns — a
new mixerTogglePart/mixerSetPart seam keeps that panel the sole owner),
pair a transcription with its studio stem (writes the one pairing store,
S.stemLinks, and syncs via /stem-op op 'links'), double-click to open the
native editor, and a stem row-menu to lock the metronome guide. Audio
removal is non-destructive (a removedSourceIds tombstone).

parts-view.js is rewritten to draw unified lanes matching the header rows
(shared layout, shared hit-test _partsTrackRowAtYPure); the in-canvas
gutter shrinks to LABEL_W since the DOM column now owns names/controls.
partsViewMode becomes the one-way landing surface; leaving happens by
OPENING a transcription (host.openTrackSessionTarget). A vertical wheel
over the area scrolls the shared lane stack; horizontal still pans.

Scoped deliberately: no per-lane stem waveforms or meters and no audio
strips on audio rows — stem playback is the engine slice (next PR). The
host hooks (trackWaveform, scrollTrackArea) land inert-but-honest: the
master lane draws from S.waveformPeaks today, stems light up with the
engine.

Tests: new track_session_ui.test.mjs (lane geometry/density/drop/rename
pures), a unified-row hit-test case in parts_view.test.js (its @pure
slice now strips `export`), two responsive/rename CSS-contract cases in
theme_chrome.test.mjs; the shared history env's fake element gained a
faithful DOM-method surface. 182 JS suites, 291 pytest, lint 0 errors.

Stacked on feat/editor-tempo-bulk-locks (#291).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
_beatLockStorageKeyPure returned the shared key `editorBeatLocks:` for a
project with no filename, so locks set while charting one new import
leaked into the NEXT new import (both keyed by ''). It now returns null
for an empty filename; _saveBeatLocks / _restoreBeatLocks treat null as
"nowhere to persist" and scrub any legacy blank-key residue, so a fresh
project always starts with a clean lock set.

A straggler from #291 (bulk barline locks) — same beat-lock code, missed
in the re-cut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@ChrisBeWithYou
ChrisBeWithYou force-pushed the feat/editor-metronome-guide branch from 95b0219 to 31cc404 Compare July 16, 2026 03:57
@ChrisBeWithYou
ChrisBeWithYou force-pushed the feat/editor-tempo-bulk-locks branch from e0c109c to 4f00d59 Compare July 16, 2026 03:57
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
…t tree

Makes the track-session tree (from the foundation PR) a first-class UI:
a resizable header column beside the timeline, one header cell per lane
the canvas draws — both surfaces read the SAME pure geometry
(_trackSessionLaneLayoutPure / fitted heights), so they can never drift.

Per row: inline rename, drag reorder + folders, lane-height resize,
mute/solo/volume (the SAME canonical S.partMix the mixer panel owns — a
new mixerTogglePart/mixerSetPart seam keeps that panel the sole owner),
pair a transcription with its studio stem (writes the one pairing store,
S.stemLinks, and syncs via /stem-op op 'links'), double-click to open the
native editor, and a stem row-menu to lock the metronome guide. Audio
removal is non-destructive (a removedSourceIds tombstone).

parts-view.js is rewritten to draw unified lanes matching the header rows
(shared layout, shared hit-test _partsTrackRowAtYPure); the in-canvas
gutter shrinks to LABEL_W since the DOM column now owns names/controls.
partsViewMode becomes the one-way landing surface; leaving happens by
OPENING a transcription (host.openTrackSessionTarget). A vertical wheel
over the area scrolls the shared lane stack; horizontal still pans.

Scoped deliberately: no per-lane stem waveforms or meters and no audio
strips on audio rows — stem playback is the engine slice (next PR). The
host hooks (trackWaveform, scrollTrackArea) land inert-but-honest: the
master lane draws from S.waveformPeaks today, stems light up with the
engine.

Tests: new track_session_ui.test.mjs (lane geometry/density/drop/rename
pures), a unified-row hit-test case in parts_view.test.js (its @pure
slice now strips `export`), two responsive/rename CSS-contract cases in
theme_chrome.test.mjs; the shared history env's fake element gained a
faithful DOM-method surface. 182 JS suites, 291 pytest, lint 0 errors.

Stacked on feat/editor-tempo-bulk-locks (#291).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
_beatLockStorageKeyPure returned the shared key `editorBeatLocks:` for a
project with no filename, so locks set while charting one new import
leaked into the NEXT new import (both keyed by ''). It now returns null
for an empty filename; _saveBeatLocks / _restoreBeatLocks treat null as
"nowhere to persist" and scrub any legacy blank-key residue, so a fresh
project always starts with a clean lock set.

A straggler from #291 (bulk barline locks) — same beat-lock code, missed
in the re-cut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
_beatLockStorageKeyPure returned the shared key `editorBeatLocks:` for a
project with no filename, so locks set while charting one new import
leaked into the NEXT new import (both keyed by ''). It now returns null
for an empty filename; _saveBeatLocks / _restoreBeatLocks treat null as
"nowhere to persist" and scrub any legacy blank-key residue, so a fresh
project always starts with a clean lock set.

A straggler from #291 (bulk barline locks) — same beat-lock code, missed
in the re-cut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@byrongamatos
byrongamatos changed the base branch from feat/editor-metronome-guide to main July 16, 2026 18:28
Lock/unlock a whole barline selection as ONE undoable edit. The new
TempoLockCmd is a first-class exec/rollback command (replacing the old
toggle-plus-checkpoint): entries re-resolve by time (±0.02s) so undo
survives a grid rebuild, every apply re-persists the editor-pref lock
list, and the new `sessionNeutral` history flag keeps lock toggles out
of markSessionDirty — undoable, but never a false Save prompt.

The plan is uniform and uncapped: multi-selection wins over the single
focus, target state = lock-all unless every selected barline is already
locked, no size limit (Ctrl/Cmd+A then S locks the whole song). The
tempo toolbar gains a lock button whose face derives from the same plan
the verb executes ("Lock 12 barlines"), the right-click menu's lock item
is group-aware (right-click outside the group collapses to the clicked
barline), and Ctrl/Cmd-click toggles individual poles.

Selection ergonomics: the marker lane is split into a top HANDLE band
(direct edits — forgiving pole grabs, sub-beat rubato) and the lane body
below (marquee territory) — while the thin full-height barline keeps a
precise 2.5px drag target anywhere and a held selection modifier
restores the forgiving width. Marquee finalize now routes through a pure
selector that focuses the drag end. Hover mirrors the same band-aware
tolerance so the cursor never promises a grab the click would route to
the marquee.

Stacked on feat/editor-metronome-guide (#290).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@byrongamatos
byrongamatos force-pushed the feat/editor-tempo-bulk-locks branch from 4f00d59 to 6d351f9 Compare July 16, 2026 18:30
@byrongamatos
byrongamatos merged commit b89efcc into main Jul 16, 2026
3 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-tempo-bulk-locks branch July 16, 2026 18:30
byrongamatos pushed a commit that referenced this pull request Jul 16, 2026
…t tree

Makes the track-session tree (from the foundation PR) a first-class UI:
a resizable header column beside the timeline, one header cell per lane
the canvas draws — both surfaces read the SAME pure geometry
(_trackSessionLaneLayoutPure / fitted heights), so they can never drift.

Per row: inline rename, drag reorder + folders, lane-height resize,
mute/solo/volume (the SAME canonical S.partMix the mixer panel owns — a
new mixerTogglePart/mixerSetPart seam keeps that panel the sole owner),
pair a transcription with its studio stem (writes the one pairing store,
S.stemLinks, and syncs via /stem-op op 'links'), double-click to open the
native editor, and a stem row-menu to lock the metronome guide. Audio
removal is non-destructive (a removedSourceIds tombstone).

parts-view.js is rewritten to draw unified lanes matching the header rows
(shared layout, shared hit-test _partsTrackRowAtYPure); the in-canvas
gutter shrinks to LABEL_W since the DOM column now owns names/controls.
partsViewMode becomes the one-way landing surface; leaving happens by
OPENING a transcription (host.openTrackSessionTarget). A vertical wheel
over the area scrolls the shared lane stack; horizontal still pans.

Scoped deliberately: no per-lane stem waveforms or meters and no audio
strips on audio rows — stem playback is the engine slice (next PR). The
host hooks (trackWaveform, scrollTrackArea) land inert-but-honest: the
master lane draws from S.waveformPeaks today, stems light up with the
engine.

Tests: new track_session_ui.test.mjs (lane geometry/density/drop/rename
pures), a unified-row hit-test case in parts_view.test.js (its @pure
slice now strips `export`), two responsive/rename CSS-contract cases in
theme_chrome.test.mjs; the shared history env's fake element gained a
faithful DOM-method surface. 182 JS suites, 291 pytest, lint 0 errors.

Stacked on feat/editor-tempo-bulk-locks (#291).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
byrongamatos added a commit that referenced this pull request Jul 16, 2026
…t tree (#292)

* feat(editor): the Tracks area — a DAW track column over the persistent tree

Makes the track-session tree (from the foundation PR) a first-class UI:
a resizable header column beside the timeline, one header cell per lane
the canvas draws — both surfaces read the SAME pure geometry
(_trackSessionLaneLayoutPure / fitted heights), so they can never drift.

Per row: inline rename, drag reorder + folders, lane-height resize,
mute/solo/volume (the SAME canonical S.partMix the mixer panel owns — a
new mixerTogglePart/mixerSetPart seam keeps that panel the sole owner),
pair a transcription with its studio stem (writes the one pairing store,
S.stemLinks, and syncs via /stem-op op 'links'), double-click to open the
native editor, and a stem row-menu to lock the metronome guide. Audio
removal is non-destructive (a removedSourceIds tombstone).

parts-view.js is rewritten to draw unified lanes matching the header rows
(shared layout, shared hit-test _partsTrackRowAtYPure); the in-canvas
gutter shrinks to LABEL_W since the DOM column now owns names/controls.
partsViewMode becomes the one-way landing surface; leaving happens by
OPENING a transcription (host.openTrackSessionTarget). A vertical wheel
over the area scrolls the shared lane stack; horizontal still pans.

Scoped deliberately: no per-lane stem waveforms or meters and no audio
strips on audio rows — stem playback is the engine slice (next PR). The
host hooks (trackWaveform, scrollTrackArea) land inert-but-honest: the
master lane draws from S.waveformPeaks today, stems light up with the
engine.

Tests: new track_session_ui.test.mjs (lane geometry/density/drop/rename
pures), a unified-row hit-test case in parts_view.test.js (its @pure
slice now strips `export`), two responsive/rename CSS-contract cases in
theme_chrome.test.mjs; the shared history env's fake element gained a
faithful DOM-method surface. 182 JS suites, 291 pytest, lint 0 errors.

Stacked on feat/editor-tempo-bulk-locks (#291).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

* Preserve transcription identity across track edits

* Guard inline transcription renames

* Restore tombstoned audio tracks

* fix(editor): keep Tracks-column fader draggable and invalidate render on audioUrl

Two bugs in the new Tracks header column:

- Dragging a per-track fader rebuilt the panel's innerHTML on every `input`
  (via host.partMixChanged → refreshTrackSession, whose memo key includes
  S.partMix), destroying the <input type=range> under the pointer and
  aborting the native drag. Suppress the rebuild for the synchronous span of
  a header-column fader input — the fader already reflects its own value, the
  same way the mixer panel avoids re-rendering itself from its fader input.

- The refresh memo key omitted S.audioUrl even though render() derives the
  Master Mix source from it (_liveSources). A late audio load that set
  S.audioUrl without touching S.stems did not invalidate the memo, leaving
  the column missing the Master Mix until an unrelated refresh. Add S.audioUrl
  to the key. (Reported by Codex first pass, P2.)

Regression tests in tests/track_session_fader_drag.test.mjs cover both:
the fader input must not rebuild innerHTML; an S.audioUrl-only change must.

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

* fix(editor): address CodeRabbit findings on the Tracks column (PR #292)

Data integrity:
- _trackSessionNormalizePure keeps every persisted removed-source tombstone
  even when its source is not yet loaded, so a session installed before its
  audio arrives (S.audioUrl set late) no longer loses a `master` tombstone
  and resurrects the Master Mix row once the audio loads.
- deleteTrack shifts the surviving `arr:<n>` mix strips down one slot on a
  transcription delete (new _partMixDropArrangementPure) instead of wiping
  S.partMix, preserving every other track's mute/solo/volume.
- _syncPairing stamps each full-snapshot write and ignores responses a later
  pairing has superseded, so a slow earlier response can't clobber the newer
  S.stemLinks selection.

Functional correctness:
- mixerSetPart refreshes the mixer panel so its companion fader tracks a
  Tracks-column fader change (symmetry with mixerTogglePart).
- _editorTogglePartsView gates entry on the unified rows, letting an
  audio-only session (audio before any arrangement/drum) into Tracks.
- initTrackSession resizes the canvas after applying a restored non-default
  header width so hit geometry isn't left stale.

Docs: resolve the contradictory Unreleased changelog notes.

Regression tests (fail pre-fix, pass after):
- tests/track_session.test.mjs: late-audio tombstone; partMix key shift.
- tests/track_session_pairing_race.test.mjs: stale pairing response ignored.

Skipped (not valid): canvas lane selection already arms the arrangement
target inline (host.selectTrackSessionTarget is a no-op for drums and
redundant for arrangements). Skipped (out of scope): full keyboard
resize/reorder is a net-new a11y interaction model, not a correctness fix.

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

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
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