adamelmore 3 недель назад
Родитель
Сommit
84b12a8fb7

+ 6 - 3
packages/app/src/app.tsx

@@ -21,6 +21,7 @@ import { PromptProvider } from "@/context/prompt"
 import { FileProvider } from "@/context/file"
 import { CommentsProvider } from "@/context/comments"
 import { NotificationProvider } from "@/context/notification"
+import { ModelsProvider } from "@/context/models"
 import { DialogProvider } from "@opencode-ai/ui/context/dialog"
 import { CommandProvider } from "@/context/command"
 import { LanguageProvider, useLanguage } from "@/context/language"
@@ -116,9 +117,11 @@ export function AppInterface(props: { defaultUrl?: string }) {
                   <PermissionProvider>
                     <LayoutProvider>
                       <NotificationProvider>
-                        <CommandProvider>
-                          <Layout>{props.children}</Layout>
-                        </CommandProvider>
+                        <ModelsProvider>
+                          <CommandProvider>
+                            <Layout>{props.children}</Layout>
+                          </CommandProvider>
+                        </ModelsProvider>
                       </NotificationProvider>
                     </LayoutProvider>
                   </PermissionProvider>

+ 7 - 7
packages/app/src/components/settings-models.tsx

@@ -7,17 +7,17 @@ import { TextField } from "@opencode-ai/ui/text-field"
 import type { IconName } from "@opencode-ai/ui/icons/provider"
 import { type Component, For, Show } from "solid-js"
 import { useLanguage } from "@/context/language"
-import { type ModelKey, useLocal } from "@/context/local"
+import { useModels } from "@/context/models"
 import { popularProviders } from "@/hooks/use-providers"
 
-type ModelItem = ReturnType<ReturnType<typeof useLocal>["model"]["list"]>[number]
+type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
 
 export const SettingsModels: Component = () => {
-  const local = useLocal()
   const language = useLanguage()
+  const models = useModels()
 
   const list = useFilteredList<ModelItem>({
-    items: (_filter) => local.model.list(),
+    items: (_filter) => models.list(),
     key: (x) => `${x.provider.id}:${x.id}`,
     filterKeys: ["provider.name", "name", "id"],
     sortBy: (a, b) => a.name.localeCompare(b.name),
@@ -103,7 +103,7 @@ export const SettingsModels: Component = () => {
                   <div class="bg-surface-raised-base px-4 rounded-lg">
                     <For each={group.items}>
                       {(item) => {
-                        const key: ModelKey = { providerID: item.provider.id, modelID: item.id }
+                        const key = { providerID: item.provider.id, modelID: item.id }
                         return (
                           <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
                             <div class="min-w-0">
@@ -111,9 +111,9 @@ export const SettingsModels: Component = () => {
                             </div>
                             <div class="flex-shrink-0">
                               <Switch
-                                checked={!!local.model.visible(key)}
+                                checked={models.visible(key)}
                                 onChange={(checked) => {
-                                  local.model.setVisibility(key, checked)
+                                  models.setVisibility(key, checked)
                                 }}
                                 hideLabel
                               >

+ 13 - 104
packages/app/src/context/local.tsx

@@ -1,14 +1,12 @@
 import { createStore, produce, reconcile } from "solid-js/store"
 import { batch, createEffect, createMemo, onCleanup } from "solid-js"
-import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
 import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useSDK } from "./sdk"
 import { useSync } from "./sync"
 import { base64Encode } from "@opencode-ai/util/encode"
 import { useProviders } from "@/hooks/use-providers"
-import { DateTime } from "luxon"
-import { Persist, persisted } from "@/utils/persist"
+import { useModels } from "@/context/models"
 import { showToast } from "@opencode-ai/ui/toast"
 import { useLanguage } from "@/context/language"
 
@@ -112,18 +110,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
     })()
 
     const model = (() => {
-      const [store, setStore, _, modelReady] = persisted(
-        Persist.global("model", ["model.v1"]),
-        createStore<{
-          user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
-          recent: ModelKey[]
-          variant?: Record<string, string | undefined>
-        }>({
-          user: [],
-          recent: [],
-          variant: {},
-        }),
-      )
+      const models = useModels()
 
       const [ephemeral, setEphemeral] = createStore<{
         model: Record<string, ModelKey | undefined>
@@ -131,57 +118,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         model: {},
       })
 
-      const available = createMemo(() =>
-        providers.connected().flatMap((p) =>
-          Object.values(p.models).map((m) => ({
-            ...m,
-            provider: p,
-          })),
-        ),
-      )
-
-      const latest = createMemo(() =>
-        pipe(
-          available(),
-          filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
-          groupBy((x) => x.provider.id),
-          mapValues((models) =>
-            pipe(
-              models,
-              groupBy((x) => x.family),
-              values(),
-              (groups) =>
-                groups.flatMap((g) => {
-                  const first = firstBy(g, [(x) => x.release_date, "desc"])
-                  return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
-                }),
-            ),
-          ),
-          values(),
-          flat(),
-        ),
-      )
-
-      const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
-
-      const userVisibilityMap = createMemo(() => {
-        const map = new Map<string, "show" | "hide">()
-        for (const item of store.user) {
-          map.set(`${item.providerID}:${item.modelID}`, item.visibility)
-        }
-        return map
-      })
-
-      const list = createMemo(() =>
-        available().map((m) => ({
-          ...m,
-          name: m.name.replace("(latest)", "").trim(),
-          latest: m.name.includes("(latest)"),
-        })),
-      )
-
-      const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
-
       const fallbackModel = createMemo<ModelKey | undefined>(() => {
         if (sync.data.config.model) {
           const [providerID, modelID] = sync.data.config.model.split("/")
@@ -193,7 +129,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           }
         }
 
-        for (const item of store.recent) {
+        for (const item of models.recent.list()) {
           if (isModelValid(item)) {
             return item
           }
@@ -225,10 +161,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           fallbackModel,
         )
         if (!key) return undefined
-        return find(key)
+        return models.find(key)
       })
 
-      const recent = createMemo(() => store.recent.map(find).filter(Boolean))
+      const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
 
       const cycle = (direction: 1 | -1) => {
         const recentList = recent()
@@ -253,54 +189,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         })
       }
 
-      function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
-        const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
-        if (index >= 0) {
-          setStore("user", index, { visibility })
-        } else {
-          setStore("user", store.user.length, { ...model, visibility })
-        }
-      }
-
       return {
-        ready: modelReady,
+        ready: models.ready,
         current,
         recent,
-        list,
+        list: models.list,
         cycle,
         set(model: ModelKey | undefined, options?: { recent?: boolean }) {
           batch(() => {
             const currentAgent = agent.current()
             const next = model ?? fallbackModel()
             if (currentAgent) setEphemeral("model", currentAgent.name, next)
-            if (model) updateVisibility(model, "show")
-            if (options?.recent && model) {
-              const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
-              if (uniq.length > 5) uniq.pop()
-              setStore("recent", uniq)
-            }
+            if (model) models.setVisibility(model, true)
+            if (options?.recent && model) models.recent.push(model)
           })
         },
         visible(model: ModelKey) {
-          const key = `${model.providerID}:${model.modelID}`
-          const visibility = userVisibilityMap().get(key)
-          if (visibility === "hide") return false
-          if (visibility === "show") return true
-          if (latestSet().has(key)) return true
-          // For models without valid release_date (e.g. custom models), show by default
-          const m = find(model)
-          if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
-          return false
+          return models.visible(model)
         },
         setVisibility(model: ModelKey, visible: boolean) {
-          updateVisibility(model, visible ? "show" : "hide")
+          models.setVisibility(model, visible)
         },
         variant: {
           current() {
             const m = current()
             if (!m) return undefined
-            const key = `${m.provider.id}/${m.id}`
-            return store.variant?.[key]
+            return models.variant.get({ providerID: m.provider.id, modelID: m.id })
           },
           list() {
             const m = current()
@@ -311,12 +225,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           set(value: string | undefined) {
             const m = current()
             if (!m) return
-            const key = `${m.provider.id}/${m.id}`
-            if (!store.variant) {
-              setStore("variant", { [key]: value })
-            } else {
-              setStore("variant", key, value)
-            }
+            models.variant.set({ providerID: m.provider.id, modelID: m.id }, value)
           },
           cycle() {
             const variants = this.list()

+ 140 - 0
packages/app/src/context/models.tsx

@@ -0,0 +1,140 @@
+import { createMemo } from "solid-js"
+import { createStore } from "solid-js/store"
+import { DateTime } from "luxon"
+import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
+import { createSimpleContext } from "@opencode-ai/ui/context"
+import { useProviders } from "@/hooks/use-providers"
+import { Persist, persisted } from "@/utils/persist"
+
+export type ModelKey = { providerID: string; modelID: string }
+
+type Visibility = "show" | "hide"
+type User = ModelKey & { visibility: Visibility; favorite?: boolean }
+type Store = {
+  user: User[]
+  recent: ModelKey[]
+  variant?: Record<string, string | undefined>
+}
+
+export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
+  name: "Models",
+  init: () => {
+    const providers = useProviders()
+
+    const [store, setStore, _, ready] = persisted(
+      Persist.global("model", ["model.v1"]),
+      createStore<Store>({
+        user: [],
+        recent: [],
+        variant: {},
+      }),
+    )
+
+    const available = createMemo(() =>
+      providers.connected().flatMap((p) =>
+        Object.values(p.models).map((m) => ({
+          ...m,
+          provider: p,
+        })),
+      ),
+    )
+
+    const latest = createMemo(() =>
+      pipe(
+        available(),
+        filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
+        groupBy((x) => x.provider.id),
+        mapValues((models) =>
+          pipe(
+            models,
+            groupBy((x) => x.family),
+            values(),
+            (groups) =>
+              groups.flatMap((g) => {
+                const first = firstBy(g, [(x) => x.release_date, "desc"])
+                return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
+              }),
+          ),
+        ),
+        values(),
+        flat(),
+      ),
+    )
+
+    const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
+
+    const visibility = createMemo(() => {
+      const map = new Map<string, Visibility>()
+      for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility)
+      return map
+    })
+
+    const list = createMemo(() =>
+      available().map((m) => ({
+        ...m,
+        name: m.name.replace("(latest)", "").trim(),
+        latest: m.name.includes("(latest)"),
+      })),
+    )
+
+    const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID)
+
+    function update(model: ModelKey, state: Visibility) {
+      const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
+      if (index >= 0) {
+        setStore("user", index, { visibility: state })
+        return
+      }
+      setStore("user", store.user.length, { ...model, visibility: state })
+    }
+
+    const visible = (model: ModelKey) => {
+      const key = `${model.providerID}:${model.modelID}`
+      const state = visibility().get(key)
+      if (state === "hide") return false
+      if (state === "show") return true
+      if (latestSet().has(key)) return true
+      const m = find(model)
+      if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
+      return false
+    }
+
+    const setVisibility = (model: ModelKey, state: boolean) => {
+      update(model, state ? "show" : "hide")
+    }
+
+    const push = (model: ModelKey) => {
+      const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
+      if (uniq.length > 5) uniq.pop()
+      setStore("recent", uniq)
+    }
+
+    const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}`
+    const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)]
+
+    const setVariant = (model: ModelKey, value: string | undefined) => {
+      const key = variantKey(model)
+      if (!store.variant) {
+        setStore("variant", { [key]: value })
+        return
+      }
+      setStore("variant", key, value)
+    }
+
+    return {
+      ready,
+      list,
+      find,
+      visible,
+      setVisibility,
+      recent: {
+        list: createMemo(() => store.recent),
+        push,
+      },
+      variant: {
+        get: getVariant,
+        set: setVariant,
+      },
+    }
+  },
+})