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
@@ -243,6 +243,8 @@ export const AgentsPage: React.FC = () => {
if (result.ok) {
if (result.requiresManualRestart) {
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
} else if (result.restartDeferred) {
toast.success(t('settings.view.pendingRestart.saved'));
} else {
toast.success(isNewAgent ? t('settings.agents.page.toast.created') : t('settings.agents.page.toast.updated'));
}
@@ -188,6 +188,8 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
if (result.ok) {
if (result.requiresManualRestart) {
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
} else if (result.restartDeferred) {
toast.success(t('settings.view.pendingRestart.saved'));
} else if (confirmActionType === 'delete') {
toast.success(t('settings.agents.sidebar.toast.agentDeleted', { name: confirmActionAgent.name }));
} else {
@@ -20,7 +20,7 @@ import {
} from '@/lib/responseStyle';
import type { DesktopSettings } from '@/lib/desktop';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import { noteDeferredRestartFromPayload, recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
import {
SettingsSection,
@@ -242,13 +242,18 @@ export const BehaviorPage: React.FC = () => {
throw new Error(await readApiError(response, t('settings.behavior.page.toast.saveFailed')));
}
const payload = await response.json().catch(() => null);
if (!noteDeferredRestartFromPayload(payload, 'behavior', { id: 'agents-md' })) {
recordDeferredOpenCodeRestart('behavior', { id: 'agents-md' });
}
await saveBehaviorSetting({
globalBehaviorPrompt: content,
}, t('settings.behavior.page.toast.saveFailed'));
setPrompt(content);
setInitialPrompt(content);
toast.success(t('settings.behavior.page.toast.saved'));
toast.success(t('settings.view.pendingRestart.saved'));
} catch (error) {
console.error('Failed to save behavior:', error);
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
@@ -266,19 +271,8 @@ export const BehaviorPage: React.FC = () => {
t('settings.behavior.page.toast.saveFailed'),
);
setInitialOptimizeSystemPrompt(optimizeSystemPrompt);
} catch (error) {
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
toast.error(message);
setIsApplyingPromptOptimization(false);
return;
}
try {
await reloadOpenCodeConfiguration({
message: t('settings.behavior.page.systemPromptOptimization.restarting'),
mode: 'projects',
scopes: ['all'],
});
recordDeferredOpenCodeRestart('behavior', { id: 'optimize-system-prompt' });
toast.success(t('settings.view.pendingRestart.saved'));
} catch (error) {
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
toast.error(message);
@@ -317,7 +311,7 @@ export const BehaviorPage: React.FC = () => {
>
{isApplyingPromptOptimization
? t('settings.common.actions.saving')
: t('settings.openchamber.opencodeCli.actions.saveAndReload')}
: t('settings.common.actions.saveChanges')}
</Button>
</SettingsSection>
)}
@@ -930,6 +930,8 @@ export const McpPage: React.FC = () => {
: t('settings.mcp.page.toast.savedReloadFailed')), {
description: result.warning || t('settings.mcp.page.toast.retryRefreshHint'),
});
} else if (result.restartDeferred) {
toast.success(t('settings.view.pendingRestart.saved'));
} else {
toast.success(result.message || (isNewServer
? t('settings.mcp.page.toast.serverCreatedReloading')
@@ -12,11 +12,12 @@ import {
} from '@/components/sections/shared/SettingsSection';
import { isDesktopShell, requestFileAccess } from '@/lib/desktop';
import { updateDesktopSettings } from '@/lib/persistence';
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import { recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
import { useUIStore } from '@/stores/useUIStore';
import { useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { isWindowsArm64 } from '@/lib/platform';
import { toast } from '@/components/ui';
export const OpenCodeCliSettings: React.FC = () => {
const { t } = useI18n();
@@ -89,11 +90,8 @@ export const OpenCodeCliSettings: React.FC = () => {
? trimmed.slice(1, -1).trim()
: trimmed;
await updateDesktopSettings({ opencodeBinary: unquoted });
await reloadOpenCodeConfiguration({
message: t('settings.openchamber.opencodeCli.actions.restartingOpenCode'),
mode: 'projects',
scopes: ['all'],
});
recordDeferredOpenCodeRestart('cli', { id: 'opencode-binary' });
toast.success(t('settings.view.pendingRestart.saved'));
} finally {
setIsSaving(false);
}
@@ -179,7 +177,7 @@ export const OpenCodeCliSettings: React.FC = () => {
disabled={isLoading || isSaving}
className="shrink-0 !font-normal"
>
{isSaving ? t('settings.common.actions.saving') : t('settings.openchamber.opencodeCli.actions.saveAndReload')}
{isSaving ? t('settings.common.actions.saving') : t('settings.common.actions.saveChanges')}
</Button>
</div>
</SettingsInset>
@@ -109,7 +109,11 @@ export const AddPluginDialog: React.FC<AddPluginDialogProps> = ({
});
}
if (result.ok) {
toast.success(result.message || t('settings.plugins.toast.created'));
if (result.restartDeferred) {
toast.success(t('settings.view.pendingRestart.saved'));
} else {
toast.success(result.message || t('settings.plugins.toast.created'));
}
if (result.reloadFailed) {
toast.warning(t('settings.plugins.toast.reloadFailed'));
}
@@ -183,6 +183,8 @@ export const PluginsPage: React.FC = () => {
result.message || t('settings.plugins.toast.reloadFailed'),
{ description: result.warning },
);
} else if (result.restartDeferred) {
toast.success(t('settings.view.pendingRestart.saved'));
} else {
toast.success(result.message || t('settings.plugins.toast.updated'));
}
@@ -299,6 +301,8 @@ export const PluginsPage: React.FC = () => {
result.message || t('settings.plugins.toast.reloadFailed'),
{ description: result.warning },
);
} else if (result.restartDeferred) {
toast.success(t('settings.view.pendingRestart.saved'));
} else {
toast.success(result.message || t('settings.plugins.toast.updated'));
}
@@ -17,7 +17,7 @@ import {
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import type { IconName } from "@/components/icon/icons";
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import { noteDeferredRestartFromPayload, recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
import { cn } from '@/lib/utils';
import { copyTextToClipboard } from '@/lib/clipboard';
import { openExternalUrl } from '@/lib/url';
@@ -351,7 +351,7 @@ export const ProvidersPage: React.FC = () => {
toast.success(t('settings.providers.page.toast.apiKeySaved'));
setApiKeyInputs((prev) => ({ ...prev, [providerId]: '' }));
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
recordDeferredOpenCodeRestart('providers', { id: providerId });
setSelectedProvider(providerId);
} catch (error) {
console.error('Failed to save API key:', error);
@@ -444,7 +444,7 @@ export const ProvidersPage: React.FC = () => {
toast.success(t('settings.providers.page.toast.oauthCompleted'));
setOauthCodes((prev) => ({ ...prev, [codeKey]: '' }));
setPendingOAuth(null);
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
recordDeferredOpenCodeRestart('providers', { id: providerId });
setSelectedProvider(providerId);
} catch (error) {
console.error('Failed to complete OAuth flow:', error);
@@ -490,7 +490,9 @@ export const ProvidersPage: React.FC = () => {
}
toast.success(t('settings.providers.page.toast.providerDisconnected'));
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
if (!noteDeferredRestartFromPayload(payload, 'providers', { id: providerId })) {
recordDeferredOpenCodeRestart('providers', { id: providerId });
}
} catch (error) {
console.error('Failed to disconnect provider:', error);
toast.error(t('settings.providers.page.toast.providerDisconnectFailed'));