Claude stores some tool failures as errored tool_result rows. The UI either attached those rows to hidden tool output or dropped them when no matching tool call was rendered, which made validation failures disappear from chat history. Render unattached errored tool results, unwrap Claude tool_use_error content, and keep tool errors visible even for tools whose successful output is hidden. Also remove the permission-grant recovery controls from rendered error history so denied tool use stays a plain error message.
Tool Rendering System
Overview
Config-driven architecture for rendering tool executions in chat. All tool display behavior is defined in toolConfigs.ts — no scattered conditionals. Two base display patterns: OneLineDisplay for compact tools, CollapsibleDisplay for tools with expandable content.
Non-error tool results route through ToolRenderer with mode="result" (single source of truth). Error results are handled inline in MessageComponent with a red error box.
Architecture
tools/
├── components/
│ ├── OneLineDisplay.tsx # Compact one-line tool display
│ ├── CollapsibleDisplay.tsx # Expandable tool display (uses children pattern)
│ ├── CollapsibleSection.tsx # <details>/<summary> wrapper
│ ├── ContentRenderers/
│ │ ├── ToolDiffViewer.tsx # File diff viewer (memoized)
│ │ ├── MarkdownContent.tsx # Markdown renderer
│ │ ├── FileListContent.tsx # Comma-separated clickable file list
│ │ ├── TodoListContent.tsx # Todo items with status badges
│ │ ├── TaskListContent.tsx # Task tracker with progress bar
│ │ └── TextContent.tsx # Plain text / JSON / code
├── configs/
│ └── toolConfigs.ts # All tool configs + ToolDisplayConfig type
├── ToolRenderer.tsx # Main router (React.memo wrapped)
└── README.md
Display Patterns
OneLineDisplay
Used by: Bash, Read, Grep, Glob, TodoRead, TaskCreate, TaskUpdate, TaskGet
Renders as a single line with border-l-2 accent. Supports multiple rendering modes based on action:
- terminal (
style: 'terminal') — Dark pill around command text, green$prompt - open-file — Shows filename only (truncated from full path), clickable to open
- jump-to-results — Shows pattern with anchor link to result section
- copy — Shows value with hover copy button
- none — Plain display
<OneLineDisplay
toolName="Read"
icon="terminal" // Optional icon or style keyword
label="Read" // Tool label
value="/path/to/file.ts" // Main display value
secondary="description" // Optional secondary text (italic)
action="open-file" // Action type
onAction={() => ...} // Click handler
colorScheme={{ // Per-tool colors
primary: 'text-...',
border: 'border-...',
icon: 'text-...'
}}
resultId="tool-result-x" // For jump-to-results anchor
toolResult={...} // For conditional jump arrow
toolId="x" // Tool use ID
/>
CollapsibleDisplay
Used by: Edit, Write, ApplyPatch, Grep/Glob results, TodoWrite, TaskList/TaskGet results, ExitPlanMode, Default
Wraps CollapsibleSection (<details>/<summary>) with a border-l-2 accent colored by tool category. Accepts children directly (not contentProps).
<CollapsibleDisplay
toolName="Edit"
toolId="123"
title="filename.ts" // Section title (can be clickable)
defaultOpen={false}
onTitleClick={() => ...} // Makes title a clickable link (for edit tools)
showRawParameters={true} // Show raw JSON toggle
rawContent="..." // Raw JSON string
toolCategory="edit" // Drives border color
>
<ToolDiffViewer {...} /> // Content as children
</CollapsibleDisplay>
Tool category colors (via border-l-2):
| Category | Tools | Color |
|---|---|---|
edit |
Edit, Write, ApplyPatch | amber |
bash |
Bash | green |
search |
Grep, Glob | gray |
todo |
TodoWrite, TodoRead | violet |
task |
TaskCreate/Update/List/Get | violet |
plan |
ExitPlanMode | indigo |
default |
everything else | neutral gray |
Content Renderers
Specialized components for different content types, rendered as children of CollapsibleDisplay:
| contentType | Component | Used by |
|---|---|---|
diff |
DiffViewer |
Edit, Write, ApplyPatch |
markdown |
MarkdownContent |
ExitPlanMode |
file-list |
FileListContent |
Grep/Glob results |
todo-list |
TodoListContent |
TodoWrite, TodoRead |
task |
TaskListContent |
TaskList, TaskGet results |
text |
TextContent |
Default fallback |
success-message |
inline SVG | TodoWrite result |
Adding a New Tool
Step 1: Add config to configs/toolConfigs.ts
MyTool: {
input: {
type: 'one-line', // or 'collapsible'
label: 'MyTool',
getValue: (input) => input.some_field,
action: 'open-file',
colorScheme: {
primary: 'text-purple-600 dark:text-purple-400',
border: 'border-purple-400 dark:border-purple-500'
}
},
result: {
hideOnSuccess: true // Only show errors
}
}
Step 2: If the tool needs a category color, add it to getToolCategory() in ToolRenderer.tsx.
That's it. The ToolRenderer auto-routes based on config.
Configuration Reference
ToolDisplayConfig
interface ToolDisplayConfig {
input: {
type: 'one-line' | 'collapsible' | 'hidden';
// One-line
icon?: string;
label?: string;
getValue?: (input) => string;
getSecondary?: (input) => string | undefined;
action?: 'copy' | 'open-file' | 'jump-to-results' | 'none';
style?: string; // 'terminal' for Bash
wrapText?: boolean;
colorScheme?: {
primary?: string;
secondary?: string;
background?: string;
border?: string;
icon?: string;
};
// Collapsible
title?: string | ((input) => string);
defaultOpen?: boolean;
contentType?: 'diff' | 'markdown' | 'file-list' | 'todo-list' | 'text' | 'task';
getContentProps?: (input, helpers?) => any;
actionButton?: 'none';
};
result?: {
hidden?: boolean; // Never show
hideOnSuccess?: boolean; // Only show errors
type?: 'one-line' | 'collapsible' | 'special';
title?: string | ((result) => string);
defaultOpen?: boolean;
contentType?: 'markdown' | 'file-list' | 'todo-list' | 'text' | 'success-message' | 'task';
getMessage?: (result) => string;
getContentProps?: (result) => any;
};
}
All Configured Tools
| Tool | Input | Result | Notes |
|---|---|---|---|
| Bash | terminal one-line | hide success | Dark command pill, green accent |
| Read | one-line (open-file) | hidden | Shows filename, clicks to open |
| Edit | collapsible (diff) | hide success | Amber border, clickable filename |
| Write | collapsible (diff) | hide success | "New" badge on diff |
| ApplyPatch | collapsible (diff) | hide success | "Patch" badge on diff |
| Grep | one-line (jump) | collapsible file-list | Collapsed by default |
| Glob | one-line (jump) | collapsible file-list | Collapsed by default |
| TodoWrite | collapsible (todo-list) | success message | |
| TodoRead | one-line | collapsible todo-list | |
| TaskCreate | one-line | hide success | Shows task subject |
| TaskUpdate | one-line | hide success | Shows #id → status |
| TaskList | one-line | collapsible task | Progress bar, status icons |
| TaskGet | one-line | collapsible task | Task details with status |
| ExitPlanMode | collapsible (markdown) | collapsible markdown | Also registered as exit_plan_mode |
| Default | collapsible (code) | collapsible text | Fallback for unknown tools |
Performance
- ToolRenderer is wrapped with
React.memo— skips re-render when props haven't changed - parsedData is memoized with
useMemo— JSON parsing only runs when input changes - ToolDiffViewer memoizes
createDiff()— expensive diff computation cached - MessageComponent caches
localStoragereads and timestamp formatting viauseMemo - Tool results route through
ToolRenderer(no duplicate rendering paths) CollapsibleDisplayuses children pattern (no wasteful contentProps indirection)- Configs are static module-level objects — zero runtime overhead for lookups