|
|
@@ -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<ThemeOption[]>(() => 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<ShellSelectOption[]>(() => {
|
|
|
+ const list = shells.latest
|
|
|
+ const current = globalSync.data.config.shell
|
|
|
+
|
|
|
+ const nameCounts = new Map<string, number>()
|
|
|
+ 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 = () => (
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.language.title")}
|
|
|
+ description={language.t("settings.general.row.language.description")}
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-language"
|
|
|
+ options={languageOptions()}
|
|
|
+ current={languageOptions().find((o) => o.value === language.locale())}
|
|
|
+ value={(o) => o.value}
|
|
|
+ label={(o) => o.label}
|
|
|
+ onSelect={(option) => option && language.setLocale(option.value)}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("command.permissions.autoaccept.enable")}
|
|
|
+ description={language.t("toast.permissions.autoaccept.on.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-auto-accept-permissions">
|
|
|
+ <Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.shell.title")}
|
|
|
+ description={language.t("settings.general.row.shell.description")}
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-shell"
|
|
|
+ options={shellOptions()}
|
|
|
+ current={shellOptions().find((o) => 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 })
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.reasoningSummaries.title")}
|
|
|
+ description={language.t("settings.general.row.reasoningSummaries.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-feed-reasoning-summaries">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showReasoningSummaries()}
|
|
|
+ onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.shellToolPartsExpanded.title")}
|
|
|
+ description={language.t("settings.general.row.shellToolPartsExpanded.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-feed-shell-tool-parts-expanded">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.shellToolPartsExpanded()}
|
|
|
+ onChange={(checked) => settings.general.setShellToolPartsExpanded(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.editToolPartsExpanded.title")}
|
|
|
+ description={language.t("settings.general.row.editToolPartsExpanded.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-feed-edit-tool-parts-expanded">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.editToolPartsExpanded()}
|
|
|
+ onChange={(checked) => settings.general.setEditToolPartsExpanded(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showSessionProgressBar.title")}
|
|
|
+ description={language.t("settings.general.row.showSessionProgressBar.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-session-progress-bar">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showSessionProgressBar()}
|
|
|
+ onChange={(checked) => settings.general.setShowSessionProgressBar(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.newLayoutDesigns.title")}
|
|
|
+ description={language.t("settings.general.row.newLayoutDesigns.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-new-layout-designs">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.newLayoutDesigns()}
|
|
|
+ onChange={(checked) => {
|
|
|
+ settings.general.setNewLayoutDesigns(checked)
|
|
|
+ if (checked) return
|
|
|
+ void import("@/components/dialog-settings").then((module) => {
|
|
|
+ dialog.show(() => <module.DialogSettings />)
|
|
|
+ })
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+
|
|
|
+ const AdvancedSection = () => (
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <h3 class="settings-v2-section-title">{language.t("settings.general.section.advanced")}</h3>
|
|
|
+
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showFileTree.title")}
|
|
|
+ description={language.t("settings.general.row.showFileTree.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-file-tree">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showFileTree()}
|
|
|
+ onChange={(checked) => settings.general.setShowFileTree(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showNavigation.title")}
|
|
|
+ description={language.t("settings.general.row.showNavigation.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-navigation">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showNavigation()}
|
|
|
+ onChange={(checked) => settings.general.setShowNavigation(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showSearch.title")}
|
|
|
+ description={language.t("settings.general.row.showSearch.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-search">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showSearch()}
|
|
|
+ onChange={(checked) => settings.general.setShowSearch(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showTerminal.title")}
|
|
|
+ description={language.t("settings.general.row.showTerminal.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-terminal">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showTerminal()}
|
|
|
+ onChange={(checked) => settings.general.setShowTerminal(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showStatus.title")}
|
|
|
+ description={language.t("settings.general.row.showStatus.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-status">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showStatus()}
|
|
|
+ onChange={(checked) => settings.general.setShowStatus(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.showCustomAgents.title")}
|
|
|
+ description={language.t("settings.general.row.showCustomAgents.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-show-custom-agents">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.showCustomAgents()}
|
|
|
+ onChange={(checked) => settings.general.setShowCustomAgents(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+
|
|
|
+ const AppearanceSection = () => (
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <h3 class="settings-v2-section-title">{language.t("settings.general.section.appearance")}</h3>
|
|
|
+
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.colorScheme.title")}
|
|
|
+ description={language.t("settings.general.row.colorScheme.description")}
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-color-scheme"
|
|
|
+ options={colorSchemeOptions()}
|
|
|
+ current={colorSchemeOptions().find((o) => 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()
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.theme.title")}
|
|
|
+ description={
|
|
|
+ <>
|
|
|
+ {language.t("settings.general.row.theme.description")}{" "}
|
|
|
+ <Link class="settings-v2-link" href="https://opencode.ai/docs/themes/">
|
|
|
+ {language.t("common.learnMore")}
|
|
|
+ </Link>
|
|
|
+ </>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-theme"
|
|
|
+ options={themeOptions()}
|
|
|
+ current={themeOptions().find((o) => 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()
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.uiFont.title")}
|
|
|
+ description={language.t("settings.general.row.uiFont.description")}
|
|
|
+ >
|
|
|
+ <div class="w-full sm:w-[220px]">
|
|
|
+ <TextInputV2
|
|
|
+ data-action="settings-ui-font"
|
|
|
+ type="text"
|
|
|
+ appearance="base"
|
|
|
+ value={sans()}
|
|
|
+ onInput={(event) => 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()) }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.font.title")}
|
|
|
+ description={language.t("settings.general.row.font.description")}
|
|
|
+ >
|
|
|
+ <div class="w-full sm:w-[220px]">
|
|
|
+ <TextInputV2
|
|
|
+ data-action="settings-code-font"
|
|
|
+ type="text"
|
|
|
+ appearance="base"
|
|
|
+ value={mono()}
|
|
|
+ onInput={(event) => 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()) }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.terminalFont.title")}
|
|
|
+ description={language.t("settings.general.row.terminalFont.description")}
|
|
|
+ >
|
|
|
+ <div class="w-full sm:w-[220px]">
|
|
|
+ <TextInputV2
|
|
|
+ data-action="settings-terminal-font"
|
|
|
+ type="text"
|
|
|
+ appearance="base"
|
|
|
+ value={terminal()}
|
|
|
+ onInput={(event) => 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()) }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+
|
|
|
+ const NotificationsSection = () => (
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <h3 class="settings-v2-section-title">{language.t("settings.general.section.notifications")}</h3>
|
|
|
+
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.notifications.agent.title")}
|
|
|
+ description={language.t("settings.general.notifications.agent.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-notifications-agent">
|
|
|
+ <Switch
|
|
|
+ checked={settings.notifications.agent()}
|
|
|
+ onChange={(checked) => settings.notifications.setAgent(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.notifications.permissions.title")}
|
|
|
+ description={language.t("settings.general.notifications.permissions.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-notifications-permissions">
|
|
|
+ <Switch
|
|
|
+ checked={settings.notifications.permissions()}
|
|
|
+ onChange={(checked) => settings.notifications.setPermissions(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.notifications.errors.title")}
|
|
|
+ description={language.t("settings.general.notifications.errors.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-notifications-errors">
|
|
|
+ <Switch
|
|
|
+ checked={settings.notifications.errors()}
|
|
|
+ onChange={(checked) => settings.notifications.setErrors(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+
|
|
|
+ const SoundsSection = () => (
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <h3 class="settings-v2-section-title">{language.t("settings.general.section.sounds")}</h3>
|
|
|
+
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.sounds.agent.title")}
|
|
|
+ description={language.t("settings.general.sounds.agent.description")}
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-sounds-agent"
|
|
|
+ {...soundSelectProps(
|
|
|
+ () => settings.sounds.agentEnabled(),
|
|
|
+ () => settings.sounds.agent(),
|
|
|
+ (value) => settings.sounds.setAgentEnabled(value),
|
|
|
+ (id) => settings.sounds.setAgent(id),
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.sounds.permissions.title")}
|
|
|
+ description={language.t("settings.general.sounds.permissions.description")}
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-sounds-permissions"
|
|
|
+ {...soundSelectProps(
|
|
|
+ () => settings.sounds.permissionsEnabled(),
|
|
|
+ () => settings.sounds.permissions(),
|
|
|
+ (value) => settings.sounds.setPermissionsEnabled(value),
|
|
|
+ (id) => settings.sounds.setPermissions(id),
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.sounds.errors.title")}
|
|
|
+ description={language.t("settings.general.sounds.errors.description")}
|
|
|
+ >
|
|
|
+ <SelectV2
|
|
|
+ data-action="settings-sounds-errors"
|
|
|
+ {...soundSelectProps(
|
|
|
+ () => settings.sounds.errorsEnabled(),
|
|
|
+ () => settings.sounds.errors(),
|
|
|
+ (value) => settings.sounds.setErrorsEnabled(value),
|
|
|
+ (id) => settings.sounds.setErrors(id),
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ </SettingsRowV2>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+
|
|
|
+ const UpdatesSection = () => (
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <h3 class="settings-v2-section-title">{language.t("settings.general.section.updates")}</h3>
|
|
|
+
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.updates.row.startup.title")}
|
|
|
+ description={language.t("settings.updates.row.startup.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-updates-startup">
|
|
|
+ <Switch
|
|
|
+ checked={settings.updates.startup()}
|
|
|
+ disabled={!platform.checkUpdate}
|
|
|
+ onChange={(checked) => settings.updates.setStartup(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.releaseNotes.title")}
|
|
|
+ description={language.t("settings.general.row.releaseNotes.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-release-notes">
|
|
|
+ <Switch
|
|
|
+ checked={settings.general.releaseNotes()}
|
|
|
+ onChange={(checked) => settings.general.setReleaseNotes(checked)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.updates.row.check.title")}
|
|
|
+ description={language.t("settings.updates.row.check.description")}
|
|
|
+ >
|
|
|
+ <ButtonV2 size="normal" variant="neutral" disabled={store.checking || !platform.checkUpdate} onClick={check}>
|
|
|
+ {store.checking
|
|
|
+ ? language.t("settings.updates.action.checking")
|
|
|
+ : language.t("settings.updates.action.checkNow")}
|
|
|
+ </ButtonV2>
|
|
|
+ </SettingsRowV2>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+
|
|
|
+ const DisplaySection = () => (
|
|
|
+ <Show when={desktop()}>
|
|
|
+ <div class="settings-v2-section">
|
|
|
+ <h3 class="settings-v2-section-title">{language.t("settings.general.section.display")}</h3>
|
|
|
+
|
|
|
+ <SettingsListV2>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={language.t("settings.general.row.pinchZoom.title")}
|
|
|
+ description={language.t("settings.general.row.pinchZoom.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-pinch-zoom">
|
|
|
+ <Switch checked={pinchZoom.latest} onChange={onPinchZoomChange} />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+
|
|
|
+ <Show when={linux()}>
|
|
|
+ <SettingsRowV2
|
|
|
+ title={
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span>{language.t("settings.general.row.wayland.title")}</span>
|
|
|
+ <Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
|
|
|
+ <span class="text-text-weak">
|
|
|
+ <Icon name="help" size="small" />
|
|
|
+ </span>
|
|
|
+ </Tooltip>
|
|
|
+ </div>
|
|
|
+ }
|
|
|
+ description={language.t("settings.general.row.wayland.description")}
|
|
|
+ >
|
|
|
+ <div data-action="settings-wayland">
|
|
|
+ <Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
|
|
|
+ </div>
|
|
|
+ </SettingsRowV2>
|
|
|
+ </Show>
|
|
|
+ </SettingsListV2>
|
|
|
+ </div>
|
|
|
+ </Show>
|
|
|
+ )
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <div class="settings-v2-tab-header">
|
|
|
+ <h2 class="settings-v2-tab-title">{language.t("settings.tab.general")}</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="settings-v2-tab-body">
|
|
|
+ <GeneralSection />
|
|
|
+
|
|
|
+ <AppearanceSection />
|
|
|
+
|
|
|
+ <NotificationsSection />
|
|
|
+
|
|
|
+ <SoundsSection />
|
|
|
+
|
|
|
+ <UpdatesSection />
|
|
|
+
|
|
|
+ <DisplaySection />
|
|
|
+
|
|
|
+ <Show when={desktop()}>
|
|
|
+ <AdvancedSection />
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )
|
|
|
+}
|