Fix flickering / wrong panes in the wide-pane Target picker

The Target dropdown keyed panes off feedBackSplitscreen.panelIndexFor,
which can return the focused index for any canvas — so both split panes'
keys ping-ponged, rebuilding the <select> every frame (flicker) and
listing wrong/duplicate entries. The registry also never dropped panes
from a prior song or a closed split.

- Key each pane by a stable per-renderer-instance id (_paneUid, assigned
  once in init) instead of the split panel index.
- Prune panes not reported within ~1.5s (song change / split teardown).
- Mark the dropdown dirty only when the pane SET changes, not on every
  per-frame re-report, and skip rebuilding while the <select> is focused.
- Hide the Target row entirely when there's a single pane.
- Label panes by arrangement name, falling back to "Pane N".

Per-pane overrides are now session-only (keyed by ephemeral instance ids),
so they're no longer persisted to localStorage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-01 02:01:41 -04:00
co-authored by Claude Opus 4.8
parent 9f0d48cb1f
commit 6c42d83c31
2 changed files with 73 additions and 18 deletions
+18
View File
@@ -174,6 +174,24 @@ test('a Target select and pane registry drive the per-pane picker', () => {
'camUpdate must register its pane each frame');
});
test('panes are keyed by a stable per-instance id, not the split panel index', () => {
// Keying by a stable instance uid keeps the Target picker steady; the split
// plugin's panel index can ping-pong with focus and cause flicker/dupes.
assert.match(src, /_paneUid\s*=\s*\+\+\s*_aspectPaneCounter/,
'each renderer instance must take a stable pane uid in init()');
assert.match(src, /const\s+_paneKey\s*=\s*'pane'\s*\+\s*_paneUid\s*;/,
'camUpdate must key the pane by its stable instance uid');
});
test('the target dropdown prunes dead panes and does not rebuild while focused', () => {
assert.match(src, /function\s+_aspectPrunePanes\s*\(\)[\s\S]*?delete\s+reg\[k\]/,
'_aspectPrunePanes must drop panes not seen recently');
assert.match(src, /_aspectPrunePanes\(\)\s*;[\s\S]*?if\s*\(\s*_aspectPanesDirty\s*\)\s*_aspectBuildTargets\(\)/,
'the readout tick must prune then rebuild only when dirty');
assert.match(src, /function\s+_aspectBuildTargets\s*\(\)[\s\S]*?document\.activeElement\s*===\s*_aspectTargetSel[\s\S]*?return/,
'_aspectBuildTargets must skip rebuilding while the select is focused');
});
test('the panel has a dismiss (close) control', () => {
assert.match(
src,