diff --git a/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx
new file mode 100644
index 0000000000..197b78e356
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.test.tsx
@@ -0,0 +1,120 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { waitFor } from '../../../test-utils/async.js';
+import { render } from '../../../test-utils/render.js';
+import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
+import { Kind, CoreToolCallStatus } from '@google/gemini-cli-core';
+import type { IndividualToolCallDisplay } from '../../types.js';
+import { KeypressProvider } from '../../contexts/KeypressContext.js';
+import { OverflowProvider } from '../../contexts/OverflowContext.js';
+import { vi } from 'vitest';
+import { Text } from 'ink';
+
+vi.mock('../../utils/MarkdownDisplay.js', () => ({
+ MarkdownDisplay: ({ text }: { text: string }) => {text},
+}));
+
+describe('', () => {
+ const mockToolCalls: IndividualToolCallDisplay[] = [
+ {
+ callId: 'call-1',
+ name: 'agent_1',
+ description: 'Test agent 1',
+ confirmationDetails: undefined,
+ status: CoreToolCallStatus.Executing,
+ kind: Kind.Agent,
+ resultDisplay: {
+ isSubagentProgress: true,
+ agentName: 'api-monitor',
+ state: 'running',
+ recentActivity: [
+ {
+ id: 'act-1',
+ type: 'tool_call',
+ status: 'running',
+ content: '',
+ displayName: 'Action Required',
+ description: 'Verify server is running',
+ },
+ ],
+ },
+ },
+ {
+ callId: 'call-2',
+ name: 'agent_2',
+ description: 'Test agent 2',
+ confirmationDetails: undefined,
+ status: CoreToolCallStatus.Success,
+ kind: Kind.Agent,
+ resultDisplay: {
+ isSubagentProgress: true,
+ agentName: 'db-manager',
+ state: 'completed',
+ result: 'Database schema validated',
+ recentActivity: [
+ {
+ id: 'act-2',
+ type: 'thought',
+ status: 'completed',
+ content: 'Database schema validated',
+ },
+ ],
+ },
+ },
+ ];
+
+ const renderSubagentGroup = (
+ toolCallsToRender: IndividualToolCallDisplay[],
+ height?: number,
+ ) => (
+
+
+
+
+
+ );
+
+ it('renders nothing if there are no agent tool calls', async () => {
+ const { lastFrame } = render(renderSubagentGroup([], 40));
+ expect(lastFrame({ allowEmpty: true })).toBe('');
+ });
+
+ it('renders collapsed view by default with correct agent counts and states', async () => {
+ const { lastFrame, waitUntilReady } = render(
+ renderSubagentGroup(mockToolCalls, 40),
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('expands when availableTerminalHeight is undefined', async () => {
+ const { lastFrame, rerender } = render(
+ renderSubagentGroup(mockToolCalls, 40),
+ );
+
+ // Default collapsed view
+ await waitFor(() => {
+ expect(lastFrame()).toContain('(ctrl+o to expand)');
+ });
+
+ // Expand view
+ rerender(renderSubagentGroup(mockToolCalls, undefined));
+ await waitFor(() => {
+ expect(lastFrame()).toContain('(ctrl+o to collapse)');
+ });
+
+ // Collapse view
+ rerender(renderSubagentGroup(mockToolCalls, 40));
+ await waitFor(() => {
+ expect(lastFrame()).toContain('(ctrl+o to expand)');
+ });
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx
new file mode 100644
index 0000000000..2d3f8a44c8
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/SubagentGroupDisplay.tsx
@@ -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 = ({
+ 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,
+ ) => (
+
+
+ {icon}
+
+
+
+ {agentName}
+
+
+
+ ·
+
+
+
+ {content}
+ {displayArgs && ` ${displayArgs}`}
+
+
+
+ );
+
+ return (
+
+
+ ≡
+
+ {headerText}
+
+ {isExpandable && {toggleText}}
+
+
+ {toolCalls.map((toolCall) => {
+ const progress = toolCall.resultDisplay;
+
+ if (!isSubagentProgress(progress)) {
+ const agentName = toolCall.name || 'agent';
+ if (!isExpanded) {
+ return renderCollapsedRow(
+ toolCall.callId,
+ agentName,
+ !,
+ 'Starting...',
+ );
+ } else {
+ return (
+
+
+ !
+
+ {agentName}
+
+
+
+ Starting...
+
+
+ );
+ }
+ }
+
+ 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 !;
+ case 'completed':
+ return ✓;
+ case 'cancelled':
+ return ℹ;
+ case 'error':
+ return ✗;
+ default:
+ return checkExhaustive(state);
+ }
+ };
+
+ return renderCollapsedRow(
+ toolCall.callId,
+ progress.agentName,
+ renderStatusIcon(),
+ lastActivity?.type === 'thought' ? `💭 ${content}` : content,
+ displayArgs,
+ );
+ }
+
+ // Expanded View: Render full history
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx
index e8b67301ad..f2c57f9662 100644
--- a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx
+++ b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.test.tsx
@@ -36,7 +36,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -60,7 +60,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -82,7 +82,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -104,7 +104,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -128,7 +128,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -149,7 +149,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -164,7 +164,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
@@ -185,7 +185,7 @@ describe('', () => {
};
const { lastFrame, waitUntilReady } = render(
- ,
+ ,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
diff --git a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx
index b34a904b3e..5d1086c759 100644
--- a/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx
+++ b/packages/cli/src/ui/components/messages/SubagentProgressDisplay.tsx
@@ -8,18 +8,21 @@ import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import Spinner from 'ink-spinner';
+import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import type {
SubagentProgress,
SubagentActivityItem,
} from '@google/gemini-cli-core';
import { TOOL_STATUS } from '../../constants.js';
import { STATUS_INDICATOR_WIDTH } from './ToolShared.js';
+import { safeJsonToMarkdown } from '@google/gemini-cli-core';
export interface SubagentProgressDisplayProps {
progress: SubagentProgress;
+ terminalWidth: number;
}
-const formatToolArgs = (args?: string): string => {
+export const formatToolArgs = (args?: string): string => {
if (!args) return '';
try {
const parsed: unknown = JSON.parse(args);
@@ -54,7 +57,7 @@ const formatToolArgs = (args?: string): string => {
export const SubagentProgressDisplay: React.FC<
SubagentProgressDisplayProps
-> = ({ progress }) => {
+> = ({ progress, terminalWidth }) => {
let headerText: string | undefined;
let headerColor = theme.text.secondary;
@@ -67,6 +70,9 @@ export const SubagentProgressDisplay: React.FC<
} else if (progress.state === 'completed') {
headerText = `Subagent ${progress.agentName} completed.`;
headerColor = theme.status.success;
+ } else {
+ headerText = `Running subagent ${progress.agentName}...`;
+ headerColor = theme.text.primary;
}
return (
@@ -146,6 +152,23 @@ export const SubagentProgressDisplay: React.FC<
return null;
})}
+
+ {progress.state === 'completed' && progress.result && (
+
+ {progress.terminateReason && progress.terminateReason !== 'GOAL' && (
+
+
+ Agent Finished Early ({progress.terminateReason})
+
+
+ )}
+
+
+ )}
);
};
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index ee3a98930f..69da3a1029 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -15,12 +15,14 @@ import type {
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
+import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import { isShellTool } from './ToolShared.js';
import {
shouldHideToolCall,
CoreToolCallStatus,
+ Kind,
} from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
@@ -125,12 +127,36 @@ export const ToolGroupMessage: React.FC = ({
let countToolCallsWithResults = 0;
for (const tool of visibleToolCalls) {
- if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
+ if (
+ tool.kind !== Kind.Agent &&
+ tool.resultDisplay !== undefined &&
+ tool.resultDisplay !== ''
+ ) {
countToolCallsWithResults++;
}
}
const countOneLineToolCalls =
- visibleToolCalls.length - countToolCallsWithResults;
+ visibleToolCalls.filter((t) => t.kind !== Kind.Agent).length -
+ countToolCallsWithResults;
+ const groupedTools = useMemo(() => {
+ const groups: Array<
+ IndividualToolCallDisplay | IndividualToolCallDisplay[]
+ > = [];
+ for (const tool of visibleToolCalls) {
+ if (tool.kind === Kind.Agent) {
+ const lastGroup = groups[groups.length - 1];
+ if (Array.isArray(lastGroup)) {
+ lastGroup.push(tool);
+ } else {
+ groups.push([tool]);
+ }
+ } else {
+ groups.push(tool);
+ }
+ }
+ return groups;
+ }, [visibleToolCalls]);
+
const availableTerminalHeightPerToolMessage = availableTerminalHeight
? Math.max(
Math.floor(
@@ -167,8 +193,29 @@ export const ToolGroupMessage: React.FC = ({
width={terminalWidth}
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
>
- {visibleToolCalls.map((tool, index) => {
+ {groupedTools.map((group, index) => {
const isFirst = index === 0;
+ const resolvedIsFirst =
+ borderTopOverride !== undefined
+ ? borderTopOverride && isFirst
+ : isFirst;
+
+ if (Array.isArray(group)) {
+ return (
+
+ );
+ }
+
+ const tool = group;
const isShellToolCall = isShellTool(tool.name);
const commonProps = {
@@ -176,10 +223,7 @@ export const ToolGroupMessage: React.FC = ({
availableTerminalHeight: availableTerminalHeightPerToolMessage,
terminalWidth: contentWidth,
emphasis: 'medium' as const,
- isFirst:
- borderTopOverride !== undefined
- ? borderTopOverride && isFirst
- : isFirst,
+ isFirst: resolvedIsFirst,
borderColor,
borderDimColor,
isExpandable,
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
index 0bbe3446e0..3b7cfaa8da 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
@@ -102,7 +102,12 @@ export const ToolResultDisplay: React.FC = ({
);
} else if (isSubagentProgress(contentData)) {
- content = ;
+ content = (
+
+ );
} else if (typeof contentData === 'string' && renderOutputAsMarkdown) {
content = (
> renders collapsed view by default with correct agent counts and states 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ ≡ 2 Agents (1 running, 1 completed)... (ctrl+o to expand) │
+│ ! api-monitor · Action Required Verify server is running │
+│ ✓ db-manager · 💭 Completed successfully │
+"
+`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap
index 8a4c5bd4c4..2d31c9c652 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/SubagentProgressDisplay.test.tsx.snap
@@ -1,7 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[` > renders "Request cancelled." with the info icon 1`] = `
-"ℹ Request cancelled.
+"Running subagent TestAgent...
+
+ℹ Request cancelled.
"
`;
@@ -11,31 +13,43 @@ exports[` > renders cancelled state correctly 1`] = `
`;
exports[` > renders correctly with command fallback 1`] = `
-"⠋ run_shell_command echo hello
+"Running subagent TestAgent...
+
+⠋ run_shell_command echo hello
"
`;
exports[` > renders correctly with description in args 1`] = `
-"⠋ run_shell_command Say hello
+"Running subagent TestAgent...
+
+⠋ run_shell_command Say hello
"
`;
exports[` > renders correctly with displayName and description from item 1`] = `
-"⠋ RunShellCommand Executing echo hello
+"Running subagent TestAgent...
+
+⠋ RunShellCommand Executing echo hello
"
`;
exports[` > renders correctly with file_path 1`] = `
-"✓ write_file /tmp/test.txt
+"Running subagent TestAgent...
+
+✓ write_file /tmp/test.txt
"
`;
exports[` > renders thought bubbles correctly 1`] = `
-"💭 Thinking about life
+"Running subagent TestAgent...
+
+💭 Thinking about life
"
`;
exports[` > truncates long args 1`] = `
-"⠋ run_shell_command This is a very long description that should definitely be tr...
+"Running subagent TestAgent...
+
+⠋ run_shell_command This is a very long description that should definitely be tr...
"
`;
diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts
index c394b866ad..2034e14b87 100644
--- a/packages/cli/src/ui/hooks/useGeminiStream.ts
+++ b/packages/cli/src/ui/hooks/useGeminiStream.ts
@@ -38,6 +38,7 @@ import {
GeminiCliOperation,
getPlanModeExitMessage,
isBackgroundExecutionData,
+ Kind,
} from '@google/gemini-cli-core';
import type {
Config,
@@ -408,7 +409,8 @@ export const useGeminiStream = (
// Push completed tools to history as they finish
useEffect(() => {
const toolsToPush: TrackedToolCall[] = [];
- for (const tc of toolCalls) {
+ for (let i = 0; i < toolCalls.length; i++) {
+ const tc = toolCalls[i];
if (pushedToolCallIdsRef.current.has(tc.request.callId)) continue;
if (
@@ -416,6 +418,40 @@ export const useGeminiStream = (
tc.status === 'error' ||
tc.status === 'cancelled'
) {
+ // TODO(#22883): This lookahead logic is a tactical UI fix to prevent parallel agents
+ // from tearing visually when they finish at slightly different times.
+ // Architecturally, `useGeminiStream` should not be responsible for stitching
+ // together semantic batches using timing/refs. `packages/core` should be
+ // refactored to emit structured `ToolBatch` or `Turn` objects, and this layer
+ // should simply render those semantic boundaries.
+ // If this is an agent tool, look ahead to ensure all subsequent
+ // contiguous agents in the same batch are also finished before pushing.
+ const isAgent = tc.tool?.kind === Kind.Agent;
+ if (isAgent) {
+ let contigAgentsComplete = true;
+ for (let j = i + 1; j < toolCalls.length; j++) {
+ const nextTc = toolCalls[j];
+ if (nextTc.tool?.kind === Kind.Agent) {
+ if (
+ nextTc.status !== 'success' &&
+ nextTc.status !== 'error' &&
+ nextTc.status !== 'cancelled'
+ ) {
+ contigAgentsComplete = false;
+ break;
+ }
+ } else {
+ // End of the contiguous agent block
+ break;
+ }
+ }
+
+ if (!contigAgentsComplete) {
+ // Wait for the entire contiguous block of agents to finish
+ break;
+ }
+ }
+
toolsToPush.push(tc);
} else {
// Stop at first non-terminal tool to preserve order
@@ -425,27 +461,27 @@ export const useGeminiStream = (
if (toolsToPush.length > 0) {
const newPushed = new Set(pushedToolCallIdsRef.current);
- let isFirst = isFirstToolInGroupRef.current;
for (const tc of toolsToPush) {
newPushed.add(tc.request.callId);
- const isLastInBatch = tc === toolCalls[toolCalls.length - 1];
-
- const historyItem = mapTrackedToolCallsToDisplay(tc, {
- borderTop: isFirst,
- borderBottom: isLastInBatch,
- ...getToolGroupBorderAppearance(
- { type: 'tool_group', tools: toolCalls },
- activeShellPtyId,
- !!isShellFocused,
- [],
- backgroundShells,
- ),
- });
- addItem(historyItem);
- isFirst = false;
}
+ const isLastInBatch =
+ toolsToPush[toolsToPush.length - 1] === toolCalls[toolCalls.length - 1];
+
+ const historyItem = mapTrackedToolCallsToDisplay(toolsToPush, {
+ borderTop: isFirstToolInGroupRef.current,
+ borderBottom: isLastInBatch,
+ ...getToolGroupBorderAppearance(
+ { type: 'tool_group', tools: toolCalls },
+ activeShellPtyId,
+ !!isShellFocused,
+ [],
+ backgroundShells,
+ ),
+ });
+ addItem(historyItem);
+
setPushedToolCallIds(newPushed);
setIsFirstToolInGroup(false);
}
diff --git a/packages/core/src/agents/local-invocation.test.ts b/packages/core/src/agents/local-invocation.test.ts
index b56fea54b6..0cd77176ba 100644
--- a/packages/core/src/agents/local-invocation.test.ts
+++ b/packages/core/src/agents/local-invocation.test.ts
@@ -207,8 +207,11 @@ describe('LocalSubagentInvocation', () => {
),
},
]);
- expect(result.returnDisplay).toBe('Analysis complete.');
- expect(result.returnDisplay).not.toContain('Termination Reason');
+ const display = result.returnDisplay as SubagentProgress;
+ expect(display.isSubagentProgress).toBe(true);
+ expect(display.state).toBe('completed');
+ expect(display.result).toBe('Analysis complete.');
+ expect(display.terminateReason).toBe(AgentTerminateMode.GOAL);
});
it('should show detailed UI for non-goal terminations (e.g., TIMEOUT)', async () => {
@@ -220,11 +223,11 @@ describe('LocalSubagentInvocation', () => {
const result = await invocation.execute(signal, updateOutput);
- expect(result.returnDisplay).toContain(
- '### Subagent MockAgent Finished Early',
- );
- expect(result.returnDisplay).toContain('**Termination Reason:** TIMEOUT');
- expect(result.returnDisplay).toContain('Partial progress...');
+ const display = result.returnDisplay as SubagentProgress;
+ expect(display.isSubagentProgress).toBe(true);
+ expect(display.state).toBe('completed');
+ expect(display.result).toBe('Partial progress...');
+ expect(display.terminateReason).toBe(AgentTerminateMode.TIMEOUT);
});
it('should stream THOUGHT_CHUNK activities from the executor', async () => {
@@ -250,8 +253,8 @@ describe('LocalSubagentInvocation', () => {
await invocation.execute(signal, updateOutput);
- expect(updateOutput).toHaveBeenCalledTimes(3); // Initial + 2 updates
- const lastCall = updateOutput.mock.calls[2][0] as SubagentProgress;
+ expect(updateOutput).toHaveBeenCalledTimes(4); // Initial + 2 updates + Final completion
+ const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
expect(lastCall.recentActivity).toContainEqual(
expect.objectContaining({
type: 'thought',
@@ -283,8 +286,8 @@ describe('LocalSubagentInvocation', () => {
await invocation.execute(signal, updateOutput);
- expect(updateOutput).toHaveBeenCalledTimes(3);
- const lastCall = updateOutput.mock.calls[2][0] as SubagentProgress;
+ expect(updateOutput).toHaveBeenCalledTimes(4); // Initial + 2 updates + Final completion
+ const lastCall = updateOutput.mock.calls[3][0] as SubagentProgress;
expect(lastCall.recentActivity).toContainEqual(
expect.objectContaining({
type: 'thought',
@@ -312,7 +315,10 @@ describe('LocalSubagentInvocation', () => {
// Execute without the optional callback
const result = await invocation.execute(signal);
expect(result.error).toBeUndefined();
- expect(result.returnDisplay).toBe('Done');
+ const display = result.returnDisplay as SubagentProgress;
+ expect(display.isSubagentProgress).toBe(true);
+ expect(display.state).toBe('completed');
+ expect(display.result).toBe('Done');
});
it('should handle executor run failure', async () => {
diff --git a/packages/core/src/agents/local-invocation.ts b/packages/core/src/agents/local-invocation.ts
index 6ef30e773c..142a0bc518 100644
--- a/packages/core/src/agents/local-invocation.ts
+++ b/packages/core/src/agents/local-invocation.ts
@@ -6,7 +6,6 @@
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import { LocalAgentExecutor } from './local-executor.js';
-import { safeJsonToMarkdown } from '../utils/markdownUtils.js';
import {
BaseToolInvocation,
type ToolResult,
@@ -246,28 +245,27 @@ export class LocalSubagentInvocation extends BaseToolInvocation<
throw cancelError;
}
- const displayResult = safeJsonToMarkdown(output.result);
+ const progress: SubagentProgress = {
+ isSubagentProgress: true,
+ agentName: this.definition.name,
+ recentActivity: [...recentActivity],
+ state: 'completed',
+ result: output.result,
+ terminateReason: output.terminate_reason,
+ };
+
+ if (updateOutput) {
+ updateOutput(progress);
+ }
const resultContent = `Subagent '${this.definition.name}' finished.
Termination Reason: ${output.terminate_reason}
Result:
${output.result}`;
- const displayContent =
- output.terminate_reason === AgentTerminateMode.GOAL
- ? displayResult
- : `
-### Subagent ${this.definition.name} Finished Early
-
-**Termination Reason:** ${output.terminate_reason}
-
-**Result/Summary:**
-${displayResult}
-`;
-
return {
llmContent: [{ text: resultContent }],
- returnDisplay: displayContent,
+ returnDisplay: progress,
};
} catch (error) {
const errorMessage =
diff --git a/packages/core/src/agents/types.ts b/packages/core/src/agents/types.ts
index 41db981a7b..2c703f90fd 100644
--- a/packages/core/src/agents/types.ts
+++ b/packages/core/src/agents/types.ts
@@ -87,6 +87,8 @@ export interface SubagentProgress {
agentName: string;
recentActivity: SubagentActivityItem[];
state?: 'running' | 'completed' | 'error' | 'cancelled';
+ result?: string;
+ terminateReason?: AgentTerminateMode;
}
export function isSubagentProgress(obj: unknown): obj is SubagentProgress {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index a76e7aa2d4..47412dd73c 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -118,6 +118,7 @@ export * from './utils/channel.js';
export * from './utils/constants.js';
export * from './utils/sessionUtils.js';
export * from './utils/cache.js';
+export * from './utils/markdownUtils.js';
// Export services
export * from './services/fileDiscoveryService.js';