PR #3521
Sections
Review

fix: remove host model warnings from profile selectors

main ← feature/investigate-misleadi-70f 18 files +312 −298 PR #3521 ↗

Profile selectors no longer show host-model warnings; the executor catalog owns model selection and warnings appear only after launch.

Why this change

The task-create selector compared the saved profile model with the host probe catalog and showed an amber warning. The host catalog can differ from the executor catalog, so the warning was often wrong and blocked selection.

What it does

Architecture, end to end

Host probe stays an editing hint. The selector shows only health status. The executor decides the model at launch and persists a warning when the effective model differs.

flowchart LR
  HostProbe[Host probe catalog] --> Editor[Profile editor]
  Editor -- advisory --> EditorHint[Missing model and variation hint]
  HostProbe -. no warning .-> Selector[Task-create selector]
  Selector --> Health[Capability health only]
  Selector --> Executor[Selected executor ACP catalog]
  Executor --> Policy[applyStartModelPolicy]
  Policy --> Warning[Persisted model_selection_warning in chat]

Key code changes

Drag to pan. Use the + and − buttons to zoom. Click a node to open the full code. The arrows show how the parts interact.

drag to pan · +/− to zoom · click a node for details

Remove host-model warnings from selector hookapps/web/components/task-create-dialog-options.tsx ↗
useAgentProfileOptions(agentProfiles, context)
Click for details →

The hook now refreshes capability health only and renders one label for both dropdown and trigger.

Before and after
"use client";

import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import { t } from "@/lib/i18n";
import { IconAlertTriangle, IconGitBranch, IconTerminal2 } from "@tabler/icons-react";
import { IconGitBranch, IconTerminal2 } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@kandev/ui/drawer";
import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow";
  Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import type { AvailableAgent } from "@/lib/types/http-agents";
import type { AgentProfileOption } from "@/lib/state/slices";
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
import { useFeature } from "@/hooks/domains/features/use-feature";
import { isSelectableAgentProfile } from "@/lib/state/slices/settings/types";
import {
  isSelectableAgentProfile,
  refreshProfileCapabilities,
} from "@/lib/state/slices/settings/types";
import { formatUserHomePath, truncateRepoPath } from "@/lib/utils";
import { getExecutorIcon } from "@/lib/executor-icons";
import { AgentLogo } from "@/components/agent-logo";
import { getCapabilityWarning } from "@/lib/capability-warning";
import { findUniqueModelVariation } from "@/lib/model-variation";
import { useTouchDrawer } from "@/hooks/use-compact-task-chrome";
New hook body
export function useAgentProfileOptions(
  agentProfiles: AgentProfileOption[],
  context?: AgentProfileRecentUseContext,
): OptionItem[] {
  const { t } = useTranslation();
  // Keep capability discovery alive for every selector surface. The host
  // catalog supplies health status only; it never participates in model-ID
  // matching or selector eligibility.
  const availableAgents = useAvailableAgents();
  const dynamicRoutingEnabled = useFeature("dynamicAgentRouting");
  const storeApi = useAppStoreApi();
  const recentUseLoaded = useAppStore((state) => !context || state.agentProfileRecentUse.loaded);
  const recentProfileIds = useAppStore((state) =>
    context ? state.agentProfileRecentUse?.records[context]?.profileIds : undefined,
  );
  useEffect(() => {
    if (!context || recentUseLoaded) return;
    void ensureAgentProfileRecentUseLoaded(storeApi);
  }, [context, recentUseLoaded, storeApi]);
  return useMemo(() => {
    const profilesWithCapabilities = refreshProfileCapabilities(
      agentProfiles,
      availableAgents.items,
    );
    const selectable = profilesWithCapabilities.filter((profile) =>
      isSelectableAgentProfile(profile, dynamicRoutingEnabled),
    );
    const orderedProfiles = context
      ? orderAgentProfilesByRecentUse(selectable, recentProfileIds)
      : selectable;
    return orderedProfiles.map((profile: AgentProfileOption) => {
      const parts = profile.label.split(" \u2022 ");
      const agentLabel = parts[0] ?? profile.label;
      const profileLabel = parts[1] ?? "";
      const isPassthrough = profile.cli_passthrough === true;
      const warning = getCapabilityWarning(profile.capability_status, profile.capability_error);
      const renderProfileLabel = () => (
        <span className="flex min-w-0 flex-1 flex-col gap-1">
          <span className="flex shrink-0 items-center justify-between gap-2">
            <span className="flex shrink-0 items-center gap-1.5">
              <AgentLogo agentName={profile.agent_name} className="shrink-0" />
              <span>{agentLabel}</span>
              {warning && (
                <warning.Icon className={`size-3.5 ${warning.color}`} title={warning.title} />
              )}
            </span>
            <span className="flex shrink-0 items-center gap-1.5">
              {isPassthrough && (
                <IconTerminal2 className="size-3.5 text-muted-foreground" title={t("common:cliModeYourPromptWillBe")} />
              )}
              {profileLabel ? (
                <ScrollOnOverflow className="rounded-full border border-border px-2 py-0.5 text-xs">
                  {profileLabel}
                </ScrollOnOverflow>
              ) : null}
            </span>
          </span>
        </span>
      );
      return {
        value: profile.id,
        label: profile.label,
        disabled: undefined,
        disabledReason: undefined,
        renderLabel: renderProfileLabel,
        renderTriggerLabel: renderProfileLabel,
      };
    });
  }, [agentProfiles, availableAgents.items, context, dynamicRoutingEnabled, recentProfileIds, t]);
}
describe("useAgentProfileOptions model-independent labels")
Click for details →

Tests now assert that no host-model advisory appears for any catalog shape and that health indicators remain.

New parameterized coverage
describe("useAgentProfileOptions model-independent labels", () => {
  // @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.1
  it.each([
    ["exact", "gpt-5", ["gpt-5"]],
    ["missing", GONE_MODEL, ["gpt-5"]],
    ["unique variation", "opus", ["opus[1m]"]],
    ["multiple variations", "opus", ["opus[1m]", "opus[270k]"]],
    ["legacy effort IDs", "gpt-6-astra", ["gpt-6-astra[low]", "gpt-6-astra[high]"]],
    ["bracketed request", "opus[1m]", ["opus[270k]"]],
    ["empty catalog", GONE_MODEL, []],
    ["provider default", "", ["gpt-5"]],
  ])("does not show host model advisories in either label: %s", (_, model, models) => {
    setAvailableAgents([{
      ...AGENT_WITH_GPT,
      model_config: {
        ...AGENT_WITH_GPT.model_config,
        available_models: (models as string[]).map((id) => ({ id, name: id })),
      },
    }]);
    const profile = profileOption({ model: model as string });
    const { result } = renderHook(() => useAgentProfileOptions([profile]));
    const option = result.current[0]!;
    expect(option.disabled).toBeUndefined();
    expect(option.disabledReason).toBeUndefined();
    render(
      <TooltipProvider>
        <div data-testid="option-label">{option.renderLabel()}</div>
        <div data-testid="selected-label">{option.renderTriggerLabel?.()}</div>
      </TooltipProvider>,
    );
    for (const label of ["option-label", "selected-label"]) {
      const element = screen.getByTestId(label);
      expect(element.textContent).toContain("hybrid");
      expect(element.querySelector("button, .tabler-icon-alert-triangle")).toBeNull();
      expect(element.querySelector('[title*="host probe"]')).toBeNull();
    }
  });

  // @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.3
  it.each(["auth_required", "not_installed", "failed"] as const)(
    "preserves %s health indicators when the saved model is absent",
    (capability_status) => {
      setAvailableAgents([]);
      const profile = profileOption({
        model: GONE_MODEL,
        capability_status,
        capability_error: "Agent needs attention",
      });
      const { result } = renderHook(() => useAgentProfileOptions([profile]));
      const option = result.current[0]!;
      render(
        <TooltipProvider>
          <div>{option.renderLabel()}</div>
          <div>{option.renderTriggerLabel?.()}</div>
        </TooltipProvider>,
      );
      expect(screen.getAllByTitle("Agent needs attention")).toHaveLength(2);
      expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
    },
  );
});
Stability check
it("keeps labels stable when a pending host catalog changes", () => {
  setAvailableAgents([]);
  const profile = profileOption({ model: GONE_MODEL });
  const { rerender } = render(<OptionsProbe profiles={[profile]} />);
  const initialLabel = screen.getByTestId("option-0").innerHTML;
  setAvailableAgents([AGENT_WITH_GPT]);
  rerender(<OptionsProbe profiles={[profile]} />);
  expect(screen.getByTestId("option-0").innerHTML).toBe(initialLabel);
  expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
});

it("refreshes capability health from the host snapshot without inspecting model IDs", () => {
  setAvailableAgents([{
    ...AGENT_WITH_GPT,
    available: false,
    model_config: { ...AGENT_WITH_GPT.model_config, error: "Agent unavailable" },
  }]);
  const { result } = renderHook(() => useAgentProfileOptions([profileOption({ model: GONE_MODEL })]));
  const option = result.current[0]!;
  render(<TooltipProvider><div>{option.renderLabel()}</div><div>{option.renderTriggerLabel?.()}</div></TooltipProvider>);
  expect(screen.getAllByTitle("Agent unavailable")).toHaveLength(2);
  expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
});
"profileStartModelNotAdvertisedOnHost"
Click for details →

The two selector-only keys are deleted from en, pseudo, pt-pt, zh-cn, zh-hk, and zh-tw.

Locale diff
  "profileStartModelNotAdvertisedOnHost": "The host probe did not advertise {{model}}. The selected executor will decide the model at launch.",
  "profileStartModelUniqueVariationOnHost": "The host probe found one possible variation of {{model}}: {{variation}}. The selected executor will decide the model at launch.",
  "modelVariationAdvisory": "The host catalog has one possible variation of {{model}}: {{variation}}. The saved model remains unchanged until launch.",
Kept key
{
  "modelVariationAdvisory": "The host catalog has one possible variation of {{model}}: {{variation}}. The saved model remains unchanged until launch.",
  "startModelUnavailable": "No longer available; select a different model",
  "fallbackModelUnavailable": "Fallback model no longer available; select a different one"
}
launchExecutorOnlyModelProfile(page, apiClient, profile, mobile)
Click for details →

The new helper creates a profile whose model is absent on the host but present on the executor and verifies no warning appears.

New helper
export async function launchExecutorOnlyModelProfile(
  page: Page,
  apiClient: ApiClient,
  profile: AgentProfile,
  mobile: boolean,
) {
  await expect.poll(async () => {
    const { agents } = await apiClient.listAvailableAgents();
    return agents.find((agent) => agent.name === "mock-agent")?.model_config.status;
  }).toBe("ok");
  const { agents } = await apiClient.listAvailableAgents();
  const hostModels = agents.find((a) => a.name === "mock-agent")!.model_config.available_models.map((m) => m.id);
  expect(hostModels.length).toBeGreaterThan(0);
  expect(hostModels).not.toContain(profile.model);
  const kanban = new KanbanPage(page);
  await kanban.goto();
  await page.reload();
  if (mobile) await page.getByRole("button", { name: "Add task" }).tap();
  else await kanban.createTaskButton.first().click();
  const dialog = page.getByTestId("create-task-dialog");
  const selector = dialog.getByTestId("agent-profile-selector");
  await selector.click();
  const option = page.getByRole("listbox").getByRole("option", { name: profile.name });
  await expect(option).toBeEnabled();
  await expect(option.locator("button, .tabler-icon-alert-triangle")).toHaveCount(0);
  if (mobile) await option.tap();
  else {
    const search = page.locator("[cmdk-input]");
    await search.fill(profile.name);
    await search.press("Enter");
  }
  await expect(page.getByRole("listbox")).not.toBeVisible();
  await expect(selector).toContainText(profile.name);
  await expect(selector.locator("button, .tabler-icon-alert-triangle")).toHaveCount(0);
  await dialog.getByTestId("task-title-input").fill("Use my saved model");
  await dialog.getByTestId("task-description-input").fill("/e2e:simple-message");
  await dialog.getByTestId("submit-start-agent").click();
  if (mobile) await kanban.taskCardByTitle("Use my saved model").tap();
  await expect(page).toHaveURL(/\/t\/[^/?]+/);
  const taskId = new URL(page.url()).pathname.split("/")[2];
  await expect.poll(async () => (await apiClient.listTaskSessions(taskId)).sessions.length).toBe(1);
  const { sessions } = await apiClient.listTaskSessions(taskId);
  const sessionId = sessions[0].id;
  expect(sessions[0].agent_profile_id).toBe(profile.id);
  await waitForSessionDone(apiClient, taskId, sessionId, "Waiting for requested executor model");
  const session = new SessionPage(page);
  await session.waitForLoad();
  await assertRequestedModel(page, apiClient, taskId, sessionId, profile);
  await page.reload();
  await session.waitForLoad();
  await assertRequestedModel(page, apiClient, taskId, sessionId, profile);
  if (mobile) await assertNoDocumentHorizontalOverflow(page, "requested model after reload");
}
Updated desktop spec
test("keeps a host-mismatched profile selectable", async ({ testPage, apiClient, prCapture }) => {
  const profile = await createMismatchedProfile(apiClient, "Host mismatch selectable profile");
  const kanban = new KanbanPage(testPage);
  await kanban.goto();
  await testPage.reload({ waitUntil: "networkidle" });
  await kanban.createTaskButton.first().click();
  const dialog = testPage.getByTestId("create-task-dialog");
  const selector = dialog.getByTestId("agent-profile-selector");
  await selector.click();
  const option = testPage.getByRole("listbox").getByRole("option", { name: profile.name, exact: false });
  await expect(option).toBeVisible();
  await expect(option).toBeEnabled();
  await expect(option.getByTestId("agent-profile-model-probe-warning")).toHaveCount(0);
  await expect(option.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
  await prCapture.screenshot("desktop-profile-options", { caption: "Saved profiles remain selectable without host-model warnings." });
  const search = testPage.locator("[cmdk-input]");
  await search.fill(profile.name);
  await search.press("Enter");
  await expect(testPage.getByRole("listbox")).not.toBeVisible();
  await expect(selector).toContainText(profile.name);
});
REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003
Click for details →

Requirement 003 now forbids host-model advisories in selectors and keeps editor and chat warnings.

Requirement 003
### REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003: Show model warnings at the point of action

**Intent:** Let users select a saved profile without treating a host discovery difference as evidence of an executor failure.

- **AC-003.1:** A host model-catalog difference shall not add an icon, message, tooltip, or help action to a profile selector. This applies to its options and its selected label, including missing models, one or several model variations, and empty or pending catalogs.
- **AC-003.2:** On desktop and mobile, an otherwise eligible profile shall remain selectable by keyboard or pointer. On touch devices, tapping its row shall select it without opening model help.
- **AC-003.3:** Existing authentication, installation, and capability-probe failure indicators shall remain visible. Their presence shall not depend on whether the saved model is advertised.
- **AC-003.4:** The profile editor shall retain its missing-model treatment, unique-variation advisory, and fallback controls.
- **AC-003.5:** When the executor applies the requested model successfully, a host catalog difference shall not cause a model-selection warning in task chat.
- **AC-003.6:** Selecting a profile shall not rewrite its saved model, reasoning configuration, or fallback settings.
Public docs
### Host probes and executor model catalogs

The model list shown while editing a profile comes from a host probe. It is an editing hint, not a launch gate. A profile remains selectable when its saved model is missing from that host list. Profile selectors do not show a model warning for this difference. Inspect the model list in profile settings for discovery details. Authentication, installation, and probe-failure indicators remain visible on profile selectors.

At task launch, the selected executor's ACP catalog is authoritative. For profiles without automatic fallback, Kandev follows the four-step order: exact model, explicit fallback, one unique bracketed variation, then agent default. Kandev stores one warning in task chat with the requested model and the effective model when known.
Read the changes as a list

Remove host-model warnings from selector hook

apps/web/components/task-create-dialog-options.tsx

The hook now refreshes capability health only and renders one label for both dropdown and trigger.

Before and after
"use client";

import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import { t } from "@/lib/i18n";
import { IconAlertTriangle, IconGitBranch, IconTerminal2 } from "@tabler/icons-react";
import { IconGitBranch, IconTerminal2 } from "@tabler/icons-react";
import { Badge } from "@kandev/ui/badge";
  Drawer,
  DrawerContent,
  DrawerDescription,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@kandev/ui/drawer";
import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow";
  Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import type { AvailableAgent } from "@/lib/types/http-agents";
import type { AgentProfileOption } from "@/lib/state/slices";
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
import { useFeature } from "@/hooks/domains/features/use-feature";
import { isSelectableAgentProfile } from "@/lib/state/slices/settings/types";
import {
  isSelectableAgentProfile,
  refreshProfileCapabilities,
} from "@/lib/state/slices/settings/types";
import { formatUserHomePath, truncateRepoPath } from "@/lib/utils";
import { getExecutorIcon } from "@/lib/executor-icons";
import { AgentLogo } from "@/components/agent-logo";
import { getCapabilityWarning } from "@/lib/capability-warning";
import { findUniqueModelVariation } from "@/lib/model-variation";
import { useTouchDrawer } from "@/hooks/use-compact-task-chrome";
New hook body
export function useAgentProfileOptions(
  agentProfiles: AgentProfileOption[],
  context?: AgentProfileRecentUseContext,
): OptionItem[] {
  const { t } = useTranslation();
  // Keep capability discovery alive for every selector surface. The host
  // catalog supplies health status only; it never participates in model-ID
  // matching or selector eligibility.
  const availableAgents = useAvailableAgents();
  const dynamicRoutingEnabled = useFeature("dynamicAgentRouting");
  const storeApi = useAppStoreApi();
  const recentUseLoaded = useAppStore((state) => !context || state.agentProfileRecentUse.loaded);
  const recentProfileIds = useAppStore((state) =>
    context ? state.agentProfileRecentUse?.records[context]?.profileIds : undefined,
  );
  useEffect(() => {
    if (!context || recentUseLoaded) return;
    void ensureAgentProfileRecentUseLoaded(storeApi);
  }, [context, recentUseLoaded, storeApi]);
  return useMemo(() => {
    const profilesWithCapabilities = refreshProfileCapabilities(
      agentProfiles,
      availableAgents.items,
    );
    const selectable = profilesWithCapabilities.filter((profile) =>
      isSelectableAgentProfile(profile, dynamicRoutingEnabled),
    );
    const orderedProfiles = context
      ? orderAgentProfilesByRecentUse(selectable, recentProfileIds)
      : selectable;
    return orderedProfiles.map((profile: AgentProfileOption) => {
      const parts = profile.label.split(" \u2022 ");
      const agentLabel = parts[0] ?? profile.label;
      const profileLabel = parts[1] ?? "";
      const isPassthrough = profile.cli_passthrough === true;
      const warning = getCapabilityWarning(profile.capability_status, profile.capability_error);
      const renderProfileLabel = () => (
        <span className="flex min-w-0 flex-1 flex-col gap-1">
          <span className="flex shrink-0 items-center justify-between gap-2">
            <span className="flex shrink-0 items-center gap-1.5">
              <AgentLogo agentName={profile.agent_name} className="shrink-0" />
              <span>{agentLabel}</span>
              {warning && (
                <warning.Icon className={`size-3.5 ${warning.color}`} title={warning.title} />
              )}
            </span>
            <span className="flex shrink-0 items-center gap-1.5">
              {isPassthrough && (
                <IconTerminal2 className="size-3.5 text-muted-foreground" title={t("common:cliModeYourPromptWillBe")} />
              )}
              {profileLabel ? (
                <ScrollOnOverflow className="rounded-full border border-border px-2 py-0.5 text-xs">
                  {profileLabel}
                </ScrollOnOverflow>
              ) : null}
            </span>
          </span>
        </span>
      );
      return {
        value: profile.id,
        label: profile.label,
        disabled: undefined,
        disabledReason: undefined,
        renderLabel: renderProfileLabel,
        renderTriggerLabel: renderProfileLabel,
      };
    });
  }, [agentProfiles, availableAgents.items, context, dynamicRoutingEnabled, recentProfileIds, t]);
}

Model-independent selector tests

apps/web/components/task-create-dialog-options.test.tsx

Tests now assert that no host-model advisory appears for any catalog shape and that health indicators remain.

New parameterized coverage
describe("useAgentProfileOptions model-independent labels", () => {
  // @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.1
  it.each([
    ["exact", "gpt-5", ["gpt-5"]],
    ["missing", GONE_MODEL, ["gpt-5"]],
    ["unique variation", "opus", ["opus[1m]"]],
    ["multiple variations", "opus", ["opus[1m]", "opus[270k]"]],
    ["legacy effort IDs", "gpt-6-astra", ["gpt-6-astra[low]", "gpt-6-astra[high]"]],
    ["bracketed request", "opus[1m]", ["opus[270k]"]],
    ["empty catalog", GONE_MODEL, []],
    ["provider default", "", ["gpt-5"]],
  ])("does not show host model advisories in either label: %s", (_, model, models) => {
    setAvailableAgents([{
      ...AGENT_WITH_GPT,
      model_config: {
        ...AGENT_WITH_GPT.model_config,
        available_models: (models as string[]).map((id) => ({ id, name: id })),
      },
    }]);
    const profile = profileOption({ model: model as string });
    const { result } = renderHook(() => useAgentProfileOptions([profile]));
    const option = result.current[0]!;
    expect(option.disabled).toBeUndefined();
    expect(option.disabledReason).toBeUndefined();
    render(
      <TooltipProvider>
        <div data-testid="option-label">{option.renderLabel()}</div>
        <div data-testid="selected-label">{option.renderTriggerLabel?.()}</div>
      </TooltipProvider>,
    );
    for (const label of ["option-label", "selected-label"]) {
      const element = screen.getByTestId(label);
      expect(element.textContent).toContain("hybrid");
      expect(element.querySelector("button, .tabler-icon-alert-triangle")).toBeNull();
      expect(element.querySelector('[title*="host probe"]')).toBeNull();
    }
  });

  // @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.3
  it.each(["auth_required", "not_installed", "failed"] as const)(
    "preserves %s health indicators when the saved model is absent",
    (capability_status) => {
      setAvailableAgents([]);
      const profile = profileOption({
        model: GONE_MODEL,
        capability_status,
        capability_error: "Agent needs attention",
      });
      const { result } = renderHook(() => useAgentProfileOptions([profile]));
      const option = result.current[0]!;
      render(
        <TooltipProvider>
          <div>{option.renderLabel()}</div>
          <div>{option.renderTriggerLabel?.()}</div>
        </TooltipProvider>,
      );
      expect(screen.getAllByTitle("Agent needs attention")).toHaveLength(2);
      expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
    },
  );
});
Stability check
it("keeps labels stable when a pending host catalog changes", () => {
  setAvailableAgents([]);
  const profile = profileOption({ model: GONE_MODEL });
  const { rerender } = render(<OptionsProbe profiles={[profile]} />);
  const initialLabel = screen.getByTestId("option-0").innerHTML;
  setAvailableAgents([AGENT_WITH_GPT]);
  rerender(<OptionsProbe profiles={[profile]} />);
  expect(screen.getByTestId("option-0").innerHTML).toBe(initialLabel);
  expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
});

it("refreshes capability health from the host snapshot without inspecting model IDs", () => {
  setAvailableAgents([{
    ...AGENT_WITH_GPT,
    available: false,
    model_config: { ...AGENT_WITH_GPT.model_config, error: "Agent unavailable" },
  }]);
  const { result } = renderHook(() => useAgentProfileOptions([profileOption({ model: GONE_MODEL })]));
  const option = result.current[0]!;
  render(<TooltipProvider><div>{option.renderLabel()}</div><div>{option.renderTriggerLabel?.()}</div></TooltipProvider>);
  expect(screen.getAllByTitle("Agent unavailable")).toHaveLength(2);
  expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
});

Remove unused locale keys

apps/web/src/locales/en/settings.json

The two selector-only keys are deleted from en, pseudo, pt-pt, zh-cn, zh-hk, and zh-tw.

Locale diff
  "profileStartModelNotAdvertisedOnHost": "The host probe did not advertise {{model}}. The selected executor will decide the model at launch.",
  "profileStartModelUniqueVariationOnHost": "The host probe found one possible variation of {{model}}: {{variation}}. The selected executor will decide the model at launch.",
  "modelVariationAdvisory": "The host catalog has one possible variation of {{model}}: {{variation}}. The saved model remains unchanged until launch.",
Kept key
{
  "modelVariationAdvisory": "The host catalog has one possible variation of {{model}}: {{variation}}. The saved model remains unchanged until launch.",
  "startModelUnavailable": "No longer available; select a different model",
  "fallbackModelUnavailable": "Fallback model no longer available; select a different one"
}

E2E helper for executor-only launch

apps/web/e2e/tests/settings/profile-model-selection-helpers.ts

The new helper creates a profile whose model is absent on the host but present on the executor and verifies no warning appears.

New helper
export async function launchExecutorOnlyModelProfile(
  page: Page,
  apiClient: ApiClient,
  profile: AgentProfile,
  mobile: boolean,
) {
  await expect.poll(async () => {
    const { agents } = await apiClient.listAvailableAgents();
    return agents.find((agent) => agent.name === "mock-agent")?.model_config.status;
  }).toBe("ok");
  const { agents } = await apiClient.listAvailableAgents();
  const hostModels = agents.find((a) => a.name === "mock-agent")!.model_config.available_models.map((m) => m.id);
  expect(hostModels.length).toBeGreaterThan(0);
  expect(hostModels).not.toContain(profile.model);
  const kanban = new KanbanPage(page);
  await kanban.goto();
  await page.reload();
  if (mobile) await page.getByRole("button", { name: "Add task" }).tap();
  else await kanban.createTaskButton.first().click();
  const dialog = page.getByTestId("create-task-dialog");
  const selector = dialog.getByTestId("agent-profile-selector");
  await selector.click();
  const option = page.getByRole("listbox").getByRole("option", { name: profile.name });
  await expect(option).toBeEnabled();
  await expect(option.locator("button, .tabler-icon-alert-triangle")).toHaveCount(0);
  if (mobile) await option.tap();
  else {
    const search = page.locator("[cmdk-input]");
    await search.fill(profile.name);
    await search.press("Enter");
  }
  await expect(page.getByRole("listbox")).not.toBeVisible();
  await expect(selector).toContainText(profile.name);
  await expect(selector.locator("button, .tabler-icon-alert-triangle")).toHaveCount(0);
  await dialog.getByTestId("task-title-input").fill("Use my saved model");
  await dialog.getByTestId("task-description-input").fill("/e2e:simple-message");
  await dialog.getByTestId("submit-start-agent").click();
  if (mobile) await kanban.taskCardByTitle("Use my saved model").tap();
  await expect(page).toHaveURL(/\/t\/[^/?]+/);
  const taskId = new URL(page.url()).pathname.split("/")[2];
  await expect.poll(async () => (await apiClient.listTaskSessions(taskId)).sessions.length).toBe(1);
  const { sessions } = await apiClient.listTaskSessions(taskId);
  const sessionId = sessions[0].id;
  expect(sessions[0].agent_profile_id).toBe(profile.id);
  await waitForSessionDone(apiClient, taskId, sessionId, "Waiting for requested executor model");
  const session = new SessionPage(page);
  await session.waitForLoad();
  await assertRequestedModel(page, apiClient, taskId, sessionId, profile);
  await page.reload();
  await session.waitForLoad();
  await assertRequestedModel(page, apiClient, taskId, sessionId, profile);
  if (mobile) await assertNoDocumentHorizontalOverflow(page, "requested model after reload");
}
Updated desktop spec
test("keeps a host-mismatched profile selectable", async ({ testPage, apiClient, prCapture }) => {
  const profile = await createMismatchedProfile(apiClient, "Host mismatch selectable profile");
  const kanban = new KanbanPage(testPage);
  await kanban.goto();
  await testPage.reload({ waitUntil: "networkidle" });
  await kanban.createTaskButton.first().click();
  const dialog = testPage.getByTestId("create-task-dialog");
  const selector = dialog.getByTestId("agent-profile-selector");
  await selector.click();
  const option = testPage.getByRole("listbox").getByRole("option", { name: profile.name, exact: false });
  await expect(option).toBeVisible();
  await expect(option).toBeEnabled();
  await expect(option.getByTestId("agent-profile-model-probe-warning")).toHaveCount(0);
  await expect(option.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
  await prCapture.screenshot("desktop-profile-options", { caption: "Saved profiles remain selectable without host-model warnings." });
  const search = testPage.locator("[cmdk-input]");
  await search.fill(profile.name);
  await search.press("Enter");
  await expect(testPage.getByRole("listbox")).not.toBeVisible();
  await expect(selector).toContainText(profile.name);
});

Spec and public docs update

docs/specs/agents/requirements/no-silent-model-fallback.md

Requirement 003 now forbids host-model advisories in selectors and keeps editor and chat warnings.

Requirement 003
### REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003: Show model warnings at the point of action

**Intent:** Let users select a saved profile without treating a host discovery difference as evidence of an executor failure.

- **AC-003.1:** A host model-catalog difference shall not add an icon, message, tooltip, or help action to a profile selector. This applies to its options and its selected label, including missing models, one or several model variations, and empty or pending catalogs.
- **AC-003.2:** On desktop and mobile, an otherwise eligible profile shall remain selectable by keyboard or pointer. On touch devices, tapping its row shall select it without opening model help.
- **AC-003.3:** Existing authentication, installation, and capability-probe failure indicators shall remain visible. Their presence shall not depend on whether the saved model is advertised.
- **AC-003.4:** The profile editor shall retain its missing-model treatment, unique-variation advisory, and fallback controls.
- **AC-003.5:** When the executor applies the requested model successfully, a host catalog difference shall not cause a model-selection warning in task chat.
- **AC-003.6:** Selecting a profile shall not rewrite its saved model, reasoning configuration, or fallback settings.
Public docs
### Host probes and executor model catalogs

The model list shown while editing a profile comes from a host probe. It is an editing hint, not a launch gate. A profile remains selectable when its saved model is missing from that host list. Profile selectors do not show a model warning for this difference. Inspect the model list in profile settings for discovery details. Authentication, installation, and probe-failure indicators remain visible on profile selectors.

At task launch, the selected executor's ACP catalog is authoritative. For profiles without automatic fallback, Kandev follows the four-step order: exact model, explicit fallback, one unique bracketed variation, then agent default. Kandev stores one warning in task chat with the requested model and the effective model when known.

Risk

3 / 10 Low
1 low5 medium10 high

Why this score

  • No backend, schema, or migration change; only frontend selector rendering changes.
  • Unit tests cover eight catalog shapes and health states; E2E covers desktop and mobile selection and launch.
  • Rollback is a single revert; no persisted data changes and no new runtime warning path.

Trade-offs and review notes

Where to look first

  1. Verify useAgentProfileOptions no longer imports or calls advertisedModelIDs, findUniqueModelVariation, or ModelProbeWarning.
  2. Confirm both renderLabel and renderTriggerLabel use the same health-only label and that no button or amber icon remains.
  3. Check that all five locale files delete only the two selector keys and keep modelVariationAdvisory.
  4. Review profile-model-selection-helpers.ts for correct host-vs-executor precondition and zero-warning assertion.