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:
co-authored by
Serhii Dziupin
parent
775da6e9f4
commit
57819dd164
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user