Skip to content

Commit 85e5d48

Browse files
authored
perf(control-ui): render chat history incrementally
Render dashboard chat history incrementally; preserve Talk settings callback contracts, native Talk select labels, and raw-copy baseline after rebase.
1 parent b6cee3f commit 85e5d48

8 files changed

Lines changed: 671 additions & 74 deletions

File tree

ui/src/i18n/.i18n/raw-copy-baseline.json

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/src/styles/chat/layout.css

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,8 @@
10141014
color: var(--muted);
10151015
}
10161016

1017-
.agent-chat__talk-options input {
1017+
.agent-chat__talk-options input,
1018+
.agent-chat__talk-options select {
10181019
width: 100%;
10191020
min-width: 0;
10201021
height: 34px;
@@ -1029,7 +1030,16 @@
10291030
box-sizing: border-box;
10301031
}
10311032

1032-
.agent-chat__talk-options input:focus {
1033+
.agent-chat__talk-options select {
1034+
appearance: none;
1035+
padding-right: 26px;
1036+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
1037+
background-position: right 8px center;
1038+
background-repeat: no-repeat;
1039+
}
1040+
1041+
.agent-chat__talk-options input:focus,
1042+
.agent-chat__talk-options select:focus {
10331043
outline: none;
10341044
box-shadow: var(--focus-ring);
10351045
}

ui/src/ui/app.talk.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,29 @@ describe("OpenClawApp Talk controls", () => {
152152
expect(app.chatError).toBe("voice provider missing");
153153
expect(stopMock).toHaveBeenCalledOnce();
154154
});
155+
156+
it("keeps the Talk options toggle inside the open-panel click guard", async () => {
157+
await import("./app.ts");
158+
const app = document.createElement("openclaw-app");
159+
const guardHost = app as unknown as {
160+
chatMobileControlsPointerdownHandler: (event: Event) => void;
161+
realtimeTalkOptionsOpen: boolean;
162+
};
163+
const toggle = document.createElement("button");
164+
toggle.setAttribute("aria-label", "Talk options");
165+
app.append(toggle);
166+
167+
guardHost.realtimeTalkOptionsOpen = true;
168+
guardHost.chatMobileControlsPointerdownHandler({
169+
composedPath: () => [toggle, app, document, window],
170+
} as unknown as Event);
171+
172+
expect(guardHost.realtimeTalkOptionsOpen).toBe(true);
173+
174+
guardHost.chatMobileControlsPointerdownHandler({
175+
composedPath: () => [document, window],
176+
} as unknown as Event);
177+
178+
expect(guardHost.realtimeTalkOptionsOpen).toBe(false);
179+
});
155180
});

ui/src/ui/app.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,9 @@ export class OpenClawApp extends LitElement {
760760
});
761761
if (this.realtimeTalkOptionsOpen) {
762762
const insideTalkOptions = Array.from(
763-
this.querySelectorAll(".agent-chat__talk-options, [aria-label='Talk settings']"),
763+
this.querySelectorAll(
764+
".agent-chat__talk-options, [aria-label='Talk settings'], [aria-label='Talk options']",
765+
),
764766
).some((node) => path.includes(node));
765767
if (!insideTalkOptions) {
766768
this.realtimeTalkOptionsOpen = false;

ui/src/ui/chat/build-chat-items.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,27 @@ describe("buildChatItems", () => {
260260
expect(messageRecord(groups[groups.length - 1]).content).toBe("message 104");
261261
});
262262

263+
it("honors a smaller history render window and preserves the hidden-count notice", () => {
264+
const items = buildChatItems(
265+
createProps({
266+
historyRenderLimit: 30,
267+
messages: Array.from({ length: 105 }, (_, index) => ({
268+
role: index % 2 === 0 ? "user" : "assistant",
269+
content: `message ${index}`,
270+
timestamp: index,
271+
})),
272+
}),
273+
);
274+
275+
const groups = items.filter((item) => item.kind === "group");
276+
277+
const noticeGroup = requireGroup(items[0]);
278+
expect(messageRecord(noticeGroup).content).toBe("Showing last 30 messages (75 hidden).");
279+
expect(groups).toHaveLength(31);
280+
expect(messageRecord(groups[1]).content).toBe("message 75");
281+
expect(messageRecord(groups[groups.length - 1]).content).toBe("message 104");
282+
});
283+
263284
it("budgets rendered history by tool-result content size", () => {
264285
const largeOutput = "x".repeat(100_000);
265286
const items = buildChatItems(

ui/src/ui/chat/build-chat-items.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type BuildChatItemsProps = {
2323
showToolCalls: boolean;
2424
searchOpen?: boolean;
2525
searchQuery?: string;
26+
historyRenderLimit?: number;
2627
};
2728

2829
function appendCanvasBlockToAssistantMessage(
@@ -468,7 +469,18 @@ function countVisibleHistoryMessages(messages: unknown[], showToolCalls: boolean
468469
return count;
469470
}
470471

471-
function resolveHistoryStartIndex(messages: unknown[], showToolCalls: boolean): number {
472+
function resolveHistoryRenderLimit(limit: number | undefined): number {
473+
if (typeof limit !== "number" || !Number.isFinite(limit)) {
474+
return CHAT_HISTORY_RENDER_LIMIT;
475+
}
476+
return Math.max(1, Math.min(CHAT_HISTORY_RENDER_LIMIT, Math.floor(limit)));
477+
}
478+
479+
function resolveHistoryStartIndex(
480+
messages: unknown[],
481+
showToolCalls: boolean,
482+
renderLimit: number,
483+
): number {
472484
let visibleCount = 0;
473485
let renderChars = 0;
474486
let startIndex = messages.length;
@@ -477,7 +489,7 @@ function resolveHistoryStartIndex(messages: unknown[], showToolCalls: boolean):
477489
if (isHiddenToolMessage(message, showToolCalls)) {
478490
continue;
479491
}
480-
if (visibleCount >= CHAT_HISTORY_RENDER_LIMIT) {
492+
if (visibleCount >= renderLimit) {
481493
break;
482494
}
483495
const remainingBudget = Math.max(1, CHAT_HISTORY_RENDER_CHAR_BUDGET - renderChars + 1);
@@ -494,6 +506,7 @@ function resolveHistoryStartIndex(messages: unknown[], showToolCalls: boolean):
494506

495507
export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | MessageGroup> {
496508
let items: ChatItem[] = [];
509+
const historyRenderLimit = resolveHistoryRenderLimit(props.historyRenderLimit);
497510
const history = (Array.isArray(props.messages) ? props.messages : []).filter(
498511
(message) => !isAssistantHeartbeatAckForDisplay(message),
499512
);
@@ -505,7 +518,7 @@ export function buildChatItems(props: BuildChatItemsProps): Array<ChatItem | Mes
505518
text: string | null;
506519
timestamp: number | null;
507520
}>;
508-
const historyStart = resolveHistoryStartIndex(history, props.showToolCalls);
521+
const historyStart = resolveHistoryStartIndex(history, props.showToolCalls, historyRenderLimit);
509522
const hiddenHistoryCount = countVisibleHistoryMessages(
510523
history.slice(0, historyStart),
511524
props.showToolCalls,

0 commit comments

Comments
 (0)