diff --git a/webui/src/components/MessageBubble.tsx b/webui/src/components/MessageBubble.tsx
index ce372f652..373a088cf 100644
--- a/webui/src/components/MessageBubble.tsx
+++ b/webui/src/components/MessageBubble.tsx
@@ -4,6 +4,7 @@ import {
useMemo,
useRef,
useState,
+ type ComponentPropsWithoutRef,
type ReactNode,
} from "react";
import {
@@ -80,6 +81,42 @@ function ForkArrowIcon({ className }: { className?: string }) {
);
}
+type MessageTimestampProps = Omit<
+ ComponentPropsWithoutRef<"time">,
+ "dateTime" | "title"
+> & {
+ timestamp: number;
+ tooltipLabel: string;
+};
+
+function MessageTimestamp({
+ timestamp,
+ tooltipLabel,
+ className,
+ children,
+ ...props
+}: MessageTimestampProps) {
+ return (
+
+
+
+
+ {tooltipLabel}
+
+ );
+}
+
function MessageCopyButton({ content }: { content: string }) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
@@ -307,14 +344,13 @@ export function MessageBubble({
{showCreatedAt ? (
-
+
) : null}
) : null}
{showAssistantTimestamp ? (
-
+
) : null}
{showAutomationTrigger ? (
{
expect(onForkFromHere).toHaveBeenCalledTimes(1);
});
- it("shows the assistant completion time in the former latency slot", () => {
+ it("shows the assistant completion time in the former latency slot", async () => {
const completedAt = Date.UTC(2026, 6, 25, 12, 34, 56);
const { container } = render(
{
const time = container.querySelector("[data-assistant-completed-at]");
expect(time).toHaveTextContent(formatMessageEndTime(completedAt));
expect(time).toHaveAttribute("dateTime", new Date(completedAt).toISOString());
- expect(time).toHaveAttribute("title", fmtDateTime(completedAt));
+ expect(time).not.toHaveAttribute("title");
+ expect(time).toHaveAttribute("tabIndex", "0");
expect(time).toHaveClass(
+ "cursor-help",
"text-[11px]",
"leading-none",
"text-muted-foreground/70",
"tabular-nums",
);
+
+ fireEvent.pointerMove(time!);
+ expect(await screen.findByRole("tooltip")).toHaveTextContent(fmtDateTime(completedAt));
});
it("falls back to the assistant creation time when replay has no completion time", () => {
@@ -391,11 +396,11 @@ describe("MessageBubble", () => {
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("title");
expect(time).not.toHaveAttribute("data-assistant-completed-at");
});
- it("renders the creation time for user messages", () => {
+ it("renders the creation time for user messages", async () => {
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));
+ expect(time).not.toHaveAttribute("title");
+ expect(time).toHaveAttribute("tabIndex", "0");
+
+ fireEvent.pointerMove(time!);
+ expect(await screen.findByRole("tooltip")).toHaveTextContent(fmtDateTime(createdAt));
});
it("does not infer completion time from the assistant creation timestamp", () => {