adamelmore пре 3 недеља
родитељ
комит
1934ee13d8

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

@@ -6,12 +6,8 @@ import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { SettingsGeneral } from "./settings-general"
 import { SettingsKeybinds } from "./settings-keybinds"
-import { SettingsPermissions } from "./settings-permissions"
 import { SettingsProviders } from "./settings-providers"
 import { SettingsModels } from "./settings-models"
-import { SettingsAgents } from "./settings-agents"
-import { SettingsCommands } from "./settings-commands"
-import { SettingsMcp } from "./settings-mcp"
 
 export const DialogSettings: Component = () => {
   const language = useLanguage()
@@ -45,6 +41,10 @@ export const DialogSettings: Component = () => {
                       <Icon name="server" />
                       {language.t("settings.providers.title")}
                     </Tabs.Trigger>
+                    <Tabs.Trigger value="models">
+                      <Icon name="server" />
+                      {language.t("settings.models.title")}
+                    </Tabs.Trigger>
                   </div>
                 </div>
               </div>
@@ -64,9 +64,9 @@ export const DialogSettings: Component = () => {
         <Tabs.Content value="providers" class="no-scrollbar">
           <SettingsProviders />
         </Tabs.Content>
-        {/* <Tabs.Content value="models" class="no-scrollbar"> */}
-        {/*   <SettingsModels /> */}
-        {/* </Tabs.Content> */}
+        <Tabs.Content value="models" class="no-scrollbar">
+          <SettingsModels />
+        </Tabs.Content>
         {/* <Tabs.Content value="agents" class="no-scrollbar"> */}
         {/*   <SettingsAgents /> */}
         {/* </Tabs.Content> */}

+ 126 - 5
packages/app/src/components/settings-models.tsx

@@ -1,14 +1,135 @@
-import { Component } from "solid-js"
+import { useFilteredList } from "@opencode-ai/ui/hooks"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { Switch } from "@opencode-ai/ui/switch"
+import { Icon } from "@opencode-ai/ui/icon"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+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 { popularProviders } from "@/hooks/use-providers"
+
+type ModelItem = ReturnType<ReturnType<typeof useLocal>["model"]["list"]>[number]
 
 export const SettingsModels: Component = () => {
+  const local = useLocal()
   const language = useLanguage()
 
+  const list = useFilteredList<ModelItem>({
+    items: (_filter) => local.model.list(),
+    key: (x) => `${x.provider.id}:${x.id}`,
+    filterKeys: ["provider.name", "name", "id"],
+    sortBy: (a, b) => a.name.localeCompare(b.name),
+    groupBy: (x) => x.provider.id,
+    sortGroupsBy: (a, b) => {
+      const aIndex = popularProviders.indexOf(a.category)
+      const bIndex = popularProviders.indexOf(b.category)
+      const aPopular = aIndex >= 0
+      const bPopular = bIndex >= 0
+
+      if (aPopular && !bPopular) return -1
+      if (!aPopular && bPopular) return 1
+      if (aPopular && bPopular) return aIndex - bIndex
+
+      const aName = a.items[0].provider.name
+      const bName = b.items[0].provider.name
+      return aName.localeCompare(bName)
+    },
+  })
+
   return (
-    <div class="flex flex-col h-full overflow-y-auto">
-      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
-        <p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
+    <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
+      <div
+        class="sticky top-0 z-10"
+        style={{
+          background:
+            "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
+        }}
+      >
+        <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
+          <h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
+          <div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
+            <Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
+            <TextField
+              variant="ghost"
+              type="text"
+              value={list.filter()}
+              onChange={list.onInput}
+              placeholder={language.t("dialog.model.search.placeholder")}
+              spellcheck={false}
+              autocorrect="off"
+              autocomplete="off"
+              autocapitalize="off"
+              class="flex-1"
+            />
+            <Show when={list.filter()}>
+              <IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
+            </Show>
+          </div>
+        </div>
+      </div>
+
+      <div class="flex flex-col gap-8 max-w-[720px]">
+        <Show
+          when={!list.grouped.loading}
+          fallback={
+            <div class="flex flex-col items-center justify-center py-12 text-center">
+              <span class="text-14-regular text-text-weak">
+                {language.t("common.loading")}
+                {language.t("common.loading.ellipsis")}
+              </span>
+            </div>
+          }
+        >
+          <Show
+            when={list.flat().length > 0}
+            fallback={
+              <div class="flex flex-col items-center justify-center py-12 text-center">
+                <span class="text-14-regular text-text-weak">{language.t("dialog.model.empty")}</span>
+                <Show when={list.filter()}>
+                  <span class="text-14-regular text-text-strong mt-1">&quot;{list.filter()}&quot;</span>
+                </Show>
+              </div>
+            }
+          >
+            <For each={list.grouped.latest}>
+              {(group) => (
+                <div class="flex flex-col gap-1">
+                  <div class="flex items-center gap-2 pb-2">
+                    <ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
+                    <span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
+                  </div>
+                  <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 }
+                        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">
+                              <span class="text-14-regular text-text-strong truncate block">{item.name}</span>
+                            </div>
+                            <div class="flex-shrink-0">
+                              <Switch
+                                checked={!!local.model.visible(key)}
+                                onChange={(checked) => {
+                                  local.model.setVisibility(key, checked)
+                                }}
+                                hideLabel
+                              >
+                                {item.name}
+                              </Switch>
+                            </div>
+                          </div>
+                        )
+                      }}
+                    </For>
+                  </div>
+                </div>
+              )}
+            </For>
+          </Show>
+        </Show>
       </div>
     </div>
   )