Web UI: show Compaction divider in chat history (#11341)

This commit is contained in:
Tak Hoffman
2026-02-07 12:37:22 -06:00
committed by GitHub
parent f0722498a4
commit 82419eaad6
5 changed files with 117 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
readFirstUserMessageFromTranscript,
readLastMessagePreviewFromTranscript,
readSessionMessages,
readSessionPreviewItemsFromTranscript,
} from "./session-utils.fs.js";
@@ -343,6 +344,53 @@ describe("readLastMessagePreviewFromTranscript", () => {
});
});
describe("readSessionMessages", () => {
let tmpDir: string;
let storePath: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-fs-test-"));
storePath = path.join(tmpDir, "sessions.json");
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test("includes synthetic compaction markers for compaction entries", () => {
const sessionId = "test-session-compaction";
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
const lines = [
JSON.stringify({ type: "session", version: 1, id: sessionId }),
JSON.stringify({ message: { role: "user", content: "Hello" } }),
JSON.stringify({
type: "compaction",
id: "comp-1",
timestamp: "2026-02-07T00:00:00.000Z",
summary: "Compacted history",
firstKeptEntryId: "x",
tokensBefore: 123,
}),
JSON.stringify({ message: { role: "assistant", content: "World" } }),
];
fs.writeFileSync(transcriptPath, lines.join("\n"), "utf-8");
const out = readSessionMessages(sessionId, storePath);
expect(out).toHaveLength(3);
const marker = out[1] as {
role: string;
content?: Array<{ text?: string }>;
__openclaw?: { kind?: string; id?: string };
timestamp?: number;
};
expect(marker.role).toBe("system");
expect(marker.content?.[0]?.text).toBe("Compaction");
expect(marker.__openclaw?.kind).toBe("compaction");
expect(marker.__openclaw?.id).toBe("comp-1");
expect(typeof marker.timestamp).toBe("number");
});
});
describe("readSessionPreviewItemsFromTranscript", () => {
let tmpDir: string;
let storePath: string;

View File

@@ -28,6 +28,23 @@ export function readSessionMessages(
const parsed = JSON.parse(line);
if (parsed?.message) {
messages.push(parsed.message);
continue;
}
// Compaction entries are not "message" records, but they're useful context for debugging.
// Emit a lightweight synthetic message that the Web UI can render as a divider.
if (parsed?.type === "compaction") {
const ts = typeof parsed.timestamp === "string" ? Date.parse(parsed.timestamp) : Number.NaN;
const timestamp = Number.isFinite(ts) ? ts : Date.now();
messages.push({
role: "system",
content: [{ type: "text", text: "Compaction" }],
timestamp,
__openclaw: {
kind: "compaction",
id: typeof parsed.id === "string" ? parsed.id : undefined,
},
});
}
} catch {
// ignore bad lines

View File

@@ -54,6 +54,33 @@
opacity: 0.7;
}
/* Chat divider (e.g., compaction marker) */
.chat-divider {
display: flex;
align-items: center;
gap: 10px;
margin: 18px 8px;
color: var(--muted);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
user-select: none;
}
.chat-divider__line {
flex: 1 1 0;
height: 1px;
background: var(--border);
opacity: 0.9;
}
.chat-divider__label {
padding: 2px 10px;
border: 1px solid var(--border);
border-radius: 999px;
background: rgba(255, 255, 255, 0.02);
}
/* Avatar Styles */
.chat-avatar {
width: 40px;

View File

@@ -5,6 +5,7 @@
/** Union type for items in the chat thread */
export type ChatItem =
| { kind: "message"; key: string; message: unknown }
| { kind: "divider"; key: string; label: string; timestamp: number }
| { kind: "stream"; key: string; text: string; startedAt: number }
| { kind: "reading-indicator"; key: string };

View File

@@ -224,6 +224,16 @@ export function renderChat(props: ChatProps) {
buildChatItems(props),
(item) => item.key,
(item) => {
if (item.kind === "divider") {
return html`
<div class="chat-divider" role="separator" data-ts=${String(item.timestamp)}>
<span class="chat-divider__line"></span>
<span class="chat-divider__label">${item.label}</span>
<span class="chat-divider__line"></span>
</div>
`;
}
if (item.kind === "reading-indicator") {
return renderReadingIndicatorGroup(assistantIdentity);
}
@@ -477,6 +487,20 @@ function buildChatItems(props: ChatProps): Array<ChatItem | MessageGroup> {
for (let i = historyStart; i < history.length; i++) {
const msg = history[i];
const normalized = normalizeMessage(msg);
const raw = msg as Record<string, unknown>;
const marker = raw.__openclaw as Record<string, unknown> | undefined;
if (marker && marker.kind === "compaction") {
items.push({
kind: "divider",
key:
typeof marker.id === "string"
? `divider:compaction:${marker.id}`
: `divider:compaction:${normalized.timestamp}:${i}`,
label: "Compaction",
timestamp: normalized.timestamp ?? Date.now(),
});
continue;
}
if (!props.showThinking && normalized.role.toLowerCase() === "toolresult") {
continue;