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
+5
View File
@@ -1356,6 +1356,11 @@ export interface SkillsInstallResponse {
skipped?: Array<{ skillName: string; reason: string }>;
error?: SkillsInstallError;
requiresReload?: boolean;
requiresRestart?: boolean;
restartDeferred?: boolean;
requiresManualRestart?: boolean;
reloadFailed?: boolean;
warning?: string;
message?: string;
reloadDelayMs?: number;
}
@@ -40,6 +40,14 @@ export const settingsDict = {
'settings.view.badge.beta': 'beta',
'settings.view.actions.reloadOpenCode': 'Reload OpenCode',
'settings.view.actions.reloadOpenCodeTooltip': 'Restart OpenCode and reload its configuration.',
'settings.view.actions.applyAndRestartOpenCode': 'Apply & Restart OpenCode',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': 'Apply 1 pending configuration change and restart OpenCode.',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': 'Apply {count} pending configuration changes and restart OpenCode.',
'settings.view.pendingRestart.applying': 'Applying changes and restarting OpenCode…',
'settings.view.pendingRestart.applied': 'OpenCode restarted with pending configuration changes.',
'settings.view.pendingRestart.applyFailed': 'Failed to apply configuration changes.',
'settings.view.pendingRestart.manualRestartRequired': 'Saved on disk. Restart your connected OpenCode server to apply the changes.',
'settings.view.pendingRestart.saved': 'Saved. Restart OpenCode to apply.',
'settings.view.actions.backToSettings': 'Back to Settings',
'settings.view.actions.closeSettings': 'Close settings',
'settings.view.actions.openSectionList': 'Open section list',
@@ -40,6 +40,15 @@ export const settingsDict = {
"settings.view.badge.beta": "beta",
"settings.view.actions.reloadOpenCode": "Recargar OpenCode",
"settings.view.actions.reloadOpenCodeTooltip": "Restablecer OpenCode y recargar su configuración.",
"settings.view.actions.applyAndRestartOpenCode": "Aplicar y reiniciar OpenCode",
"settings.view.actions.applyAndRestartOpenCodeTooltipSingle": "Aplicar 1 cambio de configuración pendiente y reiniciar OpenCode.",
"settings.view.actions.applyAndRestartOpenCodeTooltipPlural": "Aplicar {count} cambios de configuración pendientes y reiniciar OpenCode.",
"settings.view.pendingRestart.applying": "Aplicando cambios y reiniciando OpenCode…",
"settings.view.pendingRestart.applied": "OpenCode se reinició con los cambios de configuración pendientes.",
"settings.view.pendingRestart.applyFailed": "No se pudieron aplicar los cambios de configuración.",
"settings.view.pendingRestart.manualRestartRequired": "Guardado en disco. Reinicia el servidor OpenCode conectado para aplicar los cambios.",
"settings.view.pendingRestart.saved": "Guardado. Reinicia OpenCode para aplicar.",
"settings.view.actions.backToSettings": "Volver a Configuración",
"settings.view.actions.closeSettings": "Cerrar configuración",
"settings.view.actions.openSectionList": "Abrir lista de secciones",
@@ -40,6 +40,15 @@ export const settingsDict = {
'settings.view.badge.beta': 'bêta',
'settings.view.actions.reloadOpenCode': 'Recharger OpenCode',
'settings.view.actions.reloadOpenCodeTooltip': 'Redémarrez OpenCode et rechargez sa configuration.',
'settings.view.actions.applyAndRestartOpenCode': 'Appliquer et redémarrer OpenCode',
'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.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.actions.backToSettings': 'Retour aux paramètres',
'settings.view.actions.closeSettings': 'Fermer les paramètres',
'settings.view.actions.openSectionList': 'Ouvrir la liste des sections',
@@ -40,6 +40,15 @@ export const settingsDict = {
'settings.view.badge.beta': 'ベータ',
'settings.view.actions.reloadOpenCode': 'OpenCode を再読み込み',
'settings.view.actions.reloadOpenCodeTooltip': 'OpenCode を再起動して設定を再読み込みします。',
'settings.view.actions.applyAndRestartOpenCode': '適用して OpenCode を再起動',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': '保留中の設定変更 1 件を適用して OpenCode を再起動します。',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': '保留中の設定変更 {count} 件を適用して OpenCode を再起動します。',
'settings.view.pendingRestart.applying': '変更を適用して OpenCode を再起動しています…',
'settings.view.pendingRestart.applied': '保留中の設定変更を適用して OpenCode を再起動しました。',
'settings.view.pendingRestart.applyFailed': '設定変更の適用に失敗しました。',
'settings.view.pendingRestart.manualRestartRequired': 'ディスクに保存しました。接続中の OpenCode サーバーを再起動して変更を適用してください。',
'settings.view.pendingRestart.saved': '保存しました。適用するには OpenCode を再起動してください。',
'settings.view.actions.backToSettings': '設定に戻る',
'settings.view.actions.closeSettings': '設定を閉じる',
'settings.view.actions.openSectionList': 'セクション一覧を開く',
@@ -40,6 +40,15 @@ export const settingsDict = {
'settings.view.badge.beta': 'beta',
'settings.view.actions.reloadOpenCode': 'OpenCode 다시 로드',
'settings.view.actions.reloadOpenCodeTooltip': 'OpenCode를 재시작하고 설정을 다시 불러옵니다.',
'settings.view.actions.applyAndRestartOpenCode': '적용하고 OpenCode 다시 시작',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': '보류 중인 구성 변경 1개를 적용하고 OpenCode를 다시 시작합니다.',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': '보류 중인 구성 변경 {count}개를 적용하고 OpenCode를 다시 시작합니다.',
'settings.view.pendingRestart.applying': '변경 사항을 적용하고 OpenCode를 다시 시작하는 중…',
'settings.view.pendingRestart.applied': '보류 중인 구성 변경으로 OpenCode를 다시 시작했습니다.',
'settings.view.pendingRestart.applyFailed': '구성 변경을 적용하지 못했습니다.',
'settings.view.pendingRestart.manualRestartRequired': '디스크에 저장되었습니다. 연결된 OpenCode 서버를 다시 시작하여 변경 사항을 적용하세요.',
'settings.view.pendingRestart.saved': '저장되었습니다. 적용하려면 OpenCode를 다시 시작하세요.',
'settings.view.actions.backToSettings': '설정으로 돌아가기',
'settings.view.actions.closeSettings': '설정 닫기',
'settings.view.actions.openSectionList': '섹션 목록 열기',
@@ -1913,6 +1913,15 @@ export const settingsDict = {
'settings.view.actions.openSectionList': 'Otwórz listę sekcji',
'settings.view.actions.reloadOpenCode': 'Przeładuj OpenCode',
'settings.view.actions.reloadOpenCodeTooltip': 'Uruchom ponownie OpenCode i przeładuj jego konfigurację.',
'settings.view.actions.applyAndRestartOpenCode': 'Zastosuj i uruchom ponownie OpenCode',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': 'Zastosuj 1 oczekującą zmianę konfiguracji i uruchom ponownie OpenCode.',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': 'Zastosuj {count} oczekujących zmian konfiguracji i uruchom ponownie OpenCode.',
'settings.view.pendingRestart.applying': 'Stosowanie zmian i ponowne uruchamianie OpenCode…',
'settings.view.pendingRestart.applied': 'OpenCode został uruchomiony ponownie z oczekującymi zmianami konfiguracji.',
'settings.view.pendingRestart.applyFailed': 'Nie udało się zastosować zmian konfiguracji.',
'settings.view.pendingRestart.manualRestartRequired': 'Zapisano na dysku. Uruchom ponownie podłączony serwer OpenCode, aby zastosować zmiany.',
'settings.view.pendingRestart.saved': 'Zapisano. Uruchom ponownie OpenCode, aby zastosować.',
'settings.view.actions.resizeNavigation': 'Zmień rozmiar nawigacji',
'settings.view.search.placeholder': 'Szukaj ustawień',
'settings.view.search.aria': 'Szukaj ustawień',
@@ -40,6 +40,15 @@ export const settingsDict = {
"settings.view.badge.beta": "beta",
"settings.view.actions.reloadOpenCode": "Recarregar OpenCode",
"settings.view.actions.reloadOpenCodeTooltip": "Reiniciar OpenCode e recarregar suas configurações.",
"settings.view.actions.applyAndRestartOpenCode": "Aplicar e reiniciar o OpenCode",
"settings.view.actions.applyAndRestartOpenCodeTooltipSingle": "Aplicar 1 alteração de configuração pendente e reiniciar o OpenCode.",
"settings.view.actions.applyAndRestartOpenCodeTooltipPlural": "Aplicar {count} alterações de configuração pendentes e reiniciar o OpenCode.",
"settings.view.pendingRestart.applying": "Aplicando alterações e reiniciando o OpenCode…",
"settings.view.pendingRestart.applied": "OpenCode reiniciado com as alterações de configuração pendentes.",
"settings.view.pendingRestart.applyFailed": "Falha ao aplicar as alterações de configuração.",
"settings.view.pendingRestart.manualRestartRequired": "Salvo no disco. Reinicie o servidor OpenCode conectado para aplicar as alterações.",
"settings.view.pendingRestart.saved": "Salvo. Reinicie o OpenCode para aplicar.",
"settings.view.actions.backToSettings": "Voltar às configurações",
"settings.view.actions.closeSettings": "Fechar configurações",
"settings.view.actions.openSectionList": "Abrir lista de seções",
@@ -40,6 +40,15 @@ export const settingsDict = {
"settings.view.badge.beta": "бета-версія",
"settings.view.actions.reloadOpenCode": "Перезавантажити OpenCode",
"settings.view.actions.reloadOpenCodeTooltip": "Перезапустити OpenCode і перезавантажити його конфігурацію.",
"settings.view.actions.applyAndRestartOpenCode": "Застосувати та перезапустити OpenCode",
"settings.view.actions.applyAndRestartOpenCodeTooltipSingle": "Застосувати 1 очікувану зміну конфігурації та перезапустити OpenCode.",
"settings.view.actions.applyAndRestartOpenCodeTooltipPlural": "Застосувати {count} очікуваних змін конфігурації та перезапустити OpenCode.",
"settings.view.pendingRestart.applying": "Застосування змін і перезапуск OpenCode…",
"settings.view.pendingRestart.applied": "OpenCode перезапущено з очікуваними змінами конфігурації.",
"settings.view.pendingRestart.applyFailed": "Не вдалося застосувати зміни конфігурації.",
"settings.view.pendingRestart.manualRestartRequired": "Збережено на диску. Перезапустіть підключений сервер OpenCode, щоб застосувати зміни.",
"settings.view.pendingRestart.saved": "Збережено. Перезапустіть OpenCode, щоб застосувати.",
"settings.view.actions.backToSettings": "Назад до налаштувань",
"settings.view.actions.closeSettings": "Закрити налаштування",
"settings.view.actions.openSectionList": "Відкрити список розділів",
@@ -40,6 +40,15 @@ export const settingsDict = {
'settings.view.badge.beta': '测试版',
'settings.view.actions.reloadOpenCode': '重新加载 OpenCode',
'settings.view.actions.reloadOpenCodeTooltip': '重启 OpenCode 并重新加载其配置。',
'settings.view.actions.applyAndRestartOpenCode': '应用并重启 OpenCode',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': '应用 1 项待处理的配置更改并重启 OpenCode。',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': '应用 {count} 项待处理的配置更改并重启 OpenCode。',
'settings.view.pendingRestart.applying': '正在应用更改并重启 OpenCode…',
'settings.view.pendingRestart.applied': '已使用待处理的配置更改重启 OpenCode。',
'settings.view.pendingRestart.applyFailed': '应用配置更改失败。',
'settings.view.pendingRestart.manualRestartRequired': '已保存到磁盘。请重启已连接的 OpenCode 服务器以应用更改。',
'settings.view.pendingRestart.saved': '已保存。请重启 OpenCode 以应用。',
'settings.view.actions.backToSettings': '返回设置',
'settings.view.actions.closeSettings': '关闭设置',
'settings.view.actions.openSectionList': '打开分组列表',
@@ -38,6 +38,15 @@
'settings.view.badge.beta': 'beta',
'settings.view.actions.reloadOpenCode': '重新載入 OpenCode',
'settings.view.actions.reloadOpenCodeTooltip': '重新啟動 OpenCode 並重新載入其設定。',
'settings.view.actions.applyAndRestartOpenCode': '套用並重新啟動 OpenCode',
'settings.view.actions.applyAndRestartOpenCodeTooltipSingle': '套用 1 項待處理的設定變更並重新啟動 OpenCode。',
'settings.view.actions.applyAndRestartOpenCodeTooltipPlural': '套用 {count} 項待處理的設定變更並重新啟動 OpenCode。',
'settings.view.pendingRestart.applying': '正在套用變更並重新啟動 OpenCode…',
'settings.view.pendingRestart.applied': '已使用待處理的設定變更重新啟動 OpenCode。',
'settings.view.pendingRestart.applyFailed': '套用設定變更失敗。',
'settings.view.pendingRestart.manualRestartRequired': '已儲存到磁碟。請重新啟動已連線的 OpenCode 伺服器以套用變更。',
'settings.view.pendingRestart.saved': '已儲存。請重新啟動 OpenCode 以套用。',
'settings.view.actions.backToSettings': '返回設定頁',
'settings.view.actions.closeSettings': '關閉設定',
'settings.view.actions.openSectionList': '開啟群組清單',
@@ -0,0 +1,62 @@
import { describe, expect, mock, test } from 'bun:test';
describe('deferred OpenCode restart helpers', () => {
test('isDeferredRestartPayload detects deferred responses', async () => {
const { isDeferredRestartPayload } = await import('./deferredRestart');
expect(isDeferredRestartPayload({
requiresReload: false,
requiresRestart: true,
restartDeferred: true,
})).toBe(true);
expect(isDeferredRestartPayload({
requiresReload: false,
requiresRestart: true,
})).toBe(true);
expect(isDeferredRestartPayload({
requiresReload: true,
message: 'reloading',
})).toBe(false);
expect(isDeferredRestartPayload({
requiresManualRestart: true,
requiresRestart: true,
restartDeferred: true,
})).toBe(false);
});
test('noteDeferredRestartFromPayload records pending changes', async () => {
const { usePendingOpenCodeRestartStore } = await import('@/stores/usePendingOpenCodeRestartStore');
usePendingOpenCodeRestartStore.getState().clear();
const { noteDeferredRestartFromPayload } = await import('./deferredRestart');
const noted = noteDeferredRestartFromPayload({
requiresReload: false,
requiresRestart: true,
restartDeferred: true,
}, 'mcp', { id: 'filesystem' });
expect(noted).toBe(true);
expect(usePendingOpenCodeRestartStore.getState().changes).toHaveLength(1);
expect(usePendingOpenCodeRestartStore.getState().changes[0]?.scope).toBe('mcp');
});
test('applyPendingOpenCodeRestart clears pending changes after success', async () => {
mock.module('@/stores/useAgentsStore', () => ({
reloadOpenCodeConfiguration: async () => undefined,
}));
const { usePendingOpenCodeRestartStore } = await import('@/stores/usePendingOpenCodeRestartStore');
usePendingOpenCodeRestartStore.getState().clear();
usePendingOpenCodeRestartStore.getState().recordChange({ scope: 'agents', id: 'demo' });
const { applyPendingOpenCodeRestart } = await import('./deferredRestart');
const result = await applyPendingOpenCodeRestart({ message: 'Applying…' });
expect(result).toEqual({ ok: true });
expect(usePendingOpenCodeRestartStore.getState().changes).toHaveLength(0);
expect(usePendingOpenCodeRestartStore.getState().isApplying).toBe(false);
});
});
@@ -0,0 +1,79 @@
import {
usePendingOpenCodeRestartStore,
type PendingOpenCodeRestartScope,
} from '@/stores/usePendingOpenCodeRestartStore';
export type ConfigMutationPayload = {
requiresReload?: boolean;
requiresRestart?: boolean;
restartDeferred?: boolean;
requiresManualRestart?: boolean;
reloadFailed?: boolean;
message?: string;
warning?: string;
reloadDelayMs?: number;
} | null | undefined;
export function isDeferredRestartPayload(payload: ConfigMutationPayload): boolean {
if (!payload || typeof payload !== 'object') {
return false;
}
if (payload.requiresManualRestart === true) {
return false;
}
return payload.restartDeferred === true || (payload.requiresRestart === true && payload.requiresReload !== true);
}
export function recordDeferredOpenCodeRestart(
scope: PendingOpenCodeRestartScope,
options?: { id?: string; label?: string },
): void {
usePendingOpenCodeRestartStore.getState().recordChange({
scope,
id: options?.id,
label: options?.label,
});
}
/**
* If the mutation response deferred the OpenCode restart, record it and return true.
* Callers should skip immediate refresh overlays when this returns true.
*/
export function noteDeferredRestartFromPayload(
payload: ConfigMutationPayload,
scope: PendingOpenCodeRestartScope,
options?: { id?: string; label?: string },
): boolean {
if (!isDeferredRestartPayload(payload)) {
return false;
}
recordDeferredOpenCodeRestart(scope, options);
return true;
}
export async function applyPendingOpenCodeRestart(options?: {
message?: string;
}): Promise<{ ok: boolean; requiresManualRestart?: boolean }> {
const store = usePendingOpenCodeRestartStore.getState();
if (store.isApplying) {
return { ok: false };
}
store.setApplying(true);
try {
const { reloadOpenCodeConfiguration } = await import('@/stores/useAgentsStore');
await reloadOpenCodeConfiguration({
message: options?.message,
mode: 'projects',
scopes: ['all'],
});
usePendingOpenCodeRestartStore.getState().clear();
return { ok: true };
} catch (error) {
usePendingOpenCodeRestartStore.getState().setApplying(false);
if ((error as Error & { requiresManualRestart?: boolean })?.requiresManualRestart) {
return { ok: false, requiresManualRestart: true };
}
throw error;
}
}