{canManage &&
diff --git a/apps/web/e2e/tests/settings/agents-browse-page.spec.ts b/apps/web/e2e/tests/settings/agents-browse-page.spec.ts
index eb9e388250..423fd1a33b 100644
--- a/apps/web/e2e/tests/settings/agents-browse-page.spec.ts
+++ b/apps/web/e2e/tests/settings/agents-browse-page.spec.ts
@@ -99,4 +99,45 @@ test.describe("Agents browse page", () => {
// Compatibility guard: the exact test ID PR #2544 introduced is gone too.
await expect(testPage.getByTestId("available-to-install-trigger")).toHaveCount(0);
});
+
+ test("renders the saved fallback summary after the model badge", 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");
+ }
+
+ const fallbackModel = "saved-explicit-model";
+ const profileName = "Desktop fallback summary";
+ let profileId: string | undefined;
+
+ try {
+ await testPage.goto("/settings/agents");
+ const seededRow = testPage
+ .getByTestId("agent-profile-row")
+ .filter({ hasText: agent.profiles[0].name });
+ await expect(seededRow).toBeVisible({ timeout: 15_000 });
+
+ const profile = await apiClient.createAgentProfile(agent.id, profileName, {
+ model: agent.profiles[0].model,
+ fallback_model: fallbackModel,
+ });
+ profileId = profile.id;
+ await testPage.reload();
+
+ const row = testPage.getByTestId("agent-profile-row").filter({ hasText: profile.name });
+ await expect(row).toBeVisible({ timeout: 15_000 });
+ await expect(row.locator('[data-slot="badge"]')).toHaveText([
+ profile.model,
+ `fallback: ${fallbackModel}`,
+ ]);
+ } finally {
+ if (profileId) {
+ await apiClient.deleteAgentProfile(profileId, true);
+ }
+ }
+ });
});
diff --git a/apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts b/apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts
index 7c4f76b4ac..5601a302db 100644
--- a/apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts
+++ b/apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts
@@ -53,4 +53,68 @@ test.describe("Agent settings profile layout on mobile", () => {
new RegExp(`/settings/agents/${encodeURIComponent(agent.name)}\\?mode=create$`),
);
});
+
+ test("wraps the saved fallback summary 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");
+ }
+
+ const fallbackModel = `saved-explicit-model-${"x".repeat(128)}`;
+ const profileName = "Mobile fallback summary";
+ let profileId: string | undefined;
+
+ try {
+ await testPage.goto("/settings/agents");
+ const seededRow = testPage
+ .getByTestId("agent-profile-row")
+ .filter({ hasText: agent.profiles[0].name });
+ await expect(seededRow).toBeVisible({ timeout: 15_000 });
+
+ const profile = await apiClient.createAgentProfile(agent.id, profileName, {
+ model: agent.profiles[0].model,
+ fallback_model: fallbackModel,
+ });
+ profileId = profile.id;
+ await testPage.reload();
+
+ const row = testPage.getByTestId("agent-profile-row").filter({ hasText: profile.name });
+ await expect(row).toBeVisible({ timeout: 15_000 });
+ await expect
+ .poll(
+ async () =>
+ testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
+ { timeout: 15_000 },
+ )
+ .toBe(true);
+ await expect(row.locator('[data-slot="badge"]')).toHaveText([
+ profile.model,
+ `fallback: ${fallbackModel}`,
+ ]);
+ const fallbackBadge = row.locator('[data-slot="badge"]').nth(1);
+ const [badgeBox, rowBox] = await Promise.all([
+ fallbackBadge.boundingBox(),
+ row.boundingBox(),
+ ]);
+ expect(badgeBox).not.toBeNull();
+ expect(rowBox).not.toBeNull();
+ expect(badgeBox!.width).toBeLessThanOrEqual(rowBox!.width);
+ expect(
+ await fallbackBadge.evaluate((element) => element.scrollWidth <= element.clientWidth + 1),
+ ).toBe(true);
+ expect(
+ await fallbackBadge.evaluate((element) => element.scrollHeight <= element.clientHeight + 1),
+ ).toBe(true);
+ expect(badgeBox!.y).toBeGreaterThanOrEqual(rowBox!.y - 1);
+ expect(badgeBox!.y + badgeBox!.height).toBeLessThanOrEqual(rowBox!.y + rowBox!.height + 1);
+ } finally {
+ if (profileId) {
+ await apiClient.deleteAgentProfile(profileId, true);
+ }
+ }
+ });
});
diff --git a/apps/web/lib/agent-profile-fallback.test.ts b/apps/web/lib/agent-profile-fallback.test.ts
new file mode 100644
index 0000000000..c0ec3462dd
--- /dev/null
+++ b/apps/web/lib/agent-profile-fallback.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, it } from "vitest";
+
+import { classifyAgentProfileFallback } from "./agent-profile-fallback";
+
+describe("classifyAgentProfileFallback", () => {
+ it("classifies a profile with no configured fallback as none", () => {
+ expect(classifyAgentProfileFallback({ autoFallback: false, fallbackModel: "" })).toEqual({
+ kind: "none",
+ });
+ });
+
+ it("classifies exact-model selection before a saved explicit fallback", () => {
+ expect(
+ classifyAgentProfileFallback({
+ requireExactModel: true,
+ autoFallback: false,
+ fallbackModel: "saved-explicit-model",
+ }),
+ ).toEqual({ kind: "exact" });
+ });
+
+ it("classifies exact-model selection before saved automatic fallback", () => {
+ expect(
+ classifyAgentProfileFallback({
+ requireExactModel: true,
+ autoFallback: true,
+ fallbackModel: "",
+ }),
+ ).toEqual({ kind: "exact" });
+ });
+
+ it("classifies automatic fallback as next", () => {
+ expect(classifyAgentProfileFallback({ autoFallback: true, fallbackModel: "" })).toEqual({
+ kind: "next",
+ });
+ });
+
+ it("preserves an explicit fallback model opaquely", () => {
+ const model = " provider/model:with spaces ";
+ expect(classifyAgentProfileFallback({ autoFallback: false, fallbackModel: model })).toEqual({
+ kind: "model",
+ model,
+ });
+ });
+
+ it("gives automatic fallback precedence over an explicit saved model", () => {
+ expect(
+ classifyAgentProfileFallback({ autoFallback: true, fallbackModel: "saved-explicit-model" }),
+ ).toEqual({ kind: "next" });
+ });
+});
diff --git a/apps/web/lib/agent-profile-fallback.ts b/apps/web/lib/agent-profile-fallback.ts
new file mode 100644
index 0000000000..dca84b0bef
--- /dev/null
+++ b/apps/web/lib/agent-profile-fallback.ts
@@ -0,0 +1,16 @@
+export type AgentProfileFallbackState =
+ | { kind: "none" }
+ | { kind: "exact" }
+ | { kind: "next" }
+ | { kind: "model"; model: string };
+
+export function classifyAgentProfileFallback(profile: {
+ autoFallback?: boolean;
+ fallbackModel?: string;
+ requireExactModel?: boolean;
+}): AgentProfileFallbackState {
+ if (profile.requireExactModel) return { kind: "exact" };
+ if (profile.autoFallback) return { kind: "next" };
+ if (profile.fallbackModel) return { kind: "model", model: profile.fallbackModel };
+ return { kind: "none" };
+}
diff --git a/apps/web/lib/api/domains/agent-profile-normalize.ts b/apps/web/lib/api/domains/agent-profile-normalize.ts
index 9c0640712d..7f45a38362 100644
--- a/apps/web/lib/api/domains/agent-profile-normalize.ts
+++ b/apps/web/lib/api/domains/agent-profile-normalize.ts
@@ -6,7 +6,7 @@
// Wired via thin wrappers around the server actions / WS payloads in
// `app/actions/agents.ts` and `lib/ws/handlers/agents.ts`.
-import type { ProfileEnvVar } from "@/lib/types/http";
+import type { Agent, ProfileEnvVar } from "@/lib/types/http";
import type {
AgentProfile,
DynamicAgentPolicy,
@@ -254,6 +254,16 @@ export function normalizeAgentProfile(raw: unknown): AgentProfile {
};
}
+/** Normalize all profiles on an agent returned by a boot, HTTP, or WS boundary. */
+export function normalizeAgentProfiles(agent: Agent): Agent {
+ return {
+ ...agent,
+ profiles: Array.isArray(agent.profiles)
+ ? agent.profiles.map((profile) => normalizeAgentProfile(profile))
+ : [],
+ };
+}
+
function setPayloadField
(
payload: Partial,
key: K,
diff --git a/apps/web/lib/api/domains/settings-api.ts b/apps/web/lib/api/domains/settings-api.ts
index 63b0233d69..664421fd12 100644
--- a/apps/web/lib/api/domains/settings-api.ts
+++ b/apps/web/lib/api/domains/settings-api.ts
@@ -246,18 +246,11 @@ export async function deleteExecutorProfile(
}
// Agents
-import { normalizeAgentProfile } from "@/lib/api/domains/agent-profile-normalize";
-
-function normalizeAgentResponse(agent: Agent): Agent {
- return {
- ...agent,
- profiles: (agent.profiles ?? []).map((profile) => normalizeAgentProfile(profile)),
- };
-}
+import { normalizeAgentProfiles } from "@/lib/api/domains/agent-profile-normalize";
export async function listAgents(options?: ApiRequestOptions): Promise {
const res = await fetchJson("/api/v1/agents", options);
- return { ...res, agents: (res.agents ?? []).map(normalizeAgentResponse) };
+ return { ...res, agents: (res.agents ?? []).map(normalizeAgentProfiles) };
}
export async function listAgentDiscovery(
@@ -356,7 +349,7 @@ export async function createCustomTUIAgent(
...options,
init: { method: "POST", body: JSON.stringify(payload), ...(options?.init ?? {}) },
});
- return normalizeAgentResponse(res);
+ return normalizeAgentProfiles(res);
}
/**
@@ -391,7 +384,7 @@ export async function updateCustomTUIAgentMCPStrategy(
...(options?.init ?? {}),
},
});
- return normalizeAgentResponse(res);
+ return normalizeAgentProfiles(res);
}
export async function fetchDynamicModels(
diff --git a/apps/web/lib/state/default-state.test.ts b/apps/web/lib/state/default-state.test.ts
index d80c7da666..60b857c305 100644
--- a/apps/web/lib/state/default-state.test.ts
+++ b/apps/web/lib/state/default-state.test.ts
@@ -40,3 +40,41 @@ describe("failed Inbox hydration state", () => {
expect(state.failedInbox.readRevisionByWorkspaceId).toEqual({});
});
});
+
+describe("settings agent hydration", () => {
+ it("normalizes fallback fields from boot-hydrated profiles", () => {
+ const state = mergeInitialState({
+ settingsAgents: {
+ items: [
+ {
+ profiles: [
+ {
+ id: "explicit-profile",
+ fallback_model: "provider/model",
+ auto_fallback: false,
+ },
+ {
+ id: "automatic-profile",
+ fallback_model: "",
+ auto_fallback: true,
+ },
+ ],
+ },
+ ],
+ },
+ } as unknown as HydrationState);
+
+ expect(state.settingsAgents.items[0]?.profiles).toMatchObject([
+ {
+ id: "explicit-profile",
+ fallbackModel: "provider/model",
+ autoFallback: false,
+ },
+ {
+ id: "automatic-profile",
+ fallbackModel: "",
+ autoFallback: true,
+ },
+ ]);
+ });
+});
diff --git a/apps/web/lib/state/default-state.ts b/apps/web/lib/state/default-state.ts
index c657b7af51..39607cf3ca 100644
--- a/apps/web/lib/state/default-state.ts
+++ b/apps/web/lib/state/default-state.ts
@@ -27,6 +27,18 @@ import { seedSettledSessionBoundaries } from "@/lib/state/slices/session/turn-ac
import { migrateSidebarViewDraft, migrateView } from "./slices/ui/ui-slice";
import { mergeAgentProfileRecentUseState } from "@/lib/agent-profile-recent-use";
import { normalizeThreadViews } from "./slices/ui/thread-view-builtins";
+import { normalizeAgentProfiles } from "@/lib/api/domains/agent-profile-normalize";
+
+function mergeHydratedSettingsAgents(
+ incoming: HydrationState["settingsAgents"],
+): DefaultState["settingsAgents"] {
+ if (!incoming) return defaultState.settingsAgents;
+ return {
+ ...defaultState.settingsAgents,
+ ...incoming,
+ items: incoming.items.map(normalizeAgentProfiles),
+ };
+}
export const defaultState = {
kanban: defaultKanbanState.kanban,
@@ -456,7 +468,7 @@ export function mergeInitialState(initialState?: HydrationState): DefaultState {
repositoryBranches: { ...defaultState.repositoryBranches, ...initialState.repositoryBranches },
repositoryScripts: { ...defaultState.repositoryScripts, ...initialState.repositoryScripts },
executors: { ...defaultState.executors, ...initialState.executors },
- settingsAgents: { ...defaultState.settingsAgents, ...initialState.settingsAgents },
+ settingsAgents: mergeHydratedSettingsAgents(initialState.settingsAgents),
agentDiscovery: { ...defaultState.agentDiscovery, ...initialState.agentDiscovery },
availableAgents: { ...defaultState.availableAgents, ...initialState.availableAgents },
agentProfiles: { ...defaultState.agentProfiles, ...initialState.agentProfiles },
diff --git a/apps/web/lib/state/hydration/hydrator.test.ts b/apps/web/lib/state/hydration/hydrator.test.ts
index 3c09d6bc97..ede58b9df0 100644
--- a/apps/web/lib/state/hydration/hydrator.test.ts
+++ b/apps/web/lib/state/hydration/hydrator.test.ts
@@ -425,6 +425,46 @@ describe("mergeInitialState — sidebar views from boot settings", () => {
});
});
+describe("hydrateState — settings agent profiles", () => {
+ it("normalizes fallback fields from nested boot hydration", () => {
+ const result = produce(makeAppDraft(), (draft: Draft) => {
+ hydrateState(draft, {
+ settingsAgents: {
+ items: [
+ {
+ profiles: [
+ {
+ id: "explicit-profile",
+ fallback_model: "provider/model",
+ auto_fallback: false,
+ },
+ {
+ id: "automatic-profile",
+ fallback_model: "",
+ auto_fallback: true,
+ },
+ ],
+ },
+ ],
+ },
+ } as unknown as Partial);
+ });
+
+ expect(result.settingsAgents.items[0]?.profiles).toMatchObject([
+ {
+ id: "explicit-profile",
+ fallbackModel: "provider/model",
+ autoFallback: false,
+ },
+ {
+ id: "automatic-profile",
+ fallbackModel: "",
+ autoFallback: true,
+ },
+ ]);
+ });
+});
+
describe("hydrateState — user settings revisions", () => {
it("applies a newer boot snapshot after an earlier websocket update", () => {
const result = produce(makeAppDraft(), (draft: Draft) => {
diff --git a/apps/web/lib/state/hydration/hydrator.ts b/apps/web/lib/state/hydration/hydrator.ts
index 4f7e448949..7dbb54c597 100644
--- a/apps/web/lib/state/hydration/hydrator.ts
+++ b/apps/web/lib/state/hydration/hydrator.ts
@@ -27,6 +27,7 @@ import {
readMcpAttachmentHistory,
shouldReplaceMcpAttachmentHistory,
} from "@/lib/state/slices/session-runtime/mcp-attachment-reconciliation";
+import { normalizeAgentProfiles } from "@/lib/api/domains/agent-profile-normalize";
import { preserveOmittedExecutorFields } from "@/lib/kanban/map-task";
import { mergeStepOrderRevisions } from "@/lib/kanban/workflow-step-order";
import { deepMerge, mergeSessionMap, mergeLoadingState } from "./merge-strategies";
@@ -170,7 +171,10 @@ function hydrateSettings(draft: Draft, state: HydrationState): void {
const preserveLiveAgentProfiles =
(state.agentProfiles?.version ?? 0) < draft.agentProfiles.version;
if (state.settingsAgents && !preserveLiveAgentProfiles) {
- deepMerge(draft.settingsAgents, state.settingsAgents);
+ deepMerge(draft.settingsAgents, {
+ ...state.settingsAgents,
+ items: state.settingsAgents.items.map(normalizeAgentProfiles),
+ });
}
if (state.agentProfiles) {
// Preserve a newer profile mutation delivered over WebSocket while this
diff --git a/apps/web/src/locales/en/agents.json b/apps/web/src/locales/en/agents.json
index 1759db39a7..7fa9412cc6 100644
--- a/apps/web/src/locales/en/agents.json
+++ b/apps/web/src/locales/en/agents.json
@@ -140,6 +140,10 @@
"mcpStrategyOption": "{{name}}: {{mechanism}}",
"model": "Model",
"modelHeading": "Model",
+ "fallbackNone": "fallback: none",
+ "fallbackExact": "fallback: exact",
+ "fallbackNext": "fallback: next",
+ "fallbackModel": "fallback: {{model}}",
"modelRequiredForAllProfiles": "Model is required for all profiles.",
"noAuthHintLogin": "Click Open terminal to sign in, then refresh.",
"noAuthHintNotInstalled": "Install the agent CLI, then click Refresh.",
diff --git a/apps/web/src/locales/pseudo/agents.json b/apps/web/src/locales/pseudo/agents.json
index ac52404f86..56ee639e50 100644
--- a/apps/web/src/locales/pseudo/agents.json
+++ b/apps/web/src/locales/pseudo/agents.json
@@ -140,6 +140,10 @@
"mcpStrategyOption": "{{name}}: {{mechanism}}",
"model": "Ḿōďēĺ",
"modelHeading": "Ḿōďēĺ",
+ "fallbackNone": "ƒàĺĺƀàćķ: ńōńē",
+ "fallbackExact": "ƒàĺĺƀàćķ: ēxàćţ",
+ "fallbackNext": "ƒàĺĺƀàćķ: ńēxţ",
+ "fallbackModel": "ƒàĺĺƀàćķ: {{model}}",
"modelRequiredForAllProfiles": "Ḿōďēĺ ĩś ŕēqũĩŕēď ƒōŕ àĺĺ ƥŕōƒĩĺēś.",
"noAuthHintLogin": "Ćĺĩćķ Ōƥēń ţēŕḿĩńàĺ ţō śĩĝń ĩń, ţĥēń ŕēƒŕēśĥ.",
"noAuthHintNotInstalled": "Ĩńśţàĺĺ ţĥē àĝēńţ ĆĹĨ, ţĥēń ćĺĩćķ Ŕēƒŕēśĥ.",
diff --git a/apps/web/src/locales/pt-pt/agents.json b/apps/web/src/locales/pt-pt/agents.json
index 722bd9b31e..99733bc02b 100644
--- a/apps/web/src/locales/pt-pt/agents.json
+++ b/apps/web/src/locales/pt-pt/agents.json
@@ -128,6 +128,10 @@
"mcpStrategyOption": "{{name}}: {{mechanism}}",
"model": "Modelo",
"modelHeading": "Modelo",
+ "fallbackNone": "alternativa: nenhum",
+ "fallbackExact": "alternativa: exato",
+ "fallbackNext": "alternativa: seguinte",
+ "fallbackModel": "alternativa: {{model}}",
"modelOptionsAuthenticationRequired": "Inicie sessão neste agente para carregar as opções do modelo.",
"modelOptionsResolutionFailed": "Não foi possível carregar as opções do modelo.",
"modelRequiredForAllProfiles": "O modelo é obrigatório em todos os perfis.",
diff --git a/apps/web/src/locales/zh-cn/agents.json b/apps/web/src/locales/zh-cn/agents.json
index 2773b708da..3877d0df2c 100644
--- a/apps/web/src/locales/zh-cn/agents.json
+++ b/apps/web/src/locales/zh-cn/agents.json
@@ -138,6 +138,10 @@
"mcpStrategyOption": "{{name}}:{{mechanism}}",
"model": "模型",
"modelHeading": "模型",
+ "fallbackNone": "回退:无",
+ "fallbackExact": "回退:精确模型",
+ "fallbackNext": "回退:下一个",
+ "fallbackModel": "回退:{{model}}",
"modelRequiredForAllProfiles": "所有配置都需要模型。",
"noAuthHintLogin": "点击“打开终端”登录,然后刷新。",
"noAuthHintNotInstalled": "安装智能体 CLI,然后点击“刷新”。",
diff --git a/apps/web/src/locales/zh-hk/agents.json b/apps/web/src/locales/zh-hk/agents.json
index 60421677e6..37daf8f9c6 100644
--- a/apps/web/src/locales/zh-hk/agents.json
+++ b/apps/web/src/locales/zh-hk/agents.json
@@ -138,6 +138,10 @@
"mcpStrategyOption": "{{name}}:{{mechanism}}",
"model": "模型",
"modelHeading": "模型",
+ "fallbackNone": "後備:無",
+ "fallbackExact": "後備:精確模型",
+ "fallbackNext": "後備:下一個",
+ "fallbackModel": "後備:{{model}}",
"modelRequiredForAllProfiles": "所有設定都需要模型。",
"noAuthHintLogin": "點擊「開啟終端」登入,然後刷新。",
"noAuthHintNotInstalled": "安裝代理程式 CLI,然後點擊「刷新」。",
diff --git a/apps/web/src/locales/zh-tw/agents.json b/apps/web/src/locales/zh-tw/agents.json
index 2af6c35106..b00a50ac5c 100644
--- a/apps/web/src/locales/zh-tw/agents.json
+++ b/apps/web/src/locales/zh-tw/agents.json
@@ -138,6 +138,10 @@
"mcpStrategyOption": "{{name}}:{{mechanism}}",
"model": "模型",
"modelHeading": "模型",
+ "fallbackNone": "備援:無",
+ "fallbackExact": "備援:精確模型",
+ "fallbackNext": "備援:下一個",
+ "fallbackModel": "備援:{{model}}",
"modelRequiredForAllProfiles": "所有設定都需要模型。",
"noAuthHintLogin": "點選「開啟終端」登入,然後重新整理。",
"noAuthHintNotInstalled": "安裝代理程式 CLI,然後點選「重新整理」。",
diff --git a/docs/plans/configured-fallback-summary/plan.md b/docs/plans/configured-fallback-summary/plan.md
new file mode 100644
index 0000000000..26b8b75176
--- /dev/null
+++ b/docs/plans/configured-fallback-summary/plan.md
@@ -0,0 +1,87 @@
+---
+created: 2026-09-08
+status: complete
+requirements:
+ - REQ-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001
+system_design:
+ - ../../specs/agents/system-design/configured-fallback-summary.md
+legacy_specs: []
+---
+
+# Implementation Plan: Configured Fallback Summary
+
+## Overview
+
+Expose each saved agent profile's fallback policy as a pill in the existing
+Agents settings profile rows. Implement the pure state classification first,
+then render and localize it, and finally prove desktop and mobile behavior.
+
+## Scope
+
+### In scope
+
+- Show `fallback: exact`, `fallback: none`, `fallback: next`, or
+ `fallback: ` after the saved model pill.
+- Derive the display from normalized profile fields with exact-model selection
+ taking precedence over saved fallbacks, then automatic fallback taking
+ precedence over an explicit saved model.
+- Localize the new labels in all required web locales.
+- Preserve wrapping, accessibility, and mobile overflow behavior.
+
+### Out of scope
+
+- Runtime model selection or fallback policy changes.
+- Profile editor controls or persistence changes.
+- Executor catalog probing and effective-model warnings.
+
+## Technical approach
+
+Add a pure helper under `apps/web/lib/` that classifies normalized
+`AgentProfile` fallback fields into exact, executor-default, automatic, or
+explicit-model display states. Use it from `ProfileRowCard` in
+`apps/web/components/settings/agents/agent-profiles-section.tsx`, translating
+only the display label and keeping model IDs opaque. Keep the existing
+flex-wrapped metadata container and place the fallback badge after the model
+badge.
+
+Add the four labels to `agents.json` for `en`, `pt-pt`, `zh-cn`, `zh-hk`, and
+`zh-tw`, plus pseudo-locale generation/ratchet coverage through the repository's
+existing i18n checks.
+
+## Tests
+
+- `apps/web/lib/*fallback*.test.ts`: exact, executor-default, automatic,
+ explicit, and precedence classification.
+- `apps/web/components/settings/agents/agent-profiles-section.test.tsx`: badge
+ ordering and opaque explicit model rendering.
+
+## E2E tests
+
+- Extend `apps/web/e2e/tests/settings/agents-browse-page.spec.ts` with a saved
+ profile row assertion for the desktop outcome.
+- Extend `apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts` with
+ the same fallback summary assertion and the existing no-horizontal-overflow
+ check.
+
+## Work orders
+
+- [x] [Task 01: Render configured fallback summary](task-01-render-configured-fallback-summary.md)
+
+## Verification results
+
+- `make fmt` passed.
+- `make typecheck` passed.
+- `make lint` passed with an external `TMPDIR` to avoid host temp-disk quota.
+- `cd apps/web && pnpm run i18n:check && pnpm run i18n:ratchet` passed.
+- Focused web tests passed: 17 tests across the helper and profile-row suites.
+- Desktop and mobile focused E2E specs passed: 2 tests in each project.
+- `make test` reached the backend suite but remains red on unrelated existing
+ repository/worktree path and error-sanitization tests; an initial attempt
+ also hit the host temporary-disk quota.
+
+## Risks
+
+- Automatic fallback must win when both `auto_fallback` and `fallback_model`
+ are present, matching runtime precedence.
+- Locale catalogs must retain matching keys and placeholders in every required
+ locale.
diff --git a/docs/plans/configured-fallback-summary/task-01-render-configured-fallback-summary.md b/docs/plans/configured-fallback-summary/task-01-render-configured-fallback-summary.md
new file mode 100644
index 0000000000..b0f81f131d
--- /dev/null
+++ b/docs/plans/configured-fallback-summary/task-01-render-configured-fallback-summary.md
@@ -0,0 +1,94 @@
+---
+id: "01-render-configured-fallback-summary"
+title: "Render configured fallback summary"
+status: done
+wave: 1
+depends_on: []
+plan: "plan.md"
+requirements:
+ - REQ-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001
+acceptance_criteria:
+ - AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.1
+ - AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.2
+ - AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.3
+ - AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.4
+system_design:
+ - ../../specs/agents/system-design/configured-fallback-summary.md
+---
+
+# Task 01: Render configured fallback summary
+
+## Summary
+
+Add a pure fallback-state classifier and show its localized result as a badge
+following the model badge in shared Agents settings profile rows. Preserve the
+existing responsive row layout and saved profile data.
+
+## In scope
+
+- Add helper and unit tests for exact, executor-default, automatic, explicit,
+ and precedence
+ cases.
+- Add the fallback badge and component regression coverage.
+- Add required locale keys and update desktop/mobile settings E2E assertions.
+
+## Out of scope
+
+- Runtime fallback behavior, executor model catalogs, and profile persistence.
+- New settings controls or interaction patterns.
+
+## Acceptance
+
+- Profile rows show the four required fallback states in the specified order.
+- A saved explicit model is rendered unchanged, while automatic fallback takes
+ precedence when both fields are set. Exact-model selection takes precedence
+ when dormant automatic or explicit fallback values are also saved.
+- Desktop and phone-sized rows remain accessible, wrapped, and free of document
+ horizontal overflow.
+
+## Verification
+
+```bash
+cd apps && pnpm --filter @kandev/web test -- --run lib/*fallback* components/settings/agents/agent-profiles-section
+cd apps/web && pnpm run typecheck && pnpm run i18n:check && pnpm run i18n:ratchet
+cd apps && pnpm --filter @kandev/web e2e:run -- settings/agents-browse-page.spec.ts settings/mobile-agent-profile-layout.spec.ts
+```
+
+## Files likely touched
+
+- `apps/web/lib/agent-profile-fallback.ts`
+- `apps/web/lib/agent-profile-fallback.test.ts`
+- `apps/web/components/settings/agents/agent-profiles-section.tsx`
+- `apps/web/components/settings/agents/agent-profiles-section.test.tsx`
+- `apps/web/src/locales/*/agents.json`
+- `apps/web/e2e/tests/settings/agents-browse-page.spec.ts`
+- `apps/web/e2e/tests/settings/mobile-agent-profile-layout.spec.ts`
+
+## Dependencies
+
+None.
+
+## Risks
+
+- Reusing translated interpolation values for `none` and `next` would leave
+ those state names untranslated; use dedicated localized labels.
+- The shared row is used by mobile and desktop, so badge order must not depend
+ on viewport branches.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- `docs/specs/agents/requirements/configured-fallback-summary.md`
+- `docs/specs/agents/system-design/configured-fallback-summary.md`
+- Existing `AgentProfile` normalization and profile-row tests.
+
+## Results
+
+Implemented the classifier, localized profile-row fallback badge, unit/component
+regressions, and desktop/mobile E2E coverage. `make fmt`, `make typecheck`,
+`make lint`, i18n checks, focused web tests, and both focused E2E projects pass.
+The repository-wide `make test` command reaches unrelated backend test failures
+in repository/worktree path and error-sanitization coverage.
diff --git a/docs/specs/agents/README.md b/docs/specs/agents/README.md
index d4011e84ef..ea1d1f1e84 100644
--- a/docs/specs/agents/README.md
+++ b/docs/specs/agents/README.md
@@ -28,6 +28,8 @@ surface shared by task and Office consumers.
system](../office/README.md).
- Presentation-only behavior belongs to the [UI system](../ui/README.md).
+
+
## Migration record
Migration remains in progress while legacy source detail is extracted from the
diff --git a/docs/specs/agents/requirements/configured-fallback-summary.md b/docs/specs/agents/requirements/configured-fallback-summary.md
new file mode 100644
index 0000000000..9d4dd90943
--- /dev/null
+++ b/docs/specs/agents/requirements/configured-fallback-summary.md
@@ -0,0 +1,62 @@
+---
+status: active
+system: agents
+created: 2026-09-08
+owners:
+ - kandev
+---
+
+# Configured fallback summary requirements
+
+## Overview
+
+Agent profile rows on the Agents settings page expose the fallback policy saved
+for each profile. The summary lets users distinguish executor-default behavior,
+exact-model selection, automatic selection of the next model, and a configured
+explicit fallback without opening the profile editor.
+
+## Terminology
+
+- **Exact-model state:** `require_exact_model` is true. Saved automatic or
+ explicit fallback values are dormant while this policy is enabled.
+- **Executor-default state:** `require_exact_model` is false, `auto_fallback` is
+ false, and `fallback_model` is empty. The executor may use its default model.
+- **Automatic fallback state:** `require_exact_model` is false and
+ `auto_fallback` is true. The configured explicit fallback value is ignored by
+ runtime precedence.
+- **Explicit fallback state:** `require_exact_model` is false,
+ `auto_fallback` is false, and `fallback_model` is non-empty.
+
+## Requirements
+
+### REQ-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001: Show the configured fallback policy
+
+**Intent:** Make the saved fallback policy visible in the profile list.
+
+**User story:** As a Kandev user, I want each agent profile row to show its
+configured fallback policy, so that I can understand how an unavailable start
+model will be handled without opening every profile.
+
+#### Acceptance criteria
+
+- **AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.1:** When a profile row is shown
+ on Settings > Agents, it shall render a fallback pill immediately after the
+ model pill, with `fallback: exact` for the exact-model state, `fallback: none`
+ for the executor-default state, `fallback: next` for the automatic fallback
+ state, or `fallback: ` for the explicit fallback state.
+- **AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.2:** The fallback pill shall use
+ the profile's saved `require_exact_model`, `auto_fallback`, and
+ `fallback_model` values, and shall not change those values or infer runtime
+ availability.
+- **AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.3:** The fallback summary shall
+ remain readable and wrap with the existing profile metadata on phone-sized
+ screens without introducing document-level horizontal overflow.
+- **AC-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001.4:** The fallback summary shall
+ be localized through the Agents translation namespace while preserving the
+ configured model identifier as opaque product data.
+
+## Out of scope
+
+- Changing runtime fallback precedence or model selection.
+- Editing profile fallback settings from the profile list.
+- Showing executor availability, runtime effective models, or fallback warnings.
diff --git a/docs/specs/agents/system-design/configured-fallback-summary.md b/docs/specs/agents/system-design/configured-fallback-summary.md
new file mode 100644
index 0000000000..fa9f1b8c92
--- /dev/null
+++ b/docs/specs/agents/system-design/configured-fallback-summary.md
@@ -0,0 +1,78 @@
+---
+status: current
+system: agents
+requirements:
+ - REQ-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001
+created: 2026-09-08
+owners:
+ - kandev
+---
+
+# Configured fallback summary system design
+
+## Context and boundaries
+
+The agent system owns the saved `AgentProfile` model policy. The Agents settings
+page presents that policy as a read-only summary; it does not consult executor
+catalogs or runtime session state. Runtime precedence remains defined by the
+[no silent model fallback design](no-silent-model-fallback-01.md).
+
+## Requirement mapping
+
+| Requirement | Design section |
+| --- | --- |
+| `REQ-AGENTS-CONFIGURED-FALLBACK-SUMMARY-001` | Profile-row presentation and responsive behavior |
+
+## Profile-row presentation
+
+`apps/web/components/settings/agents/agent-profiles-section.tsx` remains the
+shared renderer for profile rows on the Agents index and grouped agent cards.
+The canonical `AgentProfile` fields `requireExactModel`/`require_exact_model`,
+`autoFallback`/`auto_fallback`, and `fallbackModel`/`fallback_model` are already
+normalized at the API boundary.
+
+A small pure presentation helper shall classify the profile into `exact`,
+`none`, `next`, or an explicit model identifier using this precedence:
+
+1. `require_exact_model` true -> `exact`.
+2. Otherwise, `auto_fallback` true -> `next`.
+3. Otherwise, a non-empty `fallback_model` -> that model identifier.
+4. Otherwise -> `none` (the executor-default state).
+
+The row shall render the existing model `Badge` first and the new fallback
+`Badge` immediately after it. The fallback badge may appear alongside the
+existing mode badge. The model identifier is interpolated unchanged; the
+helper must not trim, validate, or compare it against advertised models.
+
+The translation namespace `agents` owns separate localized labels for the exact,
+executor-default, and automatic states plus an interpolated explicit-model
+label. No module-scope translation call is permitted.
+
+## Responsive and accessibility behavior
+
+The row keeps its existing flex-wrap metadata layout and link/action hit areas.
+The additional badge is content-only: desktop retains the current row anatomy,
+and phone-sized layouts wrap the badges instead of adding a second scroll owner
+or changing navigation. Existing profile-row links remain the accessible
+navigation target; the fallback badge is non-interactive and does not obscure
+that target.
+
+The nearest shipped mobile exemplar is the existing profile-row layout covered
+by `mobile-agent-profile-layout.spec.ts`; its flex wrapping and horizontal
+overflow assertion remain the geometry baseline. A focused mobile E2E assertion
+shall verify the fallback text in the same row and preserve the no-overflow
+contract.
+
+## Verification boundaries
+
+Unit coverage tests the pure classification helper for exact, executor-default,
+automatic, and explicit profiles, including exact precedence when dormant
+fallback values are saved and automatic precedence when exact mode is off.
+Component coverage verifies ordering after the model badge and opaque model
+rendering. Desktop and mobile Playwright coverage verifies the rendered row from
+real saved profile data and confirms the phone layout remains usable.
+
+## Related design
+
+- [No Silent Model Fallback Part 1](no-silent-model-fallback-01.md)
+- [No Silent Model Fallback Part 2](no-silent-model-fallback-02.md)