Skip to content

Commit 44f2d45

Browse files
committed
Move useEffect before early return to satisfy hooks rules
React hooks must be called in the same order on every render, so they cannot be placed after conditional returns. Moved the timer useEffect before the early return and adjusted it to safely check workspaceState.
1 parent 8cc0a9c commit 44f2d45

File tree

1 file changed

+24
-20
lines changed

1 file changed

+24
-20
lines changed

src/components/AIView.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,30 @@ const AIViewInner: React.FC<AIViewProps> = ({
245245
}
246246
}, [workspaceState, editingMessage]);
247247

248+
// Force re-evaluation after 2s when last message is a recent user message
249+
// This ensures retry barrier appears even if stream-start never arrives
250+
// Must be before early return to satisfy React Hooks rules
251+
useEffect(() => {
252+
if (!workspaceState) return;
253+
const { messages, canInterrupt } = workspaceState;
254+
255+
if (messages.length === 0) return;
256+
const lastMessage = messages[messages.length - 1];
257+
258+
if (lastMessage.type === "user" && !canInterrupt) {
259+
const messageAge = Date.now() - (lastMessage.timestamp ?? 0);
260+
const timeUntilCheck = Math.max(0, 2100 - messageAge); // 2.1s to ensure we're past threshold
261+
262+
if (timeUntilCheck > 0) {
263+
const timer = setTimeout(() => {
264+
// Force re-render to re-evaluate showRetryBarrier
265+
setForceRecheck((prev) => prev + 1);
266+
}, timeUntilCheck);
267+
return () => clearTimeout(timer);
268+
}
269+
}
270+
}, [workspaceState]);
271+
248272
// Return early if workspace state not loaded yet
249273
if (!workspaceState) {
250274
return (
@@ -272,26 +296,6 @@ const AIViewInner: React.FC<AIViewProps> = ({
272296
// Uses same logic as useResumeManager for DRY
273297
const showRetryBarrier = !canInterrupt && hasInterruptedStream(messages);
274298

275-
// Force re-evaluation after 2s when last message is a recent user message
276-
// This ensures retry barrier appears even if stream-start never arrives
277-
useEffect(() => {
278-
if (messages.length === 0) return;
279-
const lastMessage = messages[messages.length - 1];
280-
281-
if (lastMessage.type === "user" && !canInterrupt) {
282-
const messageAge = Date.now() - (lastMessage.timestamp ?? 0);
283-
const timeUntilCheck = Math.max(0, 2100 - messageAge); // 2.1s to ensure we're past threshold
284-
285-
if (timeUntilCheck > 0) {
286-
const timer = setTimeout(() => {
287-
// Force re-render to re-evaluate showRetryBarrier
288-
setForceRecheck((prev) => prev + 1);
289-
}, timeUntilCheck);
290-
return () => clearTimeout(timer);
291-
}
292-
}
293-
}, [messages, canInterrupt]);
294-
295299
// Note: We intentionally do NOT reset autoRetry when streams start.
296300
// If user pressed Ctrl+C, autoRetry stays false until they manually retry.
297301
// This makes state transitions explicit and predictable.

0 commit comments

Comments
 (0)