source dump of claude code
at main 177 lines 5.3 kB view raw
1import { useCallback, useRef } from 'react' 2import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js' 3import { 4 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 5 logEvent, 6} from '../services/analytics/index.js' 7import { abortSpeculation } from '../services/PromptSuggestion/speculation.js' 8import { useAppState, useSetAppState } from '../state/AppState.js' 9 10type Props = { 11 inputValue: string 12 isAssistantResponding: boolean 13} 14 15export function usePromptSuggestion({ 16 inputValue, 17 isAssistantResponding, 18}: Props): { 19 suggestion: string | null 20 markAccepted: () => void 21 markShown: () => void 22 logOutcomeAtSubmission: ( 23 finalInput: string, 24 opts?: { skipReset: boolean }, 25 ) => void 26} { 27 const promptSuggestion = useAppState(s => s.promptSuggestion) 28 const setAppState = useSetAppState() 29 const isTerminalFocused = useTerminalFocus() 30 const { 31 text: suggestionText, 32 promptId, 33 shownAt, 34 acceptedAt, 35 generationRequestId, 36 } = promptSuggestion 37 38 const suggestion = 39 isAssistantResponding || inputValue.length > 0 ? null : suggestionText 40 41 const isValidSuggestion = suggestionText && shownAt > 0 42 43 // Track engagement depth for telemetry 44 const firstKeystrokeAt = useRef<number>(0) 45 const wasFocusedWhenShown = useRef<boolean>(true) 46 const prevShownAt = useRef<number>(0) 47 48 // Capture focus state when a new suggestion appears (shownAt changes) 49 if (shownAt > 0 && shownAt !== prevShownAt.current) { 50 prevShownAt.current = shownAt 51 wasFocusedWhenShown.current = isTerminalFocused 52 firstKeystrokeAt.current = 0 53 } else if (shownAt === 0) { 54 prevShownAt.current = 0 55 } 56 57 // Record first keystroke while suggestion is visible 58 if ( 59 inputValue.length > 0 && 60 firstKeystrokeAt.current === 0 && 61 isValidSuggestion 62 ) { 63 firstKeystrokeAt.current = Date.now() 64 } 65 66 const resetSuggestion = useCallback(() => { 67 abortSpeculation(setAppState) 68 69 setAppState(prev => ({ 70 ...prev, 71 promptSuggestion: { 72 text: null, 73 promptId: null, 74 shownAt: 0, 75 acceptedAt: 0, 76 generationRequestId: null, 77 }, 78 })) 79 }, [setAppState]) 80 81 const markAccepted = useCallback(() => { 82 if (!isValidSuggestion) return 83 setAppState(prev => ({ 84 ...prev, 85 promptSuggestion: { 86 ...prev.promptSuggestion, 87 acceptedAt: Date.now(), 88 }, 89 })) 90 }, [isValidSuggestion, setAppState]) 91 92 const markShown = useCallback(() => { 93 // Check shownAt inside setAppState callback to avoid depending on it 94 // (depending on shownAt causes infinite loop when this callback is called) 95 setAppState(prev => { 96 // Only mark shown if not already shown and suggestion exists 97 if (prev.promptSuggestion.shownAt !== 0 || !prev.promptSuggestion.text) { 98 return prev 99 } 100 return { 101 ...prev, 102 promptSuggestion: { 103 ...prev.promptSuggestion, 104 shownAt: Date.now(), 105 }, 106 } 107 }) 108 }, [setAppState]) 109 110 const logOutcomeAtSubmission = useCallback( 111 (finalInput: string, opts?: { skipReset: boolean }) => { 112 if (!isValidSuggestion) return 113 114 // Determine if accepted: either Tab was pressed (acceptedAt set) OR 115 // final input matches suggestion (empty Enter case) 116 const tabWasPressed = acceptedAt > shownAt 117 const wasAccepted = tabWasPressed || finalInput === suggestionText 118 const timeMs = wasAccepted ? acceptedAt || Date.now() : Date.now() 119 120 logEvent('tengu_prompt_suggestion', { 121 source: 122 'cli' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 123 outcome: (wasAccepted 124 ? 'accepted' 125 : 'ignored') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 126 prompt_id: 127 promptId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 128 ...(generationRequestId && { 129 generationRequestId: 130 generationRequestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 131 }), 132 ...(wasAccepted && { 133 acceptMethod: (tabWasPressed 134 ? 'tab' 135 : 'enter') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 136 }), 137 ...(wasAccepted && { 138 timeToAcceptMs: timeMs - shownAt, 139 }), 140 ...(!wasAccepted && { 141 timeToIgnoreMs: timeMs - shownAt, 142 }), 143 ...(firstKeystrokeAt.current > 0 && { 144 timeToFirstKeystrokeMs: firstKeystrokeAt.current - shownAt, 145 }), 146 wasFocusedWhenShown: wasFocusedWhenShown.current, 147 similarity: 148 Math.round( 149 (finalInput.length / (suggestionText?.length || 1)) * 100, 150 ) / 100, 151 ...(process.env.USER_TYPE === 'ant' && { 152 suggestion: 153 suggestionText as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 154 userInput: 155 finalInput as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 156 }), 157 }) 158 if (!opts?.skipReset) resetSuggestion() 159 }, 160 [ 161 isValidSuggestion, 162 acceptedAt, 163 shownAt, 164 suggestionText, 165 promptId, 166 generationRequestId, 167 resetSuggestion, 168 ], 169 ) 170 171 return { 172 suggestion, 173 markAccepted, 174 markShown, 175 logOutcomeAtSubmission, 176 } 177}