import { memo } from "react"; import { permissionMode } from "../../permissions/mode"; import type { AdvancedDiffSuccess } from "../helpers/diff"; import type { ApprovalRequest } from "../helpers/stream"; import { isFileEditTool, isFileWriteTool, isPatchTool, isShellTool, isTaskTool, } from "../helpers/toolNameMapping.js"; import { InlineBashApproval } from "./InlineBashApproval"; import { InlineEnterPlanModeApproval } from "./InlineEnterPlanModeApproval"; import { InlineFileEditApproval } from "./InlineFileEditApproval"; import { InlineGenericApproval } from "./InlineGenericApproval"; import { InlineQuestionApproval } from "./InlineQuestionApproval"; import { InlineTaskApproval } from "./InlineTaskApproval"; import { StaticPlanApproval } from "./StaticPlanApproval"; // Types for parsed tool data type BashInfo = { toolName: string; command: string; description?: string; }; type FileEditInfo = { toolName: string; filePath: string; content?: string; oldString?: string; newString?: string; replaceAll?: boolean; edits?: Array<{ old_string: string; new_string: string; replace_all?: boolean; }>; patchInput?: string; toolCallId?: string; }; type TaskInfo = { subagentType: string; description: string; prompt: string; model?: string; }; type Question = { question: string; header: string; options: Array<{ label: string; description: string }>; multiSelect: boolean; }; type Props = { approval: ApprovalRequest; // Common handlers onApprove: (diffs?: Map) => void; onApproveAlways: ( scope: "project" | "session", diffs?: Map, ) => void; onDeny: (reason: string) => void; onCancel?: () => void; isFocused?: boolean; approveAlwaysText?: string; allowPersistence?: boolean; showPreview?: boolean; defaultScope?: "project" | "session"; // Special handlers for ExitPlanMode onPlanApprove?: (acceptEdits: boolean) => void; onPlanKeepPlanning?: (reason: string) => void; // Special handlers for AskUserQuestion onQuestionSubmit?: (answers: Record) => void; // Special handlers for EnterPlanMode onEnterPlanModeApprove?: () => void; onEnterPlanModeReject?: () => void; // External data for FileEdit approvals precomputedDiff?: AdvancedDiffSuccess; allDiffs?: Map; // Plan viewer data (for ExitPlanMode 'o' key) planContent?: string; planFilePath?: string; agentName?: string; }; // Parse bash info from approval args function getBashInfo(approval: ApprovalRequest): BashInfo | null { try { const args = JSON.parse(approval.toolArgs || "{}"); const t = approval.toolName.toLowerCase(); let command = ""; let description = ""; if (t === "shell") { // Shell tool uses command array and justification const cmdVal = args.command; command = Array.isArray(cmdVal) ? cmdVal.join(" ") : typeof cmdVal === "string" ? cmdVal : "(no command)"; description = typeof args.justification === "string" ? args.justification : ""; } else { // Bash/shell_command uses command string and description command = typeof args.command === "string" ? args.command : "(no command)"; description = typeof args.description === "string" ? args.description : typeof args.justification === "string" ? args.justification : ""; } return { toolName: approval.toolName, command, description, }; } catch { return null; } } // Parse file edit info from approval args function getFileEditInfo(approval: ApprovalRequest): FileEditInfo | null { try { const args = JSON.parse(approval.toolArgs || "{}"); // For patch tools, use the input field if (isPatchTool(approval.toolName)) { return { toolName: approval.toolName, filePath: "", // Patch can have multiple files patchInput: args.input as string | undefined, toolCallId: approval.toolCallId, }; } // For regular file edit/write tools return { toolName: approval.toolName, filePath: String(args.file_path || ""), content: args.content as string | undefined, oldString: args.old_string as string | undefined, newString: args.new_string as string | undefined, replaceAll: args.replace_all as boolean | undefined, edits: args.edits as FileEditInfo["edits"], toolCallId: approval.toolCallId, }; } catch { return null; } } // Parse task info from approval args function getTaskInfo(approval: ApprovalRequest): TaskInfo | null { try { const args = JSON.parse(approval.toolArgs || "{}"); return { subagentType: typeof args.subagent_type === "string" ? args.subagent_type : "unknown", description: typeof args.description === "string" ? args.description : "(no description)", prompt: typeof args.prompt === "string" ? args.prompt : "(no prompt)", model: typeof args.model === "string" ? args.model : undefined, }; } catch { return { subagentType: "unknown", description: "(parse error)", prompt: "(parse error)", }; } } // Parse questions from AskUserQuestion args function getQuestions(approval: ApprovalRequest): Question[] { try { const args = JSON.parse(approval.toolArgs || "{}"); return (args.questions as Question[]) || []; } catch { return []; } } /** * ApprovalSwitch - Unified approval component that renders the appropriate * specialized approval UI based on tool type. * * This consolidates the approval rendering logic that was previously duplicated * in the transcript rendering and fallback UI paths. */ export const ApprovalSwitch = memo( ({ approval, onApprove, onApproveAlways, onDeny, onCancel, isFocused = true, approveAlwaysText, allowPersistence = true, onPlanApprove, onPlanKeepPlanning, onQuestionSubmit, onEnterPlanModeApprove, onEnterPlanModeReject, precomputedDiff, allDiffs, showPreview = true, defaultScope = "project", planContent, planFilePath, agentName, }: Props) => { const toolName = approval.toolName; // 1. ExitPlanMode → StaticPlanApproval if (toolName === "ExitPlanMode" && onPlanApprove && onPlanKeepPlanning) { const showAcceptEditsOption = permissionMode.getMode() === "plan" && permissionMode.getModeBeforePlan() !== "bypassPermissions"; return ( onPlanApprove(false)} onApproveAndAcceptEdits={() => onPlanApprove(true)} onKeepPlanning={onPlanKeepPlanning} onCancel={onCancel ?? (() => {})} showAcceptEditsOption={showAcceptEditsOption} isFocused={isFocused} planContent={planContent} planFilePath={planFilePath} agentName={agentName} /> ); } // 2. File edit/write/patch tools → InlineFileEditApproval if ( isFileEditTool(toolName) || isFileWriteTool(toolName) || isPatchTool(toolName) ) { const fileEditInfo = getFileEditInfo(approval); if (fileEditInfo) { return ( onApprove(diffs)} onApproveAlways={(scope, diffs) => onApproveAlways(scope, diffs)} onDeny={onDeny} onCancel={onCancel} isFocused={isFocused} approveAlwaysText={approveAlwaysText} allowPersistence={allowPersistence} defaultScope={defaultScope} showPreview={showPreview} /> ); } } // 3. Shell/Bash tools → InlineBashApproval if (isShellTool(toolName)) { const bashInfo = getBashInfo(approval); if (bashInfo) { return ( onApprove()} onApproveAlways={(scope) => onApproveAlways(scope)} onDeny={onDeny} onCancel={onCancel} isFocused={isFocused} approveAlwaysText={approveAlwaysText} allowPersistence={allowPersistence} defaultScope={defaultScope} showPreview={showPreview} /> ); } } // 4. EnterPlanMode → InlineEnterPlanModeApproval if ( toolName === "EnterPlanMode" && onEnterPlanModeApprove && onEnterPlanModeReject ) { return ( ); } // 5. AskUserQuestion → InlineQuestionApproval // Guard: only render specialized UI if questions are valid, otherwise fall through // to InlineGenericApproval (matches pattern for Bash/Task with malformed args) if (toolName === "AskUserQuestion" && onQuestionSubmit) { const questions = getQuestions(approval); if (questions.length > 0) { return ( ); } } // 6. Task tool → InlineTaskApproval if (isTaskTool(toolName)) { const taskInfo = getTaskInfo(approval); if (taskInfo) { return ( onApprove()} onApproveAlways={(scope) => onApproveAlways(scope)} onDeny={onDeny} onCancel={onCancel} isFocused={isFocused} approveAlwaysText={approveAlwaysText} allowPersistence={allowPersistence} /> ); } } // 7. Fallback → InlineGenericApproval return ( onApprove()} onApproveAlways={(scope) => onApproveAlways(scope)} onDeny={onDeny} onCancel={onCancel} isFocused={isFocused} approveAlwaysText={approveAlwaysText} allowPersistence={allowPersistence} defaultScope={defaultScope} showPreview={showPreview} /> ); }, ); ApprovalSwitch.displayName = "ApprovalSwitch";