fix(ui): stop dashboard chat history reload storm (#45541)

* UI: stop dashboard chat history reload storm

* Changelog: add PR number for chat reload fix

* fix: resolve branch typecheck regressions
This commit is contained in:
Val Alexander
2026-03-13 19:19:53 -05:00
committed by GitHub
parent 4f1195f5ab
commit 0e8672af87
14 changed files with 190 additions and 46 deletions

View File

@@ -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<typeof vi.fn>;
stop: ReturnType<typeof vi.fn>;
@@ -70,6 +72,14 @@ vi.mock("./gateway.ts", () => {
return { GatewayBrowserClient, resolveGatewayErrorDetailCode };
});
vi.mock("./controllers/chat.ts", async (importOriginal) => {
const actual = await importOriginal<typeof import("./controllers/chat.ts")>();
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<string>(),
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", () => {

View File

@@ -339,17 +339,6 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
host as unknown as Parameters<typeof handleAgentEvent>[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;
}