From 1bebe26454887d6274661d15ff44af490c0fb731 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 19 Nov 2025 21:45:16 -0500 Subject: [PATCH] tui: streamline model selection and provider discovery - Show unconnected providers in the list so users can find and connect new services without leaving the workflow. - Group recently used models at the top for faster access to common configurations. - Add a 'Free' badge to opencode models to clearly identify no-cost options. - Adjust dialog sizing to improve readability of the expanded model list. --- .../cli/cmd/tui/component/dialog-model.tsx | 67 ++++++++++++++----- .../opencode/src/cli/cmd/tui/context/sdk.tsx | 1 - .../opencode/src/cli/cmd/tui/context/sync.tsx | 12 ++++ .../src/cli/cmd/tui/ui/dialog-prompt.tsx | 2 +- 4 files changed, 64 insertions(+), 18 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 bcd1d98d5..c330fdf9a 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, isDeepEqual, sortBy } from "remeda" -import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" +import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select" import { useDialog } from "@tui/ui/dialog" import { useTheme } from "../context/theme" @@ -10,6 +10,15 @@ function Free() { const { theme } = useTheme() return Free } +const PROVIDER_PRIORITY: Record = { + opencode: 0, + anthropic: 1, + "github-copilot": 2, + openai: 3, + google: 4, + openrouter: 5, + vercel: 6, +} export function DialogModel() { const local = useLocal() @@ -17,9 +26,15 @@ export function DialogModel() { const dialog = useDialog() const [ref, setRef] = createSignal>() + const connected = createMemo(() => + sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)), + ) + + const showRecent = createMemo(() => !ref()?.filter && local.model.recent().length > 0 && connected()) + const options = createMemo(() => { return [ - ...(!ref()?.filter + ...(showRecent() ? local.model.recent().flatMap((item) => { const provider = sync.data.provider.find((x) => x.id === item.providerID)! if (!provider) return [] @@ -36,6 +51,16 @@ export function DialogModel() { description: provider.name, category: "Recent", footer: model.cost?.input === 0 && provider.id === "opencode" ? : undefined, + onSelect: () => { + dialog.clear() + local.model.set( + { + providerID: provider.id, + modelID: model.id, + }, + { recent: true }, + ) + }, }, ] }) @@ -57,27 +82,37 @@ export function DialogModel() { }, title: info.name ?? model, description: provider.name, - category: provider.name, + category: connected() ? provider.name : undefined, footer: info.cost?.input === 0 && provider.id === "opencode" ? : undefined, + onSelect() { + dialog.clear() + local.model.set( + { + providerID: provider.id, + modelID: model, + }, + { recent: true }, + ) + }, })), - filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))), + filter((x) => !showRecent() || !local.model.recent().find((y) => isDeepEqual(y, x.value))), sortBy((x) => x.title), ), ), ), + ...(!connected() + ? pipe( + sync.data.provider_next.all, + map((provider) => ({ + title: provider.name, + category: "Connect a provider", + value: provider.id, + })), + sortBy((x) => PROVIDER_PRIORITY[x.value] ?? 99), + ) + : []), ] }) - return ( - { - dialog.clear() - local.model.set(option.value, { recent: true }) - }} - /> - ) + return } diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 8b7564eb5..41f69f0d9 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -18,7 +18,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ sdk.event.subscribe().then(async (events) => { for await (const event of events.stream) { - console.log("event", event.type) emitter.emit(event.type, event) } }) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 74fea2fd0..76dea5af9 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -12,6 +12,7 @@ import type { McpStatus, FormatterStatus, SessionStatus, + ProviderListResponse, } from "@opencode-ai/sdk" import { createStore, produce, reconcile } from "solid-js/store" import { useSDK } from "@tui/context/sdk" @@ -28,6 +29,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ status: "loading" | "partial" | "complete" provider: Provider[] provider_default: Record + provider_next: ProviderListResponse agent: Agent[] command: Command[] permission: { @@ -56,6 +58,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } formatter: FormatterStatus[] }>({ + provider_next: { + all: [], + default: {}, + connected: [], + }, config: {}, status: "loading", agent: [], @@ -241,6 +248,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ setStore("provider_default", x.data!.default) }) }), + sdk.client.provider.list({ throwOnError: true }).then((x) => { + batch(() => { + setStore("provider_next", x.data!) + }) + }), sdk.client.app.agents({ throwOnError: true }).then((x) => setStore("agent", x.data ?? [])), sdk.client.config.get({ throwOnError: true }).then((x) => setStore("config", x.data!)), ]) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx index eaf427aff..58798accd 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx @@ -24,7 +24,7 @@ export function DialogPrompt(props: DialogPromptProps) { }) onMount(() => { - dialog.setSize("large") + dialog.setSize("medium") setTimeout(() => { textarea.focus() }, 1)