-
Notifications
You must be signed in to change notification settings - Fork 795
fix(web): Improve WebSocket reconnection experience in Web UI #1669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -190,6 +190,8 @@ type UseSessionStreamOptions = { | |
| onSessionStatus?: (status: SessionStatus) => void; | ||
| /** Callback when first turn is complete (for auto-renaming) */ | ||
| onFirstTurnComplete?: () => void; | ||
| /** Callback when reconnecting (to refresh external data like session list) */ | ||
| onReconnect?: () => void; | ||
| }; | ||
|
|
||
| type UseSessionStreamReturn = { | ||
|
|
@@ -842,7 +844,7 @@ export function useSessionStream( | |
| }, []); | ||
|
|
||
| // Reset all state | ||
| const resetState = useCallback((preserveSlashCommands = false) => { | ||
| const resetState = useCallback((preserveSlashCommands = false, preserveMessages = false) => { | ||
| resetStepState(); | ||
| currentToolCallsRef.current?.clear(); | ||
| currentToolCallIdRef.current = null; | ||
|
|
@@ -856,8 +858,6 @@ export function useSessionStream( | |
| setError(null); | ||
| setSessionStatus(null); | ||
| lastStatusSeqRef.current = null; | ||
| isReplayingRef.current = true; | ||
| setIsReplayingHistory(true); | ||
| setAwaitingFirstResponse(false); | ||
| // Reset first turn tracking | ||
| hasTurnStartedRef.current = false; | ||
|
|
@@ -877,6 +877,15 @@ export function useSessionStream( | |
| } else if (slashCommandsLenRef.current > 0) { | ||
| usingCachedCommandsRef.current = true; | ||
| } | ||
| // Handle messages: preserve or clear | ||
| if (!preserveMessages) { | ||
| setMessages([]); | ||
| // Only reset replay state when clearing messages | ||
| isReplayingRef.current = true; | ||
| setIsReplayingHistory(true); | ||
| } | ||
| // Note: when preserveMessages=true (reconnect), we keep isReplayingHistory unchanged | ||
| // to avoid triggering scroll in VirtualizedMessageList | ||
| }, [resetStepState, setAwaitingFirstResponse]); | ||
|
|
||
| // Process a SubagentEvent: accumulate inner events into parent Agent tool's subagentSteps | ||
|
|
@@ -2081,6 +2090,8 @@ export function useSessionStream( | |
| "[SessionStream] History loaded, waiting for environment...", | ||
| ); | ||
| isReplayingRef.current = false; | ||
| // Set isReplayingHistory to false to prevent layout shifts on reconnect | ||
| setIsReplayingHistory(false); | ||
| // Keep status as "submitted" - input stays disabled until session_status | ||
| setStatus((current) => (current === "ready" ? current : "submitted")); | ||
|
|
||
|
|
@@ -2380,15 +2391,20 @@ export function useSessionStream( | |
| [updateMessageById], | ||
| ); | ||
|
|
||
| // Connect to WebSocket | ||
| const connect = useCallback(() => { | ||
| // Connect options type | ||
| type ConnectOptions = { | ||
| isReconnect?: boolean; | ||
| }; | ||
|
|
||
| // Connect to WebSocket | ||
| const connect = useCallback((options?: ConnectOptions) => { | ||
| if (!sessionId) return; | ||
|
|
||
| const isReconnect = options?.isReconnect ?? false; | ||
| initializeRetryCountRef.current = 0; // Reset retry count for new connection | ||
|
|
||
| // Close existing connection | ||
| if (wsRef.current) { | ||
| console.log("[SessionStream] Closing existing WebSocket"); | ||
| wsRef.current.close(); | ||
| wsRef.current = null; | ||
| } | ||
|
|
@@ -2398,8 +2414,15 @@ export function useSessionStream( | |
| } | ||
|
|
||
| awaitingIdleRef.current = false; | ||
| resetState(true); // preserve slashCommands on reconnect | ||
| setMessages([]); | ||
|
|
||
| if (isReconnect) { | ||
| // Reconnect: preserve messages and slash commands | ||
| resetState(true, true); | ||
| } else { | ||
|
Comment on lines
+2418
to
+2421
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This reconnect path preserves Useful? React with 👍 / 👎. |
||
| // First connect: reset everything | ||
| resetState(true); | ||
| } | ||
|
Comment on lines
+2418
to
+2424
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 WebSocket reconnect duplicates all messages because history replay generates new IDs against preserved messages On reconnect, Before vs after this PRThe old code correctly handled reconnect by clearing messages: resetState(true); // old: always reset isReplayingRef to true
setMessages([]); // old: clear messages before replayThe new code preserves messages but doesn't account for the server replaying history: resetState(true, true); // preserveMessages=true: skips setMessages([]) AND skips isReplayingRef=truePrompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| setStatus("submitted"); | ||
| setAwaitingFirstResponse(Boolean(pendingMessageRef.current)); | ||
|
|
||
|
|
@@ -2748,9 +2771,11 @@ export function useSessionStream( | |
| disconnect(); | ||
| // Small delay before reconnecting | ||
| reconnectTimeoutRef.current = window.setTimeout(() => { | ||
| connect(); | ||
| connect({ isReconnect: true }); | ||
| // Trigger reconnect callback to refresh external data (e.g., session list) | ||
| options.onReconnect?.(); | ||
| }, 100); | ||
| }, [disconnect, connect]); | ||
| }, [disconnect, connect, options.onReconnect]); | ||
|
|
||
| // Keep refs in sync so useLayoutEffect can use stable references | ||
| connectRef.current = connect; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When reconnect calls
resetState(true, true), this branch skipsisReplayingRef.current = true, even thoughresetStatestill clears first-turn tracking refs. That makes replayed history events go throughprocessEventas non-replay events, soTurnBeginsetshasTurnStartedRefand a later idlesession_statuscan fireonFirstTurnCompleteagain. In this repo,chat-workspace-containerwires that callback togenerateTitle, so a reconnect on any non-empty session can trigger extra title-generation calls and other non-replay side effects.Useful? React with 👍 / 👎.