(props: Props)
| 35 | highlights?: TextHighlight[]; |
| 36 | }; |
| 37 | export default function TextInput(props: Props): React.ReactNode { |
| 38 | const [theme] = useTheme(); |
| 39 | const isTerminalFocused = useTerminalFocus(); |
| 40 | // Hoisted to mount-time — this component re-renders on every keystroke. |
| 41 | const accessibilityEnabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_ACCESSIBILITY), []); |
| 42 | const settings = useSettings(); |
| 43 | const reducedMotion = settings.prefersReducedMotion ?? false; |
| 44 | const voiceState = feature('VOICE_MODE') ? |
| 45 | // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant |
| 46 | useVoiceState(s => s.voiceState) : 'idle' as const; |
| 47 | const isVoiceRecording = voiceState === 'recording'; |
| 48 | const audioLevels = feature('VOICE_MODE') ? |
| 49 | // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant |
| 50 | useVoiceState(s_0 => s_0.voiceAudioLevels) : []; |
| 51 | const smoothedRef = useRef<number[]>(new Array(CURSOR_WAVEFORM_WIDTH).fill(0)); |
| 52 | const needsAnimation = isVoiceRecording && !reducedMotion; |
| 53 | const [animRef, animTime] = feature('VOICE_MODE') ? |
| 54 | // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant |
| 55 | useAnimationFrame(needsAnimation ? 50 : null) : [() => {}, 0]; |
| 56 | |
| 57 | // Show hint when terminal regains focus and clipboard has an image |
| 58 | useClipboardImageHint(isTerminalFocused, !!props.onImagePaste); |
| 59 | |
| 60 | // Cursor invert function: mini waveform during voice recording, |
| 61 | // standard chalk.inverse otherwise. No warmup pulse — the ~120ms |
| 62 | // warmup window is too short for a 1s-period pulse to register, and |
| 63 | // driving TextInput re-renders at 50ms during warmup (while spaces |
| 64 | // are simultaneously arriving every 30-80ms) causes visible stutter. |
| 65 | const canShowCursor = isTerminalFocused && !accessibilityEnabled; |
| 66 | let invert: (text: string) => string; |
| 67 | if (!canShowCursor) { |
| 68 | invert = (text: string) => text; |
| 69 | } else if (isVoiceRecording && !reducedMotion) { |
| 70 | // Single-bar waveform from the latest audio level |
| 71 | const smoothed = smoothedRef.current; |
| 72 | const raw = audioLevels.length > 0 ? audioLevels[audioLevels.length - 1] ?? 0 : 0; |
| 73 | const target = Math.min(raw * LEVEL_BOOST, 1); |
| 74 | smoothed[0] = (smoothed[0] ?? 0) * SMOOTH + target * (1 - SMOOTH); |
| 75 | const displayLevel = smoothed[0] ?? 0; |
| 76 | const barIndex = Math.max(1, Math.min(Math.round(displayLevel * (BARS.length - 1)), BARS.length - 1)); |
| 77 | const isSilent = raw < SILENCE_THRESHOLD; |
| 78 | const hue = animTime / 1000 * 90 % 360; |
| 79 | const { |
| 80 | r, |
| 81 | g, |
| 82 | b |
| 83 | } = isSilent ? { |
| 84 | r: 128, |
| 85 | g: 128, |
| 86 | b: 128 |
| 87 | } : hueToRgb(hue); |
| 88 | invert = () => chalk.rgb(r, g, b)(BARS[barIndex]!); |
| 89 | } else { |
| 90 | invert = chalk.inverse; |
| 91 | } |
| 92 | const textInputState = useTextInput({ |
| 93 | value: props.value, |
| 94 | onChange: props.onChange, |
nothing calls this directly
no test coverage detected