/** * @license * Copyright 2025 Qwen * SPDX-License-Identifier: Apache-2.0 */ import React, { useMemo } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { TaskResultDisplay, SubagentStatsSummary, } from '@qwen-code/qwen-code-core'; import { theme } from '../../semantic-colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { COLOR_OPTIONS } from './constants.js'; import { fmtDuration } from './utils.js'; export type DisplayMode = 'compact' | 'default' | 'verbose'; export interface SubagentExecutionDisplayProps { data: TaskResultDisplay; } const getStatusColor = ( status: TaskResultDisplay['status'] | 'executing' | 'success', ) => { switch (status) { case 'running': case 'executing': return theme.status.warning; case 'completed': case 'success': return theme.status.success; case 'failed': return theme.status.error; default: return Colors.Gray; } }; const getStatusText = (status: TaskResultDisplay['status']) => { switch (status) { case 'running': return 'Running'; case 'completed': return 'Completed'; case 'failed': return 'Failed'; default: return 'Unknown'; } }; /** * Component to display subagent execution progress and results. * This is now a pure component that renders the provided SubagentExecutionResultDisplay data. * Real-time updates are handled by the parent component updating the data prop. */ export const SubagentExecutionDisplay: React.FC< SubagentExecutionDisplayProps > = ({ data }) => { const [displayMode, setDisplayMode] = React.useState('default'); const agentColor = useMemo(() => { const colorOption = COLOR_OPTIONS.find( (option) => option.name === data.subagentColor, ); return colorOption?.value || theme.text.accent; }, [data.subagentColor]); const footerText = React.useMemo(() => { // This component only listens to keyboard shortcut events when the subagent is running if (data.status !== 'running') return ''; if (displayMode === 'verbose') return 'Press ctrl+r to show less.'; if (displayMode === 'default') { const hasMoreLines = data.taskPrompt.split('\n').length > 10; const hasMoreToolCalls = data.toolCalls && data.toolCalls.length > 5; if (hasMoreToolCalls || hasMoreLines) { return 'Press ctrl+s to show more.'; } return ''; } return ''; }, [displayMode, data.toolCalls, data.taskPrompt, data.status]); // Handle ctrl+s and ctrl+r keypresses to control display mode useKeypress( (key) => { if (key.ctrl && key.name === 's') { setDisplayMode((current) => current === 'default' ? 'verbose' : 'verbose', ); } else if (key.ctrl && key.name === 'r') { setDisplayMode((current) => current === 'verbose' ? 'default' : 'default', ); } }, { isActive: true }, ); return ( {/* Header with subagent name and status */} {data.subagentName} {/* Task description */} {/* Progress section for running tasks */} {data.status === 'running' && data.toolCalls && data.toolCalls.length > 0 && ( )} {/* Results section for completed/failed tasks */} {(data.status === 'completed' || data.status === 'failed') && ( )} {/* Footer with keyboard shortcuts */} {footerText && ( {footerText} )} ); }; /** * Task prompt section with truncation support */ const TaskPromptSection: React.FC<{ taskPrompt: string; displayMode: DisplayMode; }> = ({ taskPrompt, displayMode }) => { const lines = taskPrompt.split('\n'); const shouldTruncate = lines.length > 10; const showFull = displayMode === 'verbose'; const displayLines = showFull ? lines : lines.slice(0, 10); return ( Task Detail: {shouldTruncate && displayMode === 'default' && ( Showing the first 10 lines. )} {displayLines.join('\n') + (shouldTruncate && !showFull ? '...' : '')} ); }; /** * Status dot component with similar height as text */ const StatusDot: React.FC<{ status: TaskResultDisplay['status']; }> = ({ status }) => ( ); /** * Status indicator component */ const StatusIndicator: React.FC<{ status: TaskResultDisplay['status']; }> = ({ status }) => { const color = getStatusColor(status); const text = getStatusText(status); return {text}; }; /** * Tool calls list - format consistent with ToolInfo in ToolMessage.tsx */ const ToolCallsList: React.FC<{ toolCalls: TaskResultDisplay['toolCalls']; displayMode: DisplayMode; }> = ({ toolCalls, displayMode }) => { const calls = toolCalls || []; const shouldTruncate = calls.length > 5; const showAll = displayMode === 'verbose'; const displayCalls = showAll ? calls : calls.slice(-5); // Show last 5 // Reverse the order to show most recent first const reversedDisplayCalls = [...displayCalls].reverse(); return ( Tools: {shouldTruncate && displayMode === 'default' && ( {' '} Showing the last 5 of {calls.length} tools. )} {reversedDisplayCalls.map((toolCall, index) => ( ))} ); }; /** * Individual tool call item - consistent with ToolInfo format */ const ToolCallItem: React.FC<{ toolCall: { name: string; status: 'executing' | 'success' | 'failed'; error?: string; args?: Record; result?: string; resultDisplay?: string; description?: string; }; }> = ({ toolCall }) => { const STATUS_INDICATOR_WIDTH = 3; // Map subagent status to ToolCallStatus-like display const statusIcon = React.useMemo(() => { const color = getStatusColor(toolCall.status); switch (toolCall.status) { case 'executing': return ; // Using same as ToolMessage case 'success': return ; case 'failed': return ( x ); default: return o; } }, [toolCall.status]); const description = React.useMemo(() => { if (!toolCall.description) return ''; const firstLine = toolCall.description.split('\n')[0]; return firstLine.length > 80 ? firstLine.substring(0, 80) + '...' : firstLine; }, [toolCall.description]); // Get first line of resultDisplay for truncated output const truncatedOutput = React.useMemo(() => { if (!toolCall.resultDisplay) return ''; const firstLine = toolCall.resultDisplay.split('\n')[0]; return firstLine.length > 80 ? firstLine.substring(0, 80) + '...' : firstLine; }, [toolCall.resultDisplay]); return ( {/* First line: status icon + tool name + description (consistent with ToolInfo) */} {statusIcon} {toolCall.name} {' '} {description} {toolCall.error && ( - {toolCall.error} )} {/* Second line: truncated returnDisplay output */} {truncatedOutput && ( {truncatedOutput} )} ); }; /** * Execution summary details component */ const ExecutionSummaryDetails: React.FC<{ data: TaskResultDisplay; displayMode: DisplayMode; }> = ({ data, displayMode: _displayMode }) => { const stats = data.executionSummary; if (!stats) { return ( • No summary available ); } return ( Duration: {fmtDuration(stats.totalDurationMs)} Rounds: {stats.rounds} Tokens: {stats.totalTokens.toLocaleString()} ); }; /** * Tool usage statistics component */ const ToolUsageStats: React.FC<{ executionSummary?: SubagentStatsSummary; }> = ({ executionSummary }) => { if (!executionSummary) { return ( • No tool usage data available ); } return ( Total Calls: {executionSummary.totalToolCalls} Success Rate:{' '} {executionSummary.successRate.toFixed(1)}% {' '} ( {executionSummary.successfulToolCalls} success ,{' '} {executionSummary.failedToolCalls} failed ) ); }; /** * Results section for completed executions - matches the clean layout from the image */ const ResultsSection: React.FC<{ data: TaskResultDisplay; displayMode: DisplayMode; }> = ({ data, displayMode }) => ( {/* Tool calls section - clean list format */} {data.toolCalls && data.toolCalls.length > 0 && ( )} {/* Execution Summary section */} Execution Summary: {/* Tool Usage section */} {data.executionSummary && ( Tool Usage: )} {/* Error reason for failed tasks */} {data.status === 'failed' && data.terminateReason && ( ❌ Failed: {data.terminateReason} )} );