From c7e8ff616f66ab5a71c53bbef8f02033c3c4f0a0 Mon Sep 17 00:00:00 2001 From: "Claude (Codeman maintainer)" Date: Sun, 14 Jun 2026 22:38:19 +0200 Subject: [PATCH] fix(tabs): re-evaluate auto-wrap on resize and on every full tab rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review polish on the desktop tab auto-wrap: - Auto-wrap is purely width-driven, but updateTabOverflowMode() was only called at the tail of _renderSessionTabsImmediate (SSE content renders). Window resize — the primary trigger for tabs crossing the one-row overflow threshold — never re-evaluated it, so narrowing/widening the window left the wrap state stale until an unrelated status event fired a render. Call it from the debounced window-resize handler (no-op on mobile/tablet, where the method bails). - Move the re-evaluation into _fullRenderSessionTabs() as well, so the incremental branch's two early `_fullRenderSessionTabs(); return;` paths (badge add/remove, which change tab width) and the manual two-rows toggle (applyTabWrapSettings → _fullRender…) re-evaluate too. The latter also fixes a transient where enabling manual two-rows while auto-wrap was on left both classes set (clipping folder tabs to 96px) until the next render. - Add boundary cases to the policy test: exact fit and the +1 sub-pixel tolerance (no wrap), 2px over (wrap), and a single overflowing tab (no wrap). Verified: tab-overflow test passes; tsc, check:frontend-syntax, check:public-assets, prettier all clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/web/public/app.js | 6 ++++++ src/web/public/mobile-handlers.js | 3 +++ test/tab-overflow.test.ts | 14 ++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/web/public/app.js b/src/web/public/app.js index 56541be1..8ef35206 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2765,6 +2765,12 @@ class CodemanApp { // Update connection lines after tabs change (positions may have shifted) this.updateConnectionLines(); + + // Re-evaluate desktop auto-wrap for every full rebuild, including the incremental + // branch's early `_fullRenderSessionTabs(); return;` paths and the manual two-rows + // toggle (applyTabWrapSettings calls this) which would otherwise leave a stale + // tabs-auto-wrap class until the next content render. + this.updateTabOverflowMode(); } // Set up arrow key navigation for session tabs (accessibility) diff --git a/src/web/public/mobile-handlers.js b/src/web/public/mobile-handlers.js index a24fd569..f55bd2e1 100644 --- a/src/web/public/mobile-handlers.js +++ b/src/web/public/mobile-handlers.js @@ -139,6 +139,9 @@ const MobileDetection = { resizeTimeout = setTimeout(() => { this.updateBodyClass(); this.updateAppHeight(); + // Tab auto-wrap is width-driven, so it must re-evaluate on resize — the only + // other trigger is a tab content render. No-op on mobile/tablet (method bails). + if (typeof app !== 'undefined') app.updateTabOverflowMode?.(); }, 100); }; window.addEventListener('resize', this._resizeHandler); diff --git a/test/tab-overflow.test.ts b/test/tab-overflow.test.ts index 6c3a006d..0ed7895a 100644 --- a/test/tab-overflow.test.ts +++ b/test/tab-overflow.test.ts @@ -48,4 +48,18 @@ describe('tab overflow layout policy', () => { }) ).toBe(false); }); + + it('respects the boundary conditions (exact fit, +1 tolerance, and tabCount < 2)', () => { + const helper = loadTabOverflowHelper(); + const base = { deviceType: 'desktop' as const, manualTwoRows: false, tabCount: 6 }; + + // Exact fit: no overflow, no wrap. + expect(helper.shouldAutoWrapTabs({ ...base, scrollWidth: 800, clientWidth: 800 })).toBe(false); + // Within the +1 sub-pixel tolerance: still no wrap. + expect(helper.shouldAutoWrapTabs({ ...base, scrollWidth: 801, clientWidth: 800 })).toBe(false); + // 2px over: wrap. + expect(helper.shouldAutoWrapTabs({ ...base, scrollWidth: 802, clientWidth: 800 })).toBe(true); + // A single overflowing tab must not wrap (need at least 2 to form a second row). + expect(helper.shouldAutoWrapTabs({ ...base, tabCount: 1, scrollWidth: 1400, clientWidth: 760 })).toBe(false); + }); });