import { useState } from 'react'; import { Code, Columns2, History, MessageSquare, Plus, RotateCcw, Terminal, X } from 'lucide-react'; import type { Chat, WorkspacePane } from '@/api/types'; import { StatusDot } from '@/components/StatusDot'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useLongPress } from '@/hooks/useLongPress'; import { sessionEvents } from '@/hooks/sessionEvents'; import { cn } from '@/lib/utils'; interface Props { pane: WorkspacePane; tabs: Chat[]; // v2.6.x (Batch 3a): stable session-scoped tab number per chat id. Keyed by // chat.id, NEVER by tab position. tabNumbers: Record; onSwitchTab: (tabIdx: number) => void; onRemoveTab: (chatId: string) => void; onCloseOthers: (chatId: string) => void; onCloseToRight: (chatId: string) => void; onCloseAll: () => void; onNewTab: () => void; onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void; onReopenPane?: () => void; onShowHistory: () => void; onRename: (chatId: string, name: string) => Promise; onRemovePane?: () => void; } export function ChatTabBar({ pane, tabs, tabNumbers, onSwitchTab, onRemoveTab, onCloseOthers, onCloseToRight, onCloseAll, onNewTab, onSplitPane, onReopenPane, onShowHistory, onRename, onRemovePane, }: Props) { const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); // Long-press: dispatch a synthetic contextmenu event on the tab so the // existing Radix ContextMenuTrigger opens at the touch coordinates. Works // because asChild composition makes the tab div the trigger element. const longPress = useLongPress(({ clientX, clientY, target }) => { if (!target || !(target instanceof Element)) return; const tab = target.closest('[data-tab-id]') as HTMLElement | null; if (!tab) return; tab.dispatchEvent( new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX, clientY }), ); }); function startRename(chatId: string, currentName: string | null) { setRenamingId(chatId); setRenameValue(currentName ?? ''); } async function finishRename() { if (renamingId && renameValue.trim()) { await onRename(renamingId, renameValue.trim()); } setRenamingId(null); } return (
{tabs.map((chat, tabIdx) => { const isActive = tabIdx === pane.activeChatIdx; const isLast = tabIdx === tabs.length - 1; const onlyTab = tabs.length === 1; const label = chat.name ?? 'New chat'; // v2.6.x: stable tab number keyed by chat.id (NOT tab position). // Omit gracefully when not yet assigned. const tabNumber = tabNumbers[chat.id]; return (
onSwitchTab(tabIdx)} onTouchStart={longPress.onTouchStart} onTouchMove={longPress.onTouchMove} onTouchEnd={longPress.onTouchEnd} onTouchCancel={longPress.onTouchCancel} style={{ WebkitTouchCallout: 'none' }} className={cn( 'group flex items-center gap-1.5 px-3 py-1.5 text-xs border-r border-border cursor-default select-none shrink-0', isActive ? 'bg-background text-foreground' : 'bg-muted/30 text-muted-foreground hover:bg-muted/60' )} > {renamingId === chat.id ? ( setRenameValue(e.target.value)} onBlur={() => void finishRename()} onKeyDown={(e) => { if (e.key === 'Enter') void finishRename(); if (e.key === 'Escape') setRenamingId(null); }} onClick={(e) => e.stopPropagation()} className="bg-transparent border-b border-border text-xs outline-none w-28" /> ) : ( {tabNumber !== undefined ? `${tabNumber} · ${label}` : label} )}
New chat sessionEvents.emit({ type: 'open_chat_in_new_pane', chat_id: chat.id }) } > Open in new pane startRename(chat.id, chat.name)}> Rename onRemoveTab(chat.id)}> Close onCloseOthers(chat.id)} > Close others onCloseToRight(chat.id)} > Close to right onCloseAll()}> Close all
); })} {tabs.length === 0 && (
Session
)}
{/* New BooChat opens a tab in THIS pane; terminal/coder can't be tabs, so they split into a new pane (matches the Split menu). */} New BooChat onSplitPane('terminal')}> New BooTerm onSplitPane('coder')}> New BooCode onSplitPane('chat')}> New BooChat onSplitPane('terminal')}> New BooTerm onSplitPane('coder')}> New BooCode {onReopenPane && ( )} {onRemovePane && ( )}
); }