From 8e2c7549cacacd3bb8bdb6073a0f723e825f63a3 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 3 Jun 2026 16:00:32 +0300 Subject: [PATCH] fix: improve OpenCode settings handling Improves OpenCode CLI and shortcut settings flows Updates runtime API and persistence handling Adds coverage for settings helper behavior --- .../src/components/auth/SessionAuthGate.tsx | 37 ++++++++++++- .../openchamber/KeyboardShortcutsSettings.tsx | 23 ++++++-- .../openchamber/OpenCodeCliSettings.tsx | 7 ++- .../components/update/OpenCodeUpdateToast.tsx | 2 + packages/ui/src/lib/api/types.ts | 3 ++ packages/ui/src/lib/desktop.ts | 3 ++ packages/ui/src/lib/persistence.ts | 53 +++++++++++++++++-- .../server/lib/opencode/settings-helpers.js | 31 ++++++++++- .../lib/opencode/settings-helpers.test.js | 48 +++++++++++++++++ 9 files changed, 197 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/components/auth/SessionAuthGate.tsx b/packages/ui/src/components/auth/SessionAuthGate.tsx index dfcc5e9e..5a7c252b 100644 --- a/packages/ui/src/components/auth/SessionAuthGate.tsx +++ b/packages/ui/src/components/auth/SessionAuthGate.tsx @@ -125,6 +125,10 @@ const issueDesktopClientToken = async (): Promise => { return typeof payload?.token === 'string' ? payload.token.trim() : ''; }; +const shouldUseDesktopShellPasswordLogin = (): boolean => { + return isDesktopShell() && !isLocalDesktopRuntime(); +}; + const issueDesktopClientTokenViaShell = async (password: string, trustDevice: boolean): Promise => { if (!isDesktopShell() || typeof window === 'undefined') { return ''; @@ -220,7 +224,7 @@ const LoadingScreen: React.FC = () => ( ); -const ErrorScreen: React.FC = ({ onRetry, errorType = 'network', retryAfter }) => { +const ErrorScreen: React.FC = ({ onRetry, errorType = 'network', retryAfter, children }) => { const { t } = useI18n(); const isRateLimit = errorType === 'rate-limit'; const minutes = retryAfter ? Math.ceil(retryAfter / 60) : 1; @@ -243,6 +247,7 @@ const ErrorScreen: React.FC = ({ onRetry, errorType = 'network + {children} ); @@ -258,6 +263,7 @@ interface ErrorScreenProps { onRetry: () => void; errorType?: 'network' | 'rate-limit'; retryAfter?: number; + children?: React.ReactNode; } export const SessionAuthGate: React.FC = ({ children }) => { @@ -381,6 +387,12 @@ export const SessionAuthGate: React.FC = ({ children }) => setIsTunnelLocked(false); } catch (error) { console.warn('Failed to check session status:', error); + if (shouldUseDesktopShellPasswordLogin()) { + setState('locked'); + setRetryAfter(undefined); + setIsTunnelLocked(false); + return; + } setState('error'); setIsTunnelLocked(false); } @@ -529,6 +541,16 @@ export const SessionAuthGate: React.FC = ({ children }) => setState('error'); } catch (error) { console.warn('Failed to submit UI password:', error); + const clientToken = shouldUseDesktopShellPasswordLogin() + ? await issueDesktopClientTokenViaShell(password, trustDevice) + : ''; + if (clientToken) { + setPassword(''); + setIsTunnelLocked(false); + await applyDesktopClientToken(clientToken); + setState('authenticated'); + return; + } setErrorMessage(t('sessionAuth.error.networkRetry')); setIsTunnelLocked(false); setState('error'); @@ -620,7 +642,18 @@ export const SessionAuthGate: React.FC = ({ children }) => } if (state === 'error') { - return void checkStatus()} errorType="network" />; + return ( + void checkStatus()} errorType="network"> + {showHostSwitcher && ( +
+ +

+ {t('sessionAuth.locked.hostSwitcherHint')} +

+
+ )} +
+ ); } if (state === 'rate-limited') { diff --git a/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx b/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx index 8dbd9108..1e1b3c44 100644 --- a/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/KeyboardShortcutsSettings.tsx @@ -5,6 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { Icon } from "@/components/icon/Icon"; import { useUIStore } from '@/stores/useUIStore'; import { cn } from '@/lib/utils'; +import { updateDesktopSettings } from '@/lib/persistence'; import { formatShortcutForDisplay, getCustomizableShortcutActions, @@ -70,6 +71,10 @@ export const KeyboardShortcutsSettings: React.FC = () => { conflictActionId: string; } | null>(null); + const persistShortcutOverrides = React.useCallback((nextOverrides: Record) => { + void updateDesktopSettings({ shortcutOverrides: nextOverrides }); + }, []); + const findConflict = React.useCallback((actionId: string, combo: ShortcutCombo): string | null => { const normalized = normalizeCombo(combo); for (const action of actions) { @@ -93,7 +98,9 @@ export const KeyboardShortcutsSettings: React.FC = () => { return; } + const nextOverrides = { ...shortcutOverrides, [actionId]: normalized }; setShortcutOverride(actionId, normalized); + persistShortcutOverrides(nextOverrides); setPendingOverwrite(null); setErrorText(''); setWarningText(isRiskyBrowserShortcut(normalized) ? t('settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut') : ''); @@ -102,15 +109,21 @@ export const KeyboardShortcutsSettings: React.FC = () => { delete rest[actionId]; return rest; }); - }, [findConflict, setShortcutOverride, t]); + }, [findConflict, persistShortcutOverrides, setShortcutOverride, shortcutOverrides, t]); const confirmOverwrite = React.useCallback(() => { if (!pendingOverwrite) { return; } + const nextOverrides = { + ...shortcutOverrides, + [pendingOverwrite.conflictActionId]: UNASSIGNED_SHORTCUT, + [pendingOverwrite.actionId]: pendingOverwrite.combo, + }; setShortcutOverride(pendingOverwrite.conflictActionId, UNASSIGNED_SHORTCUT); setShortcutOverride(pendingOverwrite.actionId, pendingOverwrite.combo); + persistShortcutOverrides(nextOverrides); setPendingOverwrite(null); setErrorText(''); setWarningText(isRiskyBrowserShortcut(pendingOverwrite.combo) ? t('settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut') : ''); @@ -119,10 +132,13 @@ export const KeyboardShortcutsSettings: React.FC = () => { delete rest[pendingOverwrite.actionId]; return rest; }); - }, [pendingOverwrite, setShortcutOverride, t]); + }, [pendingOverwrite, persistShortcutOverrides, setShortcutOverride, shortcutOverrides, t]); const resetOne = React.useCallback((actionId: string) => { + const nextOverrides = { ...shortcutOverrides }; + delete nextOverrides[actionId]; clearShortcutOverride(actionId); + persistShortcutOverrides(nextOverrides); setDraftByAction((current) => { const rest = { ...current }; delete rest[actionId]; @@ -131,7 +147,7 @@ export const KeyboardShortcutsSettings: React.FC = () => { setPendingOverwrite(null); setErrorText(''); setWarningText(''); - }, [clearShortcutOverride]); + }, [clearShortcutOverride, persistShortcutOverrides, shortcutOverrides]); return (
@@ -145,6 +161,7 @@ export const KeyboardShortcutsSettings: React.FC = () => { className="!font-normal" onClick={() => { resetAllShortcutOverrides(); + persistShortcutOverrides({}); setDraftByAction({}); setPendingOverwrite(null); setErrorText(''); diff --git a/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx index 6fa6a9d8..d540eab4 100644 --- a/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenCodeCliSettings.tsx @@ -82,6 +82,11 @@ export const OpenCodeCliSettings: React.FC = () => { } }, [t, value]); + const handleShowUpdateNotificationsChange = React.useCallback((enabled: boolean) => { + setShowOpenCodeUpdateNotifications(enabled); + void updateDesktopSettings({ showOpenCodeUpdateNotifications: enabled }); + }, [setShowOpenCodeUpdateNotifications]); + return (
@@ -147,7 +152,7 @@ export const OpenCodeCliSettings: React.FC = () => {