From c0f93583e462e18a1bd07c2d7a241f24f89ef80f Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 25 Mar 2026 14:33:45 -0400 Subject: [PATCH] fix(site): soften tool failure display and improve subagent timeout UX (#23617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .../components/ai-elements/tool.stories.tsx | 181 +++++++++++++++++- .../ai-elements/tool/ChatSummarizedTool.tsx | 11 +- .../ai-elements/tool/ComputerTool.tsx | 11 +- .../ai-elements/tool/CreateWorkspaceTool.tsx | 13 +- .../ai-elements/tool/EditFilesTool.tsx | 11 +- .../ai-elements/tool/ExecuteTool.tsx | 7 +- .../ai-elements/tool/ListTemplatesTool.tsx | 11 +- .../ai-elements/tool/ProposePlanTool.tsx | 15 +- .../ai-elements/tool/ReadFileTool.tsx | 11 +- .../ai-elements/tool/ReadTemplateTool.tsx | 13 +- .../ai-elements/tool/SubagentTool.tsx | 62 ++++-- site/src/components/ai-elements/tool/Tool.tsx | 29 ++- .../components/ai-elements/tool/ToolIcon.tsx | 29 +-- .../ai-elements/tool/WriteFileTool.tsx | 11 +- .../AgentDetail/ConversationTimeline.tsx | 8 + 15 files changed, 283 insertions(+), 140 deletions(-) diff --git a/site/src/components/ai-elements/tool.stories.tsx b/site/src/components/ai-elements/tool.stories.tsx index fba611d89f..7a40bd1f6a 100644 --- a/site/src/components/ai-elements/tool.stories.tsx +++ b/site/src/components/ai-elements/tool.stories.tsx @@ -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(); + }, +}; diff --git a/site/src/components/ai-elements/tool/ChatSummarizedTool.tsx b/site/src/components/ai-elements/tool/ChatSummarizedTool.tsx index 71a493dec4..a80ad90178 100644 --- a/site/src/components/ai-elements/tool/ChatSummarizedTool.tsx +++ b/site/src/components/ai-elements/tool/ChatSummarizedTool.tsx @@ -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={ <> - + {isRunning ? "Summarizing…" : "Summarized"} {isError && ( - + {errorMessage || "Failed to summarize chat"} diff --git a/site/src/components/ai-elements/tool/ComputerTool.tsx b/site/src/components/ai-elements/tool/ComputerTool.tsx index 112539fb95..e0f685886d 100644 --- a/site/src/components/ai-elements/tool/ComputerTool.tsx +++ b/site/src/components/ai-elements/tool/ComputerTool.tsx @@ -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={ <> - + {isRunning ? "Taking screenshot…" : "Screenshot"} {isError && ( - + {errorMessage || "Failed to take screenshot"} diff --git a/site/src/components/ai-elements/tool/CreateWorkspaceTool.tsx b/site/src/components/ai-elements/tool/CreateWorkspaceTool.tsx index 865c553d15..fdcca82ce5 100644 --- a/site/src/components/ai-elements/tool/CreateWorkspaceTool.tsx +++ b/site/src/components/ai-elements/tool/CreateWorkspaceTool.tsx @@ -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 (
- - {label} - + {label} {isError && ( - + {errorMessage || "Failed to create workspace"} diff --git a/site/src/components/ai-elements/tool/EditFilesTool.tsx b/site/src/components/ai-elements/tool/EditFilesTool.tsx index c30ae5619a..5ef291977a 100644 --- a/site/src/components/ai-elements/tool/EditFilesTool.tsx +++ b/site/src/components/ai-elements/tool/EditFilesTool.tsx @@ -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={ <> - + {label} {isError && ( - + {errorMessage || "Failed to edit files"} diff --git a/site/src/components/ai-elements/tool/ExecuteTool.tsx b/site/src/components/ai-elements/tool/ExecuteTool.tsx index 48fb011fc6..2671d2307f 100644 --- a/site/src/components/ai-elements/tool/ExecuteTool.tsx +++ b/site/src/components/ai-elements/tool/ExecuteTool.tsx @@ -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(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 = ( - + ); } else if (timedOut) { label = `Timed out waiting for ${providerLabel} authentication`; diff --git a/site/src/components/ai-elements/tool/ListTemplatesTool.tsx b/site/src/components/ai-elements/tool/ListTemplatesTool.tsx index 7e5231e344..9dcb2e9f56 100644 --- a/site/src/components/ai-elements/tool/ListTemplatesTool.tsx +++ b/site/src/components/ai-elements/tool/ListTemplatesTool.tsx @@ -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={ <> - + {label} {isError && ( - + {errorMessage || "Failed to list templates"} diff --git a/site/src/components/ai-elements/tool/ProposePlanTool.tsx b/site/src/components/ai-elements/tool/ProposePlanTool.tsx index a3ecd50562..9fdcf489c8 100644 --- a/site/src/components/ai-elements/tool/ProposePlanTool.tsx +++ b/site/src/components/ai-elements/tool/ProposePlanTool.tsx @@ -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 (
- + {isRunning ? `Proposing ${filename}…` : `Proposed ${filename}`} {effectiveError && ( - diff --git a/site/src/components/ai-elements/tool/ReadFileTool.tsx b/site/src/components/ai-elements/tool/ReadFileTool.tsx index 1bef9d7cb3..805e17d887 100644 --- a/site/src/components/ai-elements/tool/ReadFileTool.tsx +++ b/site/src/components/ai-elements/tool/ReadFileTool.tsx @@ -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={ <> - + Read {path.split("/").pop() || path} {isError && ( - + {errorMessage || "Failed to read file"} diff --git a/site/src/components/ai-elements/tool/ReadTemplateTool.tsx b/site/src/components/ai-elements/tool/ReadTemplateTool.tsx index b84af238b0..2e231aaf14 100644 --- a/site/src/components/ai-elements/tool/ReadTemplateTool.tsx +++ b/site/src/components/ai-elements/tool/ReadTemplateTool.tsx @@ -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 (
- - {label} - + {label} {isError && ( - + {errorMessage || "Failed to read template"} diff --git a/site/src/components/ai-elements/tool/SubagentTool.tsx b/site/src/components/ai-elements/tool/SubagentTool.tsx index f811f891d2..d15f55f2db 100644 --- a/site/src/components/ai-elements/tool/SubagentTool.tsx +++ b/site/src/components/ai-elements/tool/SubagentTool.tsx @@ -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 = { - 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 ( - - ); + if (isTimeout && !subagentCompleted) { + return ; } - if (toolStatus === "error") { - return ( - - ); + if ((isError && !subagentCompleted) || toolStatus === "error") { + return ; } 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} /> {SUBAGENT_VERBS[toolName]?.[ - toolStatus === "completed" ? "completed" : "running" + isTimeout + ? "timeout" + : toolStatus === "completed" + ? "completed" + : toolStatus === "error" + ? "error" + : "running" ] ?? ""} {title} {chatId && ( diff --git a/site/src/components/ai-elements/tool/Tool.tsx b/site/src/components/ai-elements/tool/Tool.tsx index d6f286ffd1..85db384ee4 100644 --- a/site/src/components/ai-elements/tool/Tool.tsx +++ b/site/src/components/ai-elements/tool/Tool.tsx @@ -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 = ({ 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 ( = ({ report={chatId ? report || undefined : undefined} toolStatus={subagentToolStatus} isError={subagentIsError} + isTimeout={isTimeout} /> ); }; @@ -504,18 +515,16 @@ const GenericToolRenderer: FC = ({ isRunning={isRunning} serverName={mcpServer?.display_name} /> - *]:text-content-destructive")}> - - + {isError && ( - + {errorMessage || "Tool call failed"} diff --git a/site/src/components/ai-elements/tool/ToolIcon.tsx b/site/src/components/ai-elements/tool/ToolIcon.tsx index 80280f0f2b..ed798236bb 100644 --- a/site/src/components/ai-elements/tool/ToolIcon.tsx +++ b/site/src/components/ai-elements/tool/ToolIcon.tsx @@ -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 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 = (
setImgError(true)} /> diff --git a/site/src/components/ai-elements/tool/WriteFileTool.tsx b/site/src/components/ai-elements/tool/WriteFileTool.tsx index f9cda50e44..4ea34d4b68 100644 --- a/site/src/components/ai-elements/tool/WriteFileTool.tsx +++ b/site/src/components/ai-elements/tool/WriteFileTool.tsx @@ -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={ <> - + {label} {isError && ( - + {errorMessage || "Failed to write file"} diff --git a/site/src/pages/AgentsPage/components/AgentDetail/ConversationTimeline.tsx b/site/src/pages/AgentsPage/components/AgentDetail/ConversationTimeline.tsx index df97c80a06..73b4f33309 100644 --- a/site/src/pages/AgentsPage/components/AgentDetail/ConversationTimeline.tsx +++ b/site/src/pages/AgentsPage/components/AgentDetail/ConversationTimeline.tsx @@ -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; }>( ({ 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 = ({ urlTransform, mcpServers, }) => { + const subagentTitles = buildSubagentTitles(parsedMessages); + if (parsedMessages.length === 0) { return null; } @@ -1091,6 +1098,7 @@ export const ConversationTimeline: FC = ({ urlTransform={urlTransform} isAfterEditingMessage={afterEditingMessageIds.has(message.id)} mcpServers={mcpServers} + subagentTitles={subagentTitles} /> ), )}