import { Box } from "ink"; import { memo } from "react"; import type { AdvancedDiffSuccess } from "../helpers/diff"; import { parsePatchOperations } from "../helpers/formatArgsDisplay"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer"; import { colors } from "./colors"; import { BashPreview } from "./previews/BashPreview"; import { PlanPreview } from "./previews/PlanPreview"; import { Text } from "./Text"; const SOLID_LINE = "─"; const DOTTED_LINE = "╌"; type Props = { toolName: string; toolArgs: string; precomputedDiff?: AdvancedDiffSuccess; allDiffs?: Map; planContent?: string; planFilePath?: string; toolCallId?: string; }; /** * Get a human-readable header for file edit tools */ function getFileEditHeader(toolName: string, toolArgs: string): string { const t = toolName.toLowerCase(); try { const args = JSON.parse(toolArgs); // Handle patch tools if (t === "apply_patch" || t === "applypatch") { if (args.input) { const operations = parsePatchOperations(args.input); if (operations.length > 1) { return `Apply patch to ${operations.length} files?`; } else if (operations.length === 1) { const op = operations[0]; if (op) { const { relative } = require("node:path"); const cwd = process.cwd(); const relPath = relative(cwd, op.path); const displayPath = relPath.startsWith("..") ? op.path : relPath; if (op.kind === "add") return `Write to ${displayPath}?`; if (op.kind === "update") return `Update ${displayPath}?`; if (op.kind === "delete") return `Delete ${displayPath}?`; } } } return "Apply patch?"; } // Handle single-file edit/write tools const filePath = args.file_path || ""; const { relative } = require("node:path"); const cwd = process.cwd(); const relPath = relative(cwd, filePath); const displayPath = relPath.startsWith("..") ? filePath : relPath; if ( t === "write" || t === "write_file" || t === "writefile" || t === "write_file_gemini" || t === "writefilegemini" ) { const { existsSync } = require("node:fs"); try { if (existsSync(filePath)) { return `Overwrite ${displayPath}?`; } } catch { // Ignore } return `Write to ${displayPath}?`; } if ( t === "edit" || t === "str_replace_editor" || t === "str_replace_based_edit_tool" ) { return `Update ${displayPath}?`; } if (t === "multi_edit" || t === "multiedit") { return `Apply edits to ${displayPath}?`; } } catch { // Fall through } return `${toolName} requires approval`; } /** * ApprovalPreview - Renders the preview content for an eagerly-committed approval * * This component renders the "preview" part of an approval that was committed * early to enable flicker-free approval UI. It ensures visual parity with * what the inline approval components show. */ export const ApprovalPreview = memo( ({ toolName, toolArgs, precomputedDiff, allDiffs, planContent, toolCallId, }: Props) => { const columns = useTerminalWidth(); const solidLine = SOLID_LINE.repeat(Math.max(columns, 10)); const dottedLine = DOTTED_LINE.repeat(Math.max(columns, 10)); // ExitPlanMode: Use PlanPreview component if (toolName === "ExitPlanMode" && planContent) { return ( ); } // Bash/Shell: Use BashPreview component if ( toolName === "Bash" || toolName === "shell" || toolName === "Shell" || toolName === "shell_command" ) { try { const args = JSON.parse(toolArgs); const command = typeof args.command === "string" ? args.command : Array.isArray(args.command) ? args.command.join(" ") : ""; const description = args.description || args.justification || ""; return ( ); } catch { // Fall through to generic } } // File Edit tools: Render diff preview if ( toolName === "Edit" || toolName === "MultiEdit" || toolName === "Write" || toolName === "str_replace_editor" || toolName === "str_replace_based_edit_tool" || toolName === "apply_patch" || toolName === "ApplyPatch" || toolName === "memory_apply_patch" ) { const headerText = getFileEditHeader(toolName, toolArgs); try { const args = JSON.parse(toolArgs); // Handle patch tools (can have multiple files) if ( args.input && (toolName === "apply_patch" || toolName === "ApplyPatch" || toolName === "memory_apply_patch") ) { const operations = parsePatchOperations(args.input); return ( {solidLine} {headerText} {dottedLine} {operations.map((op, idx) => { const { relative } = require("node:path"); const cwd = process.cwd(); const relPath = relative(cwd, op.path); const displayPath = relPath.startsWith("..") ? op.path : relPath; const diffKey = toolCallId ? `${toolCallId}:${op.path}` : undefined; const opDiff = diffKey && allDiffs ? allDiffs.get(diffKey) : undefined; if (op.kind === "add") { return ( {idx > 0 && } {displayPath} ); } if (op.kind === "update") { return ( {idx > 0 && } {displayPath} ); } if (op.kind === "delete") { return ( {idx > 0 && } Delete {displayPath} ); } return null; })} {dottedLine} ); } // Single file edit/write const filePath = args.file_path || ""; return ( {solidLine} {headerText} {dottedLine} {/* Write */} {args.content !== undefined && ( )} {/* Multi-edit */} {args.edits && Array.isArray(args.edits) && ( ({ old_string: e.old_string || "", new_string: e.new_string || "", }), )} /> )} {/* Single edit */} {args.old_string !== undefined && !args.edits && ( )} {dottedLine} ); } catch { // Fall through to generic } } // Generic fallback return ( {solidLine} {toolName} requires approval {dottedLine} ); }, ); ApprovalPreview.displayName = "ApprovalPreview";