diff --git a/src/cli/components/ModelSelector.tsx b/src/cli/components/ModelSelector.tsx index 23df8ce..8136825 100644 --- a/src/cli/components/ModelSelector.tsx +++ b/src/cli/components/ModelSelector.tsx @@ -1,9 +1,19 @@ // Import useInput from vendored Ink for bracketed paste support import { Box, Text, useInput } from "ink"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { models } from "../../agent/model"; import { colors } from "./colors"; +type UiModel = { + id: string; + handle: string; + label: string; + description: string; + isDefault?: boolean; + isFeatured?: boolean; + updateArgs?: Record; +}; + interface ModelSelectorProps { currentModel?: string; onSelect: (modelId: string) => void; @@ -15,17 +25,37 @@ export function ModelSelector({ onSelect, onCancel, }: ModelSelectorProps) { + const typedModels = models as UiModel[]; + const [showAll, setShowAll] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); + const featuredModels = useMemo( + () => typedModels.filter((model) => model.isFeatured), + [typedModels], + ); + + const visibleModels = useMemo(() => { + if (showAll) return typedModels; + if (featuredModels.length > 0) return featuredModels; + return typedModels.slice(0, 5); + }, [featuredModels, showAll, typedModels]); + + const totalItems = showAll ? visibleModels.length : visibleModels.length + 1; + useInput((_input, key) => { if (key.upArrow) { setSelectedIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { - setSelectedIndex((prev) => Math.min(models.length - 1, prev + 1)); + setSelectedIndex((prev) => Math.min(totalItems - 1, prev + 1)); } else if (key.return) { - const selectedModel = models[selectedIndex]; - if (selectedModel) { - onSelect(selectedModel.id); + if (!showAll && selectedIndex === visibleModels.length) { + setShowAll(true); + setSelectedIndex(0); + } else { + const selectedModel = visibleModels[selectedIndex]; + if (selectedModel) { + onSelect(selectedModel.id); + } } } else if (key.escape) { onCancel(); @@ -41,7 +71,7 @@ export function ModelSelector({ - {models.map((model, index) => { + {visibleModels.map((model, index) => { const isSelected = index === selectedIndex; const isCurrent = model.handle === currentModel; @@ -69,6 +99,20 @@ export function ModelSelector({ ); })} + {!showAll && ( + + + {selectedIndex === visibleModels.length ? "›" : " "} + + Show all models + + )} ); diff --git a/src/models.json b/src/models.json index 07b3e27..9ebde55 100644 --- a/src/models.json +++ b/src/models.json @@ -5,6 +5,7 @@ "label": "Claude Sonnet 4.5 (default)", "description": "The recommended default model (currently Sonnet 4.5)", "isDefault": true, + "isFeatured": true, "updateArgs": { "context_window": 180000 } }, { @@ -17,18 +18,12 @@ "context_window": 180000 } }, - { - "id": "gemini-3", - "handle": "google_ai/gemini-3-pro-preview", - "label": "Gemini 3 Pro", - "description": "Google's smartest model", - "updateArgs": { "context_window": 180000 } - }, { "id": "opus", "handle": "anthropic/claude-opus-4-1-20250805", "label": "Claude Opus 4.1", - "description": "Anthropic's smartest (and slowest) model", + "description": "Anthropic's largest (and slowest) model", + "isFeatured": true, "updateArgs": { "context_window": 180000 } }, { @@ -36,6 +31,7 @@ "handle": "anthropic/claude-haiku-4-5-20251001", "label": "Claude Haiku 4.5", "description": "Anthropic's fastest model", + "isFeatured": true, "updateArgs": { "context_window": 180000 } }, { @@ -76,6 +72,7 @@ "handle": "openai/gpt-5.1", "label": "GPT-5.1 (medium)", "description": "OpenAI's latest model (using their recommended reasoning level)", + "isFeatured": true, "updateArgs": { "reasoning_effort": "medium", "verbosity": "medium", @@ -109,6 +106,7 @@ "handle": "openai/gpt-5.1-codex", "label": "GPT-5.1-Codex (medium)", "description": "GPT-5.1-Codex with recommended reasoning level", + "isFeatured": true, "updateArgs": { "reasoning_effort": "medium", "verbosity": "medium", @@ -228,6 +226,14 @@ "context_window": 128000 } }, + { + "id": "gemini-3", + "handle": "google_ai/gemini-3-pro-preview", + "label": "Gemini 3 Pro", + "description": "Google's smartest model", + "isFeatured": true, + "updateArgs": { "context_window": 180000 } + }, { "id": "gemini-flash", "handle": "google_ai/gemini-2.5-flash",