mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
fix(control-ui): clarify chat context details
Summary:
- Show full date and time in Control UI chat message footers.
- Collapse assistant model/token/context metadata behind an explicit Context disclosure.
- Update changelog attribution guidance to allow multi-author credited entries.
Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/chat/grouped-render.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm test src/commands/gateway-status/helpers.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub CI passed on f071a38177
This commit is contained in:
BIN
.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png
vendored
Normal file
BIN
.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -120,7 +120,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
|
||||
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
|
||||
- Changelog user-facing only; pure test/internal usually no entry.
|
||||
- Changelog placement: active version `### Changes`/`### Fixes`; at most one contributor mention, prefer `Thanks @user`.
|
||||
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s).
|
||||
|
||||
## Git
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys.
|
||||
- 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. (#71337) Thanks @BunsDev.
|
||||
- Telegram: remove the startup persisted-offset `getUpdates` preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar.
|
||||
- Telegram: keep the polling stall watchdog active even when grammY reports the runner as not running while its task is still pending, so a rebuilt transport cannot leave `getUpdates` silent until a manual gateway restart. Fixes #69064. Thanks @LDLoeb.
|
||||
- Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.
|
||||
|
||||
@@ -188,6 +188,8 @@ describe("resolveAuthForTarget", () => {
|
||||
it("redacts resolver internals from unresolved SecretRef diagnostics", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_GATEWAY_PASSWORD: undefined,
|
||||
OPENCLAW_GATEWAY_TOKEN: undefined,
|
||||
MISSING_GATEWAY_TOKEN: undefined,
|
||||
},
|
||||
async () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user