Fix deferred restart review findings.

Drop phantom pending records when provider disconnect removes nothing
and when Behavior AGENTS.md payloads are not deferred. Clear the Apply
badge after external OpenCode manual-restart guidance. Align skills and
commands save toasts with deferred restart feedback, and fix French
OpenCode elision.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-03 08:56:05 +00:00
co-authored by Serhii Dziupin
parent 7c27a39cf4
commit 0cde9e9413
7 changed files with 71 additions and 12 deletions
@@ -243,9 +243,7 @@ export const BehaviorPage: React.FC = () => {
}
const payload = await response.json().catch(() => null);
if (!noteDeferredRestartFromPayload(payload, 'behavior', { id: 'agents-md' })) {
recordDeferredOpenCodeRestart('behavior', { id: 'agents-md' });
}
const deferred = noteDeferredRestartFromPayload(payload, 'behavior', { id: 'agents-md' });
await saveBehaviorSetting({
globalBehaviorPrompt: content,
@@ -253,7 +251,11 @@ export const BehaviorPage: React.FC = () => {
setPrompt(content);
setInitialPrompt(content);
toast.success(t('settings.view.pendingRestart.saved'));
toast.success(
deferred
? t('settings.view.pendingRestart.saved')
: t('settings.behavior.page.toast.saved'),
);
} catch (error) {
console.error('Failed to save behavior:', error);
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
@@ -4,6 +4,7 @@ import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/components/ui';
import { useCommandsStore, type CommandConfig, type CommandScope } from '@/stores/useCommandsStore';
import { usePendingOpenCodeRestartStore } from '@/stores/usePendingOpenCodeRestartStore';
import { useShallow } from 'zustand/react/shallow';
import { ModelSelector } from '../agents/ModelSelector';
import { AgentSelector } from './AgentSelector';
@@ -170,7 +171,16 @@ export const CommandsPage: React.FC = () => {
}
if (success) {
toast.success(isNewCommand ? t('settings.commands.page.toast.created') : t('settings.commands.page.toast.updated'));
const deferred = usePendingOpenCodeRestartStore.getState().changes.some(
(change) => change.scope === 'commands' && change.id.startsWith(`commands:${commandName}:`),
);
toast.success(
deferred
? t('settings.view.pendingRestart.saved')
: isNewCommand
? t('settings.commands.page.toast.created')
: t('settings.commands.page.toast.updated'),
);
} else {
toast.error(isNewCommand ? t('settings.commands.page.toast.createFailed') : t('settings.commands.page.toast.updateFailed'));
}
@@ -490,9 +490,9 @@ export const ProvidersPage: React.FC = () => {
}
toast.success(t('settings.providers.page.toast.providerDisconnected'));
if (!noteDeferredRestartFromPayload(payload, 'providers', { id: providerId })) {
recordDeferredOpenCodeRestart('providers', { id: providerId });
}
// Only accumulate when the server actually deferred a restart (e.g. auth removed).
// removed:false payloads must not create a phantom pending Apply & Restart.
noteDeferredRestartFromPayload(payload, 'providers', { id: providerId });
} catch (error) {
console.error('Failed to disconnect provider:', error);
toast.error(t('settings.providers.page.toast.providerDisconnectFailed'));
@@ -6,6 +6,7 @@ import { Textarea } from '@/components/ui/textarea';
import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor';
import { toast } from '@/components/ui';
import { useSkillsStore, type SkillConfig, type SkillScope, type SupportingFile, type PendingFile } from '@/stores/useSkillsStore';
import { usePendingOpenCodeRestartStore } from '@/stores/usePendingOpenCodeRestartStore';
import { useShallow } from 'zustand/react/shallow';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
@@ -352,7 +353,16 @@ const SkillsInstalledPage: React.FC = () => {
}
if (success) {
toast.success(isNewSkill ? t('settings.skills.page.toast.skillCreated') : t('settings.skills.page.toast.skillUpdated'));
const deferred = usePendingOpenCodeRestartStore.getState().changes.some(
(change) => change.scope === 'skills' && change.id.startsWith(`skills:${skillName}:`),
);
toast.success(
deferred
? t('settings.view.pendingRestart.saved')
: isNewSkill
? t('settings.skills.page.toast.skillCreated')
: t('settings.skills.page.toast.skillUpdated'),
);
} else {
toast.error(isNewSkill ? t('settings.skills.page.toast.createSkillFailed') : t('settings.skills.page.toast.updateSkillFailed'));
}
@@ -44,14 +44,14 @@ export const settingsDict = {
'settings.view.actions.applyAndRestartOpenCode': 'Appliquer et redémarrer',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': 'Appliquer 1 modification de configuration en attente et redémarrer OpenCode.',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': 'Appliquer {count} modifications de configuration en attente et redémarrer OpenCode.',
'settings.view.pendingRestart.applying': 'Application des modifications et redémarrage de OpenCode…',
'settings.view.pendingRestart.applying': 'Application des modifications et redémarrage d\'OpenCode…',
'settings.view.pendingRestart.applied': 'OpenCode a redémarré avec les modifications de configuration en attente.',
'settings.view.pendingRestart.applyFailed': 'Échec de l\'application des modifications de configuration.',
'settings.view.pendingRestart.manualRestartRequired': 'Enregistré sur le disque. Redémarrez le serveur OpenCode connecté pour appliquer les modifications.',
'settings.view.pendingRestart.saved': 'Enregistré. Redémarrez OpenCode pour appliquer.',
'settings.view.pendingRestart.confirm.title': 'Appliquer et redémarrer ?',
'settings.view.pendingRestart.confirm.description': 'Le redémarrage de OpenCode arrêtera tous les chats en cours. Vos modifications de configuration enregistrées prendront effet après le redémarrage.',
'settings.view.pendingRestart.confirm.description': 'Le redémarrage d\'OpenCode arrêtera tous les chats en cours. Vos modifications de configuration enregistrées prendront effet après le redémarrage.',
'settings.view.pendingRestart.confirm.dontShowAgain': 'Ne plus afficher',
'settings.view.pendingRestart.confirm.cancel': 'Annuler', 'settings.view.actions.backToSettings': 'Retour aux paramètres',
'settings.view.actions.closeSettings': 'Fermer les paramètres',
@@ -43,6 +43,20 @@ describe('deferred OpenCode restart helpers', () => {
expect(usePendingOpenCodeRestartStore.getState().changes[0]?.scope).toBe('mcp');
});
test('noteDeferredRestartFromPayload ignores non-deferred payloads', async () => {
const { usePendingOpenCodeRestartStore } = await import('@/stores/usePendingOpenCodeRestartStore');
usePendingOpenCodeRestartStore.getState().clear();
const { noteDeferredRestartFromPayload } = await import('./deferredRestart');
const noted = noteDeferredRestartFromPayload({
requiresReload: false,
message: 'Provider was not connected',
}, 'providers', { id: 'openai' });
expect(noted).toBe(false);
expect(usePendingOpenCodeRestartStore.getState().changes).toHaveLength(0);
});
test('applyPendingOpenCodeRestart clears pending changes after success', async () => {
mock.module('@/stores/useAgentsStore', () => ({
reloadOpenCodeConfiguration: async () => undefined,
@@ -59,4 +73,25 @@ describe('deferred OpenCode restart helpers', () => {
expect(usePendingOpenCodeRestartStore.getState().changes).toHaveLength(0);
expect(usePendingOpenCodeRestartStore.getState().isApplying).toBe(false);
});
test('applyPendingOpenCodeRestart clears pending changes on manual restart', async () => {
mock.module('@/stores/useAgentsStore', () => ({
reloadOpenCodeConfiguration: async () => {
const error = new Error('Restart your connected OpenCode server');
(error as Error & { requiresManualRestart?: boolean }).requiresManualRestart = true;
throw error;
},
}));
const { usePendingOpenCodeRestartStore } = await import('@/stores/usePendingOpenCodeRestartStore');
usePendingOpenCodeRestartStore.getState().clear();
usePendingOpenCodeRestartStore.getState().recordChange({ scope: 'mcp', id: 'filesystem' });
const { applyPendingOpenCodeRestart } = await import('./deferredRestart');
const result = await applyPendingOpenCodeRestart({ message: 'Applying…' });
expect(result).toEqual({ ok: false, requiresManualRestart: true });
expect(usePendingOpenCodeRestartStore.getState().changes).toHaveLength(0);
expect(usePendingOpenCodeRestartStore.getState().isApplying).toBe(false);
});
});
@@ -70,10 +70,12 @@ export async function applyPendingOpenCodeRestart(options?: {
usePendingOpenCodeRestartStore.getState().clear();
return { ok: true };
} catch (error) {
usePendingOpenCodeRestartStore.getState().setApplying(false);
if ((error as Error & { requiresManualRestart?: boolean })?.requiresManualRestart) {
// Changes are already on disk; clear the badge after delivering manual-restart guidance.
usePendingOpenCodeRestartStore.getState().clear();
return { ok: false, requiresManualRestart: true };
}
usePendingOpenCodeRestartStore.getState().setApplying(false);
throw error;
}
}