/** * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useEffect, useMemo, useState } from 'react'; import { Box, Text } from 'ink'; import { type ArenaManager, type ArenaAgentState, type InProcessBackend, type AgentStatsSummary, isTerminalStatus, ArenaSessionStatus, DISPLAY_MODE, } from '@qwen-code/qwen-code-core'; import { theme } from '../../semantic-colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { formatDuration } from '../../utils/formatters.js'; import { getArenaStatusLabel } from '../../utils/displayUtils.js'; const STATUS_REFRESH_INTERVAL_MS = 2000; const IN_PROCESS_REFRESH_INTERVAL_MS = 1000; interface ArenaStatusDialogProps { manager: ArenaManager; closeArenaDialog: () => void; width?: number; } function truncate(str: string, maxLen: number): string { if (str.length <= maxLen) return str; return str.slice(0, maxLen - 1) + '…'; } function pad( str: string, len: number, align: 'left' | 'right' = 'left', ): string { if (str.length >= len) return str.slice(0, len); const padding = ' '.repeat(len - str.length); return align === 'right' ? padding + str : str + padding; } function getElapsedMs(agent: ArenaAgentState): number { if (isTerminalStatus(agent.status)) { return agent.stats.durationMs; } return Date.now() - agent.startedAt; } function getSessionStatusLabel(status: ArenaSessionStatus): { text: string; color: string; } { switch (status) { case ArenaSessionStatus.RUNNING: return { text: 'Running', color: theme.status.success }; case ArenaSessionStatus.INITIALIZING: return { text: 'Initializing', color: theme.status.warning }; case ArenaSessionStatus.COMPLETED: return { text: 'Completed', color: theme.status.success }; case ArenaSessionStatus.CANCELLED: return { text: 'Cancelled', color: theme.status.warning }; case ArenaSessionStatus.FAILED: return { text: 'Failed', color: theme.status.error }; default: return { text: String(status), color: theme.text.secondary }; } } const MAX_MODEL_NAME_LENGTH = 35; export function ArenaStatusDialog({ manager, closeArenaDialog, width, }: ArenaStatusDialogProps): React.JSX.Element { const [tick, setTick] = useState(0); // Detect in-process backend for live stats reading const backend = manager.getBackend(); const isInProcess = backend?.type === DISPLAY_MODE.IN_PROCESS; const inProcessBackend = isInProcess ? (backend as InProcessBackend) : null; useEffect(() => { const interval = isInProcess ? IN_PROCESS_REFRESH_INTERVAL_MS : STATUS_REFRESH_INTERVAL_MS; const timer = setInterval(() => { setTick((prev) => prev + 1); }, interval); return () => clearInterval(timer); }, [isInProcess]); // Force re-read on every tick void tick; const sessionStatus = manager.getSessionStatus(); const sessionLabel = getSessionStatusLabel(sessionStatus); const agents = manager.getAgentStates(); const task = manager.getTask() ?? ''; // For in-process mode, read live stats directly from AgentInteractive const liveStats = useMemo(() => { if (!inProcessBackend) return null; const statsMap = new Map(); for (const agent of agents) { const interactive = inProcessBackend.getAgent(agent.agentId); if (interactive) { statsMap.set(agent.agentId, interactive.getStats()); } } return statsMap; // eslint-disable-next-line react-hooks/exhaustive-deps }, [inProcessBackend, agents, tick]); const maxTaskLen = 60; const displayTask = task.length > maxTaskLen ? task.slice(0, maxTaskLen - 1) + '…' : task; const colStatus = 14; const colTime = 8; const colTokens = 10; const colRounds = 8; const colTools = 8; useKeypress( (key) => { if (key.name === 'escape' || key.name === 'q' || key.name === 'return') { closeArenaDialog(); } }, { isActive: true }, ); // Inner content width: total width minus border (2) and paddingX (2*2) const innerWidth = (width ?? 80) - 6; return ( {/* Title */} Arena Status · {sessionLabel.text} {isInProcess && ( <> · In-Process )} {/* Task */} Task: "{displayTask}" {/* Table header */} Agent Status Time Tokens Rounds Tools {/* Separator */} {'─'.repeat(innerWidth)} {/* Agent rows */} {agents.map((agent) => { const label = agent.model.displayName || agent.model.modelId; const { text: statusText, color } = getArenaStatusLabel(agent.status); const elapsed = getElapsedMs(agent); // Use live stats from AgentInteractive when in-process, otherwise // fall back to the cached ArenaAgentState.stats (file-polled). const live = liveStats?.get(agent.agentId); const totalTokens = live?.totalTokens ?? agent.stats.totalTokens; const rounds = live?.rounds ?? agent.stats.rounds; const toolCalls = live?.totalToolCalls ?? agent.stats.toolCalls; const successfulToolCalls = live?.successfulToolCalls ?? agent.stats.successfulToolCalls; const failedToolCalls = live?.failedToolCalls ?? agent.stats.failedToolCalls; return ( {truncate(label, MAX_MODEL_NAME_LENGTH)} {statusText} {pad(formatDuration(elapsed), colTime - 1, 'right')} {pad(totalTokens.toLocaleString(), colTokens - 1, 'right')} {pad(String(rounds), colRounds - 1, 'right')} {failedToolCalls > 0 ? ( {successfulToolCalls} / {failedToolCalls} ) : ( {pad(String(toolCalls), colTools - 1, 'right')} )} {/* In-process mode: show extra detail row with cost + thought tokens */} {live && (live.estimatedCost > 0 || live.thoughtTokens > 0) && ( {live.estimatedCost > 0 && `Cost: $${live.estimatedCost.toFixed(4)}`} {live.estimatedCost > 0 && live.thoughtTokens > 0 && ' · '} {live.thoughtTokens > 0 && `Thinking: ${live.thoughtTokens.toLocaleString()} tok`} {live.cachedTokens > 0 && ` · Cached: ${live.cachedTokens.toLocaleString()} tok`} )} ); })} {agents.length === 0 && ( No agents registered yet. )} ); }