From 7e737cb4aa5ae86ba87a591ad96bfa6780d16446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Wed, 3 Jun 2026 12:42:03 +0800 Subject: [PATCH] fix(desktop): restore tab bar window dragging Electron handles custom draggable chrome through CSS app-region rules, not the old runtime startDragging path. Mark the tab strip and empty scroll gutter as native drag regions while keeping tab items and controls explicitly no-drag so tab reordering and close/tool buttons keep receiving pointer events. Constraint: Electron drag regions swallow pointer events unless interactive children are marked no-drag Rejected: Keep calling startDragging from the empty gutter | Electron desktopHost does not expose that as the active migration path Confidence: high Scope-risk: narrow Directive: Do not mark tab items themselves as drag regions without revalidating tab reorder behavior Tested: cd desktop && bun run test src/components/layout/TabBar.test.tsx --run Tested: cd desktop && bun run lint Tested: bun run check:desktop --- desktop/src/components/layout/TabBar.test.tsx | 27 ++++++++++--------- desktop/src/components/layout/TabBar.tsx | 22 ++++----------- desktop/src/theme/globals.css | 8 ++++-- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx index 1b8ad014..9ce9babe 100644 --- a/desktop/src/components/layout/TabBar.test.tsx +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -307,8 +307,10 @@ describe('TabBar', () => { render() }) - expect(screen.getByTestId('tab-bar')).not.toHaveAttribute('data-desktop-drag-region') + expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-desktop-drag-region') + expect(screen.getByTestId('tab-bar-scroll-region')).toHaveAttribute('data-desktop-drag-region') expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-desktop-drag-region') + expect(screen.getByText('Untitled Session').closest('.tab-bar-interactive')).toBeInTheDocument() }) it('keeps the desktop tab strip at a roomier titlebar height', async () => { @@ -332,7 +334,7 @@ describe('TabBar', () => { }) const tabBar = screen.getByTestId('tab-bar') - const tab = screen.getByText('Untitled Session').closest('.tab-bar-hit-area') + const tab = screen.getByText('Untitled Session').closest('.tab-bar-interactive') expect(tabBar).toHaveClass('min-h-11') expect(tab).toHaveClass('min-h-11') @@ -506,7 +508,7 @@ describe('TabBar', () => { expect(screen.queryByTestId('open-project-menu')).not.toBeInTheDocument() }) - it('starts dragging when clicking the empty tab-bar gutter', async () => { + it('marks the empty tab-bar gutter as a native drag region without runtime dragging', async () => { const { TabBar } = await import('./TabBar') const { useTabStore } = await import('../../stores/tabStore') const { useChatStore } = await import('../../stores/chatStore') @@ -526,14 +528,13 @@ describe('TabBar', () => { render() }) - const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden') + const scrollRegion = screen.getByTestId('tab-bar-scroll-region') expect(scrollRegion).toBeInTheDocument() + expect(scrollRegion).toHaveAttribute('data-desktop-drag-region') - fireEvent.mouseDown(scrollRegion!) + fireEvent.mouseDown(scrollRegion) - await waitFor(() => { - expect(startDraggingMock).toHaveBeenCalledTimes(1) - }) + expect(startDraggingMock).not.toHaveBeenCalled() }) it('does not start dragging when clicking a tab', async () => { @@ -582,10 +583,10 @@ describe('TabBar', () => { render() }) - expect(screen.getByTestId('tab-bar').querySelector('.tab-bar-hit-area')).toBeInTheDocument() + expect(screen.getByTestId('tab-bar').querySelector('.tab-bar-interactive')).toBeInTheDocument() - const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area') - const secondTab = screen.getByText('Second Session').closest('.tab-bar-hit-area') + const firstTab = screen.getByText('First Session').closest('.tab-bar-interactive') + const secondTab = screen.getByText('Second Session').closest('.tab-bar-interactive') expect(firstTab).toBeTruthy() expect(secondTab).toBeTruthy() @@ -630,7 +631,7 @@ describe('TabBar', () => { render() }) - const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area') + const firstTab = screen.getByText('First Session').closest('.tab-bar-interactive') expect(firstTab).toBeTruthy() fireEvent.mouseDown(firstTab!, { button: 0, clientX: 20, clientY: 10 }) @@ -664,7 +665,7 @@ describe('TabBar', () => { render() }) - const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area') + const firstTab = screen.getByText('First Session').closest('.tab-bar-interactive') const closeButton = screen.getByLabelText('Close First Session') expect(firstTab).toHaveClass('group') diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 90f13ef4..5b2e087e 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -22,7 +22,6 @@ const TAB_WIDTH = 180 const DRAG_START_THRESHOLD = 4 const desktopHost = getDesktopHost() const isDesktopRuntime = desktopHost.isDesktop -const canStartWindowDragging = desktopHost.capabilities.windowControls type PendingCloseRequest = { tabs: Tab[] @@ -92,7 +91,6 @@ export function TabBar() { const pendingDragRef = useRef<{ index: number; startX: number; startY: number } | null>(null) const suppressClickRef = useRef(false) const tabRefs = useRef(new Map()) - const startDraggingRef = useRef<(() => Promise) | null>(null) const t = useTranslation() const runningSessionIds = useMemo(() => { const ids = new Set() @@ -105,11 +103,6 @@ export function TabBar() { return ids }, [activeChatSessionIds, tabs]) - useEffect(() => { - if (!canStartWindowDragging) return - startDraggingRef.current = () => getDesktopHost().window.startDragging() - }, []) - const updateScrollState = useCallback(() => { const el = scrollRef.current if (!el) return @@ -326,16 +319,10 @@ export function TabBar() { setActiveTab(sessionId) } - 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(() => {}) - }, []) - return (
@@ -347,9 +334,10 @@ export function TabBar() {
e.preventDefault()} - onMouseDown={handleScrollRegionMouseDown} > {tabs.map((tab, index) => (