diff --git a/src/web/public/tile-grid.js b/src/web/public/tile-grid.js index 74c457ac..c8c04f85 100644 --- a/src/web/public/tile-grid.js +++ b/src/web/public/tile-grid.js @@ -1535,7 +1535,10 @@ Object.assign(CodemanApp.prototype, { this.closeSessionSidebarOnHandheld?.(); this.renderSessionTabs?.(); const activeTab = document.querySelector(`.session-tab.active[data-id="${sessionId}"]`); - if (activeTab) { + // Not while it still glows: on every skin but OG the glow is `animation: + // none`, so animationend never comes and each tile focus used to leave + // another listener behind on the tab (49 after 50 focus changes). + if (activeTab && !activeTab.classList.contains('tab-glow')) { activeTab.classList.add('tab-glow'); activeTab.addEventListener('animationend', () => activeTab.classList.remove('tab-glow'), { once: true }); } diff --git a/test/tile-grid-select-branch.test.ts b/test/tile-grid-select-branch.test.ts index 3599f965..b52b1a91 100644 --- a/test/tile-grid-select-branch.test.ts +++ b/test/tile-grid-select-branch.test.ts @@ -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');