diff --git a/src/components/chat/tools/ToolRenderer.tsx b/src/components/chat/tools/ToolRenderer.tsx
index 7023a1ac..5713fcc7 100644
--- a/src/components/chat/tools/ToolRenderer.tsx
+++ b/src/components/chat/tools/ToolRenderer.tsx
@@ -6,6 +6,8 @@ import type { SubagentChildTool } from '../types/types';
import { getToolConfig } from './configs/toolConfigs';
import { OneLineDisplay, CollapsibleDisplay, ToolDiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent, QuestionAnswerContent, SubagentContainer } from './components';
import { PlanDisplay } from './components/PlanDisplay';
+import { ToolStatusBadge } from './components/ToolStatusBadge';
+import type { ToolStatus } from './components/ToolStatusBadge';
type DiffLine = {
type: string;
@@ -39,12 +41,24 @@ function getToolCategory(toolName: string): string {
if (toolName === 'Bash') return 'bash';
if (['TodoWrite', 'TodoRead'].includes(toolName)) return 'todo';
if (['TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet'].includes(toolName)) return 'task';
- if (toolName === 'Task') return 'agent'; // Subagent task
+ if (toolName === 'Task') return 'agent';
if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') return 'plan';
if (toolName === 'AskUserQuestion') return 'question';
return 'default';
}
+function deriveToolStatus(toolResult: any): ToolStatus {
+ if (!toolResult) return 'running';
+ if (toolResult.isError) {
+ const content = String(toolResult.content || '').toLowerCase();
+ if (content.includes('permission') || content.includes('denied') || content.includes('not allowed')) {
+ return 'denied';
+ }
+ return 'error';
+ }
+ return 'completed';
+}
+
/**
* Main tool renderer router
* Routes to OneLineDisplay or CollapsibleDisplay based on tool config
@@ -76,6 +90,12 @@ export const ToolRenderer: React.FC
= memo(({
}
}, [mode, toolInput, toolResult]);
+ // Only derive and show status badge on input renders
+ const toolStatus = useMemo(
+ () => mode === 'input' ? deriveToolStatus(toolResult) : undefined,
+ [mode, toolResult],
+ );
+
const handleAction = useCallback(() => {
if (displayConfig?.action === 'open-file' && onFileOpen) {
const value = displayConfig.getValue?.(parsedData) || '';
@@ -85,9 +105,7 @@ export const ToolRenderer: React.FC = memo(({
// Route subagent containers to dedicated component (after hooks to satisfy Rules of Hooks)
if (isSubagentContainer && subagentState) {
- if (mode === 'result') {
- return null;
- }
+ if (mode === 'result') return null;
return (
= memo(({
wrapText={displayConfig.wrapText}
colorScheme={displayConfig.colorScheme}
resultId={mode === 'input' ? `tool-result-${toolId}` : undefined}
+ status={toolStatus}
/>
);
}
@@ -164,7 +183,6 @@ export const ToolRenderer: React.FC = memo(({
onFileOpen
}) || {};
- // Build the content component based on contentType
let contentComponent: React.ReactNode = null;
switch (displayConfig.contentType) {
@@ -241,7 +259,6 @@ export const ToolRenderer: React.FC = memo(({
}
}
- // For edit tools, make the title (filename) clickable to open the file
const handleTitleClick = (toolName === 'Edit' || toolName === 'Write' || toolName === 'ApplyPatch') && contentProps.filePath && onFileOpen
? () => onFileOpen(contentProps.filePath, {
old_string: contentProps.oldContent,
@@ -249,6 +266,8 @@ export const ToolRenderer: React.FC = memo(({
})
: undefined;
+ const badgeElement = toolStatus ? : undefined;
+
return (
= memo(({
title={title}
defaultOpen={defaultOpen}
onTitleClick={handleTitleClick}
+ badge={badgeElement}
showRawParameters={mode === 'input' && showRawParameters}
rawContent={rawToolInput}
toolCategory={getToolCategory(toolName)}
diff --git a/src/components/chat/tools/components/CollapsibleDisplay.tsx b/src/components/chat/tools/components/CollapsibleDisplay.tsx
index 1175893e..51faf12b 100644
--- a/src/components/chat/tools/components/CollapsibleDisplay.tsx
+++ b/src/components/chat/tools/components/CollapsibleDisplay.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../../../../shared/view/ui';
import { CollapsibleSection } from './CollapsibleSection';
interface CollapsibleDisplayProps {
@@ -7,6 +8,7 @@ interface CollapsibleDisplayProps {
title: string;
defaultOpen?: boolean;
action?: React.ReactNode;
+ badge?: React.ReactNode;
onTitleClick?: () => void;
children: React.ReactNode;
showRawParameters?: boolean;
@@ -17,14 +19,14 @@ interface CollapsibleDisplayProps {
const borderColorMap: Record = {
edit: 'border-l-amber-500 dark:border-l-amber-400',
- search: 'border-l-gray-400 dark:border-l-gray-500',
+ search: 'border-l-muted-foreground/40',
bash: 'border-l-green-500 dark:border-l-green-400',
todo: 'border-l-violet-500 dark:border-l-violet-400',
task: 'border-l-violet-500 dark:border-l-violet-400',
agent: 'border-l-purple-500 dark:border-l-purple-400',
plan: 'border-l-indigo-500 dark:border-l-indigo-400',
question: 'border-l-blue-500 dark:border-l-blue-400',
- default: 'border-l-gray-300 dark:border-l-gray-600',
+ default: 'border-l-border',
};
export const CollapsibleDisplay: React.FC = ({
@@ -32,14 +34,14 @@ export const CollapsibleDisplay: React.FC = ({
title,
defaultOpen = false,
action,
+ badge,
onTitleClick,
children,
showRawParameters = false,
rawContent,
className = '',
- toolCategory
+ toolCategory,
}) => {
- // Fall back to default styling for unknown/new categories so className never includes "undefined".
const borderColor = borderColorMap[toolCategory || 'default'] || borderColorMap.default;
return (
@@ -49,15 +51,16 @@ export const CollapsibleDisplay: React.FC = ({
toolName={toolName}
open={defaultOpen}
action={action}
+ badge={badge}
onTitleClick={onTitleClick}
>
{children}
{showRawParameters && rawContent && (
-
-
+
+
raw params
-
-
- {rawContent}
-
-
+
+
+
+ {rawContent}
+
+
+
)}
diff --git a/src/components/chat/tools/components/CollapsibleSection.tsx b/src/components/chat/tools/components/CollapsibleSection.tsx
index c19e8e8e..8e1f3185 100644
--- a/src/components/chat/tools/components/CollapsibleSection.tsx
+++ b/src/components/chat/tools/components/CollapsibleSection.tsx
@@ -1,10 +1,13 @@
import React from 'react';
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../../../../shared/view/ui';
+import { cn } from '../../../../lib/utils';
interface CollapsibleSectionProps {
title: string;
toolName?: string;
open?: boolean;
action?: React.ReactNode;
+ badge?: React.ReactNode;
onTitleClick?: () => void;
children: React.ReactNode;
className?: string;
@@ -18,44 +21,68 @@ export const CollapsibleSection: React.FC