From a2c3aefcaf52e5776531e904ec4173cf9ee2d768 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:03:34 +1000 Subject: [PATCH] fix(app): align settings with v2 APIs --- .../project-settings-extensions.tsx | 10 +-- .../src/components/settings-v2/appearance.tsx | 6 +- .../src/components/settings-v2/extensions.tsx | 18 ++--- .../settings-v2/general-controllers.ts | 73 ++++++++++++++++++- .../src/components/settings-v2/providers.tsx | 39 +++------- packages/app/src/i18n/parity.test.ts | 5 ++ 6 files changed, 99 insertions(+), 52 deletions(-) diff --git a/packages/app/src/components/project-settings-extensions.tsx b/packages/app/src/components/project-settings-extensions.tsx index 9505c71d6a1..0e42d665168 100644 --- a/packages/app/src/components/project-settings-extensions.tsx +++ b/packages/app/src/components/project-settings-extensions.tsx @@ -8,7 +8,7 @@ import { useSDK } from "@/context/sdk" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useSync } from "@/context/sync" -import { ExternalLink } from "./external-link" +import { Link } from "./link" type SkillItem = { name: string @@ -111,9 +111,7 @@ export const ProjectSettingsExtensions: Component = () => { const [serverSkills] = createResource( serverSDK, (sdk): Promise => - sdk.api.skill - .list() - .then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))), + sdk.api.skill.list().then((result) => result.data.map((item) => ({ name: item.name, location: item.location }))), { initialValue: [] }, ) const [directorySkills] = createResource( @@ -200,9 +198,9 @@ export const ProjectSettingsExtensions: Component = () => {
{language.t("project.settings.extensions.added")} - + {language.t("settings.extensions.addSkills")} - +
0}> {skillRows(projectSkills())} diff --git a/packages/app/src/components/settings-v2/appearance.tsx b/packages/app/src/components/settings-v2/appearance.tsx index beb72b0b850..c9a937081ec 100644 --- a/packages/app/src/components/settings-v2/appearance.tsx +++ b/packages/app/src/components/settings-v2/appearance.tsx @@ -2,7 +2,7 @@ import { Component, createMemo } from "solid-js" import { SelectV2 } from "@opencode-ai/ui/v2/select-v2" import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" import { useLanguage } from "@/context/language" -import { ExternalLink } from "../external-link" +import { Link } from "../link" import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import { createAppearanceSettingsController, type AppearanceSettingsController } from "./general-controllers" @@ -104,9 +104,9 @@ export const SettingsAppearanceV2: Component = () => { description={ <> {language.t("settings.general.row.theme.description")}{" "} - + {language.t("common.learnMore")} - + } > diff --git a/packages/app/src/components/settings-v2/extensions.tsx b/packages/app/src/components/settings-v2/extensions.tsx index bbe4e26453c..efb09093622 100644 --- a/packages/app/src/components/settings-v2/extensions.tsx +++ b/packages/app/src/components/settings-v2/extensions.tsx @@ -5,7 +5,7 @@ import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" -import { ExternalLink } from "../external-link" +import { Link } from "../link" import { InlineServerSelect } from "./parts/server-select" import "./settings-v2.css" @@ -26,14 +26,14 @@ export const SettingsExtensionsV2: Component = () => { const configMcp = serverSync().data.config.mcp ?? {} return Object.entries(configMcp).map(([name, config]) => ({ name, - enabled: config.enabled !== false, + enabled: typeof config !== "object" || config === null || !("enabled" in config) || config.enabled !== false, })) }) const handleMcpToggle = (item: McpRowItem, checked: boolean) => { const before = serverSync().data.config.mcp ?? {} const config = before[item.name] - if (!config) return + if (typeof config !== "object" || config === null) return const next = { ...before, [item.name]: { ...config, enabled: checked } } serverSync().set("config", "mcp", next) void serverSync() @@ -49,11 +49,9 @@ export const SettingsExtensionsV2: Component = () => { }) }) - const [skills] = createResource( - serverSdk, - (sdk) => sdk.api.skill.list().then((result) => result.data), - { initialValue: [] }, - ) + const [skills] = createResource(serverSdk, (sdk) => sdk.api.skill.list().then((result) => result.data), { + initialValue: [], + }) return ( <> @@ -130,12 +128,12 @@ export const SettingsExtensionsV2: Component = () => { {language.t("settings.extensions.availableAll")} - {language.t("settings.extensions.addSkills")} - +
diff --git a/packages/app/src/components/settings-v2/general-controllers.ts b/packages/app/src/components/settings-v2/general-controllers.ts index 9893cdf9a32..09f3d9ed4ee 100644 --- a/packages/app/src/components/settings-v2/general-controllers.ts +++ b/packages/app/src/components/settings-v2/general-controllers.ts @@ -1,4 +1,4 @@ -import { createMemo, onCleanup, onMount, type Accessor } from "solid-js" +import { createMemo, createResource, onCleanup, onMount, type Accessor } from "solid-js" import type { ColorScheme } from "@opencode-ai/ui/theme/context" import { useTheme } from "@opencode-ai/ui/theme/context" import { @@ -14,6 +14,57 @@ import { useSettings, } from "@/context/settings" import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" +import { useServerSync } from "@/context/server-sync" + +type ShellOption = { + path: string + name: string + acceptable: boolean +} + +type ShellSelectOption = { + id: string + value: string + name: string + terminalOnly: boolean +} + +export function createShellOptions(input: { shells: ShellOption[]; current: string | undefined }) { + const counts = input.shells.reduce((result, shell) => { + result.set(shell.name, (result.get(shell.name) ?? 0) + 1) + return result + }, new Map()) + const options: ShellSelectOption[] = [ + { id: "auto", value: "", name: "", terminalOnly: false }, + ...input.shells.map((shell) => { + const ambiguous = (counts.get(shell.name) ?? 0) > 1 + return { + id: shell.path, + value: ambiguous ? shell.path : shell.name, + name: ambiguous ? shell.path : shell.name, + terminalOnly: !shell.acceptable, + } + }), + ] + if (input.current && !options.some((option) => option.value === input.current)) { + options.push({ id: input.current, value: input.current, name: input.current, terminalOnly: false }) + } + return options +} + +export function createShellSettingsController() { + const serverSync = useServerSync() + const [shells] = createResource(async () => [] as ShellOption[], { initialValue: [] as ShellOption[] }) + const current = createMemo(() => serverSync().data.config.shell ?? "") + return { + shells: () => shells.latest, + current, + select: (value: string) => { + if (value === current()) return + void serverSync().updateConfig({ shell: value }) + }, + } +} export function createAppearanceSettingsController() { const settings = useSettings() @@ -86,19 +137,33 @@ export function createSoundSettingsController() { }, }) return { - agent: channel(settings.sounds.agentEnabled, settings.sounds.agent, settings.sounds.setAgentEnabled, settings.sounds.setAgent), + agent: channel( + settings.sounds.agentEnabled, + settings.sounds.agent, + settings.sounds.setAgentEnabled, + settings.sounds.setAgent, + ), permissions: channel( settings.sounds.permissionsEnabled, settings.sounds.permissions, settings.sounds.setPermissionsEnabled, settings.sounds.setPermissions, ), - errors: channel(settings.sounds.errorsEnabled, settings.sounds.errors, settings.sounds.setErrorsEnabled, settings.sounds.setErrors), + errors: channel( + settings.sounds.errorsEnabled, + settings.sounds.errors, + settings.sounds.setErrorsEnabled, + settings.sounds.setErrors, + ), } } function soundPreview() { - const state = { cleanup: undefined as (() => void) | undefined, timeout: undefined as NodeJS.Timeout | undefined, run: 0 } + const state = { + cleanup: undefined as (() => void) | undefined, + timeout: undefined as NodeJS.Timeout | undefined, + run: 0, + } const stop = () => { state.run += 1 state.cleanup?.() diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 026eafdf4ec..35a1588aad8 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -52,9 +52,15 @@ export const SettingsProvidersV2: Component<{ } const connected = createMemo(() => { - return providers - .connected() - .filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)) + return providers.connected().filter( + (provider) => + provider.id !== "opencode" || + Object.values(provider.models).some((model) => { + if (typeof model !== "object" || model === null || !("cost" in model)) return false + const cost = model.cost + return typeof cost === "object" && cost !== null && "input" in cost + }), + ) }) const popular = createMemo(() => { @@ -98,29 +104,6 @@ export const SettingsProvidersV2: Component<{ return true } - const disableProvider = async (providerID: string, name: string) => { - return - const before = serverSync().data.config.disabled_providers ?? [] - const next = before.includes(providerID) ? before : [...before, providerID] - sync.set("config", "disabled_providers", next) - - await sync - .updateConfig({ disabled_providers: next }) - .then(() => { - showToast({ - variant: "success", - icon: "circle-check", - title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), - description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), - }) - }) - .catch((err: unknown) => { - sync.set("config", "disabled_providers", before) - const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) - }) - } - const disconnect = async (providerID: string, name: string) => { const location = props.directory() ? { directory: props.directory() } : undefined await serverSdk() @@ -129,9 +112,7 @@ export const SettingsProvidersV2: Component<{ const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? [] if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`) await Promise.all( - credentials.map((credential) => - serverSdk().api.credential.remove({ credentialID: credential.id, location }), - ), + credentials.map((credential) => serverSdk().api.credential.remove({ credentialID: credential.id, location })), ) showToast({ variant: "success", diff --git a/packages/app/src/i18n/parity.test.ts b/packages/app/src/i18n/parity.test.ts index b524acdbbed..22321e69fbe 100644 --- a/packages/app/src/i18n/parity.test.ts +++ b/packages/app/src/i18n/parity.test.ts @@ -21,6 +21,11 @@ const appLocales = [ ] as const const desktopLocales = appLocales.filter((locale) => locale !== "th" && locale !== "tr") const appFallbackKeys = new Set([ + "dialog.provider.custom.label", + "dialog.model.unpaid.viewMoreProviders", + "session.header.reveal.finder", + "session.header.reveal.fileExplorer", + "session.header.reveal.containingFolder", "command.session.export", "command.session.export.description", "context.export.session",