import { Match, Show, Switch, createEffect, createSignal, createMemo } from "solid-js"; import type { ToolState as BaseToolState, MessagePart, Permission } from "../../types"; import { DiffViewer, getDiffStats } from "./DiffViewer"; type ToolName = | "read" | "write" | "edit" | "web_search" | "webfetch" | "grep" | "glob" | "list" | "bash" | "todowrite" | "todoread" | "playwright_browser_navigate" | "playwright_browser_click" | "playwright_browser_type" | "playwright_browser_snapshot" | "playwright_browser_take_screenshot" | "clipboard_copy_selection" | "clipboard_cut_selection" | "clipboard_paste_clipboard" | "task" | "query_db" | "logs" | "enrich_profile"; type ReadInput = { filePath?: string; path?: string }; type WriteInput = { filePath?: string; path?: string }; type WebSearchInput = { query?: string }; type WebFetchInput = { url?: string }; type GrepInput = { pattern?: string }; type GlobInput = { pattern?: string }; type ListInput = { path?: string }; type BashInput = { command?: string; description?: string }; type PlaywrightNavigateInput = { url?: string }; type PlaywrightClickInput = { element?: string }; type PlaywrightTypeInput = { element?: string }; type ClipboardInput = { selection?: string }; type TaskInput = { description?: string }; type ToolInputMap = { read: ReadInput; write: WriteInput; edit: WriteInput; web_search: WebSearchInput; webfetch: WebFetchInput; grep: GrepInput; glob: GlobInput; list: ListInput; bash: BashInput; todowrite: Record; todoread: Record; playwright_browser_navigate: PlaywrightNavigateInput; playwright_browser_click: PlaywrightClickInput; playwright_browser_type: PlaywrightTypeInput; playwright_browser_snapshot: Record; playwright_browser_take_screenshot: Record; clipboard_copy_selection: ClipboardInput; clipboard_cut_selection: ClipboardInput; clipboard_paste_clipboard: ClipboardInput; task: TaskInput; query_db: Record; logs: Record; enrich_profile: Record; }; type ToolInput = ToolInputMap[ToolName] | Record; type ToolState = Omit & { input?: ToolInput; }; // Icon components const ChecklistIcon = () => ( ); const FileIcon = () => ( ); const FileDiffIcon = () => ( ); const GlobeIcon = () => ( ); const MagnifyingGlassIcon = () => ( ); const FolderIcon = () => ( ); const TerminalIcon = () => ( ); const GenericToolIcon = () => ( ); const ChevronDownIcon = (props: { isOpen: boolean }) => ( ); const EnterIcon = () => ( ); interface ToolCallProps { part: MessagePart; workspaceRoot?: string; pendingPermissions?: Map; onPermissionResponse?: ( permissionId: string, response: "once" | "always" | "reject" ) => void; } interface ToolDisplayInfo { icon: any; text: string; monospace: boolean; isLight?: boolean; // For todo tools isFilePath?: boolean; // For file paths that need special rendering dirPath?: string; // Directory part of the path fileName?: string; // File name part } function toRelativePath( absolutePath: string | undefined, workspaceRoot?: string ): string | undefined { if (!absolutePath || !workspaceRoot) return absolutePath; // Ensure paths have consistent separators const normalizedAbsolute = absolutePath.replace(/\\/g, "/"); const normalizedRoot = workspaceRoot.replace(/\\/g, "/"); // Check if the path starts with the workspace root if (normalizedAbsolute.startsWith(normalizedRoot)) { let relativePath = normalizedAbsolute.slice(normalizedRoot.length); // Remove leading slash if present if (relativePath.startsWith("/")) { relativePath = relativePath.slice(1); } return relativePath || "."; } return absolutePath; } function splitFilePath(filePath: string): { dirPath: string; fileName: string; } { const lastSlash = Math.max( filePath.lastIndexOf("/"), filePath.lastIndexOf("\\") ); if (lastSlash === -1) { // No directory, just filename return { dirPath: "", fileName: filePath }; } return { dirPath: filePath.substring(0, lastSlash + 1), // Include trailing slash fileName: filePath.substring(lastSlash + 1), }; } function getToolDisplayInfo( tool: ToolName | string | undefined, state: ToolState, workspaceRoot?: string ): ToolDisplayInfo { if (!tool) return { icon: GenericToolIcon, text: "Tool", monospace: false }; const inputs = state.input || {}; switch (tool) { // File reads case "read": { const relativePath = toRelativePath( (inputs as ReadInput).filePath || (inputs as ReadInput).path, workspaceRoot ); if (relativePath) { const { dirPath, fileName } = splitFilePath(relativePath); return { icon: FileIcon, text: relativePath, monospace: false, isFilePath: true, dirPath, fileName, }; } return { icon: FileIcon, text: "Read file", monospace: false, }; } // File writes/edits case "write": case "edit": { const relativePath = toRelativePath( (inputs as WriteInput).filePath || (inputs as WriteInput).path, workspaceRoot ); if (relativePath) { const { dirPath, fileName } = splitFilePath(relativePath); return { icon: FileDiffIcon, text: relativePath, monospace: false, isFilePath: true, dirPath, fileName, }; } return { icon: FileDiffIcon, text: "Edit file", monospace: false, }; } // Web search case "web_search": return { icon: GlobeIcon, text: (inputs as WebSearchInput).query || "Search", monospace: false, }; // Web fetch case "webfetch": return { icon: GlobeIcon, text: (inputs as WebFetchInput).url || "Fetch page", monospace: false, }; // Grep/glob search case "grep": return { icon: MagnifyingGlassIcon, text: (inputs as GrepInput).pattern || "Search pattern", monospace: true, }; case "glob": return { icon: MagnifyingGlassIcon, text: (inputs as GlobInput).pattern || "File pattern", monospace: true, }; // List directory case "list": return { icon: FolderIcon, text: (inputs as ListInput).path || ".", monospace: true, }; // Bash case "bash": return { icon: TerminalIcon, text: (inputs as BashInput).command || (inputs as BashInput).description || "Run command", monospace: true, }; // Todo tools (lighter weight) case "todowrite": case "todoread": return { icon: ChecklistIcon, text: tool === "todowrite" ? "Updated todos" : "Read todos", monospace: false, isLight: true, }; // Playwright browser tools case "playwright_browser_navigate": return { icon: GlobeIcon, text: (inputs as PlaywrightNavigateInput).url || "Navigate", monospace: false, }; case "playwright_browser_click": return { icon: GenericToolIcon, text: `Click: ${(inputs as PlaywrightClickInput).element || "element"}`, monospace: false, }; case "playwright_browser_type": return { icon: GenericToolIcon, text: `Type: ${(inputs as PlaywrightTypeInput).element || "element"}`, monospace: false, }; case "playwright_browser_snapshot": return { icon: GenericToolIcon, text: "Take snapshot", monospace: false, }; case "playwright_browser_take_screenshot": return { icon: GenericToolIcon, text: "Screenshot", monospace: false, }; // Clipboard operations case "clipboard_copy_selection": return { icon: GenericToolIcon, text: `Copy: ${(inputs as ClipboardInput).selection || "selection"}`, monospace: true, }; case "clipboard_cut_selection": return { icon: GenericToolIcon, text: `Cut: ${(inputs as ClipboardInput).selection || "selection"}`, monospace: true, }; case "clipboard_paste_clipboard": return { icon: GenericToolIcon, text: `Paste: ${(inputs as ClipboardInput).selection || "location"}`, monospace: true, }; // Task/agent case "task": return { icon: GenericToolIcon, text: (inputs as TaskInput).description || "Run task", monospace: false, }; // Database case "query_db": return { icon: GenericToolIcon, text: "Database query", monospace: false, }; case "logs": return { icon: GenericToolIcon, text: "Fetch logs", monospace: false, }; // Profile enrichment case "enrich_profile": return { icon: GenericToolIcon, text: "Enrich profile", monospace: false, }; // Default default: return { icon: GenericToolIcon, text: state.title || tool, monospace: false, }; } } export function ToolCall(props: ToolCallProps) { const tool = props.part.tool as ToolName | string | undefined; const state = props.part.state as ToolState | undefined; if (!state) return null; const shouldDefaultOpen = tool === "edit" || tool === "write" || tool === "bash"; const [isOpen, setIsOpen] = createSignal(shouldDefaultOpen); const [toolCallRef, setToolCallRef] = createSignal( null ); const displayInfo = getToolDisplayInfo(tool, state, props.workspaceRoot); const Icon = displayInfo.icon; const hasDiff = !!(state.metadata?.diff); const isEditTool = tool === "edit" || tool === "write"; const hasOutput = !!(state.output || state.error || (isEditTool && hasDiff)); const diffStats = createMemo(() => { if (isEditTool && hasDiff && state.metadata?.diff) { return getDiffStats(state.metadata.diff); } return null; }); // Look up permission from pendingPermissions map using callID const permission = createMemo(() => { const perms = props.pendingPermissions; if (!perms) return undefined; const callID = props.part.callID; if (callID && perms.has(callID)) { return perms.get(callID); } // Also check by part ID as fallback if (perms.has(props.part.id)) { return perms.get(props.part.id); } return undefined; }); const needsPermission = createMemo(() => !!permission()); console.log("[ToolCall] Rendering:", { partId: props.part.id, callID: props.part.callID, tool, hasPermission: !!permission(), needsPermission: needsPermission(), }); // Auto-focus the tool call container when permission prompt appears createEffect(() => { if (needsPermission()) { const container = toolCallRef(); if (container) { container.focus(); } } }); const handlePermissionResponse = (response: "once" | "always" | "reject") => { const perm = permission(); console.log( "[ToolCall] Permission response button clicked:", response, "for", perm?.id ); console.log( "[ToolCall] onPermissionResponse prop exists?", !!props.onPermissionResponse ); if (perm?.id && props.onPermissionResponse) { console.log("[ToolCall] Calling onPermissionResponse"); props.onPermissionResponse(perm.id, response); } else { console.error( "[ToolCall] Cannot respond - missing permission ID or handler" ); } }; return (
{Icon && } {displayInfo.text}
{ if (needsPermission() && e.key === "Enter") { e.preventDefault(); e.stopPropagation(); console.log( "[ToolCall] Enter key pressed in tool call container" ); handlePermissionResponse("once"); } }} >
hasOutput && setIsOpen(!isOpen())} style={{ cursor: hasOutput ? "pointer" : "default" }} > {Icon && } {displayInfo.text} } > {displayInfo.dirPath} {displayInfo.fileName} 0}> +{diffStats()!.additions} 0}> -{diffStats()!.deletions} {hasOutput && }
{state.error || state.output} }>
{state.output}
); }