import { feature } from 'bun:bundle' import * as React from 'react' import { useSettings } from '../../hooks/useSettings.js' import { Box, Text, useAnimationFrame } from '@anthropic/ink' import { interpolateColor, toRGBColor } from '../Spinner/utils.js' type Props = { voiceState: 'idle' | 'recording' | 'processing' } // Processing shimmer colors: dim gray to lighter gray (matches ThinkingShimmerText) const PROCESSING_DIM = { r: 153, g: 153, b: 153 } const PROCESSING_BRIGHT = { r: 185, g: 185, b: 185 } const PULSE_PERIOD_S = 2 // 2 second period for all pulsing animations export function VoiceIndicator(props: Props): React.ReactNode { if (!feature('VOICE_MODE')) return null return } function VoiceIndicatorImpl({ voiceState }: Props): React.ReactNode { switch (voiceState) { case 'recording': return listening… case 'processing': return case 'idle': return null } } // Static — the warmup window (~120ms between space #2 and activation) // is too brief for a 1s-period shimmer to register, and a 50ms animation // timer here runs concurrently with auto-repeat spaces arriving every // 30-80ms, compounding re-renders during an already-busy window. export function VoiceWarmupHint(): React.ReactNode { if (!feature('VOICE_MODE')) return null return keep holding… } function ProcessingShimmer(): React.ReactNode { const settings = useSettings() const reducedMotion = settings.prefersReducedMotion ?? false const [ref, time] = useAnimationFrame(reducedMotion ? null : 50) if (reducedMotion) { return Voice: processing… } const elapsedSec = time / 1000 const opacity = (Math.sin((elapsedSec * Math.PI * 2) / PULSE_PERIOD_S) + 1) / 2 const color = toRGBColor( interpolateColor(PROCESSING_DIM, PROCESSING_BRIGHT, opacity), ) return ( Voice: processing… ) }