diff --git a/packages/app/src/components/server/server-row-menu.tsx b/packages/app/src/components/server/server-row-menu.tsx index 0a2920dec7f..6cd63b85a8c 100644 --- a/packages/app/src/components/server/server-row-menu.tsx +++ b/packages/app/src/components/server/server-row-menu.tsx @@ -78,11 +78,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..2ae41e5e208 --- /dev/null +++ b/packages/app/src/components/settings-v2/appearance.tsx @@ -0,0 +1,178 @@ +import { Component, Show, createMemo, createResource } 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 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 [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("") + } + + return ( + <> +
+
+

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

+ + {language.t("common.reset")} + +
+
+ +
+
+ + + 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} + /> + + + + + + +
+ + +
+

{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 df7c37b6845..fde2fa9e476 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 { SettingsWorkspacesV2 } from "./workspaces" +import { SettingsProjectsV2 } from "./projects" +import { SettingsExtensionsV2 } from "./extensions" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTabs } from "@/context/tabs" import { useServerSync } from "@/context/server-sync" +import "./settings-v2.css" export const DialogSettings: Component<{ sessionID?: string @@ -50,54 +54,73 @@ export const DialogSettings: Component<{ >
-
-
-
- {language.t("settings.section.desktop")} -
- - - {language.t("settings.tab.general")} - - - - {language.t("settings.tab.shortcuts")} - -
-
+
+
+ + + {language.t("settings.tab.preferences")} + + + + {language.t("settings.general.section.appearance")} + + + + {language.t("settings.tab.notifications")} + + + + {language.t("settings.tab.shortcuts")} + +
-
- {language.t("settings.section.server")} -
- - - {language.t("settings.tab.workspaces")} - - - - {language.t("status.popover.tab.servers")} - - - - {language.t("settings.providers.title")} - - - - {language.t("settings.models.title")} - -
-
+
+ + + {language.t("settings.tab.workspaces")} + + + + {language.t("status.popover.tab.servers")} + + + + {language.t("settings.tab.projects")} + +
+ +
+ + + {language.t("settings.providers.title")} + + + + {language.t("settings.models.title")} + + + + {language.t("settings.tab.extensions")} +
+
+ + + + + + + @@ -107,12 +130,18 @@ export const DialogSettings: Component<{ + + + + + + ) 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..02ed8096892 --- /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/general.tsx b/packages/app/src/components/settings-v2/general.tsx index 8e131f3a41d..fc6cce162f4 100644 --- a/packages/app/src/components/settings-v2/general.tsx +++ b/packages/app/src/components/settings-v2/general.tsx @@ -1,74 +1,25 @@ -import { Component, Show, createMemo, createResource } from "solid-js" +import { Component, Show, createMemo } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" 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 { useDialog } from "@opencode-ai/ui/context/dialog" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useUpdaterAction } from "../updater-action" import { type WorkspaceDefaultDestination, useSettings } from "@/context/settings" -import { ExternalLink } from "../external-link" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" import { - createAppearanceSettingsController, createPermissionScopeController, createShellOptions, createShellSettingsController, - createSoundSettingsController, - soundOptions, - type AppearanceSettingsController, type PermissionScopeController, type ShellSettingsController, - type SoundSettingsController, } from "./general-controllers" import "./settings-v2.css" -const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"] -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 soundSettings = { - agent: { - action: "settings-sounds-agent", - title: "settings.general.sounds.agent.title", - description: "settings.general.sounds.agent.description", - }, - permissions: { - action: "settings-sounds-permissions", - title: "settings.general.sounds.permissions.title", - description: "settings.general.sounds.permissions.description", - }, - errors: { - action: "settings-sounds-errors", - title: "settings.general.sounds.errors.title", - description: "settings.general.sounds.errors.description", - }, -} as const - const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => { const language = useLanguage() return ( @@ -147,130 +98,6 @@ const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) ) } -const AppearanceSection: Component<{ controller: AppearanceSettingsController }> = (props) => { - const language = useLanguage() - return ( -
-

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

- - - option === props.controller.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 && props.controller.scheme.select(option)} - /> - - - - {language.t("settings.general.row.theme.description")}{" "} - - {language.t("common.learnMore")} - - - } - > - option.id} - label={(option) => option.name} - onSelect={props.controller.theme.select} - /> - - - - - - -
- ) -} - -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 }} - /> -
-
- ) -} - -const SoundsSection: Component<{ controller: SoundSettingsController }> = (props) => { - const language = useLanguage() - return ( -
-

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

- - - - - -
- ) -} - -const SoundSetting: Component<{ - kind: "agent" | "permissions" | "errors" - channel: SoundSettingsController["agent"] -}> = (props) => { - const language = useLanguage() - const config = () => soundSettings[props.kind] - return ( - - option.id} - label={(option) => language.t(option.label)} - onHighlight={props.channel.highlight} - onSelect={props.channel.select} - placement="bottom-end" - gutter={6} - /> - - ) -} - const LanguageSetting = () => { const language = useLanguage() const options = createMemo(() => @@ -310,21 +137,17 @@ export const SettingsGeneralV2: Component<{ const updater = useUpdaterAction() const permissionScope = createPermissionScopeController(() => props.sessionID) const shell = createShellSettingsController() - const appearance = createAppearanceSettingsController() - const sounds = createSoundSettingsController() const desktop = createMemo(() => platform.platform === "desktop") - 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 = () => { + settings.general.setAutoSave(true) + settings.general.setShowReasoningSummaries(true) + settings.general.setShellToolPartsExpanded(false) + settings.general.setEditToolPartsExpanded(false) + settings.general.setShowFileTree(false) + settings.general.setShowSearch(false) + settings.general.setShowStatus(false) + settings.general.setShowCustomAgents(false) } const InterfaceSection = () => ( @@ -455,50 +278,7 @@ export const SettingsGeneralV2: Component<{ />
- -
- ) - const NotificationsSection = () => ( -
-

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

- - - -
- settings.notifications.setAgent(checked)} - /> -
-
- - -
- settings.notifications.setPermissions(checked)} - /> -
-
- - -
- settings.notifications.setErrors(checked)} - /> -
-
) @@ -532,30 +312,15 @@ export const SettingsGeneralV2: Component<{ ) - // We can probably remove this, right? - const DisplaySection = () => ( - -
-

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

- - - -
- -
-
-
-
-
- ) - return ( <>
-

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

+
+

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

+ + {language.t("common.reset")} + +
@@ -569,18 +334,10 @@ export const SettingsGeneralV2: Component<{ - - - - - - - -
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 7007cbef64a..5d9c9ab9479 100644 --- a/packages/app/src/components/settings-v2/models.tsx +++ b/packages/app/src/components/settings-v2/models.tsx @@ -4,14 +4,16 @@ 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 { ServerConnection, useServer } from "@/context/server" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" 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" @@ -23,8 +25,10 @@ const PROVIDER_ICON_SIZE = 16 export const SettingsModelsV2: Component = () => { const language = useLanguage() const models = useModels() + const server = useServer() const serverSdk = useServerSDK() useServerSync()().loadProviders() + const [selectedServer, setSelectedServer] = createSignal(server.key) const [store, setStore] = persisted( Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"), createStore({ collapsed: {} as Record }), @@ -55,7 +59,14 @@ export const SettingsModelsV2: Component = () => { return ( <>
-

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

+
+

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

+ +