fix: remove host model warnings from profile selectors (#3521)
This commit is contained in:
@@ -58,19 +58,6 @@ const AGENT_WITH_GPT: AvailableAgent = {
|
|||||||
},
|
},
|
||||||
} as unknown as AvailableAgent;
|
} as unknown as AvailableAgent;
|
||||||
|
|
||||||
const AGENT_WITH_OPUS_VARIATION: AvailableAgent = {
|
|
||||||
name: "omp-acp",
|
|
||||||
available: true,
|
|
||||||
model_config: {
|
|
||||||
default_model: "opus[1m]",
|
|
||||||
available_models: [{ id: "opus[1m]", name: "Opus (1m)" }],
|
|
||||||
current_model_id: "opus[1m]",
|
|
||||||
available_modes: [],
|
|
||||||
supports_dynamic_models: false,
|
|
||||||
status: "ok",
|
|
||||||
},
|
|
||||||
} as unknown as AvailableAgent;
|
|
||||||
|
|
||||||
const GONE_MODEL = "claude-gone";
|
const GONE_MODEL = "claude-gone";
|
||||||
const DATA_DISABLED = "data-disabled";
|
const DATA_DISABLED = "data-disabled";
|
||||||
const MODEL_PROBE_WARNING_TEST_ID = "agent-profile-model-probe-warning";
|
const MODEL_PROBE_WARNING_TEST_ID = "agent-profile-model-probe-warning";
|
||||||
@@ -118,14 +105,6 @@ function renderOptions(profiles: AgentProfileOption[]) {
|
|||||||
return screen.getByTestId("option-0");
|
return screen.getByTestId("option-0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModelProbeWarning() {
|
|
||||||
return screen.getByTestId(MODEL_PROBE_WARNING_TEST_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelProbeWarningLabel() {
|
|
||||||
return getModelProbeWarning().getAttribute("aria-label");
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
setAvailableAgents([AGENT_WITH_GPT]);
|
setAvailableAgents([AGENT_WITH_GPT]);
|
||||||
@@ -226,80 +205,122 @@ describe("useAgentProfileOptions recent-use ordering", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("useAgentProfileOptions executor-authoritative model hint", () => {
|
describe("useAgentProfileOptions model-independent labels", () => {
|
||||||
it("keeps a profile whose start model is absent from the host probe selectable", () => {
|
// @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.1
|
||||||
const option = renderOptions([profileOption({ model: GONE_MODEL })]);
|
it.each([
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
["exact", "gpt-5", ["gpt-5"]],
|
||||||
expect(getModelProbeWarningLabel()).toContain(GONE_MODEL);
|
["missing", GONE_MODEL, ["gpt-5"]],
|
||||||
});
|
["unique variation", "opus", ["opus[1m]"]],
|
||||||
|
["multiple variations", "opus", ["opus[1m]", "opus[270k]"]],
|
||||||
it("keeps a profile with an available start model selectable", () => {
|
["legacy effort IDs", "gpt-6-astra", ["gpt-6-astra[low]", "gpt-6-astra[high]"]],
|
||||||
const option = renderOptions([profileOption({ model: "gpt-5" })]);
|
["bracketed request", "opus[1m]", ["opus[270k]"]],
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
["empty catalog", GONE_MODEL, []],
|
||||||
});
|
["provider default", "", ["gpt-5"]],
|
||||||
|
])("does not show host model advisories in either label: %s", (_, model, models) => {
|
||||||
it("names a unique advertised variation without disabling the profile", () => {
|
setAvailableAgents([
|
||||||
setAvailableAgents([AGENT_WITH_OPUS_VARIATION]);
|
{
|
||||||
const option = renderOptions([profileOption({ model: "opus" })]);
|
...AGENT_WITH_GPT,
|
||||||
|
model_config: {
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
...AGENT_WITH_GPT.model_config,
|
||||||
expect(getModelProbeWarningLabel()).toContain("opus[1m]");
|
available_models: (models as string[]).map((id) => ({ id, name: id })),
|
||||||
});
|
},
|
||||||
|
},
|
||||||
it("keeps a profile with an empty (agent default) model selectable", () => {
|
|
||||||
const option = renderOptions([profileOption({ model: "" })]);
|
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps a gone-model profile with a fallback selectable", () => {
|
|
||||||
const option = renderOptions([profileOption({ model: GONE_MODEL, fallback_model: "gpt-5" })]);
|
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
|
||||||
expect(option.getAttribute("data-reason")).toBeNull();
|
|
||||||
|
|
||||||
expect(getModelProbeWarningLabel()).toContain(GONE_MODEL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps a profile selectable when both saved models are absent from the host probe", () => {
|
|
||||||
const option = renderOptions([
|
|
||||||
profileOption({ model: GONE_MODEL, fallback_model: "other-gone" }),
|
|
||||||
]);
|
]);
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
const profile = profileOption({ model: model as string });
|
||||||
expect(getModelProbeWarningLabel()).toContain(GONE_MODEL);
|
const { result } = renderHook(() => useAgentProfileOptions([profile]));
|
||||||
});
|
const option = result.current[0]!;
|
||||||
|
expect(option.disabled).toBeUndefined();
|
||||||
it("keeps auto-fallback profiles selectable and shows the host probe hint", () => {
|
expect(option.disabledReason).toBeUndefined();
|
||||||
const option = renderOptions([profileOption({ model: GONE_MODEL, auto_fallback: true })]);
|
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
|
||||||
expect(getModelProbeWarningLabel()).toContain(GONE_MODEL);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the host probe hint as one compact warning trigger", () => {
|
|
||||||
const option = renderOptions([profileOption({ model: GONE_MODEL })]);
|
|
||||||
|
|
||||||
expect(getModelProbeWarning()).toBeTruthy();
|
|
||||||
expect(option.textContent).not.toContain(
|
|
||||||
"The host probe did not advertise claude-gone. The selected executor will decide the model at launch.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses a non-interactive warning indicator in the selected trigger", () => {
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useAgentProfileOptions([profileOption({ model: GONE_MODEL })]),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div>{result.current[0]?.renderTriggerLabel?.()}</div>
|
<div data-testid="option-label">{option.renderLabel()}</div>
|
||||||
|
<div data-testid="selected-label">{option.renderTriggerLabel?.()}</div>
|
||||||
</TooltipProvider>,
|
</TooltipProvider>,
|
||||||
);
|
);
|
||||||
|
for (const label of ["option-label", "selected-label"]) {
|
||||||
expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
|
const element = screen.getByTestId(label);
|
||||||
expect(screen.getByTitle(/claude-gone/)).toBeTruthy();
|
expect(element.textContent).toContain("hybrid");
|
||||||
|
expect(element.querySelector("button, .tabler-icon-alert-triangle")).toBeNull();
|
||||||
|
expect(element.querySelector('[title*="host probe"]')).toBeNull();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not gate when the agent model list is unknown (probe not landed)", () => {
|
// @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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.6
|
||||||
|
it.each([false, true])(
|
||||||
|
"preserves saved profile settings with auto fallback %s",
|
||||||
|
(auto_fallback) => {
|
||||||
|
const profile = profileOption({
|
||||||
|
model: GONE_MODEL,
|
||||||
|
fallback_model: "other-gone",
|
||||||
|
auto_fallback,
|
||||||
|
cli_passthrough: true,
|
||||||
|
});
|
||||||
|
const saved = structuredClone(profile);
|
||||||
|
const option = renderOptions([profile]);
|
||||||
|
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
||||||
|
expect(option.getAttribute("data-reason")).toBeNull();
|
||||||
|
expect(screen.queryByTestId(MODEL_PROBE_WARNING_TEST_ID)).toBeNull();
|
||||||
|
expect(option.querySelector(".tabler-icon-terminal-2")).not.toBeNull();
|
||||||
|
expect(profile).toEqual(saved);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("keeps labels stable when a pending host catalog changes", () => {
|
||||||
setAvailableAgents([]);
|
setAvailableAgents([]);
|
||||||
const option = renderOptions([profileOption({ model: GONE_MODEL })]);
|
const profile = profileOption({ model: GONE_MODEL });
|
||||||
expect(option.getAttribute(DATA_DISABLED)).toBeNull();
|
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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
||||||
import { t } from "@/lib/i18n";
|
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";
|
import { Badge } from "@kandev/ui/badge";
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
} from "@kandev/ui/drawer";
|
|
||||||
import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow";
|
import { ScrollOnOverflow } from "@kandev/ui/scroll-on-overflow";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
|
||||||
import type {
|
import type {
|
||||||
LocalRepository,
|
LocalRepository,
|
||||||
Repository,
|
Repository,
|
||||||
@@ -23,17 +14,17 @@ import type {
|
|||||||
Executor,
|
Executor,
|
||||||
ExecutorProfile,
|
ExecutorProfile,
|
||||||
} from "@/lib/types/http";
|
} from "@/lib/types/http";
|
||||||
import type { AvailableAgent } from "@/lib/types/http-agents";
|
|
||||||
import type { AgentProfileOption } from "@/lib/state/slices";
|
import type { AgentProfileOption } from "@/lib/state/slices";
|
||||||
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
|
import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents";
|
||||||
import { useFeature } from "@/hooks/domains/features/use-feature";
|
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 { formatUserHomePath, truncateRepoPath } from "@/lib/utils";
|
||||||
import { getExecutorIcon } from "@/lib/executor-icons";
|
import { getExecutorIcon } from "@/lib/executor-icons";
|
||||||
import { AgentLogo } from "@/components/agent-logo";
|
import { AgentLogo } from "@/components/agent-logo";
|
||||||
import { getCapabilityWarning } from "@/lib/capability-warning";
|
import { getCapabilityWarning } from "@/lib/capability-warning";
|
||||||
import { findUniqueModelVariation } from "@/lib/model-variation";
|
|
||||||
import { useTouchDrawer } from "@/hooks/use-compact-task-chrome";
|
|
||||||
import { buildBranchKeywords } from "./branch-picker-options";
|
import { buildBranchKeywords } from "./branch-picker-options";
|
||||||
import {
|
import {
|
||||||
ensureAgentProfileRecentUseLoaded,
|
ensureAgentProfileRecentUseLoaded,
|
||||||
@@ -50,50 +41,6 @@ type OptionItem = {
|
|||||||
disabledReason?: string;
|
disabledReason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ModelProbeWarning({ note }: { note: string }) {
|
|
||||||
const usesTouchDrawer = useTouchDrawer();
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
||||||
const trigger = (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex min-h-11 min-w-8 shrink-0 cursor-help items-center justify-center rounded-sm border-0 bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
aria-label={note}
|
|
||||||
aria-expanded={usesTouchDrawer ? drawerOpen : undefined}
|
|
||||||
aria-haspopup={usesTouchDrawer ? "dialog" : undefined}
|
|
||||||
data-testid="agent-profile-model-probe-warning"
|
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
|
||||||
onClick={(event) => event.stopPropagation()}
|
|
||||||
>
|
|
||||||
<IconAlertTriangle className="size-3.5 text-amber-500" aria-hidden />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (usesTouchDrawer) {
|
|
||||||
return (
|
|
||||||
<Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerHeader>
|
|
||||||
<DrawerTitle className="sr-only">{note}</DrawerTitle>
|
|
||||||
<DrawerDescription>{note}</DrawerDescription>
|
|
||||||
</DrawerHeader>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">{note}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModelProbeWarningIndicator({ note }: { note: string }) {
|
|
||||||
return <IconAlertTriangle className="size-3.5 text-amber-500" title={note} aria-hidden />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRepositoryOptions(
|
export function useRepositoryOptions(
|
||||||
repositories: Repository[],
|
repositories: Repository[],
|
||||||
discoveredRepositories: LocalRepository[],
|
discoveredRepositories: LocalRepository[],
|
||||||
@@ -185,19 +132,15 @@ export function useBranchOptions(branchOptionsRaw: Branch[]) {
|
|||||||
}, [branchOptionsRaw]);
|
}, [branchOptionsRaw]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// advertisedModelIDs returns the currently advertised model IDs for an agent
|
|
||||||
// from the host-utility probe cache (empty when the probe has not landed).
|
|
||||||
function advertisedModelIDs(availableAgents: AvailableAgent[], agentName: string): string[] {
|
|
||||||
const agent = availableAgents.find((a) => a.name === agentName);
|
|
||||||
return agent?.model_config?.available_models?.map((m) => m.id) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAgentProfileOptions(
|
export function useAgentProfileOptions(
|
||||||
agentProfiles: AgentProfileOption[],
|
agentProfiles: AgentProfileOption[],
|
||||||
context?: AgentProfileRecentUseContext,
|
context?: AgentProfileRecentUseContext,
|
||||||
): OptionItem[] {
|
): OptionItem[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { items: availableAgents } = useAvailableAgents();
|
// 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 dynamicRoutingEnabled = useFeature("dynamicAgentRouting");
|
||||||
const storeApi = useAppStoreApi();
|
const storeApi = useAppStoreApi();
|
||||||
const recentUseLoaded = useAppStore((state) => !context || state.agentProfileRecentUse.loaded);
|
const recentUseLoaded = useAppStore((state) => !context || state.agentProfileRecentUse.loaded);
|
||||||
@@ -209,9 +152,13 @@ export function useAgentProfileOptions(
|
|||||||
void ensureAgentProfileRecentUseLoaded(storeApi);
|
void ensureAgentProfileRecentUseLoaded(storeApi);
|
||||||
}, [context, recentUseLoaded, storeApi]);
|
}, [context, recentUseLoaded, storeApi]);
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
const profilesWithCapabilities = refreshProfileCapabilities(
|
||||||
|
agentProfiles,
|
||||||
|
availableAgents.items,
|
||||||
|
);
|
||||||
// Disabled profiles stay in the store (existing sessions keep their
|
// Disabled profiles stay in the store (existing sessions keep their
|
||||||
// labels) but are never offered as a choice for new work.
|
// labels) but are never offered as a choice for new work.
|
||||||
const selectable = agentProfiles.filter((profile) =>
|
const selectable = profilesWithCapabilities.filter((profile) =>
|
||||||
isSelectableAgentProfile(profile, dynamicRoutingEnabled),
|
isSelectableAgentProfile(profile, dynamicRoutingEnabled),
|
||||||
);
|
);
|
||||||
const orderedProfiles = context
|
const orderedProfiles = context
|
||||||
@@ -223,26 +170,7 @@ export function useAgentProfileOptions(
|
|||||||
const profileLabel = parts[1] ?? "";
|
const profileLabel = parts[1] ?? "";
|
||||||
const isPassthrough = profile.cli_passthrough === true;
|
const isPassthrough = profile.cli_passthrough === true;
|
||||||
const warning = getCapabilityWarning(profile.capability_status, profile.capability_error);
|
const warning = getCapabilityWarning(profile.capability_status, profile.capability_error);
|
||||||
// The host-utility probe is an editing hint only. The selected
|
const renderProfileLabel = () => (
|
||||||
// executor owns the launch-time model catalog, so a host-only mismatch
|
|
||||||
// must never remove a profile from the task selector.
|
|
||||||
const advertised = advertisedModelIDs(availableAgents, profile.agent_name);
|
|
||||||
const startModelGone = Boolean(
|
|
||||||
profile.model && advertised.length > 0 && !advertised.includes(profile.model),
|
|
||||||
);
|
|
||||||
const uniqueVariation = startModelGone
|
|
||||||
? findUniqueModelVariation(profile.model ?? "", advertised)
|
|
||||||
: null;
|
|
||||||
let modelProbeNote: string | undefined;
|
|
||||||
if (startModelGone) {
|
|
||||||
modelProbeNote = uniqueVariation
|
|
||||||
? t("settings:profileStartModelUniqueVariationOnHost", {
|
|
||||||
model: profile.model,
|
|
||||||
variation: uniqueVariation,
|
|
||||||
})
|
|
||||||
: t("settings:profileStartModelNotAdvertisedOnHost", { model: profile.model });
|
|
||||||
}
|
|
||||||
const renderProfileLabel = (modelProbeWarning: React.ReactNode) => (
|
|
||||||
<span className="flex min-w-0 flex-1 flex-col gap-1">
|
<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 justify-between gap-2">
|
||||||
<span className="flex shrink-0 items-center gap-1.5">
|
<span className="flex shrink-0 items-center gap-1.5">
|
||||||
@@ -251,7 +179,6 @@ export function useAgentProfileOptions(
|
|||||||
{warning && (
|
{warning && (
|
||||||
<warning.Icon className={`size-3.5 ${warning.color}`} title={warning.title} />
|
<warning.Icon className={`size-3.5 ${warning.color}`} title={warning.title} />
|
||||||
)}
|
)}
|
||||||
{modelProbeWarning}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="flex shrink-0 items-center gap-1.5">
|
<span className="flex shrink-0 items-center gap-1.5">
|
||||||
{isPassthrough && (
|
{isPassthrough && (
|
||||||
@@ -274,15 +201,11 @@ export function useAgentProfileOptions(
|
|||||||
label: profile.label,
|
label: profile.label,
|
||||||
disabled: undefined,
|
disabled: undefined,
|
||||||
disabledReason: undefined,
|
disabledReason: undefined,
|
||||||
renderLabel: () =>
|
renderLabel: renderProfileLabel,
|
||||||
renderProfileLabel(modelProbeNote ? <ModelProbeWarning note={modelProbeNote} /> : null),
|
renderTriggerLabel: renderProfileLabel,
|
||||||
renderTriggerLabel: () =>
|
|
||||||
renderProfileLabel(
|
|
||||||
modelProbeNote ? <ModelProbeWarningIndicator note={modelProbeNote} /> : null,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [agentProfiles, availableAgents, context, dynamicRoutingEnabled, recentProfileIds, t]);
|
}, [agentProfiles, availableAgents.items, context, dynamicRoutingEnabled, recentProfileIds, t]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useExecutorOptions(executors: Executor[]): OptionItem[] {
|
export function useExecutorOptions(executors: Executor[]): OptionItem[] {
|
||||||
|
|||||||
@@ -43,3 +43,16 @@ export async function readModelSelectionWarnings(
|
|||||||
const { messages } = await apiClient.listSessionMessages(sessionId);
|
const { messages } = await apiClient.listSessionMessages(sessionId);
|
||||||
return messages.filter((message) => message.metadata?.kind === "model_selection_warning");
|
return messages.filter((message) => message.metadata?.kind === "model_selection_warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createExecutorOnlyModelProfile(apiClient: ApiClient): Promise<AgentProfile> {
|
||||||
|
const { agents } = await apiClient.listAgents();
|
||||||
|
const agent = agents.find((item) => item.name === "mock-agent");
|
||||||
|
if (!agent) throw new Error("The E2E fixture must provide a mock agent");
|
||||||
|
return apiClient.createAgentProfile(agent.id, "Opus High", {
|
||||||
|
model: AMBIGUOUS_MODEL_VARIATIONS[0],
|
||||||
|
config_options: { effort: "high" },
|
||||||
|
fallback_model: "mock-smart",
|
||||||
|
auto_fallback: false,
|
||||||
|
env_vars: [{ key: MODEL_CATALOG_ENV, value: "ambiguous" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ import { expect, test } from "../../fixtures/test-base";
|
|||||||
import { KanbanPage } from "../../pages/kanban-page";
|
import { KanbanPage } from "../../pages/kanban-page";
|
||||||
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
|
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
|
||||||
import {
|
import {
|
||||||
|
createExecutorOnlyModelProfile,
|
||||||
createModelVariationProfile,
|
createModelVariationProfile,
|
||||||
createMismatchedProfile,
|
createMismatchedProfile,
|
||||||
MODEL_VARIATION_BASE,
|
|
||||||
UNIQUE_MODEL_VARIATION,
|
|
||||||
UNADVERTISED_MODEL,
|
|
||||||
} from "../session/model-mismatch-warning-helpers";
|
} from "../session/model-mismatch-warning-helpers";
|
||||||
|
import { launchExecutorOnlyModelProfile } from "./profile-model-selection-helpers";
|
||||||
|
|
||||||
test.describe("executor-authoritative model selection on mobile", () => {
|
test.describe("executor-authoritative model selection on mobile", () => {
|
||||||
test("keeps the host-mismatched profile reachable by touch", async ({ testPage, apiClient }) => {
|
test("keeps the host-mismatched profile reachable by touch", async ({
|
||||||
|
testPage,
|
||||||
|
apiClient,
|
||||||
|
prCapture,
|
||||||
|
}) => {
|
||||||
const profile = await createMismatchedProfile(apiClient, "Mobile host mismatch profile");
|
const profile = await createMismatchedProfile(apiClient, "Mobile host mismatch profile");
|
||||||
try {
|
try {
|
||||||
const kanban = new KanbanPage(testPage);
|
const kanban = new KanbanPage(testPage);
|
||||||
@@ -29,23 +32,25 @@ test.describe("executor-authoritative model selection on mobile", () => {
|
|||||||
.getByRole("option", { name: profile.name, exact: false });
|
.getByRole("option", { name: profile.name, exact: false });
|
||||||
await expect(option).toBeVisible();
|
await expect(option).toBeVisible();
|
||||||
await expect(option).toBeEnabled();
|
await expect(option).toBeEnabled();
|
||||||
const warning = option.getByTestId("agent-profile-model-probe-warning");
|
await expect(option.getByTestId("agent-profile-model-probe-warning")).toHaveCount(0);
|
||||||
await expect(warning).toBeVisible();
|
await expect(option.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
const warningText = `The host probe did not advertise ${UNADVERTISED_MODEL}. The selected executor will decide the model at launch.`;
|
await prCapture.screenshot("mobile-profile-options", {
|
||||||
await expect(dialog).not.toContainText(warningText);
|
caption: "Saved profiles remain selectable without host-model warnings.",
|
||||||
await warning.tap();
|
});
|
||||||
await expect(
|
await option.tap();
|
||||||
testPage
|
await expect(testPage.getByRole("listbox")).not.toBeVisible();
|
||||||
.locator('[data-slot="drawer-content"][data-state="open"]')
|
await expect(selector).toContainText(profile.name);
|
||||||
.filter({ hasText: warningText }),
|
await prCapture.screenshot("mobile-selected-profile", {
|
||||||
).toBeVisible();
|
caption: "The selected profile keeps its name without a model advisory.",
|
||||||
await assertNoDocumentHorizontalOverflow(testPage, "mobile model mismatch selector");
|
});
|
||||||
|
await expect(selector.locator("button, .tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
|
await assertNoDocumentHorizontalOverflow(testPage, "mobile profile selection");
|
||||||
} finally {
|
} finally {
|
||||||
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opens the unique variation advisory by touch without overflow", async ({
|
test("selects a unique-variation profile by touch without model help", async ({
|
||||||
testPage,
|
testPage,
|
||||||
apiClient,
|
apiClient,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -70,18 +75,28 @@ test.describe("executor-authoritative model selection on mobile", () => {
|
|||||||
.getByRole("option", { name: profile.name, exact: false });
|
.getByRole("option", { name: profile.name, exact: false });
|
||||||
await expect(option).toBeVisible();
|
await expect(option).toBeVisible();
|
||||||
await expect(option).toBeEnabled();
|
await expect(option).toBeEnabled();
|
||||||
const warning = option.getByTestId("agent-profile-model-probe-warning");
|
await expect(option.getByTestId("agent-profile-model-probe-warning")).toHaveCount(0);
|
||||||
await expect(warning).toBeVisible();
|
await expect(option.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
const warningText = `The host probe found one possible variation of ${MODEL_VARIATION_BASE}: ${UNIQUE_MODEL_VARIATION}. The selected executor will decide the model at launch.`;
|
await option.tap();
|
||||||
await warning.tap();
|
await expect(testPage.getByRole("listbox")).not.toBeVisible();
|
||||||
await expect(
|
await expect(selector).toContainText(profile.name);
|
||||||
testPage
|
await expect(selector.locator("button, .tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
.locator('[data-slot="drawer-content"][data-state="open"]')
|
await assertNoDocumentHorizontalOverflow(testPage, "mobile profile selection");
|
||||||
.filter({ hasText: warningText }),
|
|
||||||
).toBeVisible();
|
|
||||||
await assertNoDocumentHorizontalOverflow(testPage, "mobile unique variation advisory");
|
|
||||||
} finally {
|
} finally {
|
||||||
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.5
|
||||||
|
// @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.6
|
||||||
|
test("launches a host-mismatched profile on its requested executor model without a warning", async ({
|
||||||
|
testPage,
|
||||||
|
apiClient,
|
||||||
|
}) => {
|
||||||
|
const profile = await createExecutorOnlyModelProfile(apiClient);
|
||||||
|
try {
|
||||||
|
await launchExecutorOnlyModelProfile(testPage, apiClient, profile, true);
|
||||||
|
} finally {
|
||||||
|
await apiClient.deleteAgentProfile(profile.id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { expect, test } from "../../fixtures/test-base";
|
import { expect, test } from "../../fixtures/test-base";
|
||||||
import { KanbanPage } from "../../pages/kanban-page";
|
import { KanbanPage } from "../../pages/kanban-page";
|
||||||
import {
|
import {
|
||||||
|
createExecutorOnlyModelProfile,
|
||||||
createModelVariationProfile,
|
createModelVariationProfile,
|
||||||
createMismatchedProfile,
|
createMismatchedProfile,
|
||||||
MODEL_VARIATION_BASE,
|
|
||||||
UNIQUE_MODEL_VARIATION,
|
|
||||||
UNADVERTISED_MODEL,
|
|
||||||
} from "../session/model-mismatch-warning-helpers";
|
} from "../session/model-mismatch-warning-helpers";
|
||||||
|
import { launchExecutorOnlyModelProfile } from "./profile-model-selection-helpers";
|
||||||
|
|
||||||
test.describe("executor-authoritative model selection", () => {
|
test.describe("executor-authoritative model selection", () => {
|
||||||
test("keeps a host-mismatched profile selectable", async ({ testPage, apiClient }) => {
|
test("keeps a host-mismatched profile selectable", async ({ testPage, apiClient, prCapture }) => {
|
||||||
const profile = await createMismatchedProfile(apiClient, "Host mismatch selectable profile");
|
const profile = await createMismatchedProfile(apiClient, "Host mismatch selectable profile");
|
||||||
try {
|
try {
|
||||||
const kanban = new KanbanPage(testPage);
|
const kanban = new KanbanPage(testPage);
|
||||||
@@ -28,24 +27,27 @@ test.describe("executor-authoritative model selection", () => {
|
|||||||
.getByRole("option", { name: profile.name, exact: false });
|
.getByRole("option", { name: profile.name, exact: false });
|
||||||
await expect(option).toBeVisible();
|
await expect(option).toBeVisible();
|
||||||
await expect(option).toBeEnabled();
|
await expect(option).toBeEnabled();
|
||||||
const warning = option.getByTestId("agent-profile-model-probe-warning");
|
await expect(option.getByTestId("agent-profile-model-probe-warning")).toHaveCount(0);
|
||||||
await expect(warning).toBeVisible();
|
await expect(option.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
const warningText = `The host probe did not advertise ${UNADVERTISED_MODEL}. The selected executor will decide the model at launch.`;
|
await prCapture.screenshot("desktop-profile-options", {
|
||||||
await expect(dialog).not.toContainText(warningText);
|
caption: "Saved profiles remain selectable without host-model warnings.",
|
||||||
await warning.hover();
|
});
|
||||||
await expect(
|
const search = testPage.locator("[cmdk-input]");
|
||||||
testPage
|
await search.fill(profile.name);
|
||||||
.locator('[data-slot="tooltip-content"]:not([data-state="closed"])')
|
await search.press("Enter");
|
||||||
.filter({ hasText: warningText }),
|
await expect(testPage.getByRole("listbox")).not.toBeVisible();
|
||||||
).toBeVisible();
|
await expect(selector).toContainText(profile.name);
|
||||||
await option.click();
|
await prCapture.screenshot("desktop-selected-profile", {
|
||||||
|
caption: "The selected profile keeps its name without a model advisory.",
|
||||||
|
});
|
||||||
|
await expect(selector.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
await expect(selector.locator("button")).toHaveCount(0);
|
await expect(selector.locator("button")).toHaveCount(0);
|
||||||
} finally {
|
} finally {
|
||||||
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("names one host-advertised variation while keeping the profile selectable", async ({
|
test("selects a unique-variation profile without a host model advisory", async ({
|
||||||
testPage,
|
testPage,
|
||||||
apiClient,
|
apiClient,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -70,18 +72,27 @@ test.describe("executor-authoritative model selection", () => {
|
|||||||
.getByRole("option", { name: profile.name, exact: false });
|
.getByRole("option", { name: profile.name, exact: false });
|
||||||
await expect(option).toBeVisible();
|
await expect(option).toBeVisible();
|
||||||
await expect(option).toBeEnabled();
|
await expect(option).toBeEnabled();
|
||||||
const warning = option.getByTestId("agent-profile-model-probe-warning");
|
await expect(option.getByTestId("agent-profile-model-probe-warning")).toHaveCount(0);
|
||||||
await expect(warning).toBeVisible();
|
await expect(option.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
const warningText = `The host probe found one possible variation of ${MODEL_VARIATION_BASE}: ${UNIQUE_MODEL_VARIATION}. The selected executor will decide the model at launch.`;
|
|
||||||
await warning.hover();
|
|
||||||
await expect(
|
|
||||||
testPage
|
|
||||||
.locator('[data-slot="tooltip-content"]:not([data-state="closed"])')
|
|
||||||
.filter({ hasText: warningText }),
|
|
||||||
).toBeVisible();
|
|
||||||
await option.click();
|
await option.click();
|
||||||
|
await expect(testPage.getByRole("listbox")).not.toBeVisible();
|
||||||
|
await expect(selector).toContainText(profile.name);
|
||||||
|
await expect(selector.locator(".tabler-icon-alert-triangle")).toHaveCount(0);
|
||||||
} finally {
|
} finally {
|
||||||
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
await apiClient.deleteAgentProfile(profile.id, true).catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.5
|
||||||
|
// @covers AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.6
|
||||||
|
test("launches a host-mismatched profile on its requested executor model without a warning", async ({
|
||||||
|
testPage,
|
||||||
|
apiClient,
|
||||||
|
}) => {
|
||||||
|
const profile = await createExecutorOnlyModelProfile(apiClient);
|
||||||
|
try {
|
||||||
|
await launchExecutorOnlyModelProfile(testPage, apiClient, profile, false);
|
||||||
|
} finally {
|
||||||
|
await apiClient.deleteAgentProfile(profile.id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
import type { AgentProfile } from "../../../lib/types/http-agents";
|
||||||
|
import type { ApiClient } from "../../helpers/api-client";
|
||||||
|
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
|
||||||
|
import { waitForSessionDone } from "../../helpers/session";
|
||||||
|
import { KanbanPage } from "../../pages/kanban-page";
|
||||||
|
import { SessionPage } from "../../pages/session-page";
|
||||||
|
import { readModelSelectionWarnings } from "../session/model-mismatch-warning-helpers";
|
||||||
|
|
||||||
|
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((agent) => agent.name === "mock-agent")!
|
||||||
|
.model_config.available_models.map((model) => model.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");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertRequestedModel(
|
||||||
|
page: Page,
|
||||||
|
apiClient: ApiClient,
|
||||||
|
taskId: string,
|
||||||
|
sessionId: string,
|
||||||
|
profile: AgentProfile,
|
||||||
|
) {
|
||||||
|
const { sessions } = await apiClient.listTaskSessions(taskId);
|
||||||
|
const current = sessions.find((session) => session.id === sessionId)!;
|
||||||
|
expect(current.metadata?.runtime_config).toMatchObject({
|
||||||
|
model: profile.model,
|
||||||
|
config_options: { effort: "high" },
|
||||||
|
});
|
||||||
|
expect(current.metadata?.acp_model_state).toMatchObject({
|
||||||
|
current_model_id: profile.model,
|
||||||
|
models: expect.arrayContaining([expect.objectContaining({ model_id: profile.model })]),
|
||||||
|
});
|
||||||
|
expect(await readModelSelectionWarnings(apiClient, sessionId)).toHaveLength(0);
|
||||||
|
const modelState = current.metadata?.acp_model_state as
|
||||||
|
| { models?: Array<{ model_id?: string; name?: string }> }
|
||||||
|
| undefined;
|
||||||
|
const requestedModelName = modelState?.models?.find(
|
||||||
|
(model) => model.model_id === profile.model,
|
||||||
|
)?.name;
|
||||||
|
expect(requestedModelName).toBeTruthy();
|
||||||
|
const modelTrigger = page.getByRole("button", { name: "Session model settings" });
|
||||||
|
await expect(modelTrigger).toContainText(requestedModelName!);
|
||||||
|
await expect(modelTrigger).toContainText("High");
|
||||||
|
const saved = await apiClient.getAgentProfile(profile.id);
|
||||||
|
expect(saved.model).toBe(profile.model);
|
||||||
|
expect(saved.config_options).toEqual(profile.config_options);
|
||||||
|
expect(saved.fallback_model).toBe(profile.fallback_model);
|
||||||
|
expect(saved.auto_fallback).toBe(profile.auto_fallback);
|
||||||
|
}
|
||||||
@@ -503,8 +503,6 @@
|
|||||||
"previewSound": "Preview sound",
|
"previewSound": "Preview sound",
|
||||||
"profileForTasksCreatedByAgents": "Profile for Tasks Created by Agents",
|
"profileForTasksCreatedByAgents": "Profile for Tasks Created by Agents",
|
||||||
"programming": "Programming",
|
"programming": "Programming",
|
||||||
"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.",
|
"modelVariationAdvisory": "The host catalog has one possible variation of {{model}}: {{variation}}. The saved model remains unchanged until launch.",
|
||||||
"promptAdd": "Add prompt",
|
"promptAdd": "Add prompt",
|
||||||
"promptEditorAriaLabel": "Prompt editor",
|
"promptEditorAriaLabel": "Prompt editor",
|
||||||
|
|||||||
@@ -503,8 +503,6 @@
|
|||||||
"previewSound": "Ƥŕēvĩēŵ śōũńď",
|
"previewSound": "Ƥŕēvĩēŵ śōũńď",
|
||||||
"profileForTasksCreatedByAgents": "Ƥŕōƒĩĺē ƒōŕ Ţàśķś Ćŕēàţēď ƀŷ Àĝēńţś",
|
"profileForTasksCreatedByAgents": "Ƥŕōƒĩĺē ƒōŕ Ţàśķś Ćŕēàţēď ƀŷ Àĝēńţś",
|
||||||
"programming": "Ƥŕōĝŕàḿḿĩńĝ",
|
"programming": "Ƥŕōĝŕàḿḿĩńĝ",
|
||||||
"profileStartModelNotAdvertisedOnHost": "Ţĥē ĥōśţ ƥŕōƀē ďĩď ńōţ àďvēŕţĩśē {{model}}. Ţĥē śēĺēćţēď ēxēćũţōŕ ŵĩĺĺ ďēćĩďē ţĥē ḿōďēĺ àţ ĺàũńćĥ.",
|
|
||||||
"profileStartModelUniqueVariationOnHost": "Ţĥē ĥōśţ ƥŕōƀē ƒōũńď ōńē ƥōśśĩƀĺē vàŕĩàţĩōń ōƒ {{model}}: {{variation}}. Ţĥē śēĺēćţēď ēxēćũţōŕ ŵĩĺĺ ďēćĩďē ţĥē ḿōďēĺ àţ ĺàũńćĥ.",
|
|
||||||
"modelVariationAdvisory": "Ţĥē ĥōśţ ćàţàĺōĝ ĥàś ōńē ƥōśśĩƀĺē vàŕĩàţĩōń ōƒ {{model}}: {{variation}}. Ţĥē śàvēď ḿōďēĺ ŕēḿàĩńś ũńćĥàńĝēď ũńţĩĺ ĺàũńćĥ.",
|
"modelVariationAdvisory": "Ţĥē ĥōśţ ćàţàĺōĝ ĥàś ōńē ƥōśśĩƀĺē vàŕĩàţĩōń ōƒ {{model}}: {{variation}}. Ţĥē śàvēď ḿōďēĺ ŕēḿàĩńś ũńćĥàńĝēď ũńţĩĺ ĺàũńćĥ.",
|
||||||
"promptAdd": "Àďď ƥŕōḿƥţ",
|
"promptAdd": "Àďď ƥŕōḿƥţ",
|
||||||
"promptEditorAriaLabel": "Ƥŕōḿƥţ ēďĩţōŕ",
|
"promptEditorAriaLabel": "Ƥŕōḿƥţ ēďĩţōŕ",
|
||||||
|
|||||||
@@ -750,8 +750,6 @@
|
|||||||
"taskBehavior": "Comportamento das tarefas",
|
"taskBehavior": "Comportamento das tarefas",
|
||||||
"terminalAndEditors": "Terminal e editores",
|
"terminalAndEditors": "Terminal e editores",
|
||||||
"workspacesAndAccess": "Workspaces e acesso",
|
"workspacesAndAccess": "Workspaces e acesso",
|
||||||
"profileStartModelNotAdvertisedOnHost": "A sondagem do anfitrião não anunciou {{model}}. O executor selecionado decidirá o modelo no arranque.",
|
|
||||||
"profileStartModelUniqueVariationOnHost": "A sondagem do anfitrião encontrou uma única variação possível de {{model}}: {{variation}}. O executor selecionado decidirá o modelo no arranque.",
|
|
||||||
"modelVariationAdvisory": "O catálogo do anfitrião tem uma única variação possível de {{model}}: {{variation}}. O modelo guardado permanece inalterado até ao arranque.",
|
"modelVariationAdvisory": "O catálogo do anfitrião tem uma única variação possível de {{model}}: {{variation}}. O modelo guardado permanece inalterado até ao arranque.",
|
||||||
"navUnits": "Unidades da organização",
|
"navUnits": "Unidades da organização",
|
||||||
"unitsTitle": "Unidades da organização",
|
"unitsTitle": "Unidades da organização",
|
||||||
|
|||||||
@@ -750,8 +750,6 @@
|
|||||||
"keyValueAddItem": "添加条目",
|
"keyValueAddItem": "添加条目",
|
||||||
"keyValueKeyPlaceholder": "键",
|
"keyValueKeyPlaceholder": "键",
|
||||||
"keyValueValuePlaceholder": "值",
|
"keyValueValuePlaceholder": "值",
|
||||||
"profileStartModelNotAdvertisedOnHost": "主机探测未公布 {{model}}。所选执行器将在启动时决定使用的模型。",
|
|
||||||
"profileStartModelUniqueVariationOnHost": "主机探测发现 {{model}} 的一个可能变体:{{variation}}。所选执行器将在启动时决定使用的模型。",
|
|
||||||
"modelVariationAdvisory": "主机目录中有一个可能的 {{model}} 变体:{{variation}}。保存的模型在启动前保持不变。",
|
"modelVariationAdvisory": "主机目录中有一个可能的 {{model}} 变体:{{variation}}。保存的模型在启动前保持不变。",
|
||||||
"navUnits": "组织单元",
|
"navUnits": "组织单元",
|
||||||
"unitsTitle": "组织单元",
|
"unitsTitle": "组织单元",
|
||||||
|
|||||||
@@ -750,8 +750,6 @@
|
|||||||
"keyValueAddItem": "新增條目",
|
"keyValueAddItem": "新增條目",
|
||||||
"keyValueKeyPlaceholder": "鍵",
|
"keyValueKeyPlaceholder": "鍵",
|
||||||
"keyValueValuePlaceholder": "值",
|
"keyValueValuePlaceholder": "值",
|
||||||
"profileStartModelNotAdvertisedOnHost": "主機探測未公佈 {{model}}。所選執行器將在啓動時決定使用的模型。",
|
|
||||||
"profileStartModelUniqueVariationOnHost": "主機探測發現 {{model}} 的一個可能變體:{{variation}}。所選執行器將在啓動時決定使用的模型。",
|
|
||||||
"modelVariationAdvisory": "主機目錄中有一個可能的 {{model}} 變體:{{variation}}。已儲存的模型在啟動前保持不變。",
|
"modelVariationAdvisory": "主機目錄中有一個可能的 {{model}} 變體:{{variation}}。已儲存的模型在啟動前保持不變。",
|
||||||
"navUnits": "組織單元",
|
"navUnits": "組織單元",
|
||||||
"unitsTitle": "組織單元",
|
"unitsTitle": "組織單元",
|
||||||
|
|||||||
@@ -750,8 +750,6 @@
|
|||||||
"keyValueAddItem": "新增條目",
|
"keyValueAddItem": "新增條目",
|
||||||
"keyValueKeyPlaceholder": "鍵",
|
"keyValueKeyPlaceholder": "鍵",
|
||||||
"keyValueValuePlaceholder": "值",
|
"keyValueValuePlaceholder": "值",
|
||||||
"profileStartModelNotAdvertisedOnHost": "主機探測未公佈 {{model}}。所選執行器將在啟動時決定使用的模型。",
|
|
||||||
"profileStartModelUniqueVariationOnHost": "主機探測發現 {{model}} 的一個可能變體:{{variation}}。所選執行器將在啟動時決定使用的模型。",
|
|
||||||
"modelVariationAdvisory": "主機目錄中有一個可能的 {{model}} 變體:{{variation}}。已儲存的模型在啟動前保持不變。",
|
"modelVariationAdvisory": "主機目錄中有一個可能的 {{model}} 變體:{{variation}}。已儲存的模型在啟動前保持不變。",
|
||||||
"navUnits": "組織單元",
|
"navUnits": "組織單元",
|
||||||
"unitsTitle": "組織單元",
|
"unitsTitle": "組織單元",
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
created: 2026-09-08
|
||||||
|
status: completed
|
||||||
|
requirements:
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003
|
||||||
|
system_design:
|
||||||
|
- ../../specs/agents/system-design/no-silent-model-fallback-01.md
|
||||||
|
- ../../specs/agents/system-design/no-silent-model-fallback-02.md
|
||||||
|
legacy_specs: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implementation Plan: Profile selector model warnings
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Remove host model advisories from saved-profile selectors. Keep profile editing
|
||||||
|
diagnostics and actual executor model-selection warnings available where users
|
||||||
|
can act on them.
|
||||||
|
|
||||||
|
The agent system owns this change because it owns profile model intent and
|
||||||
|
executor model authority. Amend the existing
|
||||||
|
[requirements](../../specs/agents/requirements/no-silent-model-fallback.md);
|
||||||
|
do not create a separate UI specification.
|
||||||
|
|
||||||
|
One work order delivers the shared selector change with unit, desktop, mobile,
|
||||||
|
and runtime-warning regression coverage. The user authorized implementation,
|
||||||
|
PR creation, a 15-minute wait, and PR fixup through `@implement`.
|
||||||
|
Deployment remains outside this package.
|
||||||
|
|
||||||
|
## Evidence and classification
|
||||||
|
|
||||||
|
This is a change to intended product behavior. The former system design
|
||||||
|
explicitly required the amber selector icon; the implementation follows that
|
||||||
|
rule. Requirement 002.7 now locates the host advisory in the profile editor.
|
||||||
|
Requirement 003 defines selector behavior and preservation checks.
|
||||||
|
|
||||||
|
Read-only investigation on 2026-09-08 established:
|
||||||
|
|
||||||
|
- The live instance used backend port 38429 and database
|
||||||
|
`/root/.kandev/data/kandev.db`.
|
||||||
|
- Profile `4fe051a9-f5cc-422b-bd0c-05048c22699e`, named Astra High, stored
|
||||||
|
`gpt-6-astra` and `settings.config_options.reasoning_effort = high`.
|
||||||
|
- Session `cfac5c57-f9a8-4b89-bc4c-2db8a0d6e20f` persisted that model and
|
||||||
|
effort in its settled runtime configuration.
|
||||||
|
- At 22:29:53 +01:00, logs reported that start-model policy applied Astra and
|
||||||
|
published `current_model_id = gpt-6-astra`.
|
||||||
|
- A fresh `acpdbg probe --timeout 45s codex-acp` completed with exit code 0.
|
||||||
|
Codex ACP 1.10.0 exposed 31 legacy model/effort IDs and six canonical model
|
||||||
|
configuration values. Both representations included Astra.
|
||||||
|
- Both live capability and available-agent APIs contained the canonical Astra
|
||||||
|
ID at inspection time.
|
||||||
|
|
||||||
|
The confirmed warning mechanism is the exact membership check in
|
||||||
|
`useAgentProfileOptions`: a nonempty browser host catalog that omits the
|
||||||
|
saved ID produces an amber advisory in both render paths. It never checks the
|
||||||
|
eventual executor outcome. A host catalog with several bracketed variants also
|
||||||
|
takes this path because no single variation matches.
|
||||||
|
|
||||||
|
The browser catalog at screenshot time was not captured. Staleness and legacy
|
||||||
|
ID representation are possible triggers, not established cache defects. The
|
||||||
|
design does not depend on choosing between those hypotheses.
|
||||||
|
|
||||||
|
The temporary probe capture was
|
||||||
|
`/tmp/kandev-model-warning-probe-OL5GGn/codex-acp-probe-20260908-213045.jsonl`.
|
||||||
|
It is optional local evidence, not a dependency for implementation. Do not
|
||||||
|
commit raw protocol frames or copy the live database into test fixtures.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
- Remove host model advisory icons, help triggers, and selected-label indicators.
|
||||||
|
- Apply the result through the existing shared profile-option hook.
|
||||||
|
- Preserve capability health indicators, eligibility, recent-use ordering,
|
||||||
|
profile labels, saved configuration, and passthrough indicators.
|
||||||
|
- Preserve profile-editor model diagnostics and persisted task warnings.
|
||||||
|
- Remove unused selector translations and explain warning placement in public
|
||||||
|
agent-profile documentation when the implementation ships.
|
||||||
|
- Cover desktop keyboard selection, mobile row taps, successful model launch,
|
||||||
|
actual fallback, and reload.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Host probe refresh, browser cache invalidation, or new polling.
|
||||||
|
- ACP normalization changes or provider-specific bracket parsing.
|
||||||
|
- Model/fallback policy, runtime warning metadata, database schema, or migrations.
|
||||||
|
- Runtime model-selector redesign, profile-editor redesign, Office routing,
|
||||||
|
and unrelated capability-warning accessibility changes.
|
||||||
|
- New flags, telemetry, screenshots for public docs, or provider network calls.
|
||||||
|
|
||||||
|
## Technical approach
|
||||||
|
|
||||||
|
In `apps/web/components/task-create-dialog-options.tsx`, remove the
|
||||||
|
model-catalog comparison and the two model-warning renderers. Keep the
|
||||||
|
capability hook active, and use its snapshot only to refresh health fields on
|
||||||
|
profile options. Keep the existing option interface and shared label rendering
|
||||||
|
compatible with consumers.
|
||||||
|
|
||||||
|
The shared hook serves these existing callers:
|
||||||
|
|
||||||
|
- `components/task-create-dialog-computed.ts`
|
||||||
|
- `components/task/new-subtask-dialog.tsx`
|
||||||
|
- `components/task/new-session-dialog.tsx`
|
||||||
|
- `components/quick-chat/quick-chat-setup.tsx`
|
||||||
|
- `components/automations/config-section.tsx`
|
||||||
|
- `app/office/setup/agent-profile-setup-controls.tsx`
|
||||||
|
|
||||||
|
These callers require no separate presentation branches. In particular,
|
||||||
|
Office keeps its surrounding compatibility filtering.
|
||||||
|
|
||||||
|
Do not remove `getCapabilityWarning`, model fields from profile state, or the
|
||||||
|
editor's `findUniqueModelVariation` helper. Remove only the two unused
|
||||||
|
`settings` selector keys from every locale, including pseudo. Keep
|
||||||
|
`modelVariationAdvisory` and runtime-warning translations.
|
||||||
|
|
||||||
|
The profile editor already prefers structured model configuration options in
|
||||||
|
`components/settings/profile-model-fields.tsx`. Preserve that canonical source;
|
||||||
|
this task does not introduce another model-ID resolver.
|
||||||
|
|
||||||
|
Public documentation is owned by the same work order: add a short explanation
|
||||||
|
under “Host probes and executor model catalogs” in
|
||||||
|
`docs/public/agents-and-profiles.md`. This is an explanation section within
|
||||||
|
the existing agent-profile guide.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
The following regression scenarios define the acceptance coverage.
|
||||||
|
|
||||||
|
| Acceptance | Test file and scenario |
|
||||||
|
| --- | --- |
|
||||||
|
| 003.1 | `components/task-create-dialog-options.test.tsx`: “does not show host model advisories in option or selected labels”, parameterized over exact, missing, unique, multiple, bracketed, empty, pending, and changed host catalogs. |
|
||||||
|
| 003.2, 003.6 | Same file: preserve eligibility, ordering, profile label and saved values; retain existing disabled-profile and recent-use cases. |
|
||||||
|
| 003.3 | Same file: “preserves capability health warnings independently of model membership”, for auth_required, not_installed, failed, and healthy status. |
|
||||||
|
| 003.4, 002.7 | `components/settings/profile-form-fields.test.tsx` and `lib/model-variation.test.ts`: retain missing-model, unique-variation, and fallback-control coverage. |
|
||||||
|
| 003.5, 001.4 | `components/task/chat/messages/status-message.test.tsx`: retain structured model-selection warning rendering. |
|
||||||
|
| 001.5, 003.6 | Browser reload checks and saved-profile API comparison in the successful-launch scenario below. |
|
||||||
|
|
||||||
|
The first red test must render a healthy-capability profile with a missing host
|
||||||
|
model and assert no advisory in either label. It fails on the current
|
||||||
|
implementation because the dropdown renders a warning button and the selected
|
||||||
|
label renders a titled icon. Absence of the old test ID alone is insufficient.
|
||||||
|
|
||||||
|
## E2E tests
|
||||||
|
|
||||||
|
| Project / file under `apps/web/e2e/tests` | Outcome and acceptance |
|
||||||
|
| --- | --- |
|
||||||
|
| chromium / `settings/no-silent-model-fallback.spec.ts` | Replace tooltip assertions with warning-free missing/unique profile selection. Check option and selected label, keyboard selection, and no nested warning control. Covers 003.1–003.2. |
|
||||||
|
| mobile-chrome / `settings/mobile-no-silent-model-fallback.spec.ts` | Replace warning-drawer taps with row selection. Verify selection closes the picker, updates its label, and leaves the create flow usable without horizontal overflow. Covers 003.1–003.2. |
|
||||||
|
| Both settings files above | Add “launches a host-mismatched profile on its requested executor model without a warning”. Submit through the UI, verify effective model and zero model-selection warnings after completion and reload, then compare saved profile configuration. Covers 003.5–003.6 and 001.5. |
|
||||||
|
| chromium / `session/model-mismatch-warning.spec.ts` | Preserve actual fallback, unique-variation, ambiguous-catalog, single-warning, and reload coverage. Covers 001.4 and the preservation side of 003.5. |
|
||||||
|
| mobile-chrome / `session/mobile-model-mismatch-warning.spec.ts` | Preserve visible actual fallback warning and reload without horizontal overflow. Covers 001.4 and 003.5. |
|
||||||
|
|
||||||
|
For the successful-launch fixture, extend
|
||||||
|
`session/model-mismatch-warning-helpers.ts` with a disposable mock profile
|
||||||
|
whose exact model is `opus[270k]` and whose profile environment selects the
|
||||||
|
existing `MOCK_AGENT_MODEL_CATALOG=ambiguous` fixture. The default E2E host
|
||||||
|
catalog includes `opus[1m]`, so it cannot serve as the omitted model. Confirm that the host
|
||||||
|
catalog omits that ID while the executor advertises it. Assert those
|
||||||
|
preconditions so an ordinary exact-match launch cannot pass as the regression.
|
||||||
|
Use existing model options for valid saved configuration values; no live Codex
|
||||||
|
account is needed.
|
||||||
|
|
||||||
|
## Work orders
|
||||||
|
|
||||||
|
- [x] [Task 01: Remove host model advisories from profile selectors](task-01-remove-host-model-advisories.md) — done, sequential, no dependencies.
|
||||||
|
|
||||||
|
## Verification results
|
||||||
|
|
||||||
|
Planning checks on 2026-09-08:
|
||||||
|
|
||||||
|
- Specification-linter tests: 30 passed.
|
||||||
|
- Full specification lint: passed after reducing the amended design to its size limit.
|
||||||
|
- Whitespace check: passed.
|
||||||
|
|
||||||
|
Implementation checks on 2026-09-08:
|
||||||
|
|
||||||
|
- Red: 11 component assertions and two selector cases per browser project failed
|
||||||
|
on the former model-warning controls.
|
||||||
|
- Green: 56 targeted unit tests, six desktop browser tests, and four mobile
|
||||||
|
browser tests passed.
|
||||||
|
- Typecheck, targeted ESLint, localization checks, and the localization ratchet passed.
|
||||||
|
- Public-documentation validator: 61 tests passed and 46 pages validated.
|
||||||
|
- Desktop and mobile screenshots show selectable profiles without a model
|
||||||
|
advisory or an empty warning control.
|
||||||
|
|
||||||
|
Task 01 records the commands, fixture correction, and mobile interaction detail.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Removing all warning icons would hide agent health errors; tests must distinguish
|
||||||
|
those indicators from model advisories.
|
||||||
|
- Removing only the dropdown button leaves the selected-label warning intact.
|
||||||
|
- Capability polling is shared with the selector hook. Keep it independent from
|
||||||
|
model-ID matching so health status remains current in selector-only surfaces.
|
||||||
|
- Old mobile tests deliberately open the warning drawer. Replace those steps
|
||||||
|
with completed row selection rather than deleting the tests.
|
||||||
|
- The saved screenshot does not establish a stale-cache defect. Do not expand
|
||||||
|
implementation into speculative discovery changes.
|
||||||
|
- Runtime unique-variation behavior is already covered by current code and
|
||||||
|
requirements, although its ADR remains proposed. This package does not
|
||||||
|
change that policy or its decision status.
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
---
|
||||||
|
id: "01-remove-host-model-advisories"
|
||||||
|
title: "Remove host model advisories from profile selectors"
|
||||||
|
status: done
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
plan: "plan.md"
|
||||||
|
requirements:
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003
|
||||||
|
acceptance_criteria:
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-001.4
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-001.5
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-002.7
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.1
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.2
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.3
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.4
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.5
|
||||||
|
- AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.6
|
||||||
|
system_design:
|
||||||
|
- ../../specs/agents/system-design/no-silent-model-fallback-01.md
|
||||||
|
- ../../specs/agents/system-design/no-silent-model-fallback-02.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 01: Remove host model advisories from profile selectors
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Remove the host-catalog warning from profile options and their selected labels.
|
||||||
|
Keep agent health indicators, editor diagnostics, and actual model-selection
|
||||||
|
warnings. Deliver the change through the shared hook with focused regression
|
||||||
|
tests and updated public guidance.
|
||||||
|
|
||||||
|
## In scope
|
||||||
|
|
||||||
|
- Own the shared hook, associated component tests, selector-copy cleanup, and
|
||||||
|
existing desktop/mobile model-selection E2E files.
|
||||||
|
- Add a successful exact-model launch with differing host and executor catalogs,
|
||||||
|
using disposable mock profiles and existing catalog fixtures.
|
||||||
|
- Keep the existing actual-fallback browser tests as preservation checks.
|
||||||
|
- Update the agent-profile guide's explanation of warning placement.
|
||||||
|
- Update this work order and the plan's results after the exact checks pass.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Production Go changes, ACP normalization, discovery cache/polling, schema,
|
||||||
|
migrations, model-selection policy, or runtime warning metadata.
|
||||||
|
- Caller-specific warning flags, new overlays, new settings, or profile rewrites.
|
||||||
|
- Broad verification and repository-wide review. The user's later `@implement`
|
||||||
|
request separately authorized commit, push, PR creation, and PR fixup.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
1. Both label render paths pass the plan's host-catalog matrix without model
|
||||||
|
advisories. Existing health indicators, eligibility, ordering, and saved
|
||||||
|
profile configuration remain intact.
|
||||||
|
2. Desktop keyboard and mobile touch flows select and launch the requested
|
||||||
|
profile. A successful exact-model launch has no model-selection warning;
|
||||||
|
actual fallback still displays one persisted warning after reload.
|
||||||
|
3. Unused selector translations are removed consistently. The profile editor
|
||||||
|
keeps its advisories and the public guide explains the shipped behavior.
|
||||||
|
|
||||||
|
## Implementation sequence
|
||||||
|
|
||||||
|
1. Read the linked requirements and design section 8, scoped web guidance,
|
||||||
|
existing source, and test helpers. Mark this work order in progress only
|
||||||
|
after the user requests implementation.
|
||||||
|
2. Run the new component absence assertions against the current implementation
|
||||||
|
and record their expected failure. Cover both render paths, not only the old
|
||||||
|
warning-button test ID.
|
||||||
|
3. Update the desktop/mobile selector scenarios for the intended behavior and
|
||||||
|
run the focused files against the current production build to record red.
|
||||||
|
Keep actual-fallback tests intact.
|
||||||
|
4. Remove the host model comparison, warning renderers, and exclusive imports.
|
||||||
|
Preserve the hook interface, health warning renderer, and caller data loading.
|
||||||
|
5. Complete the successful executor-model fixture, saved-value checks, and
|
||||||
|
localization cleanup. Update public guidance and run the commands below.
|
||||||
|
6. Mark done only after targeted checks pass. Record outcomes and any remaining
|
||||||
|
limitations in this file and the manifest.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run from the repository root. Install once in a fresh worktree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk pnpm --dir apps install --frozen-lockfile
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the first command for red and green component checks. The listed preservation
|
||||||
|
suites cover editor and chat behavior without a broad audit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk pnpm --dir apps/web exec vitest run components/task-create-dialog-options.test.tsx components/settings/profile-form-fields.test.tsx lib/model-variation.test.ts components/task/chat/messages/status-message.test.tsx
|
||||||
|
rtk pnpm --dir apps/web exec eslint components/task-create-dialog-options.tsx components/task-create-dialog-options.test.tsx
|
||||||
|
rtk pnpm --dir apps/web run typecheck
|
||||||
|
rtk pnpm --dir apps/web run i18n:check
|
||||||
|
rtk pnpm --dir apps/web run i18n:ratchet
|
||||||
|
```
|
||||||
|
|
||||||
|
Run projects sequentially. The managed runner builds production assets and
|
||||||
|
isolates its instance. Check discovery counts; zero tests is not a pass.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk pnpm --dir apps/web e2e:run --project chromium tests/settings/no-silent-model-fallback.spec.ts tests/session/model-mismatch-warning.spec.ts
|
||||||
|
rtk pnpm --dir apps/web e2e:run --project mobile-chrome tests/settings/mobile-no-silent-model-fallback.spec.ts tests/session/mobile-model-mismatch-warning.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect the rendered mobile selection state or its test screenshot for row
|
||||||
|
spacing, viewport containment, and the absence of an empty warning hit area.
|
||||||
|
Tests must verify selection completion and no document horizontal overflow.
|
||||||
|
|
||||||
|
After public-documentation changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk node --test scripts/validate-public-docs.test.mjs
|
||||||
|
rtk node scripts/validate-public-docs.mjs
|
||||||
|
rtk git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Audit removed controls and keys:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rtk rg -n 'agent-profile-model-probe-warning|profileStartModelNotAdvertisedOnHost|profileStartModelUniqueVariationOnHost' apps/web
|
||||||
|
```
|
||||||
|
|
||||||
|
Only intentional negative assertions may retain the removed control identity.
|
||||||
|
Production references to the two translation keys must be absent. Keep
|
||||||
|
`modelVariationAdvisory` and existing runtime-warning keys.
|
||||||
|
|
||||||
|
## Files likely touched
|
||||||
|
|
||||||
|
- `apps/web/components/task-create-dialog-options.tsx`
|
||||||
|
- `apps/web/components/task-create-dialog-options.test.tsx`
|
||||||
|
- `apps/web/e2e/tests/settings/no-silent-model-fallback.spec.ts`
|
||||||
|
- `apps/web/e2e/tests/settings/mobile-no-silent-model-fallback.spec.ts`
|
||||||
|
- `apps/web/e2e/tests/settings/profile-model-selection-helpers.ts`
|
||||||
|
- `apps/web/e2e/tests/session/model-mismatch-warning-helpers.ts`
|
||||||
|
- `apps/web/src/locales/{en,pseudo,pt-pt,zh-cn,zh-hk,zh-tw}/settings.json`
|
||||||
|
- `docs/public/agents-and-profiles.md`
|
||||||
|
|
||||||
|
Preservation inputs, normally unchanged:
|
||||||
|
|
||||||
|
- `apps/web/lib/capability-warning.ts`
|
||||||
|
- `apps/web/components/settings/profile-model-fields.tsx`
|
||||||
|
- `apps/web/components/settings/profile-form-fields.test.tsx`
|
||||||
|
- `apps/web/lib/model-variation.ts` and its test file
|
||||||
|
- `apps/web/components/task/chat/messages/status-message.test.tsx`
|
||||||
|
- `apps/web/e2e/tests/session/model-mismatch-warning.spec.ts`
|
||||||
|
- `apps/web/e2e/tests/session/mobile-model-mismatch-warning.spec.ts`
|
||||||
|
- The six shared-hook consumers listed in the manifest
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
The selected-label path can retain a warning after the option path is fixed.
|
||||||
|
Blanket icon removal can hide capability errors. Fixture setup must prove
|
||||||
|
different host and executor catalogs, and cleanup must delete only the
|
||||||
|
disposable profiles created by the tests.
|
||||||
|
|
||||||
|
Keep capability polling active through the shared selector hook. Apply its
|
||||||
|
snapshot to health fields only. Do not reintroduce model-ID matching.
|
||||||
|
|
||||||
|
## Parallelism
|
||||||
|
|
||||||
|
`sequential`. No subagents are authorized.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- [Requirements](../../specs/agents/requirements/no-silent-model-fallback.md):
|
||||||
|
001.4–001.5, 002.7, and 003.
|
||||||
|
- [Design part 1](../../specs/agents/system-design/no-silent-model-fallback-01.md):
|
||||||
|
section 8, runtime warning contract, and tests.
|
||||||
|
- [Design part 2](../../specs/agents/system-design/no-silent-model-fallback-02.md):
|
||||||
|
warning-removal and evidence risks.
|
||||||
|
- [Executor authority ADR](../../decisions/2026-08-15-executor-authoritative-model-selection.md).
|
||||||
|
- [Variation resolution ADR](../../decisions/2026-09-07-unique-model-variation-resolution.md).
|
||||||
|
- Existing option/selected-label renderers and their component harness.
|
||||||
|
- Existing model-mismatch helpers and mock catalog environment fixture.
|
||||||
|
- Mobile exemplar and composition rationale in design section 8.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
Completed on 2026-09-08.
|
||||||
|
|
||||||
|
- Removed host-catalog membership checks and both model-warning render paths
|
||||||
|
from the shared hook. Kept capability polling active and applied its snapshot
|
||||||
|
to health fields only. Eligibility and recent-use behavior remain unchanged.
|
||||||
|
- Removed the two selector-only keys from all six locale catalogs. Editor
|
||||||
|
diagnostics and runtime-warning code remain unchanged.
|
||||||
|
- The red component run failed 11 of 23 assertions. The old production build
|
||||||
|
failed both selector cases in each browser project on the warning controls.
|
||||||
|
- The four targeted unit suites passed all 56 tests.
|
||||||
|
- The managed desktop run passed six tests with `--host --project chromium`.
|
||||||
|
The mobile run passed four tests with `--host --no-build --project mobile-chrome`.
|
||||||
|
Both used the exact file lists in Verification. The mobile run reused the
|
||||||
|
unchanged production assets from the desktop run.
|
||||||
|
- The successful-launch fixture proves that the host omits `opus[270k]` and the
|
||||||
|
executor advertises it. It verifies the model, effort, zero warnings, and
|
||||||
|
unchanged saved profile after completion and reload.
|
||||||
|
- The first mobile launch run failed because it assumed desktop navigation.
|
||||||
|
The corrected test taps the created task card before it checks the session.
|
||||||
|
- Targeted ESLint passed for the component, its test, and all four changed or
|
||||||
|
new E2E files. Typecheck, `i18n:check`, and `i18n:ratchet` passed.
|
||||||
|
- Public-documentation validation passed: 61 validator tests and 46 pages.
|
||||||
|
- Fresh desktop/mobile screenshots show both the option list and selected label.
|
||||||
|
The mobile screenshots show usable row spacing with no empty warning control.
|
||||||
|
|
||||||
|
### PR fixup remediation
|
||||||
|
|
||||||
|
- Greptile's P1 finding was valid: the shared selector hook also kept capability
|
||||||
|
revalidation alive for selector-only surfaces. The hook now calls
|
||||||
|
`useAvailableAgents` and applies `refreshProfileCapabilities` to the local
|
||||||
|
option input. The host catalog supplies health status only. It cannot remove,
|
||||||
|
disable, or rewrite a profile because of a model ID.
|
||||||
|
- The remediation reran the focused desktop project (six passed) and mobile
|
||||||
|
project (four passed) after rebuilding the desktop production assets.
|
||||||
|
- The E2E launch helper now derives the expected model display name from the
|
||||||
|
executor's settled ACP model state. It no longer couples the assertion to a
|
||||||
|
fixture display string.
|
||||||
|
- Claude's trigger-label suggestion was already satisfied by the parameterized
|
||||||
|
test loop, which checks positive profile text and warning absence in both
|
||||||
|
render paths.
|
||||||
|
|
||||||
|
No backend changes, schema changes, live-instance restarts, or profile rewrites
|
||||||
|
were required. The screenshot-time browser catalog remains unknown.
|
||||||
@@ -258,7 +258,10 @@ stale browser action does not replace a newer route decision.
|
|||||||
|
|
||||||
The model list shown while editing a profile comes from a host probe. It is an
|
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
|
editing hint, not a launch gate. A profile remains selectable when its saved
|
||||||
model is missing from that host list.
|
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
|
At task launch, the selected executor's ACP catalog is authoritative. For
|
||||||
profiles without automatic fallback, Kandev follows the four-step order above.
|
profiles without automatic fallback, Kandev follows the four-step order above.
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ surface shared by task and Office consumers.
|
|||||||
- [Agent MCP Timeout Budgets](requirements/mcp-timeout-budgets.md)
|
- [Agent MCP Timeout Budgets](requirements/mcp-timeout-budgets.md)
|
||||||
- [Mock-agent slow command duration syntax](requirements/mock-agent-slow-duration.md)
|
- [Mock-agent slow command duration syntax](requirements/mock-agent-slow-duration.md)
|
||||||
- [Native Code Review](requirements/native-code-review.md)
|
- [Native Code Review](requirements/native-code-review.md)
|
||||||
- [No Silent Model Fallback](requirements/no-silent-model-fallback.md)
|
- [No Silent Model Fallback](requirements/no-silent-model-fallback.md), including
|
||||||
|
profile-selector warning placement and executor-authoritative task warnings.
|
||||||
- [Copy agent configuration to isolated executors](requirements/portable-agent-configuration.md)
|
- [Copy agent configuration to isolated executors](requirements/portable-agent-configuration.md)
|
||||||
- [Disable an Agent Profile](requirements/profile-disable.md)
|
- [Disable an Agent Profile](requirements/profile-disable.md)
|
||||||
- [Agent Profile Recent Use](requirements/profile-recent-use.md)
|
- [Agent Profile Recent Use](requirements/profile-recent-use.md)
|
||||||
|
|||||||
@@ -82,11 +82,44 @@ my session to an unrelated default model.
|
|||||||
variation, it shall keep the saved model unchanged and shall persist a
|
variation, it shall keep the saved model unchanged and shall persist a
|
||||||
warning that identifies the inferred effective model.
|
warning that identifies the inferred effective model.
|
||||||
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-002.7:** The host profile probe shall
|
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-002.7:** The host profile probe shall
|
||||||
present unique-variation resolution as an advisory. It shall not become the
|
present unique-variation resolution as an advisory in the profile editor.
|
||||||
launch authority or disable the profile.
|
It shall not become the launch authority or add a model warning to a profile
|
||||||
|
selector. It shall not disable the profile.
|
||||||
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-002.8:** The same resolution order shall
|
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-002.8:** The same resolution order shall
|
||||||
apply at initial launch, context reset, and workspace rebind.
|
apply at initial launch, context reset, and workspace rebind.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
#### Acceptance criteria
|
||||||
|
|
||||||
|
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-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-AGENTS-NO-SILENT-MODEL-FALLBACK-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-AGENTS-NO-SILENT-MODEL-FALLBACK-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-AGENTS-NO-SILENT-MODEL-FALLBACK-003.4:** The profile editor shall retain
|
||||||
|
its missing-model treatment, unique-variation advisory, and fallback controls.
|
||||||
|
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.5:** When the executor applies the
|
||||||
|
requested model successfully, a host catalog difference shall not cause a
|
||||||
|
model-selection warning in task chat. Actual fallback warnings shall retain
|
||||||
|
the persistence and visibility defined by requirement 001.
|
||||||
|
- **AC-AGENTS-NO-SILENT-MODEL-FALLBACK-003.6:** Selecting a profile shall not
|
||||||
|
rewrite its saved model, reasoning configuration, or fallback settings.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The September 2026 selector amendment removes host model advisories from
|
||||||
|
profile selection. It preserves executor model selection and profile editing.
|
||||||
|
Delivery is tracked in the [implementation package](../../../plans/profile-selector-model-warnings/plan.md).
|
||||||
|
|
||||||
## Out of scope
|
## Out of scope
|
||||||
|
|
||||||
- Ranking multiple variations by context size, speed, price, or display order.
|
- Ranking multiple variations by context size, speed, price, or display order.
|
||||||
@@ -94,6 +127,7 @@ my session to an unrelated default model.
|
|||||||
- Inferring a base model from an already bracketed request.
|
- Inferring a base model from an already bracketed request.
|
||||||
- Rewriting the profile model to the current executor's advertised variation.
|
- Rewriting the profile model to the current executor's advertised variation.
|
||||||
- Changing Office post-start provider routing or mid-turn model switching.
|
- Changing Office post-start provider routing or mid-turn model switching.
|
||||||
|
- Changing host discovery refresh, cache invalidation, or ACP model normalization.
|
||||||
|
|
||||||
## System design
|
## System design
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ system: agents
|
|||||||
requirements:
|
requirements:
|
||||||
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001
|
||||||
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003
|
||||||
created: 2026-08-23
|
created: 2026-08-23
|
||||||
owners:
|
owners:
|
||||||
- kandev
|
- kandev
|
||||||
@@ -21,6 +22,7 @@ resolution from a bare requested model to one advertised bracketed variation.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001` | [Migrated source detail](#migrated-source-detail) |
|
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001` | [Migrated source detail](#migrated-source-detail) |
|
||||||
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002` | [Unique model-variation resolution](#unique-model-variation-resolution) |
|
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002` | [Unique model-variation resolution](#unique-model-variation-resolution) |
|
||||||
|
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003` | [Profile selection and warning placement](#8-profile-selection-and-warning-placement) |
|
||||||
|
|
||||||
## Migrated source detail
|
## Migrated source detail
|
||||||
|
|
||||||
@@ -139,7 +141,7 @@ over `fallback_model`):
|
|||||||
| Session start, model selection unsupported | Continue on the agent default and persist a warning. | Same | Same |
|
| Session start, model selection unsupported | Continue on the agent default and persist a warning. | Same | Same |
|
||||||
| Mid-session model/auth failure (office run, post-start) | Unchanged ADR behavior: office re-dispatches via the workspace routing chain (`routingerr.Decide(ContextOffice)`; availability codes → `DecisionFallback`). The profile's model policy does **not** gate office fallback — the workspace routing configuration is the office authorization owner. | Same as default-on-mismatch: `fallback_model` is a session-start policy, not an office routing input. | Legacy: re-dispatch to next candidate in the provider order (unchanged). |
|
| Mid-session model/auth failure (office run, post-start) | Unchanged ADR behavior: office re-dispatches via the workspace routing chain (`routingerr.Decide(ContextOffice)`; availability codes → `DecisionFallback`). The profile's model policy does **not** gate office fallback — the workspace routing configuration is the office authorization owner. | Same as default-on-mismatch: `fallback_model` is a session-start policy, not an office routing input. | Legacy: re-dispatch to next candidate in the provider order (unchanged). |
|
||||||
| Boot reconciliation | Never overwrite a gone start model (keep it; UI shows it red). Same for a gone `fallback_model`. | Same | Same (reconciler is mode-independent). |
|
| Boot reconciliation | Never overwrite a gone start model (keep it; UI shows it red). Same for a gone `fallback_model`. | Same | Same (reconciler is mode-independent). |
|
||||||
| New-task / new-agent profile picker | Profile selectable with a host-catalog warning. | Profile selectable with a host-catalog warning. | Profile selectable with a host-catalog warning. |
|
| New-task / new-agent profile picker | Profile selectable without a host model advisory. | Same. | Same. |
|
||||||
| Model picker (profile editor, session toolbar) | Gone models greyed out, unselectable, visible. | Same. | Same. |
|
| Model picker (profile editor, session toolbar) | Gone models greyed out, unselectable, visible. | Same. | Same. |
|
||||||
|
|
||||||
`SetModel` failures that mean "this agent does not support model selection"
|
`SetModel` failures that mean "this agent does not support model selection"
|
||||||
@@ -227,17 +229,17 @@ table. This calculation is advisory because the host catalog can differ from
|
|||||||
the selected executor catalog.
|
the selected executor catalog.
|
||||||
|
|
||||||
When the host advertises one variation, the task profile option remains
|
When the host advertises one variation, the task profile option remains
|
||||||
selectable and identifies the possible target in its existing amber advisory.
|
selectable without a host model advisory. The profile editor keeps the saved
|
||||||
The profile editor keeps the saved bare ID visible and describes the unique
|
bare ID visible and describes the unique
|
||||||
host variation instead of reporting that the model is simply gone. Selecting
|
host variation instead of reporting that the model is simply gone. Selecting
|
||||||
the advertised variation remains an explicit way to update the profile.
|
the advertised variation remains an explicit way to update the profile.
|
||||||
|
|
||||||
When the host advertises zero or multiple variations, the current missing-model
|
When the host advertises zero or multiple variations, the profile editor's
|
||||||
presentation remains. The runtime can still reach a different decision from
|
missing-model presentation remains. The runtime can still reach a different decision from
|
||||||
the selected executor catalog.
|
the selected executor catalog.
|
||||||
|
|
||||||
The task-create advisory keeps its existing desktop tooltip and coarse-pointer
|
The task-create selector has no host model tooltip or help drawer.
|
||||||
drawer. The profile editor uses its existing responsive selector. The durable
|
The profile editor uses its existing responsive selector. The durable
|
||||||
chat warning uses the existing status-message layout on desktop and mobile.
|
chat warning uses the existing status-message layout on desktop and mobile.
|
||||||
This change adds no nested scroll area or hover-only information.
|
This change adds no nested scroll area or hover-only information.
|
||||||
|
|
||||||
@@ -496,27 +498,57 @@ All new copy is externalized via `t()` into the `settings` i18n namespace
|
|||||||
(`apps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/settings.json`) — the i18n ratchet
|
(`apps/web/src/locales/{en,pseudo,pt-pt,zh-cn}/settings.json`) — the i18n ratchet
|
||||||
judges added lines even in unmigrated files.
|
judges added lines even in unmigrated files.
|
||||||
|
|
||||||
### 8. Profile picker warnings (new-task / new-agent)
|
### 8. Profile selection and warning placement
|
||||||
|
|
||||||
`apps/web/lib/state/slices/settings/types.ts` — `AgentProfileOption` gains
|
`apps/web/lib/state/slices/settings/types.ts` — `AgentProfileOption` gains
|
||||||
`model`, `fallbackModel`, `autoFallback` (populated in
|
`model`, `fallbackModel`, `autoFallback` (populated in
|
||||||
`toAgentProfileOption`).
|
`toAgentProfileOption`).
|
||||||
|
|
||||||
`apps/web/components/task-create-dialog-options.tsx`
|
`apps/web/components/task-create-dialog-options.tsx`
|
||||||
(`useAgentProfileOptions`) can compute a host-catalog difference.
|
(`useAgentProfileOptions`) derives profile labels and eligibility without
|
||||||
This difference is advisory only.
|
comparing the saved model with the host model catalog. Remove
|
||||||
|
`advertisedModelIDs`, `ModelProbeWarning`, and `ModelProbeWarningIndicator` from
|
||||||
|
this module, plus imports used only by those helpers. The hook no longer needs
|
||||||
|
`useAvailableAgents` solely to produce model advisories.
|
||||||
|
|
||||||
- Every profile remains selectable.
|
Both `renderLabel` and `renderTriggerLabel` use the same profile presentation.
|
||||||
- A missing host model shows one amber warning icon beside the profile name.
|
Keep the option interface compatible with its consumers. Preserve profile
|
||||||
- On fine pointers, hovering or focusing the warning icon reveals the full
|
eligibility, recent-use ordering, names, logos, and CLI passthrough indicators.
|
||||||
localized advisory that the executor decides availability at launch. On
|
Keep `getCapabilityWarning` and its authentication, installation, and probe
|
||||||
coarse pointers, tapping the icon opens the same advisory in a drawer. The
|
failure states. Those states describe agent health rather than a model mismatch.
|
||||||
advisory is not shown as an always-visible secondary row in the option list.
|
|
||||||
- The warning does not promise that an explicit fallback will be available.
|
Consumers include task creation, new subtasks, new sessions, quick chat,
|
||||||
- The warning does not change the saved profile model.
|
automation configuration, and Office setup. All receive this behavior from the
|
||||||
|
shared hook. Do not add consumer-specific warning suppression flags.
|
||||||
|
|
||||||
|
Retain profile model/configuration fields and the editor's
|
||||||
|
`findUniqueModelVariation`. After checking references, remove these unused
|
||||||
|
selector keys from every locale:
|
||||||
|
`profileStartModelNotAdvertisedOnHost` and
|
||||||
|
`profileStartModelUniqueVariationOnHost`.
|
||||||
|
|
||||||
`apps/web/app/office/setup/agent-profile-setup-controls.tsx`
|
`apps/web/app/office/setup/agent-profile-setup-controls.tsx`
|
||||||
(`useSelectableProfileOptions`) uses the same advisory behavior.
|
(`useSelectableProfileOptions`) retains its Office eligibility filtering around
|
||||||
|
the shared profile options.
|
||||||
|
|
||||||
|
#### Desktop and mobile composition
|
||||||
|
|
||||||
|
Keep the touch-usable combobox and shared selection logic. A row tap completes
|
||||||
|
the temporary profile choice without another help drawer or an empty hit area.
|
||||||
|
Preserve keyboard selection, focus return, picker-owned scrolling, and safe-area
|
||||||
|
constraints. Assert no document horizontal overflow.
|
||||||
|
|
||||||
|
Use `e2e/tests/settings/mobile-no-silent-model-fallback.spec.ts` as the shipped
|
||||||
|
flow and `components/task/mobile/mobile-picker-sheet.tsx` as the curated
|
||||||
|
temporary-choice precedent. No picker-shell replacement is needed.
|
||||||
|
|
||||||
|
#### Rationale and compatibility
|
||||||
|
|
||||||
|
A host observation cannot establish the executor outcome. A neutral icon or
|
||||||
|
better ID comparison would still report valid catalog differences during
|
||||||
|
selection. Keep the existing executor-authority ADR and runtime behavior.
|
||||||
|
This change needs no resolver, cache, schema, or new ADR. Explain warning
|
||||||
|
placement in `docs/public/agents-and-profiles.md` when implementation ships.
|
||||||
|
|
||||||
The persisted `model_selection_warning` chat message renders through
|
The persisted `model_selection_warning` chat message renders through
|
||||||
`apps/web/components/task/chat/messages/status-message.tsx`.
|
`apps/web/components/task/chat/messages/status-message.tsx`.
|
||||||
@@ -566,8 +598,11 @@ Frontend (Vitest, `*.test.ts(x)`):
|
|||||||
|
|
||||||
- `model-config-selector`: disabled option not selectable; greyed class.
|
- `model-config-selector`: disabled option not selectable; greyed class.
|
||||||
- `session-models` WS handler: stale active model is kept (not cleared).
|
- `session-models` WS handler: stale active model is kept (not cleared).
|
||||||
- `useAgentProfileOptions`: every host-mismatch profile remains selectable and
|
- `useAgentProfileOptions`: every otherwise eligible host-mismatch profile
|
||||||
shows an advisory warning.
|
remains selectable with no model advisory in either label. Cover canonical
|
||||||
|
IDs, legacy bracketed IDs, unique and multiple variations, and empty catalogs.
|
||||||
|
- Capability health warnings, recent-use ordering, disabled-profile filtering,
|
||||||
|
and saved profile values remain unchanged by selector rendering.
|
||||||
- Profile editor: gone start model renders red + disabled; auto-fallback keeps
|
- Profile editor: gone start model renders red + disabled; auto-fallback keeps
|
||||||
the explicit fallback choice visible but disables its controls.
|
the explicit fallback choice visible but disables its controls.
|
||||||
- Profile editors: fallback settings start collapsed; expanding exposes both
|
- Profile editors: fallback settings start collapsed; expanding exposes both
|
||||||
@@ -581,8 +616,11 @@ E2E (Playwright, `apps/web/e2e`):
|
|||||||
|
|
||||||
- Mock backend (`KANDEV_E2E_MOCK=true`): create a profile whose start model
|
- Mock backend (`KANDEV_E2E_MOCK=true`): create a profile whose start model
|
||||||
is not in the host catalog. Make sure that the task-create picker keeps the
|
is not in the host catalog. Make sure that the task-create picker keeps the
|
||||||
profile selectable, shows one warning icon, and reveals the advisory warning
|
profile selectable without a host model warning in the option or selected
|
||||||
through the fine-pointer tooltip or coarse-pointer drawer.
|
label. Select the profile with keyboard on desktop and a row tap on mobile.
|
||||||
|
- Launch a profile whose executor advertises its exact model while the host
|
||||||
|
omits it. Verify the selected model, no model-selection warning, and unchanged
|
||||||
|
profile values after reload.
|
||||||
- Launch with an executor catalog that omits the profile model. Make sure that
|
- Launch with an executor catalog that omits the profile model. Make sure that
|
||||||
no model-selection call occurs, the task continues, and chat shows one warning.
|
no model-selection call occurs, the task continues, and chat shows one warning.
|
||||||
- Reload the task page. Make sure that the warning remains in chat.
|
- Reload the task page. Make sure that the warning remains in chat.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ system: agents
|
|||||||
requirements:
|
requirements:
|
||||||
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001
|
||||||
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002
|
||||||
|
- REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003
|
||||||
created: 2026-08-23
|
created: 2026-08-23
|
||||||
owners:
|
owners:
|
||||||
- kandev
|
- kandev
|
||||||
@@ -21,6 +22,7 @@ variation resolution.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001` | [Migrated source detail](#migrated-source-detail) |
|
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-001` | [Migrated source detail](#migrated-source-detail) |
|
||||||
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002` | [Risks & Open Questions](#risks--open-questions) |
|
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-002` | [Risks & Open Questions](#risks--open-questions) |
|
||||||
|
| `REQ-AGENTS-NO-SILENT-MODEL-FALLBACK-003` | [Risks & Open Questions](#risks--open-questions) |
|
||||||
|
|
||||||
## Migrated source detail
|
## Migrated source detail
|
||||||
|
|
||||||
@@ -36,8 +38,8 @@ variation resolution.
|
|||||||
workspace routing configuration, not the execution profile — and is
|
workspace routing configuration, not the execution profile — and is
|
||||||
documented in the behavior matrix above.
|
documented in the behavior matrix above.
|
||||||
- **Probe staleness**: the advertised list can be stale (probe cached).
|
- **Probe staleness**: the advertised list can be stale (probe cached).
|
||||||
The profile picker uses it only as a hint. The executor session catalog owns
|
The profile editor uses it only as a hint. Profile selectors do not render
|
||||||
the launch decision.
|
host model advisories. The executor session catalog owns the launch decision.
|
||||||
- **Cold Claude model lists**: a valid restricted model can be absent from a
|
- **Cold Claude model lists**: a valid restricted model can be absent from a
|
||||||
cold bridge's initial list. Pre-session exposure lets the bridge include and
|
cold bridge's initial list. Pre-session exposure lets the bridge include and
|
||||||
select the configured model. If the bridge still omits it, Kandev uses the
|
select the configured model. If the bridge still omits it, Kandev uses the
|
||||||
@@ -46,7 +48,7 @@ variation resolution.
|
|||||||
before the initial process starts. It does not restart a live ACP bridge to
|
before the initial process starts. It does not restart a live ACP bridge to
|
||||||
expose a newly selected hidden model during context reset.
|
expose a newly selected hidden model during context reset.
|
||||||
- **Office vs. kanban surfaces**: both share the same agent-profile rows.
|
- **Office vs. kanban surfaces**: both share the same agent-profile rows.
|
||||||
The advisory picker behavior covers kanban task creation and Office setup.
|
The shared selector behavior covers kanban task creation and Office setup.
|
||||||
Office run-detail routing surfaces are unchanged.
|
Office run-detail routing surfaces are unchanged.
|
||||||
- **Collapsed controls remain legible**: the disclosure header summarizes the
|
- **Collapsed controls remain legible**: the disclosure header summarizes the
|
||||||
effective mode, and dirty-state decoration is applied to the disclosure
|
effective mode, and dirty-state decoration is applied to the disclosure
|
||||||
@@ -65,3 +67,9 @@ variation resolution.
|
|||||||
- **Legacy automatic fallback remains unchanged**: when `auto_fallback` is
|
- **Legacy automatic fallback remains unchanged**: when `auto_fallback` is
|
||||||
enabled and the requested model is absent, Kandev does not apply an explicit
|
enabled and the requested model is absent, Kandev does not apply an explicit
|
||||||
fallback or infer a variation. It continues with the provider default.
|
fallback or infer a variation. It continues with the provider default.
|
||||||
|
- **Removal must be narrow**: authentication, missing-CLI, and failed-probe
|
||||||
|
indicators remain. Profile-editor advisories and persisted task warnings
|
||||||
|
retain their existing behavior.
|
||||||
|
- **Evidence has limits**: a model warning does not prove a cache defect.
|
||||||
|
Discovery refresh or normalization changes require separate root-cause
|
||||||
|
evidence; this amendment does not change those contracts.
|
||||||
|
|||||||
Reference in New Issue
Block a user