feat(web): streamline agent settings profiles (#2620)

This commit is contained in:
Carlos Florêncio
2026-08-13 12:38:44 +01:00
committed by GitHub
parent 45c9ba5554
commit c745bf399c
12 changed files with 584 additions and 49 deletions
+14 -7
View File
@@ -77,27 +77,24 @@ function InstalledAgentsHeader({
<h3 className="text-lg font-semibold">{t("agents:installedAgents")}</h3>
<p className="text-sm text-muted-foreground">{t("agents:installedAgentsDescription")}</p>
</div>
<div className="flex w-full flex-wrap gap-2 sm:w-auto">
<div className="flex w-full flex-wrap gap-2 sm:w-auto" data-testid="installed-agents-actions">
<Button
variant="outline"
size="sm"
onClick={onOpenShell}
className="cursor-pointer"
className="h-11 cursor-pointer md:h-6"
data-testid="open-host-shell"
>
<IconTerminal2 className="h-4 w-4 mr-2" />
{t("agents:terminal")}
</Button>
<Button variant="outline" size="sm" onClick={onOpenTuiDialog} className="cursor-pointer">
<IconPlus className="h-4 w-4 mr-2" />
{t("agents:addTuiAgent")}
</Button>
<Button
variant="outline"
size="sm"
onClick={onRescan}
disabled={rescanning}
className="cursor-pointer"
className="h-11 cursor-pointer md:h-6"
data-testid="rescan-agents-button"
>
{rescanning ? (
<IconLoader2 className="h-4 w-4 mr-2 animate-spin" />
@@ -106,6 +103,16 @@ function InstalledAgentsHeader({
)}
{t("agents:rescan")}
</Button>
<Button
variant="outline"
size="sm"
onClick={onOpenTuiDialog}
className="h-11 cursor-pointer md:h-6"
data-testid="new-agent-button"
>
<IconPlus className="h-4 w-4 mr-2" />
{t("agents:addTuiAgent")}
</Button>
</div>
</div>
);
@@ -21,6 +21,8 @@ const AGENT = {
profiles: [profile("p-1", "Alpha"), profile("p-2", "Beta")],
} as unknown as Agent;
const EMPTY_AGENT = { ...AGENT, profiles: [] } as unknown as Agent;
// A minimal store: the component must read it at write time, so the test needs
// real read-after-write semantics rather than a frozen snapshot.
let storeState: {
@@ -100,7 +102,7 @@ vi.mock("@/components/settings/agent-profile-delete-dialog", () => ({
) : null,
}));
import { ProfileRow } from "./agent-profiles-section";
import { AgentProfilesSubList, ProfileRow } from "./agent-profiles-section";
function renderRows() {
return render(
@@ -191,3 +193,28 @@ describe("ProfileRow duplicate", () => {
await waitFor(() => expect(mocks.duplicateAgentProfileAction).toHaveBeenCalledWith("p-1"));
});
});
describe("AgentProfilesSubList layout", () => {
beforeEach(() => cleanup());
afterEach(() => cleanup());
it("renders profile rows without the count or create action", () => {
render(<AgentProfilesSubList savedAgent={AGENT} agentName="claude" />);
expect(screen.queryByText("2 profiles", { exact: true })).toBeNull();
expect(screen.getAllByTestId("agent-profile-row")).toHaveLength(2);
expect(screen.queryByTestId("new-profile-claude")).toBeNull();
});
it("omits the profile body when no agent record exists", () => {
render(<AgentProfilesSubList savedAgent={undefined} agentName="claude" />);
expect(screen.queryByTestId("agent-profiles-claude")).toBeNull();
});
it("omits the profile body when the saved agent has no profiles", () => {
render(<AgentProfilesSubList savedAgent={EMPTY_AGENT} agentName="claude" />);
expect(screen.queryByTestId("agent-profiles-claude")).toBeNull();
});
});
@@ -2,7 +2,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { IconCopy, IconDotsVertical, IconPlus, IconTrash } from "@tabler/icons-react";
import { IconCopy, IconDotsVertical, IconTrash } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import { Button } from "@kandev/ui/button";
import { Card, CardContent } from "@kandev/ui/card";
@@ -24,18 +24,14 @@ import type { Agent, AgentProfile } from "@/lib/types/http";
import { RecordDot } from "@/components/settings/record-dot";
import { DisabledBadge } from "@/components/settings/record-badges";
function agentSetupHref(agentName: string): string {
return `/settings/agents/${encodeURIComponent(agentName)}?mode=create`;
}
function profileHref(agentName: string, profileId: string): string {
return `/settings/agents/${encodeURIComponent(agentName)}/profiles/${encodeURIComponent(profileId)}`;
}
/**
* An agent's profiles inside its group card on the Agents page: a contrasted
* body with a count, a prominent "New profile" action, and one clickable row
* per profile (no agent branding — the group header already names the agent).
* An agent's profiles inside its group card on the Agents page: one clickable
* row per profile (no agent branding or duplicate creation controls because
* the group header already names the agent and owns its action).
*/
export function AgentProfilesSubList({
savedAgent,
@@ -44,35 +40,18 @@ export function AgentProfilesSubList({
savedAgent: Agent | undefined;
agentName: string;
}) {
const { t } = useTranslation();
const profiles = savedAgent?.profiles ?? [];
if (!savedAgent || savedAgent.profiles.length === 0) return null;
return (
<div
className="border-t border-border/70 bg-background p-3 space-y-2"
className="border-t border-border/70 bg-background p-3"
data-testid={`agent-profiles-${agentName}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-muted-foreground">
{profiles.length === 0
? t("agents:noProfilesYet")
: t("agents:profileCount", { count: profiles.length })}
</span>
<Button size="sm" className="cursor-pointer" asChild>
<Link href={agentSetupHref(agentName)} data-testid={`new-profile-${agentName}`}>
<IconPlus className="h-4 w-4 mr-2" />
{t("agents:newProfile")}
</Link>
</Button>
<div className="grid gap-2">
{savedAgent.profiles.map((profile) => (
<ProfileRow key={profile.id} agent={savedAgent} profile={profile} />
))}
</div>
{/* Narrowed on `savedAgent` rather than on `profiles.length`: the rows
need the agent itself, and only this check proves it is there. */}
{savedAgent && savedAgent.profiles.length > 0 && (
<div className="grid gap-2">
{savedAgent.profiles.map((profile) => (
<ProfileRow key={profile.id} agent={savedAgent} profile={profile} />
))}
</div>
)}
</div>
);
}
@@ -0,0 +1,67 @@
import { cleanup, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Agent, AgentDiscovery } from "@/lib/types/http";
vi.mock("@/components/routing/app-link", () => ({
default: ({ children, ...props }: { children?: ReactNode }) => <a {...props}>{children}</a>,
}));
vi.mock("@/components/agent-logo", () => ({
AgentLogo: () => <span data-testid="agent-logo" />,
}));
vi.mock("@/components/settings/host-shell-dialog", () => ({
HostShellDialog: () => null,
}));
vi.mock("@/components/settings/agent-login-dialog", () => ({
AgentLoginDialog: () => null,
}));
vi.mock("@/components/settings/agent-runtime-update-control", () => ({
AgentRuntimeUpdateControl: () => null,
}));
import { InstalledAgentCard } from "./installed-agent-card";
const DISCOVERY = {
name: "mock-agent",
supports_mcp: false,
available: true,
matched_path: null,
login_command: undefined,
} as unknown as AgentDiscovery;
const SAVED_WITHOUT_PROFILES = {
id: "agent-1",
name: "mock-agent",
profiles: [],
} as unknown as Agent;
function renderCard(savedAgent?: Agent) {
return render(
<InstalledAgentCard agent={DISCOVERY} savedAgent={savedAgent} displayName="Mock Agent" />,
);
}
afterEach(() => cleanup());
describe("InstalledAgentCard setup links", () => {
it("uses the base setup route when no agent record exists", () => {
renderCard();
expect(screen.getByTestId("setup-profile-mock-agent").getAttribute("href")).toBe(
"/settings/agents/mock-agent",
);
});
it("uses create mode when a saved agent has no profiles", () => {
renderCard(SAVED_WITHOUT_PROFILES);
expect(screen.getByTestId("setup-profile-mock-agent").getAttribute("href")).toBe(
"/settings/agents/mock-agent?mode=create",
);
});
});
@@ -4,7 +4,7 @@ import { useState } from "react";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import Link from "@/components/routing/app-link";
import { IconLoader2, IconLock, IconSettings } from "@tabler/icons-react";
import { IconLoader2, IconLock, IconPlus, IconSettings } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
import { NotInstalledBadge } from "@/components/settings/record-badges";
import { Button } from "@kandev/ui/button";
@@ -136,6 +136,7 @@ export function InstalledAgentCard({
const { t } = useTranslation();
const configured = Boolean(savedAgent && savedAgent.profiles.length > 0);
const hasAgentRecord = Boolean(savedAgent);
const agentHref = `/settings/agents/${encodeURIComponent(agent.name)}`;
const [loginOpen, setLoginOpen] = useState(false);
const [shellOpen, setShellOpen] = useState(false);
const authRequired = capabilityStatus === "auth_required";
@@ -153,7 +154,10 @@ export function InstalledAgentCard({
return (
<Card className="min-w-0 gap-0 py-0" data-testid={`agent-group-${agent.name}`}>
{/* Header section: identity + agent-level actions. */}
<div className="flex min-w-0 flex-wrap items-start justify-between gap-3 px-3 py-2.5">
<div
className="flex min-w-0 flex-wrap items-start justify-between gap-3 px-3 py-2.5"
data-testid={`agent-card-header-${agent.name}`}
>
<div className="min-w-0 flex-1">
<InstalledAgentIdentity
agent={agent}
@@ -177,13 +181,21 @@ export function InstalledAgentCard({
onUpdate={onUpdate}
/>
)}
{!hasAgentRecord && (
// Sized to match the runtime-update trigger it sits beside — a
// coarse-pointer target on a phone, 28px from `sm` up. The default
// `sm` size (24px) both mismatched that neighbour on desktop and
// left a sub-target-size tap area on touch.
<Button className="h-11 cursor-pointer sm:h-7" asChild>
<Link href={`/settings/agents/${encodeURIComponent(agent.name)}`}>
{configured ? (
<Button className="h-11 cursor-pointer md:h-7" asChild>
<Link href={`${agentHref}?mode=create`} data-testid={`new-profile-${agent.name}`}>
<IconPlus className="mr-2 h-4 w-4" />
{t("agents:newProfile")}
</Link>
</Button>
) : (
// Keep the setup action for agents without a usable profile. A
// saved record needs create mode so its first profile is drafted.
<Button className="h-11 cursor-pointer md:h-7" asChild>
<Link
href={hasAgentRecord ? `${agentHref}?mode=create` : agentHref}
data-testid={`setup-profile-${agent.name}`}
>
<IconSettings className="mr-2 h-4 w-4" />
{t("agents:setupProfile")}
</Link>
@@ -0,0 +1,47 @@
import { test, expect } from "../../fixtures/test-base";
test.describe("Agent settings profile layout", () => {
test("keeps profile creation in the agent card header", async ({ testPage, apiClient }) => {
const { agents } = await apiClient.listAgents();
const agent = agents[0];
if (!agent || agent.profiles.length === 0) {
throw new Error("The E2E fixture must provide a configured agent profile");
}
await testPage.goto("/settings/agents");
const card = testPage.getByTestId(`agent-group-${agent.name}`);
const header = card.getByTestId(`agent-card-header-${agent.name}`);
const newProfile = header.getByTestId(`new-profile-${agent.name}`);
await expect(newProfile).toBeVisible({ timeout: 15_000 });
await expect(newProfile).toHaveAttribute(
"href",
`/settings/agents/${encodeURIComponent(agent.name)}?mode=create`,
);
// The action belongs to the card header, not the profile-list body.
await expect(
card.getByTestId(`agent-profiles-${agent.name}`).getByTestId(`new-profile-${agent.name}`),
).toHaveCount(0);
await expect(card.getByText(/^\d+ profiles?$/)).toHaveCount(0);
await newProfile.click();
await expect(testPage).toHaveURL(
new RegExp(`/settings/agents/${encodeURIComponent(agent.name)}\\?mode=create$`),
);
});
test("orders installed-agent actions with refresh before agent creation", async ({
testPage,
}) => {
await testPage.goto("/settings/agents");
const actions = testPage.getByTestId("installed-agents-actions");
await expect(actions).toBeVisible({ timeout: 15_000 });
await expect(
actions
.locator("[data-testid]")
.evaluateAll((elements) => elements.map((element) => element.getAttribute("data-testid"))),
).resolves.toEqual(["open-host-shell", "rescan-agents-button", "new-agent-button"]);
});
});
@@ -0,0 +1,56 @@
import { test, expect } from "../../fixtures/test-base";
test.describe("Agent settings profile layout on mobile", () => {
test("keeps creation reachable without horizontal overflow", async ({ testPage, apiClient }) => {
const { agents } = await apiClient.listAgents();
const agent = agents[0];
if (!agent || agent.profiles.length === 0) {
throw new Error("The E2E fixture must provide a configured agent profile");
}
await testPage.goto("/settings/agents");
await expect
.poll(
async () =>
testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
{ timeout: 15_000 },
)
.toBe(true);
const card = testPage.getByTestId(`agent-group-${agent.name}`);
const newProfile = card
.getByTestId(`agent-card-header-${agent.name}`)
.getByTestId(`new-profile-${agent.name}`);
await expect(newProfile).toBeVisible({ timeout: 15_000 });
await expect
.poll(
async () => {
const box = await newProfile.boundingBox();
return box ? Math.min(box.width, box.height) : null;
},
{ timeout: 10_000 },
)
.toBeGreaterThanOrEqual(44);
const actions = testPage.getByTestId("installed-agents-actions");
for (const testId of ["open-host-shell", "rescan-agents-button", "new-agent-button"]) {
const control = actions.getByTestId(testId);
await expect(control).toBeVisible();
await expect
.poll(
async () => {
const box = await control.boundingBox();
return box ? Math.min(box.width, box.height) : null;
},
{ timeout: 10_000 },
)
.toBeGreaterThanOrEqual(44);
}
await newProfile.tap();
await expect(testPage).toHaveURL(
new RegExp(`/settings/agents/${encodeURIComponent(agent.name)}\\?mode=create$`),
);
});
});
@@ -0,0 +1,128 @@
---
spec: docs/specs/agents/settings-profile-layout.md
created: 2026-08-13
status: building
---
# Implementation Plan: Simplify the agent settings profile layout
## Overview
Move profile creation into each installed-agent card header and remove the
redundant profile-count/action row from the card body. Reorder the existing
installed-agent toolbar so refresh/rescan is immediately before the rightmost
agent-creation action. Preserve the current profile rows and agent-level
controls, then prove the layout and navigation on desktop and mobile.
The change is frontend-only. No API, store, persistence, or locale contract is
needed because the existing translated labels and routes are reused.
## Frontend
### Agent profile sub-list
- `apps/web/components/settings/agents/agent-profiles-section.tsx`
- Remove the profile-count/empty-state header row and its creation link.
- Keep the existing profile rows and their actions in the card body.
- Avoid rendering an empty profile body when no saved profiles exist; the
card header supplies the setup action.
### Installed-agent card
- `apps/web/components/settings/installed-agent-card.tsx`
- Add the existing `New profile` link to the configured agent's header,
alongside agent-level controls, using the existing `?mode=create` route.
- Keep `Setup profile` as the only header creation action for an agent with
no saved record.
- Preserve touch-sized header actions on mobile and the existing status,
authentication, and runtime-update controls.
### Installed-agent toolbar
- `apps/web/app/settings/agents/page.tsx`
- Reorder the current header controls so the refresh/rescan button is
immediately before the existing Add TUI Agent action and the latter is
rightmost.
- Add stable test IDs for the toolbar and the two ordered controls if the
existing markup does not provide a reliable scoped selector.
- Keep the terminal action available and keep the current agent-creation
behavior and label.
## Tests
- **What:** A configured agent shows profile creation in the card header,
profile rows have no count/action sub-header, and an unconfigured agent has
only its setup action.
**File:** `apps/web/components/settings/agents/agent-profiles-section.test.tsx`
and `apps/web/components/settings/installed-agent-card.test.tsx`.
**How:** React Testing Library assertions against the rendered card/list,
including the creation link target and absence of count/empty-state copy.
- **What:** Existing profile-row duplicate/delete behavior remains intact.
**File:** `apps/web/components/settings/agents/agent-profiles-section.test.tsx`
**How:** Keep and rerun the current store read-after-write tests.
- **What:** The installed-agent toolbar keeps terminal available and orders
refresh/rescan directly before the rightmost agent-creation action.
**File:** `apps/web/e2e/tests/settings/agent-profile-layout.spec.ts`
**How:** Navigate through the real settings page, scope the toolbar by its
test ID, assert the ordered controls, and verify the configured card's
**New profile** link route.
## E2E Tests
- **Scenario:** GIVEN a configured agent with one or more profiles, WHEN the
user opens `/settings/agents`, THEN the card header contains **New profile**,
profile rows are directly below it, and no profile-count/action row exists.
**File:** `apps/web/e2e/tests/settings/agent-profile-layout.spec.ts`.
- **Scenario:** GIVEN a phone viewport below 768px, WHEN the user opens the
agents settings page, THEN the same creation action and ordered toolbar are
touch-reachable and the document has no horizontal overflow.
**File:** `apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts`.
**What to verify:** Use the configured `mobile-chrome` project, assert the
header action hitbox is at least 44px, use `.tap()` for the creation action,
and assert the settings document remains contained.
## Mobile design contract
- Desktop outcome: manage an agent's saved profiles and create another profile
from the same agent card.
- Mobile entry point: the installed-agent card on `/settings/agents`; no new
drawer, route, or mobile-only control is needed because the action is a
direct link and the existing settings scroll container remains the sole
vertical scroll owner.
- Nearest shipped exemplar: the managed runtime update controls on the same
agent card, whose mobile E2E coverage verifies 44px controls and no document
overflow. Reuse its touch-sized action treatment and existing card geometry.
- Mobile hierarchy: agent identity/status first, creation and agent-level
controls in the header, then profile rows in one vertical list.
- Presentation: inline card-header action; no drawer is justified because
profile creation is a direct destination rather than a temporary choice.
- Shared state and logic: reuse the existing agent/profile store data and route
handlers; only the placement and responsive classes change.
## Verification Results
Passed.
- Component regression coverage passed: `cd apps/web && pnpm test -- components/settings/agents/agent-profiles-section.test.tsx components/settings/installed-agent-card.test.tsx` (8 tests).
- Static checks passed: `cd apps/web && pnpm run typecheck`, the focused ESLint command from task 01, and `cd apps/web && pnpm run i18n:check` (with the repository's existing advisory catalog warnings).
- Desktop managed E2E passed: `cd apps/web && pnpm e2e:run --project chromium tests/settings/agent-profile-layout.spec.ts` (2 tests).
- Mobile managed E2E passed: `cd apps/web && pnpm e2e:run --project mobile-chrome tests/settings/mobile-agent-profile-layout.spec.ts` (1 test).
- Review follow-up coverage passed for both setup routes, the saved-empty profile body guard, and the toolbar ordering invariant.
## Implementation Waves And Parallel Candidates
Wave 1:
- [x] [task-01-agent-settings-layout](task-01-agent-settings-layout.md)
Wave 2:
- [x] [task-02-agent-settings-e2e](task-02-agent-settings-e2e.md)
The tasks are sequential because the E2E selectors and layout assertions
depend on the component markup from task 01.
## Open Questions
- None. The existing **Add TUI Agent** control is treated as the requested
agent-creation action; its semantics and translated label remain unchanged.
@@ -0,0 +1,74 @@
---
id: "01-agent-settings-layout"
title: "Agent settings layout"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/agents/settings-profile-layout.md"
---
# Task 01: Agent settings layout
## Acceptance
- Configured agent cards show the existing `New profile` link in their header;
the profile body has no count or separate creation/empty-state row.
- Unconfigured agent cards show only the existing `Setup profile` header action,
while saved profile rows and their duplicate/delete behavior remain intact.
- The installed-agent toolbar places refresh/rescan immediately before the
rightmost Add TUI Agent action, keeps terminal available, and preserves
touch-safe targets on mobile.
## Verification
```bash
cd apps && pnpm install --frozen-lockfile
cd apps/web && pnpm test -- components/settings/agents/agent-profiles-section.test.tsx components/settings/installed-agent-card.test.tsx
cd apps/web && pnpm run typecheck
cd apps/web && pnpm exec eslint app/settings/agents/page.tsx components/settings/agents/agent-profiles-section.tsx components/settings/installed-agent-card.tsx
cd apps/web && pnpm run i18n:check
```
The focused card test covers both unconfigured setup-link branches, while the
existing profile-section test keeps the profile-row regression coverage.
## Files likely touched
- `apps/web/app/settings/agents/page.tsx`
- `apps/web/components/settings/agents/agent-profiles-section.tsx`
- `apps/web/components/settings/agents/agent-profiles-section.test.tsx`
- `apps/web/components/settings/installed-agent-card.tsx`
- `apps/web/components/settings/installed-agent-card.test.tsx`
## Dependencies
None.
## Parallelism
Sequential. The component markup, selector IDs, and unit assertions are one
small vertical slice.
## Inputs
- Spec: `docs/specs/agents/settings-profile-layout.md`, especially What and
the first four scenarios.
- Plan: `plan.md`, Frontend and Mobile design contract sections.
- Existing patterns: `InstalledAgentCard`, `AgentProfilesSubList`, and the
current `agent-profiles-section.test.tsx` store read-after-write tests.
## Output contract
Report the final changed files, exact unit/typecheck/lint/i18n commands and
results, any selector or responsive decisions, blockers, risks, and synchronized
task/plan status in the same conversation.
## Results
- `cd apps && pnpm install --frozen-lockfile` passed.
- `cd apps/web && pnpm test -- components/settings/agents/agent-profiles-section.test.tsx components/settings/installed-agent-card.test.tsx` passed (2 files, 8 tests), including both setup-link routes and both empty-body guards.
- `cd apps/web && pnpm run typecheck` passed.
- `cd apps/web && pnpm exec eslint app/settings/agents/page.tsx components/settings/agents/agent-profiles-section.tsx components/settings/installed-agent-card.tsx` passed.
- `cd apps/web && pnpm run i18n:check` passed. It reported the repository's existing advisory catalog parity/unreferenced-key warnings; this change adds no locale keys.
- Review follow-up added setup-link coverage, the saved-empty profile-body guard, and removed a redundant toolbar-count assertion in the desktop E2E test.
@@ -0,0 +1,65 @@
---
id: "02-agent-settings-e2e"
title: "Agent settings E2E coverage"
status: done
wave: 2
depends_on: ["01-agent-settings-layout"]
plan: "plan.md"
spec: "../../specs/agents/settings-profile-layout.md"
---
# Task 02: Agent settings E2E coverage
## Acceptance
- The desktop E2E test proves the configured card has the `New profile` action
in its header, no profile-count/action row, and the installed-agent toolbar
order is terminal, refresh/rescan, then rightmost Add TUI Agent.
- The mobile E2E test proves the same user value with the `mobile-chrome`
project, uses touch for the creation action, verifies a 44px-or-larger hitbox,
and detects no document-level horizontal overflow.
## Verification
```bash
cd apps && pnpm install --frozen-lockfile
cd apps/web && pnpm e2e:run --project chromium tests/settings/agent-profile-layout.spec.ts
cd apps/web && pnpm e2e:run --project mobile-chrome tests/settings/mobile-agent-profile-layout.spec.ts
```
The managed runner must rebuild the production Vite assets before the tests.
Do not use headed mode. If a focused test needs the exact fixture path or a
different project selector, record the final command actually run below.
## Files likely touched
- `apps/web/e2e/tests/settings/agent-profile-layout.spec.ts`
- `apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts`
## Dependencies
Task 01 must land first so the tests target final markup and selectors.
## Parallelism
Sequential. E2E selectors and layout assertions depend on task 01.
## Inputs
- Spec: `docs/specs/agents/settings-profile-layout.md`, all Scenarios.
- Plan: `plan.md`, E2E Tests and Mobile design contract sections.
- Existing patterns: `agent-profile-duplicate.spec.ts`,
`mobile-agent-profile-duplicate.spec.ts`, and
`mobile-agent-runtime-update.spec.ts`.
## Output contract
Report changed test files, exact managed-runner commands and test counts,
screenshots or failure artifacts if produced, cleanup evidence, blockers, and
synchronized task/plan status in the same conversation.
## Results
- `cd apps/web && pnpm e2e:run --project chromium tests/settings/agent-profile-layout.spec.ts` passed (2 tests).
- `cd apps/web && pnpm e2e:run --project mobile-chrome tests/settings/mobile-agent-profile-layout.spec.ts` passed (1 test).
- Both managed runs rebuilt the backend and pseudo-locale Vite assets and started from the runner's cleaned E2E artifact directories. No failure artifacts remain.
+1
View File
@@ -116,6 +116,7 @@ Roles, governance gates, and granular permissions that apply across human users
|---|---|
| [runtime-updates](agents/runtime-updates.md) | approved |
| [profile-disable](agents/profile-disable.md) | draft |
| [settings-profile-layout](agents/settings-profile-layout.md) | shipped |
| [dynamic-provider-options](agents/dynamic-provider-options.md) | shipped |
| [utility-agent-profiles](agents/utility-agent-profiles.md) | approved |
| [roles](agents/roles.md) | shipped |
@@ -0,0 +1,72 @@
---
status: shipped
created: 2026-08-13
owner: kandev
---
# Simplify the agent settings profile layout
## Why
The agent settings page repeats the profile count and profile creation action
in a separate sub-header inside every agent card. This adds a second hierarchy
between the agent identity and its profiles and makes the primary action harder
to associate with the agent it changes.
## What
- On `/settings/agents`, each configured agent card exposes its existing **New
profile** action in the card header, on the right with the other agent-level
actions. The action continues to open that agent's profile creation flow at
`/settings/agents/<agent>?mode=create`.
- An agent without a saved profile keeps one **Setup profile** action in its
card header. It does not show a second profile-creation action in an empty
profile area.
- The profile list body no longer renders a profile-count or empty-state
header row. When profiles exist, the saved profile rows appear directly in
the card body. When no profiles exist, the card header remains the only
setup entry point.
- The installed-agent section keeps its existing terminal and refresh/rescan
actions. The refresh/rescan action appears immediately before the existing
agent-creation action, and the agent-creation action is the rightmost action
in that toolbar. The current agent-creation behavior and translated label
remain unchanged.
- Existing profile rows, profile links, duplicate/delete actions, agent status
badges, authentication controls, and runtime-update controls keep their
current behavior.
- The desktop and mobile layouts expose the same actions and outcomes. On a
narrow touch viewport, card-header actions remain visible, reachable with a
touch target of at least 44px, and do not create document-level horizontal
overflow. The settings content keeps its existing scroll owner.
## Scenarios
- **GIVEN** a configured agent with one or more profiles, **WHEN** the user
opens `/settings/agents`, **THEN** the agent card header shows **New
profile** on the right, the profile rows appear directly below it, and no
profile-count text or separate count/action row is rendered.
- **GIVEN** an agent with no saved profile, **WHEN** the user opens
`/settings/agents`, **THEN** the card header shows **Setup profile**, no
profile-count or empty-state action row is rendered, and clicking the action
opens that agent's profile creation flow.
- **GIVEN** the installed-agent section is visible, **WHEN** the user reads
its right-side toolbar, **THEN** refresh/rescan is immediately followed by
the agent-creation action, which is the rightmost action, while the terminal
action remains available.
- **GIVEN** a configured agent card, **WHEN** the user clicks **New profile**,
**THEN** the browser navigates to that agent's profile creation route and
the existing profile rows remain unchanged.
- **GIVEN** a phone viewport below 768px, **WHEN** the user opens the agents
settings page, **THEN** the card-header creation action and the section
toolbar actions remain visible and touch-reachable, the profile rows remain
vertically usable, and `document.documentElement.scrollWidth` does not
exceed `window.innerWidth`.
## Out of scope
- Changing the profile editor, profile-row actions, agent discovery, agent
installation, or the backend/API contracts.
- Renaming the existing translated **New profile**, **Setup profile**, refresh,
or agent-creation labels.
- Changing the profile count text used by unrelated task or picker surfaces.
- Changing the settings sidebar hierarchy or the `/office/agents` surface.