⌚ feat: Show Message Timestamps on Hover#13709
Conversation
Reveal a message's time inline next to the author name on hover. Recent messages (under 24h) show a relative time ("10 minutes ago") with the absolute date on hover; older messages show the absolute date directly.
A shared MessageTimestamp component is used by both MessageRender and ContentRender, with createdAt added to their memo comparators so the timestamp appears once it's available.
Resolves #5199
There was a problem hiding this comment.
Pull request overview
Adds hover-revealed per-message timestamps in the chat UI, using a shared <time>-based component and a new locale-aware formatter utility, to address the lack of sent/received time visibility (Issue #5199).
Changes:
- Introduces
getMessageTimestamp()(relative/absolute/ISO + “recent” threshold) andisValidTimestamp()utilities. - Adds a shared
MessageTimestampUI component and renders it in both user and assistant message headers. - Updates
React.memocomparators to includecreatedAtso timestamps appear when populated after initial render; adds unit tests for the new formatter.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| client/src/utils/messages.ts | Adds timestamp formatting + validation utilities and types. |
| client/src/utils/tests/messages.test.ts | Adds unit tests covering formatting behavior and edge cases. |
| client/src/components/Messages/ContentRender.tsx | Renders timestamp for assistant/content turns; updates memo comparator. |
| client/src/components/Chat/Messages/ui/MessageRender.tsx | Renders timestamp for user turns; updates memo comparator. |
| client/src/components/Chat/Messages/ui/MessageTimestamp.tsx | New shared <time> component with hover/focus reveal behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <time | ||
| dateTime={timestamp.iso} | ||
| title={timestamp.isRecent ? timestamp.absolute : undefined} | ||
| className="ml-2 text-xs font-normal text-text-secondary transition-opacity duration-200 md:opacity-0 md:group-focus-within:opacity-100 md:group-hover:opacity-100" |
There was a problem hiding this comment.
Good catch, addressed in 61fc312. swapped the md: breakpoint for a [@media(hover:hover)]:opacity-0 arbitrary variant, so the timestamp only hides-until-hover where a pointer can actually hover and stays visible on touch/non-hover devices (tablets included). The reveal still uses group-hover/group-focus-within, and the element remains in the accessibility tree regardless
Use a (hover: hover) media query instead of the md: breakpoint so the timestamp stays visible on touch and other non-hover devices (e.g. tablets wider than md), while still revealing on hover/focus where a pointer supports it.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 61fc312320
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| <h2 className={cn('select-none font-semibold', fontSize)}> | ||
| <span className="sr-only">{getHeaderPrefixForScreenReader(msg, localize)}</span> | ||
| {messageLabel} | ||
| <MessageTimestamp value={msg.createdAt ?? msg.clientTimestamp} /> |
There was a problem hiding this comment.
Add timestamps to the Assistants content renderer
This wires the timestamp into the normal content renderer, but Assistants endpoint messages with content are routed by MultiMessage to client/src/components/Chat/Messages/MessageParts.tsx, which has its own header and was not updated. In Assistants chats, responses are initialized with content, so those messages still never render MessageTimestamp even though the feature is intended to cover every prompt/response.
Useful? React with 👍 / 👎.
| const timestamp = useMemo( | ||
| () => getMessageTimestamp(value, i18n.language), | ||
| [value, i18n.language], |
There was a problem hiding this comment.
Refresh relative timestamp labels over time
getMessageTimestamp reads Date.now(), but this memo only recomputes when the message timestamp or language changes. In an open chat, recent labels such as now or 2 minutes ago freeze at the value from the first render and do not advance until the component is remounted or the timestamp prop changes, so hover timestamps become stale for the common case of leaving a conversation open.
Useful? React with 👍 / 👎.
| <h2 className={cn('select-none font-semibold', fontSize)}> | ||
| <span className="sr-only">{getHeaderPrefixForScreenReader(msg, localize)}</span> | ||
| {messageLabel} | ||
| <MessageTimestamp value={msg.createdAt ?? msg.clientTimestamp} /> |
There was a problem hiding this comment.
Show timestamps for parallel response rows
For multi-response/parallel messages, useContentMetadata sets hasParallelContent, so this header branch is skipped and the newly added timestamp is never rendered; the parallel columns use SiblingHeader for agent names but do not include a message-level time. In conversations using the added/parallel response flow, those response messages still have no visible timestamp despite this feature adding one for the normal message path.
Useful? React with 👍 / 👎.
| <h2 className={cn('select-none font-semibold', fontSize)}> | ||
| <span className="sr-only">{getHeaderPrefixForScreenReader(msg, localize)}</span> | ||
| {messageLabel} | ||
| <MessageTimestamp value={msg.createdAt ?? msg.clientTimestamp} /> |
There was a problem hiding this comment.
Include timestamps in shared conversations
This adds the timestamp only to the main chat renderers, but shared links render the same saved messages through client/src/components/Share/Message.tsx, which has a separate author header and never uses MessageTimestamp. Anyone viewing a shared conversation therefore still has no way to see when those prompts or responses were sent, even though the underlying messages have createdAt.
Useful? React with 👍 / 👎.
Extend the hover timestamp to the Assistants (MessageParts), shared-link, search, and parallel/multi-response renderers so every prompt and response shows when it was sent. The parent message's createdAt is threaded down to parallel column headers (SiblingHeader). Add a shared, ref-counted minute ticker (useTimeTick) so relative labels like "2 minutes ago" stay current while a conversation is left open instead of freezing at first render.
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
* ✨ feat: Show Message Timestamps on Hover
Reveal a message's time inline next to the author name on hover. Recent messages (under 24h) show a relative time ("10 minutes ago") with the absolute date on hover; older messages show the absolute date directly.
A shared MessageTimestamp component is used by both MessageRender and ContentRender, with createdAt added to their memo comparators so the timestamp appears once it's available.
Resolves danny-avila#5199
* 🎨 fix: Gate message timestamp reveal on hover capability, not width
Use a (hover: hover) media query instead of the md: breakpoint so the timestamp stays visible on touch and other non-hover devices (e.g. tablets wider than md), while still revealing on hover/focus where a pointer supports it.
* 🎨 fix: Show message timestamps across all renderers and keep them live
Extend the hover timestamp to the Assistants (MessageParts), shared-link, search, and parallel/multi-response renderers so every prompt and response shows when it was sent. The parent message's createdAt is threaded down to parallel column headers (SiblingHeader).
Add a shared, ref-counted minute ticker (useTimeTick) so relative labels like "2 minutes ago" stay current while a conversation is left open instead of freezing at first render.
Summary
Closes #5199
There's currently no way to tell when a message was sent or received. This adds a timestamp to every message, both user prompts and assistant responses, shown inline next to the author's name.
Behavior:
<time>element's nativetitle, the same pattern GitHub uses.Implementation notes:
MessageTimestampcomponent renders a semantic<time dateTime=…>element, used by bothMessageRender(user turns) andContentRender(assistant/content turns).getMessageTimestamputil formats the relative/absolute strings using the nativeIntl.RelativeTimeFormat/Intl.DateTimeFormatAPIs, locale-aware from the active i18n language, with no new translation keys and no added dependencies.createdAtwas added to both components'React.memocomparators. These comparators whitelist the fields that trigger a re-render; withoutcreatedAtlisted, a message that receives its timestamp after the first render would never display it.Accessibility: the
<time>element stays in the DOM (only its opacity is toggled), so screen readers always announce the timestamp in the message header even though it's only visually shown on hover.No data-model or backend changes as
message.createdAtalready exists.Change Type
Testing
client/src/utils/__tests__/messages.test.ts): cover relative/absolute formatting, the "now" case, the 24h recency threshold (isRecent), invalid/missing input, and malformed-locale fallback.Test Configuration:
Checklist