import { useState } from 'react'; import { Clipboard, Code, History, MessageSquare, Terminal, X } from 'lucide-react'; import type { WorkspacePane, WorkspaceTabKind } from '@/api/types'; import { StatusDot } from '@/components/StatusDot'; import { PaneHeaderActions } from '@/components/PaneHeaderActions'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu'; import { useLongPress } from '@/hooks/useLongPress'; import { sessionEvents } from '@/hooks/sessionEvents'; import { cn } from '@/lib/utils'; // Mixed tabs: a pane can hold tabs of different kinds. Each tab is described by // its id, kind, and label (terminal tabs have no chats row, so their label is // supplied by the caller). export interface TabDescriptor { id: string; kind: WorkspaceTabKind; name: string | null; } interface Props { pane: WorkspacePane; tabs: TabDescriptor[]; // v2.6.x (Batch 3a): stable session-scoped tab number per id. tabNumbers: Record; onSwitchTab: (tabIdx: number) => void; onRemoveTab: (id: string) => void; onCloseOthers: (id: string) => void; onCloseToRight: (id: string) => void; onCloseAll: () => void; // Mixed tabs: the "+" adds a tab of the chosen kind to THIS pane. onNewTab: (kind: WorkspaceTabKind) => void; onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void; onReopenPane?: () => void; onShowHistory: () => void; onRename: (chatId: string, name: string) => Promise; onRemovePane?: () => void; // iOS-safe terminal paste, shown only when the active tab is a terminal. onTerminalPaste?: () => void; } function iconForKind(kind: WorkspaceTabKind) { if (kind === 'coder') return Code; if (kind === 'terminal') return Terminal; return MessageSquare; } function defaultName(kind: WorkspaceTabKind): string { if (kind === 'coder') return 'BooCoder'; if (kind === 'terminal') return 'Terminal'; return 'New chat'; } export function ChatTabBar({ pane, tabs, tabNumbers, onSwitchTab, onRemoveTab, onCloseOthers, onCloseToRight, onCloseAll, onNewTab, onSplitPane, onReopenPane, onShowHistory, onRename, onRemovePane, onTerminalPaste, }: 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. 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(id: string, currentName: string | null) { setRenamingId(id); setRenameValue(currentName ?? ''); } async function finishRename() { if (renamingId && renameValue.trim()) { await onRename(renamingId, renameValue.trim()); } setRenamingId(null); } return (
{tabs.map((tab, tabIdx) => { const isActive = tabIdx === pane.activeChatIdx; const isLast = tabIdx === tabs.length - 1; const onlyTab = tabs.length === 1; const TabIcon = iconForKind(tab.kind); const label = tab.name ?? defaultName(tab.kind); const canRename = tab.kind !== 'terminal'; const tabNumber = tabNumbers[tab.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' )} > {tab.kind !== 'terminal' && } {renamingId === tab.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} )}
onNewTab(tab.kind)}> New {defaultName(tab.kind)} {tab.kind !== 'terminal' && ( sessionEvents.emit({ type: 'open_chat_in_new_pane', chat_id: tab.id }) } > Open in new pane )} {canRename && ( <> startRename(tab.id, tab.name)}> Rename )} onRemoveTab(tab.id)}> Close onCloseOthers(tab.id)}> Close others onCloseToRight(tab.id)}> Close to right onCloseAll()}> Close all
); })} {tabs.length === 0 && (
Session
)}
{onTerminalPaste && ( )}
); }