Просмотр исходного кода

feat(app): better model selector

Adam 1 месяц назад
Родитель
Сommit
b2aa387376

+ 71 - 40
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
@@ -20,6 +23,70 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
       .filter((m) => (props.provider ? m.provider.id === props.provider : true)),
   )
 
+  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"
@@ -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"

+ 21 - 15
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"