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

fix: favorites and recents stay visible when filtering models (#6053)

Daniel Gray 2 месяцев назад
Родитель
Сommit
52048c327d

+ 132 - 125
packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx

@@ -6,6 +6,7 @@ import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
 import { useDialog } from "@tui/ui/dialog"
 import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
 import { Keybind } from "@/util/keybind"
+import * as fuzzysort from "fuzzysort"
 
 export function useConnected() {
   const sync = useSync()
@@ -19,6 +20,7 @@ export function DialogModel(props: { providerID?: string }) {
   const sync = useSync()
   const dialog = useDialog()
   const [ref, setRef] = createSignal<DialogSelectRef<unknown>>()
+  const [query, setQuery] = createSignal("")
 
   const connected = useConnected()
   const providers = createDialogProviderOptions()
@@ -30,7 +32,7 @@ export function DialogModel(props: { providerID?: string }) {
   })
 
   const options = createMemo(() => {
-    const query = ref()?.filter
+    const q = query()
     const favorites = showExtra() ? local.model.favorite() : []
     const recents = local.model.recent()
 
@@ -42,148 +44,151 @@ export function DialogModel(props: { providerID?: string }) {
           .slice(0, 5)
       : []
 
-    const favoriteOptions = !query
-      ? favorites.flatMap((item) => {
-          const provider = sync.data.provider.find((x) => x.id === item.providerID)
-          if (!provider) return []
-          const model = provider.models[item.modelID]
-          if (!model) return []
-          return [
-            {
-              key: item,
-              value: {
+    const favoriteOptions = favorites.flatMap((item) => {
+      const provider = sync.data.provider.find((x) => x.id === item.providerID)
+      if (!provider) return []
+      const model = provider.models[item.modelID]
+      if (!model) return []
+      return [
+        {
+          key: item,
+          value: {
+            providerID: provider.id,
+            modelID: model.id,
+          },
+          title: model.name ?? item.modelID,
+          description: provider.name,
+          category: "Favorites",
+          disabled: provider.id === "opencode" && model.id.includes("-nano"),
+          footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
+          onSelect: () => {
+            dialog.clear()
+            local.model.set(
+              {
                 providerID: provider.id,
                 modelID: model.id,
               },
-              title: model.name ?? item.modelID,
-              description: provider.name,
-              category: "Favorites",
-              disabled: provider.id === "opencode" && model.id.includes("-nano"),
-              footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
-              onSelect: () => {
-                dialog.clear()
-                local.model.set(
-                  {
-                    providerID: provider.id,
-                    modelID: model.id,
-                  },
-                  { recent: true },
-                )
-              },
-            },
-          ]
-        })
-      : []
+              { recent: true },
+            )
+          },
+        },
+      ]
+    })
 
-    const recentOptions = !query
-      ? recentList.flatMap((item) => {
-          const provider = sync.data.provider.find((x) => x.id === item.providerID)
-          if (!provider) return []
-          const model = provider.models[item.modelID]
-          if (!model) return []
-          return [
-            {
-              key: item,
-              value: {
+    const recentOptions = recentList.flatMap((item) => {
+      const provider = sync.data.provider.find((x) => x.id === item.providerID)
+      if (!provider) return []
+      const model = provider.models[item.modelID]
+      if (!model) return []
+      return [
+        {
+          key: item,
+          value: {
+            providerID: provider.id,
+            modelID: model.id,
+          },
+          title: model.name ?? item.modelID,
+          description: provider.name,
+          category: "Recent",
+          disabled: provider.id === "opencode" && model.id.includes("-nano"),
+          footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
+          onSelect: () => {
+            dialog.clear()
+            local.model.set(
+              {
                 providerID: provider.id,
                 modelID: model.id,
               },
-              title: model.name ?? item.modelID,
-              description: provider.name,
-              category: "Recent",
-              disabled: provider.id === "opencode" && model.id.includes("-nano"),
-              footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
-              onSelect: () => {
+              { recent: true },
+            )
+          },
+        },
+      ]
+    })
+
+    const providerOptions = pipe(
+      sync.data.provider,
+      sortBy(
+        (provider) => provider.id !== "opencode",
+        (provider) => provider.name,
+      ),
+      flatMap((provider) =>
+        pipe(
+          provider.models,
+          entries(),
+          filter(([_, info]) => info.status !== "deprecated"),
+          filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
+          map(([model, info]) => {
+            const value = {
+              providerID: provider.id,
+              modelID: model,
+            }
+            return {
+              value,
+              title: info.name ?? model,
+              description: favorites.some(
+                (item) => item.providerID === value.providerID && item.modelID === value.modelID,
+              )
+                ? "(Favorite)"
+                : undefined,
+              category: connected() ? provider.name : undefined,
+              disabled: provider.id === "opencode" && model.includes("-nano"),
+              footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
+              onSelect() {
                 dialog.clear()
                 local.model.set(
                   {
                     providerID: provider.id,
-                    modelID: model.id,
+                    modelID: model,
                   },
                   { recent: true },
                 )
               },
-            },
-          ]
-        })
-      : []
-
-    return [
-      ...favoriteOptions,
-      ...recentOptions,
-      ...pipe(
-        sync.data.provider,
-        sortBy(
-          (provider) => provider.id !== "opencode",
-          (provider) => provider.name,
-        ),
-        flatMap((provider) =>
-          pipe(
-            provider.models,
-            entries(),
-            filter(([_, info]) => info.status !== "deprecated"),
-            filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)),
-            map(([model, info]) => {
-              const value = {
-                providerID: provider.id,
-                modelID: model,
-              }
-              return {
-                value,
-                title: info.name ?? model,
-                description: favorites.some(
-                  (item) => item.providerID === value.providerID && item.modelID === value.modelID,
-                )
-                  ? "(Favorite)"
-                  : undefined,
-                category: connected() ? provider.name : undefined,
-                disabled: provider.id === "opencode" && model.includes("-nano"),
-                footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined,
-                onSelect() {
-                  dialog.clear()
-                  local.model.set(
-                    {
-                      providerID: provider.id,
-                      modelID: model,
-                    },
-                    { recent: true },
-                  )
-                },
-              }
-            }),
-            filter((x) => {
-              if (query) return true
-              const value = x.value
-              const inFavorites = favorites.some(
-                (item) => item.providerID === value.providerID && item.modelID === value.modelID,
-              )
-              if (inFavorites) return false
-              const inRecents = recents.some(
-                (item) => item.providerID === value.providerID && item.modelID === value.modelID,
-              )
-              if (inRecents) return false
-              return true
-            }),
-            sortBy(
-              (x) => x.footer !== "Free",
-              (x) => x.title,
-            ),
+            }
+          }),
+          filter((x) => {
+            const value = x.value
+            const inFavorites = favorites.some(
+              (item) => item.providerID === value.providerID && item.modelID === value.modelID,
+            )
+            if (inFavorites) return false
+            const inRecents = recents.some(
+              (item) => item.providerID === value.providerID && item.modelID === value.modelID,
+            )
+            if (inRecents) return false
+            return true
+          }),
+          sortBy(
+            (x) => x.footer !== "Free",
+            (x) => x.title,
           ),
         ),
       ),
-      ...(!connected()
-        ? pipe(
-            providers(),
-            map((option) => {
-              return {
-                ...option,
-                category: "Popular providers",
-              }
-            }),
-            take(6),
-          )
-        : []),
-    ]
+    )
+
+    const popularProviders = !connected()
+      ? pipe(
+          providers(),
+          map((option) => {
+            return {
+              ...option,
+              category: "Popular providers",
+            }
+          }),
+          take(6),
+        )
+      : []
+
+    // Apply fuzzy filtering to each section separately, maintaining section order
+    if (q) {
+      const filteredFavorites = fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj)
+      const filteredRecents = fuzzysort.go(q, recentOptions, { keys: ["title"] }).map((x) => x.obj)
+      const filteredProviders = fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
+      const filteredPopular = fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj)
+      return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular]
+    }
+
+    return [...favoriteOptions, ...recentOptions, ...providerOptions, ...popularProviders]
   })
 
   const provider = createMemo(() =>
@@ -215,6 +220,8 @@ export function DialogModel(props: { providerID?: string }) {
         },
       ]}
       ref={setRef}
+      onFilter={setQuery}
+      skipFilter={true}
       title={title()}
       current={local.model.current()}
       options={options()}

+ 3 - 1
packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx

@@ -19,6 +19,7 @@ export interface DialogSelectProps<T> {
   onMove?: (option: DialogSelectOption<T>) => void
   onFilter?: (query: string) => void
   onSelect?: (option: DialogSelectOption<T>) => void
+  skipFilter?: boolean
   keybind?: {
     keybind: Keybind.Info
     title: string
@@ -74,7 +75,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
     const result = pipe(
       props.options,
       filter((x) => x.disabled !== true),
-      (x) => (!needle ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj)),
+      (x) =>
+        !needle || props.skipFilter ? x : fuzzysort.go(needle, x, { keys: ["title", "category"] }).map((x) => x.obj),
     )
     return result
   })