summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2026-02-10 10:13:37 -0500
committerGitHub <[email protected]>2026-02-10 15:13:37 +0000
commit27fa9dc843d002b67113a1a01727aae61563427e (patch)
treeec2ba4627f0b3b53c88d69864f075cf2e10b1557
parent1e03a55acdb1e80b747d0604d698f4cbef97ace1 (diff)
downloadopencode-27fa9dc843d002b67113a1a01727aae61563427e.tar.gz
opencode-27fa9dc843d002b67113a1a01727aae61563427e.zip
refactor: clean up dialog-model.tsx per code review (#12983)
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx195
-rw-r--r--packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx20
2 files changed, 78 insertions, 137 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index 4ad92eeb8..c30b8d12a 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -2,7 +2,7 @@ import { createMemo, createSignal } from "solid-js"
import { useLocal } from "@tui/context/local"
import { useSync } from "@tui/context/sync"
import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
-import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
+import { DialogSelect } from "@tui/ui/dialog-select"
import { useDialog } from "@tui/ui/dialog"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
import { useKeybind } from "../context/keybind"
@@ -20,96 +20,51 @@ export function DialogModel(props: { providerID?: string }) {
const sync = useSync()
const dialog = useDialog()
const keybind = useKeybind()
- const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
const [query, setQuery] = createSignal("")
const connected = useConnected()
const providers = createDialogProviderOptions()
- const showExtra = createMemo(() => {
- if (!connected()) return false
- if (props.providerID) return false
- return true
- })
+ const showExtra = createMemo(() => connected() && !props.providerID)
const options = createMemo(() => {
- const q = query()
- const needle = q.trim()
+ const needle = query().trim()
const showSections = showExtra() && needle.length === 0
const favorites = connected() ? local.model.favorite() : []
const recents = local.model.recent()
- const recentList = showSections
- ? recents.filter(
- (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
- )
- : []
-
- const favoriteOptions = showSections
- ? favorites.flatMap((item) => {
- const provider = sync.data.provider.find((x) => x.id === item.providerID)
- if (!provider) return []
- const model = provider.models[item.modelID]
- if (!model) return []
- return [
- {
- key: item,
- value: {
- providerID: provider.id,
- modelID: model.id,
- },
- title: model.name ?? item.modelID,
- description: provider.name,
- category: "Favorites",
- disabled: provider.id === "opencode" && model.id.includes("-nano"),
- footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
- onSelect: () => {
- dialog.clear()
- local.model.set(
- {
- providerID: provider.id,
- modelID: model.id,
- },
- { recent: true },
- )
- },
+ function toOptions(items: typeof favorites, category: string) {
+ if (!showSections) return []
+ return items.flatMap((item) => {
+ const provider = sync.data.provider.find((x) => x.id === item.providerID)
+ if (!provider) return []
+ const model = provider.models[item.modelID]
+ if (!model) return []
+ return [
+ {
+ key: item,
+ value: { providerID: provider.id, modelID: model.id },
+ title: model.name ?? item.modelID,
+ description: provider.name,
+ category,
+ disabled: provider.id === "opencode" && model.id.includes("-nano"),
+ footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
+ onSelect: () => {
+ dialog.clear()
+ local.model.set({ providerID: provider.id, modelID: model.id }, { recent: true })
},
- ]
- })
- : []
+ },
+ ]
+ })
+ }
- const recentOptions = showSections
- ? recentList.flatMap((item) => {
- const provider = sync.data.provider.find((x) => x.id === item.providerID)
- if (!provider) return []
- const model = provider.models[item.modelID]
- if (!model) return []
- return [
- {
- key: item,
- value: {
- providerID: provider.id,
- modelID: model.id,
- },
- title: model.name ?? item.modelID,
- description: provider.name,
- category: "Recent",
- disabled: provider.id === "opencode" && model.id.includes("-nano"),
- footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
- onSelect: () => {
- dialog.clear()
- local.model.set(
- {
- providerID: provider.id,
- modelID: model.id,
- },
- { recent: true },
- )
- },
- },
- ]
- })
- : []
+ const favoriteOptions = toOptions(favorites, "Favorites")
+ const recentOptions = toOptions(
+ recents.filter(
+ (item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
+ ),
+ "Recent",
+ )
const providerOptions = pipe(
sync.data.provider,
@@ -123,45 +78,26 @@ export function DialogModel(props: { providerID?: string }) {
entries(),
filter(([_, info]) => info.status !== "deprecated"),
filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
- map(([model, info]) => {
- const value = {
- providerID: provider.id,
- modelID: model,
- }
- return {
- value,
- title: info.name ?? model,
- description: favorites.some(
- (item) => item.providerID === value.providerID && item.modelID === value.modelID,
- )
- ? "(Favorite)"
- : undefined,
- category: connected() ? provider.name : undefined,
- disabled: provider.id === "opencode" && model.includes("-nano"),
- footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
- onSelect() {
- dialog.clear()
- local.model.set(
- {
- providerID: provider.id,
- modelID: model,
- },
- { recent: true },
- )
- },
- }
- }),
+ map(([model, info]) => ({
+ value: { providerID: provider.id, modelID: model },
+ title: info.name ?? model,
+ description: favorites.some((item) => item.providerID === provider.id && item.modelID === model)
+ ? "(Favorite)"
+ : undefined,
+ category: connected() ? provider.name : undefined,
+ disabled: provider.id === "opencode" && model.includes("-nano"),
+ footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
+ onSelect() {
+ dialog.clear()
+ local.model.set({ providerID: provider.id, modelID: model }, { recent: true })
+ },
+ })),
filter((x) => {
if (!showSections) return true
- const value = x.value
- const inFavorites = favorites.some(
- (item) => item.providerID === value.providerID && item.modelID === value.modelID,
- )
- if (inFavorites) return false
- const inRecents = recents.some(
- (item) => item.providerID === value.providerID && item.modelID === value.modelID,
- )
- if (inRecents) return false
+ if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
+ return false
+ if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID))
+ return false
return true
}),
sortBy(
@@ -175,21 +111,19 @@ export function DialogModel(props: { providerID?: string }) {
const popularProviders = !connected()
? pipe(
providers(),
- map((option) => {
- return {
- ...option,
- category: "Popular providers",
- }
- }),
+ map((option) => ({
+ ...option,
+ category: "Popular providers",
+ })),
take(6),
)
: []
- // Search shows a single merged list (favorites inline)
if (needle) {
- const filteredProviders = fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
- const filteredPopular = fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj)
- return [...filteredProviders, ...filteredPopular]
+ return [
+ ...fuzzysort.go(needle, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj),
+ ...fuzzysort.go(needle, popularProviders, { keys: ["title"] }).map((x) => x.obj),
+ ]
}
return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
@@ -199,13 +133,11 @@ export function DialogModel(props: { providerID?: string }) {
props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null,
)
- const title = createMemo(() => {
- if (provider()) return provider()!.name
- return "Select model"
- })
+ const title = createMemo(() => provider()?.name ?? "Select model")
return (
- <DialogSelect
+ <DialogSelect<ReturnType<typeof options>[number]["value"]>
+ options={options()}
keybind={[
{
keybind: keybind.all.model_provider_list?.[0],
@@ -223,12 +155,11 @@ export function DialogModel(props: { providerID?: string }) {
},
},
]}
- ref={setRef}
onFilter={setQuery}
+ flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
- options={options()}
/>
)
}
diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
index 490a10072..151f73cf7 100644
--- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
+++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx
@@ -15,6 +15,7 @@ export interface DialogSelectProps<T> {
title: string
placeholder?: string
options: DialogSelectOption<T>[]
+ flat?: boolean
ref?: (ref: DialogSelectRef<T>) => void
onMove?: (option: DialogSelectOption<T>) => void
onFilter?: (query: string) => void
@@ -100,7 +101,10 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
setStore("input", "keyboard")
})
- const grouped = createMemo(() => {
+ const flatten = createMemo(() => props.flat && store.filter.length > 0)
+
+ const grouped = createMemo<[string, DialogSelectOption<T>[]][]>(() => {
+ if (flatten()) return [["", filtered()]]
const result = pipe(
filtered(),
groupBy((x) => x.category ?? ""),
@@ -117,10 +121,16 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
)
})
+ const rows = createMemo(() => {
+ const headers = grouped().reduce((acc, [category], i) => {
+ if (!category) return acc
+ return acc + (i > 0 ? 2 : 1)
+ }, 0)
+ return flat().length + headers
+ })
+
const dimensions = useTerminalDimensions()
- const height = createMemo(() =>
- Math.min(flat().length + grouped().length * 2 - 1, Math.floor(dimensions().height / 2) - 6),
- )
+ const height = createMemo(() => Math.min(rows(), Math.floor(dimensions().height / 2) - 6))
const selected = createMemo(() => flat()[store.selected])
@@ -311,7 +321,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
>
<Option
title={option.title}
- footer={option.footer}
+ footer={flatten() ? (option.category ?? option.footer) : option.footer}
description={option.description !== category ? option.description : undefined}
active={active()}
current={current()}