diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx index 784e19bd..4d2bde3b 100644 --- a/desktop/src/components/layout/TabBar.test.tsx +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -9,6 +9,7 @@ const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({ const windowControlsMock = vi.hoisted(() => ({ show: true, })) +const scrollIntoViewMock = vi.hoisted(() => vi.fn()) vi.mock('@tauri-apps/api/window', () => ({ getCurrentWindow: getCurrentWindowMock, @@ -64,8 +65,14 @@ describe('TabBar', () => { value: {}, }) + Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', { + configurable: true, + value: scrollIntoViewMock, + }) + startDraggingMock.mockClear() getCurrentWindowMock.mockClear() + scrollIntoViewMock.mockClear() windowControlsMock.show = true vi.resetModules() }) @@ -88,6 +95,42 @@ describe('TabBar', () => { delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__ }) + it('scrolls the active tab into view when the active tab changes', async () => { + const { TabBar } = await import('./TabBar') + const { useTabStore } = await import('../../stores/tabStore') + const { useChatStore } = await import('../../stores/chatStore') + + useTabStore.setState({ + tabs: [ + { sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' }, + { sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' }, + { sessionId: 'tab-3', title: 'Third Session', type: 'session', status: 'idle' }, + { sessionId: 'tab-4', title: 'Fourth Session', type: 'session', status: 'idle' }, + { sessionId: 'tab-5', title: 'New Session', type: 'session', status: 'idle' }, + ], + activeTabId: 'tab-1', + }) + useChatStore.setState({ + sessions: {}, + disconnectSession: vi.fn(), + } as Partial>) + + await act(async () => { + render() + }) + scrollIntoViewMock.mockClear() + + await act(async () => { + useTabStore.getState().setActiveTab('tab-5') + }) + + expect(scrollIntoViewMock).toHaveBeenCalledWith({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }) + }) + it('keeps the overflow button flush against window controls on Windows', async () => { const { TabBar } = await import('./TabBar') const { useTabStore } = await import('../../stores/tabStore') diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 2eaface7..b29a587b 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -93,6 +93,21 @@ export function TabBar() { } }, [updateScrollState, tabs.length]) + useEffect(() => { + if (!activeTabId) return + const activeTabEl = tabRefs.current.get(activeTabId) + if (!activeTabEl) return + + activeTabEl.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }) + + const frame = window.requestAnimationFrame(updateScrollState) + return () => window.cancelAnimationFrame(frame) + }, [activeTabId, tabs.length, updateScrollState]) + useEffect(() => { if (!contextMenu) return const close = () => setContextMenu(null)