mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-15 05:07:35 +00:00
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.
206 lines
6.1 KiB
TypeScript
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';
|