/** * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useMemo } from 'react'; import { Box, Text } from 'ink'; import { type ArenaManager, AgentStatus, type Config, } from '@qwen-code/qwen-code-core'; import { theme } from '../semantic-colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { MessageType, type HistoryItemWithoutId } from '../types.js'; import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; import { formatDuration } from '../utils/formatters.js'; import { getArenaStatusLabel } from '../utils/displayUtils.js'; import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js'; import type { DescriptiveRadioSelectItem } from './shared/DescriptiveRadioButtonSelect.js'; interface ArenaSelectDialogProps { manager: ArenaManager; config: Config; addItem: UseHistoryManagerReturn['addItem']; closeArenaDialog: () => void; } export function ArenaSelectDialog({ manager, config, addItem, closeArenaDialog, }: ArenaSelectDialogProps): React.JSX.Element { const pushMessage = useCallback( (result: { messageType: 'info' | 'error'; content: string }) => { const item: HistoryItemWithoutId = { type: result.messageType === 'info' ? MessageType.INFO : MessageType.ERROR, text: result.content, }; addItem(item, Date.now()); try { const chatRecorder = config.getChatRecordingService(); chatRecorder?.recordSlashCommand({ phase: 'result', rawCommand: '/arena select', outputHistoryItems: [{ ...item } as Record], }); } catch { // Best-effort recording } }, [addItem, config], ); const onSelect = useCallback( async (agentId: string) => { closeArenaDialog(); const mgr = config.getArenaManager(); if (!mgr) { pushMessage({ messageType: 'error', content: 'No arena session found. Start one with /arena start.', }); return; } const agent = mgr.getAgentState(agentId) ?? mgr.getAgentStates().find((item) => item.agentId === agentId); const label = agent?.model.displayName || agent?.model.modelId || agentId; const result = await mgr.applyAgentResult(agentId); if (!result.success) { pushMessage({ messageType: 'error', content: `Failed to apply changes from ${label}: ${result.error}`, }); return; } try { await config.cleanupArenaRuntime(true); } catch (err) { pushMessage({ messageType: 'error', content: `Warning: failed to clean up arena resources: ${err instanceof Error ? err.message : String(err)}`, }); } pushMessage({ messageType: 'info', content: `Applied changes from ${label} to workspace. Arena session complete.`, }); }, [closeArenaDialog, config, pushMessage], ); const onDiscard = useCallback(async () => { closeArenaDialog(); const mgr = config.getArenaManager(); if (!mgr) { pushMessage({ messageType: 'error', content: 'No arena session found. Start one with /arena start.', }); return; } try { await config.cleanupArenaRuntime(true); pushMessage({ messageType: 'info', content: 'Arena results discarded. All worktrees cleaned up.', }); } catch (err) { pushMessage({ messageType: 'error', content: `Failed to clean up arena worktrees: ${err instanceof Error ? err.message : String(err)}`, }); } }, [closeArenaDialog, config, pushMessage]); const result = manager.getResult(); const agents = manager.getAgentStates(); const items: Array> = useMemo( () => agents.map((agent) => { const label = agent.model.displayName || agent.model.modelId; const statusInfo = getArenaStatusLabel(agent.status); const duration = formatDuration(agent.stats.durationMs); const tokens = agent.stats.totalTokens.toLocaleString(); // Build diff summary from cached result if available let diffAdditions = 0; let diffDeletions = 0; if (agent.status === AgentStatus.COMPLETED && result) { const agentResult = result.agents.find( (a) => a.agentId === agent.agentId, ); if (agentResult?.diff) { const lines = agentResult.diff.split('\n'); for (const line of lines) { if (line.startsWith('+') && !line.startsWith('+++')) { diffAdditions++; } else if (line.startsWith('-') && !line.startsWith('---')) { diffDeletions++; } } } } // Title: full model name (not truncated) const title = {label}; // Description: status, time, tokens, changes (unified with Arena Complete columns) const description = ( {statusInfo.text} · {duration} · {tokens} tokens {(diffAdditions > 0 || diffDeletions > 0) && ( <> · +{diffAdditions} / -{diffDeletions} lines )} ); return { key: agent.agentId, value: agent.agentId, title, description, disabled: agent.status !== AgentStatus.COMPLETED, }; }), [agents, result], ); useKeypress( (key) => { if (key.name === 'escape') { closeArenaDialog(); } if (key.name === 'd' && !key.ctrl && !key.meta) { onDiscard(); } }, { isActive: true }, ); const task = result?.task || ''; return ( {/* Neutral title color (not green) */} Arena Results Task: {`"${task.length > 60 ? task.slice(0, 59) + '…' : task}"`} Select a winner to apply changes: !item.disabled)} onSelect={(agentId: string) => { onSelect(agentId); }} isFocused={true} showNumbers={false} /> Enter to select, d to discard all, Esc to cancel ); }