fix(ui): retry errored talk sessions

This commit is contained in:
Vincent Koc
2026-05-04 01:05:10 -07:00
parent e5f5989aa9
commit ef79347763
3 changed files with 70 additions and 7 deletions

View File

@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
- 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.
- Control UI/Talk: retry from a failed realtime Talk session on the next Talk click instead of requiring a separate stale-session stop click first. 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.

View File

@@ -0,0 +1,57 @@
/* @vitest-environment jsdom */
import { beforeEach, describe, expect, it, vi } from "vitest";
const { realtimeTalkCtor, startMock, stopMock } = vi.hoisted(() => ({
realtimeTalkCtor: vi.fn(),
startMock: vi.fn(),
stopMock: vi.fn(),
}));
vi.mock("./chat/realtime-talk.ts", () => ({
RealtimeTalkSession: realtimeTalkCtor,
}));
describe("OpenClawApp Talk controls", () => {
beforeEach(() => {
realtimeTalkCtor.mockReset();
startMock.mockReset();
stopMock.mockReset();
realtimeTalkCtor.mockImplementation(
function MockRealtimeTalkSession(this: { start: typeof startMock; stop: typeof stopMock }) {
this.start = startMock;
this.stop = stopMock;
},
);
startMock.mockResolvedValue(undefined);
});
it("retries Talk immediately when the previous session is already in error state", async () => {
const { OpenClawApp } = await import("./app.ts");
const app = new OpenClawApp() as unknown as {
client: unknown;
connected: boolean;
realtimeTalkActive: boolean;
realtimeTalkStatus: string;
realtimeTalkSession: { stop(): void } | null;
sessionKey: string;
toggleRealtimeTalk(): Promise<void>;
};
const staleStop = vi.fn();
app.client = { request: vi.fn() } as never;
app.connected = true;
app.sessionKey = "main";
app.realtimeTalkActive = true;
app.realtimeTalkStatus = "error";
app.realtimeTalkSession = { stop: staleStop };
await app.toggleRealtimeTalk();
expect(staleStop).toHaveBeenCalledOnce();
expect(realtimeTalkCtor).toHaveBeenCalledOnce();
expect(startMock).toHaveBeenCalledOnce();
expect(stopMock).not.toHaveBeenCalled();
expect(app.realtimeTalkStatus).toBe("connecting");
expect(app.realtimeTalkSession).not.toBeNull();
});
});

View File

@@ -908,13 +908,18 @@ export class OpenClawApp extends LitElement {
async toggleRealtimeTalk() {
if (this.realtimeTalkSession) {
this.realtimeTalkSession.stop();
this.realtimeTalkSession = null;
this.realtimeTalkActive = false;
this.realtimeTalkStatus = "idle";
this.realtimeTalkDetail = null;
this.realtimeTalkTranscript = null;
return;
if (this.realtimeTalkStatus === "error") {
this.realtimeTalkSession.stop();
this.realtimeTalkSession = null;
} else {
this.realtimeTalkSession.stop();
this.realtimeTalkSession = null;
this.realtimeTalkActive = false;
this.realtimeTalkStatus = "idle";
this.realtimeTalkDetail = null;
this.realtimeTalkTranscript = null;
return;
}
}
if (!this.client || !this.connected) {
this.lastError = "Gateway not connected";