mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: subagent runtime & CLI display - done
This commit is contained in:
@@ -196,7 +196,7 @@ describe('<ToolMessage />', () => {
|
||||
|
||||
it('shows subagent execution display for task tool with proper result display', () => {
|
||||
const subagentResultDisplay = {
|
||||
type: 'subagent_execution' as const,
|
||||
type: 'task_execution' as const,
|
||||
subagentName: 'file-search',
|
||||
taskDescription: 'Search for files matching pattern',
|
||||
status: 'running' as const,
|
||||
|
||||
@@ -34,7 +34,7 @@ type DisplayRendererResult =
|
||||
| { type: 'todo'; data: TodoResultDisplay }
|
||||
| { type: 'string'; data: string }
|
||||
| { type: 'diff'; data: { fileDiff: string; fileName: string } }
|
||||
| { type: 'subagent_execution'; data: TaskResultDisplay };
|
||||
| { type: 'task'; data: TaskResultDisplay };
|
||||
|
||||
/**
|
||||
* Custom hook to determine the type of result display and return appropriate rendering info
|
||||
@@ -65,10 +65,10 @@ const useResultDisplayRenderer = (
|
||||
typeof resultDisplay === 'object' &&
|
||||
resultDisplay !== null &&
|
||||
'type' in resultDisplay &&
|
||||
resultDisplay.type === 'subagent_execution'
|
||||
resultDisplay.type === 'task_execution'
|
||||
) {
|
||||
return {
|
||||
type: 'subagent_execution',
|
||||
type: 'task',
|
||||
data: resultDisplay as TaskResultDisplay,
|
||||
};
|
||||
}
|
||||
@@ -216,7 +216,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
{displayRenderer.type === 'todo' && (
|
||||
<TodoResultRenderer data={displayRenderer.data} />
|
||||
)}
|
||||
{displayRenderer.type === 'subagent_execution' && (
|
||||
{displayRenderer.type === 'task' && (
|
||||
<SubagentExecutionRenderer
|
||||
data={displayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
|
||||
@@ -48,12 +48,12 @@ export const AgentViewerStep = ({ selectedAgent }: AgentViewerStepProps) => {
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(agent.backgroundColor) && (
|
||||
{shouldShowColor(agent.color) && (
|
||||
<Box>
|
||||
<Text bold>Color: </Text>
|
||||
<Box backgroundColor={getColorForDisplay(agent.backgroundColor)}>
|
||||
<Text color="black">{` ${agent.name} `}</Text>
|
||||
</Box>
|
||||
<Text
|
||||
color={getColorForDisplay(agent.color)}
|
||||
>{` ${agent.name} `}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ export function AgentsManagerDialog({
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<ColorSelector
|
||||
backgroundColor={selectedAgent?.backgroundColor || 'auto'}
|
||||
color={selectedAgent?.color || 'auto'}
|
||||
agentName={selectedAgent?.name || 'Agent'}
|
||||
onSelect={async (color) => {
|
||||
// Save changes and reload agents
|
||||
@@ -258,7 +258,7 @@ export function AgentsManagerDialog({
|
||||
const subagentManager = config.getSubagentManager();
|
||||
await subagentManager.updateSubagent(
|
||||
selectedAgent.name,
|
||||
{ backgroundColor: color },
|
||||
{ color },
|
||||
selectedAgent.level,
|
||||
);
|
||||
// Reload agents to get updated state
|
||||
|
||||
@@ -14,7 +14,7 @@ import { COLOR_OPTIONS } from './constants.js';
|
||||
const colorOptions: ColorOption[] = COLOR_OPTIONS;
|
||||
|
||||
interface ColorSelectorProps {
|
||||
backgroundColor?: string;
|
||||
color?: string;
|
||||
agentName?: string;
|
||||
onSelect: (color: string) => void;
|
||||
}
|
||||
@@ -23,16 +23,16 @@ interface ColorSelectorProps {
|
||||
* Color selection with preview.
|
||||
*/
|
||||
export function ColorSelector({
|
||||
backgroundColor = 'auto',
|
||||
color = 'auto',
|
||||
agentName = 'Agent',
|
||||
onSelect,
|
||||
}: ColorSelectorProps) {
|
||||
const [selectedColor, setSelectedColor] = useState<string>(backgroundColor);
|
||||
const [selectedColor, setSelectedColor] = useState<string>(color);
|
||||
|
||||
// Update selected color when backgroundColor prop changes
|
||||
// Update selected color when color prop changes
|
||||
useEffect(() => {
|
||||
setSelectedColor(backgroundColor);
|
||||
}, [backgroundColor]);
|
||||
setSelectedColor(color);
|
||||
}, [color]);
|
||||
|
||||
const handleSelect = (selectedValue: string) => {
|
||||
const colorOption = colorOptions.find(
|
||||
@@ -75,8 +75,8 @@ export function ColorSelector({
|
||||
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.Gray}>Preview:</Text>
|
||||
<Box marginLeft={2} backgroundColor={currentColor.value}>
|
||||
<Text color="black">{` ${agentName} `}</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text color={currentColor.value}>{` ${agentName} `}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -122,7 +122,7 @@ export function CreationSummary({
|
||||
tools: Array.isArray(state.selectedTools)
|
||||
? state.selectedTools
|
||||
: undefined,
|
||||
backgroundColor: state.backgroundColor,
|
||||
color: state.color,
|
||||
};
|
||||
|
||||
// Create the subagent
|
||||
@@ -243,12 +243,12 @@ export function CreationSummary({
|
||||
<Text>{toolsDisplay}</Text>
|
||||
</Box>
|
||||
|
||||
{shouldShowColor(state.backgroundColor) && (
|
||||
{shouldShowColor(state.color) && (
|
||||
<Box>
|
||||
<Text bold>Color: </Text>
|
||||
<Box backgroundColor={getColorForDisplay(state.backgroundColor)}>
|
||||
<Text color="black">{` ${state.generatedName} `}</Text>
|
||||
</Box>
|
||||
<Text
|
||||
color={getColorForDisplay(state.color)}
|
||||
>{` ${state.generatedName} `}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export function SubagentCreationWizard({
|
||||
case WIZARD_STEPS.COLOR_SELECTION:
|
||||
return (
|
||||
<ColorSelector
|
||||
backgroundColor={state.backgroundColor}
|
||||
color={state.color}
|
||||
agentName={state.generatedName}
|
||||
onSelect={(color) => {
|
||||
dispatch({ type: 'SET_BACKGROUND_COLOR', color });
|
||||
@@ -230,7 +230,7 @@ export function SubagentCreationWizard({
|
||||
stepProps,
|
||||
state.currentStep,
|
||||
state.selectedTools,
|
||||
state.backgroundColor,
|
||||
state.color,
|
||||
state.generatedName,
|
||||
config,
|
||||
handleNext,
|
||||
|
||||
@@ -4,15 +4,54 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { TaskResultDisplay } from '@qwen-code/qwen-code-core';
|
||||
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.
|
||||
@@ -20,61 +59,133 @@ export interface SubagentExecutionDisplayProps {
|
||||
*/
|
||||
export const SubagentExecutionDisplay: React.FC<
|
||||
SubagentExecutionDisplayProps
|
||||
> = ({ data }) => (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{/* Header with subagent name and status */}
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<StatusDot status={data.status} />
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
{data.subagentName}
|
||||
</Text>
|
||||
<Text color={Colors.Gray}> • </Text>
|
||||
<StatusIndicator status={data.status} />
|
||||
> = ({ data }) => {
|
||||
const [displayMode, setDisplayMode] = React.useState<DisplayMode>('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 (
|
||||
<Box flexDirection="column" paddingX={1} gap={1}>
|
||||
{/* Header with subagent name and status */}
|
||||
<Box flexDirection="row">
|
||||
<Text bold color={agentColor}>
|
||||
{data.subagentName}
|
||||
</Text>
|
||||
<StatusDot status={data.status} />
|
||||
<StatusIndicator status={data.status} />
|
||||
</Box>
|
||||
|
||||
{/* Task description */}
|
||||
<TaskPromptSection
|
||||
taskPrompt={data.taskPrompt}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
|
||||
{/* Progress section for running tasks */}
|
||||
{data.status === 'running' &&
|
||||
data.toolCalls &&
|
||||
data.toolCalls.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<ToolCallsList
|
||||
toolCalls={data.toolCalls}
|
||||
displayMode={displayMode}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Results section for completed/failed tasks */}
|
||||
{(data.status === 'completed' || data.status === 'failed') && (
|
||||
<ResultsSection data={data} displayMode={displayMode} />
|
||||
)}
|
||||
|
||||
{/* Footer with keyboard shortcuts */}
|
||||
{footerText && (
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.Gray}>{footerText}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
{/* Task description */}
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text color={Colors.Gray}>Task: </Text>
|
||||
<Text wrap="wrap">{data.taskDescription}</Text>
|
||||
/**
|
||||
* 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 (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={theme.text.primary}>Task Detail: </Text>
|
||||
{shouldTruncate && displayMode === 'default' && (
|
||||
<Text color={Colors.Gray}> Showing the first 10 lines.</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box paddingLeft={1}>
|
||||
<Text wrap="wrap">
|
||||
{displayLines.join('\n') + (shouldTruncate && !showFull ? '...' : '')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Progress section for running tasks */}
|
||||
{data.status === 'running' && (
|
||||
<ProgressSection progress={data.progress || { toolCalls: [] }} />
|
||||
)}
|
||||
|
||||
{/* Results section for completed/failed tasks */}
|
||||
{(data.status === 'completed' || data.status === 'failed') && (
|
||||
<ResultsSection data={data} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Status dot component with similar height as text
|
||||
*/
|
||||
const StatusDot: React.FC<{
|
||||
status: TaskResultDisplay['status'];
|
||||
}> = ({ status }) => {
|
||||
const color = React.useMemo(() => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return Colors.AccentYellow;
|
||||
case 'completed':
|
||||
return Colors.AccentGreen;
|
||||
case 'failed':
|
||||
return Colors.AccentRed;
|
||||
default:
|
||||
return Colors.Gray;
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Box marginRight={1}>
|
||||
<Text color={color}>●</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}> = ({ status }) => (
|
||||
<Box marginLeft={1} marginRight={1}>
|
||||
<Text color={getStatusColor(status)}>●</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* Status indicator component
|
||||
@@ -82,109 +193,95 @@ const StatusDot: React.FC<{
|
||||
const StatusIndicator: React.FC<{
|
||||
status: TaskResultDisplay['status'];
|
||||
}> = ({ status }) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return <Text color={Colors.AccentYellow}>Running</Text>;
|
||||
case 'completed':
|
||||
return <Text color={Colors.AccentGreen}>Completed</Text>;
|
||||
case 'failed':
|
||||
return <Text color={Colors.AccentRed}>Failed</Text>;
|
||||
default:
|
||||
return <Text color={Colors.Gray}>Unknown</Text>;
|
||||
}
|
||||
const color = getStatusColor(status);
|
||||
const text = getStatusText(status);
|
||||
return <Text color={color}>{text}</Text>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Progress section for running executions
|
||||
* Tool calls list - format consistent with ToolInfo in ToolMessage.tsx
|
||||
*/
|
||||
const ProgressSection: React.FC<{
|
||||
progress: {
|
||||
toolCalls?: Array<{
|
||||
name: string;
|
||||
status: 'executing' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}>;
|
||||
};
|
||||
}> = ({ progress }) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{progress.toolCalls && progress.toolCalls.length > 0 && (
|
||||
<CleanToolCallsList toolCalls={progress.toolCalls} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
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
|
||||
|
||||
/**
|
||||
* Clean tool calls list - format consistent with ToolInfo in ToolMessage.tsx
|
||||
*/
|
||||
const CleanToolCallsList: React.FC<{
|
||||
toolCalls: Array<{
|
||||
name: string;
|
||||
status: 'executing' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}>;
|
||||
}> = ({ toolCalls }) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text bold>Tools:</Text>
|
||||
// Reverse the order to show most recent first
|
||||
const reversedDisplayCalls = [...displayCalls].reverse();
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text color={theme.text.primary}>Tools:</Text>
|
||||
{shouldTruncate && displayMode === 'default' && (
|
||||
<Text color={Colors.Gray}>
|
||||
{' '}
|
||||
Showing the last 5 of {calls.length} tools.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{reversedDisplayCalls.map((toolCall, index) => (
|
||||
<ToolCallItem key={`${toolCall.name}-${index}`} toolCall={toolCall} />
|
||||
))}
|
||||
</Box>
|
||||
{toolCalls.map((toolCall, index) => (
|
||||
<CleanToolCallItem
|
||||
key={`${toolCall.name}-${index}`}
|
||||
toolCall={toolCall}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Individual tool call item - consistent with ToolInfo format
|
||||
*/
|
||||
const CleanToolCallItem: React.FC<{
|
||||
const ToolCallItem: React.FC<{
|
||||
toolCall: {
|
||||
name: string;
|
||||
status: 'executing' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: 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 <Text color={Colors.AccentYellow}>⊷</Text>; // Using same as ToolMessage
|
||||
return <Text color={color}>⊷</Text>; // Using same as ToolMessage
|
||||
case 'success':
|
||||
return <Text color={Colors.AccentGreen}>✔</Text>;
|
||||
return <Text color={color}>✔</Text>;
|
||||
case 'failed':
|
||||
return (
|
||||
<Text color={Colors.AccentRed} bold>
|
||||
<Text color={color} bold>
|
||||
x
|
||||
</Text>
|
||||
);
|
||||
default:
|
||||
return <Text color={Colors.Gray}>o</Text>;
|
||||
return <Text color={color}>o</Text>;
|
||||
}
|
||||
}, [toolCall.status]);
|
||||
|
||||
const description = getToolDescription(toolCall);
|
||||
|
||||
// Get first line of returnDisplay for truncated output
|
||||
const truncatedOutput = React.useMemo(() => {
|
||||
if (!toolCall.returnDisplay) return '';
|
||||
const firstLine = toolCall.returnDisplay.split('\n')[0];
|
||||
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.returnDisplay]);
|
||||
}, [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 (
|
||||
<Box flexDirection="column" paddingLeft={1} marginBottom={0}>
|
||||
@@ -197,7 +294,7 @@ const CleanToolCallItem: React.FC<{
|
||||
</Text>{' '}
|
||||
<Text color={Colors.Gray}>{description}</Text>
|
||||
{toolCall.error && (
|
||||
<Text color={Colors.AccentRed}> - {toolCall.error}</Text>
|
||||
<Text color={theme.status.error}> - {toolCall.error}</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -212,59 +309,16 @@ const CleanToolCallItem: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get tool description from args
|
||||
*/
|
||||
const getToolDescription = (toolCall: {
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
}): string => {
|
||||
if (!toolCall.args) return '';
|
||||
|
||||
// Handle common tool patterns
|
||||
if (toolCall.name === 'Glob' && toolCall.args['glob_pattern']) {
|
||||
return `"${toolCall.args['glob_pattern']}"`;
|
||||
}
|
||||
if (toolCall.name === 'ReadFile' && toolCall.args['target_file']) {
|
||||
const path = toolCall.args['target_file'] as string;
|
||||
return path.split('/').pop() || path;
|
||||
}
|
||||
if (toolCall.name === 'SearchFileContent' && toolCall.args['pattern']) {
|
||||
return `"${toolCall.args['pattern']}"`;
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
const firstArg = Object.values(toolCall.args)[0];
|
||||
if (typeof firstArg === 'string' && firstArg.length < 50) {
|
||||
return firstArg;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Execution summary details component
|
||||
*/
|
||||
const ExecutionSummaryDetails: React.FC<{
|
||||
data: TaskResultDisplay;
|
||||
}> = ({ data }) => {
|
||||
// Parse execution summary for structured data
|
||||
const summaryData = React.useMemo(() => {
|
||||
if (!data.executionSummary) return null;
|
||||
displayMode: DisplayMode;
|
||||
}> = ({ data, displayMode: _displayMode }) => {
|
||||
const stats = data.executionSummary;
|
||||
|
||||
// Try to extract structured data from execution summary
|
||||
const durationMatch = data.executionSummary.match(/Duration:\s*([^\n]+)/i);
|
||||
const roundsMatch = data.executionSummary.match(/Rounds:\s*(\d+)/i);
|
||||
const tokensMatch = data.executionSummary.match(/Tokens:\s*([\d,]+)/i);
|
||||
|
||||
return {
|
||||
duration: durationMatch?.[1] || 'N/A',
|
||||
rounds: roundsMatch?.[1] || 'N/A',
|
||||
tokens: tokensMatch?.[1] || 'N/A',
|
||||
};
|
||||
}, [data.executionSummary]);
|
||||
|
||||
if (!summaryData) {
|
||||
if (!stats) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text color={Colors.Gray}>• No summary available</Text>
|
||||
@@ -275,13 +329,13 @@ const ExecutionSummaryDetails: React.FC<{
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text>
|
||||
• <Text bold>Duration:</Text> {summaryData.duration}
|
||||
• <Text>Duration: {fmtDuration(stats.totalDurationMs)}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
• <Text bold>Rounds:</Text> {summaryData.rounds}
|
||||
• <Text>Rounds: {stats.rounds}</Text>
|
||||
</Text>
|
||||
<Text>
|
||||
• <Text bold>Tokens:</Text> {summaryData.tokens}
|
||||
• <Text>Tokens: {stats.totalTokens.toLocaleString()}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -291,37 +345,35 @@ const ExecutionSummaryDetails: React.FC<{
|
||||
* Tool usage statistics component
|
||||
*/
|
||||
const ToolUsageStats: React.FC<{
|
||||
toolCalls: Array<{
|
||||
name: string;
|
||||
status: 'executing' | 'success' | 'failed';
|
||||
error?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
returnDisplay?: string;
|
||||
}>;
|
||||
}> = ({ toolCalls }) => {
|
||||
const stats = React.useMemo(() => {
|
||||
const total = toolCalls.length;
|
||||
const successful = toolCalls.filter(
|
||||
(call) => call.status === 'success',
|
||||
).length;
|
||||
const failed = toolCalls.filter((call) => call.status === 'failed').length;
|
||||
const successRate =
|
||||
total > 0 ? ((successful / total) * 100).toFixed(1) : '0.0';
|
||||
|
||||
return { total, successful, failed, successRate };
|
||||
}, [toolCalls]);
|
||||
executionSummary?: SubagentStatsSummary;
|
||||
}> = ({ executionSummary }) => {
|
||||
if (!executionSummary) {
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text color={Colors.Gray}>• No tool usage data available</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={1}>
|
||||
<Text>
|
||||
• <Text bold>Total Calls:</Text> {stats.total}
|
||||
• <Text bold>Total Calls:</Text> {executionSummary.totalToolCalls}
|
||||
</Text>
|
||||
<Text>
|
||||
• <Text bold>Success Rate:</Text>{' '}
|
||||
<Text color={Colors.AccentGreen}>{stats.successRate}%</Text> (
|
||||
<Text color={Colors.AccentGreen}>{stats.successful} success</Text>,{' '}
|
||||
<Text color={Colors.AccentRed}>{stats.failed} failed</Text>)
|
||||
<Text color={Colors.AccentGreen}>
|
||||
{executionSummary.successRate.toFixed(1)}%
|
||||
</Text>{' '}
|
||||
(
|
||||
<Text color={Colors.AccentGreen}>
|
||||
{executionSummary.successfulToolCalls} success
|
||||
</Text>
|
||||
,{' '}
|
||||
<Text color={Colors.AccentRed}>
|
||||
{executionSummary.failedToolCalls} failed
|
||||
</Text>
|
||||
)
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -332,47 +384,39 @@ const ToolUsageStats: React.FC<{
|
||||
*/
|
||||
const ResultsSection: React.FC<{
|
||||
data: TaskResultDisplay;
|
||||
}> = ({ data }) => (
|
||||
<Box flexDirection="column">
|
||||
displayMode: DisplayMode;
|
||||
}> = ({ data, displayMode }) => (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{/* Tool calls section - clean list format */}
|
||||
{data.progress?.toolCalls && data.progress.toolCalls.length > 0 && (
|
||||
<CleanToolCallsList toolCalls={data.progress.toolCalls} />
|
||||
{data.toolCalls && data.toolCalls.length > 0 && (
|
||||
<ToolCallsList toolCalls={data.toolCalls} displayMode={displayMode} />
|
||||
)}
|
||||
|
||||
{/* Task Completed section */}
|
||||
<Box flexDirection="row" marginTop={1} marginBottom={1}>
|
||||
<Text>📄 </Text>
|
||||
<Text bold>Task Completed: </Text>
|
||||
<Text>{data.taskDescription}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Execution Summary section */}
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text>📊 </Text>
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
<Text color={theme.text.primary}>
|
||||
Execution Summary:
|
||||
</Text>
|
||||
</Box>
|
||||
<ExecutionSummaryDetails data={data} />
|
||||
<ExecutionSummaryDetails data={data} displayMode={displayMode} />
|
||||
</Box>
|
||||
|
||||
{/* Tool Usage section */}
|
||||
{data.progress?.toolCalls && data.progress.toolCalls.length > 0 && (
|
||||
{data.executionSummary && (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text>🔧 </Text>
|
||||
<Text bold color={Colors.AccentBlue}>
|
||||
<Text color={theme.text.primary}>
|
||||
Tool Usage:
|
||||
</Text>
|
||||
</Box>
|
||||
<ToolUsageStats toolCalls={data.progress.toolCalls} />
|
||||
<ToolUsageStats executionSummary={data.executionSummary} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error reason for failed tasks */}
|
||||
{data.status === 'failed' && data.terminateReason && (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={Colors.AccentRed}>❌ Failed: </Text>
|
||||
<Text color={Colors.Gray}>{data.terminateReason}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const initialWizardState: CreationWizardState = {
|
||||
generatedDescription: '',
|
||||
generatedName: '',
|
||||
selectedTools: [],
|
||||
backgroundColor: 'auto',
|
||||
color: 'auto',
|
||||
isGenerating: false,
|
||||
validationErrors: [],
|
||||
canProceed: false,
|
||||
@@ -84,7 +84,7 @@ export function wizardReducer(
|
||||
case 'SET_BACKGROUND_COLOR':
|
||||
return {
|
||||
...state,
|
||||
backgroundColor: action.color,
|
||||
color: action.color,
|
||||
canProceed: true,
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@ function validateStep(step: number, state: CreationWizardState): boolean {
|
||||
return true; // Always can proceed from tool selection
|
||||
|
||||
case WIZARD_STEPS.FINAL_CONFIRMATION: // Final confirmation
|
||||
return state.backgroundColor.length > 0;
|
||||
return state.color.length > 0;
|
||||
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -34,8 +34,8 @@ export interface CreationWizardState {
|
||||
/** Selected tools for the subagent */
|
||||
selectedTools: string[];
|
||||
|
||||
/** Background color for runtime display */
|
||||
backgroundColor: string;
|
||||
/** Color for runtime display */
|
||||
color: string;
|
||||
|
||||
/** Whether LLM generation is in progress */
|
||||
isGenerating: boolean;
|
||||
@@ -108,7 +108,7 @@ export interface WizardResult {
|
||||
systemPrompt: string;
|
||||
location: SubagentLevel;
|
||||
tools?: string[];
|
||||
backgroundColor: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const MANAGEMENT_STEPS = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { COLOR_OPTIONS } from './constants.js';
|
||||
|
||||
export const shouldShowColor = (backgroundColor?: string): boolean =>
|
||||
backgroundColor !== undefined && backgroundColor !== 'auto';
|
||||
export const shouldShowColor = (color?: string): boolean =>
|
||||
color !== undefined && color !== 'auto';
|
||||
|
||||
export const getColorForDisplay = (colorName?: string): string | undefined =>
|
||||
!colorName || colorName === 'auto'
|
||||
@@ -20,3 +20,16 @@ export function sanitizeInput(input: string): string {
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
); // Limit length
|
||||
}
|
||||
|
||||
export function fmtDuration(ms: number): string {
|
||||
if (ms < 1000) return `${Math.round(ms)}ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||
if (ms < 3600000) {
|
||||
const m = Math.floor(ms / 60000);
|
||||
const s = Math.floor((ms % 60000) / 1000);
|
||||
return `${m}m ${s}s`;
|
||||
}
|
||||
const h = Math.floor(ms / 3600000);
|
||||
const m = Math.floor((ms % 3600000) / 60000);
|
||||
return `${h}h ${m}m`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user