Files
boocode/apps/web/src/components/PaneHeaderActions.tsx
indifferentketchup d6d246c15b feat(web,coder): arena pane — compare 2-6 AI competitors on same prompt
Arena is a new pane kind for competitive AI evaluation. A Battle runs
the same prompt against 2-6 Contestants across two concurrent lanes:
local lane (llama-swap models, serial) and cloud lane (parallel).

Added to all three registries: @boocode/contracts WsFrameSchema,
server InferenceFrame, and web WsFrame.

Backend (apps/coder):
- arena-runner: battle scheduler, lane classifier, benchmark, results
  writer, resume, user winner override
- arena-analyzer: two-stage digest→judge analysis on DEFAULT_MODEL
- arena-decisions: status transitions and resume logic (unit-tested)
- arena-analyzer-helpers: pure helper functions (unit-tested)
- arena-model-call: model call utility for analysis
- arena routes: create/get/list/stop/analyze/cross-examine/winner/diff
- schema: battles, contestants, cross_examinations tables (idempotent)
- remove old /api/arena* routes and tasks.arena_id column

Frontend (apps/web):
- ArenaLauncherDialog: battle type, prompt, contestant selection
- ArenaPane: live roster, streaming output, analysis, cross-exam
- DiffView: unified diff with line-by-line color for coding contests
- Winner override per-row dropdown (Trophy icon)
- battle_updated WS handler for live winner/analysis updates
- arena pane kind in Workspace, ChatTabBar, useSidebar

Cross-app:
- ArenaState and ArenaContestantShape/WsFrame types (contracts)
- battle_* frames in WsFrameSchema, InferenceFrame, and web WsFrame
- manifest.json written per battle results folder
- /Arena added to .gitignore

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:25:29 +00:00

165 lines
5.4 KiB
TypeScript

import { Code, Columns2, History, MessageSquare, Plus, RotateCcw, Swords, Terminal, Workflow, X } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';
// Shared pane-header action cluster: + (new) / Split / Reopen-closed-pane /
// Session history / Close. Rendered in the chat tab bar (ChatTabBar) and the
// desktop coder + terminal pane headers (Workspace) so all pane kinds share one
// control set. Extracted to avoid a divergent copy per header.
interface Props {
// Mixed tabs: the "+" menu adds a tab of the chosen kind to THIS pane. Split
// (the second control) adds a new pane.
onNewTab: (kind: 'chat' | 'terminal' | 'coder') => void;
onSplitPane: (kind: 'chat' | 'terminal' | 'coder') => void;
// When provided, shows a "New Orchestrator" item that opens the flow launcher.
// Orchestrators are always split (run-bound; can't live as a tab in another pane).
onNewOrchestrator?: () => void;
// When provided, shows a "New Arena" item that opens the arena launcher.
onNewArena?: () => void;
onReopenPane?: () => void;
onShowHistory: () => void;
onRemovePane?: () => void;
// Highlights the History button when the pane is showing the landing page.
historyActive?: boolean;
// Positioning/spacing supplied by the parent (e.g. "ml-auto px-1").
className?: string;
}
const BTN =
'inline-flex items-center justify-center p-1 rounded text-muted-foreground hover:bg-muted hover:text-foreground max-md:min-h-[44px] max-md:min-w-[44px]';
export function PaneHeaderActions({
onNewTab,
onSplitPane,
onNewOrchestrator,
onNewArena,
onReopenPane,
onShowHistory,
onRemovePane,
historyActive,
className,
}: Props) {
return (
<div className={cn('flex items-center gap-0.5 shrink-0', className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
onClick={(e) => e.stopPropagation()}
className={BTN}
aria-label="New tab"
title="New tab (chat / terminal / coder)"
>
<Plus size={12} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit">
{/* Mixed tabs: every item adds a tab of that kind to THIS pane. */}
<DropdownMenuItem onSelect={() => onNewTab('chat')}>
<MessageSquare size={14} /> New BooChat
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onNewTab('terminal')}>
<Terminal size={14} /> New BooTerm
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onNewTab('coder')}>
<Code size={14} /> New BooCode
</DropdownMenuItem>
{onNewOrchestrator && (
<DropdownMenuItem onSelect={onNewOrchestrator}>
<Workflow size={14} /> New Orchestrator
</DropdownMenuItem>
)}
{onNewArena && (
<DropdownMenuItem onSelect={onNewArena}>
<Swords size={14} /> New Arena
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
onClick={(e) => e.stopPropagation()}
className={cn(BTN, 'max-md:hidden')}
aria-label="Split pane"
title="Split pane"
>
<Columns2 size={12} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit">
<DropdownMenuItem onSelect={() => onSplitPane('chat')}>
<MessageSquare size={14} /> New BooChat
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onSplitPane('terminal')}>
<Terminal size={14} /> New BooTerm
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => onSplitPane('coder')}>
<Code size={14} /> New BooCode
</DropdownMenuItem>
{onNewOrchestrator && (
<DropdownMenuItem onSelect={onNewOrchestrator}>
<Workflow size={14} /> New Orchestrator
</DropdownMenuItem>
)}
{onNewArena && (
<DropdownMenuItem onSelect={onNewArena}>
<Swords size={14} /> New Arena
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
{onReopenPane && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onReopenPane();
}}
className={cn(BTN, 'max-md:hidden')}
aria-label="Reopen closed pane"
title="Reopen closed pane"
>
<RotateCcw size={12} />
</button>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onShowHistory();
}}
className={cn(BTN, 'max-md:hidden', historyActive && 'text-foreground bg-muted/50')}
aria-label="Session history"
title="Session history"
>
<History size={12} />
</button>
{onRemovePane && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemovePane();
}}
className={BTN}
aria-label="Close pane"
title="Close pane"
>
<X size={12} />
</button>
)}
</div>
);
}