diff --git a/components/settings/agent-section.tsx b/components/settings/agent-section.tsx index aa1271f..6aaeedf 100644 --- a/components/settings/agent-section.tsx +++ b/components/settings/agent-section.tsx @@ -600,10 +600,6 @@ export function AgentSection({ const chosen = coord ? catalog?.find((m) => m.id === coord.id && m.provider === coord.provider) : undefined; const levels = chosen ? chosen.supportedLevels : THINKING_LEVELS; const thisMenuOpen = openMenu?.target === slot ? openMenu.kind : null; - // Vision slot: image-capable models float first, but all stay - // selectable (a custom endpoint that doesn't report modality must - // remain pickable) — so this orders, never filters. - const slotCatalog = catalog; return ( { // Auto-populate (chunk 2, owner: "populate everywhere"): the // fresh pick writes a level — the stored one rides as-is diff --git a/components/settings/model-menu.tsx b/components/settings/model-menu.tsx index 99d4f78..bb061c1 100644 --- a/components/settings/model-menu.tsx +++ b/components/settings/model-menu.tsx @@ -21,7 +21,7 @@ import { useEffect, useRef, useState } from "react"; import type { ModelCatalogEntry } from "@/src/core"; -import { fuzzyScore, subscriptionLabel } from "@/src/core"; +import { CUSTOM_PROVIDER, fuzzyScore, subscriptionLabel } from "@/src/core"; import { formatTokens } from "@/components/usage-format"; import { SearchMenuList } from "./search-menu-list"; @@ -39,7 +39,7 @@ export function ModelMenuList({ effectiveProvider, onPick, footer, - prioritizeImage, + vision, }: { /** The full catalog — undefined while it loads. */ models: ModelCatalogEntry[] | undefined; @@ -54,19 +54,25 @@ export function ModelMenuList({ onPick: (entry: ModelCatalogEntry) => void; /** Quiet footer note under the list (context-specific). */ footer?: string; - /** Vision slot: image-capable models float to the top (still all - * selectable — a custom endpoint that doesn't report modality must - * stay pickable, so this orders rather than filters). */ - prioritizeImage?: boolean; + /** Vision slot: only image-capable models are selectable — plus every + * custom-endpoint model, whose capability the endpoint may not report. + * Image-capable ones float to the top. */ + vision?: boolean; }) { + // Vision slot: built-ins must report image input; custom-endpoint models + // stay selectable (the endpoint may not report a modality). Image-capable + // ones float to the top of the list. + const visionItems = vision + ? models?.filter((m) => m.input.includes("image") || m.provider === CUSTOM_PROVIDER.id) + : models; return ( <> `${m.provider}/${m.id}`} searchTerms={(m) => [m.provider, m.id, m.name]} compare={(a, b) => a.id.localeCompare(b.id)} - prioritize={prioritizeImage ? (m) => m.input.includes("image") : undefined} + prioritize={vision ? (m) => m.input.includes("image") : undefined} isSelected={(m) => m.id === effectiveId && m.provider === (effectiveProvider ?? m.provider)} onPick={onPick} searchTestid="model-search" diff --git a/components/setup-dialog.tsx b/components/setup-dialog.tsx index 3aee5ca..98b6ad7 100644 --- a/components/setup-dialog.tsx +++ b/components/setup-dialog.tsx @@ -519,7 +519,9 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) { // Unavailable metadata cannot erase or clamp a saved preference. const levelsForEntry = metadata?.supportedLevels ?? (entry ? Object.keys(THINKING_LABEL) : []); const available = catalog?.models ?? (failed ? [] : undefined); - const slotCatalog = slot === "vision" ? available?.filter((m) => m.input.includes("image")) : available; + const slotCatalog = slot === "vision" + ? available?.filter((m) => m.input.includes("image") || m.provider === CUSTOM_PROVIDER.id) + : available; return (
@@ -573,7 +575,7 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) { models={slotCatalog} effectiveId={entry?.id} effectiveProvider={entry?.provider} - prioritizeImage={slot === "vision"} + vision={slot === "vision"} onPick={(m) => { editedSlots.current.add(slot); setPicked((prev) => ({ ...prev, [slot]: m })); diff --git a/tests/setup-dialog-interaction.test.tsx b/tests/setup-dialog-interaction.test.tsx index e635ce0..4e3b8fb 100644 --- a/tests/setup-dialog-interaction.test.tsx +++ b/tests/setup-dialog-interaction.test.tsx @@ -172,13 +172,15 @@ describe("setup polish interactions", () => { assert.equal(closed, 0); }); - it("the vision slot lists unknown-capability models (image-capable first, nothing hidden)", async () => { + it("the vision slot filters out built-in text-only models (image-capable or custom only)", async () => { await mount({ keyed: true }); await click(element('[data-testid="setup-trigger-vision"]')); - // The text-only fixture stays selectable in the vision slot: the slot - // orders image-capable first but no longer filters, so a custom endpoint - // that reports no modality can still be picked for Vision. - assert.ok(element('[data-testid="setup-menu-vision"]').textContent?.includes("fixture-text")); + // The text-only openrouter fixture is neither image-capable nor a custom- + // endpoint model, so the vision slot filters it out (built-ins must report + // image input; only custom models stay selectable when capability is unknown). + const menu = element('[data-testid="setup-menu-vision"]'); + assert.ok(!menu.textContent?.includes("fixture-text")); + assert.ok(menu.textContent?.includes("No models match.")); }); it("catalog failure uses the approved two-sentence error", async () => {