diff --git a/ui/src/components/transcript/RunTranscriptView.test.tsx b/ui/src/components/transcript/RunTranscriptView.test.tsx
index 3e5a42c7bbb..e1a869caa34 100644
--- a/ui/src/components/transcript/RunTranscriptView.test.tsx
+++ b/ui/src/components/transcript/RunTranscriptView.test.tsx
@@ -1,11 +1,32 @@
-// @vitest-environment node
+// @vitest-environment jsdom
+import { act, type ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
+import { createRoot, type Root } from "react-dom/client";
import type { TranscriptEntry } from "../../adapters";
import { ThemeProvider } from "../../context/ThemeContext";
import { RunTranscriptView, normalizeTranscript } from "./RunTranscriptView";
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+
+function render(ui: ReactNode) {
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+ const root: Root = createRoot(container);
+ act(() => {
+ root.render(ui);
+ });
+ return {
+ container,
+ unmount: () => {
+ act(() => root.unmount());
+ container.remove();
+ },
+ };
+}
+
describe("RunTranscriptView", () => {
it("keeps running command stdout inside the command fold instead of a standalone stdout block", () => {
const entries: TranscriptEntry[] = [
@@ -111,29 +132,36 @@ describe("RunTranscriptView", () => {
expect(html).not.toContain("result");
});
- it("adds timestamp and age tooltip to raw mode labels", () => {
+ it("adds timestamp and age tooltip to raw mode labels lazily on hover", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-12T00:03:00.000Z"));
+ const { container, unmount } = render(
+
+
+ ,
+ );
try {
- const html = renderToStaticMarkup(
-
-
- ,
- );
+ const label = container.querySelector("span[data-timestamp]");
+ expect(label?.textContent).toBe("stdout");
+ expect(label?.getAttribute("title")).toBeNull();
- expect(html).toContain("Timestamp:");
- expect(html).toContain("Ago: 3m ago");
- expect(html).toContain(">stdout");
+ act(() => {
+ label?.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
+ });
+
+ expect(label?.getAttribute("title")).toContain("Timestamp:");
+ expect(label?.getAttribute("title")).toContain("Ago: 3m ago");
} finally {
+ unmount();
vi.useRealTimers();
}
});
diff --git a/ui/src/components/transcript/RunTranscriptView.tsx b/ui/src/components/transcript/RunTranscriptView.tsx
index 887f42f41b6..b20deeae374 100644
--- a/ui/src/components/transcript/RunTranscriptView.tsx
+++ b/ui/src/components/transcript/RunTranscriptView.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useState } from "react";
+import { useMemo, useState, type MouseEvent as ReactMouseEvent } from "react";
import type { TranscriptEntry } from "../../adapters";
import { MarkdownBody } from "../MarkdownBody";
import { timeAgo } from "../../lib/timeAgo";
@@ -142,6 +142,13 @@ function formatRawEntryTimestampTooltip(ts: string): string {
return `Timestamp: ${formatDateTime(ts)}; Ago: ${timeAgo(ts)}`;
}
+function handleRawTimestampLabelHover(event: ReactMouseEvent) {
+ if (event.currentTarget.title) return;
+ const ts = event.currentTarget.dataset.timestamp;
+ if (!ts) return;
+ event.currentTarget.title = formatRawEntryTimestampTooltip(ts);
+}
+
function humanizeLabel(value: string): string {
return value
.replace(/[_-]+/g, " ")
@@ -1373,7 +1380,8 @@ function RawTranscriptView({
>
{entry.kind}