diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 78eb614f..799b7c7e 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -12,11 +12,15 @@ export function TabBar() { const closeTab = useTabStore((s) => s.closeTab) const disconnectSession = useChatStore((s) => s.disconnectSession) + const moveTab = useTabStore((s) => s.moveTab) + const scrollRef = useRef(null) const [canScrollLeft, setCanScrollLeft] = useState(false) const [canScrollRight, setCanScrollRight] = useState(false) const [contextMenu, setContextMenu] = useState<{ sessionId: string; x: number; y: number } | null>(null) const [closingTabId, setClosingTabId] = useState(null) + const [dragOverIndex, setDragOverIndex] = useState(null) + const dragIndexRef = useRef(null) const t = useTranslation() const updateScrollState = useCallback(() => { @@ -86,6 +90,16 @@ export function TabBar() { } } + const handleCloseLeft = (sessionId: string) => { + setContextMenu(null) + const idx = tabs.findIndex((t) => t.sessionId === sessionId) + const leftIds = tabs.slice(0, idx).map((t) => t.sessionId) + for (const id of leftIds) { + disconnectSession(id) + closeTab(id) + } + } + const handleCloseRight = (sessionId: string) => { setContextMenu(null) const idx = tabs.findIndex((t) => t.sessionId === sessionId) @@ -96,6 +110,41 @@ export function TabBar() { } } + const handleCloseAll = () => { + setContextMenu(null) + const allIds = tabs.map((t) => t.sessionId) + for (const id of allIds) { + disconnectSession(id) + closeTab(id) + } + } + + const handleDragStart = (index: number) => { + dragIndexRef.current = index + } + + const handleDragOver = (e: React.DragEvent, index: number) => { + e.preventDefault() + if (dragIndexRef.current === null || dragIndexRef.current === index) { + setDragOverIndex(null) + return + } + setDragOverIndex(index) + } + + const handleDrop = (index: number) => { + if (dragIndexRef.current !== null && dragIndexRef.current !== index) { + moveTab(dragIndexRef.current, index) + } + dragIndexRef.current = null + setDragOverIndex(null) + } + + const handleDragEnd = () => { + dragIndexRef.current = null + setDragOverIndex(null) + } + if (tabs.length === 0) return null return ( @@ -107,15 +156,20 @@ export function TabBar() { )} -
- {tabs.map((tab) => ( +
e.preventDefault()}> + {tabs.map((tab, index) => ( setActiveTab(tab.sessionId)} onClose={() => handleClose(tab.sessionId)} onContextMenu={(e) => handleContextMenu(e, tab.sessionId)} + onDragStart={() => handleDragStart(index)} + onDragOver={(e) => handleDragOver(e, index)} + onDrop={() => handleDrop(index)} + onDragEnd={handleDragEnd} /> ))}
@@ -143,12 +197,25 @@ export function TabBar() { > {t('tabs.closeOthers')} + +
+
)} @@ -186,23 +253,34 @@ export function TabBar() { ) } -function TabItem({ tab, isActive, onClick, onClose, onContextMenu }: { +function TabItem({ tab, isActive, isDragOver, onClick, onClose, onContextMenu, onDragStart, onDragOver, onDrop, onDragEnd }: { tab: Tab isActive: boolean + isDragOver: boolean onClick: () => void onClose: () => void onContextMenu: (e: React.MouseEvent) => void + onDragStart: () => void + onDragOver: (e: React.DragEvent) => void + onDrop: () => void + onDragEnd: () => void }) { return (
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index d61dabfb..b669712b 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -438,7 +438,9 @@ export const en = { // ─── Tabs ────────────────────────────────────── 'tabs.close': 'Close', 'tabs.closeOthers': 'Close Others', + 'tabs.closeLeft': 'Close to the Left', 'tabs.closeRight': 'Close to the Right', + 'tabs.closeAll': 'Close All', 'tabs.closeConfirmTitle': 'Session Running', 'tabs.closeConfirmMessage': 'This session is still running. What would you like to do?', 'tabs.closeConfirmKeep': 'Keep Running', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 9cbe328b..e9be3840 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -440,7 +440,9 @@ export const zh: Record = { // ─── Tabs ────────────────────────────────────── 'tabs.close': '关闭', 'tabs.closeOthers': '关闭其他', + 'tabs.closeLeft': '关闭左侧', 'tabs.closeRight': '关闭右侧', + 'tabs.closeAll': '关闭所有', 'tabs.closeConfirmTitle': '会话运行中', 'tabs.closeConfirmMessage': '此会话仍在运行,你想要怎么做?', 'tabs.closeConfirmKeep': '保持运行', diff --git a/desktop/src/stores/tabStore.ts b/desktop/src/stores/tabStore.ts index 970e1aa9..d2a12c8b 100644 --- a/desktop/src/stores/tabStore.ts +++ b/desktop/src/stores/tabStore.ts @@ -29,6 +29,7 @@ type TabStore = { setActiveTab: (sessionId: string) => void updateTabTitle: (sessionId: string, title: string) => void updateTabStatus: (sessionId: string, status: Tab['status']) => void + moveTab: (fromIndex: number, toIndex: number) => void saveTabs: () => void restoreTabs: () => Promise @@ -92,6 +93,17 @@ export const useTabStore = create((set, get) => ({ })) }, + moveTab: (fromIndex, toIndex) => { + if (fromIndex === toIndex) return + const { tabs } = get() + if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length) return + const newTabs = [...tabs] + const [moved] = newTabs.splice(fromIndex, 1) + newTabs.splice(toIndex, 0, moved!) + set({ tabs: newTabs }) + get().saveTabs() + }, + saveTabs: () => { const { tabs, activeTabId } = get() const data: TabPersistence = {