fix: improve OpenCode settings handling

Improves OpenCode CLI and shortcut settings flows
Updates runtime API and persistence handling
Adds coverage for settings helper behavior
This commit is contained in:
Bohdan Triapitsyn
2026-06-03 16:00:32 +03:00
parent 570ae9dbc8
commit 8e2c7549ca
9 changed files with 197 additions and 10 deletions
@@ -125,6 +125,10 @@ const issueDesktopClientToken = async (): Promise<string> => {
return typeof payload?.token === 'string' ? payload.token.trim() : '';
};
const shouldUseDesktopShellPasswordLogin = (): boolean => {
return isDesktopShell() && !isLocalDesktopRuntime();
};
const issueDesktopClientTokenViaShell = async (password: string, trustDevice: boolean): Promise<string> => {
if (!isDesktopShell() || typeof window === 'undefined') {
return '';
@@ -220,7 +224,7 @@ const LoadingScreen: React.FC = () => (
</div>
);
const ErrorScreen: React.FC<ErrorScreenProps> = ({ onRetry, errorType = 'network', retryAfter }) => {
const ErrorScreen: React.FC<ErrorScreenProps> = ({ 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<ErrorScreenProps> = ({ onRetry, errorType = 'network
<Button type="button" onClick={onRetry} className="w-full max-w-xs">
{t('sessionAuth.error.retry')}
</Button>
{children}
</div>
</AuthShell>
);
@@ -258,6 +263,7 @@ interface ErrorScreenProps {
onRetry: () => void;
errorType?: 'network' | 'rate-limit';
retryAfter?: number;
children?: React.ReactNode;
}
export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ children }) => {
@@ -381,6 +387,12 @@ export const SessionAuthGate: React.FC<SessionAuthGateProps> = ({ 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<SessionAuthGateProps> = ({ 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<SessionAuthGateProps> = ({ children }) =>
}
if (state === 'error') {
return <ErrorScreen onRetry={() => void checkStatus()} errorType="network" />;
return (
<ErrorScreen onRetry={() => void checkStatus()} errorType="network">
{showHostSwitcher && (
<div className="w-full max-w-xs">
<DesktopHostSwitcherInline />
<p className="mt-1 text-center typography-micro text-muted-foreground">
{t('sessionAuth.locked.hostSwitcherHint')}
</p>
</div>
)}
</ErrorScreen>
);
}
if (state === 'rate-limited') {
@@ -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<string, ShortcutCombo>) => {
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 (
<div className="mb-8">
@@ -145,6 +161,7 @@ export const KeyboardShortcutsSettings: React.FC = () => {
className="!font-normal"
onClick={() => {
resetAllShortcutOverrides();
persistShortcutOverrides({});
setDraftByAction({});
setPendingOverwrite(null);
setErrorText('');
@@ -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 (
<div className="mb-8">
<div className="mb-1 px-1">
@@ -147,7 +152,7 @@ export const OpenCodeCliSettings: React.FC = () => {
<label className="flex cursor-pointer items-center gap-2 py-1.5">
<Checkbox
checked={showOpenCodeUpdateNotifications}
onChange={setShowOpenCodeUpdateNotifications}
onChange={handleShowUpdateNotificationsChange}
ariaLabel={t('settings.openchamber.opencodeCli.field.showUpdateNotificationsAria')}
/>
<span className="typography-ui-label text-foreground">
@@ -5,6 +5,7 @@ import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import { useUIStore } from '@/stores/useUIStore';
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { updateDesktopSettings } from '@/lib/persistence';
import { getSafeStorage } from '@/stores/utils/safeStorage';
import {
resolveOpenCodeUpdateVersion,
@@ -119,6 +120,7 @@ export const OpenCodeUpdateToast: React.FC = () => {
label: t('opencodeUpdate.toast.actions.dismiss'),
onClick: () => {
getSafeStorage().setItem(UPDATE_TOAST_DISMISSED_VERSION_KEY, version);
void updateDesktopSettings({ openCodeUpdateToastDismissedVersion: version });
toast.dismiss(UPDATE_TOAST_ID);
},
},