/**
* ModelSelector — inline model picker for chat
*
* Renders a SelectInput with available LLM models, grouped by provider.
* Esc to cancel. Current model marked with dot.
*/
import React from "react"; // eslint-disable-line @typescript-eslint/no-unused-vars
import { Box, Text, useInput } from "ink";
import SelectInput from "ink-select-input";
import { colors, symbols } from "../shared/Theme.js";
export interface ModelOption {
label: string;
value: string; // short key (e.g. "opus", "gemini-3-flash")
modelId: string; // full model ID
provider: string; // provider label for display
}
export const MODEL_OPTIONS: ModelOption[] = [
// Auto
{ label: "Auto (smart routing)", value: "auto", modelId: "auto", provider: "Auto" },
// Anthropic
{ label: "Opus 4.6", value: "opus", modelId: "claude-opus-4-6", provider: "Anthropic" },
{ label: "Sonnet 4", value: "sonnet", modelId: "claude-sonnet-4-20250514", provider: "Anthropic" },
{ label: "Haiku 4.5", value: "haiku", modelId: "claude-haiku-4-5-20251001", provider: "Anthropic" },
// Bedrock
{ label: "Sonnet 4", value: "bedrock-sonnet", modelId: "us.anthropic.claude-sonnet-4-20250514-v1:0", provider: "Bedrock" },
{ label: "Sonnet 4.5", value: "bedrock-sonnet-4.5", modelId: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", provider: "Bedrock" },
{ label: "Haiku 4.5", value: "bedrock-haiku", modelId: "us.anthropic.claude-haiku-4-5-20251001-v1:0", provider: "Bedrock" },
// Gemini
{ label: "Gemini 3 Pro", value: "gemini-3-pro", modelId: "gemini-3-pro-preview", provider: "Gemini" },
{ label: "Gemini 3 Flash", value: "gemini-3-flash", modelId: "gemini-3-flash-preview", provider: "Gemini" },
{ label: "Gemini 2.5 Pro", value: "gemini-pro", modelId: "gemini-2.5-pro", provider: "Gemini" },
{ label: "Gemini 2.5 Flash", value: "gemini-flash", modelId: "gemini-2.5-flash", provider: "Gemini" },
{ label: "Gemini 2.5 Flash Lite", value: "gemini-flash-lite", modelId: "gemini-2.5-flash-lite", provider: "Gemini" },
// OpenAI
{ label: "GPT-5", value: "gpt-5", modelId: "gpt-5", provider: "OpenAI" },
{ label: "GPT-5 mini", value: "gpt-5-mini", modelId: "gpt-5-mini", provider: "OpenAI" },
{ label: "GPT-5 nano", value: "gpt-5-nano", modelId: "gpt-5-nano", provider: "OpenAI" },
{ label: "o3", value: "o3", modelId: "o3", provider: "OpenAI" },
{ label: "o4-mini", value: "o4-mini", modelId: "o4-mini", provider: "OpenAI" },
{ label: "GPT-4o", value: "gpt-4o", modelId: "gpt-4o", provider: "OpenAI" },
];
// Group labels to render section headers
const PROVIDER_ORDER = ["Auto", "Anthropic", "Bedrock", "Gemini", "OpenAI"];
interface ModelSelectorProps {
currentModel: string; // short key
onSelect: (model: ModelOption) => void;
onCancel: () => void;
}
export function ModelSelector({ currentModel, onSelect, onCancel }: ModelSelectorProps) {
useInput((_input, key) => {
if (key.escape) onCancel();
});
// Build flat items list with separator entries for provider headers
const items: { label: string; value: string }[] = [];
for (const provider of PROVIDER_ORDER) {
const models = MODEL_OPTIONS.filter((m) => m.provider === provider);
if (models.length === 0) continue;
items.push({ label: `──── ${provider} ────`, value: `__header_${provider}` });
for (const m of models) {
items.push({ label: m.label, value: m.value });
}
}
const handleSelect = (item: { label: string; value: string }) => {
if (item.value.startsWith("__header_")) return; // skip headers
const model = MODEL_OPTIONS.find((m) => m.value === item.value);
if (model) onSelect(model);
};
// Find initial index (skip headers, land on current model)
const initialIdx = Math.max(0, items.findIndex((i) => i.value === currentModel));
return (
<Box flexDirection="column">
<Text color={colors.secondary}> Select model:</Text>
<Box height={1} />
<SelectInput
items={items}
initialIndex={initialIdx}
onSelect={handleSelect}
indicatorComponent={({ isSelected, item }: { isSelected?: boolean; item?: string }) => {
const val = typeof item === "string" ? item : "";
if (val.startsWith("__header_")) return <Text>{" "}</Text>;
return (
<Text color={isSelected ? colors.brand : colors.quaternary}>
{isSelected ? symbols.arrowRight : " "}{" "}
</Text>
);
}}
itemComponent={({ isSelected, label, value }: { isSelected?: boolean; label: string; value?: string }) => {
const val = typeof value === "string" ? value : "";
// Provider header
if (val.startsWith("__header_")) {
return <Text color={colors.dim}>{label}</Text>;
}
const isCurrent = val === currentModel;
return (
<Box>
<Text color={isSelected ? colors.brand : colors.text} bold={isSelected}>
{label}
</Text>
{isCurrent && <Text color={colors.success}> {symbols.dot} current</Text>}
</Box>
);
}}
/>
<Text color={colors.quaternary}> esc to cancel</Text>
</Box>
);
}