Files
claudecodeui/src/components/chat/tools/ToolRenderer.tsx
simosmik b8d80fdab7 refactor(tools): add agent category for Task tool
Add visual distinction for the Task tool (subagent invocation) by
introducing a new 'agent' category with purple border styling. This
separates subagent tasks from regular task management tools
(TaskCreate, TaskUpdate, etc.) for clearer user feedback.

Also refactor terminal command layout in OneLineDisplay to properly
nest flex containers, fixing copy button alignment issues.
2026-02-12 23:47:06 +03:00

206 lines
6.1 KiB
TypeScript

import React, { memo, useMemo, useCallback } from 'react';
import { getToolConfig } from './configs/toolConfigs';
import { OneLineDisplay, CollapsibleDisplay, DiffViewer, MarkdownContent, FileListContent, TodoListContent, TaskListContent, TextContent } from './components';
import type { Project } from '../../../types/app';
type DiffLine = {
type: string;
content: string;
lineNum: number;
};
interface ToolRendererProps {
toolName: string;
toolInput: any;
toolResult?: any;
toolId?: string;
mode: 'input' | 'result';
onFileOpen?: (filePath: string, diffInfo?: any) => void;
createDiff?: (oldStr: string, newStr: string) => DiffLine[];
selectedProject?: Project | null;
autoExpandTools?: boolean;
showRawParameters?: boolean;
rawToolInput?: string;
}
function getToolCategory(toolName: string): string {
if (['Edit', 'Write', 'ApplyPatch'].includes(toolName)) return 'edit';
if (['Grep', 'Glob'].includes(toolName)) return 'search';
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 === 'exit_plan_mode' || toolName === 'ExitPlanMode') return 'plan';
return 'default';
}
/**
* Main tool renderer router
* Routes to OneLineDisplay or CollapsibleDisplay based on tool config
*/
export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
toolName,
toolInput,
toolResult,
toolId,
mode,
onFileOpen,
createDiff,
selectedProject,
autoExpandTools = false,
showRawParameters = false,
rawToolInput
}) => {
const config = getToolConfig(toolName);
const displayConfig: any = mode === 'input' ? config.input : config.result;
if (!displayConfig) return null;
const parsedData = useMemo(() => {
try {
const rawData = mode === 'input' ? toolInput : toolResult;
return typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
} catch {
return mode === 'input' ? toolInput : toolResult;
}
}, [mode, toolInput, toolResult]);
const handleAction = useCallback(() => {
if (displayConfig.action === 'open-file' && onFileOpen) {
const value = displayConfig.getValue?.(parsedData) || '';
onFileOpen(value);
}
}, [displayConfig, parsedData, onFileOpen]);
if (displayConfig.type === 'one-line') {
const value = displayConfig.getValue?.(parsedData) || '';
const secondary = displayConfig.getSecondary?.(parsedData);
return (
<OneLineDisplay
toolName={toolName}
toolResult={toolResult}
toolId={toolId}
icon={displayConfig.icon}
label={displayConfig.label}
value={value}
secondary={secondary}
action={displayConfig.action}
onAction={handleAction}
style={displayConfig.style}
wrapText={displayConfig.wrapText}
colorScheme={displayConfig.colorScheme}
resultId={mode === 'input' ? `tool-result-${toolId}` : undefined}
/>
);
}
if (displayConfig.type === 'collapsible') {
const title = typeof displayConfig.title === 'function'
? displayConfig.title(parsedData)
: displayConfig.title || 'Details';
const defaultOpen = displayConfig.defaultOpen !== undefined
? displayConfig.defaultOpen
: autoExpandTools;
const contentProps = displayConfig.getContentProps?.(parsedData, {
selectedProject,
createDiff,
onFileOpen
}) || {};
// Build the content component based on contentType
let contentComponent: React.ReactNode = null;
switch (displayConfig.contentType) {
case 'diff':
if (createDiff) {
contentComponent = (
<DiffViewer
{...contentProps}
createDiff={createDiff}
onFileClick={() => onFileOpen?.(contentProps.filePath)}
/>
);
}
break;
case 'markdown':
contentComponent = <MarkdownContent content={contentProps.content || ''} />;
break;
case 'file-list':
contentComponent = (
<FileListContent
files={contentProps.files || []}
onFileClick={onFileOpen}
title={contentProps.title}
/>
);
break;
case 'todo-list':
if (contentProps.todos?.length > 0) {
contentComponent = (
<TodoListContent
todos={contentProps.todos}
isResult={contentProps.isResult}
/>
);
}
break;
case 'task':
contentComponent = <TaskListContent content={contentProps.content || ''} />;
break;
case 'text':
contentComponent = (
<TextContent
content={contentProps.content || ''}
format={contentProps.format || 'plain'}
/>
);
break;
case 'success-message': {
const msg = displayConfig.getMessage?.(parsedData) || 'Success';
contentComponent = (
<div className="flex items-center gap-1.5 text-xs text-green-600 dark:text-green-400">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
{msg}
</div>
);
break;
}
}
// 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)
: undefined;
return (
<CollapsibleDisplay
toolName={toolName}
toolId={toolId}
title={title}
defaultOpen={defaultOpen}
onTitleClick={handleTitleClick}
showRawParameters={mode === 'input' && showRawParameters}
rawContent={rawToolInput}
toolCategory={getToolCategory(toolName)}
>
{contentComponent}
</CollapsibleDisplay>
);
}
return null;
});
ToolRenderer.displayName = 'ToolRenderer';