import React, { memo, useMemo, useCallback } from 'react'; import type { Project } from '../../../types/app'; import type { SubagentChildTool } from '../types/types'; import { getToolConfig } from './configs/toolConfigs'; import { OneLineDisplay, BashCommandDisplay, 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; 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; isSubagentContainer?: boolean; subagentState?: { childTools: SubagentChildTool[]; currentToolIndex: number; isComplete: boolean; }; } 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'; if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') return 'plan'; if (toolName === 'AskUserQuestion') return 'question'; return 'default'; } // Exact denial messages from server/claude-sdk.js — other providers can't reliably signal denial const CLAUDE_DENIAL_MESSAGES = [ 'user denied tool use', 'tool disallowed by settings', 'permission request timed out', 'permission request cancelled', ]; function deriveToolStatus(toolResult: any): ToolStatus { if (!toolResult) return 'running'; if (toolResult.isError) { const content = String(toolResult.content || '').toLowerCase().trim(); if (CLAUDE_DENIAL_MESSAGES.some((msg) => content.includes(msg))) { return 'denied'; } return 'error'; } return 'completed'; } /** * Main tool renderer router * Routes to OneLineDisplay or CollapsibleDisplay based on tool config */ export const ToolRenderer: React.FC = memo(({ toolName, toolInput, toolResult, toolId, mode, onFileOpen, createDiff, selectedProject, autoExpandTools = false, showRawParameters = false, rawToolInput, isSubagentContainer, subagentState }) => { const config = getToolConfig(toolName); const displayConfig: any = mode === 'input' ? config.input : config.result; 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]); // 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) || ''; onFileOpen(value); } }, [displayConfig, parsedData, onFileOpen]); // Route subagent containers to dedicated component (after hooks to satisfy Rules of Hooks) if (isSubagentContainer && subagentState) { if (mode === 'result') return null; return ( ); } if (!displayConfig) return null; // Bash renders as a Codex-style command row: the command on a single line with // a chevron that expands to show the output inline. The combined view lives on // the input render; the separate result section is suppressed in MessageComponent. if (toolName === 'Bash' && mode === 'input') { const command = typeof parsedData === 'object' && parsedData !== null && 'command' in parsedData ? String(parsedData.command || '') : typeof toolInput === 'string' ? toolInput : typeof rawToolInput === 'string' ? rawToolInput : ''; const description = typeof parsedData === 'object' && parsedData !== null && 'description' in parsedData ? String(parsedData.description || '') : undefined; const output = typeof toolResult?.content === 'string' ? toolResult.content : toolResult?.content != null ? String(toolResult.content) : ''; return ( ); } if (displayConfig.type === 'one-line') { const value = displayConfig.getValue?.(parsedData) || ''; const secondary = displayConfig.getSecondary?.(parsedData); return ( ); } if (displayConfig.type === 'plan') { const title = typeof displayConfig.title === 'function' ? displayConfig.title(parsedData) : displayConfig.title || 'Plan'; const contentProps = displayConfig.getContentProps?.(parsedData, { selectedProject, createDiff, onFileOpen }) || {}; const isStreaming = mode === 'input' && !toolResult; return ( ); } 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 }) || {}; let contentComponent: React.ReactNode = null; switch (displayConfig.contentType) { case 'diff': if (createDiff) { contentComponent = ( onFileOpen?.(contentProps.filePath)} /> ); } break; case 'markdown': contentComponent = ; break; case 'file-list': contentComponent = ( ); break; case 'todo-list': if (contentProps.todos?.length > 0) { contentComponent = ( ); } break; case 'task': contentComponent = ; break; case 'question-answer': contentComponent = ( ); break; case 'text': contentComponent = ( ); break; case 'success-message': { const msg = displayConfig.getMessage?.(parsedData) || 'Success'; contentComponent = (
{msg}
); break; } } const handleTitleClick = (toolName === 'Edit' || toolName === 'Write' || toolName === 'ApplyPatch') && contentProps.filePath && onFileOpen ? () => onFileOpen(contentProps.filePath, { old_string: contentProps.oldContent, new_string: contentProps.newContent }) : undefined; const badgeElement = toolStatus && toolStatus !== 'completed' ? : undefined; return ( {contentComponent} ); } return null; }); ToolRenderer.displayName = 'ToolRenderer';