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