fix: wrap Utility Agents sub-sections in bounded cards (#1654)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
|
||||
import { Separator } from "@kandev/ui/separator";
|
||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { updateWorkspaceAction } from "@/app/actions/workspaces";
|
||||
@@ -47,28 +47,31 @@ export function ConfigChatAgentSection() {
|
||||
if (!workspace) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Separator />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Configuration Chat Agent</h3>
|
||||
<Card data-testid="config-chat-agent-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
<h3>Configuration Chat Agent</h3>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose which agent profile to use for the Configuration Chat. This agent can manage your
|
||||
workflows, agent profiles, and MCP configuration.
|
||||
</p>
|
||||
</div>
|
||||
<Select value={currentProfileId || "none"} onValueChange={handleChange} disabled={saving}>
|
||||
<SelectTrigger className="w-full max-w-sm cursor-pointer">
|
||||
<SelectValue placeholder="Choose an agent profile..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No default</SelectItem>
|
||||
{profiles.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id} className="cursor-pointer">
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select value={currentProfileId || "none"} onValueChange={handleChange} disabled={saving}>
|
||||
<SelectTrigger className="w-full max-w-sm cursor-pointer">
|
||||
<SelectValue placeholder="Choose an agent profile..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No default</SelectItem>
|
||||
{profiles.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id} className="cursor-pointer">
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { IconWand } from "@tabler/icons-react";
|
||||
import {
|
||||
listUtilityAgents,
|
||||
listInferenceAgents,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
type InferenceAgent,
|
||||
} from "@/lib/api/domains/utility-api";
|
||||
import { fetchUserSettings, updateUserSettings } from "@/lib/api/domains/settings-api";
|
||||
import { SettingsSection } from "@/components/settings/settings-section";
|
||||
import { UtilityAgentDialog } from "@/components/settings/utility-agent-dialog";
|
||||
import {
|
||||
DefaultModelSection,
|
||||
@@ -107,41 +109,41 @@ export function UtilityAgentsSection() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Utility Agents</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
One-shot AI helpers for commits, PRs, and prompts.
|
||||
</p>
|
||||
<SettingsSection
|
||||
icon={<IconWand className="h-5 w-5" />}
|
||||
title="Utility Agents"
|
||||
description="One-shot AI helpers for commits, PRs, and prompts."
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<DefaultModelSection
|
||||
inferenceAgents={inferenceAgents}
|
||||
defaultAgentId={defaultAgentId}
|
||||
defaultModel={defaultModel}
|
||||
onDefaultChange={handleDefaultChange}
|
||||
onRefreshAgent={refreshAgent}
|
||||
/>
|
||||
<PerActionOverridesSection
|
||||
builtins={builtins}
|
||||
allModels={allModels}
|
||||
defaultModel={defaultModel}
|
||||
onModelChange={(agent, value) => handleBuiltinChange(agent, value, setAgents)}
|
||||
onEdit={openEditDialog}
|
||||
/>
|
||||
<CustomAgentsSection
|
||||
agents={customAgents}
|
||||
onAdd={() => openEditDialog(null)}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={async (agent) => {
|
||||
try {
|
||||
await deleteUtilityAgent(agent.id);
|
||||
setAgents((prev) => prev.filter((a) => a.id !== agent.id));
|
||||
} catch {
|
||||
// Error already logged by API layer
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DefaultModelSection
|
||||
inferenceAgents={inferenceAgents}
|
||||
defaultAgentId={defaultAgentId}
|
||||
defaultModel={defaultModel}
|
||||
onDefaultChange={handleDefaultChange}
|
||||
onRefreshAgent={refreshAgent}
|
||||
/>
|
||||
<PerActionOverridesSection
|
||||
builtins={builtins}
|
||||
allModels={allModels}
|
||||
defaultModel={defaultModel}
|
||||
onModelChange={(agent, value) => handleBuiltinChange(agent, value, setAgents)}
|
||||
onEdit={openEditDialog}
|
||||
/>
|
||||
<CustomAgentsSection
|
||||
agents={customAgents}
|
||||
onAdd={() => openEditDialog(null)}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={async (agent) => {
|
||||
try {
|
||||
await deleteUtilityAgent(agent.id);
|
||||
setAgents((prev) => prev.filter((a) => a.id !== agent.id));
|
||||
} catch {
|
||||
// Error already logged by API layer
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<UtilityAgentDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { IconPencil, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
|
||||
import { Label } from "@kandev/ui/label";
|
||||
import { Separator } from "@kandev/ui/separator";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -91,50 +91,54 @@ export function DefaultModelSection({
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-base font-medium">Default utility agent model</h3>
|
||||
<Card data-testid="utility-default-model-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
<h3>Default utility agent model</h3>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Select the default model used by all built-in utility actions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<div className="w-full sm:w-[180px]">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Agent</Label>
|
||||
<Select value={defaultAgentId} onValueChange={(v) => onDefaultChange(v, "")}>
|
||||
<SelectTrigger className="cursor-pointer">
|
||||
<SelectValue placeholder="Select agent..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{inferenceAgents.map((ia) => (
|
||||
<SelectItem key={ia.id} value={ia.id}>
|
||||
{ia.display_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<div className="w-full sm:w-[180px]">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Agent</Label>
|
||||
<Select value={defaultAgentId} onValueChange={(v) => onDefaultChange(v, "")}>
|
||||
<SelectTrigger className="cursor-pointer">
|
||||
<SelectValue placeholder="Select agent..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{inferenceAgents.map((ia) => (
|
||||
<SelectItem key={ia.id} value={ia.id}>
|
||||
{ia.display_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-full sm:w-[280px]">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Model</Label>
|
||||
<ModelConfigSelector
|
||||
modelOptions={selectorModels}
|
||||
currentModel={selectedModel}
|
||||
configOptions={selectedConfigOptions}
|
||||
onModelChange={(v) => onDefaultChange(defaultAgentId, v)}
|
||||
disabled={!defaultAgentId}
|
||||
placeholder="Select model..."
|
||||
ariaLabel="Default utility model settings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full sm:w-[280px]">
|
||||
<Label className="text-xs text-muted-foreground mb-1 block">Model</Label>
|
||||
<ModelConfigSelector
|
||||
modelOptions={selectorModels}
|
||||
currentModel={selectedModel}
|
||||
configOptions={selectedConfigOptions}
|
||||
onModelChange={(v) => onDefaultChange(defaultAgentId, v)}
|
||||
disabled={!defaultAgentId}
|
||||
placeholder="Select model..."
|
||||
ariaLabel="Default utility model settings"
|
||||
{defaultAgentId && (
|
||||
<InferenceAgentStatusNote
|
||||
agent={selectedAgent}
|
||||
fallbackName={defaultAgentId}
|
||||
onRefresh={() => onRefreshAgent(defaultAgentId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{defaultAgentId && (
|
||||
<InferenceAgentStatusNote
|
||||
agent={selectedAgent}
|
||||
fallbackName={defaultAgentId}
|
||||
onRefresh={() => onRefreshAgent(defaultAgentId)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,42 +163,44 @@ export function BuiltinActionRow({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-4 py-2 px-2 rounded hover:bg-muted/50"
|
||||
className="flex flex-col gap-2 py-2 px-2 rounded hover:bg-muted/50 md:flex-row md:items-center md:gap-4"
|
||||
data-testid={`utility-action-row-${agent.id}`}
|
||||
>
|
||||
<div className="w-[420px] shrink-0">
|
||||
<div className="text-sm font-medium">{agent.name}</div>
|
||||
<div className="min-w-0 md:flex-1">
|
||||
<div className="text-sm font-medium truncate">{agent.name}</div>
|
||||
<p className="text-xs text-muted-foreground truncate">{agent.description}</p>
|
||||
</div>
|
||||
<Select value={currentValue} onValueChange={(v) => onModelChange(agent, v)}>
|
||||
<SelectTrigger className="w-[240px] cursor-pointer">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={USE_DEFAULT}>{defaultLabel}</SelectItem>
|
||||
</SelectGroup>
|
||||
{groupModelsByAgent(allModels).map((group) => (
|
||||
<SelectGroup key={group.agentName}>
|
||||
<SelectSeparator />
|
||||
<SelectLabel>{group.agentName}</SelectLabel>
|
||||
{group.models.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value}>
|
||||
{m.modelName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={currentValue} onValueChange={(v) => onModelChange(agent, v)}>
|
||||
<SelectTrigger className="min-w-0 flex-1 cursor-pointer md:w-[240px] md:flex-none">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value={USE_DEFAULT}>{defaultLabel}</SelectItem>
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(agent)}
|
||||
className="h-7 w-7 p-0 cursor-pointer text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<IconPencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{groupModelsByAgent(allModels).map((group) => (
|
||||
<SelectGroup key={group.agentName}>
|
||||
<SelectSeparator />
|
||||
<SelectLabel>{group.agentName}</SelectLabel>
|
||||
{group.models.map((m) => (
|
||||
<SelectItem key={m.value} value={m.value}>
|
||||
{m.modelName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(agent)}
|
||||
className="h-7 w-7 p-0 shrink-0 cursor-pointer text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<IconPencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -257,10 +263,13 @@ export function PerActionOverridesSection({
|
||||
const defaultLabel = defaultModel ? `Default (${defaultModel})` : "Default";
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Separator />
|
||||
<h3 className="text-base font-medium pt-2">Actions</h3>
|
||||
<div className="space-y-0">
|
||||
<Card data-testid="utility-actions-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
<h3>Actions</h3>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-0">
|
||||
{builtins.map((agent) => (
|
||||
<BuiltinActionRow
|
||||
key={agent.id}
|
||||
@@ -271,8 +280,8 @@ export function PerActionOverridesSection({
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -286,30 +295,34 @@ type CustomAgentsSectionProps = {
|
||||
|
||||
export function CustomAgentsSection({ agents, onAdd, onEdit, onDelete }: CustomAgentsSectionProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-base font-medium">Custom utility agents</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create your own utility agents with custom prompts.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={onAdd} size="sm" className="cursor-pointer">
|
||||
<IconPlus className="h-4 w-4 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{agents.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground py-4">No custom utility agents.</p>
|
||||
)}
|
||||
{agents.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{agents.map((agent) => (
|
||||
<CustomAgentRow key={agent.id} agent={agent} onEdit={onEdit} onDelete={onDelete} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Card data-testid="utility-custom-agents-card">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
<h3>Custom utility agents</h3>
|
||||
</CardTitle>
|
||||
<CardAction>
|
||||
<Button onClick={onAdd} size="sm" className="cursor-pointer">
|
||||
<IconPlus className="h-4 w-4 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create your own utility agents with custom prompts.
|
||||
</p>
|
||||
{agents.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground py-4">No custom utility agents.</p>
|
||||
)}
|
||||
{agents.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{agents.map((agent) => (
|
||||
<CustomAgentRow key={agent.id} agent={agent} onEdit={onEdit} onDelete={onDelete} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
|
||||
/**
|
||||
* Regression guard for a Card-wrapping bug: `BuiltinActionRow` used fixed
|
||||
* 420px/240px widths that, combined with the shared `Card`'s
|
||||
* `overflow-hidden`, pushed the model selector and edit button outside the
|
||||
* visible card on narrow viewports — clipped and unreachable rather than
|
||||
* stacked. See PR #1654 review discussion.
|
||||
*/
|
||||
test.describe("Mobile utility agents action rows", () => {
|
||||
test("model select and edit button stay reachable on a narrow viewport", async ({ testPage }) => {
|
||||
await testPage.route("**/api/v1/utility/inference-agents", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
agents: [
|
||||
{
|
||||
id: "claude",
|
||||
name: "claude",
|
||||
display_name: "Claude",
|
||||
status: "ok",
|
||||
models: [
|
||||
{ id: "claude-fast", name: "Claude Fast", description: "", is_default: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await testPage.route("**/api/v1/utility/agents/builtin-commit-message", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id: "builtin-commit-message",
|
||||
name: "commit-message",
|
||||
description: "Generate a commit message.",
|
||||
builtin: true,
|
||||
enabled: true,
|
||||
agent_id: "claude",
|
||||
model: "claude-fast",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await testPage.route("**/api/v1/utility/agents", (route) => {
|
||||
if (route.request().method() !== "GET") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ success: true }),
|
||||
});
|
||||
}
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
agents: [
|
||||
{
|
||||
id: "builtin-commit-message",
|
||||
name: "commit-message",
|
||||
description: "Generate a commit message.",
|
||||
builtin: true,
|
||||
enabled: true,
|
||||
agent_id: "",
|
||||
model: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await testPage.goto("/settings/utility-agents");
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Utility Agents", exact: true }),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const card = testPage.getByTestId("utility-actions-card");
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
// The card must not have to clip its own content to stay within bounds
|
||||
// — regression guard for the fixed 420px/240px row colliding with the
|
||||
// shared Card's `overflow-hidden`.
|
||||
const isOverflowing = await card.evaluate((el) => el.scrollWidth > el.clientWidth + 1);
|
||||
expect(isOverflowing).toBe(false);
|
||||
|
||||
// The model selector must also stay a comfortably usable width — not
|
||||
// just "not clipped" but not squeezed illegibly thin either. The row
|
||||
// stacks below `md`, so the select gets the card's full content width
|
||||
// to itself (minus the edit button), not a slice shared with the name
|
||||
// column.
|
||||
const row = testPage.getByTestId("utility-action-row-builtin-commit-message");
|
||||
const select = row.getByRole("combobox");
|
||||
const selectBox = await select.boundingBox();
|
||||
expect(selectBox).not.toBeNull();
|
||||
expect(selectBox!.width).toBeGreaterThanOrEqual(150);
|
||||
|
||||
// The model selector and edit button must stay clickable, not clipped
|
||||
// outside the card.
|
||||
await select.click();
|
||||
await testPage.getByRole("option", { name: "Claude Fast", exact: true }).click();
|
||||
await expect(select).toContainText("Claude Fast");
|
||||
|
||||
await row.getByRole("button").click();
|
||||
await expect(testPage.getByRole("dialog")).toBeVisible();
|
||||
await expect(testPage.getByText("Edit Utility Agent")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -167,9 +167,7 @@ test.describe("Utility Agents settings page", () => {
|
||||
).toBeVisible();
|
||||
|
||||
// Default-model section.
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Default utility agent model", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(testPage.getByText("Default utility agent model", { exact: true })).toBeVisible();
|
||||
|
||||
// Built-in actions (seeded on first boot — see builtins.go).
|
||||
// Assert a representative subset; the full list lives server-side.
|
||||
@@ -178,9 +176,7 @@ test.describe("Utility Agents settings page", () => {
|
||||
await expect(testPage.getByText("enhance-prompt", { exact: true })).toBeVisible();
|
||||
|
||||
// Custom agents section + empty state.
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Custom utility agents", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(testPage.getByText("Custom utility agents", { exact: true })).toBeVisible();
|
||||
await expect(testPage.getByText("No custom utility agents.")).toBeVisible();
|
||||
|
||||
expect(pageErrors, `uncaught errors: ${pageErrors.map((e) => e.message).join("; ")}`).toEqual(
|
||||
@@ -188,6 +184,85 @@ test.describe("Utility Agents settings page", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("wraps each settings sub-section in a bounded card, matching Voice Mode", async ({
|
||||
testPage,
|
||||
}) => {
|
||||
// Regression guard: these sub-sections used to be bare divs with no
|
||||
// visible border, unlike every other settings page (e.g. Voice Mode's
|
||||
// Card-wrapped Enable/Engine/Behavior sections).
|
||||
//
|
||||
// Mock the inference-agent + builtin-agent APIs so the Actions card
|
||||
// (which renders null when there are no builtins — see
|
||||
// PerActionOverridesSection) is deterministically present, instead of
|
||||
// depending on the backend's boot-time seed state.
|
||||
await testPage.route("**/api/v1/utility/inference-agents", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
agents: [
|
||||
{
|
||||
id: "claude",
|
||||
name: "claude",
|
||||
display_name: "Claude",
|
||||
status: "ok",
|
||||
models: [
|
||||
{ id: "claude-fast", name: "Claude Fast", description: "", is_default: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await testPage.route("**/api/v1/utility/agents", (route) => {
|
||||
if (route.request().method() !== "GET") {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ success: true }),
|
||||
});
|
||||
}
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
agents: [
|
||||
{
|
||||
id: "builtin-commit-message",
|
||||
name: "commit-message",
|
||||
description: "Generate a commit message.",
|
||||
builtin: true,
|
||||
enabled: true,
|
||||
agent_id: "",
|
||||
model: "",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await testPage.goto("/settings/utility-agents");
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Utility Agents", exact: true }),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
for (const { testId, heading } of [
|
||||
{ testId: "utility-default-model-card", heading: "Default utility agent model" },
|
||||
{ testId: "utility-actions-card", heading: "Actions" },
|
||||
{ testId: "utility-custom-agents-card", heading: "Custom utility agents" },
|
||||
{ testId: "config-chat-agent-card", heading: "Configuration Chat Agent" },
|
||||
]) {
|
||||
const card = testPage.getByTestId(testId);
|
||||
await expect(card).toBeVisible();
|
||||
await expect(card).toHaveAttribute("data-slot", "card");
|
||||
// Card titles must stay real headings (level 3), not just styled div
|
||||
// text, so screen-reader heading navigation keeps working.
|
||||
await expect(
|
||||
card.getByRole("heading", { name: heading, level: 3, exact: true }),
|
||||
).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("opens the create-agent dialog from the Add button", async ({ testPage }) => {
|
||||
const pageErrors: Error[] = [];
|
||||
testPage.on("pageerror", (err) => pageErrors.push(err));
|
||||
@@ -389,9 +464,7 @@ test.describe("Utility Agents settings page", () => {
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Utility Agents", exact: true }),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Configuration Chat Agent", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(testPage.getByText("Configuration Chat Agent", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
testPage.getByText(
|
||||
"Choose which agent profile to use for the Configuration Chat. This agent can manage your workflows, agent profiles, and MCP configuration.",
|
||||
@@ -402,8 +475,6 @@ test.describe("Utility Agents settings page", () => {
|
||||
await expect(testPage.getByRole("heading", { name: "Agents", exact: true })).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
await expect(
|
||||
testPage.getByRole("heading", { name: "Configuration Chat Agent", exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(testPage.getByText("Configuration Chat Agent", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user