feat(settings): add model selection combobox for OpenCode agent configuration

Replace plain text inputs for model and small_model with searchable
combobox dropdowns that list models from configured providers, while
still supporting custom text entry on blur.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-07 10:55:42 +08:00
parent a3d5335e7f
commit faf8ff0731
11 changed files with 152 additions and 28 deletions

View File

@@ -53,6 +53,16 @@ import {
} from "@/components/ui/select" } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxInput,
ComboboxItem,
ComboboxLabel,
ComboboxList,
} from "@/components/ui/combobox"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {
acpClearBinaryCache, acpClearBinaryCache,
@@ -932,6 +942,90 @@ const OPENCODE_PROVIDER_NPM_OPTIONS = [
}, },
] as const ] as const
interface OpenCodeModelOptionGroup {
providerId: string
label: string
models: { value: string; label: string }[]
}
function buildOpenCodeModelOptions(
config: OpenCodeConfigView | null
): OpenCodeModelOptionGroup[] {
if (!config) return []
const groups: OpenCodeModelOptionGroup[] = []
for (const providerId of config.providerIds) {
const provider = config.providers[providerId]
if (!provider || provider.modelIds.length === 0) continue
groups.push({
providerId,
label: provider.name || providerId,
models: provider.modelIds.map((modelId) => ({
value: `${providerId}/${modelId}`,
label: modelId,
})),
})
}
return groups
}
function OpenCodeModelCombobox({
value,
onValueChange,
groups,
placeholder,
}: {
value: string
onValueChange: (value: string) => void
groups: OpenCodeModelOptionGroup[]
placeholder: string
}) {
const inputRef = useRef<HTMLInputElement>(null)
const handleSelect = useCallback(
(next: string | null) => {
if (typeof next === "string" && next !== value) {
onValueChange(next)
}
},
[onValueChange, value]
)
const handleBlur = useCallback(() => {
const trimmed = (inputRef.current?.value ?? "").trim()
if (trimmed !== value) {
onValueChange(trimmed)
}
}, [onValueChange, value])
return (
<Combobox key={value} value={value} onValueChange={handleSelect}>
<ComboboxInput
ref={inputRef}
placeholder={placeholder}
onBlur={handleBlur}
showClear={false}
/>
<ComboboxContent>
<ComboboxList>
{groups.map((group) => (
<ComboboxGroup key={group.providerId}>
<ComboboxLabel>{group.label}</ComboboxLabel>
{group.models.map((model) => (
<ComboboxItem key={model.value} value={model.value}>
{model.value}
</ComboboxItem>
))}
</ComboboxGroup>
))}
<ComboboxEmpty>
{acpText("openCode.noMatchingModels", "No matching models")}
</ComboboxEmpty>
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}
function buildOpenCodeNpmOptions(currentValue: string): string[] { function buildOpenCodeNpmOptions(currentValue: string): string[] {
const next = new Set<string>( const next = new Set<string>(
OPENCODE_PROVIDER_NPM_OPTIONS.map((v) => v.value) OPENCODE_PROVIDER_NPM_OPTIONS.map((v) => v.value)
@@ -3207,6 +3301,10 @@ export function AcpAgentSettings() {
selectedConfigText, selectedConfigText,
selectedOpenCodeAuthJsonText, selectedOpenCodeAuthJsonText,
]) ])
const openCodeModelOptions = useMemo(
() => buildOpenCodeModelOptions(selectedOpenCodeConfig),
[selectedOpenCodeConfig]
)
const selectedChecks = useMemo(() => { const selectedChecks = useMemo(() => {
if (!selectedAgent || !locale) return [] if (!selectedAgent || !locale) return []
return getAgentChecks(selectedAgent, selectedCurrent) return getAgentChecks(selectedAgent, selectedCurrent)
@@ -5533,32 +5631,28 @@ supports_websockets = true`}
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground"> <label className="text-[11px] text-muted-foreground">
model {t("openCode.mainModel")}
</label> </label>
<Input <OpenCodeModelCombobox
value={selectedOpenCodeConfig?.model ?? ""} value={selectedOpenCodeConfig?.model ?? ""}
onChange={(event) => { onValueChange={(v) =>
handleOpenCodeFieldChange( handleOpenCodeFieldChange("model", v)
"model", }
event.target.value groups={openCodeModelOptions}
) placeholder="provider/model-id"
}}
placeholder="google/gemini-3-pro-preview"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground"> <label className="text-[11px] text-muted-foreground">
small_model {t("openCode.smallModel")}
</label> </label>
<Input <OpenCodeModelCombobox
value={selectedOpenCodeConfig?.smallModel ?? ""} value={selectedOpenCodeConfig?.smallModel ?? ""}
onChange={(event) => { onValueChange={(v) =>
handleOpenCodeFieldChange( handleOpenCodeFieldChange("small_model", v)
"small_model", }
event.target.value groups={openCodeModelOptions}
) placeholder="provider/model-id"
}}
placeholder="google/gemini-3-flash-preview"
/> />
</div> </div>
</div> </div>

View File

@@ -555,7 +555,10 @@
"modelId": "معرّف النموذج", "modelId": "معرّف النموذج",
"modelName": "اسم النموذج", "modelName": "اسم النموذج",
"deleteModel": "حذف النموذج {modelId}", "deleteModel": "حذف النموذج {modelId}",
"nativeJsonConfig": "إعداد JSON الأصلي لـ OpenCode" "nativeJsonConfig": "إعداد JSON الأصلي لـ OpenCode",
"mainModel": "النموذج الرئيسي",
"smallModel": "النموذج الصغير",
"noMatchingModels": "لا توجد نماذج مطابقة"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "إعداد Gateway", "gatewayConfig": "إعداد Gateway",

View File

@@ -555,7 +555,10 @@
"modelId": "Modell-ID", "modelId": "Modell-ID",
"modelName": "Modellname", "modelName": "Modellname",
"deleteModel": "Modell {modelId} löschen", "deleteModel": "Modell {modelId} löschen",
"nativeJsonConfig": "OpenCode Native JSON-Konfiguration" "nativeJsonConfig": "OpenCode Native JSON-Konfiguration",
"mainModel": "Hauptmodell",
"smallModel": "Kleines Modell",
"noMatchingModels": "Keine passenden Modelle"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Gateway-Konfiguration", "gatewayConfig": "Gateway-Konfiguration",

View File

@@ -555,7 +555,10 @@
"modelId": "Model ID", "modelId": "Model ID",
"modelName": "Model Name", "modelName": "Model Name",
"deleteModel": "Delete model {modelId}", "deleteModel": "Delete model {modelId}",
"nativeJsonConfig": "OpenCode Native JSON Config" "nativeJsonConfig": "OpenCode Native JSON Config",
"mainModel": "Main Model",
"smallModel": "Small Model",
"noMatchingModels": "No matching models"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Gateway Config", "gatewayConfig": "Gateway Config",

View File

@@ -555,7 +555,10 @@
"modelId": "ID de modelo", "modelId": "ID de modelo",
"modelName": "Nombre del modelo", "modelName": "Nombre del modelo",
"deleteModel": "Eliminar modelo {modelId}", "deleteModel": "Eliminar modelo {modelId}",
"nativeJsonConfig": "Configuración JSON nativa de OpenCode" "nativeJsonConfig": "Configuración JSON nativa de OpenCode",
"mainModel": "Modelo principal",
"smallModel": "Modelo pequeño",
"noMatchingModels": "No hay modelos coincidentes"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Configuración de Gateway", "gatewayConfig": "Configuración de Gateway",

View File

@@ -555,7 +555,10 @@
"modelId": "ID du modèle", "modelId": "ID du modèle",
"modelName": "Nom du modèle", "modelName": "Nom du modèle",
"deleteModel": "Supprimer le modèle {modelId}", "deleteModel": "Supprimer le modèle {modelId}",
"nativeJsonConfig": "Configuration JSON native OpenCode" "nativeJsonConfig": "Configuration JSON native OpenCode",
"mainModel": "Modèle principal",
"smallModel": "Petit modèle",
"noMatchingModels": "Aucun modèle correspondant"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Configuration Gateway", "gatewayConfig": "Configuration Gateway",

View File

@@ -555,7 +555,10 @@
"modelId": "モデル ID", "modelId": "モデル ID",
"modelName": "モデル名", "modelName": "モデル名",
"deleteModel": "モデル {modelId} を削除", "deleteModel": "モデル {modelId} を削除",
"nativeJsonConfig": "OpenCode ネイティブ JSON 設定" "nativeJsonConfig": "OpenCode ネイティブ JSON 設定",
"mainModel": "メインモデル",
"smallModel": "スモールモデル",
"noMatchingModels": "一致するモデルがありません"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Gateway 設定", "gatewayConfig": "Gateway 設定",

View File

@@ -555,7 +555,10 @@
"modelId": "모델 ID", "modelId": "모델 ID",
"modelName": "모델 이름", "modelName": "모델 이름",
"deleteModel": "모델 {modelId} 삭제", "deleteModel": "모델 {modelId} 삭제",
"nativeJsonConfig": "OpenCode 네이티브 JSON 구성" "nativeJsonConfig": "OpenCode 네이티브 JSON 구성",
"mainModel": "메인 모델",
"smallModel": "스몰 모델",
"noMatchingModels": "일치하는 모델 없음"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Gateway 구성", "gatewayConfig": "Gateway 구성",

View File

@@ -555,7 +555,10 @@
"modelId": "ID do modelo", "modelId": "ID do modelo",
"modelName": "Nome do modelo", "modelName": "Nome do modelo",
"deleteModel": "Excluir modelo {modelId}", "deleteModel": "Excluir modelo {modelId}",
"nativeJsonConfig": "Configuração JSON nativa do OpenCode" "nativeJsonConfig": "Configuração JSON nativa do OpenCode",
"mainModel": "Modelo principal",
"smallModel": "Modelo pequeno",
"noMatchingModels": "Nenhum modelo correspondente"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Configuração de Gateway", "gatewayConfig": "Configuração de Gateway",

View File

@@ -555,7 +555,10 @@
"modelId": "模型 ID", "modelId": "模型 ID",
"modelName": "模型名称", "modelName": "模型名称",
"deleteModel": "删除模型 {modelId}", "deleteModel": "删除模型 {modelId}",
"nativeJsonConfig": "OpenCode 原生 JSON 配置" "nativeJsonConfig": "OpenCode 原生 JSON 配置",
"mainModel": "主模型",
"smallModel": "小模型",
"noMatchingModels": "没有匹配的模型"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Gateway 配置", "gatewayConfig": "Gateway 配置",

View File

@@ -555,7 +555,10 @@
"modelId": "模型 ID", "modelId": "模型 ID",
"modelName": "模型名稱", "modelName": "模型名稱",
"deleteModel": "刪除模型 {modelId}", "deleteModel": "刪除模型 {modelId}",
"nativeJsonConfig": "OpenCode 原生 JSON 配置" "nativeJsonConfig": "OpenCode 原生 JSON 配置",
"mainModel": "主模型",
"smallModel": "小模型",
"noMatchingModels": "沒有匹配的模型"
}, },
"openClaw": { "openClaw": {
"gatewayConfig": "Gateway 配置", "gatewayConfig": "Gateway 配置",