From fc2ae89aa6c7ab5215e4fddb069be0289aa489ae Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:39:52 +0100 Subject: [PATCH] =?UTF-8?q?=E2=8C=9A=20feat:=20Show=20Message=20Timestamps?= =?UTF-8?q?=20on=20Hover=20(#13709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ 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 #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. --- .../Chat/Messages/Content/ContentParts.tsx | 8 +- .../Chat/Messages/Content/ParallelContent.tsx | 10 ++- .../Chat/Messages/Content/SiblingHeader.tsx | 5 ++ .../components/Chat/Messages/MessageParts.tsx | 2 + .../Chat/Messages/SearchMessage.tsx | 8 +- .../Chat/Messages/ui/MessageRender.tsx | 3 + .../Chat/Messages/ui/MessageTimestamp.tsx | 31 +++++++ .../src/components/Messages/ContentRender.tsx | 4 + client/src/components/Share/Message.tsx | 6 +- client/src/hooks/useTimeTick.ts | 33 +++++++ client/src/utils/__tests__/messages.test.ts | 74 +++++++++++++++- client/src/utils/messages.ts | 88 +++++++++++++++++++ 12 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 client/src/components/Chat/Messages/ui/MessageTimestamp.tsx create mode 100644 client/src/hooks/useTimeTick.ts diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index 84e8ce3db8..00569c11f5 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -6,14 +6,14 @@ import type { TAttachment, Agents, } from 'librechat-data-provider'; +import type { ToolCallGroupExpansionState } from './ToolCallGroup'; import { ParallelContentRenderer, type PartWithIndex } from './ParallelContent'; import { mapAttachments, groupSequentialToolCalls } from '~/utils'; import { MessageContext, SearchContext } from '~/Providers'; -import { EditTextPart, EmptyText } from './Parts'; import PendingSkillCall from './Parts/PendingSkillCall'; +import { EditTextPart, EmptyText } from './Parts'; import MemoryArtifacts from './MemoryArtifacts'; import ToolCallGroup from './ToolCallGroup'; -import type { ToolCallGroupExpansionState } from './ToolCallGroup'; import Container from './Container'; import Part from './Part'; @@ -105,6 +105,8 @@ type ContentPartsProps = { * the full message object) so `React.memo` stays shallow-happy. */ manualSkills?: string[]; + /** ISO timestamp of the parent message, surfaced in parallel column headers. */ + createdAt?: string | null; conversationId?: string | null; attachments?: TAttachment[]; searchResults?: { [key: string]: SearchResultData }; @@ -142,6 +144,7 @@ const ContentParts = memo(function ContentParts({ conversationId, isCreatedByUser, isLatestMessage, + createdAt, }: ContentPartsProps) { const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]); const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false; @@ -375,6 +378,7 @@ const ContentParts = memo(function ContentParts({ @@ -193,6 +196,7 @@ export const ParallelColumns = memo(function ParallelColumns({ type ParallelContentRendererProps = { content?: Array; messageId: string; + createdAt?: string | null; conversationId?: string | null; attachments?: TAttachment[]; searchResults?: { [key: string]: SearchResultData }; @@ -207,6 +211,7 @@ type ParallelContentRendererProps = { export const ParallelContentRenderer = memo(function ParallelContentRenderer({ content, messageId, + createdAt, conversationId, attachments, searchResults, @@ -253,6 +258,7 @@ export const ParallelContentRenderer = memo(function ParallelContentRenderer({ columns={columns} groupId={groupId} messageId={messageId} + createdAt={createdAt} renderPart={renderPart} isSubmitting={isSubmitting} conversationId={conversationId} diff --git a/client/src/components/Chat/Messages/Content/SiblingHeader.tsx b/client/src/components/Chat/Messages/Content/SiblingHeader.tsx index 080974ed2b..160966fb13 100644 --- a/client/src/components/Chat/Messages/Content/SiblingHeader.tsx +++ b/client/src/components/Chat/Messages/Content/SiblingHeader.tsx @@ -3,6 +3,7 @@ import { GitBranchPlus } from 'lucide-react'; import { useToastContext } from '@librechat/client'; import { EModelEndpoint, parseEphemeralAgentId, stripAgentIdSuffix } from 'librechat-data-provider'; import type { TMessage, Agent } from 'librechat-data-provider'; +import MessageTimestamp from '~/components/Chat/Messages/ui/MessageTimestamp'; import { useBranchMessageMutation } from '~/data-provider/Messages'; import MessageIcon from '~/components/Share/MessageIcon'; import { useAgentsMapContext } from '~/Providers'; @@ -14,6 +15,8 @@ type SiblingHeaderProps = { agentId?: string; /** The messageId of the parent message */ messageId?: string; + /** ISO timestamp of the parent message */ + createdAt?: string | null; /** The conversationId */ conversationId?: string | null; /** Whether a submission is in progress */ @@ -27,6 +30,7 @@ type SiblingHeaderProps = { export default function SiblingHeader({ agentId, messageId, + createdAt, conversationId, isSubmitting, }: SiblingHeaderProps) { @@ -117,6 +121,7 @@ export default function SiblingHeader({ /> {displayName} +