diff --git a/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx b/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
index f0ac7cec5b..7255c315f6 100644
--- a/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
+++ b/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
@@ -229,6 +229,27 @@ test('uses human conversation context instead of raw ids in action names', async
);
});
+test('keeps Astryx auto formatting live for user-message timestamps', async (context) => {
+ const now = Date.UTC(2026, 7, 27, 12);
+ context.mock.timers.enable({ apis: ['Date', 'setInterval'], now });
+ const { container, root } = domRoot();
+ const twoHoursAgo = now - 2 * 60 * 60 * 1_000;
+ const turn = {
+ ...turnWith([{ ...ANSWER, live: false }]),
+ user: { id: 'ask', role: 'user' as const, text: 'ask', ts: twoHoursAgo },
+ };
+
+ await renderTurn(root, turn);
+
+ const timestamp = container.querySelector('.maka-message-time-inline time');
+ assert.ok(timestamp, 'Astryx Timestamp renders the semantic time element');
+ assert.match(timestamp.textContent ?? '', /2 hours ago/);
+ assert.equal(timestamp.getAttribute('tabindex'), '0', 'the absolute-time hover card is keyboard reachable');
+
+ await act(() => context.mock.timers.tick(60 * 60 * 1_000));
+ assert.match(timestamp.textContent ?? '', /3 hours ago/);
+});
+
/**
* The live handoff announces itself exactly once, when the answer enters its
* settled phase. A bubble replayed from history mounts already past the
diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx
index e0e6f27383..f17bdb24fa 100644
--- a/packages/ui/src/chat-turn.tsx
+++ b/packages/ui/src/chat-turn.tsx
@@ -193,7 +193,7 @@ const UserMessageBody = memo(function UserMessageBody(props: {
/* `value` takes ms directly: Timestamp's own parseValue reads
anything past 1e12 as milliseconds (2001-09-09 onward), and a
chat message never predates that. */
- ()
+ ()
) : undefined
}
footer={