feat(FN-3377): compress mobile agent detail header
- Rework AgentDetailView mobile header structure to reduce vertical space while preserving key metadata - Simplify AgentsView mobile spacing and remove redundant styles tied to legacy header layout - Update dashboard CSS class coverage tests for new agent detail/mobile header classes - Expand AgentDetailView and AgentsView tests to validate condensed header rendering and interaction behavior Fusion-Task-Id: FN-3377
This commit is contained in:
@@ -156,6 +156,11 @@ describe("Agent CSS classes", () => {
|
||||
expect(viewContent).toContain("min-height: 0");
|
||||
});
|
||||
|
||||
it("removes legacy standalone mobile back-row classes from AgentsView", () => {
|
||||
expect(hasClass(".agents-mobile-back-row")).toBe(false);
|
||||
expect(hasClass(".agents-mobile-back-btn")).toBe(false);
|
||||
});
|
||||
|
||||
it("should visually group the filter controls", () => {
|
||||
const filtersBlock = extractRuleBlock(".agent-controls-filters");
|
||||
expect(filtersBlock).toContain("padding: var(--space-xs) var(--space-sm)");
|
||||
@@ -196,6 +201,7 @@ describe("Agent CSS classes", () => {
|
||||
expect(hasClass(".agent-detail-actions")).toBe(true);
|
||||
// Redesigned compact header structure
|
||||
expect(hasClass(".agent-detail-identity")).toBe(true);
|
||||
expect(hasClass(".agent-detail-inline-back")).toBe(true);
|
||||
expect(hasClass(".agent-detail-controls")).toBe(true);
|
||||
expect(hasClass(".agent-detail-utility-actions")).toBe(true);
|
||||
expect(hasClass(".agent-detail-tabs")).toBe(true);
|
||||
|
||||
@@ -94,6 +94,15 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-detail-inline-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
min-height: calc(var(--space-lg) + var(--space-md) + var(--space-xs));
|
||||
padding: calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-sm) + var(--space-xs) * 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-detail-icon {
|
||||
width: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||
height: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||
@@ -1304,13 +1313,16 @@
|
||||
}
|
||||
|
||||
.agent-detail-header {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
padding-top: max(var(--space-md), env(safe-area-inset-top, 0));
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
padding-top: max(var(--space-sm), env(safe-area-inset-top, 0));
|
||||
}
|
||||
|
||||
.agent-detail-identity {
|
||||
grid-row: 1;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
gap: var(--space-sm);
|
||||
@@ -1339,17 +1351,16 @@
|
||||
}
|
||||
|
||||
.agent-detail-header-actions {
|
||||
flex: 1 1 100%;
|
||||
flex-wrap: wrap;
|
||||
grid-row: 2;
|
||||
flex: 1 1 auto;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
order: 3;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.agent-detail-controls {
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--space-xs) + var(--space-sm) * 0.25);
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.agent-detail-controls .btn--compact {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Bot, Heart, Activity, Pause, Play, Square, Trash2, RefreshCw,
|
||||
Settings, FileText, ActivitySquare, X, Copy,
|
||||
ExternalLink, CheckCircle, XCircle, Loader2, GitBranch, ListChecks,
|
||||
ChevronDown, ChevronRight, BarChart3, BookOpen, Eye, FileEdit
|
||||
ChevronDown, ChevronRight, ChevronLeft, BarChart3, BookOpen, Eye, FileEdit
|
||||
} from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -62,6 +62,7 @@ interface AgentDetailViewProps {
|
||||
addToast: (message: string, type?: "success" | "error") => void;
|
||||
onChildClick?: (childId: string) => void;
|
||||
inline?: boolean;
|
||||
showInlineBackButton?: boolean;
|
||||
initialTab?: TabId;
|
||||
initialRunId?: string | null;
|
||||
preferActiveRun?: boolean;
|
||||
@@ -122,7 +123,7 @@ function pickDefaultAgentMemoryPath(files: MemoryFileInfo[], currentPath: string
|
||||
?? "";
|
||||
}
|
||||
|
||||
export function AgentDetailView({ agentId, projectId, onClose, addToast, onChildClick, inline = false, initialTab, initialRunId, preferActiveRun = false }: AgentDetailViewProps) {
|
||||
export function AgentDetailView({ agentId, projectId, onClose, addToast, onChildClick, inline = false, showInlineBackButton = false, initialTab, initialRunId, preferActiveRun = false }: AgentDetailViewProps) {
|
||||
const [agent, setAgent] = useState<AgentDetail | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
const [logs, setLogs] = useState<AgentLogEntry[]>([]);
|
||||
@@ -493,6 +494,17 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
|
||||
<div className="agent-detail-header">
|
||||
{/* Identity area: icon + name + badges */}
|
||||
<div className="agent-detail-identity">
|
||||
{inline && showInlineBackButton ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn agent-detail-inline-back"
|
||||
onClick={onClose}
|
||||
aria-label="Back to agents"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
Agents
|
||||
</button>
|
||||
) : null}
|
||||
<div className="agent-detail-icon">
|
||||
<Bot size={20} />
|
||||
</div>
|
||||
|
||||
@@ -1178,16 +1178,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agents-mobile-back-row {
|
||||
padding: var(--space-md);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.agents-mobile-back-btn {
|
||||
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||
}
|
||||
|
||||
.agents-view-content {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./AgentsView.css";
|
||||
import { useState, useEffect, useCallback, useRef, useMemo, useId, lazy, Suspense } from "react";
|
||||
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, List, ChevronRight, ChevronLeft, ChevronDown, ChevronUp, Filter, Upload, Network, SlidersHorizontal, Copy, Check } from "lucide-react";
|
||||
import { Plus, Play, Pause, Activity, Trash2, RefreshCw, Bot, List, ChevronRight, ChevronDown, ChevronUp, Filter, Upload, Network, SlidersHorizontal, Copy, Check } from "lucide-react";
|
||||
import type { Agent, AgentCapability, AgentOnboardingSummary, AgentState, OrgTreeNode } from "../api";
|
||||
import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree, fetchSettings, updateSettings } from "../api";
|
||||
|
||||
@@ -1409,18 +1409,11 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
</div>
|
||||
|
||||
<div className={`agents-split-detail${isMobileViewport && !selectedAgentId ? " agents-split-detail--hidden-mobile" : ""}`}>
|
||||
{isMobileDetailOpen && (
|
||||
<div className="agents-mobile-back-row">
|
||||
<button className="btn agents-mobile-back-btn" onClick={handleCloseDetail}>
|
||||
<ChevronLeft size={16} />
|
||||
Agents
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedAgentId ? (
|
||||
<Suspense fallback={null}>
|
||||
<AgentDetailView
|
||||
inline
|
||||
showInlineBackButton={isMobileViewport}
|
||||
agentId={selectedAgentId}
|
||||
projectId={projectId}
|
||||
onClose={handleCloseDetail}
|
||||
|
||||
@@ -276,9 +276,33 @@ describe("AgentDetailView", () => {
|
||||
|
||||
expect(document.querySelector(".agent-detail-overlay")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Close" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Back to agents" })).toBeNull();
|
||||
expect(screen.getByRole("heading", { name: "Test Agent" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders inline mobile back affordance inside detail header when enabled", async () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={onClose}
|
||||
addToast={vi.fn()}
|
||||
inline
|
||||
showInlineBackButton
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Back to agents")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Back to agents"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
const identityContainer = document.querySelector(".agent-detail-identity");
|
||||
expect(identityContainer?.querySelector(".agent-detail-inline-back")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps modal mode as dialog with close button", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
@@ -848,14 +872,20 @@ describe("AgentDetailView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps desktop header actions on one row and mobile wraps them safely", () => {
|
||||
it("keeps desktop header actions on one row and mobile uses explicit two-row header contracts", () => {
|
||||
const stylesContent = loadAllAppCss();
|
||||
|
||||
expect(stylesContent).toContain(".agent-detail-header-actions {");
|
||||
expect(stylesContent).toContain("justify-content: flex-end;");
|
||||
expect(stylesContent).toContain(".agent-detail-inline-back {");
|
||||
|
||||
const mobileHeaderActionsBlock = /@media \(max-width: 768px\)\s*\{[\s\S]*?\.agent-detail-header-actions\s*\{[\s\S]*?flex-wrap: wrap;[\s\S]*?\}/;
|
||||
expect(stylesContent).toMatch(mobileHeaderActionsBlock);
|
||||
expect(stylesContent).toContain("@media (max-width: 768px)");
|
||||
expect(stylesContent).toContain(".agent-detail-header {");
|
||||
expect(stylesContent).toContain("grid-template-rows: auto auto;");
|
||||
expect(stylesContent).toContain(".agent-detail-identity {");
|
||||
expect(stylesContent).toContain("grid-row: 1;");
|
||||
expect(stylesContent).toContain(".agent-detail-header-actions {");
|
||||
expect(stylesContent).toContain("grid-row: 2;");
|
||||
});
|
||||
|
||||
it("shows statistics section on dashboard", async () => {
|
||||
|
||||
@@ -31,8 +31,11 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../AgentDetailView", () => ({
|
||||
AgentDetailView: ({ agentId, inline, initialTab, initialRunId, preferActiveRun }: { agentId: string; inline?: boolean; initialTab?: string; initialRunId?: string | null; preferActiveRun?: boolean }) => (
|
||||
AgentDetailView: ({ agentId, inline, onClose, showInlineBackButton, initialTab, initialRunId, preferActiveRun }: { agentId: string; inline?: boolean; onClose?: () => void; showInlineBackButton?: boolean; initialTab?: string; initialRunId?: string | null; preferActiveRun?: boolean }) => (
|
||||
<div data-testid="agent-detail-view" data-inline={inline ? "true" : "false"} data-initial-tab={initialTab ?? "dashboard"} data-initial-run-id={initialRunId ?? ""} data-prefer-active-run={preferActiveRun ? "true" : "false"}>
|
||||
{showInlineBackButton ? (
|
||||
<button type="button" aria-label="Back to agents" onClick={onClose}>Agents</button>
|
||||
) : null}
|
||||
Agent detail: {agentId}
|
||||
</div>
|
||||
),
|
||||
@@ -325,14 +328,14 @@ describe("AgentsView", () => {
|
||||
fireEvent.click(await screen.findByRole("button", { name: "View details for Test Agent 1" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Agents" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Back to agents" })).toBeTruthy();
|
||||
expect(screen.getByTestId("agent-detail-view")).toHaveAttribute("data-inline", "true");
|
||||
});
|
||||
|
||||
expect(container.querySelector(".agents-split-sidebar--hidden-mobile")).toBeTruthy();
|
||||
expect(container.querySelector(".agents-split-detail--hidden-mobile")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Agents" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back to agents" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Select an agent")).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user