fix: align responsive agent views (#3688)

## Summary
- Keep the Agents header full width while cards reflow independently.
- Collapse header actions into an overflow menu at the compact layout
threshold.
- Apply the same responsive grid rules to Agent Teams.

## Validation
- `pnpm -C desktop build:e2e`
- Focused Agents Playwright coverage
- Pre-push desktop checks and unit tests

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
klopez4212
2026-07-30 15:58:48 +01:00
committed by GitHub
parent d0a24bcb52
commit 73589408db
4 changed files with 223 additions and 44 deletions
+93 -31
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { OctagonX, Settings2 } from "lucide-react";
import { EllipsisVertical, OctagonX, Settings2 } from "lucide-react";
import {
consumePendingSnapshotImport,
subscribeSnapshotImport,
@@ -20,10 +20,7 @@ import { SecretRevealDialog } from "./SecretRevealDialog";
import { TeamDeleteDialog } from "./TeamDeleteDialog";
import { TeamDialog } from "./TeamDialog";
import { TeamsSection } from "./TeamsSection";
import {
AGENT_CARD_GRID_COLUMNS_CLASS,
UnifiedAgentsSection,
} from "./UnifiedAgentsSection";
import { UnifiedAgentsSection } from "./UnifiedAgentsSection";
import { useManagedAgentActions } from "./useManagedAgentActions";
import { usePersonaActions } from "./usePersonaActions";
import { useTeamActions } from "./useTeamActions";
@@ -32,6 +29,12 @@ import { useBakedBuildEnvQuery } from "@/features/agents/hooks";
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { PageHeader } from "@/shared/ui/PageHeader";
import { getInheritedAgentDefaults } from "./bakedEnvHelpers";
@@ -44,6 +47,8 @@ export function AgentsView() {
const personas = usePersonaActions();
const teamImportInputRef = React.useRef<HTMLInputElement | null>(null);
const aiDefaultsTriggerRef = React.useRef<HTMLButtonElement>(null);
const fullAiDefaultsTriggerRef = React.useRef<HTMLButtonElement>(null);
const compactActionsTriggerRef = React.useRef<HTMLButtonElement>(null);
const [isAiDefaultsOpen, setIsAiDefaultsOpen] = React.useState(false);
// Exclusivity: create never sets `personaDialogState` (edit/dup/import do),
// so the create-mode and definition-edit AgentDialog mounts never coexist.
@@ -53,6 +58,22 @@ export function AgentsView() {
personas.prepareCreate();
setIsCreateDialogOpen(true);
}
function openAiDefaults(trigger: HTMLButtonElement | null) {
aiDefaultsTriggerRef.current = trigger;
setIsAiDefaultsOpen(true);
}
function setAiDefaultsDialogOpen(open: boolean) {
if (!open) {
aiDefaultsTriggerRef.current =
fullAiDefaultsTriggerRef.current?.offsetParent !== null
? fullAiDefaultsTriggerRef.current
: compactActionsTriggerRef.current;
}
setIsAiDefaultsOpen(open);
}
const teamActions = useTeamActions(
{
setActionNoticeMessage: agents.setActionNoticeMessage,
@@ -113,43 +134,84 @@ export function AgentsView() {
<>
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-7 sm:px-6 sm:py-8">
<div
className={`mx-auto grid w-full max-w-6xl ${AGENT_CARD_GRID_COLUMNS_CLASS} justify-start gap-x-3 gap-y-8`}
className="mx-auto w-full max-w-6xl space-y-8 [container-type:inline-size]"
data-testid="agents-page-content"
>
<PageHeader
className="col-[1/-1]"
action={
<div className="flex flex-wrap justify-end gap-2">
<Button
data-testid="agent-defaults-button"
ref={aiDefaultsTriggerRef}
onClick={() => setIsAiDefaultsOpen(true)}
size="sm"
variant="outline"
>
<Settings2 />
{hasSavedAgentDefaults
? "Agent defaults"
: "Set agent defaults"}
</Button>
{runningAgentCount > 0 ? (
<>
<div className="flex flex-wrap justify-end gap-2 [@container(max-width:40rem)]:hidden">
<Button
disabled={isActionPending}
onClick={() => {
void agents.handleBulkStopRunning();
}}
data-testid="agent-defaults-button"
ref={fullAiDefaultsTriggerRef}
onClick={(event) => openAiDefaults(event.currentTarget)}
size="sm"
variant="outline"
>
<OctagonX />
Stop running agents
<Settings2 />
{hasSavedAgentDefaults
? "Agent defaults"
: "Set agent defaults"}
</Button>
) : null}
</div>
{runningAgentCount > 0 ? (
<Button
disabled={isActionPending}
onClick={() => {
void agents.handleBulkStopRunning();
}}
size="sm"
variant="outline"
>
<OctagonX />
Stop running agents
</Button>
) : null}
</div>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button
aria-label="Agent actions"
className="hidden [@container(max-width:40rem)]:inline-flex"
data-testid="agent-actions-menu-trigger"
ref={compactActionsTriggerRef}
size="icon"
type="button"
variant="outline"
>
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onSelect={() => {
openAiDefaults(compactActionsTriggerRef.current);
}}
>
<Settings2 />
{hasSavedAgentDefaults
? "Agent defaults"
: "Set agent defaults"}
</DropdownMenuItem>
{runningAgentCount > 0 ? (
<DropdownMenuItem
disabled={isActionPending}
onSelect={() => {
void agents.handleBulkStopRunning();
}}
>
<OctagonX />
Stop running agents
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
</>
}
description="Set up and manage your agents."
title="Agents"
/>
<div className="col-[1/-1] flex flex-col gap-8">
<div className="flex flex-col gap-8">
<UnifiedAgentsSection
defaultModel={inheritedDefaults.model.value}
actionErrorMessage={agents.actionErrorMessage}
@@ -238,7 +300,7 @@ export function AgentsView() {
</div>
<AgentDefaultsDialog
onOpenChange={setIsAiDefaultsOpen}
onOpenChange={setAiDefaultsDialogOpen}
open={isAiDefaultsOpen}
returnFocusRef={aiDefaultsTriggerRef}
/>
@@ -20,9 +20,9 @@ import { IdentityCardSkeleton } from "@/shared/ui/identity-card-skeleton";
import { SectionHeader } from "@/shared/ui/PageHeader";
import { CreateIdentityCard } from "./CreateIdentityCard";
import { TeamIdentityCard } from "./TeamIdentityCard";
import { IDENTITY_CARD_GRID_CLASS } from "./UnifiedAgentsSection";
const TEAM_CARD_COLUMN_CLASS = "w-full";
const TEAM_CARD_GRID_CLASS = `${TEAM_CARD_COLUMN_CLASS} mx-auto grid max-w-[996px] grid-cols-[repeat(auto-fill,minmax(220px,240px))] justify-center gap-3`;
type TeamsSectionProps = {
teams: AgentTeam[];
@@ -63,7 +63,7 @@ export function TeamsSection({
</div>
{isLoading ? (
<div className={TEAM_CARD_GRID_CLASS}>
<div className={IDENTITY_CARD_GRID_CLASS}>
<IdentityCardSkeleton
footerSubtitleWidthClass="w-14"
footerTitleWidthClass="w-24"
@@ -83,7 +83,7 @@ export function TeamsSection({
) : null}
{!isLoading ? (
<div className={TEAM_CARD_GRID_CLASS}>
<div className={IDENTITY_CARD_GRID_CLASS}>
{teams.map((team) => {
const resolution = resolveTeamPersonas(team, personas);
const missingPersonaCount = resolution.missingPersonaCount;
@@ -68,7 +68,7 @@ type UnifiedAgentsSectionProps = {
const AGENT_CARD_COLUMN_CLASS = "w-full";
export const AGENT_CARD_GRID_COLUMNS_CLASS =
"grid-cols-[repeat(auto-fill,minmax(220px,240px))]";
const AGENT_CARD_GRID_CLASS = `${AGENT_CARD_COLUMN_CLASS} ${AGENT_CARD_GRID_COLUMNS_CLASS} grid justify-start gap-3`;
export const IDENTITY_CARD_GRID_CLASS = `${AGENT_CARD_COLUMN_CLASS} ${AGENT_CARD_GRID_COLUMNS_CLASS} grid justify-start gap-3 [@container(max-width:40rem)]:justify-center`;
export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
const {
@@ -153,7 +153,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
{!isLoading ? (
<div className="space-y-3" data-testid="unified-agents-groups">
<div className={AGENT_CARD_GRID_CLASS}>
<div className={IDENTITY_CARD_GRID_CLASS}>
{groups.map((group) => {
const profileAgent = pickProfileAgent(group.agents);
return (
@@ -479,7 +479,7 @@ function NewAgentCard({
function LoadingSkeleton() {
return (
<div className={AGENT_CARD_GRID_CLASS}>
<div className={IDENTITY_CARD_GRID_CLASS}>
<IdentityCardSkeleton
footerSubtitleWidthClass="w-14"
footerTitleWidthClass="w-24"
@@ -537,7 +537,7 @@ function CollapsibleAgentGroup({
<span className="text-xs text-muted-foreground">({agents.length})</span>
</button>
{!isCollapsed ? (
<div className={AGENT_CARD_GRID_CLASS}>
<div className={IDENTITY_CARD_GRID_CLASS}>
{agents.map((agent) => (
<StandaloneAgentCard
agent={agent}
+123 -6
View File
@@ -416,7 +416,7 @@ test("the new agent card offers create, discover, and import", async ({
const cardBoxes = await agentCards.evaluateAll((cards) =>
cards.map((card) => {
const box = card.getBoundingClientRect();
return { right: box.right, top: box.top };
return { left: box.left, right: box.right, top: box.top };
}),
);
const firstRowTop = Math.min(...cardBoxes.map(({ top }) => top));
@@ -425,12 +425,16 @@ test("the new agent card offers create, discover, and import", async ({
.filter(({ top }) => Math.abs(top - firstRowTop) < 1)
.map(({ right }) => right),
);
const leftmostFirstRowCard = Math.min(
...cardBoxes
.filter(({ top }) => Math.abs(top - firstRowTop) < 1)
.map(({ left }) => left),
);
expect(headerBox).not.toBeNull();
expect(
Math.abs(
(headerBox?.x ?? 0) + (headerBox?.width ?? 0) - rightmostFirstRowCard,
),
).toBeLessThan(1);
expect(Math.abs((headerBox?.x ?? 0) - leftmostFirstRowCard)).toBeLessThan(1);
expect(rightmostFirstRowCard).toBeLessThanOrEqual(
(headerBox?.x ?? 0) + (headerBox?.width ?? 0) + 1,
);
await newAgentCard.click();
await expect(
@@ -492,6 +496,63 @@ test("the new team card offers create and import", async ({ page }) => {
).toBeVisible();
});
test("team cards follow the agents grid alignment at compact widths", async ({
page,
}) => {
await installMockBridge(page, {
personas: [
{
id: "custom:team-layout",
displayName: "Team layout agent",
systemPrompt: "A test agent for team layout alignment.",
},
],
teams: [
{
id: "team-layout",
name: "Team layout",
personaIds: ["custom:team-layout"],
},
],
});
await gotoApp(page);
await page.getByTestId("open-agents-view").click();
const agentsContent = page.getByTestId("agents-page-content");
const firstAgentCard = page.getByTestId(
"persona-agent-row-custom:team-layout",
);
const firstTeamCard = page.getByTestId("team-card-team-layout");
const agentGrid = firstAgentCard.locator("xpath=..");
const teamGrid = firstTeamCard.locator("xpath=..");
const firstAgentGridCard = agentGrid.locator(":scope > *").first();
const firstTeamGridCard = teamGrid.locator(":scope > *").first();
await agentsContent.evaluate((element) => {
(element as HTMLElement).style.width = "650px";
});
const wideAgentBox = await firstAgentGridCard.boundingBox();
const wideTeamBox = await firstTeamGridCard.boundingBox();
expect(wideAgentBox).not.toBeNull();
expect(wideTeamBox).not.toBeNull();
expect(Math.abs((wideAgentBox?.x ?? 0) - (wideTeamBox?.x ?? 0))).toBeLessThan(
1,
);
await agentsContent.evaluate((element) => {
(element as HTMLElement).style.width = "600px";
});
await expect
.poll(async () => (await firstAgentGridCard.boundingBox())?.x ?? 0)
.toBeGreaterThan(wideAgentBox?.x ?? 0);
const compactAgentBox = await firstAgentGridCard.boundingBox();
const compactTeamBox = await firstTeamGridCard.boundingBox();
expect(
Math.abs((compactAgentBox?.x ?? 0) - (compactTeamBox?.x ?? 0)),
).toBeLessThan(1);
});
test("team cards use the thread-style overlapping avatar stack", async ({
page,
}) => {
@@ -634,6 +695,62 @@ test("unconfigured agent defaults use the setup label", async ({ page }) => {
);
});
test("moves agent actions into an overflow menu in a narrow view", async ({
page,
}) => {
await installMockBridge(page, {
personas: [
{
id: "custom:compact-actions",
displayName: "Compact actions agent",
isActive: true,
systemPrompt: "A test agent for compact header actions.",
},
],
managedAgents: [
{
name: "Compact actions instance",
personaId: "custom:compact-actions",
pubkey: "cd".repeat(32),
status: "running",
},
],
});
await gotoApp(page);
await page.getByTestId("open-agents-view").click();
await page.getByTestId("agents-page-content").evaluate((element) => {
(element as HTMLElement).style.width = "650px";
});
await expect(page.getByTestId("agent-defaults-button")).toBeVisible();
await expect(
page.getByText("Set up and manage your agents.", { exact: true }),
).toHaveJSProperty("scrollHeight", 24);
await page.getByTestId("agents-page-content").evaluate((element) => {
(element as HTMLElement).style.width = "600px";
});
await expect(page.getByTestId("agent-defaults-button")).toBeHidden();
await page.getByTestId("agent-actions-menu-trigger").click();
await expect(
page.getByRole("menuitem", { name: "Set agent defaults" }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: "Stop running agents" }),
).toBeVisible();
await page.getByRole("menuitem", { name: "Set agent defaults" }).click();
await expect(page.getByTestId("agent-ai-defaults-dialog")).toBeVisible();
await page.getByTestId("agents-page-content").evaluate((element) => {
(element as HTMLElement).style.width = "650px";
});
await expect(page.getByTestId("agent-defaults-button")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByTestId("agent-ai-defaults-dialog")).toHaveCount(0);
await expect(page.getByTestId("agent-defaults-button")).toBeFocused();
});
test("agent catalog chooser order stays stable when selection changes", async ({
page,
}) => {