2026-03-02 21:04:31 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* @license
|
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type React from 'react';
|
|
|
|
|
|
import { Box, Text } from 'ink';
|
|
|
|
|
|
import { theme } from '../../semantic-colors.js';
|
|
|
|
|
|
import Spinner from 'ink-spinner';
|
2026-03-17 23:11:20 -04:00
|
|
|
|
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
2026-03-02 21:04:31 +00:00
|
|
|
|
import type {
|
|
|
|
|
|
SubagentProgress,
|
|
|
|
|
|
SubagentActivityItem,
|
|
|
|
|
|
} from '@google/gemini-cli-core';
|
|
|
|
|
|
import { TOOL_STATUS } from '../../constants.js';
|
|
|
|
|
|
import { STATUS_INDICATOR_WIDTH } from './ToolShared.js';
|
2026-03-17 23:11:20 -04:00
|
|
|
|
import { safeJsonToMarkdown } from '@google/gemini-cli-core';
|
2026-03-02 21:04:31 +00:00
|
|
|
|
|
|
|
|
|
|
export interface SubagentProgressDisplayProps {
|
|
|
|
|
|
progress: SubagentProgress;
|
2026-03-17 23:11:20 -04:00
|
|
|
|
terminalWidth: number;
|
2026-03-31 17:54:22 -04:00
|
|
|
|
historyOverrides?: SubagentActivityItem[];
|
2026-03-02 21:04:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 23:11:20 -04:00
|
|
|
|
export const formatToolArgs = (args?: string): string => {
|
2026-03-02 21:04:31 +00:00
|
|
|
|
if (!args) return '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed: unknown = JSON.parse(args);
|
|
|
|
|
|
if (typeof parsed !== 'object' || parsed === null) {
|
|
|
|
|
|
return args;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
'description' in parsed &&
|
|
|
|
|
|
typeof parsed.description === 'string' &&
|
|
|
|
|
|
parsed.description
|
|
|
|
|
|
) {
|
|
|
|
|
|
return parsed.description;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ('command' in parsed && typeof parsed.command === 'string')
|
|
|
|
|
|
return parsed.command;
|
|
|
|
|
|
if ('file_path' in parsed && typeof parsed.file_path === 'string')
|
|
|
|
|
|
return parsed.file_path;
|
|
|
|
|
|
if ('dir_path' in parsed && typeof parsed.dir_path === 'string')
|
|
|
|
|
|
return parsed.dir_path;
|
|
|
|
|
|
if ('query' in parsed && typeof parsed.query === 'string')
|
|
|
|
|
|
return parsed.query;
|
|
|
|
|
|
if ('url' in parsed && typeof parsed.url === 'string') return parsed.url;
|
|
|
|
|
|
if ('target' in parsed && typeof parsed.target === 'string')
|
|
|
|
|
|
return parsed.target;
|
|
|
|
|
|
|
|
|
|
|
|
return args;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return args;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const SubagentProgressDisplay: React.FC<
|
|
|
|
|
|
SubagentProgressDisplayProps
|
2026-03-31 17:54:22 -04:00
|
|
|
|
> = ({ progress, terminalWidth, historyOverrides }) => {
|
2026-03-02 21:04:31 +00:00
|
|
|
|
let headerText: string | undefined;
|
|
|
|
|
|
let headerColor = theme.text.secondary;
|
|
|
|
|
|
|
2026-04-15 10:48:03 -07:00
|
|
|
|
const isCloud =
|
|
|
|
|
|
progress.agentName === 'cloud-subagent' ||
|
|
|
|
|
|
progress.agentName === 'cloud_subagent';
|
|
|
|
|
|
const prefix = isCloud ? '☁ Cloud' : `Subagent ${progress.agentName}`;
|
|
|
|
|
|
|
2026-03-02 21:04:31 +00:00
|
|
|
|
if (progress.state === 'cancelled') {
|
2026-04-15 10:48:03 -07:00
|
|
|
|
headerText = `${prefix} was cancelled.`;
|
2026-03-02 21:04:31 +00:00
|
|
|
|
headerColor = theme.status.warning;
|
|
|
|
|
|
} else if (progress.state === 'error') {
|
2026-04-15 10:48:03 -07:00
|
|
|
|
headerText = `${prefix} failed.`;
|
2026-03-02 21:04:31 +00:00
|
|
|
|
headerColor = theme.status.error;
|
|
|
|
|
|
} else if (progress.state === 'completed') {
|
2026-04-15 10:48:03 -07:00
|
|
|
|
headerText = `${prefix} completed.`;
|
2026-03-02 21:04:31 +00:00
|
|
|
|
headerColor = theme.status.success;
|
2026-03-17 23:11:20 -04:00
|
|
|
|
} else {
|
2026-04-15 10:48:03 -07:00
|
|
|
|
headerText = isCloud
|
|
|
|
|
|
? `☁ Running cloud subagent...`
|
|
|
|
|
|
: `Running subagent ${progress.agentName}...`;
|
|
|
|
|
|
headerColor = isCloud ? theme.status.warning : theme.text.primary;
|
2026-03-02 21:04:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box flexDirection="column" paddingY={0}>
|
|
|
|
|
|
{headerText && (
|
|
|
|
|
|
<Box marginBottom={1}>
|
|
|
|
|
|
<Text color={headerColor} italic>
|
|
|
|
|
|
{headerText}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Box flexDirection="column" marginLeft={0} gap={0}>
|
2026-03-31 17:54:22 -04:00
|
|
|
|
{(historyOverrides ?? progress.recentActivity).map(
|
|
|
|
|
|
(item: SubagentActivityItem) => {
|
|
|
|
|
|
if (item.type === 'thought') {
|
|
|
|
|
|
const isCancellation = item.content === 'Request cancelled.';
|
|
|
|
|
|
const icon = isCancellation ? 'ℹ ' : '💭';
|
|
|
|
|
|
const color = isCancellation
|
|
|
|
|
|
? theme.status.warning
|
|
|
|
|
|
: theme.text.secondary;
|
2026-03-02 21:04:31 +00:00
|
|
|
|
|
2026-03-31 17:54:22 -04:00
|
|
|
|
return (
|
|
|
|
|
|
<Box key={item.id} flexDirection="row">
|
|
|
|
|
|
<Box minWidth={STATUS_INDICATOR_WIDTH}>
|
|
|
|
|
|
<Text color={color}>{icon}</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
<Box flexGrow={1}>
|
|
|
|
|
|
<Text color={color}>{item.content}</Text>
|
|
|
|
|
|
</Box>
|
2026-03-02 21:04:31 +00:00
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
2026-03-31 17:54:22 -04:00
|
|
|
|
} else if (item.type === 'tool_call') {
|
|
|
|
|
|
const statusSymbol =
|
|
|
|
|
|
item.status === 'running' ? (
|
|
|
|
|
|
<Spinner type="dots" />
|
|
|
|
|
|
) : item.status === 'completed' ? (
|
|
|
|
|
|
<Text color={theme.status.success}>
|
|
|
|
|
|
{TOOL_STATUS.SUCCESS}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
) : item.status === 'cancelled' ? (
|
|
|
|
|
|
<Text color={theme.status.warning} bold>
|
|
|
|
|
|
{TOOL_STATUS.CANCELED}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Text color={theme.status.error}>{TOOL_STATUS.ERROR}</Text>
|
|
|
|
|
|
);
|
2026-03-02 21:04:31 +00:00
|
|
|
|
|
2026-03-31 17:54:22 -04:00
|
|
|
|
const formattedArgs =
|
|
|
|
|
|
item.description || formatToolArgs(item.args);
|
|
|
|
|
|
const displayArgs =
|
|
|
|
|
|
formattedArgs.length > 60
|
|
|
|
|
|
? formattedArgs.slice(0, 60) + '...'
|
|
|
|
|
|
: formattedArgs;
|
2026-03-02 21:04:31 +00:00
|
|
|
|
|
2026-03-31 17:54:22 -04:00
|
|
|
|
return (
|
|
|
|
|
|
<Box key={item.id} flexDirection="row">
|
|
|
|
|
|
<Box minWidth={STATUS_INDICATOR_WIDTH}>{statusSymbol}</Box>
|
|
|
|
|
|
<Box flexDirection="row" flexGrow={1} flexWrap="wrap">
|
|
|
|
|
|
<Text
|
|
|
|
|
|
bold
|
|
|
|
|
|
color={theme.text.primary}
|
|
|
|
|
|
strikethrough={item.status === 'cancelled'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.displayName || item.content}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{displayArgs && (
|
|
|
|
|
|
<Box marginLeft={1}>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
color={theme.text.secondary}
|
|
|
|
|
|
wrap="truncate"
|
|
|
|
|
|
strikethrough={item.status === 'cancelled'}
|
|
|
|
|
|
>
|
|
|
|
|
|
{displayArgs}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Box>
|
2026-03-02 21:04:31 +00:00
|
|
|
|
</Box>
|
2026-03-31 17:54:22 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
},
|
|
|
|
|
|
)}
|
2026-03-02 21:04:31 +00:00
|
|
|
|
</Box>
|
2026-03-17 23:11:20 -04:00
|
|
|
|
|
2026-03-24 11:34:04 -04:00
|
|
|
|
{progress.result && (
|
2026-03-17 23:11:20 -04:00
|
|
|
|
<Box flexDirection="column" marginTop={1}>
|
|
|
|
|
|
{progress.terminateReason && progress.terminateReason !== 'GOAL' && (
|
|
|
|
|
|
<Box marginBottom={1}>
|
|
|
|
|
|
<Text color={theme.status.warning} bold>
|
|
|
|
|
|
Agent Finished Early ({progress.terminateReason})
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<MarkdownDisplay
|
|
|
|
|
|
text={safeJsonToMarkdown(progress.result)}
|
2026-03-24 11:34:04 -04:00
|
|
|
|
isPending={progress.state !== 'completed'}
|
2026-03-17 23:11:20 -04:00
|
|
|
|
terminalWidth={terminalWidth}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
2026-03-02 21:04:31 +00:00
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|