-
+
{language.t("settings.providers.title")}
+
diff --git a/packages/app/src/components/settings-server-picker.tsx b/packages/app/src/components/settings-server-picker.tsx
new file mode 100644
index 0000000000..3f679753f0
--- /dev/null
+++ b/packages/app/src/components/settings-server-picker.tsx
@@ -0,0 +1,106 @@
+import { Button } from "@opencode-ai/ui/button"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Icon } from "@opencode-ai/ui/icon"
+import { QueryClientProvider } from "@tanstack/solid-query"
+import { createMemo, For, type ParentProps, Show } from "solid-js"
+import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
+import { ModelsProvider } from "@/context/models"
+import { ServerConnection } from "@/context/server"
+import { ServerSDKProvider } from "@/context/server-sdk"
+import { ServerSyncProvider } from "@/context/server-sync"
+import { useGlobal } from "@/context/global"
+import { useSettings } from "@/context/settings"
+
+export function SettingsServerScope(props: ParentProps) {
+ const global = useGlobal()
+ const settings = useSettings()
+
+ return (
+
+
+ {(server) => {props.children}}
+
+
+ )
+}
+
+function SettingsServerDataProviders(props: ParentProps<{ server: ServerConnection.Any }>) {
+ const global = useGlobal()
+ const serverCtx = () => global.createServerCtx(props.server)
+
+ return (
+
+
+
+ {props.children}
+
+
+
+ )
+}
+
+export function SettingsServerPicker() {
+ const global = useGlobal()
+ const settings = useSettings()
+ const selected = createMemo(() =>
+ settings.general.newLayoutDesigns() ? global.settings.server.selected() : undefined,
+ )
+
+ return (
+
+ {(conn) => (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/packages/app/src/components/settings-servers.tsx b/packages/app/src/components/settings-servers.tsx
new file mode 100644
index 0000000000..299d41f6db
--- /dev/null
+++ b/packages/app/src/components/settings-servers.tsx
@@ -0,0 +1,33 @@
+import { Show, type Component } from "solid-js"
+import { useLanguage } from "@/context/language"
+import { ServerConnectionForm, ServerConnectionList, useServerManagementController } from "./dialog-select-server"
+
+export const SettingsServers: Component = () => {
+ const language = useLanguage()
+ const controller = useServerManagementController()
+
+ return (
+
+ )
+}
diff --git a/packages/app/src/components/settings-v2/dialog-server-v2.tsx b/packages/app/src/components/settings-v2/dialog-server-v2.tsx
new file mode 100644
index 0000000000..bad1c9ab1a
--- /dev/null
+++ b/packages/app/src/components/settings-v2/dialog-server-v2.tsx
@@ -0,0 +1,129 @@
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
+import { Dialog, DialogFooter } from "@opencode-ai/ui/v2/dialog-v2"
+import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js"
+import { useLanguage } from "@/context/language"
+import { type ServerConnection } from "@/context/server"
+import { useServerManagementController } from "../dialog-select-server"
+import "./settings-v2.css"
+
+export const DialogServerV2: Component<{
+ mode: "add" | "edit"
+ server?: ServerConnection.Http
+}> = (props) => {
+ const dialog = useDialog()
+ const language = useLanguage()
+ const controller = useServerManagementController({
+ onSelect: () => dialog.close(),
+ navigateOnAdd: false,
+ })
+ const [opened, setOpened] = createSignal(false)
+
+ onMount(() => {
+ if (props.mode === "add") controller.startAdd()
+ if (props.mode === "edit" && props.server) controller.startEdit(props.server)
+ setOpened(true)
+ })
+
+ onCleanup(() => {
+ controller.resetForm()
+ })
+
+ createEffect(() => {
+ if (!opened()) return
+ if (controller.isFormMode()) return
+ dialog.close()
+ })
+
+ const keyDown = (event: KeyboardEvent) => {
+ if (event.key !== "Enter" || event.isComposing) return
+ event.preventDefault()
+ controller.submitForm()
+ }
+
+ const title = () =>
+ props.mode === "add" ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")
+
+ const submitLabel = () => {
+ if (controller.formBusy()) return language.t("dialog.server.add.checking")
+ if (props.mode === "add") return language.t("dialog.server.add.button")
+ return language.t("common.save")
+ }
+
+ return (
+
+ )
+}
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 0000000000..cfe6971b12
--- /dev/null
+++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx
@@ -0,0 +1,82 @@
+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"
+import { SettingsServersV2 } from "./servers"
+
+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 0000000000..e219222754
--- /dev/null
+++ b/packages/app/src/components/settings-v2/general.tsx
@@ -0,0 +1,776 @@
+import { Component, Show, createMemo, createResource, onMount } from "solid-js"
+import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
+import { Icon } from "@opencode-ai/ui/icon"
+import { SelectV2 } from "@opencode-ai/ui/v2/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 { 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 { useUpdaterAction } from "../updater-action"
+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 updater = useUpdaterAction()
+
+ 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 themeOptions = createMemo
(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
+
+ const serverSync = useServerSync()
+ const serverSdk = useServerSDK()
+
+ const [shells] = createResource(
+ () =>
+ serverSdk.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(() => serverSync.data.config.shell ?? "")
+
+ const shellOptions = createMemo(() => {
+ const list = shells.latest
+ const current = serverSync.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] | null) => {
+ 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}
+ placement="bottom-end"
+ gutter={6}
+ value={(o) => o.id}
+ label={(o) => o.label}
+ onSelect={(option) => {
+ if (!option) return
+ if (option.value === currentShell()) return
+ serverSync.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())}
+ placement="bottom-end"
+ gutter={6}
+ 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())}
+ placement="bottom-end"
+ gutter={6}
+ 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),
+ )}
+ placement="bottom-end"
+ gutter={6}
+ />
+
+
+
+ settings.sounds.permissionsEnabled(),
+ () => settings.sounds.permissions(),
+ (value) => settings.sounds.setPermissionsEnabled(value),
+ (id) => settings.sounds.setPermissions(id),
+ )}
+ placement="bottom-end"
+ gutter={6}
+ />
+
+
+
+ settings.sounds.errorsEnabled(),
+ () => settings.sounds.errors(),
+ (value) => settings.sounds.setErrorsEnabled(value),
+ (id) => settings.sounds.setErrors(id),
+ )}
+ placement="bottom-end"
+ gutter={6}
+ />
+
+
+
+ )
+
+ const UpdatesSection = () => (
+
+
{language.t("settings.general.section.updates")}
+
+
+
+
+ settings.general.setReleaseNotes(checked)}
+ />
+
+
+
+
+
+ {language.t(updater.action().label)}
+
+
+
+
+ )
+
+ const DisplaySection = () => (
+
+
+
{language.t("settings.general.section.display")}
+
+
+
+
+
+
+
+
+
+
+ {language.t("settings.general.row.wayland.title")}
+
+
+
+
+
+
+ }
+ description={language.t("settings.general.row.wayland.description")}
+ >
+
+
+
+
+
+
+
+ >
+ )
+}
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 0000000000..8ff5884379
--- /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 0000000000..a3f058670e
--- /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