fix(tiles): a tile focus no longer leaves a glow listener on its tab

_selectTiledSession added a once animationend listener to the focused
tile's tab on every focus. On every skin but OG the glow is `animation:
none`, so animationend never fires: the listeners piled up on the tab (and
the class stayed). It now glows a tab only when it is not glowing already,
so a tab holds at most one; on OG the animation ends, the class goes and
the next focus glows again.

Measured (50 tile focus changes, daylight-blue): animationend listeners on
the tabs 0 -> 49 before, 0 -> 6 (one per tab) after. Over the leak run's
20 grid open/close cycles the page's listener count grew 1003 -> 1042;
this is the part CDP could attribute.

Live, this code: 50 tile focus changes leave 6 animationend listeners on
the tabs, one per tab.

The single view's copy of the same block (app.js selectSession) has the
same leak on every non-OG skin; it is a separate, pre-existing copy and is
left alone here.

Test: ten focus changes leave one listener; after animationend the next
focus glows again; mutation-checked.

Scope: PR 2 (tile-grid.js).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 06:57:54 +02:00
parent 02387b5b16
commit d7f6047529
2 changed files with 29 additions and 1 deletions
+25
View File
@@ -19,7 +19,9 @@
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
FakeEl,
FakeTile,
bySelector,
idleCallbacks,
localStore,
makeGridApp,
@@ -63,6 +65,29 @@ describe('selectSession with the grid open', () => {
expect(app._focusedPane().sessionId).toBe('s-b');
});
it('focus changes leave at most one glow listener on a tab, and the glow runs again once it ended', async () => {
// On every skin but OG the glow is `animation: none`: animationend never
// fires, and every focus used to add one more once-listener to the tab.
const app = openGrid();
const tab = new FakeEl();
tab.className = 'session-tab active';
bySelector.set('.session-tab.active[data-id="s-b"]', tab);
for (let i = 0; i < 5; i++) {
await app.selectSession('s-b');
await app.selectSession('s-a');
}
expect(tab.classList.contains('tab-glow')).toBe(true);
expect(tab.listeners.animationend).toHaveLength(1);
// OG: the animation ends, the class goes, and the next focus glows again.
tab.dispatch('animationend');
tab.listeners.animationend = [];
expect(tab.classList.contains('tab-glow')).toBe(false);
await app.selectSession('s-b');
expect(tab.classList.contains('tab-glow')).toBe(true);
expect(tab.listeners.animationend).toHaveLength(1);
});
it('a user-initiated focus acknowledges the idle alert; an `auto` one does not', async () => {
const app = openGrid();
await app.selectSession('s-b');