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:
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 設定",
|
||||||
|
|||||||
@@ -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 구성",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 配置",
|
||||||
|
|||||||
@@ -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 配置",
|
||||||
|
|||||||
Reference in New Issue
Block a user