- PaneShell: per-pane chrome (kind label + close) - ChatPane: extracts message+input rendering, subscribes to useSessionStream - FileBrowserPane: tree + filter (debounced 100ms) + inline viewer via Shiki - PaneTab: tab with kind icon + context menu (Split, Close, Close others, Close to right, Close all) via shadcn ContextMenu - Workspace: tab strip + pane grid (CSS grid repeat(N,1fr)), native HTML5 drag-to-reorder, "+" button (disabled at 5), subscribes to open_file_in_browser (focus existing file-browser pane or spawn one) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
import type { DragEvent } from 'react';
|
|
import { FolderOpen, MessageSquare, X } from 'lucide-react';
|
|
import type { Pane, PaneKind } from '@/api/types';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuSeparator,
|
|
ContextMenuSub,
|
|
ContextMenuSubContent,
|
|
ContextMenuSubTrigger,
|
|
ContextMenuTrigger,
|
|
} from '@/components/ui/context-menu';
|
|
|
|
interface Props {
|
|
pane: Pane;
|
|
isActive: boolean;
|
|
onClick: () => void;
|
|
onClose: () => void;
|
|
onSplit: (kind: PaneKind) => void;
|
|
onCloseOthers: () => void;
|
|
onCloseToRight: () => void;
|
|
onCloseAll: () => void;
|
|
onDragStart: (e: DragEvent<HTMLDivElement>) => void;
|
|
onDragOver: (e: DragEvent<HTMLDivElement>) => void;
|
|
onDrop: (e: DragEvent<HTMLDivElement>) => void;
|
|
}
|
|
|
|
function basename(path: string): string {
|
|
if (!path) return '';
|
|
const parts = path.split('/');
|
|
return parts[parts.length - 1] ?? path;
|
|
}
|
|
|
|
function labelFor(pane: Pane): string {
|
|
if (pane.kind === 'chat') return 'Chat';
|
|
const openFile = pane.state.open_file;
|
|
if (openFile) return basename(openFile);
|
|
return 'Files';
|
|
}
|
|
|
|
export function PaneTab({
|
|
pane,
|
|
isActive,
|
|
onClick,
|
|
onClose,
|
|
onSplit,
|
|
onCloseOthers,
|
|
onCloseToRight,
|
|
onCloseAll,
|
|
onDragStart,
|
|
onDragOver,
|
|
onDrop,
|
|
}: Props) {
|
|
const Icon = pane.kind === 'chat' ? MessageSquare : FolderOpen;
|
|
const label = labelFor(pane);
|
|
|
|
return (
|
|
<ContextMenu>
|
|
<ContextMenuTrigger asChild>
|
|
<div
|
|
draggable
|
|
onDragStart={onDragStart}
|
|
onDragOver={onDragOver}
|
|
onDrop={onDrop}
|
|
onClick={onClick}
|
|
className={cn(
|
|
'group flex items-center gap-1.5 px-3 py-1.5 text-xs border-r border-border cursor-default select-none',
|
|
isActive
|
|
? 'bg-background text-foreground'
|
|
: 'bg-muted/30 text-muted-foreground hover:bg-muted/60'
|
|
)}
|
|
role="tab"
|
|
aria-selected={isActive}
|
|
>
|
|
<Icon size={12} className="shrink-0" />
|
|
<span className="truncate max-w-[160px]" title={label}>
|
|
{label}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onClose();
|
|
}}
|
|
className="p-0.5 hover:bg-muted rounded opacity-60 hover:opacity-100 shrink-0"
|
|
aria-label="Close tab"
|
|
>
|
|
<X size={10} />
|
|
</button>
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent>
|
|
<ContextMenuSub>
|
|
<ContextMenuSubTrigger>Split</ContextMenuSubTrigger>
|
|
<ContextMenuSubContent>
|
|
<ContextMenuItem onSelect={() => onSplit('chat')}>
|
|
<MessageSquare /> Chat
|
|
</ContextMenuItem>
|
|
<ContextMenuItem onSelect={() => onSplit('file_browser')}>
|
|
<FolderOpen /> File Browser
|
|
</ContextMenuItem>
|
|
</ContextMenuSubContent>
|
|
</ContextMenuSub>
|
|
<ContextMenuSeparator />
|
|
<ContextMenuItem onSelect={onClose}>Close</ContextMenuItem>
|
|
<ContextMenuItem onSelect={onCloseOthers}>Close others</ContextMenuItem>
|
|
<ContextMenuItem onSelect={onCloseToRight}>
|
|
Close to the right
|
|
</ContextMenuItem>
|
|
<ContextMenuItem onSelect={onCloseAll}>Close all</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
);
|
|
}
|