+
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index bd95370a6397..3970f5c21081 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -7,7 +7,8 @@ import { Switch } from "@opencode-ai/ui/switch"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
-import { showToast } from "@opencode-ai/ui/toast"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { showToast } from "@/utils/toast"
import { useParams } from "@solidjs/router"
import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
@@ -86,6 +87,7 @@ export const SettingsGeneral: Component = () => {
const language = useLanguage()
const permission = usePermission()
const platform = usePlatform()
+ const dialog = useDialog()
const params = useParams()
const settings = useSettings()
@@ -407,7 +409,13 @@ export const SettingsGeneral: Component = () => {
settings.general.setNewLayoutDesigns(checked)}
+ onChange={(checked) => {
+ settings.general.setNewLayoutDesigns(checked)
+ if (!checked) return
+ void import("@/components/settings-v2").then((module) => {
+ dialog.show(() => )
+ })
+ }}
/>
diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx
index 149a0309b5ca..98f6c9ffa04c 100644
--- a/packages/app/src/components/settings-keybinds.tsx
+++ b/packages/app/src/components/settings-keybinds.tsx
@@ -1,17 +1,29 @@
-import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
+import { Component, For, Show, createMemo, lazy, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextField } from "@opencode-ai/ui/text-field"
-import { showToast } from "@opencode-ai/ui/toast"
+import { showToast } from "@/utils/toast"
import fuzzysort from "fuzzysort"
import { formatKeybind, parseKeybind, useCommand } from "@/context/command"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { SettingsList } from "./settings-list"
+const ButtonV2 = lazy(() => import("@opencode-ai/ui/v2/button-v2").then((module) => ({ default: module.ButtonV2 })))
+const IconV2 = lazy(() => import("@opencode-ai/ui/v2/icon").then((module) => ({ default: module.Icon })))
+const IconButtonV2 = lazy(() =>
+ import("@opencode-ai/ui/v2/icon-button-v2").then((module) => ({ default: module.IconButtonV2 })),
+)
+const TextInputV2 = lazy(() =>
+ import("@opencode-ai/ui/v2/text-input-v2").then((module) => ({ default: module.TextInputV2 })),
+)
+const SettingsListV2 = lazy(() =>
+ import("./settings-v2/parts/list").then((module) => ({ default: module.SettingsListV2 })),
+)
+
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
const PALETTE_ID = "command.palette"
const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
@@ -257,7 +269,7 @@ function useKeyCapture(input: {
})
}
-export const SettingsKeybinds: Component = () => {
+export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => {
const command = useCommand()
const language = useLanguage()
const settings = useSettings()
@@ -371,85 +383,178 @@ export const SettingsKeybinds: Component = () => {
if (store.active) command.keybinds(true)
})
+ const emptyResults = (
+
+
+
+ {language.t("settings.shortcuts.search.empty")}
+
+
+
+ "{store.filter}"
+
+
+
+
+ )
+
+ const List = props.v2 ? SettingsListV2 : SettingsList
+
+ const groups = (
+
+
+ {(group) => (
+ 0}>
+
+
+ {language.t(groupKey[group])}
+
+
+
+ {(id) => (
+
+
+ {title(id)}
+
+
+
+ )}
+
+
+
+
+ )}
+
+ {emptyResults}
+
+ )
+
return (
-
-
-
-
-
{language.t("settings.shortcuts.title")}
-
+ }
+ >
+ <>
+
-
-
-
-
- {(group) => (
- 0}>
-
-
{language.t(groupKey[group])}
-
-
- {(id) => (
-
- {title(id)}
- start(id)}
- >
-
- {language.t("settings.shortcuts.pressKeys")}
-
-
-
- )}
-
-
-
-
- )}
-
-
-
-
- {language.t("settings.shortcuts.search.empty")}
-
- "{store.filter}"
-
-
-
-
-
+
{groups}
+ >
+
)
}
diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx
index ffd85f97dce1..256a4dbb3f4d 100644
--- a/packages/app/src/components/settings-providers.tsx
+++ b/packages/app/src/components/settings-providers.tsx
@@ -2,7 +2,7 @@ import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag"
-import { showToast } from "@opencode-ai/ui/toast"
+import { showToast } from "@/utils/toast"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx
new file mode 100644
index 000000000000..d574dcf495f3
--- /dev/null
+++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx
@@ -0,0 +1,80 @@
+import { Component } from "solid-js"
+import { Dialog } from "@opencode-ai/ui/v2/dialog-v2"
+import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2"
+import { Icon } from "@opencode-ai/ui/icon"
+import { useLanguage } from "@/context/language"
+import { usePlatform } from "@/context/platform"
+import { SettingsGeneralV2 } from "./general"
+import { SettingsKeybinds } from "../settings-keybinds"
+import { SettingsProvidersV2 } from "./providers"
+import { SettingsModelsV2 } from "./models"
+import "./settings-v2.css"
+
+export const DialogSettings: Component = () => {
+ const language = useLanguage()
+ const platform = usePlatform()
+
+ return (
+
+ )
+}
diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx
new file mode 100644
index 000000000000..71f39ded01b1
--- /dev/null
+++ b/packages/app/src/components/settings-v2/general.tsx
@@ -0,0 +1,825 @@
+import { Component, Show, createMemo, createResource, onMount } from "solid-js"
+import { createStore } from "solid-js/store"
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
+import { Icon } from "@opencode-ai/ui/icon"
+import { SelectV2 } from "@opencode-ai/ui/select-v2"
+import { Switch } from "@opencode-ai/ui/v2/switch-v2"
+import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { showToast } from "@/utils/toast"
+import { useParams } from "@solidjs/router"
+import { useLanguage } from "@/context/language"
+import { usePermission } from "@/context/permission"
+import { usePlatform, type DisplayBackend } from "@/context/platform"
+import { useServerSync } from "@/context/server-sync"
+import { useServerSDK } from "@/context/server-sdk"
+import {
+ monoDefault,
+ monoFontFamily,
+ monoInput,
+ sansDefault,
+ sansFontFamily,
+ sansInput,
+ terminalDefault,
+ terminalFontFamily,
+ terminalInput,
+ useSettings,
+} from "@/context/settings"
+import { decode64 } from "@/utils/base64"
+import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
+import { Link } from "../link"
+import { SettingsListV2 } from "./parts/list"
+import { SettingsRowV2 } from "./parts/row"
+import "./settings-v2.css"
+
+let demoSoundState = {
+ cleanup: undefined as (() => void) | undefined,
+ timeout: undefined as NodeJS.Timeout | undefined,
+ run: 0,
+}
+
+type ThemeOption = {
+ id: string
+ name: string
+}
+
+type ShellOption = {
+ path: string
+ name: string
+ acceptable: boolean
+}
+
+type ShellSelectOption = {
+ id: string
+ value: string
+ label: string
+}
+
+// To prevent audio from overlapping/playing very quickly when navigating the settings menus,
+// delay the playback by 100ms during quick selection changes and pause existing sounds.
+const stopDemoSound = () => {
+ demoSoundState.run += 1
+ if (demoSoundState.cleanup) {
+ demoSoundState.cleanup()
+ }
+ clearTimeout(demoSoundState.timeout)
+ demoSoundState.cleanup = undefined
+}
+
+const playDemoSound = (id: string | undefined) => {
+ stopDemoSound()
+ if (!id) return
+
+ const run = ++demoSoundState.run
+ demoSoundState.timeout = setTimeout(() => {
+ void playSoundById(id).then((cleanup) => {
+ if (demoSoundState.run !== run) {
+ cleanup?.()
+ return
+ }
+ demoSoundState.cleanup = cleanup
+ })
+ }, 100)
+}
+
+export const SettingsGeneralV2: Component = () => {
+ const theme = useTheme()
+ const language = useLanguage()
+ const permission = usePermission()
+ const platform = usePlatform()
+ const dialog = useDialog()
+ const params = useParams()
+ const settings = useSettings()
+
+ const [store, setStore] = createStore({
+ checking: false,
+ })
+
+ const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
+ const dir = createMemo(() => decode64(params.dir))
+ const accepting = createMemo(() => {
+ const value = dir()
+ if (!value) return false
+ if (!params.id) return permission.isAutoAcceptingDirectory(value)
+ return permission.isAutoAccepting(params.id, value)
+ })
+
+ const toggleAccept = (checked: boolean) => {
+ const value = dir()
+ if (!value) return
+
+ if (!params.id) {
+ if (permission.isAutoAcceptingDirectory(value) === checked) return
+ permission.toggleAutoAcceptDirectory(value)
+ return
+ }
+
+ if (checked) {
+ permission.enableAutoAccept(params.id, value)
+ return
+ }
+
+ permission.disableAutoAccept(params.id, value)
+ }
+ const desktop = createMemo(() => platform.platform === "desktop")
+
+ const check = () => {
+ if (!platform.checkUpdate) return
+ setStore("checking", true)
+
+ void platform
+ .checkUpdate()
+ .then((result) => {
+ if (!result.updateAvailable) {
+ showToast({
+ variant: "success",
+ icon: "circle-check",
+ title: language.t("settings.updates.toast.latest.title"),
+ description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
+ })
+ return
+ }
+
+ const actions = platform.updateAndRestart
+ ? [
+ {
+ label: language.t("toast.update.action.installRestart"),
+ onClick: async () => {
+ await platform.updateAndRestart!()
+ },
+ },
+ {
+ label: language.t("toast.update.action.notYet"),
+ onClick: "dismiss" as const,
+ },
+ ]
+ : [
+ {
+ label: language.t("toast.update.action.notYet"),
+ onClick: "dismiss" as const,
+ },
+ ]
+
+ showToast({
+ persistent: true,
+ icon: "download",
+ title: language.t("toast.update.title"),
+ description: language.t("toast.update.description", { version: result.version ?? "" }),
+ actions,
+ })
+ })
+ .catch((err: unknown) => {
+ const message = err instanceof Error ? err.message : String(err)
+ showToast({ title: language.t("common.requestFailed"), description: message })
+ })
+ .finally(() => setStore("checking", false))
+ }
+
+ const themeOptions = createMemo
(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
+
+ const globalSync = useServerSync()
+ const globalSdk = useServerSDK()
+
+ const [shells] = createResource(
+ () =>
+ globalSdk.client.pty
+ .shells()
+ .then((res) => res.data ?? [])
+ .catch(() => [] as ShellOption[]),
+ { initialValue: [] as ShellOption[] },
+ )
+
+ const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
+ () => (linux() && platform.getDisplayBackend ? true : false),
+ () => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
+ { initialValue: null as DisplayBackend | null },
+ )
+
+ const [pinchZoom, { mutate: setPinchZoom }] = createResource(
+ () => (desktop() && platform.getPinchZoomEnabled ? true : false),
+ () => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false),
+ { initialValue: false },
+ )
+
+ onMount(() => {
+ void theme.loadThemes()
+ })
+
+ const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
+ const currentShell = createMemo(() => globalSync.data.config.shell ?? "")
+
+ const shellOptions = createMemo(() => {
+ const list = shells.latest
+ const current = globalSync.data.config.shell
+
+ const nameCounts = new Map()
+ for (const s of list) {
+ nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
+ }
+
+ const options = [
+ autoOption,
+ ...list.map((s) => {
+ const ambiguousName = (nameCounts.get(s.name) || 0) > 1
+ const text = ambiguousName ? s.path : s.name
+ const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
+ return {
+ id: s.path,
+ // Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
+ value: ambiguousName ? s.path : s.name,
+ label,
+ }
+ }),
+ ]
+
+ if (current && !options.some((o) => o.value === current)) {
+ options.push({ id: current, value: current, label: current })
+ }
+
+ return options
+ })
+
+ const onDisplayBackendChange = (checked: boolean) => {
+ const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
+ if (!update) return
+ void update.finally(() => {
+ void refetchDisplayBackend()
+ })
+ }
+
+ const onPinchZoomChange = (checked: boolean) => {
+ setPinchZoom(checked)
+ const update = platform.setPinchZoomEnabled?.(checked)
+ if (!update) return
+ void update.catch(() => setPinchZoom(!checked))
+ }
+
+ const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
+ { value: "system", label: language.t("theme.scheme.system") },
+ { value: "light", label: language.t("theme.scheme.light") },
+ { value: "dark", label: language.t("theme.scheme.dark") },
+ ])
+
+ const languageOptions = createMemo(() =>
+ language.locales.map((locale) => ({
+ value: locale,
+ label: language.label(locale),
+ })),
+ )
+
+ const noneSound = { id: "none", label: "sound.option.none" } as const
+ const soundOptions = [noneSound, ...SOUND_OPTIONS]
+ const mono = () => monoInput(settings.appearance.font())
+ const sans = () => sansInput(settings.appearance.uiFont())
+ const terminal = () => terminalInput(settings.appearance.terminalFont())
+
+ const soundSelectProps = (
+ enabled: () => boolean,
+ current: () => string,
+ setEnabled: (value: boolean) => void,
+ set: (id: string) => void,
+ ) => ({
+ options: soundOptions,
+ current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound,
+ value: (o: (typeof soundOptions)[number]) => o.id,
+ label: (o: (typeof soundOptions)[number]) => language.t(o.label),
+ onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
+ if (!option) return
+ playDemoSound(option.id === "none" ? undefined : option.id)
+ },
+ onSelect: (option: (typeof soundOptions)[number] | undefined) => {
+ if (!option) return
+ if (option.id === "none") {
+ setEnabled(false)
+ stopDemoSound()
+ return
+ }
+ setEnabled(true)
+ set(option.id)
+ playDemoSound(option.id)
+ },
+ })
+
+ const GeneralSection = () => (
+
+
+
+ o.value === language.locale())}
+ value={(o) => o.value}
+ label={(o) => o.label}
+ onSelect={(option) => option && language.setLocale(option.value)}
+ />
+
+
+
+
+
+
+
+
+
+ o.value === currentShell()) ?? autoOption}
+ value={(o) => o.id}
+ label={(o) => o.label}
+ onSelect={(option) => {
+ if (!option) return
+ if (option.value === currentShell()) return
+ globalSync.updateConfig({ shell: option.value })
+ }}
+ />
+
+
+
+
+ settings.general.setShowReasoningSummaries(checked)}
+ />
+
+
+
+
+
+ settings.general.setShellToolPartsExpanded(checked)}
+ />
+
+
+
+
+
+ settings.general.setEditToolPartsExpanded(checked)}
+ />
+
+
+
+
+
+ settings.general.setShowSessionProgressBar(checked)}
+ />
+
+
+
+
+
+ {
+ settings.general.setNewLayoutDesigns(checked)
+ if (checked) return
+ void import("@/components/dialog-settings").then((module) => {
+ dialog.show(() => )
+ })
+ }}
+ />
+
+
+
+
+ )
+
+ const AdvancedSection = () => (
+
+
{language.t("settings.general.section.advanced")}
+
+
+
+
+ settings.general.setShowFileTree(checked)}
+ />
+
+
+
+
+
+ settings.general.setShowNavigation(checked)}
+ />
+
+
+
+
+
+ settings.general.setShowSearch(checked)}
+ />
+
+
+
+
+
+ settings.general.setShowTerminal(checked)}
+ />
+
+
+
+
+
+ settings.general.setShowStatus(checked)}
+ />
+
+
+
+
+
+ settings.general.setShowCustomAgents(checked)}
+ />
+
+
+
+
+ )
+
+ const AppearanceSection = () => (
+
+
{language.t("settings.general.section.appearance")}
+
+
+
+ o.value === theme.colorScheme())}
+ value={(o) => o.value}
+ label={(o) => o.label}
+ onSelect={(option) => option && theme.setColorScheme(option.value)}
+ onHighlight={(option) => {
+ if (!option) return
+ theme.previewColorScheme(option.value)
+ return () => theme.cancelPreview()
+ }}
+ />
+
+
+
+ {language.t("settings.general.row.theme.description")}{" "}
+
+ {language.t("common.learnMore")}
+
+ >
+ }
+ >
+ o.id === theme.themeId())}
+ value={(o) => o.id}
+ label={(o) => o.name}
+ onSelect={(option) => {
+ if (!option) return
+ theme.setTheme(option.id)
+ }}
+ onHighlight={(option) => {
+ if (!option) return
+ theme.previewTheme(option.id)
+ return () => theme.cancelPreview()
+ }}
+ />
+
+
+
+
+ settings.appearance.setUIFont(event.currentTarget.value)}
+ placeholder={sansDefault}
+ spellcheck={false}
+ autocorrect="off"
+ autocomplete="off"
+ autocapitalize="off"
+ aria-label={language.t("settings.general.row.uiFont.title")}
+ style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }}
+ />
+
+
+
+
+
+ settings.appearance.setFont(event.currentTarget.value)}
+ placeholder={monoDefault}
+ spellcheck={false}
+ autocorrect="off"
+ autocomplete="off"
+ autocapitalize="off"
+ aria-label={language.t("settings.general.row.font.title")}
+ style={{ "font-family": monoFontFamily(settings.appearance.font()) }}
+ />
+
+
+
+
+
+ settings.appearance.setTerminalFont(event.currentTarget.value)}
+ placeholder={terminalDefault}
+ spellcheck={false}
+ autocorrect="off"
+ autocomplete="off"
+ autocapitalize="off"
+ aria-label={language.t("settings.general.row.terminalFont.title")}
+ style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
+ />
+
+
+
+
+ )
+
+ const NotificationsSection = () => (
+
+
{language.t("settings.general.section.notifications")}
+
+
+
+
+ settings.notifications.setAgent(checked)}
+ />
+
+
+
+
+
+ settings.notifications.setPermissions(checked)}
+ />
+
+
+
+
+
+ settings.notifications.setErrors(checked)}
+ />
+
+
+
+
+ )
+
+ const SoundsSection = () => (
+
+
{language.t("settings.general.section.sounds")}
+
+
+
+ settings.sounds.agentEnabled(),
+ () => settings.sounds.agent(),
+ (value) => settings.sounds.setAgentEnabled(value),
+ (id) => settings.sounds.setAgent(id),
+ )}
+ />
+
+
+
+ settings.sounds.permissionsEnabled(),
+ () => settings.sounds.permissions(),
+ (value) => settings.sounds.setPermissionsEnabled(value),
+ (id) => settings.sounds.setPermissions(id),
+ )}
+ />
+
+
+
+ settings.sounds.errorsEnabled(),
+ () => settings.sounds.errors(),
+ (value) => settings.sounds.setErrorsEnabled(value),
+ (id) => settings.sounds.setErrors(id),
+ )}
+ />
+
+
+
+ )
+
+ const UpdatesSection = () => (
+
+
{language.t("settings.general.section.updates")}
+
+
+
+
+ settings.updates.setStartup(checked)}
+ />
+
+
+
+
+
+ settings.general.setReleaseNotes(checked)}
+ />
+
+
+
+
+
+ {store.checking
+ ? language.t("settings.updates.action.checking")
+ : language.t("settings.updates.action.checkNow")}
+
+
+
+
+ )
+
+ const DisplaySection = () => (
+
+
+
{language.t("settings.general.section.display")}
+
+
+
+
+
+
+
+
+
+
+ {language.t("settings.general.row.wayland.title")}
+
+
+
+
+
+
+ }
+ description={language.t("settings.general.row.wayland.description")}
+ >
+
+
+
+
+
+
+
+
+ )
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/packages/app/src/components/settings-v2/index.tsx b/packages/app/src/components/settings-v2/index.tsx
new file mode 100644
index 000000000000..8ff588437987
--- /dev/null
+++ b/packages/app/src/components/settings-v2/index.tsx
@@ -0,0 +1 @@
+export { DialogSettings } from "./dialog-settings-v2"
diff --git a/packages/app/src/components/settings-v2/models.tsx b/packages/app/src/components/settings-v2/models.tsx
new file mode 100644
index 000000000000..a3f058670e4a
--- /dev/null
+++ b/packages/app/src/components/settings-v2/models.tsx
@@ -0,0 +1,138 @@
+import { useFilteredList } from "@opencode-ai/ui/hooks"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { Switch } from "@opencode-ai/ui/v2/switch-v2"
+import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
+import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
+import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
+import { type Component, For, Show } from "solid-js"
+import { useLanguage } from "@/context/language"
+import { useModels } from "@/context/models"
+import { popularProviders } from "@/hooks/use-providers"
+import { SettingsListV2 } from "./parts/list"
+import { SettingsRowV2 } from "./parts/row"
+import "./settings-v2.css"
+
+type ModelItem = ReturnType
["list"]>[number]
+
+const PROVIDER_ICON_SIZE = 16
+
+export const SettingsModelsV2: Component = () => {
+ const language = useLanguage()
+ const models = useModels()
+
+ const list = useFilteredList({
+ items: (_filter) => models.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 (
+ <>
+
+
+
+
+ {language.t("common.loading")}
+ {language.t("common.loading.ellipsis")}
+
+ }
+ >
+ 0}
+ fallback={
+
+ {language.t("dialog.model.empty")}
+
+ "{list.filter()}"
+
+
+ }
+ >
+
+ {(group) => (
+
+
+
+
+ {(item) => {
+ const key = { providerID: item.provider.id, modelID: item.id }
+ return (
+
+
+ {
+ models.setVisibility(key, checked)
+ }}
+ hideLabel
+ >
+ {item.name}
+
+
+
+ )
+ }}
+
+
+
+ )}
+
+
+
+
+ >
+ )
+}
diff --git a/packages/app/src/components/settings-v2/parts/list.tsx b/packages/app/src/components/settings-v2/parts/list.tsx
new file mode 100644
index 000000000000..2ebbdbe98fcc
--- /dev/null
+++ b/packages/app/src/components/settings-v2/parts/list.tsx
@@ -0,0 +1,6 @@
+import type { Component, JSX } from "solid-js"
+import "../settings-v2.css"
+
+export const SettingsListV2: Component<{ children: JSX.Element }> = (props) => {
+ return
+}
diff --git a/packages/app/src/components/settings-v2/parts/row.tsx b/packages/app/src/components/settings-v2/parts/row.tsx
new file mode 100644
index 000000000000..2c48240bb378
--- /dev/null
+++ b/packages/app/src/components/settings-v2/parts/row.tsx
@@ -0,0 +1,20 @@
+import type { Component, JSX } from "solid-js"
+import "../settings-v2.css"
+
+export interface SettingsRowV2Props {
+ title: string | JSX.Element
+ description: string | JSX.Element
+ children: JSX.Element
+}
+
+export const SettingsRowV2: Component