diff --git a/CHANGELOG.md b/CHANGELOG.md index 08148e3394c..71aebca5a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis. +- Control UI/Talk: stop and clear failed realtime Talk sessions when dismissing runtime error banners, so the next Talk click starts a fresh session instead of only stopping the stale one. Thanks @vincentkoc. - Google Chat: create an isolated Google auth transport per auth client, so google-auth-library interceptor mutations do not accumulate across webhook verification and access-token clients. Thanks @vincentkoc. - Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc. - Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc. diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index 8b531f6fafd..c85408fe60f 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -911,19 +911,23 @@ describe("switchChatSession", () => { describe("dismissChatError", () => { it("clears persistent Talk error state", () => { + const stop = vi.fn(); const state = { lastError: 'Realtime voice provider "openai" is not configured', lastErrorCode: "UNAVAILABLE", - realtimeTalkActive: false, + realtimeTalkActive: true, + realtimeTalkSession: { stop }, realtimeTalkStatus: "error", realtimeTalkDetail: 'Realtime voice provider "openai" is not configured', realtimeTalkTranscript: "partial transcript", - } as AppViewState; + } as unknown as AppViewState & { realtimeTalkSession: { stop(): void } | null }; dismissChatError(state); expect(state.lastError).toBeNull(); expect(state.lastErrorCode).toBeNull(); + expect(stop).toHaveBeenCalledOnce(); + expect(state.realtimeTalkSession).toBeNull(); expect(state.realtimeTalkActive).toBe(false); expect(state.realtimeTalkStatus).toBe("idle"); expect(state.realtimeTalkDetail).toBeNull(); diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index 28efcccce82..0277a5cc653 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -623,6 +623,11 @@ export function dismissChatError(state: AppViewState) { state.lastError = null; state.lastErrorCode = null; if (state.realtimeTalkStatus === "error") { + const talkHost = state as unknown as { + realtimeTalkSession?: { stop(): void } | null; + }; + talkHost.realtimeTalkSession?.stop(); + talkHost.realtimeTalkSession = null; state.realtimeTalkActive = false; state.realtimeTalkStatus = "idle"; state.realtimeTalkDetail = null;