From f451f337c9d4cc9a612fb78a46c340e73791c941 Mon Sep 17 00:00:00 2001 From: Ronan Kearns Date: Tue, 20 Jan 2026 15:53:16 -0500 Subject: [PATCH 1/2] feat(app): add model tooltips to chooser --- .../components/dialog-select-model-unpaid.tsx | 7 ++ .../src/components/dialog-select-model.tsx | 7 ++ packages/app/src/components/model-tooltip.tsx | 55 +++++++++++++++ packages/ui/src/components/list.tsx | 69 ++++++++++--------- 4 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 packages/app/src/components/model-tooltip.tsx diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 24ec8092deb..b0a92e83944 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -5,11 +5,13 @@ import type { IconName } from "@opencode-ai/ui/icons/provider" import { List, type ListRef } from "@opencode-ai/ui/list" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Tag } from "@opencode-ai/ui/tag" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Component, onCleanup, onMount, Show } from "solid-js" import { useLocal } from "@/context/local" import { popularProviders, useProviders } from "@/hooks/use-providers" import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" +import { ModelTooltip } from "./model-tooltip" export const DialogSelectModelUnpaid: Component = () => { const local = useLocal() @@ -38,6 +40,11 @@ export const DialogSelectModelUnpaid: Component = () => { items={local.model.list} current={local.model.current()} key={(x) => `${x.provider.id}:${x.id}`} + itemWrapper={(item, node) => ( + }> + {node} + + )} onSelect={(x) => { local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index d54f9369af1..84b58d04cd6 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -7,8 +7,10 @@ import { Button } from "@opencode-ai/ui/button" import { Tag } from "@opencode-ai/ui/tag" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogManageModels } from "./dialog-manage-models" +import { ModelTooltip } from "./model-tooltip" const ModelList: Component<{ provider?: string @@ -44,6 +46,11 @@ const ModelList: Component<{ if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1 return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider) }} + itemWrapper={(item, node) => ( + }> + {node} + + )} onSelect={(x) => { local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, diff --git a/packages/app/src/components/model-tooltip.tsx b/packages/app/src/components/model-tooltip.tsx new file mode 100644 index 00000000000..a327ce281cb --- /dev/null +++ b/packages/app/src/components/model-tooltip.tsx @@ -0,0 +1,55 @@ +import { Show, type Component } from "solid-js" + +type ModelInfo = { + id: string + name: string + provider: { + name: string + } + capabilities: { + reasoning: boolean + input: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + } + limit: { + context: number + } +} + +function sourceName(model: ModelInfo) { + const value = `${model.id} ${model.name}`.toLowerCase() + + if (/claude|anthropic/.test(value)) return "Anthropic" + if (/gpt|o[1-4]|codex|openai/.test(value)) return "OpenAI" + if (/gemini|palm|bard|google/.test(value)) return "Google" + if (/grok|xai/.test(value)) return "xAI" + if (/llama|meta/.test(value)) return "Meta" + + return model.provider.name +} + +export const ModelTooltip: Component<{ model: ModelInfo }> = (props) => { + const title = () => `${sourceName(props.model)} ${props.model.name}` + const inputs = () => { + const input = props.model.capabilities.input + const order: Array = ["text", "image", "audio", "video", "pdf"] + const entries = order.filter((key) => input[key]) + return entries.length ? entries.join(", ") : undefined + } + const reasoning = () => (props.model.capabilities.reasoning ? "Allows reasoning" : "No reasoning") + const context = () => `Context limit ${props.model.limit.context.toLocaleString()}` + + return ( +
+
{title()}
+ {(value) =>
Allows: {value()}
}
+
{reasoning()}
+
{context()}
+
+ ) +} diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx index 6929f6b7347..e5e531945df 100644 --- a/packages/ui/src/components/list.tsx +++ b/packages/ui/src/components/list.tsx @@ -21,6 +21,7 @@ export interface ListProps extends FilteredListProps { activeIcon?: IconProps["name"] filter?: string search?: ListSearchProps | boolean + itemWrapper?: (item: T, node: JSX.Element) => JSX.Element } export interface ListRef { @@ -221,39 +222,43 @@ export function List(props: ListProps & { ref?: (ref: ListRef) => void })
- {(item, i) => ( - - )} + + + {(icon) => ( + + + + )} + + + ) + if (props.itemWrapper) return props.itemWrapper(item, node) + return node + }}
From 819c7065c0845d24dd6fd3b234a6c0177bdb0def Mon Sep 17 00:00:00 2001 From: Ronan Kearns Date: Tue, 20 Jan 2026 16:28:24 -0500 Subject: [PATCH 2/2] feat(app): surface latest/free status in model tooltips --- .../components/dialog-select-model-unpaid.tsx | 13 +++++- .../src/components/dialog-select-model.tsx | 11 ++++- packages/app/src/components/model-tooltip.tsx | 43 ++++++++++++------- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index b0a92e83944..4d2b2e8ff64 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -17,6 +17,8 @@ export const DialogSelectModelUnpaid: Component = () => { const local = useLocal() const dialog = useDialog() const providers = useProviders() + const isFree = (item: ReturnType[number]) => + item.provider.id === "opencode" && (!item.cost || item.cost.input === 0) let listRef: ListRef | undefined const handleKey = (e: KeyboardEvent) => { @@ -41,7 +43,12 @@ export const DialogSelectModelUnpaid: Component = () => { current={local.model.current()} key={(x) => `${x.provider.id}:${x.id}`} itemWrapper={(item, node) => ( - }> + } + > {node} )} @@ -55,7 +62,9 @@ export const DialogSelectModelUnpaid: Component = () => { {(i) => (
{i.name} - Free + + Free + Latest diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 84b58d04cd6..369242eca68 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -25,6 +25,8 @@ const ModelList: Component<{ .filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id })) .filter((m) => (props.provider ? m.provider.id === props.provider : true)), ) + const isFree = (item: ReturnType[number]) => + item.provider.id === "opencode" && (!item.cost || item.cost.input === 0) return ( ( - }> + } + > {node} )} @@ -61,7 +68,7 @@ const ModelList: Component<{ {(i) => (
{i.name} - + Free diff --git a/packages/app/src/components/model-tooltip.tsx b/packages/app/src/components/model-tooltip.tsx index a327ce281cb..02ff04d388d 100644 --- a/packages/app/src/components/model-tooltip.tsx +++ b/packages/app/src/components/model-tooltip.tsx @@ -1,21 +1,22 @@ import { Show, type Component } from "solid-js" +type InputKey = "text" | "image" | "audio" | "video" | "pdf" +type InputMap = Record + type ModelInfo = { id: string name: string provider: { name: string } - capabilities: { + capabilities?: { reasoning: boolean - input: { - text: boolean - audio: boolean - image: boolean - video: boolean - pdf: boolean - } + input: InputMap + } + modalities?: { + input: Array } + reasoning?: boolean limit: { context: number } @@ -33,15 +34,27 @@ function sourceName(model: ModelInfo) { return model.provider.name } -export const ModelTooltip: Component<{ model: ModelInfo }> = (props) => { - const title = () => `${sourceName(props.model)} ${props.model.name}` +export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?: boolean }> = (props) => { + const title = () => { + const tags: Array = [] + if (props.latest) tags.push("Latest") + if (props.free) tags.push("Free") + const suffix = tags.length ? ` (${tags.join(", ")})` : "" + return `${sourceName(props.model)} ${props.model.name}${suffix}` + } const inputs = () => { - const input = props.model.capabilities.input - const order: Array = ["text", "image", "audio", "video", "pdf"] - const entries = order.filter((key) => input[key]) - return entries.length ? entries.join(", ") : undefined + if (props.model.capabilities) { + const input = props.model.capabilities.input + const order: Array = ["text", "image", "audio", "video", "pdf"] + const entries = order.filter((key) => input[key]) + return entries.length ? entries.join(", ") : undefined + } + return props.model.modalities?.input?.join(", ") + } + const reasoning = () => { + if (props.model.capabilities) return props.model.capabilities.reasoning ? "Allows reasoning" : "No reasoning" + return props.model.reasoning ? "Allows reasoning" : "No reasoning" } - const reasoning = () => (props.model.capabilities.reasoning ? "Allows reasoning" : "No reasoning") const context = () => `Context limit ${props.model.limit.context.toLocaleString()}` return (