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>
165 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|