diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index c4a3d202..103dea81 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -184,7 +184,7 @@ function ChatInterface({ handlePermissionDecision, handleGrantToolPermission, handleInputFocusChange, - isInputFocused: _isInputFocused, + isInputFocused, commandModalPayload, closeCommandModal, showCostModal, @@ -424,6 +424,7 @@ function ChatInterface({ onTextareaPaste={handlePaste} onTextareaScrollSync={syncInputOverlayScroll} onTextareaInput={handleTextareaInput} + isInputFocused={isInputFocused} onInputFocusChange={handleInputFocusChange} placeholder={t('input.placeholder', { provider: diff --git a/src/components/chat/view/subcomponents/ActivityIndicator.tsx b/src/components/chat/view/subcomponents/ActivityIndicator.tsx index 810d2a2e..f235c2fb 100644 --- a/src/components/chat/view/subcomponents/ActivityIndicator.tsx +++ b/src/components/chat/view/subcomponents/ActivityIndicator.tsx @@ -7,6 +7,7 @@ import type { SessionActivity } from '../../../../hooks/useSessionProtection'; type ActivityIndicatorProps = { activity: SessionActivity | null; onAbort?: () => void; + isInputFocused?: boolean; }; const ACTION_KEYS = [ @@ -18,6 +19,7 @@ const ACTION_KEYS = [ 'claudeStatus.actions.reasoning', ]; const DEFAULT_ACTION_WORDS = ['Thinking', 'Processing', 'Analyzing', 'Working', 'Computing', 'Reasoning']; +const EXIT_ANIMATION_MS = 220; /** * Minimal response-in-progress indicator, in the spirit of the inline status @@ -26,11 +28,31 @@ const DEFAULT_ACTION_WORDS = ['Thinking', 'Processing', 'Analyzing', 'Working', * session has an entry in the processing map; it disappears the instant that * entry is removed. */ -export default function ActivityIndicator({ activity, onAbort }: ActivityIndicatorProps) { +export default function ActivityIndicator({ activity, onAbort, isInputFocused = false }: ActivityIndicatorProps) { const { t } = useTranslation('chat'); - const startedAt = activity?.startedAt ?? null; + const [renderedActivity, setRenderedActivity] = useState(activity); + const [isExiting, setIsExiting] = useState(false); + const startedAt = renderedActivity?.startedAt ?? null; const [elapsedSeconds, setElapsedSeconds] = useState(0); + useEffect(() => { + if (activity) { + setRenderedActivity(activity); + setIsExiting(false); + return; + } + + if (!renderedActivity) return; + + setIsExiting(true); + const timer = setTimeout(() => { + setRenderedActivity(null); + setIsExiting(false); + }, EXIT_ANIMATION_MS); + + return () => clearTimeout(timer); + }, [activity, renderedActivity]); + useEffect(() => { if (startedAt === null) return; const update = () => setElapsedSeconds(Math.max(0, Math.floor((Date.now() - startedAt) / 1000))); @@ -39,10 +61,10 @@ export default function ActivityIndicator({ activity, onAbort }: ActivityIndicat return () => clearInterval(timer); }, [startedAt]); - if (!activity) return null; + if (!renderedActivity) 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]) + const label = (renderedActivity.statusText || actionWords[Math.floor(elapsedSeconds / 4) % actionWords.length]) .replace(/\.+$/, ''); const minutes = Math.floor(elapsedSeconds / 60); @@ -50,19 +72,31 @@ export default function ActivityIndicator({ activity, onAbort }: ActivityIndicat const elapsedLabel = minutes < 1 ? t('claudeStatus.elapsed.seconds', { count: seconds, defaultValue: '{{count}}s' }) : t('claudeStatus.elapsed.minutesSeconds', { minutes, seconds, defaultValue: '{{minutes}}m {{seconds}}s' }); + const tabSurfaceClassName = [ + 'chat-activity-tab inline-flex h-8 items-center rounded-b-none rounded-t-lg border border-b-0 bg-card px-3 text-xs transition-all duration-200', + isInputFocused + ? 'border-primary/30 shadow-[0_-1px_2px_hsl(var(--foreground)/0.08),1px_0_2px_hsl(var(--foreground)/0.06),-1px_0_2px_hsl(var(--foreground)/0.06)]' + : 'border-border/50 shadow-[0_-1px_1px_hsl(var(--foreground)/0.04),1px_0_1px_hsl(var(--foreground)/0.03),-1px_0_1px_hsl(var(--foreground)/0.03)]', + ].join(' '); return ( -
-
- - {`${label}…`} - {elapsedLabel} +
+
+
+ + {`${label}…`} + {elapsedLabel} +
- {activity.canInterrupt && onAbort && ( + {renderedActivity.canInterrupt && onAbort && (