mirror of
http://10.0.2.1:3031/sauer/claude-code.git
synced 2026-06-30 15:36:57 +10:00
204 lines
30 KiB
TypeScript
204 lines
30 KiB
TypeScript
|
|
import { relative } from 'path';
|
||
|
|
import React, { useMemo } from 'react';
|
||
|
|
import { useDiffInIDE } from '../../../hooks/useDiffInIDE.js';
|
||
|
|
import { Box, Text } from '../../../ink.js';
|
||
|
|
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<T extends ToolInput = ToolInput> = {
|
||
|
|
// 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<T>;
|
||
|
|
|
||
|
|
// Worker badge for teammate permission requests
|
||
|
|
workerBadge: WorkerBadgeProps | undefined;
|
||
|
|
};
|
||
|
|
export function FilePermissionDialog<T extends ToolInput = ToolInput>({
|
||
|
|
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<T>): 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<string>. 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_0: PermissionOption, feedback?: string) => {
|
||
|
|
closeTabInIDE?.();
|
||
|
|
fileDialogResult.onChange(option_0, parsedInput, feedback?.trim());
|
||
|
|
};
|
||
|
|
if (showingDiffInIDE && ideDiffConfig && path) {
|
||
|
|
return <ShowInIDEPrompt onChange={(option_1: PermissionOption, _input, feedback_0?: string) => onChange(option_1, feedback_0)} 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 ? <Box paddingX={1} marginBottom={1}>
|
||
|
|
<Text color="warning">
|
||
|
|
{isSymlinkOutsideCwd ? `This will modify ${symlinkTarget} (outside working directory) via a symlink` : `Symlink target: ${symlinkTarget}`}
|
||
|
|
</Text>
|
||
|
|
</Box> : null;
|
||
|
|
return <>
|
||
|
|
<PermissionDialog title={title} subtitle={subtitle} innerPaddingX={0} workerBadge={workerBadge}>
|
||
|
|
{symlinkWarning}
|
||
|
|
{content}
|
||
|
|
<Box flexDirection="column" paddingX={1}>
|
||
|
|
{typeof question === 'string' ? <Text>{question}</Text> : question}
|
||
|
|
<Select options={options} inlineDescriptions onChange={value => {
|
||
|
|
const selected = options.find(opt => opt.value === value);
|
||
|
|
if (selected) {
|
||
|
|
// For reject option
|
||
|
|
if (selected.option.type === 'reject') {
|
||
|
|
const trimmedFeedback = rejectFeedback.trim();
|
||
|
|
onChange(selected.option, trimmedFeedback || undefined);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// For accept-once option, pass accept feedback if present
|
||
|
|
if (selected.option.type === 'accept-once') {
|
||
|
|
const trimmedFeedback_0 = acceptFeedback.trim();
|
||
|
|
onChange(selected.option, trimmedFeedback_0 || undefined);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
onChange(selected.option);
|
||
|
|
}
|
||
|
|
}} onCancel={() => onChange({
|
||
|
|
type: 'reject'
|
||
|
|
})} onFocus={value_0 => setFocusedOption(value_0)} onInputModeToggle={handleInputModeToggle} />
|
||
|
|
</Box>
|
||
|
|
</PermissionDialog>
|
||
|
|
<Box paddingX={1} marginTop={1}>
|
||
|
|
<Text dimColor>
|
||
|
|
Esc to cancel
|
||
|
|
{(focusedOption === 'yes' && !yesInputMode || focusedOption === 'no' && !noInputMode) && ' · Tab to amend'}
|
||
|
|
</Text>
|
||
|
|
</Box>
|
||
|
|
</>;
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJyZWxhdGl2ZSIsIlJlYWN0IiwidXNlTWVtbyIsInVzZURpZmZJbklERSIsIkJveCIsIlRleHQiLCJUb29sVXNlQ29udGV4dCIsImdldExhbmd1YWdlTmFtZSIsImdldEN3ZCIsImdldEZzSW1wbGVtZW50YXRpb24iLCJzYWZlUmVzb2x2ZVBhdGgiLCJleHBhbmRQYXRoIiwiQ29tcGxldGlvblR5cGUiLCJTZWxlY3QiLCJTaG93SW5JREVQcm9tcHQiLCJ1c2VQZXJtaXNzaW9uUmVxdWVzdExvZ2dpbmciLCJQZXJtaXNzaW9uRGlhbG9nIiwiVG9vbFVzZUNvbmZpcm0iLCJXb3JrZXJCYWRnZVByb3BzIiwiSURFRGlmZlN1cHBvcnQiLCJGaWxlT3BlcmF0aW9uVHlwZSIsIlBlcm1pc3Npb25PcHRpb24iLCJUb29sSW5wdXQiLCJ1c2VGaWxlUGVybWlzc2lvbkRpYWxvZyIsIkZpbGVQZXJtaXNzaW9uRGlhbG9nUHJvcHMiLCJ0b29sVXNlQ29uZmlybSIsInRvb2xVc2VDb250ZXh0Iiwib25Eb25lIiwib25SZWplY3QiLCJ0aXRsZSIsInN1YnRpdGxlIiwiUmVhY3ROb2RlIiwicXVlc3Rpb24iLCJjb250ZW50IiwiY29tcGxldGlvblR5cGUiLCJsYW5ndWFnZU5hbWUiLCJwYXRoIiwicGFyc2VJbnB1dCIsImlucHV0IiwiVCIsIm9wZXJhdGlvblR5cGUiLCJpZGVEaWZmU3VwcG9ydCIsIndvcmtlckJhZGdlIiwiRmlsZVBlcm1pc3Npb25EaWFsb2ciLCJsYW5ndWFnZU5hbWVPdmVycmlkZSIsInVuYXJ5RXZlbnQiLCJjb21wbGV0aW9uX3R5cGUiLCJsYW5ndWFnZV9uYW1lIiwic3ltbGlua1RhcmdldCIsImV4cGFuZGVkUGF0aCIsImZzIiwicmVzb2x2ZWRQYXRoIiwiaXNTeW1saW5rIiwiZmlsZURpYWxvZ1Jlc3VsdCIsImZpbGVQYXRoIiwib3B0aW9ucyIsImFjY2VwdEZlZWRiYWNrIiwicmVqZWN0RmVlZGJhY2siLCJzZXRGb2N1c2VkT3B0aW9uIiwiaGFuZGxlSW5wdXRNb2RlVG9nZ2xlIiwiZm9jdXNlZE9wdGlvbiIsInllc0lucHV0TW9kZSIsIm5vSW5wdXRNb2RlIiwicGFyc2VkSW5wdXQiLCJpZGVEaWZmQ29uZmlnIiwiZ2V0Q29uZmlnIiwiZGlmZlBhcmFtcyIsIm9uQ2hhbmdlIiwib3B0aW9uIiwiZmlsZV9wYXRoIiwiZWRpdHMiLCJBcnJheSIsIm9sZF9zdHJpbmciLCJuZXdfc3RyaW5nIiwicmVwbGFjZV9hbGwiLCJ0cmFuc2Zvcm1lZElucHV0IiwiYXBwbHlDaGFuZ2VzIiwibWFwIiwiZSIsImVkaXRNb2RlIiwiY29uc3QiLCJjbG9zZVRhYkluSURFIiwic2hvd2luZ0RpZmZJbklERSIsImlkZU5hbWUiLCJmZWVkYmFjayIsInRyaW0iLCJfaW5wdXQiLCJpc1N5bWxpbmtPdXRzaWRlQ3dkIiwic3RhcnRzV2l0aCIsInN5bWxpbmtXYXJuaW5nIiwidmFsdWUiLCJzZWxlY3RlZCIsImZpbmQiLCJvcHQiLCJ0eXBlIiwidHJpbW1lZEZlZWRiYWNrIiwidW5kZWZpbmVkIl0sInNvdXJjZXMiOlsiRmlsZVBlcm1pc3Npb25EaWFsb2cudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHJlbGF0aXZlIH0gZnJvbSAncGF0aCdcbmltcG9ydCBSZWFjdCwgeyB1c2VNZW1vIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VEaWZmSW5JREUgfSBmcm9tICcuLi8uLi8uLi9ob29rcy91c2VEaWZmSW5JREUuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi8uLi9pbmsuanMnXG5pbXBvcnQgdHlwZSB7IFRvb2xVc2VDb250ZXh0IH0gZnJvbSAnLi4vLi4vLi4vVG9vbC5qcydcbmltcG9ydCB7IGdldExhbmd1YWdlTmFtZSB9IGZyb20gJy4uLy4uLy4uL3V0aWxzL2NsaUhpZ2hsaWdodC5qcydcbmltcG9ydCB7IGdldEN3ZCB9IGZyb20gJy4uLy4uLy4uL3V0aWxzL2N3ZC5qcydcbmltcG9ydCB7XG4gIGdldEZzSW1wbGVtZW50YXRpb24sXG4gIHNhZmVSZXNvbHZlUGF0aCxcbn0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvZnNPcGVyYXRpb25zLmpzJ1xuaW1wb3J0IHsgZXhwYW5kUGF0aCB9IGZyb20gJy4uLy4uLy4uL3V0aWxzL3BhdGguanMnXG5pbXBvcnQgdHlwZSB7IENvbXBsZXRpb25UeXBlIH0gZnJvbSAnLi4vLi4vLi4vdXRpbHMvdW5hcnlMb2dnaW5nLmpzJ1xuaW1wb3J0IHsgU2VsZWN0IH0gZnJvbSAnLi4vLi4vQ3VzdG9tU2VsZWN0L2luZGV4LmpzJ1xuaW1wb3J0IHsgU2hvd0luSURFUHJvbXB0IH0gZnJvbSAnLi4vLi4vU2hvd0luSURFUHJvbXB0LmpzJ1xuaW1wb3J0IHsgdXNlUGVybWlzc2lvblJlcXVlc3RMb2dnaW5nIH0gZnJvbSAnLi4vaG9va3MuanMnXG5pbXBvcnQgeyBQZXJtaXNzaW9uRGlhbG9nIH0gZnJvbSAnLi4vUGVybWlzc2lvbkRpYWxvZy5qcydcbmltcG9ydCB0eXBlIHsgVG9vbFVzZUNvbmZpcm0gfSBmcm9tICcuLi9QZXJtaXNzaW9uUmVxdWVzdC5qcydcbmltcG9ydCB0eXBlIHsgV29ya2VyQmFkZ2VQcm9wcyB9IGZyb20gJy4uL1dvcmtlckJhZGdlLmpzJ1xuaW1wb3J0IHR5cGUgeyBJREVEaWZmU3VwcG9ydCB9IGZyb20gJy4vaWRlRGlmZkNvbmZpZy5qcydcbmltcG9ydCB0eXBlIHtcbiAgRmlsZU9wZXJhdGlvblR5cGUsXG4gIFBlcm1pc3Npb25PcHRpb24sXG59IGZyb20gJy4vcGVybWlzc2lvbk9wdGlvbnMuanMnXG5pbXBvcnQge1xuICB0eXBlIFRvb2xJbnB1dCxcbiAgdXNlRmlsZVBlcm1pc3Npb25EaWFsb2csXG59IGZyb20gJy4vdXNlRmlsZVBlcm1pc3Npb25EaWFsb2cuanMnXG5cbmV4cG9ydCB0eXBlIEZpbGVQZXJtaXNzaW9uRGlhbG9nUHJvcHM8VCBleHRlbmRzIFRvb2xJbnB1dCA9IFRvb2xJbnB1dD4gPSB7XG4gIC8vIFJlcXVpcmVkIHByb3BzIGZyb20gUGVybWlzc2lvblJlcXVlc3RQcm9wc1xuICB0b29sVXNlQ29uZmlybTogVG9vbFVzZUNvbmZpcm1cbiAgdG9vbFVzZUNvbnRleHQ6IFRvb2xVc2VDb250ZXh0XG4gIG9uRG9uZTogKCkgPT4gdm9pZFxuICBvblJlamVjdDogKCkgPT4gdm9pZFxuXG4gIC8vIERpYWxvZyBjdXN0b21pemF0aW9uXG4gIHRpdGxlOiBzdHJpbmdcbiAgc3VidGl0bGU/OiBSZWFjdC5SZWFjdE5vZGVcbiAgcXVlc3Rpb24/OiBzdHJpbmcgfCBSZWFjdC5SZWFjdE5vZGVcbiAgY29udGVudD86IFJlYWN0LlJlYWN0Tm9kZSAvLyBDYW4gYmUgZ2VuZXJhbCBjb250ZW50IG9yIGRpZmYgY29tcG9uZW50XG5cbiAgLy8
|