fix(site): soften tool failure display and improve subagent timeout UX (#23617)

## Summary

Tool call failures in `/agents` previously displayed alarming red
styling (red icons, red text, red alert icons) that made it look like
the user did something wrong. This PR replaces the scary error
presentation with a calm, unified style and adds a dedicated timeout
display for subagent tools.

## Changes

### Unified failure style (all tools)
- Replace red `CircleAlertIcon` + `text-content-destructive` with a
muted `TriangleAlertIcon` in `text-content-secondary` across **all 11
tool renderers**.
- Remove red icon/label recoloring on error from `ToolIcon` and all
specialized tool components.
- Error details remain accessible via tooltip on hover.

### Subagent timeout display
- `ClockIcon` with "Timed out waiting for [Title]" instead of a generic
error display.
- `CircleXIcon` for non-timeout subagent errors with proper error verbs
("Failed to spawn", "Failed waiting for", etc.) instead of the
misleading running verb ("Waiting for").
- Timeout detection from result string/error field containing "timed
out".

### Title resolution for historical messages
- `ConversationTimeline` now computes `subagentTitles` via
`useMemo(buildSubagentTitles(...))` and passes it to historical
`ChatMessageItem` rendering, so `wait_agent` can resolve the actual
agent title from a prior `spawn_agent` result even outside streaming
mode.

### Stories
8 new stories: `GenericToolFailed`, `GenericToolFailedNoResult`,
`SubagentWaitTimedOut`, `SubagentWaitTimedOutWithTitle`,
`SubagentWaitTimedOutTitleFromMap`, `SubagentSpawnError`,
`SubagentWaitError`, `MCPToolFailedUnifiedStyle`.

## Files changed (15)
- `tool/Tool.tsx` — GenericToolRenderer + SubagentRenderer
- `tool/SubagentTool.tsx` — timeout/error verbs, icon changes
- `tool/ToolIcon.tsx` — remove destructive recoloring
- `tool/*.tsx` (10 specialized tools) — unified warning icon
- `ConversationTimeline.tsx` — pass subagentTitles to historical
rendering
- `tool.stories.tsx` — 8 new stories, updated existing assertions
This commit is contained in:
Kyle Carberry
2026-03-25 18:33:45 +00:00
committed by GitHub
parent c753a622ad
commit c0f93583e4
15 changed files with 283 additions and 140 deletions
+171 -10
View File
@@ -559,13 +559,12 @@ export const MCPToolError: Story = {
mcpServers: sampleMCPServers,
},
play: async ({ canvasElement }) => {
// Error alert icon should be present.
expect(canvasElement.querySelector(".lucide-circle-alert")).not.toBeNull();
// Label text should use the destructive color.
const label = canvasElement.querySelector(
".\\[\\&\\>\\*\\]\\:text-content-destructive",
);
expect(label).not.toBeNull();
// Warning triangle icon should be present.
expect(
canvasElement.querySelector(".lucide-triangle-alert"),
).not.toBeNull();
// Label text should NOT use the destructive color.
expect(canvasElement.querySelector(".text-content-destructive")).toBeNull();
},
};
@@ -905,9 +904,10 @@ export const ComputerError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Screenshot")).toBeInTheDocument();
// Icon and label should have the destructive color class.
const label = canvas.getByText("Screenshot");
expect(label.className).toContain("text-content-destructive");
// Warning icon should be present, not the old destructive style.
expect(
canvasElement.querySelector(".lucide-triangle-alert"),
).not.toBeNull();
},
};
@@ -933,3 +933,164 @@ export const ComputerArrayResult: Story = {
expect(img.getAttribute("src")).toContain("data:image/jpeg;base64,");
},
};
// ---------------------------------------------------------------------------
// Tool failure display stories
// ---------------------------------------------------------------------------
export const GenericToolFailed: Story = {
args: {
name: "some_custom_tool",
status: "error",
isError: true,
args: { input: "test data" },
result: { error: "Connection refused: could not reach upstream service" },
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Should show "Failed" badge instead of scary red alert.
expect(
canvasElement.querySelector(".lucide-triangle-alert"),
).not.toBeNull();
// Label should NOT have destructive color.
const label = canvas.getByText("some_custom_tool");
expect(label.className).not.toContain("text-content-destructive");
// Error icon should not be present (replaced by warning triangle).
expect(canvasElement.querySelector(".lucide-circle-alert")).toBeNull();
},
};
export const GenericToolFailedNoResult: Story = {
args: {
name: "web_search",
status: "error",
isError: true,
},
play: async ({ canvasElement }) => {
expect(
canvasElement.querySelector(".lucide-triangle-alert"),
).not.toBeNull();
},
};
export const SubagentWaitTimedOut: Story = {
args: {
name: "wait_agent",
status: "error",
isError: true,
args: { chat_id: "timed-out-child" },
result: "timed out waiting for delegated subagent completion",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Should show clock icon for timeout.
expect(canvasElement.querySelector(".lucide-clock")).not.toBeNull();
// Should NOT show red alert icon.
expect(canvasElement.querySelector(".lucide-circle-alert")).toBeNull();
// Should show timeout verb.
expect(canvas.getByText(/Timed out waiting for/)).toBeInTheDocument();
},
};
export const SubagentWaitTimedOutWithTitle: Story = {
args: {
name: "wait_agent",
status: "error",
isError: true,
args: { chat_id: "timed-out-child" },
result: {
chat_id: "timed-out-child",
error: "timed out waiting for delegated subagent completion",
title: "Fix login bug",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvasElement.querySelector(".lucide-clock")).not.toBeNull();
expect(canvas.getByText(/Timed out waiting for/)).toBeInTheDocument();
expect(canvas.getByText("Fix login bug")).toBeInTheDocument();
},
};
export const SubagentWaitTimedOutTitleFromMap: Story = {
args: {
name: "wait_agent",
status: "error",
isError: true,
args: { chat_id: "timed-out-child" },
result: "timed out waiting for delegated subagent completion",
subagentTitles: new Map([["timed-out-child", "Refactor auth module"]]),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText("Refactor auth module")).toBeInTheDocument();
expect(canvas.getByText(/Timed out waiting for/)).toBeInTheDocument();
},
};
export const SubagentSpawnError: Story = {
args: {
name: "spawn_agent",
status: "error",
isError: true,
args: {
title: "Database migration",
prompt: "Run the pending migrations.",
},
result: {
chat_id: "failed-child",
error: "workspace not found",
status: "error",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Should show the muted X icon, not the red alert.
expect(canvasElement.querySelector(".lucide-circle-x")).not.toBeNull();
expect(canvasElement.querySelector(".lucide-circle-alert")).toBeNull();
// Should show error verb.
expect(canvas.getByText(/Failed to spawn/)).toBeInTheDocument();
expect(canvas.getByText("Database migration")).toBeInTheDocument();
},
};
export const SubagentWaitError: Story = {
args: {
name: "wait_agent",
status: "error",
isError: true,
args: { chat_id: "error-child" },
result: {
chat_id: "error-child",
error: "subagent crashed unexpectedly",
status: "error",
title: "Lint codebase",
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvasElement.querySelector(".lucide-circle-x")).not.toBeNull();
expect(canvas.getByText(/Failed waiting for/)).toBeInTheDocument();
expect(canvas.getByText("Lint codebase")).toBeInTheDocument();
},
};
export const MCPToolFailedUnifiedStyle: Story = {
args: {
name: "linear__list_issues",
status: "error",
isError: true,
args: { project: "backend" },
result: { error: "Authentication token expired" },
mcpServerConfigId: "mcp-server-1",
mcpServers: sampleMCPServers,
},
play: async ({ canvasElement }) => {
// Should show warning triangle icon.
expect(
canvasElement.querySelector(".lucide-triangle-alert"),
).not.toBeNull();
// Icon should NOT be red.
expect(canvasElement.querySelector(".text-content-destructive")).toBeNull();
},
};
@@ -1,4 +1,4 @@
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { cn } from "utils/cn";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
@@ -30,18 +30,13 @@ export const ChatSummarizedTool: React.FC<{
hasContent={hasSummary}
header={
<>
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
{isRunning ? "Summarizing…" : "Summarized"}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to summarize chat"}
@@ -1,4 +1,4 @@
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { useState } from "react";
import { cn } from "utils/cn";
@@ -40,18 +40,13 @@ export const ComputerTool: React.FC<{
defaultExpanded={hasImage}
header={
<>
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
{isRunning ? "Taking screenshot…" : "Screenshot"}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to take screenshot"}
@@ -1,4 +1,4 @@
import { CircleAlertIcon, ExternalLinkIcon, LoaderIcon } from "lucide-react";
import { ExternalLinkIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { Link } from "react-router";
import { cn } from "utils/cn";
@@ -46,18 +46,11 @@ export const CreateWorkspaceTool: React.FC<{
return (
<div className="w-full">
<div className="flex items-center gap-2">
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
{label}
</span>
<span className={cn("text-sm", "text-content-secondary")}>{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to create workspace"}
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import type { FileDiffMetadata } from "@pierre/diffs";
import { FileDiff } from "@pierre/diffs/react";
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { cn } from "utils/cn";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
@@ -61,18 +61,13 @@ export const EditFilesTool: React.FC<{
defaultExpanded
header={
<>
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
{label}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to edit files"}
@@ -4,6 +4,7 @@ import {
CircleAlertIcon,
ExternalLinkIcon,
LoaderIcon,
TriangleAlertIcon,
} from "lucide-react";
import type React from "react";
import { useRef, useState } from "react";
@@ -27,7 +28,7 @@ export const ExecuteTool: React.FC<{
output: string;
status: ToolStatus;
isError: boolean;
}> = ({ command, output, status, isError }) => {
}> = ({ command, output, status }) => {
const [expanded, setExpanded] = useState(false);
const outputRef = useRef<HTMLPreElement | null>(null);
const hasOutput = output.length > 0;
@@ -83,7 +84,7 @@ export const ExecuteTool: React.FC<{
}
className={cn(
"m-0 border-0 whitespace-pre-wrap break-all bg-transparent px-2.5 py-2 font-mono text-xs",
isError ? "text-content-destructive" : "text-content-secondary",
"text-content-secondary",
)}
>
{output}
@@ -199,7 +200,7 @@ export const WaitForExternalAuthTool: React.FC<{
errorMessage ||
`Failed while waiting for ${providerLabel} authentication`;
icon = (
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
);
} else if (timedOut) {
label = `Timed out waiting for ${providerLabel} authentication`;
@@ -1,4 +1,4 @@
import { CircleAlertIcon, ExternalLinkIcon, LoaderIcon } from "lucide-react";
import { ExternalLinkIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { Link } from "react-router";
import { cn } from "utils/cn";
@@ -37,18 +37,13 @@ export const ListTemplatesTool: React.FC<{
hasContent={hasContent}
header={
<>
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
{label}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to list templates"}
@@ -1,5 +1,5 @@
import { API } from "api/api";
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { useQuery } from "react-query";
import { cn } from "utils/cn";
@@ -57,22 +57,15 @@ export const ProposePlanTool: React.FC<{
return (
<div className="w-full">
<div className="flex items-center gap-1.5 py-0.5">
<span
className={cn(
"text-sm",
effectiveError
? "text-content-destructive"
: "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
{isRunning ? `Proposing ${filename}` : `Proposed ${filename}`}
</span>
{effectiveError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon
<TriangleAlertIcon
aria-label="Error"
className="h-3.5 w-3.5 shrink-0 text-content-destructive"
className="h-3.5 w-3.5 shrink-0 text-content-secondary"
/>
</TooltipTrigger>
<TooltipContent>
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import { File as FileViewer } from "@pierre/diffs/react";
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { cn } from "utils/cn";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
@@ -38,18 +38,13 @@ export const ReadFileTool: React.FC<{
hasContent={hasContent}
header={
<>
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
Read {path.split("/").pop() || path}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to read file"}
@@ -1,4 +1,4 @@
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { cn } from "utils/cn";
import {
@@ -28,18 +28,11 @@ export const ReadTemplateTool: React.FC<{
return (
<div className="flex items-center gap-1.5">
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
{label}
</span>
<span className={cn("text-sm", "text-content-secondary")}>{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to read template"}
@@ -1,7 +1,8 @@
import {
BotIcon,
ChevronDownIcon,
CircleAlertIcon,
CircleXIcon,
ClockIcon,
ExternalLinkIcon,
LoaderIcon,
} from "lucide-react";
@@ -17,11 +18,34 @@ import {
type ToolStatus,
} from "./utils";
const SUBAGENT_VERBS: Record<string, { completed: string; running: string }> = {
spawn_agent: { completed: "Spawned ", running: "Spawning " },
wait_agent: { completed: "Waited for ", running: "Waiting for " },
message_agent: { completed: "Messaged ", running: "Messaging " },
close_agent: { completed: "Terminated ", running: "Terminating " },
const SUBAGENT_VERBS: Record<
string,
{ completed: string; running: string; error: string; timeout: string }
> = {
spawn_agent: {
completed: "Spawned ",
running: "Spawning ",
error: "Failed to spawn ",
timeout: "Timed out spawning ",
},
wait_agent: {
completed: "Waited for ",
running: "Waiting for ",
error: "Failed waiting for ",
timeout: "Timed out waiting for ",
},
message_agent: {
completed: "Messaged ",
running: "Messaging ",
error: "Failed to message ",
timeout: "Timed out messaging ",
},
close_agent: {
completed: "Terminated ",
running: "Terminating ",
error: "Failed to terminate ",
timeout: "Timed out terminating ",
},
};
/**
@@ -35,17 +59,14 @@ const SubagentStatusIcon: React.FC<{
subagentStatus: string;
toolStatus: ToolStatus;
isError: boolean;
}> = ({ subagentStatus, toolStatus, isError }) => {
isTimeout: boolean;
}> = ({ subagentStatus, toolStatus, isError, isTimeout }) => {
const subagentCompleted = isSubagentSuccessStatus(subagentStatus);
if (isError && !subagentCompleted) {
return (
<CircleAlertIcon className="h-4 w-4 shrink-0 text-content-destructive" />
);
if (isTimeout && !subagentCompleted) {
return <ClockIcon className="h-4 w-4 shrink-0 text-content-secondary" />;
}
if (toolStatus === "error") {
return (
<CircleAlertIcon className="h-4 w-4 shrink-0 text-content-destructive" />
);
if ((isError && !subagentCompleted) || toolStatus === "error") {
return <CircleXIcon className="h-4 w-4 shrink-0 text-content-secondary" />;
}
if (toolStatus === "running") {
return (
@@ -72,6 +93,7 @@ export const SubagentTool: React.FC<{
report?: string;
toolStatus: ToolStatus;
isError: boolean;
isTimeout?: boolean;
}> = ({
toolName,
title,
@@ -83,6 +105,7 @@ export const SubagentTool: React.FC<{
report,
toolStatus,
isError,
isTimeout = false,
}) => {
const [expanded, setExpanded] = useState(false);
const hasPrompt = Boolean(prompt?.trim());
@@ -107,10 +130,17 @@ export const SubagentTool: React.FC<{
subagentStatus={subagentStatus}
toolStatus={toolStatus}
isError={isError}
isTimeout={isTimeout}
/>
<span className="min-w-0 flex-1 truncate text-sm text-content-secondary">
{SUBAGENT_VERBS[toolName]?.[
toolStatus === "completed" ? "completed" : "running"
isTimeout
? "timeout"
: toolStatus === "completed"
? "completed"
: toolStatus === "error"
? "error"
: "running"
] ?? ""}
<span className="text-content-secondary opacity-60">{title}</span>
{chatId && (
+19 -10
View File
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import { FileDiff, File as FileViewer } from "@pierre/diffs/react";
import type * as TypesGen from "api/typesGenerated";
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import { type ComponentPropsWithRef, type FC, memo } from "react";
import { cn } from "utils/cn";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
@@ -303,6 +303,16 @@ const SubagentRenderer: FC<ToolRendererProps> = ({
subagentToolStatus === "error" ||
((status === "error" || isError) && !subagentCompleted);
// Detect timeout from the result. A timed-out wait_agent
// typically returns an error string or an object with an
// error field containing "timed out".
const resultStr = typeof result === "string" ? result : "";
const errorStr = rec ? asString(rec.error) : "";
const isTimeout =
subagentIsError &&
(resultStr.toLowerCase().includes("timed out") ||
errorStr.toLowerCase().includes("timed out"));
return (
<SubagentTool
toolName={name}
@@ -315,6 +325,7 @@ const SubagentRenderer: FC<ToolRendererProps> = ({
report={chatId ? report || undefined : undefined}
toolStatus={subagentToolStatus}
isError={subagentIsError}
isTimeout={isTimeout}
/>
);
};
@@ -504,18 +515,16 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
isRunning={isRunning}
serverName={mcpServer?.display_name}
/>
<span className={cn(isError && "[&>*]:text-content-destructive")}>
<ToolLabel
name={name}
args={args}
result={result}
mcpSlug={mcpServer?.slug}
/>
</span>
<ToolLabel
name={name}
args={args}
result={result}
mcpSlug={mcpServer?.slug}
/>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Tool call failed"}
@@ -24,9 +24,9 @@ export const ToolIcon: React.FC<{
iconUrl?: string;
isRunning?: boolean;
serverName?: string;
}> = ({ name, isError, iconUrl, isRunning, serverName }) => {
}> = ({ name, iconUrl, isRunning, serverName }) => {
const [imgError, setImgError] = useState(false);
const color = isError ? "text-content-destructive" : "text-content-secondary";
const color = "text-content-secondary";
const base = cn("h-4 w-4 shrink-0", color, isRunning && "grayscale");
// If an MCP icon URL is provided and hasn't failed, render it.
@@ -35,13 +35,6 @@ export const ToolIcon: React.FC<{
// mode we invert to white and tune opacity to approximate
// content-secondary (light ≈ 34% lightness, dark ≈ 65%).
if (iconUrl && !imgError) {
// Always render the same DOM shape so React never unmounts
// the <img> when isError changes (avoids a reload flicker).
//
// The wrapper clips a translated copy of the image so the
// drop-shadow trick can work on error (see image classes).
// In the normal state the image is not translated and the
// wrapper's overflow-hidden is a harmless no-op.
const img = (
<div className="h-4 w-4 shrink-0 overflow-hidden">
<ExternalImage
@@ -49,19 +42,11 @@ export const ToolIcon: React.FC<{
alt={`${name} icon`}
className={cn(
"block h-4 w-4",
isError
? // Drop-shadow recolor: brightness-0 makes every
// pixel black, drop-shadow casts an exact-color
// copy 16px to the right following the alpha
// channel, -translate-x-4 shifts the colored copy
// into the original position, and the wrapper's
// overflow-hidden clips the black original.
"-translate-x-4 [filter:brightness(0)_drop-shadow(16px_0_0_hsl(var(--content-destructive)))]"
: // Monochrome: brightness-0 strips colour to black,
// dark:invert flips to white for dark backgrounds,
// opacity tuned per-theme to match content-secondary
// (light ~35% lightness, dark ~65%).
"brightness-0 opacity-[0.35] dark:invert dark:opacity-[0.65]",
// Monochrome: brightness-0 strips colour to black,
// dark:invert flips to white for dark backgrounds,
// opacity tuned per-theme to match content-secondary
// (light ~35% lightness, dark ~65%).
"brightness-0 opacity-[0.35] dark:invert dark:opacity-[0.65]",
)}
onError={() => setImgError(true)}
/>
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import type { FileDiffMetadata } from "@pierre/diffs";
import { FileDiff } from "@pierre/diffs/react";
import { CircleAlertIcon, LoaderIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { cn } from "utils/cn";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
@@ -43,18 +43,13 @@ export const WriteFileTool: React.FC<{
hasContent={hasDiff}
header={
<>
<span
className={cn(
"text-sm",
isError ? "text-content-destructive" : "text-content-secondary",
)}
>
<span className={cn("text-sm", "text-content-secondary")}>
{label}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-destructive" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to write file"}
@@ -38,6 +38,7 @@ import { ImageLightbox } from "../ImageLightbox";
import { TextPreviewDialog } from "../TextPreviewDialog";
import { ChatStatusCallout } from "./ChatStatusCallout";
import type { LiveStatusModel } from "./liveStatusModel";
import { buildSubagentTitles } from "./messageParsing";
import { useSmoothStreamingText } from "./SmoothText";
import type {
MergedTool,
@@ -398,6 +399,7 @@ const ChatMessageItem = memo<{
fadeFromBottom?: boolean;
urlTransform?: UrlTransform;
mcpServers?: readonly TypesGen.MCPServerConfig[];
subagentTitles?: Map<string, string>;
}>(
({
message,
@@ -409,6 +411,7 @@ const ChatMessageItem = memo<{
fadeFromBottom = false,
urlTransform,
mcpServers,
subagentTitles,
}) => {
const isUser = message.role === "user";
const isSavingMessage = savingMessageId === message.id;
@@ -475,6 +478,7 @@ const ChatMessageItem = memo<{
blocks: parsed.blocks,
toolByID,
keyPrefix: String(message.id),
subagentTitles,
onImageClick: setPreviewImage,
onTextFileClick: (content) => setPreviewText(content),
urlTransform,
@@ -620,6 +624,7 @@ const ChatMessageItem = memo<{
result={tool.result}
status={tool.status}
isError={tool.isError}
subagentTitles={subagentTitles}
mcpServerConfigId={tool.mcpServerConfigId}
mcpServers={mcpServers}
/>
@@ -1049,6 +1054,8 @@ export const ConversationTimeline: FC<ConversationTimelineProps> = ({
urlTransform,
mcpServers,
}) => {
const subagentTitles = buildSubagentTitles(parsedMessages);
if (parsedMessages.length === 0) {
return null;
}
@@ -1091,6 +1098,7 @@ export const ConversationTimeline: FC<ConversationTimelineProps> = ({
urlTransform={urlTransform}
isAfterEditingMessage={afterEditingMessageIds.has(message.id)}
mcpServers={mcpServers}
subagentTitles={subagentTitles}
/>
),
)}