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:
gsxdsm
2026-04-08 15:02:32 -07:00
parent 29e2475601
commit d0578a589b
3 changed files with 140 additions and 170 deletions

View File

@@ -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>
);
}

View File

@@ -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", () => {

View File

@@ -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;