-
-
{agent.name}
+
+
{agent.name}
{agent.description}
-
);
}
@@ -257,10 +263,13 @@ export function PerActionOverridesSection({
const defaultLabel = defaultModel ? `Default (${defaultModel})` : "Default";
return (
-
-
-
Actions
-
+
+
+
+ Actions
+
+
+
{builtins.map((agent) => (
))}
-
-
+
+
);
}
@@ -286,30 +295,34 @@ type CustomAgentsSectionProps = {
export function CustomAgentsSection({ agents, onAdd, onEdit, onDelete }: CustomAgentsSectionProps) {
return (
-
-
-
-
Custom utility agents
-
- Create your own utility agents with custom prompts.
-
-
-
-
- {agents.length === 0 && (
-
No custom utility agents.
- )}
- {agents.length > 0 && (
-
- {agents.map((agent) => (
-
- ))}
-
- )}
-
+
+
+
+ Custom utility agents
+
+
+
+
+
+
+
+ Create your own utility agents with custom prompts.
+
+ {agents.length === 0 && (
+ No custom utility agents.
+ )}
+ {agents.length > 0 && (
+
+ {agents.map((agent) => (
+
+ ))}
+
+ )}
+
+
);
}
diff --git a/apps/web/e2e/tests/settings/mobile-utility-agents.spec.ts b/apps/web/e2e/tests/settings/mobile-utility-agents.spec.ts
new file mode 100644
index 0000000000..d7d98cba23
--- /dev/null
+++ b/apps/web/e2e/tests/settings/mobile-utility-agents.spec.ts
@@ -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();
+ });
+});
diff --git a/apps/web/e2e/tests/settings/utility-agents.spec.ts b/apps/web/e2e/tests/settings/utility-agents.spec.ts
index 4fc83d800a..445a903cb8 100644
--- a/apps/web/e2e/tests/settings/utility-agents.spec.ts
+++ b/apps/web/e2e/tests/settings/utility-agents.spec.ts
@@ -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);
});
});