diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx index b38d04c0..1ff869c5 100644 --- a/desktop/src/components/layout/TabBar.test.tsx +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -2,6 +2,15 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import '@testing-library/jest-dom' +const startDraggingMock = vi.hoisted(() => vi.fn(() => Promise.resolve())) +const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({ + startDragging: startDraggingMock, +}))) + +vi.mock('@tauri-apps/api/window', () => ({ + getCurrentWindow: getCurrentWindowMock, +})) + vi.mock('../../i18n', () => ({ useTranslation: () => (key: string) => { const translations: Record = { @@ -42,6 +51,13 @@ describe('TabBar', () => { value: ResizeObserverMock, }) + Object.defineProperty(window, '__TAURI__', { + configurable: true, + value: {}, + }) + + startDraggingMock.mockClear() + getCurrentWindowMock.mockClear() vi.resetModules() }) @@ -53,6 +69,8 @@ describe('TabBar', () => { useChatStore.setState({ sessions: {}, } as Partial>) + + delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__ }) it('keeps the overflow button flush against window controls on Windows', async () => { @@ -133,4 +151,67 @@ describe('TabBar', () => { expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-tauri-drag-region') }) + + it('starts dragging when clicking the empty tab-bar gutter', 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: 'Untitled Session', type: 'session', status: 'idle' }, + ], + activeTabId: 'tab-1', + }) + useChatStore.setState({ + sessions: {}, + disconnectSession: vi.fn(), + } as Partial>) + + await act(async () => { + render() + }) + + await waitFor(() => { + expect(getCurrentWindowMock).toHaveBeenCalled() + }) + + const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden') + expect(scrollRegion).toBeInTheDocument() + + fireEvent.mouseDown(scrollRegion!) + + await waitFor(() => { + expect(startDraggingMock).toHaveBeenCalledTimes(1) + }) + }) + + it('does not start dragging when clicking a tab', 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: 'Untitled Session', type: 'session', status: 'idle' }, + ], + activeTabId: 'tab-1', + }) + useChatStore.setState({ + sessions: {}, + disconnectSession: vi.fn(), + } as Partial>) + + await act(async () => { + render() + }) + + await waitFor(() => { + expect(getCurrentWindowMock).toHaveBeenCalled() + }) + + fireEvent.mouseDown(screen.getByText('Untitled Session')) + + expect(startDraggingMock).not.toHaveBeenCalled() + }) }) diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 54fb2b68..b4089000 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -5,6 +5,7 @@ import { useTranslation } from '../../i18n' import { WindowControls, showWindowControls } from './WindowControls' const TAB_WIDTH = 180 +const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window) export function TabBar() { const tabs = useTabStore((s) => s.tabs) @@ -21,8 +22,19 @@ export function TabBar() { const [closingTabId, setClosingTabId] = useState(null) const [dragOverIndex, setDragOverIndex] = useState(null) const dragIndexRef = useRef(null) + const startDraggingRef = useRef<(() => Promise) | null>(null) const t = useTranslation() + useEffect(() => { + if (!isTauri) return + import(/* @vite-ignore */ '@tauri-apps/api/window') + .then(({ getCurrentWindow }) => { + const win = getCurrentWindow() + startDraggingRef.current = () => win.startDragging() + }) + .catch(() => {}) + }, []) + const updateScrollState = useCallback(() => { const el = scrollRef.current if (!el) return @@ -145,6 +157,13 @@ export function TabBar() { setDragOverIndex(null) } + const handleScrollRegionMouseDown = useCallback((event: React.MouseEvent) => { + if (event.button !== 0 || event.target !== scrollRef.current) return + const startDragging = startDraggingRef.current + if (!startDragging) return + void startDragging().catch(() => {}) + }, []) + if (tabs.length === 0 && !showWindowControls) return null return ( @@ -160,7 +179,12 @@ export function TabBar() { )} -
e.preventDefault()}> +
e.preventDefault()} + onMouseDown={handleScrollRegionMouseDown} + > {tabs.map((tab, index) => (