diff --git a/.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png b/.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png new file mode 100644 index 00000000000..333ab7c2ce9 Binary files /dev/null and b/.github/pr-assets/control-ui-chat-detail/context-collapsed-full-date.png differ diff --git a/AGENTS.md b/AGENTS.md index cd94cb0417b..735147af333 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 395812e7470..4aea049566e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/gateway-status/helpers.test.ts b/src/commands/gateway-status/helpers.test.ts index a8dd4cf96d5..0083fc91d47 100644 --- a/src/commands/gateway-status/helpers.test.ts +++ b/src/commands/gateway-status/helpers.test.ts @@ -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 () => { diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 24c2626b91b..c19deb16fee 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -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, diff --git a/ui/src/ui/chat/grouped-render.test.ts b/ui/src/ui/chat/grouped-render.test.ts index 8318c0918c7..b566db02330 100644 --- a/ui/src/ui/chat/grouped-render.test.ts +++ b/ui/src/ui/chat/grouped-render.test.ts @@ -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("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(".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(".chat-group-timestamp"); + expect(time?.textContent?.trim()).toBe(formatChatTimestampForDisplay(timestamp).label); + }); + it("renders configured local user names and avatar variants", () => { const renderUser = (opts: Partial) => { const container = document.createElement("div"); diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index aa240a6bcf9..429492c7a92 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -49,6 +49,53 @@ type AssistantAttachmentAvailability = const assistantAttachmentAvailabilityCache = new Map(); 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` + + `; +} + 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( )} @@ -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( )}