mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-12 22:31:12 -07:00
fix(cli): resolve subagent grouping and UI state persistence (#22252)
This commit is contained in:
269
packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx
Normal file
269
packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect, useId } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import {
|
||||
isSubagentProgress,
|
||||
checkExhaustive,
|
||||
type SubagentActivityItem,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
SubagentProgressDisplay,
|
||||
formatToolArgs,
|
||||
} from './SubagentProgressDisplay.js';
|
||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||
|
||||
export interface SubagentGroupDisplayProps {
|
||||
toolCalls: IndividualToolCallDisplay[];
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
borderColor?: string;
|
||||
borderDimColor?: boolean;
|
||||
isFirst?: boolean;
|
||||
isExpandable?: boolean;
|
||||
}
|
||||
|
||||
export const SubagentGroupDisplay: React.FC<SubagentGroupDisplayProps> = ({
|
||||
toolCalls,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
isFirst,
|
||||
isExpandable = true,
|
||||
}) => {
|
||||
const isExpanded = availableTerminalHeight === undefined;
|
||||
const overflowActions = useOverflowActions();
|
||||
const uniqueId = useId();
|
||||
const overflowId = `subagent-${uniqueId}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpandable && overflowActions) {
|
||||
// Register with the global overflow system so "ctrl+o to expand" shows in the sticky footer
|
||||
// and AppContainer passes the shortcut through.
|
||||
overflowActions.addOverflowingId(overflowId);
|
||||
}
|
||||
return () => {
|
||||
if (overflowActions) {
|
||||
overflowActions.removeOverflowingId(overflowId);
|
||||
}
|
||||
};
|
||||
}, [isExpandable, overflowActions, overflowId]);
|
||||
|
||||
if (toolCalls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let headerText = '';
|
||||
if (toolCalls.length === 1) {
|
||||
const singleAgent = toolCalls[0].resultDisplay;
|
||||
if (isSubagentProgress(singleAgent)) {
|
||||
switch (singleAgent.state) {
|
||||
case 'completed':
|
||||
headerText = 'Agent Completed';
|
||||
break;
|
||||
case 'cancelled':
|
||||
headerText = 'Agent Cancelled';
|
||||
break;
|
||||
case 'error':
|
||||
headerText = 'Agent Error';
|
||||
break;
|
||||
default:
|
||||
headerText = 'Running Agent...';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
headerText = 'Running Agent...';
|
||||
}
|
||||
} else {
|
||||
let completedCount = 0;
|
||||
let runningCount = 0;
|
||||
for (const tc of toolCalls) {
|
||||
const progress = tc.resultDisplay;
|
||||
if (isSubagentProgress(progress)) {
|
||||
if (progress.state === 'completed') completedCount++;
|
||||
else if (progress.state === 'running') runningCount++;
|
||||
} else {
|
||||
// It hasn't emitted progress yet, but it is "running"
|
||||
runningCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (completedCount === toolCalls.length) {
|
||||
headerText = `${toolCalls.length} Agents Completed`;
|
||||
} else if (completedCount > 0) {
|
||||
headerText = `${toolCalls.length} Agents (${runningCount} running, ${completedCount} completed)...`;
|
||||
} else {
|
||||
headerText = `Running ${toolCalls.length} Agents...`;
|
||||
}
|
||||
}
|
||||
const toggleText = `(ctrl+o to ${isExpanded ? 'collapse' : 'expand'})`;
|
||||
|
||||
const renderCollapsedRow = (
|
||||
key: string,
|
||||
agentName: string,
|
||||
icon: React.ReactNode,
|
||||
content: string,
|
||||
displayArgs?: string,
|
||||
) => (
|
||||
<Box key={key} flexDirection="row" marginLeft={0} marginTop={0}>
|
||||
<Box minWidth={2} flexShrink={0}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Box flexShrink={0}>
|
||||
<Text bold color={theme.text.primary} wrap="truncate">
|
||||
{agentName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexShrink={0}>
|
||||
<Text color={theme.text.secondary}> · </Text>
|
||||
</Box>
|
||||
<Box flexShrink={1} minWidth={0}>
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{content}
|
||||
{displayArgs && ` ${displayArgs}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={terminalWidth}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderTop={isFirst}
|
||||
borderBottom={false}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderStyle="round"
|
||||
paddingLeft={1}
|
||||
paddingTop={0}
|
||||
paddingBottom={0}
|
||||
>
|
||||
<Box flexDirection="row" gap={1} marginBottom={isExpanded ? 1 : 0}>
|
||||
<Text color={theme.text.secondary}>≡</Text>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{headerText}
|
||||
</Text>
|
||||
{isExpandable && <Text color={theme.text.secondary}>{toggleText}</Text>}
|
||||
</Box>
|
||||
|
||||
{toolCalls.map((toolCall) => {
|
||||
const progress = toolCall.resultDisplay;
|
||||
|
||||
if (!isSubagentProgress(progress)) {
|
||||
const agentName = toolCall.name || 'agent';
|
||||
if (!isExpanded) {
|
||||
return renderCollapsedRow(
|
||||
toolCall.callId,
|
||||
agentName,
|
||||
<Text color={theme.text.primary}>!</Text>,
|
||||
'Starting...',
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box
|
||||
key={toolCall.callId}
|
||||
flexDirection="column"
|
||||
marginLeft={0}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Box flexDirection="row" gap={1}>
|
||||
<Text color={theme.text.primary}>!</Text>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{agentName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2}>
|
||||
<Text color={theme.text.secondary}>Starting...</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const lastActivity: SubagentActivityItem | undefined =
|
||||
progress.recentActivity[progress.recentActivity.length - 1];
|
||||
|
||||
// Collapsed View: Show single compact line per agent
|
||||
if (!isExpanded) {
|
||||
let content = 'Starting...';
|
||||
let formattedArgs: string | undefined;
|
||||
|
||||
if (progress.state === 'completed') {
|
||||
if (
|
||||
progress.terminateReason &&
|
||||
progress.terminateReason !== 'GOAL'
|
||||
) {
|
||||
content = `Finished Early (${progress.terminateReason})`;
|
||||
} else {
|
||||
content = 'Completed successfully';
|
||||
}
|
||||
} else if (lastActivity) {
|
||||
// Match expanded view logic exactly:
|
||||
// Primary text: displayName || content
|
||||
content = lastActivity.displayName || lastActivity.content;
|
||||
|
||||
// Secondary text: description || formatToolArgs(args)
|
||||
if (lastActivity.description) {
|
||||
formattedArgs = lastActivity.description;
|
||||
} else if (lastActivity.type === 'tool_call' && lastActivity.args) {
|
||||
formattedArgs = formatToolArgs(lastActivity.args);
|
||||
}
|
||||
}
|
||||
|
||||
const displayArgs =
|
||||
progress.state === 'completed' ? '' : formattedArgs;
|
||||
|
||||
const renderStatusIcon = () => {
|
||||
const state = progress.state ?? 'running';
|
||||
switch (state) {
|
||||
case 'running':
|
||||
return <Text color={theme.text.primary}>!</Text>;
|
||||
case 'completed':
|
||||
return <Text color={theme.status.success}>✓</Text>;
|
||||
case 'cancelled':
|
||||
return <Text color={theme.status.warning}>ℹ</Text>;
|
||||
case 'error':
|
||||
return <Text color={theme.status.error}>✗</Text>;
|
||||
default:
|
||||
return checkExhaustive(state);
|
||||
}
|
||||
};
|
||||
|
||||
return renderCollapsedRow(
|
||||
toolCall.callId,
|
||||
progress.agentName,
|
||||
renderStatusIcon(),
|
||||
lastActivity?.type === 'thought' ? `💭 ${content}` : content,
|
||||
displayArgs,
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded View: Render full history
|
||||
return (
|
||||
<Box
|
||||
key={toolCall.callId}
|
||||
flexDirection="column"
|
||||
marginLeft={0}
|
||||
marginBottom={1}
|
||||
>
|
||||
<SubagentProgressDisplay
|
||||
progress={progress}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user