({
mode,
loadingStartTimeRef,
totalPausedMsRef,
pauseStartTimeRef,
spinnerTip,
responseLengthRef,
overrideColor,
overrideShimmerColor,
overrideMessage,
spinnerSuffix,
verbose,
hasActiveTools = false,
leaderIsIdle = false
}: Props)
| 80 | return <SpinnerWithVerbInner {...props} />; |
| 81 | } |
| 82 | function SpinnerWithVerbInner({ |
| 83 | mode, |
| 84 | loadingStartTimeRef, |
| 85 | totalPausedMsRef, |
| 86 | pauseStartTimeRef, |
| 87 | spinnerTip, |
| 88 | responseLengthRef, |
| 89 | overrideColor, |
| 90 | overrideShimmerColor, |
| 91 | overrideMessage, |
| 92 | spinnerSuffix, |
| 93 | verbose, |
| 94 | hasActiveTools = false, |
| 95 | leaderIsIdle = false |
| 96 | }: Props): React.ReactNode { |
| 97 | const settings = useSettings(); |
| 98 | const reducedMotion = settings.prefersReducedMotion ?? false; |
| 99 | |
| 100 | // NOTE: useAnimationFrame(50) lives in SpinnerAnimationRow, not here. |
| 101 | // This component only re-renders when props or app state change — |
| 102 | // it is no longer on the 50ms clock. All `time`-derived values |
| 103 | // (frame, glimmer, stalled intensity, token counter, thinking shimmer, |
| 104 | // elapsed-time timer) are computed inside the child. |
| 105 | |
| 106 | const tasks = useAppState(s => s.tasks); |
| 107 | const viewingAgentTaskId = useAppState(s_0 => s_0.viewingAgentTaskId); |
| 108 | const expandedView = useAppState(s_1 => s_1.expandedView); |
| 109 | const showExpandedTodos = expandedView === 'tasks'; |
| 110 | const showSpinnerTree = expandedView === 'teammates'; |
| 111 | const selectedIPAgentIndex = useAppState(s_2 => s_2.selectedIPAgentIndex); |
| 112 | const viewSelectionMode = useAppState(s_3 => s_3.viewSelectionMode); |
| 113 | // Get foregrounded teammate (if viewing a teammate's transcript) |
| 114 | const foregroundedTeammate = viewingAgentTaskId ? getViewedTeammateTask({ |
| 115 | viewingAgentTaskId, |
| 116 | tasks |
| 117 | }) : undefined; |
| 118 | const { |
| 119 | columns |
| 120 | } = useTerminalSize(); |
| 121 | const tasksV2 = useTasksV2(); |
| 122 | |
| 123 | // Track thinking status: 'thinking' | number (duration in ms) | null |
| 124 | // Shows each state for minimum 2s to avoid UI jank |
| 125 | const [thinkingStatus, setThinkingStatus] = useState<'thinking' | number | null>(null); |
| 126 | const thinkingStartRef = useRef<number | null>(null); |
| 127 | useEffect(() => { |
| 128 | let showDurationTimer: ReturnType<typeof setTimeout> | null = null; |
| 129 | let clearStatusTimer: ReturnType<typeof setTimeout> | null = null; |
| 130 | if (mode === 'thinking') { |
| 131 | // Started thinking |
| 132 | if (thinkingStartRef.current === null) { |
| 133 | thinkingStartRef.current = Date.now(); |
| 134 | setThinkingStatus('thinking'); |
| 135 | } |
| 136 | } else if (thinkingStartRef.current !== null) { |
| 137 | // Stopped thinking - calculate duration and ensure 2s minimum display |
| 138 | const duration = Date.now() - thinkingStartRef.current; |
| 139 | const elapsed = Date.now() - thinkingStartRef.current; |
nothing calls this directly
no test coverage detected