| import { useCallback, useMemo, useState } from 'react' |
| import { useAppState } from 'src/state/AppState.js' |
| import { useKeybindings } from '../../../keybindings/useKeybinding.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from '../../../services/analytics/index.js' |
| import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' |
| import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' |
| import type { CompletionType } from '../../../utils/unaryLogging.js' |
| import type { ToolUseConfirm } from '../PermissionRequest.js' |
| import { |
| type FileOperationType, |
| getFilePermissionOptions, |
| type PermissionOption, |
| type PermissionOptionWithLabel, |
| } from './permissionOptions.js' |
| import { |
| PERMISSION_HANDLERS, |
| type PermissionHandlerParams, |
| } from './usePermissionHandler.js' |
|
|
| export interface ToolInput { |
| [key: string]: unknown |
| } |
|
|
| export type UseFilePermissionDialogProps<T extends ToolInput> = { |
| filePath: string |
| completionType: CompletionType |
| languageName: string | Promise<string> |
| toolUseConfirm: ToolUseConfirm |
| onDone: () => void |
| onReject: () => void |
| parseInput: (input: unknown) => T |
| operationType?: FileOperationType |
| } |
|
|
| export type UseFilePermissionDialogResult<T> = { |
| options: PermissionOptionWithLabel[] |
| onChange: (option: PermissionOption, input: T, feedback?: string) => void |
| acceptFeedback: string |
| rejectFeedback: string |
| focusedOption: string |
| setFocusedOption: (option: string) => void |
| handleInputModeToggle: (value: string) => void |
| yesInputMode: boolean |
| noInputMode: boolean |
| } |
|
|
| |
| |
| |
| export function useFilePermissionDialog<T extends ToolInput>({ |
| filePath, |
| completionType, |
| languageName, |
| toolUseConfirm, |
| onDone, |
| onReject, |
| parseInput, |
| operationType = 'write', |
| }: UseFilePermissionDialogProps<T>): UseFilePermissionDialogResult<T> { |
| const toolPermissionContext = useAppState(s => s.toolPermissionContext) |
| const [acceptFeedback, setAcceptFeedback] = useState('') |
| const [rejectFeedback, setRejectFeedback] = useState('') |
| const [focusedOption, setFocusedOption] = useState('yes') |
| const [yesInputMode, setYesInputMode] = useState(false) |
| const [noInputMode, setNoInputMode] = useState(false) |
| |
| const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false) |
| const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false) |
|
|
| |
| const options = useMemo( |
| () => |
| getFilePermissionOptions({ |
| filePath, |
| toolPermissionContext, |
| operationType, |
| onRejectFeedbackChange: setRejectFeedback, |
| onAcceptFeedbackChange: setAcceptFeedback, |
| yesInputMode, |
| noInputMode, |
| }), |
| [filePath, toolPermissionContext, operationType, yesInputMode, noInputMode], |
| ) |
|
|
| |
| const onChange = useCallback( |
| (option: PermissionOption, input: T, feedback?: string) => { |
| const params: PermissionHandlerParams = { |
| messageId: toolUseConfirm.assistantMessage.message.id, |
| path: filePath, |
| toolUseConfirm, |
| toolPermissionContext, |
| onDone, |
| onReject, |
| completionType, |
| languageName, |
| operationType, |
| } |
|
|
| |
| const originalOnAllow = toolUseConfirm.onAllow |
| toolUseConfirm.onAllow = ( |
| _input: unknown, |
| permissionUpdates: PermissionUpdate[], |
| feedback?: string, |
| ) => { |
| originalOnAllow(input, permissionUpdates, feedback) |
| } |
|
|
| const handler = PERMISSION_HANDLERS[option.type] |
| handler(params, { |
| feedback, |
| hasFeedback: !!feedback, |
| enteredFeedbackMode: |
| option.type === 'accept-once' |
| ? yesFeedbackModeEntered |
| : noFeedbackModeEntered, |
| scope: option.type === 'accept-session' ? option.scope : undefined, |
| }) |
| }, |
| [ |
| filePath, |
| completionType, |
| languageName, |
| toolUseConfirm, |
| toolPermissionContext, |
| onDone, |
| onReject, |
| operationType, |
| yesFeedbackModeEntered, |
| noFeedbackModeEntered, |
| ], |
| ) |
|
|
| |
| const handleCycleMode = useCallback(() => { |
| const sessionOption = options.find(o => o.option.type === 'accept-session') |
| if (sessionOption) { |
| const parsedInput = parseInput(toolUseConfirm.input) |
| onChange(sessionOption.option, parsedInput) |
| } |
| }, [options, parseInput, toolUseConfirm.input, onChange]) |
|
|
| |
| useKeybindings( |
| { 'confirm:cycleMode': handleCycleMode }, |
| { context: 'Confirmation' }, |
| ) |
|
|
| |
| const handleFocusedOptionChange = useCallback( |
| (value: string) => { |
| |
| if (value !== 'yes' && yesInputMode && !acceptFeedback.trim()) { |
| setYesInputMode(false) |
| } |
| if (value !== 'no' && noInputMode && !rejectFeedback.trim()) { |
| setNoInputMode(false) |
| } |
| setFocusedOption(value) |
| }, |
| [yesInputMode, noInputMode, acceptFeedback, rejectFeedback], |
| ) |
|
|
| |
| const handleInputModeToggle = useCallback( |
| (value: string) => { |
| const analyticsProps = { |
| toolName: sanitizeToolNameForAnalytics( |
| toolUseConfirm.tool.name, |
| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| isMcp: toolUseConfirm.tool.isMcp ?? false, |
| } |
|
|
| if (value === 'yes') { |
| if (yesInputMode) { |
| setYesInputMode(false) |
| logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) |
| } else { |
| setYesInputMode(true) |
| setYesFeedbackModeEntered(true) |
| logEvent('tengu_accept_feedback_mode_entered', analyticsProps) |
| } |
| } else if (value === 'no') { |
| if (noInputMode) { |
| setNoInputMode(false) |
| logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) |
| } else { |
| setNoInputMode(true) |
| setNoFeedbackModeEntered(true) |
| logEvent('tengu_reject_feedback_mode_entered', analyticsProps) |
| } |
| } |
| }, |
| [yesInputMode, noInputMode, toolUseConfirm], |
| ) |
|
|
| return { |
| options, |
| onChange, |
| acceptFeedback, |
| rejectFeedback, |
| focusedOption, |
| setFocusedOption: handleFocusedOptionChange, |
| handleInputModeToggle, |
| yesInputMode, |
| noInputMode, |
| } |
| } |
|
|