From b900fc94ebe630155783c07324c58af7e6387a03 Mon Sep 17 00:00:00 2001 From: Alex Southwell Date: Wed, 16 Sep 2026 16:57:59 +1000 Subject: [PATCH] feat(web): reveal timestamps on tool rows and turn folds (#8641) Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com> --- .../components/chat/MessagesTimeline.test.tsx | 15 +++++- .../src/components/chat/MessagesTimeline.tsx | 52 +++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 0987b852e716b6db2581e1b06c9b6d608cb87cab..3bf5f482948dd9f31fe9bb3e5486c92373e792ef 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -9,7 +9,7 @@ import { import { act, createRef, useLayoutEffect, type ReactNode, type Ref } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { create, type ReactTestRenderer } from "react-test-renderer"; -import { beforeAll, describe, expect, it, vi } from "vite-plus/test"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import type { LegendListRef, MaintainScrollAtEndOptions } from "@legendapp/list/react"; import { shouldUseRestingComposerLayout } from "../composerFooterLayout"; import { useComposerFocusState } from "./useComposerFocusState"; @@ -146,7 +146,9 @@ function matchMedia() { let MessagesTimeline: typeof import("./MessagesTimeline").MessagesTimeline; let resolvePreviewAnnotationImage: typeof import("./MessagesTimeline").resolvePreviewAnnotationImage; -beforeAll(async () => { +const ElementStub = class ElementStub {}; + +function stubDomGlobals() { const classList = { add: () => {}, remove: () => {}, @@ -154,6 +156,7 @@ beforeAll(async () => { contains: () => false, }; + vi.stubGlobal("Element", ElementStub); vi.stubGlobal("localStorage", { getItem: () => null, setItem: () => {}, @@ -161,6 +164,7 @@ beforeAll(async () => { clear: () => {}, }); vi.stubGlobal("window", { + Element: ElementStub, matchMedia, addEventListener: () => {}, removeEventListener: () => {}, @@ -177,10 +181,17 @@ beforeAll(async () => { offsetHeight: 0, }, }); +} +beforeAll(async () => { + stubDomGlobals(); ({ MessagesTimeline, resolvePreviewAnnotationImage } = await import("./MessagesTimeline")); }, 30_000); +// The scroll-settling test clears every global stub; mounted timeline rows +// still touch `window` through the tooltip's focus handling. +beforeEach(stubDomGlobals); + const ACTIVE_THREAD_ENVIRONMENT_ID = EnvironmentId.make("environment-local"); const MESSAGE_CREATED_AT = "2026-03-17T19:12:28.000Z"; diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 09aa61bd5186fe1589e5a8addb1d0075c1b980f1..976f261981e3082f27ffef7fb5ef07364aa7bc1c 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -2063,12 +2063,49 @@ function RevertUserMessageButton({ ); } +/** + * Hover-revealed wall-clock time with a full-date tooltip — the same metadata + * presentation as message rows, for work entries and turn folds. The parent + * carries `group/timeline-row`; hover or focus on an existing control reveals + * the time without adding a tab stop. Hidden timestamps stay outside the row + * layout. Visibility changes immediately so leaving flow cannot overlap text + * during a fade-out. Render it as the row's rightmost flex child so the + * revealed time lands at the right edge, clear of disclosure controls. + */ +function TimelineRowTimestamp({ + createdAt, + timestampFormat, + className, +}: { + createdAt: string; + timestampFormat: TimestampFormat; + className?: string; +}) { + return ( + + + } + > + {formatDayAwareTimestamp(createdAt, timestampFormat)} + + {formatChatTimestampTooltip(createdAt, timestampFormat)} + + ); +} + function TurnFoldTimelineRow({ row }: { row: Extract }) { const ctx = use(TimelineRowCtx); const Icon = row.expanded ? ChevronDownIcon : ChevronRightIcon; return ( -
+
+
); } @@ -2770,7 +2812,7 @@ function WorkGroupToggleTimelineRow({ return ( ); } @@ -4234,7 +4277,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { onToggleEntry?: ((collapsed: boolean) => void) | undefined; }) { const { workEntry, workspaceRoot, isExpandedToolGroupEntry, displayLabel } = props; - const { threadRef, onImageExpand } = use(TimelineRowCtx); + const { threadRef, onImageExpand, timestampFormat } = use(TimelineRowCtx); const groupView = use(WorkGroupViewCtx); const [expanded, setExpanded] = useState( () => groupView?.state.expandedEntries.has(workEntry.id) ?? false, @@ -4336,7 +4379,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { return (
+
{expanded && viewedImage && threadRef ? ( -- 2.54.0