Merge pull request #94 from NanmiCoder/codex/fix-issue-92-tabbar-drag

fix: restore desktop tab bar window dragging
This commit is contained in:
程序员阿江-Relakkes 2026-04-20 15:51:36 +08:00 committed by GitHub
commit d8e5ab6469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 106 additions and 1 deletions

View File

@ -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<string, string> = {
@ -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<ReturnType<typeof useChatStore.getState>>)
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<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
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<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
await waitFor(() => {
expect(getCurrentWindowMock).toHaveBeenCalled()
})
fireEvent.mouseDown(screen.getByText('Untitled Session'))
expect(startDraggingMock).not.toHaveBeenCalled()
})
})

View File

@ -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<string | null>(null)
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
const dragIndexRef = useRef<number | null>(null)
const startDraggingRef = useRef<(() => Promise<void>) | 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<HTMLDivElement>) => {
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() {
</button>
)}
<div ref={scrollRef} className="flex-1 flex items-stretch overflow-x-hidden" onDragOver={(e) => e.preventDefault()}>
<div
ref={scrollRef}
className="flex-1 flex items-stretch overflow-x-hidden"
onDragOver={(e) => e.preventDefault()}
onMouseDown={handleScrollRegionMouseDown}
>
{tabs.map((tab, index) => (
<TabItem
key={tab.sessionId}