From 18516fd48886ab6c53ae0b449e523a0b044f5308 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 29 May 2026 09:14:34 -0400 Subject: [PATCH] feat(models): wire /api/models dynamic discovery to model selector dialog Fetches live Claude model list from /api/models on mount; falls back to hardcoded CLAUDE_MODELS.OPTIONS when the endpoint is unavailable. Closes #1 --- .../ProviderSelectionEmptyState.tsx | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx index 3f82438..df7941f 100644 --- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx +++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx @@ -11,6 +11,7 @@ import { GEMINI_MODELS, PROVIDERS, } from "../../../../../shared/modelConstants"; +import { authenticatedFetch } from "../../../../utils/api"; import type { ProjectSession, LLMProvider } from "../../../../types/app"; import { NextTaskBanner } from "../../../task-master"; import { @@ -30,6 +31,8 @@ import { const MOD_KEY = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl"; +type ModelOption = { value: string; label: string }; + type ProviderSelectionEmptyStateProps = { selectedSession: ProjectSession | null; currentSessionId: string | null; @@ -53,22 +56,15 @@ type ProviderSelectionEmptyStateProps = { type ProviderGroup = { id: LLMProvider; name: string; - models: { value: string; label: string }[]; + models: ModelOption[]; }; -const PROVIDER_GROUPS: ProviderGroup[] = PROVIDERS.map((p) => ({ +const STATIC_PROVIDER_GROUPS: ProviderGroup[] = PROVIDERS.map((p) => ({ id: p.id as LLMProvider, name: p.name, models: p.models.OPTIONS, })); -function getModelConfig(p: LLMProvider) { - if (p === "claude") return CLAUDE_MODELS; - if (p === "codex") return CODEX_MODELS; - if (p === "gemini") return GEMINI_MODELS; - return CURSOR_MODELS; -} - function getCurrentModel( p: LLMProvider, c: string, @@ -111,10 +107,36 @@ export default function ProviderSelectionEmptyState({ const { t } = useTranslation("chat"); const { isWindowsServer } = useServerPlatform(); const [dialogOpen, setDialogOpen] = useState(false); + const [claudeModelOptions, setClaudeModelOptions] = useState(CLAUDE_MODELS.OPTIONS); + + // Fetch live Claude model list from the server; fall back to static constants on failure. + useEffect(() => { + authenticatedFetch("/api/models") + .then((res) => { + if (!res.ok) return; + return res.json(); + }) + .then((data) => { + if (Array.isArray(data?.claude) && data.claude.length > 0) { + setClaudeModelOptions(data.claude); + } + }) + .catch(() => { + // Static fallback already set as initial state. + }); + }, []); + + const providerGroups = useMemo( + () => + STATIC_PROVIDER_GROUPS.map((g) => + g.id === "claude" ? { ...g, models: claudeModelOptions } : g, + ), + [claudeModelOptions], + ); const visibleProviderGroups = useMemo( - () => (isWindowsServer ? PROVIDER_GROUPS.filter((p) => p.id !== "cursor") : PROVIDER_GROUPS), - [isWindowsServer], + () => (isWindowsServer ? providerGroups.filter((p) => p.id !== "cursor") : providerGroups), + [isWindowsServer, providerGroups], ); useEffect(() => { @@ -137,12 +159,16 @@ export default function ProviderSelectionEmptyState({ ); const currentModelLabel = useMemo(() => { - const config = getModelConfig(provider); - const found = config.OPTIONS.find( - (o: { value: string; label: string }) => o.value === currentModel, - ); - return found?.label || currentModel; - }, [provider, currentModel]); + const options: ModelOption[] = + provider === "claude" + ? claudeModelOptions + : provider === "codex" + ? CODEX_MODELS.OPTIONS + : provider === "gemini" + ? GEMINI_MODELS.OPTIONS + : CURSOR_MODELS.OPTIONS; + return options.find((o) => o.value === currentModel)?.label ?? currentModel; + }, [provider, currentModel, claudeModelOptions]); const setModelForProvider = useCallback( (providerId: LLMProvider, modelValue: string) => {