summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-26 07:23:01 -0600
committeradamelmore <[email protected]>2026-01-26 08:15:01 -0600
commit1934ee13d8439ed38b9bcebb4e26e4d2d01f6f08 (patch)
tree21159298036ce3729f77e18b6e95e6ebc6581652
parent6c1e18f111ae8bdbedbdc0c2d4367c09d318f94a (diff)
downloadopencode-1934ee13d8439ed38b9bcebb4e26e4d2d01f6f08.tar.gz
opencode-1934ee13d8439ed38b9bcebb4e26e4d2d01f6f08.zip
wip(app): model settings
-rw-r--r--packages/app/src/components/dialog-settings.tsx14
-rw-r--r--packages/app/src/components/settings-models.tsx131
2 files changed, 133 insertions, 12 deletions
diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx
index 9dd6efd68..5efee5a3c 100644
--- a/packages/app/src/components/dialog-settings.tsx
+++ b/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> */}
diff --git a/packages/app/src/components/settings-models.tsx b/packages/app/src/components/settings-models.tsx
index 6a636879d..a3ba45f61 100644
--- a/packages/app/src/components/settings-models.tsx
+++ b/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>
)