source dump of claude code
at main 148 lines 4.6 kB view raw
1import { useState } from 'react' 2import { 3 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 4 logEvent, 5} from '../../services/analytics/index.js' 6import { sanitizeToolNameForAnalytics } from '../../services/analytics/metadata.js' 7import { useSetAppState } from '../../state/AppState.js' 8import type { ToolUseConfirm } from './PermissionRequest.js' 9import { logUnaryPermissionEvent } from './utils.js' 10 11/** 12 * Shared feedback-mode state + handlers for shell permission dialogs (Bash, 13 * PowerShell). Encapsulates the yes/no input-mode toggle, feedback text state, 14 * focus tracking, and reject handling. 15 */ 16export function useShellPermissionFeedback({ 17 toolUseConfirm, 18 onDone, 19 onReject, 20 explainerVisible, 21}: { 22 toolUseConfirm: ToolUseConfirm 23 onDone: () => void 24 onReject: () => void 25 explainerVisible: boolean 26}): { 27 yesInputMode: boolean 28 noInputMode: boolean 29 yesFeedbackModeEntered: boolean 30 noFeedbackModeEntered: boolean 31 acceptFeedback: string 32 rejectFeedback: string 33 setAcceptFeedback: (v: string) => void 34 setRejectFeedback: (v: string) => void 35 focusedOption: string 36 handleInputModeToggle: (option: string) => void 37 handleReject: (feedback?: string) => void 38 handleFocus: (value: string) => void 39} { 40 const setAppState = useSetAppState() 41 const [rejectFeedback, setRejectFeedback] = useState('') 42 const [acceptFeedback, setAcceptFeedback] = useState('') 43 const [yesInputMode, setYesInputMode] = useState(false) 44 const [noInputMode, setNoInputMode] = useState(false) 45 const [focusedOption, setFocusedOption] = useState('yes') 46 // Track whether user ever entered feedback mode (persists after collapse) 47 const [yesFeedbackModeEntered, setYesFeedbackModeEntered] = useState(false) 48 const [noFeedbackModeEntered, setNoFeedbackModeEntered] = useState(false) 49 50 // Handle Tab key toggling input mode for Yes/No options 51 function handleInputModeToggle(option: string) { 52 // Notify that user is interacting with the dialog 53 toolUseConfirm.onUserInteraction() 54 const analyticsProps = { 55 toolName: sanitizeToolNameForAnalytics( 56 toolUseConfirm.tool.name, 57 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 58 isMcp: toolUseConfirm.tool.isMcp ?? false, 59 } 60 61 if (option === 'yes') { 62 if (yesInputMode) { 63 setYesInputMode(false) 64 logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) 65 } else { 66 setYesInputMode(true) 67 setYesFeedbackModeEntered(true) 68 logEvent('tengu_accept_feedback_mode_entered', analyticsProps) 69 } 70 } else if (option === 'no') { 71 if (noInputMode) { 72 setNoInputMode(false) 73 logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) 74 } else { 75 setNoInputMode(true) 76 setNoFeedbackModeEntered(true) 77 logEvent('tengu_reject_feedback_mode_entered', analyticsProps) 78 } 79 } 80 } 81 82 function handleReject(feedback?: string) { 83 const trimmedFeedback = feedback?.trim() 84 const hasFeedback = !!trimmedFeedback 85 86 // Log escape if no feedback was provided (user pressed ESC) 87 if (!hasFeedback) { 88 logEvent('tengu_permission_request_escape', { 89 explainer_visible: explainerVisible, 90 }) 91 // Increment escape count for attribution tracking 92 setAppState(prev => ({ 93 ...prev, 94 attribution: { 95 ...prev.attribution, 96 escapeCount: prev.attribution.escapeCount + 1, 97 }, 98 })) 99 } 100 101 logUnaryPermissionEvent( 102 'tool_use_single', 103 toolUseConfirm, 104 'reject', 105 hasFeedback, 106 ) 107 108 if (trimmedFeedback) { 109 toolUseConfirm.onReject(trimmedFeedback) 110 } else { 111 toolUseConfirm.onReject() 112 } 113 114 onReject() 115 onDone() 116 } 117 118 function handleFocus(value: string) { 119 // Notify that user is interacting with the dialog (only if focus changed) 120 // This prevents triggering on the initial mount/render 121 if (value !== focusedOption) { 122 toolUseConfirm.onUserInteraction() 123 } 124 // Reset input mode when navigating away, but only if no text typed 125 if (value !== 'yes' && yesInputMode && !acceptFeedback.trim()) { 126 setYesInputMode(false) 127 } 128 if (value !== 'no' && noInputMode && !rejectFeedback.trim()) { 129 setNoInputMode(false) 130 } 131 setFocusedOption(value) 132 } 133 134 return { 135 yesInputMode, 136 noInputMode, 137 yesFeedbackModeEntered, 138 noFeedbackModeEntered, 139 acceptFeedback, 140 rejectFeedback, 141 setAcceptFeedback, 142 setRejectFeedback, 143 focusedOption, 144 handleInputModeToggle, 145 handleReject, 146 handleFocus, 147 } 148}