summaryrefslogtreecommitdiffhomepage
path: root/packages/app
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-04-27 10:54:55 +1000
committerGitHub <[email protected]>2026-04-27 00:54:55 +0000
commit141f33d24bdc059aa26bd1e32c9416ac3aed36e1 (patch)
treeffd2f8b70439cdc134cb3b7e70c7fbf7130f1b81 /packages/app
parentc4d8a8183e6c2d15831767f1b898a8d0ed0297b9 (diff)
downloadopencode-141f33d24bdc059aa26bd1e32c9416ac3aed36e1.tar.gz
opencode-141f33d24bdc059aa26bd1e32c9416ac3aed36e1.zip
feat: configurable shell selection + desktop settings UI (#20602)
Diffstat (limited to 'packages/app')
-rw-r--r--packages/app/src/components/settings-general.tsx189
-rw-r--r--packages/app/src/context/global-sync/bootstrap.ts7
-rw-r--r--packages/app/src/i18n/en.ts5
3 files changed, 132 insertions, 69 deletions
diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index f38442379..8060ae94d 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -11,7 +11,9 @@ import { showToast } from "@opencode-ai/ui/toast"
import { useParams } from "@solidjs/router"
import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission"
-import { usePlatform } from "@/context/platform"
+import { usePlatform, type DisplayBackend } from "@/context/platform"
+import { useGlobalSync } from "@/context/global-sync"
+import { useGlobalSDK } from "@/context/global-sdk"
import {
monoDefault,
monoFontFamily,
@@ -40,6 +42,18 @@ type ThemeOption = {
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 = () => {
@@ -75,10 +89,6 @@ export const SettingsGeneral: Component = () => {
const params = useParams()
const settings = useSettings()
- onMount(() => {
- void theme.loadThemes()
- })
-
const [store, setStore] = createStore({
checking: false,
})
@@ -165,6 +175,70 @@ export const SettingsGeneral: Component = () => {
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
+ const globalSync = useGlobalSync()
+ const globalSdk = useGlobalSDK()
+
+ 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 },
+ )
+
+ 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 colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
{ value: "system", label: language.t("theme.scheme.system") },
{ value: "light", label: language.t("theme.scheme.light") },
@@ -244,6 +318,27 @@ export const SettingsGeneral: Component = () => {
</SettingsRow>
<SettingsRow
+ title={language.t("settings.general.row.shell.title")}
+ description={language.t("settings.general.row.shell.description")}
+ >
+ <Select
+ 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
+ globalSync.updateConfig({ shell: option.value })
+ }}
+ variant="secondary"
+ size="small"
+ triggerVariant="settings"
+ triggerStyle={{ "min-width": "180px" }}
+ />
+ </SettingsRow>
+
+ <SettingsRow
title={language.t("settings.general.row.reasoningSummaries.title")}
description={language.t("settings.general.row.reasoningSummaries.description")}
>
@@ -651,70 +746,32 @@ export const SettingsGeneral: Component = () => {
<SoundsSection />
- {/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
- {(_) => {
- const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
- const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
-
- return (
- <div class="flex flex-col gap-1">
- <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.desktop.section.wsl")}</h3>
-
- <SettingsList>
- <SettingsRow
- title={language.t("settings.desktop.wsl.title")}
- description={language.t("settings.desktop.wsl.description")}
- >
- <div data-action="settings-wsl">
- <Switch
- checked={enabled() ?? false}
- disabled={enabledResource.state === "pending"}
- onChange={(checked) => platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())}
- />
- </div>
- </SettingsRow>
- </SettingsList>
- </div>
- )
- }}
- </Show>*/}
-
<UpdatesSection />
<Show when={linux()}>
- {(_) => {
- const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
- const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)
-
- const onChange = (checked: boolean) =>
- platform.setDisplayBackend?.(checked ? "wayland" : "auto").finally(() => actions.refetch())
-
- return (
- <div class="flex flex-col gap-1">
- <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
-
- <SettingsList>
- <SettingsRow
- 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={value() === "wayland"} onChange={onChange} />
- </div>
- </SettingsRow>
- </SettingsList>
- </div>
- )
- }}
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
+
+ <SettingsList>
+ <SettingsRow
+ 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>
+ </SettingsRow>
+ </SettingsList>
+ </div>
</Show>
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}>
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index 66f4a3b15..a83030fad 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -78,7 +78,7 @@ export async function bootstrapGlobal(input: {
() =>
retry(() =>
input.globalSDK.global.config.get().then((x) => {
- input.setGlobalStore("config", x.data!)
+ input.setGlobalStore("config", reconcile(x.data!, { merge: false }))
}),
),
]
@@ -245,7 +245,7 @@ export async function bootstrapDirectory(input: {
input.setStore("provider", input.global.provider)
}
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
- input.setStore("config", input.global.config)
+ input.setStore("config", reconcile(input.global.config, { merge: false }))
}
if (loading || input.store.provider.all.length === 0) {
input.setStore("provider_ready", false)
@@ -265,7 +265,8 @@ export async function bootstrapDirectory(input: {
input.queryClient.ensureQueryData(
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
),
- () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
+ () =>
+ retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
!seededProject &&
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 7326f7c8b..eae5aeb94 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -728,6 +728,11 @@ export const dict = {
"settings.general.row.language.title": "Language",
"settings.general.row.language.description": "Change the display language for OpenCode",
+ "settings.general.row.shell.title": "Terminal Shell",
+ "settings.general.row.shell.description":
+ "Choose the shell used for your terminal. Compatible shells are also used for agent tool calls.",
+ "settings.general.row.shell.autoDefault": "Auto (Default)",
+ "settings.general.row.shell.terminalOnly": "terminal only",
"settings.general.row.appearance.title": "Appearance",
"settings.general.row.appearance.description": "Customise how OpenCode looks on your device",
"settings.general.row.colorScheme.title": "Color scheme",