From 5f139c961270522383395ae4bb6baa2705720a74 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:26:00 +1000 Subject: [PATCH] feat(app): redesign non-modal settings --- .../src/components/server/server-row-menu.tsx | 9 +- .../src/components/settings-v2/appearance.tsx | 202 +++++++++++++++++ .../settings-v2/dialog-settings-v2.tsx | 98 +++++--- .../src/components/settings-v2/extensions.tsx | 214 ++++++++++++++++++ .../app/src/components/settings-v2/index.tsx | 8 + .../app/src/components/settings-v2/models.tsx | 15 +- .../components/settings-v2/notifications.tsx | 183 +++++++++++++++ .../settings-v2/parts/server-select.tsx | 72 ++++++ .../src/components/settings-v2/projects.tsx | 97 ++++++++ .../src/components/settings-v2/providers.tsx | 16 +- .../src/components/settings-v2/servers.tsx | 32 ++- .../components/settings-v2/settings-v2.css | 45 ++++ packages/app/src/i18n/ar.ts | 44 ++++ packages/app/src/i18n/br.ts | 44 ++++ packages/app/src/i18n/bs.ts | 44 ++++ packages/app/src/i18n/en.ts | 38 +++- packages/app/src/i18n/es.ts | 44 ++++ packages/app/src/i18n/no.ts | 44 ++++ packages/app/src/i18n/pl.ts | 44 ++++ packages/app/src/i18n/ru.ts | 44 ++++ packages/app/src/i18n/th.ts | 44 ++++ packages/app/src/i18n/tr.ts | 44 ++++ packages/app/src/i18n/uk.ts | 44 ++++ packages/app/src/i18n/zh.ts | 44 ++++ packages/app/src/i18n/zht.ts | 44 ++++ packages/app/src/wsl/settings.tsx | 12 +- packages/ui/src/components/icon.tsx | 5 + 27 files changed, 1520 insertions(+), 54 deletions(-) create mode 100644 packages/app/src/components/settings-v2/appearance.tsx create mode 100644 packages/app/src/components/settings-v2/extensions.tsx create mode 100644 packages/app/src/components/settings-v2/notifications.tsx create mode 100644 packages/app/src/components/settings-v2/parts/server-select.tsx create mode 100644 packages/app/src/components/settings-v2/projects.tsx diff --git a/packages/app/src/components/server/server-row-menu.tsx b/packages/app/src/components/server/server-row-menu.tsx index ee4a46db070..3c6a6299457 100644 --- a/packages/app/src/components/server/server-row-menu.tsx +++ b/packages/app/src/components/server/server-row-menu.tsx @@ -80,11 +80,10 @@ export const ServerRowMenuView: Component<{ > {props.labels.edit} - - {props.labels.default} - - - {props.labels.defaultRemove} + + + {props.labels.default} + diff --git a/packages/app/src/components/settings-v2/appearance.tsx b/packages/app/src/components/settings-v2/appearance.tsx new file mode 100644 index 00000000000..8465de4a2bf --- /dev/null +++ b/packages/app/src/components/settings-v2/appearance.tsx @@ -0,0 +1,202 @@ +import { Component, Show, createMemo, createResource, createSignal } from "solid-js" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +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 { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { ExternalLink } from "../external-link" +import { SettingsListV2 } from "./parts/list" +import { SettingsRowV2 } from "./parts/row" +import { + createAppearanceSettingsController, + type AppearanceSettingsController, +} from "./general-controllers" +import "./settings-v2.css" + +const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"] +const densityOptions: ("compact" | "default" | "comfortable")[] = ["compact", "default", "comfortable"] + +const fontSettings = { + ui: { + action: "settings-ui-font", + title: "settings.general.row.uiFont.title", + description: "settings.general.row.uiFont.description", + font: "ui", + input: "setUI", + }, + code: { + action: "settings-code-font", + title: "settings.general.row.font.title", + description: "settings.general.row.font.description", + font: "code", + input: "setCode", + }, + terminal: { + action: "settings-terminal-font", + title: "settings.general.row.terminalFont.title", + description: "settings.general.row.terminalFont.description", + font: "terminal", + input: "setTerminal", + }, +} as const + +const FontSetting: Component<{ + kind: "ui" | "code" | "terminal" + fonts: AppearanceSettingsController["fonts"] +}> = (props) => { + const language = useLanguage() + const config = () => fontSettings[props.kind] + return ( + +
+ props.fonts[config().input](event.currentTarget.value)} + placeholder={props.fonts[config().font]().placeholder} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + aria-label={language.t(config().title)} + style={{ "font-family": props.fonts[config().font]().family }} + /> +
+
+ ) +} + +export const SettingsAppearanceV2: Component = () => { + const language = useLanguage() + const platform = usePlatform() + const appearance = createAppearanceSettingsController() + const desktop = createMemo(() => platform.platform === "desktop") + const [density, setDensity] = createSignal<"compact" | "default" | "comfortable">("default") + + const [pinchZoom, { mutate: setPinchZoom }] = createResource( + () => desktop() && "getPinchZoomEnabled" in platform, + () => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false), + { initialValue: false }, + ) + + const onPinchZoomChange = (checked: boolean) => { + setPinchZoom(checked) + const update = platform.setPinchZoomEnabled?.(checked) + if (!update) return + void update.catch(() => setPinchZoom(!checked)) + } + + const restoreDefaults = () => { + appearance.scheme.select("system") + appearance.fonts.setUI("") + appearance.fonts.setCode("") + appearance.fonts.setTerminal("") + setDensity("default") + } + + return ( + <> +
+
+

{language.t("settings.tab.appearance")}

+ + {language.t("settings.action.restoreDefaults")} + +
+
+ +
+
+ + + option === appearance.scheme.current())} + placement="bottom-end" + gutter={6} + label={(option) => { + if (option === "system") return language.t("theme.scheme.system") + if (option === "light") return language.t("theme.scheme.light") + return language.t("theme.scheme.dark") + }} + onSelect={(option) => option && appearance.scheme.select(option)} + /> + + + + {language.t("settings.general.row.theme.description")}{" "} + + {language.t("common.learnMore")} + + + } + > + option.id} + label={(option) => option.name} + onSelect={appearance.theme.select} + /> + + + + { + if (option === "compact") return language.t("settings.appearance.density.compact") + if (option === "comfortable") return language.t("settings.appearance.density.comfortable") + return language.t("settings.appearance.density.default") + }} + onSelect={(option) => option && setDensity(option)} + /> + + + + + + +
+ + +
+

{language.t("settings.general.section.display")}

+ + +
+ +
+
+
+
+
+
+ + ) +} diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index 8507846f877..59751d2c645 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -5,15 +5,19 @@ import { Icon } from "@opencode-ai/ui/icon" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { SettingsGeneralV2 } from "./general" +import { SettingsAppearanceV2 } from "./appearance" import { SettingsKeybinds } from "../settings-keybinds" +import { SettingsNotificationsV2 } from "./notifications" import { SettingsProvidersV2 } from "./providers" import { SettingsModelsV2 } from "./models" -import "./settings-v2.css" import { SettingsServersV2 } from "./servers" +import { SettingsProjectsV2 } from "./projects" +import { SettingsExtensionsV2 } from "./extensions" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useLayout } from "@/context/layout" import { useTabs } from "@/context/tabs" import { useServerSync } from "@/context/server-sync" +import "./settings-v2.css" export const DialogSettings: Component<{ sessionID?: string @@ -52,62 +56,90 @@ export const DialogSettings: Component<{ >
-
-
-
- {language.t("settings.section.desktop")} -
- - - {language.t("settings.tab.general")} - - - - {language.t("settings.tab.shortcuts")} - -
-
+
+ {/* Group 1: Preferences */} +
+ + + {language.t("settings.tab.general")} + + + + {language.t("settings.tab.appearance")} + + + + {language.t("settings.tab.notifications")} + + + + {language.t("settings.tab.shortcuts")} + +
-
- {language.t("settings.section.server")} -
- - - {language.t("status.popover.tab.servers")} - - - - {language.t("settings.providers.title")} - - - - {language.t("settings.models.title")} - -
-
+ {/* Group 2: Environment & Workspaces */} +
+ + + {language.t("settings.tab.servers")} + + + + {language.t("settings.tab.projects")} + +
+ + {/* Group 3: Capabilities & Extensions */} +
+ + + {language.t("settings.tab.providers")} + + + + {language.t("settings.tab.models")} + + + + {language.t("settings.tab.extensions")} +
+
+ + + + + + + + + + + + + ) diff --git a/packages/app/src/components/settings-v2/extensions.tsx b/packages/app/src/components/settings-v2/extensions.tsx new file mode 100644 index 00000000000..cb6d84d79e6 --- /dev/null +++ b/packages/app/src/components/settings-v2/extensions.tsx @@ -0,0 +1,214 @@ +import { Component, For, Show, createMemo, createSignal } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +import { Switch } from "@opencode-ai/ui/v2/switch-v2" +import { useLanguage } from "@/context/language" +import { ServerConnection, useServer } from "@/context/server" +import { useServerSync } from "@/context/server-sync" +import { ExternalLink } from "../external-link" +import { InlineServerSelect } from "./parts/server-select" +import "./settings-v2.css" + +type ExtensionSubTab = "mcps" | "plugins" | "skills" + +interface McpRowItem { + name: string + status?: string +} + +interface PluginRowItem { + name: string +} + +interface SkillRowItem { + name: string +} + +export const SettingsExtensionsV2: Component = () => { + const language = useLanguage() + const server = useServer() + const serverSync = useServerSync() + const [selectedServer, setSelectedServer] = createSignal(server.key) + const [activeSubTab, setActiveSubTab] = createSignal("mcps") + + const [mcpOverrides, setMcpOverrides] = createSignal>({}) + const mcps = createMemo(() => { + const configMcp = (serverSync().data.config as { mcp?: Record })?.mcp ?? {} + const configEntries = Object.entries(configMcp).map(([name, conf]) => ({ + name, + status: conf.disabled ? "disabled" : "connected", + })) + + const firstProject = server.projects.list()[0]?.worktree + const childMcp = firstProject ? serverSync().child(firstProject, { mcp: true })[0].mcp : {} + const childEntries = Object.entries(childMcp ?? {}).map(([name, stat]) => ({ + name, + status: stat?.status, + })) + + const map = new Map() + for (const item of configEntries) map.set(item.name, item) + for (const item of childEntries) { + const existing = map.get(item.name) + map.set(item.name, { ...existing, ...item }) + } + + return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name)) + }) + + const isMcpEnabled = (item: McpRowItem) => { + if (mcpOverrides()[item.name] !== undefined) return mcpOverrides()[item.name] + return item.status === "connected" + } + + const handleMcpToggle = (item: McpRowItem, checked: boolean) => { + setMcpOverrides((prev) => ({ ...prev, [item.name]: checked })) + const firstProject = server.projects.list()[0]?.worktree + if (firstProject) { + void serverSync().mcp.toggle(firstProject, item.name) + } + } + + const plugins = createMemo(() => { + const raw = serverSync().data.config.plugin ?? [] + return raw.map((item) => { + const name = typeof item === "string" ? item : item[0] + return { name } + }) + }) + + const skills = createMemo(() => { + const configSkills = (serverSync().data.config as { skills?: string[] })?.skills ?? [] + return configSkills.map((name) => ({ name })) + }) + + return ( + <> +
+
+
+

{language.t("settings.tab.extensions")}

+ {language.t("settings.mcps.description")} +
+ +
+
+ +
+ {/* Sub-Tabs Pill Strip */} +
+ + + +
+ + {/* MCPs Sub-Tab */} + +
+
+ + {language.t("settings.extensions.availableAll")} + + {language.t("settings.extensions.manageConfig")} +
+
+ + {(item) => ( +
+
+ + {item.name} +
+ handleMcpToggle(item, checked)} + hideLabel + > + {item.name} + +
+ )} +
+
+
+
+ + {/* Plugins Sub-Tab */} + +
+
+ + {language.t("settings.extensions.availableAll")} + + {language.t("settings.extensions.manageConfig")} +
+
+ + {(plugin) => ( +
+
+ + {plugin.name} +
+
+ )} +
+
+
+
+ + {/* Skills Sub-Tab */} + +
+
+ + {language.t("settings.extensions.availableAll")} + + + {language.t("settings.extensions.addSkills")} + +
+
+ + {(skill) => ( +
+
+ + {skill.name} +
+
+ )} +
+
+
+
+
+ + ) +} diff --git a/packages/app/src/components/settings-v2/index.tsx b/packages/app/src/components/settings-v2/index.tsx index 8ff58843798..cbece20af6e 100644 --- a/packages/app/src/components/settings-v2/index.tsx +++ b/packages/app/src/components/settings-v2/index.tsx @@ -1 +1,9 @@ export { DialogSettings } from "./dialog-settings-v2" +export { SettingsGeneralV2 } from "./general" +export { SettingsAppearanceV2 } from "./appearance" +export { SettingsNotificationsV2 } from "./notifications" +export { SettingsProvidersV2 } from "./providers" +export { SettingsModelsV2 } from "./models" +export { SettingsServersV2 } from "./servers" +export { SettingsProjectsV2 } from "./projects" +export { SettingsExtensionsV2 } from "./extensions" diff --git a/packages/app/src/components/settings-v2/models.tsx b/packages/app/src/components/settings-v2/models.tsx index 482e8cbffbd..b43ff6ef345 100644 --- a/packages/app/src/components/settings-v2/models.tsx +++ b/packages/app/src/components/settings-v2/models.tsx @@ -4,13 +4,15 @@ 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 { type Component, For, Show, createSignal } from "solid-js" import { createStore } from "solid-js/store" import { useLanguage } from "@/context/language" import { useModels } from "@/context/models" import { useServerSDK } from "@/context/server-sdk" +import { ServerConnection, useServer } from "@/context/server" import { popularProviders } from "@/hooks/use-providers" import { Persist, persisted } from "@/utils/persist" +import { InlineServerSelect } from "./parts/server-select" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import "./settings-v2.css" @@ -22,7 +24,9 @@ const PROVIDER_ICON_SIZE = 16 export const SettingsModelsV2: Component = () => { const language = useLanguage() const models = useModels() + const server = useServer() const serverSdk = useServerSDK() + const [selectedServer, setSelectedServer] = createSignal(server.key) const [store, setStore] = persisted( Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"), createStore({ collapsed: {} as Record }), @@ -53,7 +57,14 @@ export const SettingsModelsV2: Component = () => { return ( <>
-

{language.t("settings.models.title")}

+
+

{language.t("settings.models.title")}

+ +