feat(FN-1237): move AgentLogViewer timestamps into badge row
- Render timestamps only when an entry includes an agent badge - Remove standalone timestamp prefixes from text and tool log rows - Keep relative timestamp formatting while colocating it with agent identity - Update AgentLogViewer tests for badge-scoped timestamps and legacy entries without agents
This commit is contained in:
@@ -215,22 +215,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
? isBlockLevel || i === 0 || (prev && (prev.agent !== entry.agent || prev.type !== entry.type))
|
||||
: false;
|
||||
|
||||
const agentBadge = showBadge ? (
|
||||
<span
|
||||
className="agent-log-agent-badge"
|
||||
style={{
|
||||
color: "var(--text-muted, #888)",
|
||||
fontSize: "11px",
|
||||
marginRight: "6px",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase" as const,
|
||||
}}
|
||||
>
|
||||
[{entry.agent}]
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
const timestampSpan = (
|
||||
const timestampSpan = showBadge ? (
|
||||
<span
|
||||
className="agent-log-timestamp"
|
||||
data-testid="agent-log-timestamp"
|
||||
@@ -243,7 +228,30 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
>
|
||||
{formatTimestamp(entry.timestamp)}
|
||||
</span>
|
||||
);
|
||||
) : null;
|
||||
|
||||
const agentBadge = showBadge ? (
|
||||
<span
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="agent-log-agent-badge"
|
||||
style={{
|
||||
color: "var(--text-muted, #888)",
|
||||
fontSize: "11px",
|
||||
marginRight: "6px",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase" as const,
|
||||
}}
|
||||
>
|
||||
[{entry.agent}]
|
||||
</span>
|
||||
{timestampSpan}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (entry.type === "tool") {
|
||||
return (
|
||||
@@ -258,7 +266,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
background: "var(--log-tool-bg)",
|
||||
}}
|
||||
>
|
||||
{agentBadge}{timestampSpan}⚡ {entry.text}
|
||||
{agentBadge}⚡ {entry.text}
|
||||
{entry.detail && (
|
||||
<span
|
||||
className="agent-log-tool-detail"
|
||||
@@ -286,7 +294,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
{agentBadge}{timestampSpan}
|
||||
{agentBadge}
|
||||
{renderMarkdown ? (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{entry.text}
|
||||
@@ -312,7 +320,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
{agentBadge}{timestampSpan}✓ {entry.text}
|
||||
{agentBadge}✓ {entry.text}
|
||||
{entry.detail && (
|
||||
<span
|
||||
className="agent-log-tool-detail"
|
||||
@@ -342,7 +350,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
{agentBadge}{timestampSpan}✗ {entry.text}
|
||||
{agentBadge}✗ {entry.text}
|
||||
{entry.detail && (
|
||||
<span
|
||||
className="agent-log-tool-detail"
|
||||
@@ -361,7 +369,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
// Default: text entries
|
||||
return (
|
||||
<span key={i} className="agent-log-text">
|
||||
{agentBadge}{timestampSpan}
|
||||
{agentBadge}
|
||||
{renderMarkdown ? (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{entry.text}
|
||||
|
||||
@@ -33,7 +33,6 @@ describe("AgentLogViewer", () => {
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(2);
|
||||
// Reversed order: second chunk first, then first chunk
|
||||
// Each entry includes a timestamp span before the text content
|
||||
expect(textSpans[0].textContent).toContain("second chunk");
|
||||
expect(textSpans[1].textContent).toContain("first chunk");
|
||||
});
|
||||
@@ -412,31 +411,35 @@ describe("AgentLogViewer", () => {
|
||||
});
|
||||
|
||||
describe("timestamp display", () => {
|
||||
it("renders a timestamp span for each log entry", () => {
|
||||
it("renders no timestamps for entries without agent field", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text" }),
|
||||
makeEntry({ text: "Read", type: "tool" }),
|
||||
makeEntry({ text: "legacy 1", type: "text" }),
|
||||
makeEntry({ text: "legacy 2", type: "text" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamps = container.querySelectorAll(".agent-log-timestamp");
|
||||
expect(timestamps).toHaveLength(2);
|
||||
expect(timestamps).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders relative timestamps for recent entries", () => {
|
||||
it("renders relative timestamps for recent entries next to the badge", () => {
|
||||
const recentTimestamp = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text", timestamp: recentTimestamp }),
|
||||
makeEntry({ text: "hello", type: "text", agent: "executor", timestamp: recentTimestamp }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamp = container.querySelector(".agent-log-timestamp");
|
||||
expect(timestamp).toBeTruthy();
|
||||
expect(timestamp!.textContent).toBe("5m ago");
|
||||
|
||||
const badge = container.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge.parentElement?.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders 'just now' for entries less than a minute old", () => {
|
||||
const recentTimestamp = new Date(Date.now() - 30 * 1000).toISOString();
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text", timestamp: recentTimestamp }),
|
||||
makeEntry({ text: "hello", type: "text", agent: "executor", timestamp: recentTimestamp }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamp = container.querySelector(".agent-log-timestamp");
|
||||
@@ -446,7 +449,7 @@ describe("AgentLogViewer", () => {
|
||||
it("renders hours ago for older entries", () => {
|
||||
const olderTimestamp = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text", timestamp: olderTimestamp }),
|
||||
makeEntry({ text: "hello", type: "text", agent: "executor", timestamp: olderTimestamp }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamp = container.querySelector(".agent-log-timestamp");
|
||||
@@ -456,7 +459,7 @@ describe("AgentLogViewer", () => {
|
||||
it("renders days ago for entries older than a day", () => {
|
||||
const oldTimestamp = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text", timestamp: oldTimestamp }),
|
||||
makeEntry({ text: "hello", type: "text", agent: "executor", timestamp: oldTimestamp }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamp = container.querySelector(".agent-log-timestamp");
|
||||
@@ -466,7 +469,7 @@ describe("AgentLogViewer", () => {
|
||||
it("renders locale date for entries older than 7 days", () => {
|
||||
const veryOldTimestamp = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text", timestamp: veryOldTimestamp }),
|
||||
makeEntry({ text: "hello", type: "text", agent: "executor", timestamp: veryOldTimestamp }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamp = container.querySelector(".agent-log-timestamp");
|
||||
@@ -475,45 +478,94 @@ describe("AgentLogViewer", () => {
|
||||
expect(timestamp!.textContent).not.toBe("just now");
|
||||
});
|
||||
|
||||
it("styles timestamps with muted color and small font", () => {
|
||||
const entries = [makeEntry({ text: "hello", type: "text" })];
|
||||
it("styles timestamps with muted color and small font inside the badge row", () => {
|
||||
const entries = [makeEntry({ text: "hello", type: "text", agent: "executor" })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamp = container.querySelector(".agent-log-timestamp") as HTMLElement;
|
||||
const badge = container.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
const timestamp = badge.parentElement?.querySelector(".agent-log-timestamp") as HTMLElement;
|
||||
expect(timestamp).toBeTruthy();
|
||||
expect(timestamp.style.fontSize).toBe("10px");
|
||||
expect(timestamp.style.opacity).toBe("0.7");
|
||||
});
|
||||
|
||||
it("includes timestamp on tool entries", () => {
|
||||
const entries = [makeEntry({ text: "Bash", type: "tool" })];
|
||||
it("includes timestamp in the badge container for tool entries", () => {
|
||||
const entries = [makeEntry({ text: "Bash", type: "tool", agent: "executor" })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const toolDiv = container.querySelector(".agent-log-tool");
|
||||
expect(toolDiv).toBeTruthy();
|
||||
expect(toolDiv!.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
const badge = toolDiv!.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge.parentElement?.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes timestamp on tool_result entries", () => {
|
||||
const entries = [makeEntry({ text: "ok", type: "tool_result" })];
|
||||
it("includes timestamp in the badge container for tool_result entries", () => {
|
||||
const entries = [makeEntry({ text: "ok", type: "tool_result", agent: "executor" })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const resultDiv = container.querySelector(".agent-log-tool-result");
|
||||
expect(resultDiv).toBeTruthy();
|
||||
expect(resultDiv!.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
const badge = resultDiv!.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge.parentElement?.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes timestamp on tool_error entries", () => {
|
||||
const entries = [makeEntry({ text: "fail", type: "tool_error" })];
|
||||
it("includes timestamp in the badge container for tool_error entries", () => {
|
||||
const entries = [makeEntry({ text: "fail", type: "tool_error", agent: "executor" })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const errorDiv = container.querySelector(".agent-log-tool-error");
|
||||
expect(errorDiv).toBeTruthy();
|
||||
expect(errorDiv!.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
const badge = errorDiv!.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge.parentElement?.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes timestamp on thinking entries", () => {
|
||||
const entries = [makeEntry({ text: "hmm", type: "thinking" })];
|
||||
it("includes timestamp in the badge container for thinking entries", () => {
|
||||
const entries = [makeEntry({ text: "hmm", type: "thinking", agent: "executor" })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const thinkingSpan = container.querySelector(".agent-log-thinking");
|
||||
expect(thinkingSpan).toBeTruthy();
|
||||
expect(thinkingSpan!.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
const badge = thinkingSpan!.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge.parentElement?.querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows exactly one timestamp for consecutive text entries from the same agent", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "chunk 1", type: "text", agent: "executor" }),
|
||||
makeEntry({ text: "chunk 2", type: "text", agent: "executor" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const timestamps = container.querySelectorAll(".agent-log-timestamp");
|
||||
expect(timestamps).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("renders timestamps at each agent transition", () => {
|
||||
const entries = [
|
||||
makeEntry({ text: "triage output", type: "text", agent: "triage" }),
|
||||
makeEntry({ text: "executor output", type: "text", agent: "executor" }),
|
||||
makeEntry({ text: "review notes", type: "text", agent: "reviewer" }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const badges = Array.from(container.querySelectorAll(".agent-log-agent-badge"));
|
||||
const timestamps = container.querySelectorAll(".agent-log-timestamp");
|
||||
|
||||
expect(badges).toHaveLength(3);
|
||||
expect(timestamps).toHaveLength(3);
|
||||
expect(badges.map((badge) => badge.textContent)).toEqual(["[reviewer]", "[executor]", "[triage]"]);
|
||||
});
|
||||
|
||||
it("badge container includes both badge text and timestamp text", () => {
|
||||
const recentTimestamp = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||
const entries = [
|
||||
makeEntry({ text: "hello", type: "text", agent: "executor", timestamp: recentTimestamp }),
|
||||
];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const badge = container.querySelector(".agent-log-agent-badge") as HTMLElement;
|
||||
expect(badge).toBeTruthy();
|
||||
|
||||
const badgeContainer = badge.parentElement as HTMLElement;
|
||||
expect(badgeContainer.textContent).toContain("[executor]");
|
||||
expect(badgeContainer.textContent).toContain("5m ago");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -623,9 +675,7 @@ describe("AgentLogViewer", () => {
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const textSpans = container.querySelectorAll(".agent-log-text");
|
||||
expect(textSpans).toHaveLength(1);
|
||||
// Timestamp is included as a separate span before the text
|
||||
expect(textSpans[0].textContent).toContain(longText);
|
||||
expect(textSpans[0].querySelector(".agent-log-timestamp")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders very long detail text without truncation", () => {
|
||||
|
||||
Reference in New Issue
Block a user