feat(FN-1192): migrate agent log styling to theme classes
- Add dedicated AgentLogViewer CSS classes for model header layout, badge/timestamp rows, and tool/thinking/result/error states - Remove inline style objects from AgentLogViewer and switch rendered elements to semantic class names for theme-driven styling - Update AgentLogViewer tests to verify class-based styling behavior instead of inline style attributes
This commit is contained in:
@@ -121,80 +121,44 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
ref={containerRef}
|
||||
className="agent-log-viewer agent-log-viewer--streaming"
|
||||
data-testid="agent-log-viewer"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.5",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
padding: "12px",
|
||||
background: "var(--bg-secondary)",
|
||||
borderRadius: "6px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
{/* Model info header */}
|
||||
<div
|
||||
className="agent-log-model-header"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: "16px",
|
||||
padding: "8px 12px",
|
||||
marginBottom: "12px",
|
||||
background: "var(--bg-tertiary)",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
color: "var(--text-muted, #888)",
|
||||
overflow: "hidden",
|
||||
minWidth: 0,
|
||||
alignItems: "center",
|
||||
}}
|
||||
data-testid="agent-log-model-header"
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
<span style={{ fontWeight: 600 }}>Executor:</span>
|
||||
<div className="agent-log-model-header" data-testid="agent-log-model-header">
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Executor:</span>
|
||||
{hasExecutorOverride ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={executorModel.provider!} size="sm" />
|
||||
<span style={{ color: "var(--text-secondary, #aaa)" }}>
|
||||
{executorModel.provider}/{executorModel.modelId}
|
||||
</span>
|
||||
<span>{executorModel.provider}/{executorModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
<span style={{ fontWeight: 600 }}>Validator:</span>
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Validator:</span>
|
||||
{hasValidatorOverride ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={validatorModel.provider!} size="sm" />
|
||||
<span style={{ color: "var(--text-secondary, #aaa)" }}>
|
||||
{validatorModel.provider}/{validatorModel.modelId}
|
||||
</span>
|
||||
<span>{validatorModel.provider}/{validatorModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||
<span style={{ fontWeight: 600 }}>Planning/Triage:</span>
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Planning/Triage:</span>
|
||||
{hasPlanningOverride ? (
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={planningModel.provider!} size="sm" />
|
||||
<span style={{ color: "var(--text-secondary, #aaa)" }}>
|
||||
{planningModel.provider}/{planningModel.modelId}
|
||||
</span>
|
||||
<span>{planningModel.provider}/{planningModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Markdown render toggle */}
|
||||
<div style={{ marginLeft: "auto" }}>
|
||||
<div className="agent-log-model-header-toggle">
|
||||
<button
|
||||
className="agent-log-mode-toggle"
|
||||
onClick={() => setRenderMarkdown((prev) => !prev)}
|
||||
@@ -216,84 +180,30 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
: false;
|
||||
|
||||
const timestampSpan = showBadge ? (
|
||||
<span
|
||||
className="agent-log-timestamp"
|
||||
data-testid="agent-log-timestamp"
|
||||
style={{
|
||||
color: "var(--text-muted, #888)",
|
||||
fontSize: "10px",
|
||||
marginRight: "6px",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
<span className="agent-log-timestamp" data-testid="agent-log-timestamp">
|
||||
{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>
|
||||
<span className="agent-log-badge-row">
|
||||
<span className="agent-log-agent-badge">[{entry.agent}]</span>
|
||||
{timestampSpan}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (entry.type === "tool") {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="agent-log-tool"
|
||||
style={{
|
||||
color: "var(--accent)",
|
||||
margin: "4px 0",
|
||||
padding: "2px 6px",
|
||||
borderLeft: "3px solid var(--accent)",
|
||||
background: "var(--log-tool-bg)",
|
||||
}}
|
||||
>
|
||||
<div key={i} className="agent-log-tool">
|
||||
{agentBadge}⚡ {entry.text}
|
||||
{entry.detail && (
|
||||
<span
|
||||
className="agent-log-tool-detail"
|
||||
style={{
|
||||
color: "var(--text-muted, #888)",
|
||||
fontSize: "12px",
|
||||
marginLeft: "6px",
|
||||
}}
|
||||
>
|
||||
— {entry.detail}
|
||||
</span>
|
||||
)}
|
||||
{entry.detail && <span className="agent-log-tool-detail">— {entry.detail}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.type === "thinking") {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="agent-log-thinking"
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
color: "var(--text-muted, #888)",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
<span key={i} className="agent-log-thinking">
|
||||
{agentBadge}
|
||||
{renderMarkdown ? (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
@@ -308,60 +218,18 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
|
||||
|
||||
if (entry.type === "tool_result") {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="agent-log-tool-result"
|
||||
style={{
|
||||
color: "var(--color-success)",
|
||||
margin: "2px 0",
|
||||
padding: "2px 6px",
|
||||
borderLeft: "3px solid var(--color-success)",
|
||||
background: "var(--log-success-bg)",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<div key={i} className="agent-log-tool-result">
|
||||
{agentBadge}✓ {entry.text}
|
||||
{entry.detail && (
|
||||
<span
|
||||
className="agent-log-tool-detail"
|
||||
style={{
|
||||
color: "var(--text-muted, #888)",
|
||||
marginLeft: "6px",
|
||||
}}
|
||||
>
|
||||
— {entry.detail}
|
||||
</span>
|
||||
)}
|
||||
{entry.detail && <span className="agent-log-tool-detail">— {entry.detail}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.type === "tool_error") {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="agent-log-tool-error"
|
||||
style={{
|
||||
color: "var(--color-error)",
|
||||
margin: "2px 0",
|
||||
padding: "2px 6px",
|
||||
borderLeft: "3px solid var(--color-error)",
|
||||
background: "var(--log-error-bg)",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<div key={i} className="agent-log-tool-error">
|
||||
{agentBadge}✗ {entry.text}
|
||||
{entry.detail && (
|
||||
<span
|
||||
className="agent-log-tool-detail"
|
||||
style={{
|
||||
color: "var(--text-muted, #888)",
|
||||
marginLeft: "6px",
|
||||
}}
|
||||
>
|
||||
— {entry.detail}
|
||||
</span>
|
||||
)}
|
||||
{entry.detail && <span className="agent-log-tool-detail">— {entry.detail}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,11 +97,13 @@ describe("AgentLogViewer", () => {
|
||||
expect(toolDiv).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has a monospace font family", () => {
|
||||
it("applies the viewer styling via the agent-log-viewer class", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
|
||||
expect(viewer.style.fontFamily).toBe("monospace");
|
||||
expect(viewer.classList.contains("agent-log-viewer")).toBe(true);
|
||||
// Theme/layout styles come from CSS classes, not inline style attributes.
|
||||
expect(viewer.style.fontFamily).toBe("");
|
||||
});
|
||||
|
||||
describe("agent badge deduplication", () => {
|
||||
@@ -478,15 +480,17 @@ describe("AgentLogViewer", () => {
|
||||
expect(timestamp!.textContent).not.toBe("just now");
|
||||
});
|
||||
|
||||
it("styles timestamps with muted color and small font inside the badge row", () => {
|
||||
it("uses the timestamp class inside the badge row", () => {
|
||||
const entries = [makeEntry({ text: "hello", type: "text", agent: "executor" })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
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");
|
||||
expect(timestamp.classList.contains("agent-log-timestamp")).toBe(true);
|
||||
// Theme styles are class-based now, not inline.
|
||||
expect(timestamp.style.fontSize).toBe("");
|
||||
expect(timestamp.style.opacity).toBe("");
|
||||
});
|
||||
|
||||
it("includes timestamp in the badge container for tool entries", () => {
|
||||
@@ -570,19 +574,21 @@ describe("AgentLogViewer", () => {
|
||||
});
|
||||
|
||||
describe("horizontal overflow prevention", () => {
|
||||
it("sets overflowX hidden on the viewer container", () => {
|
||||
it("uses the viewer class for overflow-x handling", () => {
|
||||
const longString = "A".repeat(300);
|
||||
const entries = [makeEntry({ text: longString })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
|
||||
expect(viewer.style.overflowX).toBe("hidden");
|
||||
expect(viewer.classList.contains("agent-log-viewer")).toBe(true);
|
||||
expect(viewer.style.overflowX).toBe("");
|
||||
});
|
||||
|
||||
it("sets overflow-wrap break-word on the viewer container", () => {
|
||||
it("uses the viewer class for overflow-wrap handling", () => {
|
||||
const entries = [makeEntry({ text: "x".repeat(250) })];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
|
||||
expect(viewer.style.overflowWrap).toBe("break-word");
|
||||
expect(viewer.classList.contains("agent-log-viewer")).toBe(true);
|
||||
expect(viewer.style.overflowWrap).toBe("");
|
||||
});
|
||||
|
||||
it("renders pre elements with overflow-x auto for internal scrolling", () => {
|
||||
@@ -595,11 +601,12 @@ describe("AgentLogViewer", () => {
|
||||
expect(pre.style.maxWidth).toBe("100%");
|
||||
});
|
||||
|
||||
it("wraps model header on narrow widths via flex-wrap", () => {
|
||||
it("applies model-header wrapping via class", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement;
|
||||
expect(header.style.flexWrap).toBe("wrap");
|
||||
expect(header.classList.contains("agent-log-model-header")).toBe(true);
|
||||
expect(header.style.flexWrap).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -614,12 +621,13 @@ describe("AgentLogViewer", () => {
|
||||
expect(viewer.style.maxHeight).toBe("");
|
||||
});
|
||||
|
||||
it("maintains overflow-y auto for internal scrolling", () => {
|
||||
it("uses class-based overflow-y scrolling", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
|
||||
// The viewer itself should remain the scroll container
|
||||
expect(viewer.style.overflowY).toBe("auto");
|
||||
// Scrolling behavior is now defined in CSS.
|
||||
expect(viewer.classList.contains("agent-log-viewer")).toBe(true);
|
||||
expect(viewer.style.overflowY).toBe("");
|
||||
});
|
||||
|
||||
it("uses agent-log-viewer--streaming class when entries are present", () => {
|
||||
|
||||
@@ -3337,6 +3337,100 @@ body {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.agent-log-model-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-log-model-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.agent-log-model-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-log-model-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.agent-log-model-header-toggle {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.agent-log-badge-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.agent-log-tool {
|
||||
color: var(--accent);
|
||||
margin: 4px 0;
|
||||
padding: 2px 6px;
|
||||
border-left: 3px solid var(--accent);
|
||||
background: var(--log-tool-bg);
|
||||
}
|
||||
|
||||
.agent-log-tool-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.agent-log-tool-result {
|
||||
color: var(--color-success);
|
||||
margin: 2px 0;
|
||||
padding: 2px 6px;
|
||||
border-left: 3px solid var(--color-success);
|
||||
background: var(--log-success-bg);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-log-tool-error {
|
||||
color: var(--color-error);
|
||||
margin: 2px 0;
|
||||
padding: 2px 6px;
|
||||
border-left: 3px solid var(--color-error);
|
||||
background: var(--log-error-bg);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-log-thinking {
|
||||
font-style: italic;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.agent-log-agent-badge {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.agent-log-timestamp {
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
margin-right: 6px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Agent Log markdown/plain-text mode toggle */
|
||||
.agent-log-mode-toggle {
|
||||
padding: 3px 8px;
|
||||
|
||||
Reference in New Issue
Block a user