From 955ba64218999b421943f240059cb8aa86c299e8 Mon Sep 17 00:00:00 2001 From: dimakis Date: Fri, 22 May 2026 15:54:48 +0100 Subject: [PATCH] fix(client): restore running state on reconnect + route keyboard to interrupt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On v2 reconnect, the protocol parser only dispatched SET_RUNNING for the false case, silently dropping running=true. After a WS reconnect (common on iOS backgrounding), the UI showed the idle send button instead of interrupt/queue/stop — so user messages queued via sendToChat() with no stopTask cascade, leaving subagents stuck indefinitely. Additionally, the iOS keyboard Send button fires Enter → handleKeyDown → handleSend(), bypassing the on-screen interrupt/queue/stop buttons entirely. Now handleKeyDown routes through handleInterrupt() when the session is running. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/ChatInput.tsx | 6 +++++- packages/client/__tests__/protocol-parser.test.ts | 8 +++++--- packages/client/src/protocol-parser.ts | 5 +++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ChatInput.tsx b/frontend/src/components/ChatInput.tsx index 71e19dee..6bb6f69f 100644 --- a/frontend/src/components/ChatInput.tsx +++ b/frontend/src/components/ChatInput.tsx @@ -151,7 +151,11 @@ export function ChatInput({ function handleKeyDown(e: KeyboardEvent) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - handleSend(); + if (running && onInterrupt) { + handleInterrupt(); + } else { + handleSend(); + } } } diff --git a/packages/client/__tests__/protocol-parser.test.ts b/packages/client/__tests__/protocol-parser.test.ts index 8e560a0a..0ecda2da 100644 --- a/packages/client/__tests__/protocol-parser.test.ts +++ b/packages/client/__tests__/protocol-parser.test.ts @@ -627,15 +627,17 @@ describe('reconnected', () => { expect(r.messagesActions).toContainEqual({ type: 'SET_RUNNING', running: false }); }); - it('does not dispatch SET_RUNNING when active session is still running', () => { + it('dispatches SET_RUNNING true when active session is still running', () => { + const setWsRunning = vi.fn(); const state = makeState({ currentSessionId: 'sid-1' }); const r = parseServerMessage( { type: 'reconnected', sessions: [{ sessionId: 'sid-1', replayed: 0, running: true }] }, state, - makeCallbacks(), + makeCallbacks({ setWsRunning }), POOL_KEY, ); - expect(r.messagesActions).not.toContainEqual(expect.objectContaining({ type: 'SET_RUNNING' })); + expect(r.messagesActions).toContainEqual({ type: 'SET_RUNNING', running: true }); + expect(setWsRunning).toHaveBeenCalledWith(POOL_KEY, true); }); it('no-ops when no currentSessionId', () => { diff --git a/packages/client/src/protocol-parser.ts b/packages/client/src/protocol-parser.ts index a2bba4dd..01c666e8 100644 --- a/packages/client/src/protocol-parser.ts +++ b/packages/client/src/protocol-parser.ts @@ -139,8 +139,9 @@ export function parseServerMessage( ) ) { const active = sessions.find((s) => s.sessionId === state.currentSessionId); - if (active && active.running === false) { - result.messagesActions.push({ type: 'SET_RUNNING', running: false }); + if (active) { + result.messagesActions.push({ type: 'SET_RUNNING', running: active.running }); + if (active.running) callbacks.setWsRunning?.(poolKey, true); } } callbacks.onReconnected?.();