void>();
+let tick = 0;
+let intervalId: ReturnType | null = null;
+
+const subscribe = (onStoreChange: () => void): (() => void) => {
+ listeners.add(onStoreChange);
+ if (intervalId === null) {
+ intervalId = setInterval(() => {
+ tick += 1;
+ listeners.forEach((listener) => listener());
+ }, 60_000);
+ }
+ return () => {
+ listeners.delete(onStoreChange);
+ if (listeners.size === 0 && intervalId !== null) {
+ clearInterval(intervalId);
+ intervalId = null;
+ }
+ };
+};
+
+const getSnapshot = (): number => tick;
+
+/**
+ * Subscribes to a shared, ref-counted ticker that fires once a minute, so components
+ * displaying relative time stay current while a view is left open. A single interval
+ * is shared across all subscribers and is cleared when the last one unsubscribes.
+ */
+export default function useTimeTick(): number {
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
+}
diff --git a/client/src/utils/__tests__/messages.test.ts b/client/src/utils/__tests__/messages.test.ts
index 4af9f69439..b919441348 100644
--- a/client/src/utils/__tests__/messages.test.ts
+++ b/client/src/utils/__tests__/messages.test.ts
@@ -1,6 +1,11 @@
import type { TMessage } from 'librechat-data-provider';
import type { LocalizeFunction } from '~/common';
-import { getMessageAriaLabel, getHeaderPrefixForScreenReader } from '../messages';
+import {
+ isValidTimestamp,
+ getMessageAriaLabel,
+ getMessageTimestamp,
+ getHeaderPrefixForScreenReader,
+} from '../messages';
const translations: Record = {
com_endpoint_message: 'Message',
@@ -80,3 +85,70 @@ describe('getHeaderPrefixForScreenReader', () => {
expect(getHeaderPrefixForScreenReader(msg, localize)).toBe('Response: ');
});
});
+
+describe('isValidTimestamp', () => {
+ it('returns false for missing values', () => {
+ expect(isValidTimestamp(undefined)).toBe(false);
+ expect(isValidTimestamp(null)).toBe(false);
+ expect(isValidTimestamp('')).toBe(false);
+ });
+
+ it('returns false for unparseable strings', () => {
+ expect(isValidTimestamp('not-a-date')).toBe(false);
+ });
+
+ it('returns true for ISO date strings', () => {
+ expect(isValidTimestamp('2026-06-12T15:42:00.000Z')).toBe(true);
+ });
+});
+
+describe('getMessageTimestamp', () => {
+ const NOW = new Date('2026-06-12T15:42:00.000Z').getTime();
+
+ beforeEach(() => {
+ jest.useFakeTimers().setSystemTime(NOW);
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('returns null for missing or invalid values', () => {
+ expect(getMessageTimestamp(undefined, 'en-US')).toBeNull();
+ expect(getMessageTimestamp(null, 'en-US')).toBeNull();
+ expect(getMessageTimestamp('not-a-date', 'en-US')).toBeNull();
+ });
+
+ it('formats relative and absolute time for a recent message', () => {
+ const twoHoursAgo = new Date(NOW - 2 * 60 * 60 * 1000).toISOString();
+ const result = getMessageTimestamp(twoHoursAgo, 'en-US');
+ expect(result).not.toBeNull();
+ expect(result?.relative).toBe('2 hours ago');
+ expect(result?.iso).toBe(twoHoursAgo);
+ expect(result?.absolute).toContain('2026');
+ });
+
+ it('flags messages under 24h as recent (prefer relative)', () => {
+ const justUnderADay = new Date(NOW - 23 * 60 * 60 * 1000).toISOString();
+ expect(getMessageTimestamp(justUnderADay, 'en-US')?.isRecent).toBe(true);
+ });
+
+ it('flags older messages as not recent (prefer absolute date)', () => {
+ const overADay = new Date(NOW - 25 * 60 * 60 * 1000).toISOString();
+ const monthAgo = new Date(NOW - 38 * 24 * 60 * 60 * 1000).toISOString();
+ expect(getMessageTimestamp(overADay, 'en-US')?.isRecent).toBe(false);
+ expect(getMessageTimestamp(monthAgo, 'en-US')?.isRecent).toBe(false);
+ });
+
+ it('uses "now" for the current instant', () => {
+ const result = getMessageTimestamp(new Date(NOW).toISOString(), 'en-US');
+ expect(result?.relative).toBe('now');
+ expect(result?.isRecent).toBe(true);
+ });
+
+ it('falls back to the default locale for a malformed locale tag', () => {
+ const iso = new Date(NOW - 60 * 1000).toISOString();
+ expect(() => getMessageTimestamp(iso, 'not a locale!!')).not.toThrow();
+ expect(getMessageTimestamp(iso, 'not a locale!!')).not.toBeNull();
+ });
+});
diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts
index 91312567ec..0c756e7c37 100644
--- a/client/src/utils/messages.ts
+++ b/client/src/utils/messages.ts
@@ -344,6 +344,94 @@ export const getHeaderPrefixForScreenReader = (
: `${localize('com_ui_response')}${suffix}: `;
};
+export type MessageTimestamp = {
+ /** Localized relative time, e.g. "2 hours ago". */
+ relative: string;
+ /** Localized absolute date and time, e.g. "Jun 12, 2026, 3:42 PM". */
+ absolute: string;
+ /** ISO 8601 string for the `