summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/dialog-select-model.tsx111
-rw-r--r--packages/app/src/components/prompt-input.tsx36
2 files changed, 92 insertions, 55 deletions
diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx
index 54783386a..1130e2234 100644
--- a/packages/app/src/components/dialog-select-model.tsx
+++ b/packages/app/src/components/dialog-select-model.tsx
@@ -1,4 +1,5 @@
-import { Component, createMemo, Show } from "solid-js"
+import { Popover as Kobalte } from "@kobalte/core/popover"
+import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
@@ -9,9 +10,11 @@ import { List } from "@opencode-ai/ui/list"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogManageModels } from "./dialog-manage-models"
-export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
+const ModelList: Component<{
+ provider?: string
+ onSelect: () => void
+}> = (props) => {
const local = useLocal()
- const dialog = useDialog()
const models = createMemo(() =>
local.model
@@ -21,6 +24,70 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
)
return (
+ <List
+ class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 p-1"
+ search={{ placeholder: "Search models", autofocus: true }}
+ emptyMessage="No model results"
+ key={(x) => `${x.provider.id}:${x.id}`}
+ items={models}
+ current={local.model.current()}
+ filterKeys={["provider.name", "name", "id"]}
+ sortBy={(a, b) => a.name.localeCompare(b.name)}
+ groupBy={(x) => x.provider.name}
+ sortGroupsBy={(a, b) => {
+ if (a.category === "Recent" && b.category !== "Recent") return -1
+ if (b.category === "Recent" && a.category !== "Recent") return 1
+ const aProvider = a.items[0].provider.id
+ const bProvider = b.items[0].provider.id
+ if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
+ if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
+ return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
+ }}
+ onSelect={(x) => {
+ local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
+ recent: true,
+ })
+ props.onSelect()
+ }}
+ >
+ {(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)}>
+ <Tag>Free</Tag>
+ </Show>
+ <Show when={i.latest}>
+ <Tag>Latest</Tag>
+ </Show>
+ </div>
+ )}
+ </List>
+ )
+}
+
+export const ModelSelectorPopover: Component<{
+ provider?: string
+ children: JSX.Element
+}> = (props) => {
+ const [open, setOpen] = createSignal(false)
+
+ return (
+ <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
+ <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
+ <Kobalte.Portal>
+ <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none">
+ <Kobalte.Title class="sr-only">Select model</Kobalte.Title>
+ <ModelList provider={props.provider} onSelect={() => setOpen(false)} />
+ </Kobalte.Content>
+ </Kobalte.Portal>
+ </Kobalte>
+ )
+}
+
+export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
+ const dialog = useDialog()
+
+ return (
<Dialog
title="Select model"
action={
@@ -34,43 +101,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
</Button>
}
>
- <List
- search={{ placeholder: "Search models", autofocus: true }}
- emptyMessage="No model results"
- key={(x) => `${x.provider.id}:${x.id}`}
- items={models}
- current={local.model.current()}
- filterKeys={["provider.name", "name", "id"]}
- sortBy={(a, b) => a.name.localeCompare(b.name)}
- groupBy={(x) => x.provider.name}
- sortGroupsBy={(a, b) => {
- if (a.category === "Recent" && b.category !== "Recent") return -1
- if (b.category === "Recent" && a.category !== "Recent") return 1
- const aProvider = a.items[0].provider.id
- const bProvider = b.items[0].provider.id
- if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
- if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
- return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
- }}
- onSelect={(x) => {
- local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
- recent: true,
- })
- dialog.close()
- }}
- >
- {(i) => (
- <div class="w-full flex items-center gap-x-3">
- <span>{i.name}</span>
- <Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
- <Tag>Free</Tag>
- </Show>
- <Show when={i.latest}>
- <Tag>Latest</Tag>
- </Show>
- </div>
- )}
- </List>
+ <ModelList provider={props.provider} onSelect={() => dialog.close()} />
<Button
variant="ghost"
class="ml-3 mt-5 mb-6 text-text-base self-start"
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index a45a6d587..ab2a017ca 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -16,7 +16,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { useDialog } from "@opencode-ai/ui/context/dialog"
-import { DialogSelectModel } from "@/components/dialog-select-model"
+import { ModelSelectorPopover } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
import { useCommand } from "@/context/command"
@@ -1367,20 +1367,26 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
/>
</TooltipKeybind>
- <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
- <Button
- as="div"
- variant="ghost"
- onClick={() =>
- dialog.show(() =>
- providers.paid().length > 0 ? <DialogSelectModel /> : <DialogSelectModelUnpaid />,
- )
- }
- >
- {local.model.current()?.name ?? "Select model"}
- <Icon name="chevron-down" size="small" />
- </Button>
- </TooltipKeybind>
+ <Show
+ when={providers.paid().length > 0}
+ fallback={
+ <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
+ <Button as="div" variant="ghost" onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}>
+ {local.model.current()?.name ?? "Select model"}
+ <Icon name="chevron-down" size="small" />
+ </Button>
+ </TooltipKeybind>
+ }
+ >
+ <ModelSelectorPopover>
+ <TooltipKeybind placement="top" title="Choose model" keybind={command.keybind("model.choose")}>
+ <Button as="div" variant="ghost">
+ {local.model.current()?.name ?? "Select model"}
+ <Icon name="chevron-down" size="small" />
+ </Button>
+ </TooltipKeybind>
+ </ModelSelectorPopover>
+ </Show>
<Show when={local.model.variant.list().length > 0}>
<TooltipKeybind
placement="top"