mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(ui): prevent duplicate assistant bubbles after history refresh (#126364)
* fix(ui): stabilize chat history dedupe identity * fix(ui): narrow chat history dedupe identity
This commit is contained in:
committed by
GitHub
parent
7e353d750a
commit
55fb2c8606
@@ -8,6 +8,7 @@ import type { ApplicationContext } from "../../app/context.ts";
|
||||
import type { SessionCapability } from "../../lib/sessions/index.ts";
|
||||
import "./chat-pane.ts";
|
||||
import { loadChatHistory } from "./chat-history.ts";
|
||||
import { nativeHistoryMessageIdentity } from "./chat-pane-shared.ts";
|
||||
import type { ChatPageHost } from "./chat-state-host.ts";
|
||||
|
||||
type TestChatPane = HTMLElement & {
|
||||
@@ -516,8 +517,14 @@ describe("chat pane native history pagination", () => {
|
||||
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
|
||||
const { pane } = createTestChatPane({ client, sessions: {} as SessionCapability });
|
||||
const projected = [
|
||||
nativeHistoryMessage(1, "tool call"),
|
||||
nativeHistoryMessage(1, "visible tool reply"),
|
||||
{
|
||||
...nativeHistoryMessage(1, "Same routed send"),
|
||||
openclawMessageToolMirror: { toolName: "message", toolCallId: "call-a" },
|
||||
},
|
||||
{
|
||||
...nativeHistoryMessage(1, "Same routed send"),
|
||||
openclawMessageToolMirror: { toolName: "message", toolCallId: "call-b" },
|
||||
},
|
||||
];
|
||||
|
||||
expect(pane.prependUniqueNativeMessages(projected, [nativeHistoryMessage(2)])).toEqual([
|
||||
@@ -530,6 +537,37 @@ describe("chat pane native history pagination", () => {
|
||||
).toEqual([projected[0], projected[1], nativeHistoryMessage(2)]);
|
||||
});
|
||||
|
||||
it("deduplicates byte-different live-event and history projections of one transcript row", () => {
|
||||
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
|
||||
const { pane } = createTestChatPane({ client, sessions: {} as SessionCapability });
|
||||
const liveEventProjection = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "One stored reply" }],
|
||||
__openclaw: {
|
||||
id: "assistant-message-42",
|
||||
idempotencyKey: "run-42",
|
||||
seq: 42,
|
||||
},
|
||||
};
|
||||
const historyProjection = {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "One stored reply" }],
|
||||
__openclaw: {
|
||||
id: "assistant-message-42",
|
||||
idempotencyKey: "run-42",
|
||||
recordTimestampMs: 1_786_000_000_000,
|
||||
seq: 42,
|
||||
},
|
||||
};
|
||||
|
||||
expect(nativeHistoryMessageIdentity(liveEventProjection)).toBe(
|
||||
nativeHistoryMessageIdentity(historyProjection),
|
||||
);
|
||||
expect(pane.prependUniqueNativeMessages([historyProjection], [liveEventProjection])).toEqual([
|
||||
liveEventProjection,
|
||||
]);
|
||||
});
|
||||
|
||||
it("deduplicates projected catalog transcript records by catalog message id", () => {
|
||||
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
|
||||
const { pane } = createTestChatPane({ client, sessions: {} as SessionCapability });
|
||||
|
||||
@@ -213,10 +213,12 @@ export function nativeHistoryMessageIdentity(message: unknown): string | null {
|
||||
if (!sourceIdentity) {
|
||||
return null;
|
||||
}
|
||||
const { recordTimestampMs: _recordTimestampMs, ...projectionMetadata } = metadata ?? {};
|
||||
const projection = metadata ? { ...record, __openclaw: projectionMetadata } : record;
|
||||
try {
|
||||
// One transcript record can project to multiple visible siblings. Include
|
||||
// the projection bytes so partial page overlap removes the matching sibling.
|
||||
return `${sourceIdentity}:${JSON.stringify(message)}`;
|
||||
// History alone adds recordTimestampMs; delivery metadata is not projection identity.
|
||||
// Keep every other projection byte so siblings from one transcript row stay distinct.
|
||||
return `${sourceIdentity}:${JSON.stringify(projection)}`;
|
||||
} catch {
|
||||
return sourceIdentity;
|
||||
}
|
||||
|
||||
@@ -2221,18 +2221,21 @@ describe("buildCachedChatItems", () => {
|
||||
expect(groupAt(groups, 0).messages).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("collapses consecutive duplicate text messages into one rendered item with a count", () => {
|
||||
it("collapses two distinct persisted rows with identical text into one rendered item", () => {
|
||||
const groups = messageGroups({
|
||||
messages: [
|
||||
assistantMessage([{ type: "text", text: "Same update" }], 1),
|
||||
assistantMessage([{ type: "text", text: "Same update" }], 2),
|
||||
assistantMessage([{ type: "text", text: "Same update" }], 3),
|
||||
assistantMessage([{ type: "text", text: "Same update" }], 1, {
|
||||
__openclaw: { seq: 7 },
|
||||
}),
|
||||
assistantMessage([{ type: "text", text: "Same update" }], 2, {
|
||||
__openclaw: { seq: 8 },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(groups).toHaveLength(1);
|
||||
expect(groupAt(groups, 0).messages).toHaveLength(1);
|
||||
expect(messageAt(groupAt(groups, 0), 0).duplicateCount).toBe(3);
|
||||
expect(messageAt(groupAt(groups, 0), 0).duplicateCount).toBe(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
Reference in New Issue
Block a user