diff --git a/packages/web-shell/client/components/messages/ToolGroup.test.tsx b/packages/web-shell/client/components/messages/ToolGroup.test.tsx
index f1548657e2..91e9f9f1f4 100644
--- a/packages/web-shell/client/components/messages/ToolGroup.test.tsx
+++ b/packages/web-shell/client/components/messages/ToolGroup.test.tsx
@@ -776,12 +776,122 @@ describe('tool row rendering', () => {
}
});
- it('shows failed status in the collapsed chat summary', () => {
+ it('keeps the failed label out of the collapsed chat summary', () => {
const container = renderToolGroup([
makeTool({ toolName: 'Shell', status: 'failed' }),
]);
- expect(container.querySelector('button')?.textContent).toContain('Failed');
+ const summary = container.querySelector('button');
+ expect(summary?.textContent).toContain('Shell');
+ expect(summary?.textContent).not.toContain('Failed');
+ expect(summary?.querySelector('[class*="iconError"]')).toBeNull();
+ });
+
+ it('shows the error icon in a failed tool line header', () => {
+ const container = renderToolLine(
+ makeTool({ toolName: 'Shell', status: 'failed' }),
+ );
+
+ const errorIcon = container.querySelector('[class*="iconError"]');
+ expect(errorIcon).not.toBeNull();
+ expect(errorIcon?.getAttribute('role')).toBe('img');
+ expect(errorIcon?.getAttribute('aria-label')).toBe('Failed');
+ expect(errorIcon?.querySelector('svg')).not.toBeNull();
+ expect(container.textContent).not.toContain('Failed');
+ });
+
+ it('shows an error icon instead of the failed label on expanded tool rows', () => {
+ const container = renderToolGroup([
+ makeTool({
+ toolName: 'Shell',
+ status: 'failed',
+ content: [{ type: 'content', content: { text: 'boom' } }],
+ }),
+ makeTool({ callId: 'call-2', toolName: 'Grep', status: 'completed' }),
+ ]);
+
+ const summary = container.querySelector('button') as HTMLButtonElement;
+ act(() => summary.click());
+
+ const errorIcon = container.querySelector('[class*="iconError"]');
+ expect(errorIcon).not.toBeNull();
+ expect(errorIcon?.querySelector('svg')).not.toBeNull();
+ expect(errorIcon?.textContent).not.toContain('Failed');
+ });
+
+ it('shows an error icon in the expanded single-tool card title', () => {
+ const container = renderToolGroup([
+ makeTool({
+ toolName: 'Shell',
+ status: 'failed',
+ content: [{ type: 'content', content: { text: 'boom' } }],
+ }),
+ ]);
+
+ const summary = container.querySelector('button') as HTMLButtonElement;
+ act(() => summary.click());
+
+ const titleRow = container.querySelector('[class*="expandedCardTitleRow"]');
+ expect(titleRow).not.toBeNull();
+ expect(titleRow?.querySelector('[class*="iconError"] svg')).not.toBeNull();
+ expect(titleRow?.textContent).not.toContain('Failed');
+ });
+
+ it('renders no status icon in the expanded completed tool card title', () => {
+ const container = renderToolGroup([
+ makeTool({
+ toolName: 'Shell',
+ status: 'completed',
+ content: [{ type: 'content', content: { text: 'ok' } }],
+ }),
+ ]);
+
+ const summary = container.querySelector('button') as HTMLButtonElement;
+ act(() => summary.click());
+
+ const titleRow = container.querySelector('[class*="expandedCardTitleRow"]');
+ expect(titleRow).not.toBeNull();
+ expect(titleRow?.querySelector('[class*="iconError"]')).toBeNull();
+ });
+
+ it('shows an error icon in the expanded failed todo card title', () => {
+ const container = renderToolGroup([
+ makeTool({
+ toolName: 'todo_write',
+ status: 'failed',
+ args: {
+ todos: [{ id: '1', content: 'Check UI', status: 'in_progress' }],
+ },
+ }),
+ ]);
+
+ const titleRow = container.querySelector('[class*="expandedCardTitleRow"]');
+ expect(titleRow).not.toBeNull();
+ expect(titleRow?.querySelector('[class*="iconError"] svg')).not.toBeNull();
+ });
+
+ it('shows an error icon for a single failed read tool', () => {
+ const container = renderToolGroup([
+ makeTool({
+ toolName: 'read_file',
+ status: 'failed',
+ content: [{ type: 'content', content: { text: 'Permission denied' } }],
+ }),
+ ]);
+
+ const titleRow = container.querySelector('[class*="expandedCardTitleRow"]');
+ expect(titleRow).not.toBeNull();
+ expect(titleRow?.querySelector('[class*="iconError"] svg')).not.toBeNull();
+ });
+
+ it('shows an error icon for a single failed tool without result text', () => {
+ const container = renderToolGroup([
+ makeTool({ toolName: 'glob', status: 'failed' }),
+ ]);
+
+ const titleRow = container.querySelector('[class*="expandedCardTitleRow"]');
+ expect(titleRow).not.toBeNull();
+ expect(titleRow?.querySelector('[class*="iconError"] svg')).not.toBeNull();
});
it('renders ANSI shell output as styled spans instead of escape text', () => {
@@ -1353,7 +1463,9 @@ describe('tool row rendering', () => {
act(() => header.click());
- const cardTitle = container.querySelector('[class*="expandedCardTitle"]');
+ const cardTitle = container.querySelector(
+ '[class*="expandedCardTitleRow"] [class*="expandedCardTitle"]',
+ );
expect(cardTitle?.textContent).toBe('Shell');
});
diff --git a/packages/web-shell/client/components/messages/ToolGroup.tsx b/packages/web-shell/client/components/messages/ToolGroup.tsx
index 8fd2c45736..42155fef71 100644
--- a/packages/web-shell/client/components/messages/ToolGroup.tsx
+++ b/packages/web-shell/client/components/messages/ToolGroup.tsx
@@ -348,16 +348,21 @@ function ExpandedEditContent({ tool }: { tool: ACPToolCall }) {
function ToolExpandedCard({
title,
detail,
+ status,
children,
}: {
title: string;
detail?: string;
+ status?: ACPToolCall['status'];
children?: ReactNode;
}) {
return (
- {title}
+
+ {status && }
+ {title}
+
{detail && {detail}}
{children &&
{children}
}
@@ -397,7 +402,7 @@ function TodoToolBody({
const timeline = useContext(TodoTimelineContext);
const events = timeline.get(tool.callId)?.events ?? [];
return expanded ? (
-
+
@@ -1301,8 +1306,13 @@ export const ToolLine = memo(function ToolLine({
const hideDescriptionInHeader =
showDescriptionInDetail && !isShell && !isSearch && !isRead;
const expandedCardDetail = fullDescription;
+ // A failed tool with no result text still gets the titled card so its
+ // title-row error icon remains visible when expanded.
const showExpandedSummaryPanel =
- !isTodo && expanded && !detailView && (showDescriptionInDetail || result);
+ !isTodo &&
+ expanded &&
+ !detailView &&
+ (showDescriptionInDetail || result || tool.status === 'failed');
return (
@@ -1413,7 +1423,11 @@ export const ToolLine = memo(function ToolLine({
)}
{showExpandedSummaryPanel && (
-
+
{result && (
{isRead ? (
-
+
+
+
) : (
-
+
{isShellToolName(name) && }
{(name === 'write_file' || name === 'writefile') && (
@@ -1485,7 +1505,6 @@ export const ToolGroup = memo(function ToolGroup({
const [chatExpanded, setChatExpanded] = useState(false);
const monitorDetailsRequestRef = useRef