mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-05 17:52:58 +00:00
perf(ui): guard chat transcript rerenders
Reduce Control UI draft-update work by guarding transcript group rendering while preserving assistant attachment availability invalidation. Verification: focused UI tests, format/lint/typecheck, autoreview clean, and changed gate tbx_01kt11qyc20ejbsbt8kd79bamx.
This commit is contained in:
@@ -44,6 +44,7 @@ const assistantAttachmentAvailabilityCache = new Map<string, AssistantAttachment
|
||||
const assistantAttachmentRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
const ASSISTANT_ATTACHMENT_UNAVAILABLE_RETRY_MS = 5_000;
|
||||
const ASSISTANT_ATTACHMENT_MEDIA_TICKET_REFRESH_SKEW_MS = 30_000;
|
||||
let assistantAttachmentAvailabilityRenderVersion = 0;
|
||||
|
||||
export type ChatTimestampDisplay = {
|
||||
label: string;
|
||||
@@ -94,6 +95,7 @@ function renderChatTimestamp(timestamp: number) {
|
||||
|
||||
export function resetAssistantAttachmentAvailabilityCacheForTest() {
|
||||
assistantAttachmentAvailabilityCache.clear();
|
||||
bumpAssistantAttachmentAvailabilityRenderVersion();
|
||||
for (const timer of assistantAttachmentRefreshTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
@@ -106,6 +108,29 @@ export function resetAssistantAttachmentAvailabilityCacheForTest() {
|
||||
managedImageBlobUrlMissCache.clear();
|
||||
}
|
||||
|
||||
export function getAssistantAttachmentAvailabilityRenderVersion(): number {
|
||||
return assistantAttachmentAvailabilityRenderVersion;
|
||||
}
|
||||
|
||||
function bumpAssistantAttachmentAvailabilityRenderVersion() {
|
||||
assistantAttachmentAvailabilityRenderVersion =
|
||||
(assistantAttachmentAvailabilityRenderVersion + 1) % Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function setAssistantAttachmentAvailability(
|
||||
cacheKey: string,
|
||||
availability: AssistantAttachmentAvailability,
|
||||
) {
|
||||
assistantAttachmentAvailabilityCache.set(cacheKey, availability);
|
||||
bumpAssistantAttachmentAvailabilityRenderVersion();
|
||||
}
|
||||
|
||||
function deleteAssistantAttachmentAvailability(cacheKey: string) {
|
||||
if (assistantAttachmentAvailabilityCache.delete(cacheKey)) {
|
||||
bumpAssistantAttachmentAvailabilityRenderVersion();
|
||||
}
|
||||
}
|
||||
|
||||
type ImageBlock = {
|
||||
url: string;
|
||||
openUrl?: string;
|
||||
@@ -1218,7 +1243,7 @@ function scheduleAssistantAttachmentRefresh(
|
||||
if (cached?.status !== "available" || cached.mediaTicket !== availability.mediaTicket) {
|
||||
return;
|
||||
}
|
||||
assistantAttachmentAvailabilityCache.delete(cacheKey);
|
||||
deleteAssistantAttachmentAvailability(cacheKey);
|
||||
onRequestUpdate();
|
||||
}, refreshInMs);
|
||||
assistantAttachmentRefreshTimers.set(cacheKey, timer);
|
||||
@@ -1246,21 +1271,21 @@ function resolveAssistantAttachmentAvailability(
|
||||
cached.status === "unavailable" &&
|
||||
now - cached.checkedAt >= ASSISTANT_ATTACHMENT_UNAVAILABLE_RETRY_MS
|
||||
) {
|
||||
assistantAttachmentAvailabilityCache.delete(cacheKey);
|
||||
deleteAssistantAttachmentAvailability(cacheKey);
|
||||
} else if (
|
||||
cached.status === "available" &&
|
||||
cached.mediaTicket &&
|
||||
(!cached.mediaTicketExpiresAt ||
|
||||
cached.mediaTicketExpiresAt - now <= ASSISTANT_ATTACHMENT_MEDIA_TICKET_REFRESH_SKEW_MS)
|
||||
) {
|
||||
assistantAttachmentAvailabilityCache.delete(cacheKey);
|
||||
deleteAssistantAttachmentAvailability(cacheKey);
|
||||
} else {
|
||||
scheduleAssistantAttachmentRefresh(cacheKey, cached, onRequestUpdate);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
clearAssistantAttachmentRefreshTimer(cacheKey);
|
||||
assistantAttachmentAvailabilityCache.set(cacheKey, { status: "checking" });
|
||||
setAssistantAttachmentAvailability(cacheKey, { status: "checking" });
|
||||
if (typeof fetch === "function") {
|
||||
const headers = new Headers({ Accept: "application/json" });
|
||||
if (normalizedAuthToken) {
|
||||
@@ -1283,7 +1308,7 @@ function resolveAssistantAttachmentAvailability(
|
||||
const mediaTicketExpiresAt = Date.parse(payload.mediaTicketExpiresAt ?? "");
|
||||
if (mediaTicket && !Number.isFinite(mediaTicketExpiresAt)) {
|
||||
clearAssistantAttachmentRefreshTimer(cacheKey);
|
||||
assistantAttachmentAvailabilityCache.set(cacheKey, {
|
||||
setAssistantAttachmentAvailability(cacheKey, {
|
||||
status: "unavailable",
|
||||
reason: "Attachment unavailable",
|
||||
checkedAt: Date.now(),
|
||||
@@ -1294,11 +1319,11 @@ function resolveAssistantAttachmentAvailability(
|
||||
status: "available",
|
||||
...(mediaTicket ? { mediaTicket, mediaTicketExpiresAt } : {}),
|
||||
};
|
||||
assistantAttachmentAvailabilityCache.set(cacheKey, availability);
|
||||
setAssistantAttachmentAvailability(cacheKey, availability);
|
||||
scheduleAssistantAttachmentRefresh(cacheKey, availability, onRequestUpdate);
|
||||
} else {
|
||||
clearAssistantAttachmentRefreshTimer(cacheKey);
|
||||
assistantAttachmentAvailabilityCache.set(cacheKey, {
|
||||
setAssistantAttachmentAvailability(cacheKey, {
|
||||
status: "unavailable",
|
||||
reason: payload?.reason?.trim() || "Attachment unavailable",
|
||||
checkedAt: Date.now(),
|
||||
@@ -1307,7 +1332,7 @@ function resolveAssistantAttachmentAvailability(
|
||||
})
|
||||
.catch(() => {
|
||||
clearAssistantAttachmentRefreshTimer(cacheKey);
|
||||
assistantAttachmentAvailabilityCache.set(cacheKey, {
|
||||
setAssistantAttachmentAvailability(cacheKey, {
|
||||
status: "unavailable",
|
||||
reason: "Attachment unavailable",
|
||||
checkedAt: Date.now(),
|
||||
|
||||
@@ -100,6 +100,26 @@ const buildChatItemsMock = vi.hoisted(() =>
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
const renderMessageGroupMock = vi.hoisted(() =>
|
||||
vi.fn((group: { messages: Array<{ message: unknown }> }) => {
|
||||
const element = document.createElement("div");
|
||||
element.className = "chat-group";
|
||||
element.textContent = group.messages
|
||||
.map(({ message }) => {
|
||||
if (typeof message === "object" && message !== null && "content" in message) {
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
return content == null ? "" : JSON.stringify(content);
|
||||
}
|
||||
return String(message);
|
||||
})
|
||||
.join("\n");
|
||||
return element;
|
||||
}),
|
||||
);
|
||||
const assistantAttachmentRenderVersionMock = vi.hoisted(() => ({ value: 0 }));
|
||||
|
||||
function requireFirstAttachmentsChange(
|
||||
onAttachmentsChange: ReturnType<typeof vi.fn>,
|
||||
@@ -124,23 +144,8 @@ vi.mock("../chat/build-chat-items.ts", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../chat/grouped-render.ts", () => ({
|
||||
renderMessageGroup: (group: { messages: Array<{ message: unknown }> }) => {
|
||||
const element = document.createElement("div");
|
||||
element.className = "chat-group";
|
||||
element.textContent = group.messages
|
||||
.map(({ message }) => {
|
||||
if (typeof message === "object" && message !== null && "content" in message) {
|
||||
const content = (message as { content?: unknown }).content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
return content == null ? "" : JSON.stringify(content);
|
||||
}
|
||||
return String(message);
|
||||
})
|
||||
.join("\n");
|
||||
return element;
|
||||
},
|
||||
getAssistantAttachmentAvailabilityRenderVersion: () => assistantAttachmentRenderVersionMock.value,
|
||||
renderMessageGroup: renderMessageGroupMock,
|
||||
renderReadingIndicatorGroup: () => {
|
||||
const element = document.createElement("div");
|
||||
element.className = "chat-reading-indicator";
|
||||
@@ -490,84 +495,87 @@ function clickTalkSelectOption(container: Element, name: string, value: string):
|
||||
option.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
function createChatProps(
|
||||
overrides: Partial<Parameters<typeof renderChat>[0]> = {},
|
||||
): Parameters<typeof renderChat>[0] {
|
||||
return {
|
||||
sessionKey: "main",
|
||||
onSessionKeyChange: () => undefined,
|
||||
thinkingLevel: null,
|
||||
showThinking: false,
|
||||
showToolCalls: true,
|
||||
loading: false,
|
||||
sending: false,
|
||||
compactionStatus: null,
|
||||
fallbackStatus: null,
|
||||
messages: [],
|
||||
sideResult: null,
|
||||
toolMessages: [],
|
||||
streamSegments: [],
|
||||
stream: null,
|
||||
streamStartedAt: null,
|
||||
assistantAvatarUrl: null,
|
||||
draft: "",
|
||||
queue: [],
|
||||
realtimeTalkActive: false,
|
||||
realtimeTalkStatus: "idle",
|
||||
realtimeTalkDetail: null,
|
||||
realtimeTalkTranscript: null,
|
||||
connected: true,
|
||||
canSend: true,
|
||||
disabledReason: null,
|
||||
error: null,
|
||||
sessions: null,
|
||||
sidebarOpen: false,
|
||||
sidebarContent: null,
|
||||
sidebarError: null,
|
||||
splitRatio: 0.6,
|
||||
canvasPluginSurfaceUrl: null,
|
||||
embedSandboxMode: "scripts",
|
||||
allowExternalEmbedUrls: false,
|
||||
assistantName: "Val",
|
||||
assistantAvatar: null,
|
||||
userName: null,
|
||||
userAvatar: null,
|
||||
localMediaPreviewRoots: [],
|
||||
assistantAttachmentAuthToken: null,
|
||||
autoExpandToolCalls: false,
|
||||
attachments: [],
|
||||
onAttachmentsChange: () => undefined,
|
||||
showNewMessages: false,
|
||||
onScrollToBottom: () => undefined,
|
||||
onRefresh: () => undefined,
|
||||
getDraft: () => "",
|
||||
onDraftChange: () => undefined,
|
||||
onRequestUpdate: () => undefined,
|
||||
onSend: () => undefined,
|
||||
onCompact: () => undefined,
|
||||
onToggleRealtimeTalk: () => undefined,
|
||||
onDismissError: () => undefined,
|
||||
onAbort: () => undefined,
|
||||
onQueueRemove: () => undefined,
|
||||
onQueueSteer: () => undefined,
|
||||
onDismissSideResult: () => undefined,
|
||||
onNewSession: () => undefined,
|
||||
onClearHistory: () => undefined,
|
||||
onOpenSessionCheckpoints: () => undefined,
|
||||
agentsList: null,
|
||||
currentAgentId: "main",
|
||||
onAgentChange: () => undefined,
|
||||
onNavigateToAgent: () => undefined,
|
||||
onSessionSelect: () => undefined,
|
||||
onOpenSidebar: () => undefined,
|
||||
onCloseSidebar: () => undefined,
|
||||
onSplitRatioChange: () => undefined,
|
||||
onChatScroll: () => undefined,
|
||||
basePath: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderChatView(overrides: Partial<Parameters<typeof renderChat>[0]> = {}) {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChat({
|
||||
sessionKey: "main",
|
||||
onSessionKeyChange: () => undefined,
|
||||
thinkingLevel: null,
|
||||
showThinking: false,
|
||||
showToolCalls: true,
|
||||
loading: false,
|
||||
sending: false,
|
||||
compactionStatus: null,
|
||||
fallbackStatus: null,
|
||||
messages: [],
|
||||
sideResult: null,
|
||||
toolMessages: [],
|
||||
streamSegments: [],
|
||||
stream: null,
|
||||
streamStartedAt: null,
|
||||
assistantAvatarUrl: null,
|
||||
draft: "",
|
||||
queue: [],
|
||||
realtimeTalkActive: false,
|
||||
realtimeTalkStatus: "idle",
|
||||
realtimeTalkDetail: null,
|
||||
realtimeTalkTranscript: null,
|
||||
connected: true,
|
||||
canSend: true,
|
||||
disabledReason: null,
|
||||
error: null,
|
||||
sessions: null,
|
||||
sidebarOpen: false,
|
||||
sidebarContent: null,
|
||||
sidebarError: null,
|
||||
splitRatio: 0.6,
|
||||
canvasPluginSurfaceUrl: null,
|
||||
embedSandboxMode: "scripts",
|
||||
allowExternalEmbedUrls: false,
|
||||
assistantName: "Val",
|
||||
assistantAvatar: null,
|
||||
userName: null,
|
||||
userAvatar: null,
|
||||
localMediaPreviewRoots: [],
|
||||
assistantAttachmentAuthToken: null,
|
||||
autoExpandToolCalls: false,
|
||||
attachments: [],
|
||||
onAttachmentsChange: () => undefined,
|
||||
showNewMessages: false,
|
||||
onScrollToBottom: () => undefined,
|
||||
onRefresh: () => undefined,
|
||||
getDraft: () => "",
|
||||
onDraftChange: () => undefined,
|
||||
onRequestUpdate: () => undefined,
|
||||
onSend: () => undefined,
|
||||
onCompact: () => undefined,
|
||||
onToggleRealtimeTalk: () => undefined,
|
||||
onDismissError: () => undefined,
|
||||
onAbort: () => undefined,
|
||||
onQueueRemove: () => undefined,
|
||||
onQueueSteer: () => undefined,
|
||||
onDismissSideResult: () => undefined,
|
||||
onNewSession: () => undefined,
|
||||
onClearHistory: () => undefined,
|
||||
onOpenSessionCheckpoints: () => undefined,
|
||||
agentsList: null,
|
||||
currentAgentId: "main",
|
||||
onAgentChange: () => undefined,
|
||||
onNavigateToAgent: () => undefined,
|
||||
onSessionSelect: () => undefined,
|
||||
onOpenSidebar: () => undefined,
|
||||
onCloseSidebar: () => undefined,
|
||||
onSplitRatioChange: () => undefined,
|
||||
onChatScroll: () => undefined,
|
||||
basePath: "",
|
||||
...overrides,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
render(renderChat(createChatProps(overrides)), container);
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -673,6 +681,8 @@ describe("chat composer workbench", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
buildChatItemsMock.mockClear();
|
||||
renderMessageGroupMock.mockClear();
|
||||
assistantAttachmentRenderVersionMock.value = 0;
|
||||
loadSessionsMock.mockClear();
|
||||
refreshVisibleToolsEffectiveForCurrentSessionMock.mockClear();
|
||||
resetChatViewState();
|
||||
@@ -694,6 +704,51 @@ describe("chat transcript rendering cache", () => {
|
||||
expect(buildChatItemsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not rerender transcript groups for draft-only rerenders", () => {
|
||||
const messages = [{ role: "assistant", content: "ready" }];
|
||||
const toolMessages: unknown[] = [];
|
||||
const streamSegments: Array<{ text: string; ts: number }> = [];
|
||||
const queue: ChatQueueItem[] = [];
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderChat(createChatProps({ messages, toolMessages, streamSegments, queue })),
|
||||
container,
|
||||
);
|
||||
render(
|
||||
renderChat(createChatProps({ messages, toolMessages, streamSegments, queue, draft: "h" })),
|
||||
container,
|
||||
);
|
||||
render(
|
||||
renderChat(
|
||||
createChatProps({ messages, toolMessages, streamSegments, queue, draft: "hello" }),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(renderMessageGroupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rerenders transcript groups when assistant attachment availability changes", () => {
|
||||
const messages = [{ role: "assistant", content: "ready" }];
|
||||
const toolMessages: unknown[] = [];
|
||||
const streamSegments: Array<{ text: string; ts: number }> = [];
|
||||
const queue: ChatQueueItem[] = [];
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderChat(createChatProps({ messages, toolMessages, streamSegments, queue })),
|
||||
container,
|
||||
);
|
||||
assistantAttachmentRenderVersionMock.value += 1;
|
||||
render(
|
||||
renderChat(createChatProps({ messages, toolMessages, streamSegments, queue, draft: "h" })),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(renderMessageGroupMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("rebuilds transcript items when the transcript reference changes", () => {
|
||||
const toolMessages: unknown[] = [];
|
||||
const streamSegments: Array<{ text: string; ts: number }> = [];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { ref } from "lit/directives/ref.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
@@ -21,6 +22,7 @@ import { renderContextNotice } from "../chat/context-notice.ts";
|
||||
import { DeletedMessages } from "../chat/deleted-messages.ts";
|
||||
import { exportChatMarkdown } from "../chat/export.ts";
|
||||
import {
|
||||
getAssistantAttachmentAvailabilityRenderVersion,
|
||||
renderMessageGroup,
|
||||
renderReadingIndicatorGroup,
|
||||
renderStreamingGroup,
|
||||
@@ -522,6 +524,27 @@ function buildCachedChatItems(input: BuildChatItemsProps): ReturnType<typeof bui
|
||||
return items;
|
||||
}
|
||||
|
||||
function deletedChatItemsSignature(
|
||||
deleted: DeletedMessages,
|
||||
chatItems: ReturnType<typeof buildChatItems>,
|
||||
): string {
|
||||
const deletedKeys = chatItems
|
||||
.map((item) => item.key)
|
||||
.filter((key) => deleted.has(key))
|
||||
.toSorted();
|
||||
return deletedKeys.length === 0 ? "" : deletedKeys.join("\u0000");
|
||||
}
|
||||
|
||||
function stableBooleanMapSignature(values: ReadonlyMap<string, boolean>): string {
|
||||
if (values.size === 0) {
|
||||
return "";
|
||||
}
|
||||
return Array.from(values)
|
||||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, value]) => `${key}:${value ? "1" : "0"}`)
|
||||
.join("\u0000");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset chat view ephemeral state when navigating away.
|
||||
* Clears search/slash UI that should not survive navigation.
|
||||
@@ -1377,6 +1400,8 @@ export function renderChat(props: ChatProps) {
|
||||
const hasRealtimeTalkConversation = (props.realtimeTalkConversation?.length ?? 0) > 0;
|
||||
const isEmpty = chatItems.length === 0 && !props.loading && !hasRealtimeTalkConversation;
|
||||
const showLoadingSkeleton = props.loading && chatItems.length === 0;
|
||||
const threadContextWindow =
|
||||
activeSession?.contextTokens ?? props.sessions?.defaults?.contextTokens ?? null;
|
||||
|
||||
const thread = html`
|
||||
<div
|
||||
@@ -1430,104 +1455,129 @@ export function renderChat(props: ChatProps) {
|
||||
${isEmpty && vs.searchOpen
|
||||
? html` <div class="agent-chat__empty">No matching messages</div> `
|
||||
: nothing}
|
||||
${repeat(
|
||||
chatItems,
|
||||
(item) => item.key,
|
||||
(item) => {
|
||||
if (item.kind === "divider") {
|
||||
return html`
|
||||
<div class="chat-divider" data-ts=${String(item.timestamp)}>
|
||||
<div class="chat-divider__rule" role="separator" aria-label=${item.label}>
|
||||
<span class="chat-divider__line"></span>
|
||||
<span class="chat-divider__label">${item.label}</span>
|
||||
<span class="chat-divider__line"></span>
|
||||
</div>
|
||||
${item.description || item.action
|
||||
? html`
|
||||
<div class="chat-divider__details">
|
||||
${item.description
|
||||
? html`<span class="chat-divider__description">
|
||||
${item.description}
|
||||
</span>`
|
||||
: nothing}
|
||||
${item.action?.kind === "session-checkpoints" &&
|
||||
props.onOpenSessionCheckpoints
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--subtle btn--sm chat-divider__action"
|
||||
@click=${() => props.onOpenSessionCheckpoints?.()}
|
||||
>
|
||||
${item.action.label}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
props.assistantAttachmentAuthToken ?? null,
|
||||
);
|
||||
}
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
item.isStreaming,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
props.assistantAttachmentAuthToken ?? null,
|
||||
);
|
||||
}
|
||||
if (item.kind === "group") {
|
||||
if (deleted.has(item.key)) {
|
||||
return nothing;
|
||||
}
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
sessionKey: props.sessionKey,
|
||||
agentId: props.fullMessageAgentId,
|
||||
showReasoning,
|
||||
showToolCalls: props.showToolCalls,
|
||||
autoExpandToolCalls: Boolean(props.autoExpandToolCalls),
|
||||
isToolMessageExpanded: (messageId: string) => expandedToolCards.get(messageId),
|
||||
onToggleToolMessageExpanded: (messageId: string, expanded?: boolean) => {
|
||||
expandedToolCards.set(
|
||||
messageId,
|
||||
!(expanded ?? expandedToolCards.get(messageId) ?? false),
|
||||
${guard(
|
||||
[
|
||||
chatItems,
|
||||
deletedChatItemsSignature(deleted, chatItems),
|
||||
stableBooleanMapSignature(expandedToolCards),
|
||||
getAssistantAttachmentAvailabilityRenderVersion(),
|
||||
props.sessionKey,
|
||||
props.fullMessageAgentId,
|
||||
showReasoning,
|
||||
props.showToolCalls,
|
||||
Boolean(props.autoExpandToolCalls),
|
||||
props.assistantName,
|
||||
assistantIdentity.avatar,
|
||||
props.userName,
|
||||
props.userAvatar,
|
||||
props.basePath,
|
||||
(props.localMediaPreviewRoots ?? []).join("\u0000"),
|
||||
props.assistantAttachmentAuthToken,
|
||||
props.canvasPluginSurfaceUrl,
|
||||
props.embedSandboxMode ?? "scripts",
|
||||
props.allowExternalEmbedUrls ?? false,
|
||||
threadContextWindow,
|
||||
],
|
||||
() =>
|
||||
repeat(
|
||||
chatItems,
|
||||
(item) => item.key,
|
||||
(item) => {
|
||||
if (item.kind === "divider") {
|
||||
return html`
|
||||
<div class="chat-divider" data-ts=${String(item.timestamp)}>
|
||||
<div class="chat-divider__rule" role="separator" aria-label=${item.label}>
|
||||
<span class="chat-divider__line"></span>
|
||||
<span class="chat-divider__label">${item.label}</span>
|
||||
<span class="chat-divider__line"></span>
|
||||
</div>
|
||||
${item.description || item.action
|
||||
? html`
|
||||
<div class="chat-divider__details">
|
||||
${item.description
|
||||
? html`<span class="chat-divider__description">
|
||||
${item.description}
|
||||
</span>`
|
||||
: nothing}
|
||||
${item.action?.kind === "session-checkpoints" &&
|
||||
props.onOpenSessionCheckpoints
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--subtle btn--sm chat-divider__action"
|
||||
@click=${() => props.onOpenSessionCheckpoints?.()}
|
||||
>
|
||||
${item.action.label}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
props.assistantAttachmentAuthToken ?? null,
|
||||
);
|
||||
requestUpdate();
|
||||
},
|
||||
isToolExpanded: (toolCardId: string) => expandedToolCards.get(toolCardId) ?? false,
|
||||
onToggleToolExpanded: toggleToolCardExpanded,
|
||||
onRequestUpdate: requestUpdate,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
userName: props.userName ?? null,
|
||||
userAvatar: props.userAvatar ?? null,
|
||||
basePath: props.basePath,
|
||||
localMediaPreviewRoots: props.localMediaPreviewRoots ?? [],
|
||||
assistantAttachmentAuthToken: props.assistantAttachmentAuthToken ?? null,
|
||||
canvasPluginSurfaceUrl: props.canvasPluginSurfaceUrl,
|
||||
embedSandboxMode: props.embedSandboxMode ?? "scripts",
|
||||
allowExternalEmbedUrls: props.allowExternalEmbedUrls ?? false,
|
||||
contextWindow:
|
||||
activeSession?.contextTokens ?? props.sessions?.defaults?.contextTokens ?? null,
|
||||
onDelete: () => {
|
||||
deleted.delete(item.key);
|
||||
requestUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
return nothing;
|
||||
},
|
||||
}
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
item.isStreaming,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
props.assistantAttachmentAuthToken ?? null,
|
||||
);
|
||||
}
|
||||
if (item.kind === "group") {
|
||||
if (deleted.has(item.key)) {
|
||||
return nothing;
|
||||
}
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
sessionKey: props.sessionKey,
|
||||
agentId: props.fullMessageAgentId,
|
||||
showReasoning,
|
||||
showToolCalls: props.showToolCalls,
|
||||
autoExpandToolCalls: Boolean(props.autoExpandToolCalls),
|
||||
isToolMessageExpanded: (messageId: string) => expandedToolCards.get(messageId),
|
||||
onToggleToolMessageExpanded: (messageId: string, expanded?: boolean) => {
|
||||
expandedToolCards.set(
|
||||
messageId,
|
||||
!(expanded ?? expandedToolCards.get(messageId) ?? false),
|
||||
);
|
||||
requestUpdate();
|
||||
},
|
||||
isToolExpanded: (toolCardId: string) =>
|
||||
expandedToolCards.get(toolCardId) ?? false,
|
||||
onToggleToolExpanded: toggleToolCardExpanded,
|
||||
onRequestUpdate: requestUpdate,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
userName: props.userName ?? null,
|
||||
userAvatar: props.userAvatar ?? null,
|
||||
basePath: props.basePath,
|
||||
localMediaPreviewRoots: props.localMediaPreviewRoots ?? [],
|
||||
assistantAttachmentAuthToken: props.assistantAttachmentAuthToken ?? null,
|
||||
canvasPluginSurfaceUrl: props.canvasPluginSurfaceUrl,
|
||||
embedSandboxMode: props.embedSandboxMode ?? "scripts",
|
||||
allowExternalEmbedUrls: props.allowExternalEmbedUrls ?? false,
|
||||
contextWindow: threadContextWindow,
|
||||
onDelete: () => {
|
||||
deleted.delete(item.key);
|
||||
requestUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
return nothing;
|
||||
},
|
||||
),
|
||||
)}
|
||||
${renderRealtimeTalkConversation(props)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user