From c6048dd44a41d2db3cc4bf07ea10775ba5510b5e Mon Sep 17 00:00:00 2001 From: Ellie Date: Fri, 17 Apr 2026 22:47:09 -0400 Subject: [PATCH 01/29] Redesign model picker with favorites and search - Replace provider submenus with sidebar-based model selection - Add model search, favorites, and locked-provider handling - Update settings schema and tests for favorite model persistence --- .codex | 0 apps/desktop/src/clientPersistence.test.ts | 1 + .../src/components/CommandPalette.logic.ts | 34 ++- apps/web/src/components/chat/ModelListRow.tsx | 61 ++++ .../components/chat/ModelPickerContent.tsx | 269 ++++++++++++++++++ .../components/chat/ModelPickerSidebar.tsx | 96 +++++++ .../chat/ProviderModelPicker.browser.tsx | 203 +++++++++---- .../components/chat/ProviderModelPicker.tsx | 169 ++--------- .../src/components/chat/providerIconUtils.ts | 47 +++ apps/web/src/index.css | 22 ++ apps/web/src/localApi.test.ts | 2 + packages/contracts/src/settings.ts | 26 ++ 12 files changed, 707 insertions(+), 223 deletions(-) create mode 100644 .codex create mode 100644 apps/web/src/components/chat/ModelListRow.tsx create mode 100644 apps/web/src/components/chat/ModelPickerContent.tsx create mode 100644 apps/web/src/components/chat/ModelPickerSidebar.tsx create mode 100644 apps/web/src/components/chat/providerIconUtils.ts diff --git a/.codex b/.codex new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts index 27f1e1d91a..19f9b09f9f 100644 --- a/apps/desktop/src/clientPersistence.test.ts +++ b/apps/desktop/src/clientPersistence.test.ts @@ -52,6 +52,7 @@ const clientSettings: ClientSettings = { confirmThreadArchive: true, confirmThreadDelete: false, diffWordWrap: true, + favorites: [], sidebarProjectGroupingMode: "repository_path", sidebarProjectGroupingOverrides: { "environment-1:/tmp/project-a": "separate", diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 866db58fb4..450f678dd5 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -151,22 +151,26 @@ export function buildThreadActionItems { - await input.runThread(thread); + return Object.assign( + { + kind: "action" as const, + value: `thread:${thread.id}`, + searchTerms: [thread.title, projectTitle ?? ``, thread.branch ?? ``], + title: thread.title, + description: descriptionParts.join(` · `), + timestamp: formatRelativeTimeLabel( + thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt, + ), + icon: input.icon, + }, + leadingContent ? { titleLeadingContent: leadingContent } : {}, + trailingContent ? { titleTrailingContent: trailingContent } : {}, + { + run: async () => { + await input.runThread(thread); + }, }, - }; + ); }); } diff --git a/apps/web/src/components/chat/ModelListRow.tsx b/apps/web/src/components/chat/ModelListRow.tsx new file mode 100644 index 0000000000..56536105e0 --- /dev/null +++ b/apps/web/src/components/chat/ModelListRow.tsx @@ -0,0 +1,61 @@ +import { type ProviderKind } from "@t3tools/contracts"; +import { memo } from "react"; +import { StarIcon } from "lucide-react"; +import { + PROVIDER_ICON_BY_PROVIDER, + providerIconClassName, + getProviderLabel, + getDisplayModelName, +} from "./providerIconUtils"; +import { cn } from "~/lib/utils"; + +export const ModelListRow = memo(function ModelListRow(props: { + slug: string; + name: string; + provider: ProviderKind; + isSelected: boolean; + isFavorite: boolean; + showProvider: boolean; + onSelect: () => void; + onToggleFavorite: () => void; +}) { + const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[props.provider]; + + return ( +
+ + + +
+ ); +}); diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx new file mode 100644 index 0000000000..7e971ce53f --- /dev/null +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -0,0 +1,269 @@ +import { type ProviderKind, type ServerProvider } from "@t3tools/contracts"; +import { resolveSelectableModel } from "@t3tools/shared/model"; +import { memo, useMemo, useState, useCallback } from "react"; +import { SearchIcon } from "lucide-react"; +import { ModelListRow } from "./ModelListRow"; +import { ModelPickerSidebar } from "./ModelPickerSidebar"; +import { + PROVIDER_ICON_BY_PROVIDER, + providerIconClassName, + getProviderLabel, +} from "./providerIconUtils"; +import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; +import { cn } from "~/lib/utils"; + +export const ModelPickerContent = memo(function ModelPickerContent(props: { + provider: ProviderKind; + model: string; + lockedProvider: ProviderKind | null; + providers?: ReadonlyArray; + modelOptionsByProvider: Record>; + onProviderModelChange: (provider: ProviderKind, model: string) => void; +}) { + const [searchQuery, setSearchQuery] = useState(""); + const [selectedProvider, setSelectedProvider] = useState( + "all", + ); + const favorites = useSettings((s) => s.favorites ?? []); + const { updateSettings } = useUpdateSettings(); + + const handleSelectProvider = useCallback((provider: ProviderKind | "all" | "favorites") => { + setSelectedProvider(provider); + }, []); + + // Create a Set for efficient lookup + const favoritesSet = useMemo(() => { + return new Set(favorites.map((fav) => `${fav.provider}:${fav.model}`)); + }, [favorites]); + + const readyProviderSet = useMemo(() => { + if (!props.providers || props.providers.length === 0) { + return null; + } + return new Set( + props.providers + .filter((provider) => provider.status === "ready") + .map((provider) => provider.provider), + ); + }, [props.providers]); + + // Flatten models into a searchable array + const flatModels = useMemo(() => { + return Object.entries(props.modelOptionsByProvider).flatMap(([providerKind, models]) => { + if (readyProviderSet && !readyProviderSet.has(providerKind as ProviderKind)) { + return []; + } + return models.map((m) => ({ + slug: m.slug, + name: m.name, + provider: providerKind as ProviderKind, + })); + }); + }, [props.modelOptionsByProvider, readyProviderSet]); + + // Get favorite models from the flat list + const favoriteModels = useMemo(() => { + return flatModels.filter((m) => favoritesSet.has(`${m.provider}:${m.slug}`)); + }, [flatModels, favoritesSet]); + + // Filter models based on search query and selected provider + const filteredModels = useMemo(() => { + let result = flatModels; + + // Handle favorites filter + if (selectedProvider === "favorites") { + result = result.filter((m) => favoritesSet.has(`${m.provider}:${m.slug}`)); + } else { + // Filter by locked provider if applicable + if (props.lockedProvider !== null) { + result = result.filter((m) => m.provider === props.lockedProvider); + } else if (selectedProvider !== "all") { + // Filter by selected provider (only in unlocked mode) + result = result.filter((m) => m.provider === selectedProvider); + } + } + + // Apply search query (model name + provider name) + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + result = result.filter( + (m) => m.name.toLowerCase().includes(query) || m.provider.toLowerCase().includes(query), + ); + } + + return result; + }, [flatModels, searchQuery, selectedProvider, props.lockedProvider, favoritesSet]); + + // Get visible favorite models (respecting search/provider filter) + const visibleFavoriteModels = useMemo(() => { + if (!searchQuery.trim() && selectedProvider !== "favorites") { + // No search and not in favorites mode: show favorites in dedicated section + let result = favoriteModels; + + if (props.lockedProvider !== null) { + result = result.filter((m) => m.provider === props.lockedProvider); + } else if (selectedProvider !== "all") { + result = result.filter((m) => m.provider === selectedProvider); + } + + return result; + } + + // With search or in favorites mode: don't show separate section + return []; + }, [favoriteModels, searchQuery, selectedProvider, props.lockedProvider]); + + const handleModelSelect = (modelSlug: string, provider: ProviderKind) => { + const resolvedModel = resolveSelectableModel( + provider, + modelSlug, + props.modelOptionsByProvider[provider], + ); + if (resolvedModel) { + props.onProviderModelChange(provider, resolvedModel); + } + }; + + const toggleFavorite = useCallback( + (provider: ProviderKind, model: string) => { + const newFavorites = [...favorites]; + const index = newFavorites.findIndex((f) => f.provider === provider && f.model === model); + if (index >= 0) { + newFavorites.splice(index, 1); + } else { + newFavorites.push({ provider, model }); + } + updateSettings({ favorites: newFavorites }); + }, + [favorites, updateSettings], + ); + + const isLocked = props.lockedProvider !== null; + const LockedProviderIcon = + isLocked && props.lockedProvider ? PROVIDER_ICON_BY_PROVIDER[props.lockedProvider] : null; + + // Get a model name from the locked provider to extract sub-provider info (for OpenCode) + const lockedProviderModelName = + isLocked && props.lockedProvider + ? (props.modelOptionsByProvider[props.lockedProvider]?.[0]?.name ?? "") + : ""; + + return ( +
+ {/* Locked provider header (only shown in locked mode) */} + {isLocked && LockedProviderIcon && props.lockedProvider && ( +
+ + + {getProviderLabel(props.lockedProvider, lockedProviderModelName)} + +
+ )} + + {/* Sidebar (only in unlocked mode) */} + {!isLocked && ( + + )} + + {/* Main content area */} +
+ {/* Search bar */} +
+ + setSearchQuery(e.target.value)} + onKeyDown={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + autoFocus + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/50 relative z-20" + /> +
+ + {/* Model list */} +
+ {visibleFavoriteModels.length > 0 || filteredModels.length > 0 ? ( +
+ {/* Favorites section with sticky header */} + {visibleFavoriteModels.length > 0 && ( +
+
+ FAVORITES +
+
+ {visibleFavoriteModels.map((model) => ( + handleModelSelect(model.slug, model.provider)} + onToggleFavorite={() => toggleFavorite(model.provider, model.slug)} + /> + ))} +
+
+ )} + + {/* All models section - only has sticky header if no favorites */} + {filteredModels.length > 0 && ( +
+ {visibleFavoriteModels.length === 0 && ( +
+ ALL MODELS +
+ )} + {visibleFavoriteModels.length > 0 && ( +
+ ALL MODELS +
+ )} +
+ {filteredModels.map((model) => ( + handleModelSelect(model.slug, model.provider)} + onToggleFavorite={() => toggleFavorite(model.provider, model.slug)} + /> + ))} +
+
+ )} +
+ ) : ( +
+ No models found +
+ )} +
+
+
+ ); +}); diff --git a/apps/web/src/components/chat/ModelPickerSidebar.tsx b/apps/web/src/components/chat/ModelPickerSidebar.tsx new file mode 100644 index 0000000000..40c7f62a94 --- /dev/null +++ b/apps/web/src/components/chat/ModelPickerSidebar.tsx @@ -0,0 +1,96 @@ +import { type ProviderKind, type ServerProvider } from "@t3tools/contracts"; +import { memo } from "react"; +import { StarIcon } from "lucide-react"; +import { Gemini } from "../Icons"; +import { PROVIDER_ICON_BY_PROVIDER, providerIconClassName } from "./providerIconUtils"; +import { AVAILABLE_PROVIDER_OPTIONS } from "./ProviderModelPicker"; +import { cn } from "~/lib/utils"; +import { getProviderSnapshot } from "../../providerModels"; + +export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { + selectedProvider: ProviderKind | "all" | "favorites"; + onSelectProvider: (provider: ProviderKind | "all" | "favorites") => void; + providers?: ReadonlyArray; + modelOptionsByProvider: Record>; +}) { + const handleProviderClick = (provider: ProviderKind | "all" | "favorites") => { + props.onSelectProvider(provider); + }; + + return ( +
+ {/* All button */} + + + {/* Provider buttons */} +
+ {AVAILABLE_PROVIDER_OPTIONS.map((option) => { + const OptionIcon = PROVIDER_ICON_BY_PROVIDER[option.value]; + const liveProvider = props.providers + ? getProviderSnapshot(props.providers, option.value) + : undefined; + + const isDisabled = liveProvider && liveProvider.status !== "ready"; + const isSelected = props.selectedProvider === option.value; + + return ( + + ); + })} + + {/* Gemini button (coming soon) */} + +
+ + {/* Favorites section */} +
+ +
+
+ ); +}); diff --git a/apps/web/src/components/chat/ProviderModelPicker.browser.tsx b/apps/web/src/components/chat/ProviderModelPicker.browser.tsx index abedcd6eeb..79e45326df 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.browser.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.browser.tsx @@ -173,7 +173,7 @@ describe("ProviderModelPicker", () => { document.body.innerHTML = ""; }); - it("shows provider submenus when provider switching is allowed", async () => { + it("shows provider sidebar in unlocked mode", async () => { const mounted = await mountPicker({ provider: "claudeAgent", model: "claude-opus-4-6", @@ -187,14 +187,14 @@ describe("ProviderModelPicker", () => { const text = document.body.textContent ?? ""; expect(text).toContain("Codex"); expect(text).toContain("Claude"); - expect(text).not.toContain("Claude Sonnet 4.6"); + expect(text).toContain("Claude Opus 4.6"); }); } finally { await mounted.cleanup(); } }); - it("opens provider submenus with a visible gap from the parent menu", async () => { + it("filters models by selected provider in sidebar", async () => { const mounted = await mountPicker({ provider: "claudeAgent", model: "claude-opus-4-6", @@ -203,43 +203,149 @@ describe("ProviderModelPicker", () => { try { await page.getByRole("button").click(); - const providerTrigger = page.getByRole("menuitem", { name: "Codex" }); - await providerTrigger.hover(); + // Start with all models visible await vi.waitFor(() => { - expect(document.body.textContent ?? "").toContain("GPT-5 Codex"); + const text = document.body.textContent ?? ""; + expect(text).toContain("GPT-5 Codex"); + expect(text).toContain("Claude Opus 4.6"); }); - const providerTriggerElement = Array.from( - document.querySelectorAll('[role="menuitem"]'), - ).find((element) => element.textContent?.includes("Codex")); - if (!providerTriggerElement) { - throw new Error("Expected the Codex provider trigger to be mounted."); + // Click on Codex provider in sidebar + const buttons = await page.getByTitle("Codex").all(); + if (buttons.length > 0) { + await buttons[0]!.click(); + + // Now should only show Codex models + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain("GPT-5 Codex"); + expect(text).not.toContain("Claude Opus 4.6"); + }); } + } finally { + await mounted.cleanup(); + } + }); - const providerTriggerRect = providerTriggerElement.getBoundingClientRect(); - const modelElement = Array.from( - document.querySelectorAll('[role="menuitemradio"]'), - ).find((element) => element.textContent?.includes("GPT-5 Codex")); - if (!modelElement) { - throw new Error("Expected the submenu model option to be mounted."); - } + it("shows locked provider header and only its models in locked mode", async () => { + const mounted = await mountPicker({ + provider: "claudeAgent", + model: "claude-opus-4-6", + lockedProvider: "claudeAgent", + }); - const submenuPopup = modelElement.closest('[data-slot="menu-sub-content"]'); - if (!(submenuPopup instanceof HTMLElement)) { - throw new Error("Expected submenu popup to be mounted."); - } + try { + await page.getByRole("button").click(); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + // Should show locked provider name + expect(text).toContain("claudeAgent"); + // Should show models from locked provider + expect(text).toContain("Claude Sonnet 4.6"); + expect(text).toContain("Claude Haiku 4.5"); + // Should not show other provider models + expect(text).not.toContain("GPT-5 Codex"); + }); + } finally { + await mounted.cleanup(); + } + }); + + it("searches models by name in flat list", async () => { + const mounted = await mountPicker({ + provider: "claudeAgent", + model: "claude-opus-4-6", + lockedProvider: null, + }); + + try { + await page.getByRole("button").click(); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain("Claude Opus 4.6"); + expect(text).toContain("GPT-5 Codex"); + }); - const submenuRect = submenuPopup.getBoundingClientRect(); + // Find and type in search box + const searchInput = page.getByPlaceholder("Search models..."); + await searchInput.fill("claude"); - expect(submenuRect.left).toBeGreaterThanOrEqual(providerTriggerRect.right); - expect(submenuRect.left - providerTriggerRect.right).toBeGreaterThanOrEqual(2); + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain("Claude Opus 4.6"); + expect(text).not.toContain("GPT-5 Codex"); + }); } finally { await mounted.cleanup(); } }); - it("shows models directly when the provider is locked mid-thread", async () => { + it("searches models by provider name", async () => { + const mounted = await mountPicker({ + provider: "claudeAgent", + model: "claude-opus-4-6", + lockedProvider: null, + }); + + try { + await page.getByRole("button").click(); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain("Claude Opus 4.6"); + expect(text).toContain("GPT-5 Codex"); + }); + + // Search by provider name + const searchInput = page.getByPlaceholder("Search models..."); + await searchInput.fill("codex"); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain("GPT-5 Codex"); + expect(text).not.toContain("Claude Opus 4.6"); + }); + } finally { + await mounted.cleanup(); + } + }); + + it("toggles favorite stars when clicked", async () => { + const mounted = await mountPicker({ + provider: "claudeAgent", + model: "claude-opus-4-6", + lockedProvider: null, + }); + + try { + await page.getByRole("button").click(); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain("Claude Opus 4.6"); + }); + + // Get star buttons + const starButtons = await page.getByRole("button", { name: /favor/ }).all(); + const firstStar = starButtons[0]; + if (firstStar) { + // Click to add to favorites + await firstStar.click(); + } + + // Favorites should toggle + await vi.waitFor(() => { + expect(starButtons.length).toBeGreaterThan(0); + }); + } finally { + await mounted.cleanup(); + } + }); + + it("dispatches callback with correct provider and model when selected", async () => { const mounted = await mountPicker({ provider: "claudeAgent", model: "claude-opus-4-6", @@ -252,15 +358,23 @@ describe("ProviderModelPicker", () => { await vi.waitFor(() => { const text = document.body.textContent ?? ""; expect(text).toContain("Claude Sonnet 4.6"); - expect(text).toContain("Claude Haiku 4.5"); - expect(text).not.toContain("Codex"); }); + + // Click on a model + const modelRow = page.getByText("Claude Sonnet 4.6").first(); + await modelRow.click(); + + // Verify callback was called with correct values + expect(mounted.onProviderModelChange).toHaveBeenCalledWith( + "claudeAgent", + "claude-sonnet-4-6", + ); } finally { await mounted.cleanup(); } }); - it("only shows codex spark when the server reports it for the account", async () => { + it("only shows codex spark when the server reports it", async () => { const providersWithoutSpark: ReadonlyArray = [ buildCodexProvider([ { @@ -317,7 +431,6 @@ describe("ProviderModelPicker", () => { try { await page.getByRole("button").click(); - await page.getByRole("menuitem", { name: "Codex" }).hover(); await vi.waitFor(() => { const text = document.body.textContent ?? ""; @@ -337,7 +450,6 @@ describe("ProviderModelPicker", () => { try { await page.getByRole("button").click(); - await page.getByRole("menuitem", { name: "Codex" }).hover(); await vi.waitFor(() => { expect(document.body.textContent ?? "").toContain("GPT-5.3 Codex Spark"); @@ -347,27 +459,7 @@ describe("ProviderModelPicker", () => { } }); - it("dispatches the canonical slug when a model is selected", async () => { - const mounted = await mountPicker({ - provider: "claudeAgent", - model: "claude-opus-4-6", - lockedProvider: "claudeAgent", - }); - - try { - await page.getByRole("button").click(); - await page.getByRole("menuitemradio", { name: "Claude Sonnet 4.6" }).click(); - - expect(mounted.onProviderModelChange).toHaveBeenCalledWith( - "claudeAgent", - "claude-sonnet-4-6", - ); - } finally { - await mounted.cleanup(); - } - }); - - it("shows disabled providers as non-selectable entries", async () => { + it("shows disabled providers grayed out in sidebar", async () => { const disabledProviders = TEST_PROVIDERS.slice(); const claudeIndex = disabledProviders.findIndex( (provider) => provider.provider === "claudeAgent", @@ -380,6 +472,7 @@ describe("ProviderModelPicker", () => { status: "disabled", }; } + const mounted = await mountPicker({ provider: "codex", model: "gpt-5-codex", @@ -392,9 +485,9 @@ describe("ProviderModelPicker", () => { await vi.waitFor(() => { const text = document.body.textContent ?? ""; - expect(text).toContain("Claude"); - expect(text).toContain("Disabled"); - expect(text).not.toContain("Claude Sonnet 4.6"); + expect(text).toContain("GPT-5 Codex"); + // Disabled provider should not have its models shown + expect(text).not.toContain("Claude Opus 4.6"); }); } finally { await mounted.cleanup(); diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 8b20237a83..ead0769417 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -1,26 +1,13 @@ import { type ProviderKind, type ServerProvider } from "@t3tools/contracts"; -import { resolveSelectableModel } from "@t3tools/shared/model"; import { memo, useState } from "react"; import type { VariantProps } from "class-variance-authority"; -import { type ProviderPickerKind, PROVIDER_OPTIONS } from "../../session-logic"; +import { PROVIDER_OPTIONS } from "../../session-logic"; import { ChevronDownIcon } from "lucide-react"; import { Button, buttonVariants } from "../ui/button"; -import { - Menu, - MenuGroup, - MenuItem, - MenuPopup, - MenuRadioGroup, - MenuRadioItem, - MenuSeparator as MenuDivider, - MenuSub, - MenuSubPopup, - MenuSubTrigger, - MenuTrigger, -} from "../ui/menu"; -import { ClaudeAI, CursorIcon, Gemini, Icon, OpenAI, OpenCodeIcon } from "../Icons"; +import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; import { cn } from "~/lib/utils"; -import { getProviderSnapshot } from "../../providerModels"; +import { ModelPickerContent } from "./ModelPickerContent"; +import { PROVIDER_ICON_BY_PROVIDER, providerIconClassName } from "./providerIconUtils"; function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is { value: ProviderKind; @@ -30,23 +17,7 @@ function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): o return option.available; } -const PROVIDER_ICON_BY_PROVIDER: Record = { - codex: OpenAI, - claudeAgent: ClaudeAI, - opencode: OpenCodeIcon, - cursor: CursorIcon, -}; - export const AVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter(isAvailableProviderOption); -const UNAVAILABLE_PROVIDER_OPTIONS = PROVIDER_OPTIONS.filter((option) => !option.available); -const COMING_SOON_PROVIDER_OPTIONS = [{ id: "gemini", label: "Gemini", icon: Gemini }] as const; - -function providerIconClassName( - provider: ProviderKind | ProviderPickerKind, - fallbackClassName: string, -): string { - return provider === "claudeAgent" ? "text-[#d97757]" : fallbackClassName; -} export const ProviderModelPicker = memo(function ProviderModelPicker(props: { provider: ProviderKind; @@ -67,16 +38,10 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { const selectedModelLabel = selectedProviderOptions.find((option) => option.slug === props.model)?.name ?? props.model; const ProviderIcon = PROVIDER_ICON_BY_PROVIDER[activeProvider]; - const handleModelChange = (provider: ProviderKind, value: string) => { + + const handleProviderModelChange = (provider: ProviderKind, model: string) => { if (props.disabled) return; - if (!value) return; - const resolvedModel = resolveSelectableModel( - provider, - value, - props.modelOptionsByProvider[provider], - ); - if (!resolvedModel) return; - props.onProviderModelChange(provider, resolvedModel); + props.onProviderModelChange(provider, model); setIsMenuOpen(false); }; @@ -124,117 +89,15 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {