Skip to content

feat(editor): live Tab view — engraved tablature as a third view mode - #273

Merged
byrongamatos merged 7 commits into
mainfrom
feat/editor-tab-view-live
Jul 15, 2026
Merged

feat(editor): live Tab view — engraved tablature as a third view mode#273
byrongamatos merged 7 commits into
mainfrom
feat/editor-tab-view-live

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

The view cycle grows a third full mode — String → Piano roll → Tab: the timeline area becomes live-engraved tablature of the track being edited, re-rendered on every committed edit. No save, no GP5 round-trip, no other plugin — this is the current in-memory chart, unlike the existing Tab preview modal (which stays: it proofreads the last-saved pack).

v1 contract:

  • Tab staff, read + click-to-select: clicking an engraved beat selects its source notes and seeks the playhead; editing stays in the timeline views (whose shortcuts keep working while the score refreshes live). Standard notation is the planned follow-up.
  • Fretted tracks only (keys/drums guard with the same test as the preview).
  • Documented quantization limits: onsets on a 16th grid in the beat domain (variable tempo maps engrave correctly), durations are gap-based with greedy rest fill (bars always sum exactly — the engraving can never drift out of alignment); sustains/ties/triplets not engraved yet.

How

  • src/alphatex.js — a pure generator (_alphaTexFromNotesPure): notes + beat grid + injected beatOf → alphaTex text, plus a beatMap[barIdx][beatIdx] → source-note refs aligned 1:1 with emitted beats (rests are null) — exactly the coordinates alphaTab's click events report, so a click maps straight back to editor notes. Handles meter changes (\ts re-emission), chord grouping, the string-number flip, alphaTab's octave convention in \tuning (high E MIDI 64 = e5, deliberately NOT the editor's midiToNote flavour — inlined + commented so nobody "fixes" it), capo, and honest pickup/tail skip counts.
  • src/tab-view-live.js — the lens. S.tabViewMode is an orthogonal flag like partsViewMode; the other mode toggles clear it and the draw pass is the single visibility truth (pings while on, hides the moment it's off — no toggle needs teardown knowledge). Renders via the same pinned CDN alphaTab as the preview (shared memoized loader + font dir; enablePlayer is on ONLY for the interaction layer — no soundfont download, no cursor, the editor owns all audio). Click-to-select rides beatMouseDown plus a capture-phase DOM fallback (alphaTab stops propagation on events it consumes) through renderer.boundsLookup.getBeatAtPos.
  • Wiring: view-cycle (_editorCycleViewMode), shortcut-registry row (toggleTabView, unbound by default), View menu entry, session-teardown api destroy, mount div in screen.html.

One ordering trap found live and pinned in a comment: _render must call _ensureApi before assigning _beatMap — the api build path runs _destroyApi(), which nulls the map (this silently disarmed click-to-select until a probe caught it).

Testing

  • tests/alphatex.test.mjs (new, 7 cases, fails on main): octave convention, string flip, chord + rest fill exactness, meter-change \ts, beatMap↔token alignment, pickup/tail counts, header material.
  • Full suites green: 147 pass / 0 fail, lint at the 3-warning baseline (no new warnings), screen-markup sibling list updated.
  • Live-verified on :8000-style testbed (AC/DC pak, 104 bars): full engraving (title, tuning legend, notation+tab staves), click selects the opening 3-note chord and seeks to 4.294s, re-click after re-render still selects, live-edit refresh keeps the view up, exiting hides the mount, zero page errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features
    • Added live Tab view with engraved tablature for the currently edited fretted track, updating as you edit.
    • Added a Tab view toggle to the View menu and view cycle (String → Piano roll → Tab), plus a dedicated shortcut command.
  • Mode Improvements
    • Tab view supports beat-level interaction: click a beat to select notes and move the playhead; engraving is quantized on a 16th grid with defined pickup/tail behavior.
    • Tab view is kept mutually exclusive with other editor modes and is cleaned up on screen reinjection; it’s disabled for non-fretted tracks.
  • Tests
    • Added/expanded coverage for Tab engraving output, beat-to-source mapping, and Tab view cycling behavior.

The view cycle grows a third lens (String -> Piano roll -> Tab): the
timeline area becomes live-engraved tablature of the current in-memory
chart, re-rendered (debounced, editGen-keyed) on every committed edit —
no save, no GP round-trip, unlike the saved-pack Tab preview modal.

- src/alphatex.js: pure generator — current fretted arrangement ->
  alphaTex on a 16th grid in the BEAT domain (variable tempo maps
  engrave correctly), gap-based durations with greedy rest fill so bars
  always sum exactly, \ts on meter change, chord grouping, string-number
  flip, alphaTab's octave convention in \tuning (high E = e5), and a
  beatMap aligned 1:1 with emitted beats for click mapping. Pickup/tail
  notes are skipped and counted, never silently dropped.
- src/tab-view-live.js: the lens — S.tabViewMode flag cleared by the
  other mode toggles, draw() pings visibility (single source of truth),
  fretted-only guard, click-to-select: alphaTab's beatMouseDown plus a
  CAPTURE-phase DOM fallback (alphaTab stops propagation on events it
  consumes) map the clicked beat through the beatMap to the source
  notes -> S.sel + seek. Renderer is the same pinned CDN alphaTab as
  the preview (shared loader + font dir; enablePlayer only for the
  interaction layer — no soundfont, no cursor, editor owns audio).
- Wiring: view-cycle + shortcut registry row (toggleTabView), menu bar
  entry, teardown-registered api destroy, mount in screen.html.

Ordering trap fixed en route: _render must call _ensureApi BEFORE
assigning _beatMap — the api build path runs _destroyApi(), which nulls
the map and silently disarmed click-to-select.

Verified live on :8001 (AC/DC pak): full engraving, click selects the
opening chord (3 notes) + seeks, re-click after re-render still selects,
live-edit refresh keeps the view, exit hides the mount, zero page errors.

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

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3702f90-b118-46c3-bdd3-e808eca3e357

📥 Commits

Reviewing files that changed from the base of the PR and between 7088110 and cacf83f.

📒 Files selected for processing (1)
  • tests/tab_view_cycle.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/tab_view_cycle.test.mjs

📝 Walkthrough

Walkthrough

Adds a live alphaTab tablature view with alphaTex generation, beat selection, synchronized redraws, view controls, mutually exclusive editor modes, lifecycle teardown, and related tests and documentation.

Changes

Live Tab View

Layer / File(s) Summary
alphaTex generation contract
src/alphatex.js, tests/alphatex.test.mjs
Generates quantized alphaTex with tuning, capo, rests, chords, duration decomposition, beat mappings, and pickup/tail accounting; tests cover meter-aware engraving contracts.
Live engraving renderer
screen.html, src/tab-preview.js, src/tab-view-live.js
Adds the tab mount, reuses the pinned alphaTab loader and font directory, renders current fretted arrangements, handles beat clicks, debounces updates, and tears down the alphaTab instance.
Editor mode and command integration
src/state.js, src/main.js, src/input.js, src/menu-bar.js, src/shortcuts.js, src/key-view.js, src/parts-view.js, src/drum.js, src/tempo.js, tests/tab_view_cycle.test.mjs
Adds tab-view state and commands, integrates drawing and global controls, updates view cycling, tests cycle behavior, and disables tab view when entering drum, parts, or tempo-map modes.
Markup and release documentation
CHANGELOG.md, tests/screen_markup.test.mjs
Documents tab-view behavior and preserves overlay markup assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor as Editor draw pass
  participant TabView as Live Tab View
  participant Generator as alphaTex generator
  participant AlphaTab as alphaTab renderer
  Editor->>TabView: ping while tab view is active
  TabView->>Generator: generate current arrangement
  Generator-->>TabView: return tex and beatMap
  TabView->>AlphaTab: engrave tex
  AlphaTab-->>TabView: emit beat click
  TabView->>Editor: select notes and move playhead
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a live Tab view as a third editor view mode.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/editor-tab-view-live

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/key-view.js (1)

138-158: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard drums in the cycle path too
viewFor(arr) can still return 'piano' for a drums-named arrangement, and this branch then calls editorToggleTabView(true), which refuses drums without changing mode. The shortcut can loop here forever instead of advancing to Tab/String. Add the same /^drums/i check here or skip straight to editorSetViewMode('string').

🤖 Prompt for 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.

In `@src/key-view.js` around lines 138 - 158, Update _editorCycleViewMode to
explicitly guard arrangements whose names match /^drums/i before the
viewFor(arr) piano branch. Ensure drums skip the Tab toggle and advance directly
to the appropriate string view, preventing the cycle from getting stuck while
preserving existing keys-track handling.
🤖 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 `@src/main.js`:
- Around line 226-231: Update the S.tabViewMode branch in the main update flow
so BPM and time-signature displays are refreshed before returning from
_tabViewPing(). Ensure updateBPMDisplay() and updateTempoSigDisplay() still run
during Tab view updates while preserving the existing _tabViewPing() behavior
and non-Tab mode flow.

---

Outside diff comments:
In `@src/key-view.js`:
- Around line 138-158: Update _editorCycleViewMode to explicitly guard
arrangements whose names match /^drums/i before the viewFor(arr) piano branch.
Ensure drums skip the Tab toggle and advance directly to the appropriate string
view, preventing the cycle from getting stuck while preserving existing
keys-track handling.
🪄 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: 1179ce0d-9b3d-4134-ac55-44c3c21f582a

📥 Commits

Reviewing files that changed from the base of the PR and between d7c5e3c and ac102e8.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • screen.html
  • src/alphatex.js
  • src/drum.js
  • src/input.js
  • src/key-view.js
  • src/main.js
  • src/menu-bar.js
  • src/parts-view.js
  • src/shortcuts.js
  • src/state.js
  • src/tab-preview.js
  • src/tab-view-live.js
  • src/tempo.js
  • tests/alphatex.test.mjs
  • tests/screen_markup.test.mjs

Comment thread src/main.js
ChrisBeWithYou and others added 2 commits July 14, 2026 15:00
…en.html overlays, input.js dispatch, main.js imports/drawNow, screen_markup sibling list)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…ip the Tab stop in the view cycle

Two findings from the community review:
- updateBPMDisplay/updateTempoSigDisplay now run before ANY mode fork in
  drawNow, so undo/redo changing tempo or meter refreshes the toolbar
  readouts even while the Tab lens owns the timeline.
- The view cycle skips the Tab stop for drums tracks (the lens refuses
  drums without changing mode, which left the cycle stuck on the roll);
  new tests/tab_view_cycle.test.mjs pins the full cycle including the
  drums skip and the leave-restores-String path.

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

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (3)
tests/tab_view_cycle.test.mjs (3)

34-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

seed()'s storedPiano param/return is dead code.

Every call site (seed('Lead'), seed('Drums')) passes only the name argument, and no caller uses the return value — tests 2/3 set localStorage.getItem manually right after calling seed() instead. The parameter and return storedPiano; don't do anything and make the comment above ("the piano case being driven by the caller below") harder to map to actual behavior.

🧹 Proposed cleanup
-function seed(name, storedPiano) {
+function seed(name) {
     Object.assign(S, { arrangements: [{ name, id: 'p1' }], currentArr: 0, tabViewMode: false, filename: '' });
     calls.length = 0;
-    // no filename → no stored view pref, so viewFor falls back on the name;
-    // fake "currently on the roll" via the tab of stored prefs being empty
-    // and the piano case being driven by the caller below.
-    return storedPiano;
+    // no filename → no stored view pref, so viewFor falls back on the name;
+    // callers that need the "on the roll" state stub localStorage.getItem
+    // themselves right after calling seed().
 }
🤖 Prompt for 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.

In `@tests/tab_view_cycle.test.mjs` around lines 34 - 41, Remove the unused
storedPiano parameter and return statement from seed, and update its comment to
describe only the state initialization and caller-controlled localStorage
behavior. Keep all existing seed call sites and test behavior unchanged.

1-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring claims the keys refusal path is "pinned" here, but no test exercises it.

The comment states this file pins both refusal paths — drums skipping Tab and "a keys track never cycles at all" — but only the drums case (lines 59-70) has an actual t(...) test. The KEYS_PATTERN branch in _editorCycleViewMode() (calling setStatus(...) and returning without dispatching any view change) is untested here, so a regression on that path wouldn't be caught by this suite despite the file's stated intent.

Consider adding a fifth test seeding a keys-named arrangement and asserting calls stays empty (no setView/tab dispatch).

t('a keys track never cycles (always stays on the roll)', () => {
    seed('Keys');
    _editorCycleViewMode();
    assert.deepStrictEqual(calls, [], 'keys track must not dispatch any view change');
});
🤖 Prompt for 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.

In `@tests/tab_view_cycle.test.mjs` around lines 1 - 10, Add a test alongside the
existing view-cycle tests that seeds a keys-named arrangement, invokes
_editorCycleViewMode(), and asserts the calls collection remains empty, covering
the KEYS_PATTERN refusal path without any view-change dispatch.

49-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

localStorage.getItem stub isn't restored if the assertion fails, risking leakage into later tests.

Both tests stub localStorage.getItem then reset it on the line right after assert.deepStrictEqual. If the assertion throws (caught by t()'s try/catch), the reset never runs and the stale stub carries into subsequent tests via the shared globalThis.localStorage. It happens not to break the current 4-test suite (test 4 takes the tabViewMode branch unconditionally), but it's fragile for any future test added after these two.

🧹 Proposed fix using try/finally
 t('a fretted track on the roll advances INTO the Tab lens', () => {
     seed('Lead');
     Object.assign(S, { filename: 'y.feedpak' });
     localStorage.getItem = (k) => k.startsWith('editorViewPref:') ? JSON.stringify({ p1: 'piano' }) : null;
     calls.length = 0;
-    _editorCycleViewMode();
-    assert.deepStrictEqual(calls, [['tab', true]], 'roll → Tab lens');
-    localStorage.getItem = () => null;
+    try {
+        _editorCycleViewMode();
+        assert.deepStrictEqual(calls, [['tab', true]], 'roll → Tab lens');
+    } finally {
+        localStorage.getItem = () => null;
+    }
 });

Apply the same try/finally wrapping to the drums test (lines 59-70).

🤖 Prompt for 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.

In `@tests/tab_view_cycle.test.mjs` around lines 49 - 70, Wrap the stubbed
localStorage.getItem usage in both tests, “a fretted track on the roll advances
INTO the Tab lens” and “a drums track on the roll SKIPS Tab and wraps to
String,” with try/finally blocks, placing the reset to the original no-op
behavior in finally so it always executes even when assertions fail.
🤖 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.

Nitpick comments:
In `@tests/tab_view_cycle.test.mjs`:
- Around line 34-41: Remove the unused storedPiano parameter and return
statement from seed, and update its comment to describe only the state
initialization and caller-controlled localStorage behavior. Keep all existing
seed call sites and test behavior unchanged.
- Around line 1-10: Add a test alongside the existing view-cycle tests that
seeds a keys-named arrangement, invokes _editorCycleViewMode(), and asserts the
calls collection remains empty, covering the KEYS_PATTERN refusal path without
any view-change dispatch.
- Around line 49-70: Wrap the stubbed localStorage.getItem usage in both tests,
“a fretted track on the roll advances INTO the Tab lens” and “a drums track on
the roll SKIPS Tab and wraps to String,” with try/finally blocks, placing the
reset to the original no-op behavior in finally so it always executes even when
assertions fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a4c9de3c-2f9c-4d79-8196-6311dd16004f

📥 Commits

Reviewing files that changed from the base of the PR and between ac102e8 and a631e89.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • screen.html
  • src/input.js
  • src/key-view.js
  • src/main.js
  • src/menu-bar.js
  • src/shortcuts.js
  • src/state.js
  • src/tempo.js
  • tests/screen_markup.test.mjs
  • tests/tab_view_cycle.test.mjs
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/state.js
  • screen.html
  • src/shortcuts.js
  • src/key-view.js
  • src/menu-bar.js
  • tests/screen_markup.test.mjs
  • src/main.js
  • CHANGELOG.md

 item 1)

alphaTex durations are absolute (:8 is an eighth in any meter), but the
generator hard-coded 4 ticks per ruler beat, so a 6/8 bar engraved six
QUARTER durations (overfull). Bar tick capacity now derives from the
meter's denominator (16/den ticks per beat, quarter fallback) in both
the note-bucketing and emission passes; the 16th quantization grid and
beatMap alignment are unchanged, and 4/4 markup is bit-identical.
Adds 6/8 + 7/8 suites (notes, rest fill, 16th subdivisions, meter
change, exact bar-total sums) that fail on the pre-fix code.

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

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
tests/alphatex.test.mjs (1)

133-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

barWhole silently under-counts unmatched tokens.

The regex encodes the current alphaTex token grammar (rest/chord/note). If a future token shape is introduced (dotted duration, tie, tuplet, grace note) and doesn't match, it contributes 0 to the sum instead of failing — so a bar-total assertion could silently pass on an incomplete/incorrect parse rather than catching the regression.

Consider asserting that the regex consumed every token (e.g., compare match count against s.trim().split(/\s+/).length, or throw if leftover unmatched text remains) so grammar drift fails loudly instead of silently.

♻️ Proposed defensive check
 function barWhole(bar) {
     const s = bar.replace(/\\ts \d+ \d+ ?/, '');
     let sum = 0, m;
     const re = /(?:r|\([^)]*\)|\d+\.\d+)\.(\d+)/g;
-    while ((m = re.exec(s))) sum += 1 / Number(m[1]);
+    let matched = '';
+    while ((m = re.exec(s))) { sum += 1 / Number(m[1]); matched += m[0]; }
+    if (matched.replace(/\s+/g, '') !== s.trim().replace(/\s+/g, '')) {
+        throw new Error(`barWhole: unmatched tokens in "${s}"`);
+    }
     return sum;
 }
🤖 Prompt for 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.

In `@tests/alphatex.test.mjs` around lines 133 - 141, Update barWhole so its
token-matching logic verifies that every non-whitespace token in the normalized
bar string is consumed by the regex; if any token is unmatched, fail loudly
instead of omitting it from the duration sum. Preserve the existing duration
calculation for valid rest, chord, and note tokens.
🤖 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.

Nitpick comments:
In `@tests/alphatex.test.mjs`:
- Around line 133-141: Update barWhole so its token-matching logic verifies that
every non-whitespace token in the normalized bar string is consumed by the
regex; if any token is unmatched, fail loudly instead of omitting it from the
duration sum. Preserve the existing duration calculation for valid rest, chord,
and note tokens.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ea3ba4ac-fb68-4e67-9304-4f28a5df16c3

📥 Commits

Reviewing files that changed from the base of the PR and between a631e89 and fa85881.

📒 Files selected for processing (2)
  • src/alphatex.js
  • tests/alphatex.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/alphatex.js

byrongamatos added a commit that referenced this pull request Jul 15, 2026
…build

The Score-staff switch made _ensureApi rebuild the alphaTab api on the
SAME mount node (staveProfile is a construction-time setting). But
_destroyApi only tore down alphaTab's own api — it never removed our
capture-phase mousedown fallback, which is our own closure. So every
staff toggle stacked another live listener on the surviving mount, and
each subsequent beat click fired select() / editorSeekToTime() / status
updates once per accumulated listener.

Root cause: the manual mount.addEventListener('mousedown', ..., true) in
_ensureApi had no matching removeEventListener. Name the handler, drop it
in _destroyApi before nulling _apiMount. In PR #273 this path never fired
on a surviving node (rebuild happened only on a NEW mount, GC'd the old
listener), so the leak is specific to the staff-switch guard this PR adds.

Regression test (tests/tab_view_staff.test.mjs): three staff switches on
one mount keep exactly one mousedown listener; fails pre-fix (2 !== 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The entry toggle refuses keys/drums, but switching TO such a track via the
arrangement dropdown while the lens was already on bypassed that guard
(editorSelectArrangement never cleared S.tabViewMode), and the view-cycle's
own keys short-circuit returns before it can un-toggle either. The user was
left stuck: the generator engraved `undefined.NaN.*` from keys/drums notes
(no .string/.fret), alphaTab errored, and cycling couldn't escape.

Enforce at the draw pass — the single visibility source of truth — so every
currentArr-change entry point is covered by one guard: if the current track
isn't fretted, drop the lens and let its normal view (the roll) take over.

Regression tests in tab_view_cycle.test.mjs pin the keys/drums exit and the
fretted keep-on (both new cases fail pre-fix).

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 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 `@tests/tab_view_cycle.test.mjs`:
- Around line 105-111: Update the test around _tabViewPing so the 150 ms
debounced render is cleaned up before the test completes. Use the existing
tab-view teardown to cancel or flush the pending callback, or control it with a
fake timer and restore the timer state afterward, while preserving the
synchronous assertions.
🪄 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: 2b5d19a1-7ce7-4401-8d3d-60715bb2a5b9

📥 Commits

Reviewing files that changed from the base of the PR and between fa85881 and 7088110.

📒 Files selected for processing (2)
  • src/tab-view-live.js
  • tests/tab_view_cycle.test.mjs

Comment thread tests/tab_view_cycle.test.mjs
@byrongamatos
byrongamatos merged commit f1eda8c into main Jul 15, 2026
3 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-tab-view-live branch July 15, 2026 20:25
byrongamatos added a commit that referenced this pull request Jul 15, 2026
…build

The Score-staff switch made _ensureApi rebuild the alphaTab api on the
SAME mount node (staveProfile is a construction-time setting). But
_destroyApi only tore down alphaTab's own api — it never removed our
capture-phase mousedown fallback, which is our own closure. So every
staff toggle stacked another live listener on the surviving mount, and
each subsequent beat click fired select() / editorSeekToTime() / status
updates once per accumulated listener.

Root cause: the manual mount.addEventListener('mousedown', ..., true) in
_ensureApi had no matching removeEventListener. Name the handler, drop it
in _destroyApi before nulling _apiMount. In PR #273 this path never fired
on a surviving node (rebuild happened only on a NEW mount, GC'd the old
listener), so the leak is specific to the staff-switch guard this PR adds.

Regression test (tests/tab_view_staff.test.mjs): three staff switches on
one mount keep exactly one mousedown listener; fails pre-fix (2 !== 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 15, 2026
#288)

* feat(editor): standard notation in the score view (View > Score staff)

The live score view grows a staff preference: tablature only (default,
per the v1 call), standard notation only, or both staves together —
same generated alphaTex either way, alphaTab derives pitch from
tuning + fret, so notation comes free of new generation code.

- tab-view-live.js: _scoreStaffProfilePure maps the preference to
  alphaTab's StaveProfile key (unknown/legacy stored values degrade to
  tab, never throw into the renderer); the api tracks the staff it was
  built with and rebuilds on change (staveProfile is construction-time);
  editorSetTabViewStaff validates + persists (localStorage, a reading
  preference like loop-snap) and ENTERS the view when it's off.
- menu-bar.js: a Score-staff radio trio in the View menu, same ctx/
  dispatch pattern as the loop-snap trio; checkmarks resolve at open.
- state.js: S.tabViewStaff ('tab' default).

Live-verified on :8001 (AC/DC pak): all three staves engrave (tab
frets / treble-clef pitches with accidentals / braced grand system),
click-to-select works under 'both' (3 notes), the real View-menu radio
renders + dispatches + enters the view, preference persists. 148/148,
lint baseline, new tab_view_staff suite + menu-model radio pin.

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

* fix(editor): remove stale tab-view click listener on staff-profile rebuild

The Score-staff switch made _ensureApi rebuild the alphaTab api on the
SAME mount node (staveProfile is a construction-time setting). But
_destroyApi only tore down alphaTab's own api — it never removed our
capture-phase mousedown fallback, which is our own closure. So every
staff toggle stacked another live listener on the surviving mount, and
each subsequent beat click fired select() / editorSeekToTime() / status
updates once per accumulated listener.

Root cause: the manual mount.addEventListener('mousedown', ..., true) in
_ensureApi had no matching removeEventListener. Name the handler, drop it
in _destroyApi before nulling _apiMount. In PR #273 this path never fired
on a surviving node (rebuild happened only on a NEW mount, GC'd the old
listener), so the leak is specific to the staff-switch guard this PR adds.

Regression test (tests/tab_view_staff.test.mjs): three staff switches on
one mount keep exactly one mousedown listener; fails pre-fix (2 !== 1).

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>
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