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"
import { Switch } from "@/components/ui/switch"
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 {
acpClearBinaryCache,
@@ -932,6 +942,90 @@ const OPENCODE_PROVIDER_NPM_OPTIONS = [
},
] 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[] {
const next = new Set<string>(
OPENCODE_PROVIDER_NPM_OPTIONS.map((v) => v.value)
@@ -3207,6 +3301,10 @@ export function AcpAgentSettings() {
selectedConfigText,
selectedOpenCodeAuthJsonText,
])
const openCodeModelOptions = useMemo(
() => buildOpenCodeModelOptions(selectedOpenCodeConfig),
[selectedOpenCodeConfig]
)
const selectedChecks = useMemo(() => {
if (!selectedAgent || !locale) return []
return getAgentChecks(selectedAgent, selectedCurrent)
@@ -5533,32 +5631,28 @@ supports_websockets = true`}
<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
model
{t("openCode.mainModel")}
</label>
<Input
<OpenCodeModelCombobox
value={selectedOpenCodeConfig?.model ?? ""}
onChange={(event) => {
handleOpenCodeFieldChange(
"model",
event.target.value
)
}}
placeholder="google/gemini-3-pro-preview"
onValueChange={(v) =>
handleOpenCodeFieldChange("model", v)
}
groups={openCodeModelOptions}
placeholder="provider/model-id"
/>
</div>
<div className="space-y-1.5">
<label className="text-[11px] text-muted-foreground">
small_model
{t("openCode.smallModel")}
</label>
<Input
<OpenCodeModelCombobox
value={selectedOpenCodeConfig?.smallModel ?? ""}
onChange={(event) => {
handleOpenCodeFieldChange(
"small_model",
event.target.value
)
}}
placeholder="google/gemini-3-flash-preview"
onValueChange={(v) =>
handleOpenCodeFieldChange("small_model", v)
}
groups={openCodeModelOptions}
placeholder="provider/model-id"
/>
</div>
</div>