feat(FN-2598): merge fusion/fn-2598 (auto-resolved)

- test(FN-2598): complete Step 3 — update collapsed model header coverage
- fix(FN-2598): normalize expand button spacing token
- feat(FN-2598): complete Step 2 — style compact model header
- feat(FN-2598): complete Step 1 — compact model header structure
This commit is contained in:
Fusion
2026-04-26 11:36:26 -07:00
committed by gsxdsm
parent 43523b92bf
commit 4cfb502c3d
4 changed files with 325 additions and 247 deletions

View File

@@ -4,7 +4,7 @@ import { useRef, useEffect, useState, useCallback, useLayoutEffect, useMemo } fr
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown"; import type { Components } from "react-markdown";
import { Maximize2, Minimize2, Loader2 } from "lucide-react"; import { Maximize2, Minimize2, Loader2, ChevronDown, ChevronRight } from "lucide-react";
function formatTimestamp(iso: string): string { function formatTimestamp(iso: string): string {
const date = new Date(iso); const date = new Date(iso);
@@ -127,6 +127,7 @@ export function AgentLogViewer({
const previousNewestEntryKeyRef = useRef<string | null>(null); const previousNewestEntryKeyRef = useRef<string | null>(null);
const [renderMarkdown, setRenderMarkdown] = useState(true); const [renderMarkdown, setRenderMarkdown] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [modelHeaderExpanded, setModelHeaderExpanded] = useState(false);
const chronologicalEntryKeys = useMemo( const chronologicalEntryKeys = useMemo(
() => buildEntryRenderKeys(entries), () => buildEntryRenderKeys(entries),
@@ -208,6 +209,39 @@ export function AgentLogViewer({
const hasValidatorOverride = validatorModel?.provider && validatorModel?.modelId; const hasValidatorOverride = validatorModel?.provider && validatorModel?.modelId;
const hasPlanningOverride = planningModel?.provider && planningModel?.modelId; const hasPlanningOverride = planningModel?.provider && planningModel?.modelId;
const modelProviders = useMemo(() => {
const providers: Array<{ role: string; provider: string; modelId?: string }> = [];
if (hasExecutorOverride) {
providers.push({
role: "Executor",
provider: executorModel!.provider!,
modelId: executorModel!.modelId,
});
}
if (hasValidatorOverride) {
providers.push({
role: "Validator",
provider: validatorModel!.provider!,
modelId: validatorModel!.modelId,
});
}
if (hasPlanningOverride) {
providers.push({
role: "Planning",
provider: planningModel!.provider!,
modelId: planningModel!.modelId,
});
}
return providers;
}, [
hasExecutorOverride,
executorModel,
hasValidatorOverride,
validatorModel,
hasPlanningOverride,
planningModel,
]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@@ -216,39 +250,26 @@ export function AgentLogViewer({
> >
{/* Model info header */} {/* Model info header */}
<div className="agent-log-model-header" data-testid="agent-log-model-header"> <div className="agent-log-model-header" data-testid="agent-log-model-header">
<div className="agent-log-model-group"> <div className="agent-log-model-icons">
<span className="agent-log-model-label">Executor:</span> {modelProviders.map((modelProvider) => (
{hasExecutorOverride ? ( <ProviderIcon
<span className="agent-log-model-value"> key={`${modelProvider.role}-${modelProvider.provider}-${modelProvider.modelId ?? "default"}`}
<ProviderIcon provider={executorModel.provider!} size="sm" /> provider={modelProvider.provider}
<span>{executorModel.provider}/{executorModel.modelId}</span> size="sm"
</span> />
) : ( ))}
<span className="model-badge-default">Using default</span> <button
)} className="agent-log-model-expand-btn"
</div> onClick={() => setModelHeaderExpanded((prev) => !prev)}
<div className="agent-log-model-group"> aria-label={modelHeaderExpanded ? "Collapse model details" : "Expand model details"}
<span className="agent-log-model-label">Validator:</span> aria-expanded={modelHeaderExpanded}
{hasValidatorOverride ? ( aria-controls="agent-log-model-details"
<span className="agent-log-model-value"> data-testid="agent-log-model-expand"
<ProviderIcon provider={validatorModel.provider!} size="sm" /> >
<span>{validatorModel.provider}/{validatorModel.modelId}</span> {modelHeaderExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span> </button>
) : (
<span className="model-badge-default">Using default</span>
)}
</div>
<div className="agent-log-model-group">
<span className="agent-log-model-label">Planning:</span>
{hasPlanningOverride ? (
<span className="agent-log-model-value">
<ProviderIcon provider={planningModel.provider!} size="sm" />
<span>{planningModel.provider}/{planningModel.modelId}</span>
</span>
) : (
<span className="model-badge-default">Using default</span>
)}
</div> </div>
{/* Markdown render toggle */} {/* Markdown render toggle */}
<div className="agent-log-model-header-toggle"> <div className="agent-log-model-header-toggle">
<button <button
@@ -271,6 +292,44 @@ export function AgentLogViewer({
{isFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />} {isFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
</button> </button>
</div> </div>
{modelHeaderExpanded && (
<div id="agent-log-model-details" className="agent-log-model-details">
<div className="agent-log-model-group">
<span className="agent-log-model-label">Executor:</span>
{hasExecutorOverride ? (
<span className="agent-log-model-value">
<ProviderIcon provider={executorModel.provider!} size="sm" />
<span>{executorModel.provider}/{executorModel.modelId}</span>
</span>
) : (
<span className="model-badge-default">Using default</span>
)}
</div>
<div className="agent-log-model-group">
<span className="agent-log-model-label">Validator:</span>
{hasValidatorOverride ? (
<span className="agent-log-model-value">
<ProviderIcon provider={validatorModel.provider!} size="sm" />
<span>{validatorModel.provider}/{validatorModel.modelId}</span>
</span>
) : (
<span className="model-badge-default">Using default</span>
)}
</div>
<div className="agent-log-model-group">
<span className="agent-log-model-label">Planning/Triage:</span>
{hasPlanningOverride ? (
<span className="agent-log-model-value">
<ProviderIcon provider={planningModel.provider!} size="sm" />
<span>{planningModel.provider}/{planningModel.modelId}</span>
</span>
) : (
<span className="model-badge-default">Using default</span>
)}
</div>
</div>
)}
</div> </div>
{/* Pagination summary */} {/* Pagination summary */}

View File

@@ -251,16 +251,56 @@
.agent-log-model-header { .agent-log-model-header {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 16px; gap: var(--space-lg);
padding: 8px 12px; padding: var(--space-sm) var(--space-md);
margin-bottom: 12px; margin-bottom: var(--space-md);
background: var(--bg-tertiary); background: var(--bg-tertiary);
border-radius: 4px; border-radius: var(--radius-sm);
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
align-items: center; align-items: center;
justify-content: space-between;
}
.agent-log-model-icons {
display: flex;
align-items: center;
gap: var(--space-xs);
}
.agent-log-model-expand-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-xs);
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: color var(--transition-fast);
}
.agent-log-model-expand-btn:hover {
color: var(--text);
background: var(--card-hover);
}
.agent-log-model-expand-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
.agent-log-model-details {
display: flex;
flex-basis: 100%;
flex-wrap: wrap;
gap: var(--space-lg);
padding-top: var(--space-sm);
margin-top: var(--space-sm);
border-top: 1px solid var(--border);
} }
.agent-log-model-group { .agent-log-model-group {

View File

@@ -7,7 +7,10 @@ import type { AgentLogEntry } from "@fusion/core";
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
Maximize2: () => null, Maximize2: () => null,
Minimize2: () => null, Minimize2: () => null,
Loader2: () => null,
Cpu: () => null, Cpu: () => null,
ChevronDown: () => null,
ChevronRight: () => null,
})); }));
function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry { function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry {
@@ -275,38 +278,34 @@ describe("AgentLogViewer", () => {
entries={entries} entries={entries}
loading={false} loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }} executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
expect(header!.textContent).not.toContain("Executor:");
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(header!.textContent).toContain("Executor:"); expect(header!.textContent).toContain("Executor:");
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
expect(header!.textContent).toContain("Validator:");
expect(header!.textContent).toContain("Using default");
// Verify ProviderIcon is rendered for executor
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
}); });
it("renders 'Using default' when no executor model override is set", () => { it("renders 'Using default' when no executor model override is set", () => {
const entries = [makeEntry()]; const entries = [makeEntry()];
const { container } = render( const { container } = render(
<AgentLogViewer entries={entries} loading={false} executorModel={null} /> <AgentLogViewer entries={entries} loading={false} executorModel={null} />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("renders 'Using default' when executorModel is undefined", () => { it("renders 'Using default' when executorModel is undefined", () => {
const entries = [makeEntry()]; const entries = [makeEntry()];
const { container } = render( const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
<AgentLogViewer entries={entries} loading={false} />
);
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("renders model info header with validator model when set", () => { it("renders model info header with validator model when set", () => {
@@ -316,27 +315,26 @@ describe("AgentLogViewer", () => {
entries={entries} entries={entries}
loading={false} loading={false}
validatorModel={{ provider: "openai", modelId: "gpt-4o" }} validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
expect(header!.textContent).not.toContain("Validator:");
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(header!.textContent).toContain("Validator:"); expect(header!.textContent).toContain("Validator:");
expect(header!.textContent).toContain("openai/gpt-4o"); expect(header!.textContent).toContain("openai/gpt-4o");
expect(header!.textContent).toContain("Executor:");
expect(header!.textContent).toContain("Using default");
// Verify ProviderIcon is rendered for validator
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
}); });
it("renders 'Using default' when no validator model override is set", () => { it("renders 'Using default' when no validator model override is set", () => {
const entries = [makeEntry()]; const entries = [makeEntry()];
const { container } = render( const { container } = render(
<AgentLogViewer entries={entries} loading={false} validatorModel={null} /> <AgentLogViewer entries={entries} loading={false} validatorModel={null} />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Validator:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("renders both models when both are configured", () => { it("renders both models when both are configured", () => {
@@ -347,30 +345,26 @@ describe("AgentLogViewer", () => {
loading={false} loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }} executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
validatorModel={{ provider: "openai", modelId: "gpt-4o" }} validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:");
expect(header!.textContent).toContain("anthropic/claude-opus-4");
expect(header!.textContent).toContain("Validator:");
expect(header!.textContent).toContain("openai/gpt-4o");
// Verify both ProviderIcons are rendered
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy(); expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy(); expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
expect(header!.textContent).not.toContain("anthropic/claude-opus-4");
expect(header!.textContent).not.toContain("openai/gpt-4o");
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(header!.textContent).toContain("anthropic/claude-opus-4");
expect(header!.textContent).toContain("openai/gpt-4o");
}); });
it("renders header with 'Using default' for both models when both are null/undefined", () => { it("renders header with 'Using default' for both models when both are null/undefined", () => {
const entries = [makeEntry()]; const entries = [makeEntry()];
const { container } = render( const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
<AgentLogViewer entries={entries} loading={false} />
);
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
expect(header!.textContent).toContain("Validator:");
expect(header!.textContent).toContain("Using default");
}); });
it("shows 'Using default' when executorModel has only provider but no modelId", () => { it("shows 'Using default' when executorModel has only provider but no modelId", () => {
@@ -380,12 +374,11 @@ describe("AgentLogViewer", () => {
entries={entries} entries={entries}
loading={false} loading={false}
executorModel={{ provider: "anthropic" }} executorModel={{ provider: "anthropic" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("shows 'Using default' when executorModel has only modelId but no provider", () => { it("shows 'Using default' when executorModel has only modelId but no provider", () => {
@@ -395,12 +388,11 @@ describe("AgentLogViewer", () => {
entries={entries} entries={entries}
loading={false} loading={false}
executorModel={{ modelId: "claude-sonnet-4-5" }} executorModel={{ modelId: "claude-sonnet-4-5" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("renders model info header with planning model when set", () => { it("renders model info header with planning model when set", () => {
@@ -410,36 +402,34 @@ describe("AgentLogViewer", () => {
entries={entries} entries={entries}
loading={false} loading={false}
planningModel={{ provider: "anthropic", modelId: "claude-opus-4" }} planningModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Planning:");
expect(header!.textContent).toContain("anthropic/claude-opus-4");
// Verify ProviderIcon is rendered for planning
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy(); expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
expect(header!.textContent).not.toContain("Planning/Triage:");
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(header!.textContent).toContain("Planning/Triage:");
expect(header!.textContent).toContain("anthropic/claude-opus-4");
}); });
it("renders 'Using default' for planning when no planning model is set", () => { it("renders 'Using default' for planning when no planning model is set", () => {
const entries = [makeEntry()]; const entries = [makeEntry()];
const { container } = render( const { container } = render(
<AgentLogViewer entries={entries} loading={false} planningModel={null} /> <AgentLogViewer entries={entries} loading={false} planningModel={null} />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Planning:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("renders 'Using default' for planning when planningModel is undefined", () => { it("renders 'Using default' for planning when planningModel is undefined", () => {
const entries = [makeEntry()]; const entries = [makeEntry()];
const { container } = render( const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
<AgentLogViewer entries={entries} loading={false} />
);
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Planning:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default");
}); });
it("renders all three models when all are configured", () => { it("renders all three models when all are configured", () => {
@@ -451,20 +441,21 @@ describe("AgentLogViewer", () => {
executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }} executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
validatorModel={{ provider: "openai", modelId: "gpt-4o" }} validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
planningModel={{ provider: "google", modelId: "gemini-pro" }} planningModel={{ provider: "google", modelId: "gemini-pro" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Executor:");
expect(header!.textContent).toContain("anthropic/claude-opus-4");
expect(header!.textContent).toContain("Validator:");
expect(header!.textContent).toContain("openai/gpt-4o");
expect(header!.textContent).toContain("Planning:");
expect(header!.textContent).toContain("google/gemini-pro");
// Verify all three ProviderIcons are rendered
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy(); expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy(); expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
expect(container.querySelector('[data-provider="google"]')).toBeTruthy(); expect(container.querySelector('[data-provider="google"]')).toBeTruthy();
expect(header!.textContent).not.toContain("anthropic/claude-opus-4");
expect(header!.textContent).not.toContain("openai/gpt-4o");
expect(header!.textContent).not.toContain("google/gemini-pro");
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(header!.textContent).toContain("anthropic/claude-opus-4");
expect(header!.textContent).toContain("openai/gpt-4o");
expect(header!.textContent).toContain("google/gemini-pro");
}); });
it("shows 'Using default' for planning when planningModel has only provider but no modelId", () => { it("shows 'Using default' for planning when planningModel has only provider but no modelId", () => {
@@ -474,12 +465,101 @@ describe("AgentLogViewer", () => {
entries={entries} entries={entries}
loading={false} loading={false}
planningModel={{ provider: "anthropic" }} planningModel={{ provider: "anthropic" }}
/> />,
); );
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("Planning:"); expect(header!.textContent).not.toContain("Using default");
expect(header!.textContent).toContain("Using default"); });
});
describe("model header expand/collapse", () => {
it("shows only provider icons in collapsed state, hides model text", () => {
const entries = [makeEntry()];
const { container } = render(
<AgentLogViewer
entries={entries}
loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
/>,
);
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
expect(screen.getByTestId("agent-log-model-expand")).toBeTruthy();
expect(container.textContent).not.toContain("Executor:");
expect(container.textContent).not.toContain("claude-sonnet-4-5");
});
it("shows model details when expand button is clicked", () => {
const entries = [makeEntry()];
const { container } = render(
<AgentLogViewer
entries={entries}
loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
/>,
);
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(container.textContent).toContain("Executor:");
expect(container.textContent).toContain("anthropic/claude-sonnet-4-5");
});
it("collapses model details when expand button is clicked again", () => {
const entries = [makeEntry()];
const { container } = render(
<AgentLogViewer
entries={entries}
loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
/>,
);
const button = screen.getByTestId("agent-log-model-expand");
fireEvent.click(button);
expect(container.textContent).toContain("Executor:");
fireEvent.click(button);
expect(container.textContent).not.toContain("Executor:");
});
it("shows no provider icons when no model overrides are set", () => {
const entries = [makeEntry()];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
expect(container.querySelector("[data-provider]")).toBeNull();
});
it("has aria-expanded=false when collapsed and aria-expanded=true when expanded", () => {
const entries = [makeEntry()];
render(
<AgentLogViewer
entries={entries}
loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
/>,
);
const button = screen.getByTestId("agent-log-model-expand");
expect(button.getAttribute("aria-expanded")).toBe("false");
fireEvent.click(button);
expect(button.getAttribute("aria-expanded")).toBe("true");
});
it("renders multiple provider icons for multiple overrides", () => {
const entries = [makeEntry()];
const { container } = render(
<AgentLogViewer
entries={entries}
loading={false}
executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
planningModel={{ provider: "google", modelId: "gemini-pro" }}
/>,
);
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
expect(container.querySelector('[data-provider="google"]')).toBeTruthy();
}); });
}); });

View File

@@ -48,9 +48,11 @@ vi.mock("lucide-react", () => ({
MessageSquare: () => null, MessageSquare: () => null,
ChevronUp: () => null, ChevronUp: () => null,
ChevronDown: () => null, ChevronDown: () => null,
ChevronRight: () => null,
X: () => null, X: () => null,
Maximize2: () => null, Maximize2: () => null,
Minimize2: () => null, Minimize2: () => null,
Loader2: () => null,
Bot: () => null, Bot: () => null,
CircleDot: () => null, CircleDot: () => null,
XCircle: () => null, XCircle: () => null,
@@ -1706,23 +1708,32 @@ describe("TaskDetailModal", () => {
); );
} }
it("shows resolved executor from settings when task has no explicit executor override", async () => { async function openAgentLogAndExpandModelDetails(container: HTMLElement) {
const { container } = await setupModelTest({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
fireEvent.click(screen.getByText("Logs")); fireEvent.click(screen.getByText("Logs"));
fireEvent.click(screen.getByText("Agent Log")); fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => { await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
}); });
const expandButton = screen.getByTestId("agent-log-model-expand") as HTMLButtonElement;
if (expandButton.getAttribute("aria-expanded") !== "true") {
fireEvent.click(expandButton);
}
return container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement;
}
it("shows resolved executor from settings when task has no explicit executor override", async () => {
const { container } = await setupModelTest({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
});
const header = await openAgentLogAndExpandModelDetails(container);
// Validator should also fall back to the default // Validator should also fall back to the default
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
}); });
@@ -1734,16 +1745,7 @@ describe("TaskDetailModal", () => {
validatorModelId: "gpt-4o", validatorModelId: "gpt-4o",
}); });
fireEvent.click(screen.getByText("Logs")); const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
expect(header!.textContent).toContain("openai/gpt-4o");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Executor falls back to default // Executor falls back to default
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
// Validator uses the validator-specific setting // Validator uses the validator-specific setting
@@ -1757,18 +1759,9 @@ describe("TaskDetailModal", () => {
// No validatorProvider or validatorModelId // No validatorProvider or validatorModelId
}); });
fireEvent.click(screen.getByText("Logs")); const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
// Both executor and validator should resolve to the default
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
});
// Count occurrences - should appear three times (once for executor, once for validator, once for planning) // Count occurrences - should appear three times (once for executor, once for validator, once for planning)
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g); const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g);
expect(matches).toHaveLength(3); expect(matches).toHaveLength(3);
}); });
@@ -1779,18 +1772,9 @@ describe("TaskDetailModal", () => {
{ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" }, { defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" },
); );
fireEvent.click(screen.getByText("Logs")); const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
// Task override should win
expect(header!.textContent).toContain("openai/gpt-4o");
});
// Default model should not appear for executor // Default model should not appear for executor
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
expect(header.textContent).toContain("openai/gpt-4o"); expect(header.textContent).toContain("openai/gpt-4o");
// Validator falls back to default // Validator falls back to default
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
@@ -1802,17 +1786,7 @@ describe("TaskDetailModal", () => {
{ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" }, { defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" },
); );
fireEvent.click(screen.getByText("Logs")); const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
// Task validator override should win
expect(header!.textContent).toContain("google/gemini-pro");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Executor falls back to default // Executor falls back to default
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
// Settings validator should not appear (task override wins) // Settings validator should not appear (task override wins)
@@ -1824,15 +1798,7 @@ describe("TaskDetailModal", () => {
// No defaultProvider/defaultModelId // No defaultProvider/defaultModelId
}); });
fireEvent.click(screen.getByText("Logs")); const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
expect(header.textContent).toContain("Using default"); expect(header.textContent).toContain("Using default");
// Should show "Using default" for executor, validator, and planning // Should show "Using default" for executor, validator, and planning
const defaultBadges = header.querySelectorAll(".model-badge-default"); const defaultBadges = header.querySelectorAll(".model-badge-default");
@@ -1862,16 +1828,8 @@ describe("TaskDetailModal", () => {
/>, />,
); );
fireEvent.click(screen.getByText("Logs"));
fireEvent.click(screen.getByText("Agent Log"));
// Wait for the failed fetch to settle // Wait for the failed fetch to settle
await waitFor(() => { const header = await openAgentLogAndExpandModelDetails(container);
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
expect(header.textContent).toContain("Using default"); expect(header.textContent).toContain("Using default");
const defaultBadges = header.querySelectorAll(".model-badge-default"); const defaultBadges = header.querySelectorAll(".model-badge-default");
expect(defaultBadges).toHaveLength(3); expect(defaultBadges).toHaveLength(3);
@@ -1892,16 +1850,7 @@ describe("TaskDetailModal", () => {
}, },
); );
fireEvent.click(screen.getByText("Logs")); const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
expect(header!.textContent).toContain("google/gemini-pro");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Executor uses task override // Executor uses task override
expect(header.textContent).toContain("google/gemini-pro"); expect(header.textContent).toContain("google/gemini-pro");
// Validator uses settings-specific validator // Validator uses settings-specific validator
@@ -1943,19 +1892,11 @@ describe("TaskDetailModal", () => {
/>, />,
); );
// Navigate to Agent Log subview const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Logs"));
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => { // Planning should show the runtime triage marker, not settings default
const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header.textContent).toContain("Planning/Triage:");
expect(header).toBeTruthy(); expect(header.textContent).toContain("google/gemini-pro");
// Planning should show the runtime triage marker, not settings default
expect(header!.textContent).toContain("Planning:");
expect(header!.textContent).toContain("google/gemini-pro");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Executor/Validator should still show settings default // Executor/Validator should still show settings default
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
}); });
@@ -1992,19 +1933,11 @@ describe("TaskDetailModal", () => {
/>, />,
); );
// Navigate to Agent Log subview const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Logs"));
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => { // Planning should use planningProvider/planningModelId from settings
const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header.textContent).toContain("Planning/Triage:");
expect(header).toBeTruthy(); expect(header.textContent).toContain("openai/gpt-4o");
// Planning should use planningProvider/planningModelId from settings
expect(header!.textContent).toContain("Planning:");
expect(header!.textContent).toContain("openai/gpt-4o");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Executor/Validator should show default // Executor/Validator should show default
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
// Planning should NOT show the default // Planning should NOT show the default
@@ -2017,19 +1950,12 @@ describe("TaskDetailModal", () => {
defaultModelId: "claude-sonnet-4-5", defaultModelId: "claude-sonnet-4-5",
}); });
// Navigate to Agent Log subview const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Logs"));
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => { expect(header.textContent).toContain("Planning/Triage:");
const header = container.querySelector("[data-testid='agent-log-model-header']"); expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
expect(header).toBeTruthy();
expect(header!.textContent).toContain("Planning:");
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
});
// Planning falls back to default - same as executor/validator // Planning falls back to default - same as executor/validator
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g); const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g);
expect(matches).toHaveLength(3); // executor, validator, planning expect(matches).toHaveLength(3); // executor, validator, planning
}); });
@@ -2039,17 +1965,8 @@ describe("TaskDetailModal", () => {
// No defaultProvider/defaultModelId // No defaultProvider/defaultModelId
}); });
// Navigate to Agent Log subview const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Logs")); expect(header.textContent).toContain("Planning/Triage:");
fireEvent.click(screen.getByText("Agent Log"));
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
expect(header.textContent).toContain("Planning:");
const defaultBadges = header.querySelectorAll(".model-badge-default"); const defaultBadges = header.querySelectorAll(".model-badge-default");
// 3 default badges: executor, validator, planning // 3 default badges: executor, validator, planning
expect(defaultBadges).toHaveLength(3); expect(defaultBadges).toHaveLength(3);
@@ -2091,19 +2008,10 @@ describe("TaskDetailModal", () => {
/>, />,
); );
// Navigate to Agent Log subview const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Logs")); // Per-task override should take precedence over settings
fireEvent.click(screen.getByText("Agent Log")); expect(header.textContent).toContain("Planning/Triage:");
expect(header.textContent).toContain("google/gemini-2.5-pro");
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
// Per-task override should take precedence over settings
expect(header!.textContent).toContain("Planning:");
expect(header!.textContent).toContain("google/gemini-2.5-pro");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Should NOT show the settings planning model // Should NOT show the settings planning model
expect(header.textContent).not.toContain("openai/gpt-4o"); expect(header.textContent).not.toContain("openai/gpt-4o");
}); });
@@ -2143,18 +2051,9 @@ describe("TaskDetailModal", () => {
/>, />,
); );
// Navigate to Agent Log subview const header = await openAgentLogAndExpandModelDetails(container);
fireEvent.click(screen.getByText("Logs")); // Runtime marker should win over planning settings
fireEvent.click(screen.getByText("Agent Log")); expect(header.textContent).toContain("google/gemini-pro");
await waitFor(() => {
const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy();
// Runtime marker should win over planning settings
expect(header!.textContent).toContain("google/gemini-pro");
});
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
// Should NOT show the planning settings model // Should NOT show the planning settings model
expect(header.textContent).not.toContain("openai/gpt-4o"); expect(header.textContent).not.toContain("openai/gpt-4o");
}); });