import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Shimmer } from '../../../../shared/view/ui'; import type { SessionActivity } from '../../../../hooks/useSessionProtection'; type ActivityIndicatorProps = { activity: SessionActivity | null; onAbort?: () => void; }; const ACTION_KEYS = [ 'claudeStatus.actions.thinking', 'claudeStatus.actions.processing', 'claudeStatus.actions.analyzing', 'claudeStatus.actions.working', 'claudeStatus.actions.computing', 'claudeStatus.actions.reasoning', ]; const DEFAULT_ACTION_WORDS = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning']; /** * Minimal response-in-progress indicator, in the spirit of the inline status * lines in Claude Code / Codex / OpenCode: a shimmering activity label, the * elapsed time, and an interrupt affordance. Rendered only while the viewed * session has an entry in the processing map; it disappears the instant that * entry is removed. */ export default function ActivityIndicator({ activity, onAbort }: ActivityIndicatorProps) { const { t } = useTranslation('chat'); const startedAt = activity?.startedAt ?? null; const [elapsedSeconds, setElapsedSeconds] = useState(0); useEffect(() => { if (startedAt === null) return; const update = () => setElapsedSeconds(Math.max(0, Math.floor((Date.now() - startedAt) / 1000))); update(); const timer = setInterval(update, 1000); return () => clearInterval(timer); }, [startedAt]); if (!activity) return null; const actionWords = ACTION_KEYS.map((key, i) => t(key, { defaultValue: DEFAULT_ACTION_WORDS[i] })); const label = (activity.statusText || actionWords[Math.floor(elapsedSeconds / 4) % actionWords.length]) .replace(/\.+$/, ''); const minutes = Math.floor(elapsedSeconds / 60); const seconds = elapsedSeconds % 60; const elapsedLabel = minutes < 1 ? t('claudeStatus.elapsed.seconds', { count: seconds, defaultValue: '{{count}}s' }) : t('claudeStatus.elapsed.minutesSeconds', { minutes, seconds, defaultValue: '{{minutes}}m {{seconds}}s' }); return (