feat(editor): the Tracks area — a DAW track column over the persistent tree - #292
Conversation
📝 WalkthroughWalkthroughAdds a persistent Tracks sidebar with resizable lanes, unified Parts view rendering, track selection and editing, mixer controls, transcription-stem pairing, host callbacks, responsive styling, documentation, and regression tests. ChangesTracks Session
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant MainEditor
participant TrackSession
participant PartsView
participant Host
participant StemAPI
MainEditor->>TrackSession: initialize and refresh Tracks area
PartsView->>TrackSession: derive unified rows and lane geometry
PartsView->>Host: request waveform data
Host-->>PartsView: return waveform peaks and duration
TrackSession->>StemAPI: synchronize transcription-stem pairing
TrackSession-->>MainEditor: update selection, layout, and rendered rows
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
…ack order
Three Tracks/mixer fixes surfaced by dogfooding:
1. The Master row and tempo guide could vanish ("No guide"): sources were
derived from S.audioUrl, which (a) still points at the previous song at
install time and (b) active-source switching reassigns to a focused
stem. Derive the master from the stable S.masterAudioUrl instead, pinned
at install.
2. "Master Mix" naming: the master track now defaults to the SONG name (its
recording), and the guide button follows the track's DISPLAY name so an
inline rename wins instead of reverting to a generic label.
3. Mixer strips now reorder to match a drag-reorder of the Tracks column
(and rename with it): the mixer reads host.mixerTrackOrder ->
trackSessionOrderedMixKeys() and reorders via _mixerOrderedPartsPure; the
tree commit refreshes the mixer.
Tests: _mixerOrderedPartsPure ordering (tree order, tail-stable, empty);
master-name default/override. 184 JS suites, lint 0 errors.
Fixes behaviour in the guide (#290), Tracks UI (#292), and mixer (#295)
features; carried on the stack tip. Verified live in the desktop build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
a8235fa to
e0c109c
Compare
76bf4ae to
2243b39
Compare
Stems (S.stems) now sound alongside the master, sample-aligned. The
engine is ADDITIVE: the master keeps its own path (S.audioSource →
_refGain, the audition MediaElement, A/B), and stems ride a parallel
scheduler hooked into the same start/stop/restart points, so a stem
fault can never take the recording down.
- decode: stemAudioCache (per-source {url,buffer,peaks}), syncStemAudio
(parallel, generation-guarded, one failure never blocks the rest),
resetStemAudioCache on song boundary; triggered from loadCDLC, create,
and every /stem-op adopt.
- schedule: _startStemSources creates a BufferSource per cached stem at
the SAME preRoll-shifted anchor as the master, each placed from its own
S.audioShift + source.offset via the shared _audioBufferStartPure — so
they stay aligned through seeks (_restartPlaybackAt), loops, and shift.
_stopStemSources wired into restart/stop/teardown.
- mix: per-stem gain → _refGain (transparent, never the guide limiter),
ramped by applyStemMix off host.partStripState('audio:<id>') — the SAME
S.partMix store and whole-map solo rule as the synth parts. One mixer
drives both bands. _mixerPartsPure now lists 'audio:<id>' stem strips.
- capability: host.stemMixChanged is wired (its presence flips
stemMixerAvailable() true), lighting up Solo-my-source — reconciled to
write S.partMix['audio:<id>'] (exclusive isolate over the audio band;
the recording stays audible) instead of the dead S.stemMix stub — plus
the PR-5 audio-row strips and per-stem lane waveforms.
Scoped deliberately: unity fader ceiling (no +6 dB) and no live meters —
both additive polish for a follow-up. Known limit: at audition speed < 1
the master reroutes to a pitch-preserving MediaElement and the
sample-accurate BufferSource path (stems included) is silent; stems
resume at 100%.
Tests: stem_engine.test.mjs (mixer band + solo rule + placement
alignment); stem_tracks.test.mjs reconciled to the partMix solo model;
audition_clock.test.mjs injects the new stem-scheduler stubs. 183 JS
suites, 291 pytest, lint 0 errors.
Stacked on feat/editor-tracks-ui (#292) — the final salvage slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…ack order
Three Tracks/mixer fixes surfaced by dogfooding:
1. The Master row and tempo guide could vanish ("No guide"): sources were
derived from S.audioUrl, which (a) still points at the previous song at
install time and (b) active-source switching reassigns to a focused
stem. Derive the master from the stable S.masterAudioUrl instead, pinned
at install.
2. "Master Mix" naming: the master track now defaults to the SONG name (its
recording), and the guide button follows the track's DISPLAY name so an
inline rename wins instead of reverting to a generic label.
3. Mixer strips now reorder to match a drag-reorder of the Tracks column
(and rename with it): the mixer reads host.mixerTrackOrder ->
trackSessionOrderedMixKeys() and reorders via _mixerOrderedPartsPure; the
tree commit refreshes the mixer.
Tests: _mixerOrderedPartsPure ordering (tree order, tail-stable, empty);
master-name default/override. 184 JS suites, lint 0 errors.
Fixes behaviour in the guide (#290), Tracks UI (#292), and mixer (#295)
features; carried on the stack tip. Verified live in the desktop build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
e0c109c to
4f00d59
Compare
2243b39 to
977386b
Compare
Stems (S.stems) now sound alongside the master, sample-aligned. The
engine is ADDITIVE: the master keeps its own path (S.audioSource →
_refGain, the audition MediaElement, A/B), and stems ride a parallel
scheduler hooked into the same start/stop/restart points, so a stem
fault can never take the recording down.
- decode: stemAudioCache (per-source {url,buffer,peaks}), syncStemAudio
(parallel, generation-guarded, one failure never blocks the rest),
resetStemAudioCache on song boundary; triggered from loadCDLC, create,
and every /stem-op adopt.
- schedule: _startStemSources creates a BufferSource per cached stem at
the SAME preRoll-shifted anchor as the master, each placed from its own
S.audioShift + source.offset via the shared _audioBufferStartPure — so
they stay aligned through seeks (_restartPlaybackAt), loops, and shift.
_stopStemSources wired into restart/stop/teardown.
- mix: per-stem gain → _refGain (transparent, never the guide limiter),
ramped by applyStemMix off host.partStripState('audio:<id>') — the SAME
S.partMix store and whole-map solo rule as the synth parts. One mixer
drives both bands. _mixerPartsPure now lists 'audio:<id>' stem strips.
- capability: host.stemMixChanged is wired (its presence flips
stemMixerAvailable() true), lighting up Solo-my-source — reconciled to
write S.partMix['audio:<id>'] (exclusive isolate over the audio band;
the recording stays audible) instead of the dead S.stemMix stub — plus
the PR-5 audio-row strips and per-stem lane waveforms.
Scoped deliberately: unity fader ceiling (no +6 dB) and no live meters —
both additive polish for a follow-up. Known limit: at audition speed < 1
the master reroutes to a pitch-preserving MediaElement and the
sample-accurate BufferSource path (stems included) is silent; stems
resume at 100%.
Tests: stem_engine.test.mjs (mixer band + solo rule + placement
alignment); stem_tracks.test.mjs reconciled to the partMix solo model;
audition_clock.test.mjs injects the new stem-scheduler stubs. 183 JS
suites, 291 pytest, lint 0 errors.
Stacked on feat/editor-tracks-ui (#292) — the final salvage slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…ack order
Three Tracks/mixer fixes surfaced by dogfooding:
1. The Master row and tempo guide could vanish ("No guide"): sources were
derived from S.audioUrl, which (a) still points at the previous song at
install time and (b) active-source switching reassigns to a focused
stem. Derive the master from the stable S.masterAudioUrl instead, pinned
at install.
2. "Master Mix" naming: the master track now defaults to the SONG name (its
recording), and the guide button follows the track's DISPLAY name so an
inline rename wins instead of reverting to a generic label.
3. Mixer strips now reorder to match a drag-reorder of the Tracks column
(and rename with it): the mixer reads host.mixerTrackOrder ->
trackSessionOrderedMixKeys() and reorders via _mixerOrderedPartsPure; the
tree commit refreshes the mixer.
Tests: _mixerOrderedPartsPure ordering (tree order, tail-stable, empty);
master-name default/override. 184 JS suites, lint 0 errors.
Fixes behaviour in the guide (#290), Tracks UI (#292), and mixer (#295)
features; carried on the stack tip. Verified live in the desktop build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Stems (S.stems) now sound alongside the master, sample-aligned. The
engine is ADDITIVE: the master keeps its own path (S.audioSource →
_refGain, the audition MediaElement, A/B), and stems ride a parallel
scheduler hooked into the same start/stop/restart points, so a stem
fault can never take the recording down.
- decode: stemAudioCache (per-source {url,buffer,peaks}), syncStemAudio
(parallel, generation-guarded, one failure never blocks the rest),
resetStemAudioCache on song boundary; triggered from loadCDLC, create,
and every /stem-op adopt.
- schedule: _startStemSources creates a BufferSource per cached stem at
the SAME preRoll-shifted anchor as the master, each placed from its own
S.audioShift + source.offset via the shared _audioBufferStartPure — so
they stay aligned through seeks (_restartPlaybackAt), loops, and shift.
_stopStemSources wired into restart/stop/teardown.
- mix: per-stem gain → _refGain (transparent, never the guide limiter),
ramped by applyStemMix off host.partStripState('audio:<id>') — the SAME
S.partMix store and whole-map solo rule as the synth parts. One mixer
drives both bands. _mixerPartsPure now lists 'audio:<id>' stem strips.
- capability: host.stemMixChanged is wired (its presence flips
stemMixerAvailable() true), lighting up Solo-my-source — reconciled to
write S.partMix['audio:<id>'] (exclusive isolate over the audio band;
the recording stays audible) instead of the dead S.stemMix stub — plus
the PR-5 audio-row strips and per-stem lane waveforms.
Scoped deliberately: unity fader ceiling (no +6 dB) and no live meters —
both additive polish for a follow-up. Known limit: at audition speed < 1
the master reroutes to a pitch-preserving MediaElement and the
sample-accurate BufferSource path (stems included) is silent; stems
resume at 100%.
Tests: stem_engine.test.mjs (mixer band + solo rule + placement
alignment); stem_tracks.test.mjs reconciled to the partMix solo model;
audition_clock.test.mjs injects the new stem-scheduler stubs. 183 JS
suites, 291 pytest, lint 0 errors.
Stacked on feat/editor-tracks-ui (#292) — the final salvage slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…ack order
Three Tracks/mixer fixes surfaced by dogfooding:
1. The Master row and tempo guide could vanish ("No guide"): sources were
derived from S.audioUrl, which (a) still points at the previous song at
install time and (b) active-source switching reassigns to a focused
stem. Derive the master from the stable S.masterAudioUrl instead, pinned
at install.
2. "Master Mix" naming: the master track now defaults to the SONG name (its
recording), and the guide button follows the track's DISPLAY name so an
inline rename wins instead of reverting to a generic label.
3. Mixer strips now reorder to match a drag-reorder of the Tracks column
(and rename with it): the mixer reads host.mixerTrackOrder ->
trackSessionOrderedMixKeys() and reorders via _mixerOrderedPartsPure; the
tree commit refreshes the mixer.
Tests: _mixerOrderedPartsPure ordering (tree order, tail-stable, empty);
master-name default/override. 184 JS suites, lint 0 errors.
Fixes behaviour in the guide (#290), Tracks UI (#292), and mixer (#295)
features; carried on the stack tip. Verified live in the desktop build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 12-22: Resolve the contradictory changelog entries by updating the
older Lines 64-66 note to describe the Tracks data-model foundation rather than
claiming the unified Tracks surface ships separately. Preserve the new unified
Tracks surface entry as the current shipped behavior and ensure both notes
present a consistent release history.
In `@src/mixer-panel.js`:
- Around line 188-190: Update mixerSetPart to refresh the visible mixer panel
after applying the patch through _setPart, ensuring companion-fader changes made
via the track-session path are immediately reflected while preserving the
existing update behavior.
In `@src/parts-view.js`:
- Around line 280-282: Update the Tracks entry guard around _unifiedRows() so
audio-only sessions with unified audio lanes are allowed through. Use the
unified-row result to determine whether content exists, while retaining
partCount solely for the existing “Load a song first” status text.
- Around line 257-264: Update the canvas lane-selection handler before the
drums/arrangement branch to call host.selectTrackSessionTarget(row.targetId) for
every transcription row, matching the header-selection behavior in the existing
track-session flow. Preserve the current drum status handling and arrangement
selection logic after arming the selected target.
In `@src/track-session.js`:
- Around line 717-722: Update the deletion flow around S.partMix in the
arrangement-removal logic to preserve mixer settings: remove only the deleted
arr:<index> entry, shift subsequent arrangement keys down to their new indices,
and retain unaffected arrangement and drum mixer state. Do not reset S.partMix
to an empty object before normalizing S.trackSession.
- Around line 736-754: Update _syncPairing to serialize stem-op snapshot writes
in user-action order, ensuring each request waits for the previous write before
starting. Track the latest pairing operation or request generation and ignore
responses from older operations so stale data cannot overwrite S.stemLinks or
S.stems, while preserving the existing dirty-state and refresh behavior.
- Around line 777-780: After applyTrackHeaderWidth in the track-session
initialization flow, call host.resizeCanvas() so canvas dimensions and hit
geometry are recalculated using the restored header width. Keep the
persisted-width fallback behavior unchanged.
- Around line 425-432: Update the track-session installation and normalization
flow around S.trackSession and S.audioUrl so sanitized removed-source
tombstones, including the master tombstone, are retained when audio sources are
not yet available and reconciled only once those sources arrive. Ensure late
audio loading cannot resurrect the Master Mix row, and add a regression test
covering installation before S.audioUrl becomes available followed by late audio
loading.
- Around line 780-800: Extend the track splitter setup around the pointerdown
handler and the lane-grip/row ordering flow near the referenced area with
keyboard-accessible controls. Make resize controls focusable, expose the current
width through aria-valuenow, and handle Arrow keys to resize; add keyboard
commands to move rows before, after, or into valid targets while preserving the
existing pointer and HTML-drag behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: be49465e-503f-4584-b838-8d73379e03f9
📒 Files selected for processing (17)
CHANGELOG.mdassets/v3-theme.cssdocs/USER-GUIDE.mdscreen.htmlsrc/arrangement.jssrc/host.jssrc/main.jssrc/mixer-panel.jssrc/mouse.jssrc/parts-view.jssrc/state.jssrc/track-session.jstests/_history_env.mjstests/parts_view.test.jstests/theme_chrome.test.mjstests/track_session_fader_drag.test.mjstests/track_session_ui.test.mjs
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>
|
@coderabbitai review |
✅ Action performedReview finished.
|
…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
… 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>
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>
dc54d83 to
3418dca
Compare
What this is
PR 4 of the #285 re-cut (stacked on #291 → #290 → #289 → main; retarget as parents merge). The unified Tracks UI — the persistent track tree from the foundation PR, now a surface you can see and arrange.
The surface
A resizable header column beside the timeline lists every track (master mix, studio stems, each transcription part, folders), and the canvas draws a matching lane for each row. Both read the same pure geometry (
_trackSessionLaneLayoutPure+ fitted heights), so header cells and canvas lanes always line up.Per row: inline rename (double-click / menu), drag reorder + folders, lane-height resize, mute/solo/volume, pair a transcription with its studio stem, double-click to open the native editor, and a stem's right-click menu locks it as the metronome guide. Non-destructive audio removal (a
removedSourceIdstombstone — media stays in the pack). A vertical wheel scrolls the shared lane stack; horizontal still pans the timeline.Design decisions
S.partMixthe mixer panel owns, through a new thinmixerTogglePart/mixerSetPartseam — the panel stays the sole owner, so the DAW solo rule can't fork.S.stemLinks(the pairing truth from the foundation PR) and syncs via/stem-opoplinks— the same atomic-snapshot contract the stem manager uses. Inlined (not imported from stem-tracks) to keep seams, not cycles.partsViewModeis now the one-way landing surface (the DAW arrangement-view idiom): you enter the Tracks area for any loaded song and leave it by opening a transcription (host.openTrackSessionTarget), not by re-toggling.parts-view.jsis rewritten to draw unified lanes matching the header rows; the in-canvas gutter shrinks toLABEL_Wsince the DOM column owns names/controls now.-webkit-app-region: no-dragso text selection isn't stolen by the desktop window or the draggable row.Scoped deliberately (→ the engine PR)
No per-lane stem waveforms or meters, and no audio strips on audio rows — stem playback is the engine slice. The new host hooks (
trackWaveform,scrollTrackArea) land inert-but-honest: the master lane draws fromS.waveformPeakstoday; stems light up when the engine caches theirs.Tests
tests/track_session_ui.test.mjs(new, 6): lane height clamp/default, density breakpoints, modest auto-fit (never shrinks below authored), the shared lane layout, folder-aware drop placement, and the escaped/drag-safe rename markup.tests/parts_view.test.js: a unified-row hit-test case (_partsTrackRowAtYPure); the@pureslice now stripsexportso the moved-to-exported pures still evaluate innew Function.tests/theme_chrome.test.mjs: two CSS-contract cases (responsive fold-away preserves identity controls; rename text-selection excluded from window dragging)._history_envfake element gained a faithful DOM-method surface (querySelector/classList/etc.) so headless suites don't trip on the new header refresh.Gates: 182 JS suites, pytest 291 / 2 skipped, lint 0 errors / 3 baseline warnings. Module-load smoke confirms no import cycle.
Remaining from the salvage queue
Stem playback engine + meters (the final PR).
🤖 Generated with Claude Code
https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Summary by CodeRabbit