fix(app): align settings with v2 APIs

This commit is contained in:
LukeParkerDev 2026-08-07 20:03:34 +10:00
parent 80d7766348
commit a2c3aefcaf
6 changed files with 99 additions and 52 deletions

View file

@ -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<SkillItem[]> =>
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 = () => {
<div class="project-settings-extension-section">
<div class="project-settings-extension-section-header">
<span>{language.t("project.settings.extensions.added")}</span>
<ExternalLink class="project-settings-extension-link" href="https://opencode.ai/docs/skills/">
<Link class="project-settings-extension-link" href="https://opencode.ai/docs/skills/">
{language.t("settings.extensions.addSkills")}
</ExternalLink>
</Link>
</div>
<Show when={projectSkills().length > 0}>
<ExtensionCard>{skillRows(projectSkills())}</ExtensionCard>

View file

@ -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")}{" "}
<ExternalLink class="settings-v2-link" href="https://opencode.ai/docs/themes/">
<Link class="settings-v2-link" href="https://opencode.ai/docs/themes/">
{language.t("common.learnMore")}
</ExternalLink>
</Link>
</>
}
>

View file

@ -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 = () => {
<span class="text-13-medium text-v2-text-text-base">
{language.t("settings.extensions.availableAll")}
</span>
<ExternalLink
<Link
class="text-13-regular text-v2-text-accent hover:underline"
href="https://opencode.ai/docs/skills/"
>
{language.t("settings.extensions.addSkills")}
</ExternalLink>
</Link>
</div>
<div class="bg-[var(--v2-background-bg-base)] border-[0.5px] border-[var(--v2-border-border-base)] rounded-[8px] pl-4 pr-3 overflow-hidden">
<For each={skills()}>

View file

@ -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<string, number>())
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?.()

View file

@ -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",

View file

@ -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",