({ mode, overrideMessage }: BriefSpinnerProps)
| 410 | }; |
| 411 | |
| 412 | function BriefSpinner({ mode, overrideMessage }: BriefSpinnerProps): React.ReactNode { |
| 413 | const settings = useSettings(); |
| 414 | const reducedMotion = settings.prefersReducedMotion ?? false; |
| 415 | const [randomVerb] = useState(() => sample(getSpinnerVerbs()) ?? 'Working'); |
| 416 | const verb = overrideMessage ?? randomVerb; |
| 417 | const connStatus = useAppState(s => s.remoteConnectionStatus); |
| 418 | |
| 419 | // Track CLI activity so OS/IDE "busy" indicators fire in brief mode too |
| 420 | useEffect(() => { |
| 421 | const operationId = 'spinner-' + mode; |
| 422 | activityManager.startCLIActivity(operationId); |
| 423 | return () => { |
| 424 | activityManager.endCLIActivity(operationId); |
| 425 | }; |
| 426 | }, [mode]); |
| 427 | |
| 428 | // Drive both dot cycle and shimmer from the shared clock. The viewport |
| 429 | // ref is unused — the spinner unmounts on turn end so viewport-based |
| 430 | // pausing isn't needed. |
| 431 | const [, time] = useAnimationFrame(reducedMotion ? null : 120); |
| 432 | |
| 433 | // Local tasks + remote tasks are mutually exclusive (viewer mode has an |
| 434 | // empty local AppState.tasks; local mode has remoteBackgroundTaskCount=0). |
| 435 | // Summing avoids a mode branch. |
| 436 | const runningCount = useAppState(s => count(Object.values(s.tasks), isBackgroundTask) + s.remoteBackgroundTaskCount); |
| 437 | |
| 438 | // Connection trouble overrides the verb — `claude assistant` is a pure viewer, |
| 439 | // nothing useful is happening while the WS is down. |
| 440 | const showConnWarning = connStatus === 'reconnecting' || connStatus === 'disconnected'; |
| 441 | const connText = connStatus === 'reconnecting' ? 'Reconnecting' : 'Disconnected'; |
| 442 | |
| 443 | // Dots padded to a fixed 3 columns so the right-aligned count doesn't |
| 444 | // jitter as the cycle advances. |
| 445 | const dotFrame = Math.floor(time / 300) % 3; |
| 446 | const dots = reducedMotion ? '… ' : '.'.repeat(dotFrame + 1).padEnd(3); |
| 447 | |
| 448 | // Shimmer: reverse-sweep highlight across the verb. Skip for connection |
| 449 | // warnings (shimmer reads as "working"; Reconnecting/Disconnected is not). |
| 450 | const verbWidth = useMemo(() => stringWidth(verb), [verb]); |
| 451 | const glimmerIndex = |
| 452 | reducedMotion || showConnWarning ? -100 : computeGlimmerIndex(Math.floor(time / SHIMMER_INTERVAL_MS), verbWidth); |
| 453 | const { before, shimmer, after } = computeShimmerSegments(verb, glimmerIndex); |
| 454 | |
| 455 | const { columns } = useTerminalSize(); |
| 456 | const rightText = runningCount > 0 ? `${runningCount} in background` : ''; |
| 457 | // Manual right-align via space padding — flexGrow spacers inside |
| 458 | // FullscreenLayout's `main` slot don't resolve a width and caused the |
| 459 | // diff engine to miss dot-frame updates. |
| 460 | const leftWidth = (showConnWarning ? stringWidth(connText) : verbWidth) + 3; |
| 461 | const pad = Math.max(1, columns - 2 - leftWidth - stringWidth(rightText)); |
| 462 | |
| 463 | return ( |
| 464 | <Box flexDirection="row" width="100%" marginTop={1} paddingLeft={2}> |
| 465 | {showConnWarning ? ( |
| 466 | <Text color="error">{connText + dots}</Text> |
| 467 | ) : ( |
| 468 | <> |
| 469 | {before ? <Text dimColor>{before}</Text> : null} |
nothing calls this directly
no test coverage detected