fix: session reconnect catch-up, always-on input, frozen session recovery (#524)

- WebSocketContext: emit 'websocket-reconnected' on onopen when it's a reconnect
  (hasConnectedRef tracks first-connect vs. subsequent reconnects).
- useChatRealtimeHandlers: handle 'websocket-reconnected' via onWebSocketReconnect
  callback; added to globalMessageTypes to bypass sessionId mismatch checks.
- ChatInterface: on reconnect, re-fetch JSONL session history so messages missed
  during iOS background are shown immediately. Also resets isLoading and
  canAbortSession so a dead/restarted session no longer freezes the UI forever.
- ChatComposer: remove disabled={isLoading} from textarea — users can always
  type regardless of processing state; submit button still prevents double-send.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
patrickmwatson
2026-03-11 03:06:57 -04:00
committed by GitHub
parent a77f213dd5
commit 4d8fb6e30a
4 changed files with 34 additions and 3 deletions

View File

@@ -109,6 +109,7 @@ function ChatInterface({
scrollToBottom,
scrollToBottomAndReset,
handleScroll,
loadSessionMessages,
} = useChatSessionState({
selectedProject,
selectedSession,
@@ -197,6 +198,23 @@ function ChatInterface({
setPendingPermissionRequests,
});
// On WebSocket reconnect, re-fetch the current session's messages from JSONL so missed
// streaming events (e.g. from long tool calls while iOS had the tab backgrounded) are shown.
// Also reset isLoading — if the server restarted or the session died mid-stream, the client
// would be stuck in "Processing..." forever without this reset.
const handleWebSocketReconnect = useCallback(async () => {
if (!selectedProject || !selectedSession) return;
const provider = (localStorage.getItem('selected-provider') as any) || 'claude';
const messages = await loadSessionMessages(selectedProject.name, selectedSession.id, false, provider);
if (messages && messages.length > 0) {
setChatMessages(messages);
}
// Reset loading state — if the session is still active, new WebSocket messages will
// set it back to true. If it died, this clears the permanent frozen state.
setIsLoading(false);
setCanAbortSession(false);
}, [selectedProject, selectedSession, loadSessionMessages, setChatMessages, setIsLoading, setCanAbortSession]);
useChatRealtimeHandlers({
latestMessage,
provider,
@@ -219,6 +237,7 @@ function ChatInterface({
onSessionNotProcessing,
onReplaceTemporarySession,
onNavigateToSession,
onWebSocketReconnect: handleWebSocketReconnect,
});
useEffect(() => {