fix(control-ui): clarify chat context details

This commit is contained in:
Val Alexander
2026-04-24 20:14:52 -05:00
parent b756dfcb2b
commit f78e906324
5 changed files with 166 additions and 15 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -235,6 +235,7 @@ Docs: https://docs.openclaw.ai
- Telegram/config: include generated Telegram channel config schema metadata in packaged plugin manifests so forum-topic/group config is accepted before runtime loads. Thanks @steipete.
- CLI/Claude: include user-configured `mcp.servers` in the strict Claude CLI MCP bundle config, matching Pi runs while preserving the OpenClaw loopback override. Fixes #70909. Thanks @keishingu.
- Browser/tool: keep explicit AI snapshots from inheriting the efficient role-snapshot default and preserve numeric Playwright AI refs, so `--format ai` remains a real AI snapshot path. Fixes #62550. Thanks @ly85206559.
- Control UI/chat: collapse assistant token/model context details behind an explicit Context disclosure and show full dates in message footers, making historical transcript timing clear without noisy default metadata.
- Gateway/config: keep in-process config patch reload comparisons on the resolved source snapshot when `${VAR}` env refs are restored on disk, avoiding false full gateway restarts for unchanged gateway/plugin secrets. Fixes #71208. Thanks @robbiethompson18.
- Slack/messages: serialize write-client requests and whole outbound sends per target so rapid multi-message Slack replies preserve send order. Fixes #69101. (#69105) Thanks @nightq and @ztexydt-cqh.
- Slack/messages: keep Slack bot tokens out of internal message-ordering and DM cache keys. Thanks @steipete.

View File

@@ -46,7 +46,9 @@
.chat-group-footer {
display: flex;
gap: 8px;
align-items: baseline;
row-gap: 5px;
align-items: center;
flex-wrap: wrap;
margin-top: 6px;
}
@@ -60,6 +62,7 @@
font-size: 11px;
color: var(--muted);
opacity: 0.7;
line-height: 1.2;
}
/* ── Group footer action buttons (TTS, delete) ── */
@@ -382,14 +385,81 @@ img.chat-avatar {
.msg-meta {
display: inline-flex;
align-items: center;
gap: 8px;
gap: 6px;
font-size: 11px;
line-height: 1;
color: var(--muted);
margin-top: 4px;
flex-wrap: wrap;
}
.msg-meta__summary {
list-style: none;
display: inline-flex;
align-items: center;
gap: 4px;
min-height: 22px;
padding: 2px 7px 2px 5px;
border: 1px solid var(--border);
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--bg-hover, rgba(255, 255, 255, 0.08)) 65%, transparent);
cursor: pointer;
user-select: none;
transition:
border-color var(--duration-fast) ease-out,
background var(--duration-fast) ease-out,
color var(--duration-fast) ease-out;
}
.msg-meta__summary::-webkit-details-marker {
display: none;
}
.msg-meta__summary:hover,
.msg-meta__summary:focus-visible {
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
background: var(--bg-hover, rgba(255, 255, 255, 0.08));
color: var(--fg);
}
.msg-meta__summary:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.msg-meta__summary-icon {
display: inline-flex;
width: 12px;
height: 12px;
transition: transform 120ms ease-out;
}
.msg-meta__summary-icon svg {
width: 12px;
height: 12px;
fill: none;
stroke: currentColor;
stroke-width: 2;
}
.msg-meta[open] .msg-meta__summary-icon {
transform: rotate(90deg);
}
details.msg-meta:not([open]) .msg-meta__details {
display: none;
}
.msg-meta__details {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 3px 7px;
border: 1px solid var(--border);
border-radius: var(--radius-full);
background: rgba(255, 255, 255, 0.03);
}
.msg-meta__tokens,
.msg-meta__cache,
.msg-meta__cost,

View File

@@ -5,7 +5,9 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { getSafeLocalStorage } from "../../local-storage.ts";
import type { MessageGroup } from "../types/chat-types.ts";
import {
formatChatTimestampForDisplay,
renderMessageGroup,
renderStreamingGroup,
resolveAssistantTextAvatar,
resetAssistantAttachmentAvailabilityCacheForTest,
} from "./grouped-render.ts";
@@ -304,6 +306,10 @@ describe("grouped chat rendering", () => {
},
1_000_000,
);
const meta = cached.querySelector<HTMLDetailsElement>("details.msg-meta");
expect(meta).not.toBeNull();
expect(meta?.open).toBe(false);
expect(meta?.querySelector("summary")?.textContent).toContain("Context");
expect(cached.querySelector(".msg-meta__ctx")?.textContent).toBe("44% ctx");
expect(cached.textContent).toContain("R438.4k");
expect(cached.textContent).toContain("W307");
@@ -320,6 +326,34 @@ describe("grouped chat rendering", () => {
expect(outputHeavy.querySelector(".msg-meta__ctx")?.textContent).toBe("10% ctx");
});
it("renders full dates with message timestamps", () => {
const container = document.createElement("div");
const timestamp = Date.UTC(2026, 3, 24, 18, 30);
renderAssistantMessage(container, {
role: "assistant",
content: "Done",
timestamp,
});
const time = container.querySelector<HTMLTimeElement>(".chat-group-timestamp");
const display = formatChatTimestampForDisplay(timestamp);
expect(time).not.toBeNull();
expect(time?.dateTime).toBe(display.dateTime);
expect(time?.textContent?.trim()).toBe(display.label);
expect(time?.getAttribute("title")).toBe(display.title);
});
it("renders full dates with streaming timestamps", () => {
const container = document.createElement("div");
const timestamp = Date.UTC(2026, 3, 24, 18, 30);
render(renderStreamingGroup("Working", timestamp), container);
const time = container.querySelector<HTMLTimeElement>(".chat-group-timestamp");
expect(time?.textContent?.trim()).toBe(formatChatTimestampForDisplay(timestamp).label);
});
it("renders configured local user names and avatar variants", () => {
const renderUser = (opts: Partial<RenderMessageGroupOptions>) => {
const container = document.createElement("div");

View File

@@ -49,6 +49,53 @@ type AssistantAttachmentAvailability =
const assistantAttachmentAvailabilityCache = new Map<string, AssistantAttachmentAvailability>();
const ASSISTANT_ATTACHMENT_UNAVAILABLE_RETRY_MS = 5_000;
export type ChatTimestampDisplay = {
label: string;
title: string;
dateTime: string;
};
export function formatChatTimestampForDisplay(timestamp: number): ChatTimestampDisplay {
const date = new Date(timestamp);
if (!Number.isFinite(date.getTime())) {
return {
label: "Unknown date",
title: "Unknown date",
dateTime: "",
};
}
return {
label: date.toLocaleString([], {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
}),
title: date.toLocaleString([], {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
}),
dateTime: date.toISOString(),
};
}
function renderChatTimestamp(timestamp: number) {
const display = formatChatTimestampForDisplay(timestamp);
return html`
<time class="chat-group-timestamp" datetime=${display.dateTime} title=${display.title}>
${display.label}
</time>
`;
}
export function resetAssistantAttachmentAvailabilityCacheForTest() {
assistantAttachmentAvailabilityCache.clear();
for (const blobUrl of managedImageBlobUrlResolvedCache.values()) {
@@ -238,10 +285,6 @@ export function renderStreamingGroup(
basePath?: string,
authToken?: string | null,
) {
const timestamp = new Date(startedAt).toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
});
const name = assistant?.name ?? "Assistant";
return html`
@@ -260,7 +303,7 @@ export function renderStreamingGroup(
)}
<div class="chat-group-footer">
<span class="chat-sender-name">${name}</span>
<span class="chat-group-timestamp">${timestamp}</span>
${renderChatTimestamp(startedAt)}
</div>
</div>
</div>
@@ -316,10 +359,6 @@ export function renderMessageGroup(
: normalizedRole === "tool"
? "tool"
: "other";
const timestamp = new Date(group.timestamp).toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
});
// Aggregate usage/cost/model across all messages in the group
const meta = extractGroupMeta(group, opts.contextWindow ?? null);
@@ -365,8 +404,7 @@ export function renderMessageGroup(
)}
<div class="chat-group-footer">
<span class="chat-sender-name">${who}</span>
<span class="chat-group-timestamp">${timestamp}</span>
${renderMessageMeta(meta)}
${renderChatTimestamp(group.timestamp)} ${renderMessageMeta(meta)}
${normalizedRole === "assistant" && isTtsSupported() ? renderTtsButton(group) : nothing}
${opts.onDelete
? renderDeleteButton(opts.onDelete, normalizedRole === "user" ? "left" : "right")
@@ -495,7 +533,15 @@ function renderMessageMeta(meta: GroupMeta | null) {
return nothing;
}
return html`<span class="msg-meta">${parts}</span>`;
return html`
<details class="msg-meta">
<summary class="msg-meta__summary" title="Show message context details">
<span class="msg-meta__summary-icon" aria-hidden="true">${icons.chevronRight}</span>
<span>Context</span>
</summary>
<span class="msg-meta__details">${parts}</span>
</details>
`;
}
function extractGroupText(group: MessageGroup): string {