diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx
index 18e15003..554811d6 100644
--- a/frontend/src/features/agents/CustomAgentChatView.test.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx
@@ -288,6 +288,66 @@ describe("CustomAgentChatView", () => {
}
});
+ it("replays scrollback with the current reception time instead of the reduce index", async () => {
+ const replayReceivedAtMs = new Date("2026-08-15T13:08:00.000Z").getTime();
+ const indexOneTime = expectedChatTime(1);
+ const nowSpy = vi.spyOn(Date, "now").mockReturnValue(replayReceivedAtMs);
+ const agent = {
+ launchAgentChat: vi.fn(),
+ reattachAgentChat: vi.fn(async (sessionId: string) => ({
+ sessionId,
+ scrollback: [
+ { kind: "userPrompt" as const, text: "Replay prompt" },
+ { kind: "textDelta" as const, text: "Replay answer" },
+ { kind: "final" as const, content: "Replay completed" },
+ ],
+ })),
+ sendAgentChat: vi.fn(async () => {}),
+ cancelAgentChat: vi.fn(async () => {}),
+ closeAgentChat: vi.fn(async () => {}),
+ };
+
+ try {
+ render(
+ null) },
+ } as unknown as Gateways}
+ >
+
+ ,
+ );
+
+ const agentBubble = (await screen.findByText("Replay answer")).closest("div");
+ const finalBubble = screen.getByText("Replay completed").closest("div");
+
+ expect(agentBubble).toBeTruthy();
+ expect(finalBubble).toBeTruthy();
+ expect(
+ within(agentBubble!).getByText(expectedChatTime(replayReceivedAtMs)),
+ ).toBeTruthy();
+ expect(
+ within(finalBubble!).getByText(expectedChatTime(replayReceivedAtMs)),
+ ).toBeTruthy();
+ expect(within(agentBubble!).queryByText(indexOneTime)).toBeNull();
+ expect(within(finalBubble!).queryByText(indexOneTime)).toBeNull();
+ } finally {
+ nowSpy.mockRestore();
+ }
+ });
+
it("resizes the composer vertically with mouse, keyboard, and double-click reset", async () => {
const rectSpy = vi
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx
index 6439ac46..3b4837d9 100644
--- a/frontend/src/features/agents/CustomAgentChatView.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.tsx
@@ -75,6 +75,14 @@ const chatConversationViewCache = new Map();
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
const COMPOSER_RESIZE_STEP = 12;
const FALLBACK_COMPOSER_HEIGHT = 64;
+const MIN_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
+const MAX_REASONABLE_CHAT_TIMESTAMP_MS = Date.UTC(2100, 0, 1);
+const MIN_REASONABLE_CHAT_TIMESTAMP_SECONDS = Math.floor(
+ MIN_REASONABLE_CHAT_TIMESTAMP_MS / 1000,
+);
+const MAX_REASONABLE_CHAT_TIMESTAMP_SECONDS = Math.floor(
+ MAX_REASONABLE_CHAT_TIMESTAMP_MS / 1000,
+);
export function __clearCustomAgentChatViewCacheForTests() {
chatSessionViewCache.clear();
@@ -1120,7 +1128,10 @@ export function CustomAgentChatView({
setCurrentSession(reattached.sessionId);
if (options.applyScrollback) {
scrollbackChunkCountRef.current = reattached.scrollback.length;
- const replayed = reattached.scrollback.reduce(foldChunk, emptyChatViewState());
+ const replayed = reattached.scrollback.reduce(
+ (state, chunk) => foldChunk(state, chunk),
+ emptyChatViewState(),
+ );
applyChatViewState(replayed);
setBusy(turnsIndicateBusy(replayed));
}
@@ -2175,14 +2186,34 @@ function ChatBubble({
);
}
+function normalizeChatReceivedAtMs(receivedAtMs: number | undefined): number | undefined {
+ if (!Number.isFinite(receivedAtMs) || receivedAtMs === undefined || receivedAtMs <= 0) {
+ return undefined;
+ }
+ if (
+ receivedAtMs >= MIN_REASONABLE_CHAT_TIMESTAMP_SECONDS &&
+ receivedAtMs < MAX_REASONABLE_CHAT_TIMESTAMP_SECONDS
+ ) {
+ return receivedAtMs * 1000;
+ }
+ if (
+ receivedAtMs < MIN_REASONABLE_CHAT_TIMESTAMP_MS ||
+ receivedAtMs >= MAX_REASONABLE_CHAT_TIMESTAMP_MS
+ ) {
+ return undefined;
+ }
+ return receivedAtMs;
+}
+
function formatChatReceivedTime(receivedAtMs: number | undefined): string {
- if (!Number.isFinite(receivedAtMs) || !receivedAtMs) return "";
+ const normalizedAtMs = normalizeChatReceivedAtMs(receivedAtMs);
+ if (!normalizedAtMs) return "";
try {
const parts = new Intl.DateTimeFormat(undefined, {
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
- }).formatToParts(new Date(receivedAtMs));
+ }).formatToParts(new Date(normalizedAtMs));
const hour = parts.find((part) => part.type === "hour")?.value;
const minute = parts.find((part) => part.type === "minute")?.value;
return hour && minute ? `${hour}:${minute}` : "";
@@ -2201,7 +2232,8 @@ function ChatMessageMeta({
labelClassName?: string;
}) {
const time = formatChatReceivedTime(receivedAtMs);
- const dateTime = time && receivedAtMs ? new Date(receivedAtMs).toISOString() : undefined;
+ const normalizedAtMs = normalizeChatReceivedAtMs(receivedAtMs);
+ const dateTime = time && normalizedAtMs ? new Date(normalizedAtMs).toISOString() : undefined;
return (