source dump of claude code
at main 212 lines 6.8 kB view raw
1import { useCallback, useMemo, useState } from 'react' 2import { useAppState } from 'src/state/AppState.js' 3import { useKeybindings } from '../../../keybindings/useKeybinding.js' 4import { 5 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 6 logEvent, 7} from '../../../services/analytics/index.js' 8import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' 9import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' 10import type { CompletionType } from '../../../utils/unaryLogging.js' 11import type { ToolUseConfirm } from '../PermissionRequest.js' 12import { 13 type FileOperationType, 14 getFilePermissionOptions, 15 type PermissionOption, 16 type PermissionOptionWithLabel, 17} from './permissionOptions.js' 18import { 19 PERMISSION_HANDLERS, 20 type PermissionHandlerParams, 21} from './usePermissionHandler.js' 22 23export interface ToolInput { 24 [key: string]: unknown 25} 26 27export type UseFilePermissionDialogProps<T extends ToolInput> = { 28 filePath: string 29 completionType: CompletionType 30 languageName: string | Promise<string> 31 toolUseConfirm: ToolUseConfirm 32 onDone: () => void 33 onReject: () => void 34 parseInput: (input: unknown) => T 35 operationType?: FileOperationType 36} 37 38export type UseFilePermissionDialogResult<T> = { 39 options: PermissionOptionWithLabel[] 40 onChange: (option: PermissionOption, input: T, feedback?: string) => void 41 acceptFeedback: string 42 rejectFeedback: string 43 focusedOption: string 44 setFocusedOption: (option: string) => void 45 handleInputModeToggle: (value: string) => void 46 yesInputMode: boolean 47 noInputMode: boolean 48} 49 50/** 51 * Hook for handling file permission dialogs with common logic 52 */ 53export function useFilePermissionDialog<T extends ToolInput>({ 54 filePath, 55 completionType, 56 languageName, 57 toolUseConfirm, 58 onDone, 59 onReject, 60 parseInput, 61 operationType = 'write', 62}: UseFilePermissionDialogProps<T>): UseFilePermissionDialogResult<T> { 63 const toolPermissionContext = useAppState(s => s.toolPermissionContext) 64 const [acceptFeedback, setAcceptFeedback] = useState('') 65 const [rejectFeedback, setRejectFeedback] = useState('') 66 const [focusedOption, setFocusedOption] = useState('yes') 67 const [yesInputMode, setYesInputMode] = useState(false) 68 const [noInputMode, setNoInputMode] = useState(false) 69 // Track whether user ever entered feedback mode (persists after collapse) 70 const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false) 71 const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false) 72 73 // Generate options based on context 74 const options = useMemo( 75 () => 76 getFilePermissionOptions({ 77 filePath, 78 toolPermissionContext, 79 operationType, 80 onRejectFeedbackChange: setRejectFeedback, 81 onAcceptFeedbackChange: setAcceptFeedback, 82 yesInputMode, 83 noInputMode, 84 }), 85 [filePath, toolPermissionContext, operationType, yesInputMode, noInputMode], 86 ) 87 88 // Handle option selection using shared handlers 89 const onChange = useCallback( 90 (option: PermissionOption, input: T, feedback?: string) => { 91 const params: PermissionHandlerParams = { 92 messageId: toolUseConfirm.assistantMessage.message.id, 93 path: filePath, 94 toolUseConfirm, 95 toolPermissionContext, 96 onDone, 97 onReject, 98 completionType, 99 languageName, 100 operationType, 101 } 102 103 // Override the input in toolUseConfirm to pass the parsed input 104 const originalOnAllow = toolUseConfirm.onAllow 105 toolUseConfirm.onAllow = ( 106 _input: unknown, 107 permissionUpdates: PermissionUpdate[], 108 feedback?: string, 109 ) => { 110 originalOnAllow(input, permissionUpdates, feedback) 111 } 112 113 const handler = PERMISSION_HANDLERS[option.type] 114 handler(params, { 115 feedback, 116 hasFeedback: !!feedback, 117 enteredFeedbackMode: 118 option.type === 'accept-once' 119 ? yesFeedbackModeEntered 120 : noFeedbackModeEntered, 121 scope: option.type === 'accept-session' ? option.scope : undefined, 122 }) 123 }, 124 [ 125 filePath, 126 completionType, 127 languageName, 128 toolUseConfirm, 129 toolPermissionContext, 130 onDone, 131 onReject, 132 operationType, 133 yesFeedbackModeEntered, 134 noFeedbackModeEntered, 135 ], 136 ) 137 138 // Handler for confirm:cycleMode - select accept-session option 139 const handleCycleMode = useCallback(() => { 140 const sessionOption = options.find(o => o.option.type === 'accept-session') 141 if (sessionOption) { 142 const parsedInput = parseInput(toolUseConfirm.input) 143 onChange(sessionOption.option, parsedInput) 144 } 145 }, [options, parseInput, toolUseConfirm.input, onChange]) 146 147 // Register keyboard shortcut handler via keybindings system 148 useKeybindings( 149 { 'confirm:cycleMode': handleCycleMode }, 150 { context: 'Confirmation' }, 151 ) 152 153 // Wrap setFocusedOption and reset input mode when navigating away 154 const handleFocusedOptionChange = useCallback( 155 (value: string) => { 156 // Reset input mode when navigating away, but only if no text typed 157 if (value !== 'yes' && yesInputMode && !acceptFeedback.trim()) { 158 setYesInputMode(false) 159 } 160 if (value !== 'no' && noInputMode && !rejectFeedback.trim()) { 161 setNoInputMode(false) 162 } 163 setFocusedOption(value) 164 }, 165 [yesInputMode, noInputMode, acceptFeedback, rejectFeedback], 166 ) 167 168 // Handle Tab key toggling input mode for Yes/No options 169 const handleInputModeToggle = useCallback( 170 (value: string) => { 171 const analyticsProps = { 172 toolName: sanitizeToolNameForAnalytics( 173 toolUseConfirm.tool.name, 174 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 175 isMcp: toolUseConfirm.tool.isMcp ?? false, 176 } 177 178 if (value === 'yes') { 179 if (yesInputMode) { 180 setYesInputMode(false) 181 logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) 182 } else { 183 setYesInputMode(true) 184 setYesFeedbackModeEntered(true) 185 logEvent('tengu_accept_feedback_mode_entered', analyticsProps) 186 } 187 } else if (value === 'no') { 188 if (noInputMode) { 189 setNoInputMode(false) 190 logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) 191 } else { 192 setNoInputMode(true) 193 setNoFeedbackModeEntered(true) 194 logEvent('tengu_reject_feedback_mode_entered', analyticsProps) 195 } 196 } 197 }, 198 [yesInputMode, noInputMode, toolUseConfirm], 199 ) 200 201 return { 202 options, 203 onChange, 204 acceptFeedback, 205 rejectFeedback, 206 focusedOption, 207 setFocusedOption: handleFocusedOptionChange, 208 handleInputModeToggle, 209 yesInputMode, 210 noInputMode, 211 } 212}