import { relative } from 'path' import React, { useMemo } from 'react' import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js' import { Box, Text } from '@anthropic/ink' import type { ToolUseContext } from '../../../Tool.js' import { getLanguageName } from '../../../utils/cliHighlight.js' import { getCwd } from '../../../utils/cwd.js' import { getFsImplementation, safeResolvePath, } from '../../../utils/fsOperations.js' import { expandPath } from '../../../utils/path.js' import type { CompletionType } from '../../../utils/unaryLogging.js' import { Select } from '../../CustomSelect/index.js' import { ShowInIDEPrompt } from '../../ShowInIDEPrompt.js' import { usePermissionRequestLogging } from '../hooks.js' import { PermissionDialog } from '../PermissionDialog.js' import type { ToolUseConfirm } from '../PermissionRequest.js' import type { WorkerBadgeProps } from '../WorkerBadge.js' import type { IDEDiffSupport } from './ideDiffConfig.js' import type { FileOperationType, PermissionOption, } from './permissionOptions.js' import { type ToolInput, useFilePermissionDialog, } from './useFilePermissionDialog.js' export type FilePermissionDialogProps = { // Required props from PermissionRequestProps toolUseConfirm: ToolUseConfirm toolUseContext: ToolUseContext onDone: () => void onReject: () => void // Dialog customization title: string subtitle?: React.ReactNode question?: string | React.ReactNode content?: React.ReactNode // Can be general content or diff component // Logging completionType?: CompletionType languageName?: string // override — derived from path when omitted // File/directory operations path: string | null parseInput: (input: unknown) => T operationType?: FileOperationType // IDE diff support ideDiffSupport?: IDEDiffSupport // Worker badge for teammate permission requests workerBadge: WorkerBadgeProps | undefined } export function FilePermissionDialog({ toolUseConfirm, toolUseContext, onDone, onReject, title, subtitle, question = 'Do you want to proceed?', content, completionType = 'tool_use_single', path, parseInput, operationType = 'write', ideDiffSupport, workerBadge, languageName: languageNameOverride, }: FilePermissionDialogProps): React.ReactNode { // Derive from path unless caller provided an explicit override (NotebookEdit // passes 'python'/'markdown' from cell_type). getLanguageName is async; // downstream UnaryEvent.language_name and logPermissionEvent already accept // Promise. useMemo keeps the promise stable across renders. const languageName = useMemo( () => languageNameOverride ?? (path ? getLanguageName(path) : 'none'), [languageNameOverride, path], ) const unaryEvent = useMemo( () => ({ completion_type: completionType, language_name: languageName, }), [completionType, languageName], ) usePermissionRequestLogging(toolUseConfirm, unaryEvent) const symlinkTarget = useMemo(() => { if (!path || operationType === 'read') { return null } const expandedPath = expandPath(path) const fs = getFsImplementation() const { resolvedPath, isSymlink } = safeResolvePath(fs, expandedPath) if (isSymlink) { return resolvedPath } return null }, [path, operationType]) const fileDialogResult = useFilePermissionDialog({ filePath: path || '', completionType, languageName, toolUseConfirm, onDone, onReject, parseInput, operationType, }) // Use file dialog results for options const { options, acceptFeedback, rejectFeedback, setFocusedOption, handleInputModeToggle, focusedOption, yesInputMode, noInputMode, } = fileDialogResult // Parse input using the provided parser const parsedInput = parseInput(toolUseConfirm.input) // Set up IDE diff support if enabled. Memoized: getConfig may do disk I/O // (FileWrite's getConfig calls readFileSync for the old-content diff). // Keyed on the raw input — parseInput is a pure Zod parse whose result // depends only on toolUseConfirm.input. const ideDiffConfig = useMemo( () => ideDiffSupport ? ideDiffSupport.getConfig(parseInput(toolUseConfirm.input)) : null, [ideDiffSupport, toolUseConfirm.input], ) // Create diff params based on whether IDE diff is available const diffParams = ideDiffConfig ? { onChange: ( option: PermissionOption, input: { file_path: string edits: Array<{ old_string: string new_string: string replace_all?: boolean }> }, ) => { const transformedInput = ideDiffSupport!.applyChanges( parsedInput, input.edits, ) fileDialogResult.onChange(option, transformedInput) }, toolUseContext, filePath: ideDiffConfig.filePath, edits: (ideDiffConfig.edits || []).map(e => ({ old_string: e.old_string, new_string: e.new_string, replace_all: e.replace_all || false, })), editMode: ideDiffConfig.editMode || 'single', } : { onChange: () => {}, toolUseContext, filePath: '', edits: [], editMode: 'single' as const, } const { closeTabInIDE, showingDiffInIDE, ideName } = useDiffInIDE(diffParams) const onChange = (option: PermissionOption, feedback?: string) => { closeTabInIDE?.() fileDialogResult.onChange(option, parsedInput, feedback?.trim()) } if (showingDiffInIDE && ideDiffConfig && path) { return ( onChange(option, feedback) } options={options} filePath={path} input={parsedInput} ideName={ideName} symlinkTarget={symlinkTarget} rejectFeedback={rejectFeedback} acceptFeedback={acceptFeedback} setFocusedOption={setFocusedOption} onInputModeToggle={handleInputModeToggle} focusedOption={focusedOption} yesInputMode={yesInputMode} noInputMode={noInputMode} /> ) } const isSymlinkOutsideCwd = symlinkTarget != null && relative(getCwd(), symlinkTarget).startsWith('..') const symlinkWarning = symlinkTarget ? ( {isSymlinkOutsideCwd ? `This will modify ${symlinkTarget} (outside working directory) via a symlink` : `Symlink target: ${symlinkTarget}`} ) : null return ( <> {symlinkWarning} {content} {typeof question === 'string' ? {question} : question}