diff options
| author | Luke Parker <[email protected]> | 2026-04-27 10:54:55 +1000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-27 00:54:55 +0000 |
| commit | 141f33d24bdc059aa26bd1e32c9416ac3aed36e1 (patch) | |
| tree | ffd2f8b70439cdc134cb3b7e70c7fbf7130f1b81 /packages/app | |
| parent | c4d8a8183e6c2d15831767f1b898a8d0ed0297b9 (diff) | |
| download | opencode-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.tsx | 189 | ||||
| -rw-r--r-- | packages/app/src/context/global-sync/bootstrap.ts | 7 | ||||
| -rw-r--r-- | packages/app/src/i18n/en.ts | 5 |
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", |
