diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 24ec8092deb..4d2b2e8ff64 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -5,16 +5,20 @@ 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() 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) => { @@ -38,6 +42,16 @@ 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, @@ -48,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 d54f9369af1..369242eca68 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 @@ -23,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} + + )} onSelect={(x) => { local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true, @@ -54,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 new file mode 100644 index 00000000000..02ff04d388d --- /dev/null +++ b/packages/app/src/components/model-tooltip.tsx @@ -0,0 +1,68 @@ +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?: { + reasoning: boolean + input: InputMap + } + modalities?: { + input: Array + } + reasoning?: 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; 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 = () => { + 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 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 2815805adf0..6f10fdbf188 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 + }}