diff --git a/webui/src/components/MessageBubble.tsx b/webui/src/components/MessageBubble.tsx index f81966dc3..16db7d9b8 100644 --- a/webui/src/components/MessageBubble.tsx +++ b/webui/src/components/MessageBubble.tsx @@ -253,6 +253,9 @@ export function MessageBubble({ const hasText = userContent.trim().length > 0; const showDeliveryStatus = message.deliveryStatus === "sending" || message.deliveryStatus === "failed"; + const createdAtLabel = formatMessageEndTime(message.createdAt); + const showCreatedAt = createdAtLabel.length > 0; + const createdAtTitle = showCreatedAt ? fmtDateTime(message.createdAt) : ""; const quotedContext = parsedMessage.quotedContext; const slashCommand = matchingSlashCommand(userContent, slashCommands); const messageText = slashCommand ? ( @@ -298,9 +301,19 @@ export function MessageBubble({ {messageText}

) : null} - {showDeliveryStatus || (hasText && showCopyAction) ? ( + {showDeliveryStatus || showCreatedAt || (hasText && showCopyAction) ? (
+ {showCreatedAt ? ( + + ) : null} 0 && (!empty || hasReasoning || media.length > 0); - const completedAtTitle = showCompletedAt ? fmtDateTime(completedAt) : ""; - const showAssistantFooterRow = showCopyButton || showForkButton || showCompletedAt; + const showAssistantTimestamp = + assistantTimestampLabel.length > 0 + && (!empty || hasReasoning || media.length > 0); + const assistantTimestampTitle = showAssistantTimestamp ? fmtDateTime(assistantTimestamp) : ""; + const showAssistantFooterRow = showCopyButton || showForkButton || showAssistantTimestamp; const showAssistantFooterSlot = message.role === "assistant" && (!empty || hasReasoning || media.length > 0); @@ -414,14 +438,15 @@ export function MessageBubble({ {forkLabel} ) : null} - {showCompletedAt ? ( + {showAssistantTimestamp ? ( ) : null}
diff --git a/webui/src/tests/message-bubble.test.tsx b/webui/src/tests/message-bubble.test.tsx index 62f73d164..b058b73c7 100644 --- a/webui/src/tests/message-bubble.test.tsx +++ b/webui/src/tests/message-bubble.test.tsx @@ -374,6 +374,45 @@ describe("MessageBubble", () => { ); }); + it("falls back to the assistant creation time when replay has no completion time", () => { + const createdAt = Date.UTC(2026, 6, 25, 12, 34, 56); + const { container } = render( + , + ); + + const time = container.querySelector("[data-message-timestamp]"); + expect(time).toHaveTextContent(formatMessageEndTime(createdAt)); + expect(time).toHaveAttribute("dateTime", new Date(createdAt).toISOString()); + expect(time).toHaveAttribute("title", fmtDateTime(createdAt)); + expect(time).not.toHaveAttribute("data-assistant-completed-at"); + }); + + it("renders the creation time for user messages", () => { + const createdAt = Date.UTC(2026, 6, 25, 12, 34, 56); + const { container } = render( + , + ); + + const time = container.querySelector("[data-message-created-at]"); + expect(time).toHaveTextContent(formatMessageEndTime(createdAt)); + expect(time).toHaveAttribute("dateTime", new Date(createdAt).toISOString()); + expect(time).toHaveAttribute("title", fmtDateTime(createdAt)); + }); + it("does not infer completion time from the assistant creation timestamp", () => { const createdAt = Date.UTC(2026, 6, 25, 12, 34, 0); const latencyMs = 13_000;