diff --git a/client/src/components/Chat/Messages/Content/Parts/DiffView.tsx b/client/src/components/Chat/Messages/Content/Parts/DiffView.tsx
index 9e25b9328d..4065549b01 100644
--- a/client/src/components/Chat/Messages/Content/Parts/DiffView.tsx
+++ b/client/src/components/Chat/Messages/Content/Parts/DiffView.tsx
@@ -124,7 +124,7 @@ export default function DiffView({ parsed }: { parsed: ParsedDiff }) {
return (
{lines.map((line, index) => {
if (line.type === 'hunk') {
diff --git a/client/src/components/Chat/Messages/Content/ToolCallGroup.tsx b/client/src/components/Chat/Messages/Content/ToolCallGroup.tsx
index e85adf9151..3b6b1c8a8a 100644
--- a/client/src/components/Chat/Messages/Content/ToolCallGroup.tsx
+++ b/client/src/components/Chat/Messages/Content/ToolCallGroup.tsx
@@ -20,6 +20,8 @@ import {
getActivityLabelText,
} from '~/utils';
import { useLocalize, useExpandCollapse, scheduleMessageContentLayoutReconcile } from '~/hooks';
+import { parseBackgroundHandle, splitBackgroundAttachments } from './Parts/handle';
+import { mapAttachments, filterAttachmentsForPart } from '~/utils/map';
import { ToolAuthWarning, ToolAuthWarningContext } from './auth';
import { useMCPIconMap, useMCPServerNames } from '~/hooks/MCP';
import { resolveToolCallPhase } from '~/utils/toolCallPhase';
@@ -94,7 +96,10 @@ function hasPendingAuth(part: TMessageContentParts): boolean {
);
}
-function getToolMeta(part: TMessageContentParts): ToolMeta | null {
+function getToolMeta(
+ part: TMessageContentParts,
+ attachmentsByToolCallId?: Record,
+): ToolMeta | null {
if (part.type !== ContentTypes.TOOL_CALL) {
return null;
}
@@ -109,7 +114,10 @@ function getToolMeta(part: TMessageContentParts): ToolMeta | null {
const isStandard =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isStandard) {
- const tc = toolCall as Agents.ToolCall & { progress?: number };
+ /** `agentId` disambiguates attachments when a handoff response repeats a
+ * provider tool-call id across agents; `filterAttachmentsForPart` reads it
+ * the same way. */
+ const tc = toolCall as Agents.ToolCall & { progress?: number; agentId?: string };
/** Subagents can finish with `progress === 1` and no final output
* text (the parent saw "" / undefined back). Fall back to progress
* so the group header flips from "Running N agents" to "Ran N
@@ -125,10 +133,23 @@ function getToolMeta(part: TMessageContentParts): ToolMeta | null {
name === 'set_memory' || name === 'delete_memory'
? isMemoryFailureOutput(name, tc.output ?? '')
: hasFailedOutput(tc.output);
+ /** A backgrounded bash/code task reports its verdict through a
+ * `background_task_status` attachment, not its output: the dispatch step
+ * keeps a benign handle and usually closes as `completed`. The child card
+ * folds that marker in as `extraError`, so without it here the group
+ * reported no failed action beside a card showing failure. Correlated the
+ * same way the child is, since provider tool-call ids repeat across agents
+ * in handoff responses. */
+ const backgroundFailed =
+ parseBackgroundHandle(tc.output) != null &&
+ splitBackgroundAttachments(
+ filterAttachmentsForPart(attachmentsByToolCallId?.[tc.id ?? ''], tc.agentId),
+ tc.id,
+ ).backgroundStatus === 'error';
return {
name,
iconName,
- ...resolveOutcome(runStepStatus, completed, failedOutput),
+ ...resolveOutcome(runStepStatus, completed, failedOutput || backgroundFailed),
};
}
@@ -205,12 +226,21 @@ export default function ToolCallGroup({
const cancelLayoutReconcileRef = useRef<(() => void) | null>(null);
const retainedForPendingApprovalRef = useRef(false);
+ /** Re-keyed by tool-call id so each part's metadata sees only its own
+ * attachments: `groupAttachments` arrives flattened across the group. */
+ const attachmentsByToolCallId = useMemo(
+ () => mapAttachments(groupAttachments ?? []),
+ [groupAttachments],
+ );
/** `parts` may include interleaved reasoning ("Thoughts") parts that render
* inside the body but are not actions. Count and summarize only the real
* tool calls so the header and stacked icons stay accurate. */
const toolMetadata = useMemo(
- () => parts.map((p) => getToolMeta(p.part)).filter((m): m is ToolMeta => m != null),
- [parts],
+ () =>
+ parts
+ .map((p) => getToolMeta(p.part, attachmentsByToolCallId))
+ .filter((m): m is ToolMeta => m != null),
+ [parts, attachmentsByToolCallId],
);
const count = toolMetadata.length;
/** Approval state is read from the RAW parts, not `toolMetadata`: a pending
diff --git a/client/src/components/Chat/Messages/Content/verticals.tsx b/client/src/components/Chat/Messages/Content/verticals.tsx
index 281a634849..0ddd317aee 100644
--- a/client/src/components/Chat/Messages/Content/verticals.tsx
+++ b/client/src/components/Chat/Messages/Content/verticals.tsx
@@ -65,7 +65,10 @@ export function collectSearchVerticals(attachments?: TAttachment[]): SearchVerti
}
}
for (const item of data.shopping ?? []) {
- if (item.link) {
+ /** A card with no title has no accessible name and no visible identity:
+ * the anchor carries no label, the image is decorative, and the title
+ * span renders empty. */
+ if (item.link && item.title) {
shopping.push(item);
}
}
diff --git a/packages/client/src/theme/semanticTokens.spec.ts b/packages/client/src/theme/semanticTokens.spec.ts
index 0b9cc25a19..881bc91ba2 100644
--- a/packages/client/src/theme/semanticTokens.spec.ts
+++ b/packages/client/src/theme/semanticTokens.spec.ts
@@ -165,6 +165,7 @@ const canvasSurfaces: Array = [
'rgb-surface-secondary',
'rgb-surface-dialog',
'rgb-surface-chat',
+ 'rgb-surface-code',
'rgb-presentation',
];
diff --git a/packages/client/src/theme/themes/dark.ts b/packages/client/src/theme/themes/dark.ts
index 69cabf4855..93ee5f50bf 100644
--- a/packages/client/src/theme/themes/dark.ts
+++ b/packages/client/src/theme/themes/dark.ts
@@ -48,6 +48,7 @@ export const darkTheme: IThemeRGB = {
'rgb-surface-destructive': '153 27 27', // #991b1b (red-800)
'rgb-surface-destructive-hover': '127 29 29', // #7f1d1d (red-900)
'rgb-surface-chat': '47 47 47', // #2f2f2f (gray-700)
+ 'rgb-surface-code': '33 33 33', // #212121 (gray-800)
'rgb-surface-inverted': '255 255 255', // #fff (white)
'rgb-surface-inverted-hover': '236 236 236', // #ececec (gray-100)
'rgb-text-inverted': '23 23 23', // #171717 (gray-850)
diff --git a/packages/client/src/theme/themes/default.ts b/packages/client/src/theme/themes/default.ts
index 503d02ddc4..1bd371f340 100644
--- a/packages/client/src/theme/themes/default.ts
+++ b/packages/client/src/theme/themes/default.ts
@@ -48,6 +48,7 @@ export const defaultTheme: IThemeRGB = {
'rgb-surface-destructive': '185 28 28', // #b91c1c (red-700)
'rgb-surface-destructive-hover': '153 27 27', // #991b1b (red-800)
'rgb-surface-chat': '255 255 255', // #fff (white)
+ 'rgb-surface-code': '247 247 248', // #f7f7f8 (gray-50)
'rgb-surface-inverted': '23 23 23', // #171717 (gray-850)
'rgb-surface-inverted-hover': '47 47 47', // #2f2f2f (gray-700)
'rgb-text-inverted': '255 255 255', // #fff (white)
diff --git a/packages/client/src/theme/types/index.ts b/packages/client/src/theme/types/index.ts
index b692da887b..52dba7f9a8 100644
--- a/packages/client/src/theme/types/index.ts
+++ b/packages/client/src/theme/types/index.ts
@@ -46,6 +46,7 @@ export interface IThemeRGB {
'rgb-surface-destructive'?: string;
'rgb-surface-destructive-hover'?: string;
'rgb-surface-chat'?: string;
+ 'rgb-surface-code'?: string;
'rgb-surface-inverted'?: string;
'rgb-surface-inverted-hover'?: string;
'rgb-text-inverted'?: string;
@@ -141,6 +142,7 @@ export interface IThemeVariables {
'--surface-destructive': string;
'--surface-destructive-hover': string;
'--surface-chat': string;
+ '--surface-code': string;
'--surface-inverted': string;
'--surface-inverted-hover': string;
'--text-inverted': string;
@@ -228,6 +230,7 @@ export interface IThemeColors {
'surface-destructive'?: string;
'surface-destructive-hover'?: string;
'surface-chat'?: string;
+ 'surface-code'?: string;
'surface-inverted'?: string;
'surface-inverted-hover'?: string;
'text-inverted'?: string;