opencode/packages/tui/src/component/dialog-model.tsx
opencode-agent[bot] 8f1db7d06d
fix(tui): align cli model picker behavior (#34571)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 09:36:38 -05:00

180 lines
6.1 KiB
TypeScript

import { createMemo, createSignal } from "solid-js"
import { useLocal } from "../context/local"
import { DialogSelect } from "../ui/dialog-select"
import { useDialog } from "../ui/dialog"
import { DialogIntegration } from "./dialog-integration"
import { DialogVariant } from "./dialog-variant"
import * as fuzzysort from "fuzzysort"
import { useConnected } from "./use-connected"
import { useData } from "../context/data"
export function DialogModel(props: { providerID?: string }) {
const local = useLocal()
const data = useData()
const dialog = useDialog()
const [query, setQuery] = createSignal("")
const connected = useConnected()
const providers = createMemo(() => new Map((data.location.provider.list() ?? []).map((item) => [item.id, item])))
const models = createMemo(() => data.location.model.list() ?? [])
const showExtra = createMemo(() => connected() && !props.providerID)
const options = createMemo(() => {
const needle = query().trim()
const showSections = showExtra() && needle.length === 0
const favorites = connected() ? local.model.favorite() : []
const recents = local.model.recent()
function toOptions(items: typeof favorites, category: string) {
if (!showSections) return []
return items.flatMap((item) => {
const model = models().find((model) => model.providerID === item.providerID && model.id === item.modelID)
if (!model) return []
const provider = providers().get(model.providerID)
return [
{
key: item,
value: { providerID: model.providerID, modelID: model.id },
title: model.name,
releaseDate: model.time.released,
description: provider?.name ?? model.providerID,
category,
footer: free(model) ? "Free" : undefined,
onSelect: () => {
onSelect(model.providerID, model.id)
},
},
]
})
}
const favoriteOptions = toOptions(favorites, "Favorites")
const recentOptions = toOptions(
recents.filter(
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
),
"Recent",
)
const modelOptions = sortModelOptions(
models()
.filter((model) => model.status !== "deprecated")
.filter((model) => (props.providerID ? model.providerID === props.providerID : true))
.map((model) => {
const provider = providers().get(model.providerID)
return {
value: { providerID: model.providerID, modelID: model.id },
providerID: model.providerID,
providerName: provider?.name ?? model.providerID,
title: model.name,
releaseDate: model.time.released,
description: favorites.some((item) => item.providerID === model.providerID && item.modelID === model.id)
? "(Favorite)"
: undefined,
category: connected() ? (provider?.name ?? model.providerID) : undefined,
footer: free(model) ? "Free" : undefined,
onSelect() {
onSelect(model.providerID, model.id)
},
}
})
.filter((option) => {
if (!showSections) return true
if (
favorites.some(
(item) => item.providerID === option.value.providerID && item.modelID === option.value.modelID,
)
)
return false
if (
recents.some((item) => item.providerID === option.value.providerID && item.modelID === option.value.modelID)
)
return false
return true
}),
)
if (needle) {
return fuzzysort.go(needle, modelOptions, { keys: ["title", "category"] }).map((item) => item.obj)
}
return [...favoriteOptions, ...recentOptions, ...modelOptions]
})
const provider = createMemo(() => (props.providerID ? providers().get(props.providerID) : undefined))
const title = createMemo(() => {
const value = provider()
if (!value) return "Select model"
return value.name
})
function onSelect(providerID: string, modelID: string) {
local.model.set({ providerID, modelID }, { recent: true })
const list = local.model.variant.list()
const cur = local.model.variant.current()
if (cur && list.includes(cur)) {
dialog.clear()
return
}
if (list.length > 0) {
dialog.replace(() => <DialogVariant />)
return
}
dialog.clear()
}
return (
<DialogSelect<ReturnType<typeof options>[number]["value"]>
options={options()}
actions={[
{
command: "model.dialog.provider",
title: connected() ? "Connect integration" : "View all integrations",
onTrigger() {
dialog.replace(() => (
<DialogIntegration
onConnected={(providerID) => dialog.replace(() => <DialogModel providerID={providerID} />)}
/>
))
},
},
{
command: "model.dialog.favorite",
title: "Favorite",
hidden: !connected(),
onTrigger: (option) => {
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
},
},
]}
onFilter={setQuery}
flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
/>
)
}
export function sortModelOptions<
T extends { providerID?: string; providerName?: string; releaseDate: string | number; title: string },
>(options: T[]) {
return options.toSorted((a, b) => {
const provider = Number(a.providerID !== "opencode") - Number(b.providerID !== "opencode")
if (provider !== 0) return provider
const name = (a.providerName ?? "").localeCompare(b.providerName ?? "")
if (name !== 0) return name
const release = Number(b.releaseDate) - Number(a.releaseDate)
if (release !== 0) return release
return a.title.localeCompare(b.title)
})
}
function free(model: { cost: Array<{ input: number }> }) {
return model.cost.length > 0 && model.cost.every((cost) => cost.input === 0)
}