From 4849ec4982c0986c3943865fb3e789a67eec7e0a Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 29 Jul 2026 21:01:03 +0100 Subject: [PATCH] fix: clear partial Talk startup state (#116067) --- ui/src/pages/chat/chat-realtime.test.ts | 38 +++++++++++++++++++++++++ ui/src/pages/chat/chat-realtime.ts | 13 ++------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/ui/src/pages/chat/chat-realtime.test.ts b/ui/src/pages/chat/chat-realtime.test.ts index 913df86380bb..69abc479a185 100644 --- a/ui/src/pages/chat/chat-realtime.test.ts +++ b/ui/src/pages/chat/chat-realtime.test.ts @@ -458,6 +458,44 @@ describe("chat realtime actions", () => { ]); }); + it("clears partial realtime state when session startup fails", async () => { + let rejectStart: (error: Error) => void = () => undefined; + startSpy.mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + rejectStart = reject; + }), + ); + const state = createState(); + + const starting = state.toggleRealtimeTalk(); + await vi.waitFor(() => expect(state.realtimeTalkSession).not.toBeNull()); + const session = inspectSession(state); + session.callbacks.onStatus?.("listening"); + session.callbacks.onVideoCapability?.(true); + session.callbacks.onInputLevel?.(0.8); + session.callbacks.onTranscript?.({ role: "user", text: "partial", final: false }); + session.callbacks.onVideoStream?.({} as MediaStream); + state.realtimeTalkCameraDevices = [{ deviceId: "camera", label: "Camera" }]; + state.realtimeTalkVideoPending = true; + state.realtimeTalkCameraError = true; + + rejectStart(new Error("startup failed")); + await starting; + + expect(state.realtimeTalkSession).toBeNull(); + expect(state.realtimeTalkActive).toBe(false); + expect(state.realtimeTalkStatus).toBe("error"); + expect(state.realtimeTalkDetail).toBe("startup failed"); + expect(state.realtimeTalkInputLevel.value).toBe(0); + expect(state.realtimeTalkConversation).toEqual([]); + expect(state.realtimeTalkVideoStream).toBeNull(); + expect(state.realtimeTalkCameraDevices).toEqual([]); + expect(state.realtimeTalkVideoCapable).toBe(false); + expect(state.realtimeTalkVideoPending).toBe(false); + expect(state.realtimeTalkCameraError).toBe(false); + }); + it("ignores a stopped session that rejects after its replacement starts", async () => { let rejectFirstStart: (error: Error) => void = () => undefined; startSpy.mockImplementationOnce( diff --git a/ui/src/pages/chat/chat-realtime.ts b/ui/src/pages/chat/chat-realtime.ts index 665c4c240dce..88bc9cebcdf2 100644 --- a/ui/src/pages/chat/chat-realtime.ts +++ b/ui/src/pages/chat/chat-realtime.ts @@ -261,17 +261,10 @@ export function attachChatRealtimeActions(state: ChatRealtimeState) { if (state.realtimeTalkSession !== session) { return; } - state.realtimeTalkSession = null; - session.stop(); - state.realtimeTalkActive = false; + const detail = error instanceof Error ? error.message : String(error); + stopChatRealtimeTalk(state); state.realtimeTalkStatus = "error"; - state.realtimeTalkDetail = error instanceof Error ? error.message : String(error); - state.realtimeTalkInputLevel.set(0); - state.realtimeTalkVideoStream = null; - state.realtimeTalkCameraDevices = []; - state.realtimeTalkVideoCapable = false; - state.realtimeTalkVideoPending = false; - state.realtimeTalkCameraError = false; + state.realtimeTalkDetail = detail; state.requestUpdate(); } };