"use client"; import { ArrowDownIcon, ArrowUpIcon, EyeIcon, EyeOffIcon, InfoIcon, PlusIcon, StarIcon, XIcon, } from "lucide-react"; import { useMemo, useRef, useState } from "react"; import { ProviderDriverKind, type ProviderInstanceId, type ServerProviderModel, } from "@t3tools/contracts"; import { normalizeCustomModelSlug } from "@t3tools/shared/model"; import { cn } from "../../lib/utils"; import { sortModelsForProviderInstance } from "../../modelOrdering"; import { MAX_CUSTOM_MODEL_LENGTH } from "../../modelSelection"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; /** * Placeholder text for the "add a custom model" input, keyed by driver * kind. Mirrors the prior hardcoded switch in `SettingsPanels.tsx` so the * UX is unchanged — only the owning component has moved. */ const CUSTOM_MODEL_PLACEHOLDER_BY_KIND: Partial> = { [ProviderDriverKind.make("codex")]: "gpt-6.7-codex-ultra-preview", [ProviderDriverKind.make("claudeAgent")]: "claude-sonnet-5", [ProviderDriverKind.make("cursor")]: "claude-sonnet-4-6", [ProviderDriverKind.make("opencode")]: "openai/gpt-5", }; interface ProviderModelsSectionProps { /** Identifier used to namespace input ids within the DOM. */ readonly instanceId: ProviderInstanceId; /** * Driver kind for slug normalization + input placeholder. `null` when * the section is rendered without enough provider metadata. */ readonly driverKind: ProviderDriverKind | null; /** * The live model list to display. Includes both built-in (probe-reported) * and custom entries, distinguished by `isCustom`. */ readonly models: ReadonlyArray; /** * The persisted custom-model slug list for this instance. Drives dedup, * and is the array we hand back verbatim (with the new slug appended / * removed) via `onChange`. */ readonly customModels: ReadonlyArray; /** Server-returned model slugs hidden from the model picker. */ readonly hiddenModels: ReadonlyArray; /** Model slugs favorited for this provider instance. */ readonly favoriteModels: ReadonlyArray; /** Explicit user-authored model ordering for this provider instance. */ readonly modelOrder: ReadonlyArray; /** * Commit the new custom-model list. Caller is responsible for routing the * write to the correct storage (legacy `settings.providers[kind]` vs. * `providerInstances[id].config`). */ readonly onChange: (next: ReadonlyArray) => void; readonly onHiddenModelsChange: (next: ReadonlyArray) => void; readonly onFavoriteModelsChange: (next: ReadonlyArray) => void; readonly onModelOrderChange: (next: ReadonlyArray) => void; } /** * Shared "Models" section rendered on both the built-in default and custom * provider-instance cards. Owns its own input + error local state so two * cards on screen don't fight over the input value. * * Validation mirrors the pre-consolidation logic in `SettingsPanels`: * - empty / whitespace → "Enter a model slug." * - duplicate of a non-custom (probe-reported) slug → "already built in" * - exceeds `MAX_CUSTOM_MODEL_LENGTH` → length error * - duplicate of an already-saved custom slug → already-saved error */ export function ProviderModelsSection({ instanceId, driverKind, models, customModels, hiddenModels, favoriteModels, modelOrder, onChange, onHiddenModelsChange, onFavoriteModelsChange, onModelOrderChange, }: ProviderModelsSectionProps) { const [input, setInput] = useState(""); const [error, setError] = useState(null); const listRef = useRef(null); const hiddenModelSet = useMemo(() => new Set(hiddenModels), [hiddenModels]); const favoriteModelSet = useMemo(() => new Set(favoriteModels), [favoriteModels]); const orderedModels = useMemo(() => { return sortModelsForProviderInstance(models, { favoriteModels: favoriteModelSet, groupFavorites: true, modelOrder, }); }, [favoriteModelSet, modelOrder, models]); const handleAdd = () => { const normalized = normalizeCustomModelSlug(input); if (!normalized) { setError("Enter a model slug."); return; } if (models.some((model) => !model.isCustom && model.slug === normalized)) { setError("That model is already built in."); return; } if (normalized.length > MAX_CUSTOM_MODEL_LENGTH) { setError(`Model slugs must be ${MAX_CUSTOM_MODEL_LENGTH} characters or less.`); return; } if (customModels.includes(normalized)) { setError("That custom model is already saved."); return; } onChange([...customModels, normalized]); setInput(""); setError(null); // Scroll the new row into view once the DOM reflects the commit. // `MutationObserver` handles the one-frame gap between `onChange` and // the `models` prop update; the `requestAnimationFrame` covers the // common case where the parent updates synchronously. const el = listRef.current; if (!el) return; const scrollToEnd = () => el.scrollTo({ top: el.scrollHeight, behavior: "smooth" }); requestAnimationFrame(scrollToEnd); const observer = new MutationObserver(() => { scrollToEnd(); observer.disconnect(); }); observer.observe(el, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), 2_000); }; const handleRemove = (slug: string) => { onChange(customModels.filter((model) => model !== slug)); onModelOrderChange(modelOrder.filter((model) => model !== slug)); onFavoriteModelsChange(favoriteModels.filter((model) => model !== slug)); setError(null); }; const handleToggleHidden = (slug: string) => { if (hiddenModelSet.has(slug)) { onHiddenModelsChange(hiddenModels.filter((model) => model !== slug)); return; } onHiddenModelsChange([...hiddenModels, slug]); }; const handleToggleFavorite = (slug: string) => { if (favoriteModelSet.has(slug)) { onFavoriteModelsChange(favoriteModels.filter((model) => model !== slug)); return; } onFavoriteModelsChange([...favoriteModels, slug]); }; const handleMove = (slug: string, direction: -1 | 1) => { const slugs = orderedModels.map((model) => model.slug); const index = slugs.indexOf(slug); const nextIndex = index + direction; if (index < 0 || nextIndex < 0 || nextIndex >= slugs.length) { return; } const next = [...slugs]; [next[index], next[nextIndex]] = [next[nextIndex]!, next[index]!]; onModelOrderChange(next); }; return (
Models
{models.length} model{models.length === 1 ? "" : "s"} available.
{orderedModels.map((model, index) => { const caps = model.capabilities; const capLabels: string[] = []; const isHidden = !model.isCustom && hiddenModelSet.has(model.slug); const isFavorite = favoriteModelSet.has(model.slug); const previousModel = orderedModels[index - 1]; const nextModel = orderedModels[index + 1]; const canMoveUp = previousModel !== undefined && favoriteModelSet.has(previousModel.slug) === isFavorite; const canMoveDown = nextModel !== undefined && favoriteModelSet.has(nextModel.slug) === isFavorite; const descriptors = caps?.optionDescriptors ?? []; if (descriptors.some((descriptor) => descriptor.id === "fastMode")) { capLabels.push("Fast mode"); } if (descriptors.some((descriptor) => descriptor.id === "thinking")) { capLabels.push("Thinking"); } if ( descriptors.some( (descriptor) => descriptor.type === "select" && (descriptor.id === "reasoningEffort" || descriptor.id === "effort" || descriptor.id === "reasoning" || descriptor.id === "variant"), ) ) { capLabels.push("Reasoning"); } const hasDetails = capLabels.length > 0 || model.name !== model.slug; return (
{model.name} {hasDetails ? ( } >
{model.slug} {capLabels.length > 0 ? (
{capLabels.map((label) => ( {label} ))}
) : null}
) : null} {isHidden ? ( hidden ) : null} {model.isCustom ? ( custom ) : null}
handleToggleFavorite(model.slug)} aria-label={`${isFavorite ? "Remove" : "Add"} ${model.name} ${ isFavorite ? "from" : "to" } favorites`} /> } > {isFavorite ? "Remove from favorites" : "Add to favorites"} handleMove(model.slug, -1)} aria-label={`Move ${model.name} up`} /> } > Move up handleMove(model.slug, 1)} aria-label={`Move ${model.name} down`} /> } > Move down {!model.isCustom ? ( handleToggleHidden(model.slug)} aria-label={`${isHidden ? "Show" : "Hide"} ${model.name}`} /> } > {isHidden ? ( ) : ( )} {isHidden ? "Show in picker" : "Hide from picker"} ) : null} {model.isCustom ? ( handleRemove(model.slug)} /> } > Remove custom model ) : null}
); })}
{ setInput(event.target.value); if (error) setError(null); }} onKeyDown={(event) => { if (event.key !== "Enter") return; event.preventDefault(); handleAdd(); }} placeholder={driverKind ? CUSTOM_MODEL_PLACEHOLDER_BY_KIND[driverKind] : "model-slug"} spellCheck={false} />
{error ?

{error}

: null}
); }