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