Accumulate OpenCode settings restarts behind Apply & Restart

Defer OpenCode reloads after settings mutations, track pending changes,
and expose a top-right Apply & Restart OpenCode action with a counter so
sessions stay available until the user explicitly applies.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-03 06:56:38 +00:00
co-authored by Serhii Dziupin
parent 775da6e9f4
commit 57819dd164
31 changed files with 869 additions and 167 deletions
@@ -0,0 +1,81 @@
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { useI18n } from '@/lib/i18n';
import { applyPendingOpenCodeRestart } from '@/lib/opencode/deferredRestart';
import {
selectPendingOpenCodeRestartCount,
usePendingOpenCodeRestartStore,
} from '@/stores/usePendingOpenCodeRestartStore';
import { toast } from '@/components/ui';
import { cn } from '@/lib/utils';
type PendingOpenCodeRestartActionProps = {
className?: string;
compact?: boolean;
};
export const PendingOpenCodeRestartAction: React.FC<PendingOpenCodeRestartActionProps> = ({
className,
compact = false,
}) => {
const { t } = useI18n();
const pendingCount = usePendingOpenCodeRestartStore(selectPendingOpenCodeRestartCount);
const isApplying = usePendingOpenCodeRestartStore((state) => state.isApplying);
const handleApply = React.useCallback(async () => {
try {
const result = await applyPendingOpenCodeRestart({
message: t('settings.view.pendingRestart.applying'),
});
if (result.requiresManualRestart) {
toast.warning(t('settings.view.pendingRestart.manualRestartRequired'));
return;
}
if (result.ok) {
toast.success(t('settings.view.pendingRestart.applied'));
}
} catch (error) {
const message = error instanceof Error && error.message
? error.message
: t('settings.view.pendingRestart.applyFailed');
toast.error(message);
}
}, [t]);
if (pendingCount <= 0) {
return null;
}
const tooltip = pendingCount === 1
? t('settings.view.actions.applyAndRestartOpenCodeTooltipSingle')
: t('settings.view.actions.applyAndRestartOpenCodeTooltipPlural', { count: pendingCount });
return (
<Button
type="button"
size={compact ? 'xs' : 'sm'}
disabled={isApplying}
title={tooltip}
aria-label={tooltip}
onClick={() => void handleApply()}
className={cn('gap-1.5 shadow-sm', className)}
>
<Icon name="restart" className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">
{isApplying
? t('settings.view.pendingRestart.applying')
: t('settings.view.actions.applyAndRestartOpenCode')}
</span>
<span
className={cn(
'inline-flex min-w-5 items-center justify-center rounded-md px-1.5 typography-micro font-semibold',
'bg-primary-foreground/15 text-primary-foreground',
)}
aria-hidden="true"
>
{pendingCount}
</span>
</Button>
);
};
@@ -49,6 +49,11 @@ import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { McpIcon } from '@/components/icons/McpIcon';
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import { PendingOpenCodeRestartAction } from '@/components/views/PendingOpenCodeRestartAction';
import {
selectPendingOpenCodeRestartCount,
usePendingOpenCodeRestartStore,
} from '@/stores/usePendingOpenCodeRestartStore';
import {
SETTINGS_PAGE_METADATA,
getSettingsPageMeta,
@@ -235,6 +240,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const { t } = useI18n();
const deviceInfo = useDeviceInfo();
const isMobile = forceMobile ?? deviceInfo.isMobile;
const pendingRestartCount = usePendingOpenCodeRestartStore(selectPendingOpenCodeRestartCount);
const settingsPageRaw = useUIStore((state) => state.settingsPage);
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
@@ -974,7 +980,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
{/* Footer */}
<div className="overflow-hidden transition-opacity duration-150 opacity-100">
<div className="border-t border-border bg-background px-4 py-1 space-y-0.5 sm:bg-sidebar">
{!runtimeCtx.isVSCode && (
{!runtimeCtx.isVSCode && pendingRestartCount <= 0 && (
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -984,7 +990,20 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
'text-sm font-semibold text-sidebar-foreground/90',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
)}
onClick={() => void reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] }).catch(() => undefined)}
onClick={() => {
void (async () => {
try {
await reloadOpenCodeConfiguration({
message: t('settings.view.pendingRestart.applying'),
mode: 'projects',
scopes: ['all'],
});
usePendingOpenCodeRestartStore.getState().clear();
} catch {
// ignore
}
})();
}}
>
<Icon name="restart" className="h-4 w-4 shrink-0" />
<span>{t('settings.view.actions.reloadOpenCode')}</span>
@@ -1113,6 +1132,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
</button>
)}
<PendingOpenCodeRestartAction compact className="max-w-[min(100%,14rem)]" />
{onClose && (
<button
type="button"
@@ -1140,8 +1161,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
</div>
)}
{onClose && (
<div className={cn('absolute right-0.5 z-50', isWindowed ? 'top-0.5' : 'top-1')}>
<div className={cn('absolute right-0.5 z-50 flex items-center gap-1.5', isWindowed ? 'top-0.5' : 'top-1')}>
<PendingOpenCodeRestartAction compact className="max-w-[min(100%,16rem)]" />
{onClose && (
<button
type="button"
onClick={onClose}
@@ -1151,8 +1173,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
>
<Icon name="close" className="h-5 w-5" />
</button>
</div>
)}
)}
</div>
</>
)}