feat(web): streamline agent settings profiles (#2620)
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user