diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a08c7fb2e8..d1d005a0cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Dashboard/chat UI: stop reloading full chat history on every live tool result in dashboard v2 so tool-heavy runs no longer trigger UI freeze/re-render storms while the final event still refreshes persisted history. Thanks @BunsDev. - Telegram/webhook auth: validate the Telegram webhook secret before reading or parsing request bodies, so unauthenticated requests are rejected immediately instead of consuming up to 1 MB first. Thanks @space08. - Build/plugin-sdk bundling: bundle plugin-sdk subpath entries in one shared build pass so published packages stop duplicating shared chunks and avoid the recent plugin-sdk memory blow-up. (#45426) Thanks @TarasShyn. - Browser/existing-session: accept text-only `list_pages` and `new_page` responses from Chrome DevTools MCP so live-session tab discovery and new-tab open flows keep working when the server omits structured page metadata. diff --git a/ui/src/ui/app-gateway.node.test.ts b/ui/src/ui/app-gateway.node.test.ts index c8ea860b72e..bcef1be3ed3 100644 --- a/ui/src/ui/app-gateway.node.test.ts +++ b/ui/src/ui/app-gateway.node.test.ts @@ -3,6 +3,8 @@ import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "../../../src/gateway/events.js"; import { ConnectErrorDetailCodes } from "../../../src/gateway/protocol/connect-error-details.js"; import { connectGateway, resolveControlUiClientVersion } from "./app-gateway.ts"; +const loadChatHistoryMock = vi.hoisted(() => vi.fn(async () => undefined)); + type GatewayClientMock = { start: ReturnType; stop: ReturnType; @@ -70,6 +72,14 @@ vi.mock("./gateway.ts", () => { return { GatewayBrowserClient, resolveGatewayErrorDetailCode }; }); +vi.mock("./controllers/chat.ts", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadChatHistory: loadChatHistoryMock, + }; +}); + function createHost() { return { settings: { @@ -106,7 +116,15 @@ function createHost() { assistantAgentId: null, serverVersion: null, sessionKey: "main", + chatMessages: [], + chatToolMessages: [], + chatStreamSegments: [], + chatStream: null, + chatStreamStartedAt: null, chatRunId: null, + toolStreamById: new Map(), + toolStreamOrder: [], + toolStreamSyncTimer: null, refreshSessionsAfterChat: new Set(), execApprovalQueue: [], execApprovalError: null, @@ -117,6 +135,7 @@ function createHost() { describe("connectGateway", () => { beforeEach(() => { gatewayClientInstances.length = 0; + loadChatHistoryMock.mockClear(); }); it("ignores stale client onGap callbacks after reconnect", () => { @@ -294,6 +313,73 @@ describe("connectGateway", () => { expect(host.lastError).toContain("gateway token mismatch"); expect(host.lastErrorCode).toBe("AUTH_TOKEN_MISMATCH"); }); + + it("does not reload chat history for each live tool result event", () => { + const host = createHost(); + + connectGateway(host); + const client = gatewayClientInstances[0]; + expect(client).toBeDefined(); + + client.emitEvent({ + event: "agent", + payload: { + runId: "engine-run-1", + seq: 1, + stream: "tool", + ts: 1, + sessionKey: "main", + data: { + toolCallId: "tool-1", + name: "fetch", + phase: "result", + result: { text: "ok" }, + }, + }, + }); + + expect(loadChatHistoryMock).not.toHaveBeenCalled(); + }); + + it("reloads chat history once after the final chat event when tool output was used", () => { + const host = createHost(); + + connectGateway(host); + const client = gatewayClientInstances[0]; + expect(client).toBeDefined(); + + client.emitEvent({ + event: "agent", + payload: { + runId: "engine-run-1", + seq: 1, + stream: "tool", + ts: 1, + sessionKey: "main", + data: { + toolCallId: "tool-1", + name: "fetch", + phase: "result", + result: { text: "ok" }, + }, + }, + }); + + client.emitEvent({ + event: "chat", + payload: { + runId: "engine-run-1", + sessionKey: "main", + state: "final", + message: { + role: "assistant", + content: [{ type: "text", text: "Done" }], + }, + }, + }); + + expect(loadChatHistoryMock).toHaveBeenCalledTimes(1); + }); }); describe("resolveControlUiClientVersion", () => { diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index ee761fe85e0..0cf39df0bc4 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -339,17 +339,6 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) { host as unknown as Parameters[0], evt.payload as AgentEventPayload | undefined, ); - // Reload history after each tool result so the persisted text + tool - // output replaces any truncated streaming fragments. - const agentPayload = evt.payload as AgentEventPayload | undefined; - const toolData = agentPayload?.data; - if ( - agentPayload?.stream === "tool" && - typeof toolData?.phase === "string" && - toolData.phase === "result" - ) { - void loadChatHistory(host as unknown as OpenClawApp); - } return; }