Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/app/src/components/dialog-select-model-unpaid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof local.model.list>[number]) =>
item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)

let listRef: ListRef | undefined
const handleKey = (e: KeyboardEvent) => {
Expand All @@ -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) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={12}
value={<ModelTooltip model={item} latest={item.latest} free={isFree(item)} />}
>
{node}
</Tooltip>
)}
onSelect={(x) => {
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
Expand All @@ -48,7 +62,9 @@ export const DialogSelectModelUnpaid: Component = () => {
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Tag>Free</Tag>
<Show when={isFree(i)}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
Expand Down
16 changes: 15 additions & 1 deletion packages/app/src/components/dialog-select-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<typeof models>[number]) =>
item.provider.id === "opencode" && (!item.cost || item.cost.input === 0)

return (
<List
Expand All @@ -44,6 +48,16 @@ const ModelList: Component<{
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
itemWrapper={(item, node) => (
<Tooltip
class="w-full"
placement="right-start"
gutter={12}
value={<ModelTooltip model={item} latest={item.latest} free={isFree(item)} />}
>
{node}
</Tooltip>
)}
onSelect={(x) => {
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
Expand All @@ -54,7 +68,7 @@ const ModelList: Component<{
{(i) => (
<div class="w-full flex items-center gap-x-2 text-13-regular">
<span class="truncate">{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Show when={isFree(i)}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
Expand Down
68 changes: 68 additions & 0 deletions packages/app/src/components/model-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Show, type Component } from "solid-js"

type InputKey = "text" | "image" | "audio" | "video" | "pdf"
type InputMap = Record<InputKey, boolean>

type ModelInfo = {
id: string
name: string
provider: {
name: string
}
capabilities?: {
reasoning: boolean
input: InputMap
}
modalities?: {
input: Array<string>
}
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<string> = []
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<InputKey> = ["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 (
<div class="flex flex-col gap-1 py-1">
<div class="text-13-medium">{title()}</div>
<Show when={inputs()}>{(value) => <div class="text-12-regular">Allows: {value()}</div>}</Show>
<div class="text-12-regular">{reasoning()}</div>
<div class="text-12-regular">{context()}</div>
</div>
)
}
69 changes: 37 additions & 32 deletions packages/ui/src/components/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
activeIcon?: IconProps["name"]
filter?: string
search?: ListSearchProps | boolean
itemWrapper?: (item: T, node: JSX.Element) => JSX.Element
}

export interface ListRef {
Expand Down Expand Up @@ -221,39 +222,43 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
</Show>
<div data-slot="list-items">
<For each={group.items}>
{(item, i) => (
<button
data-slot="list-item"
data-key={props.key(item)}
data-active={props.key(item) === active()}
data-selected={item === props.current}
onClick={() => handleSelect(item, i())}
type="button"
onMouseMove={(event) => {
if (!moved(event)) return
setStore("mouseActive", true)
setActive(props.key(item))
}}
onMouseLeave={() => {
if (!store.mouseActive) return
setActive(null)
}}
>
{props.children(item)}
<Show when={item === props.current}>
<span data-slot="list-item-selected-icon">
<Icon name="check-small" />
</span>
</Show>
<Show when={props.activeIcon}>
{(icon) => (
<span data-slot="list-item-active-icon">
<Icon name={icon()} />
{(item, i) => {
const node = (
<button
data-slot="list-item"
data-key={props.key(item)}
data-active={props.key(item) === active()}
data-selected={item === props.current}
onClick={() => handleSelect(item, i())}
type="button"
onMouseMove={(event) => {
if (!moved(event)) return
setStore("mouseActive", true)
setActive(props.key(item))
}}
onMouseLeave={() => {
if (!store.mouseActive) return
setActive(null)
}}
>
{props.children(item)}
<Show when={item === props.current}>
<span data-slot="list-item-selected-icon">
<Icon name="check-small" />
</span>
)}
</Show>
</button>
)}
</Show>
<Show when={props.activeIcon}>
{(icon) => (
<span data-slot="list-item-active-icon">
<Icon name={icon()} />
</span>
)}
</Show>
</button>
)
if (props.itemWrapper) return props.itemWrapper(item, node)
return node
}}
</For>
</div>
</div>
Expand Down
Loading