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
+81 -27
View File
@@ -10,6 +10,7 @@ import {
import { createDeferredSafeJSONStorage } from "./utils/safeStorage";
import { runtimeFetch } from "@/lib/runtime-fetch";
import { runBackgroundNetworkTask } from "@/lib/background-network";
import { noteDeferredRestartFromPayload } from "@/lib/opencode/deferredRestart";
import { opencodeClient } from '@/lib/opencode/client';
@@ -173,6 +174,44 @@ export const invalidateSkillsLoadCache = (directory: string | null = getCurrentD
skillsLastLoadedAt.delete(getSkillsCacheKey(directory));
};
const upsertSkillLocal = (
set: (state: Partial<SkillsStore>) => void,
get: () => SkillsStore,
name: string,
config: Partial<SkillConfig>,
) => {
const existing = get().skills.find((skill) => skill.name === name);
const path = config.targetPath ?? existing?.path ?? '';
const nextSkill: DiscoveredSkill = {
...existing,
name,
path,
scope: config.scope ?? existing?.scope ?? 'user',
source: config.source ?? existing?.source ?? 'opencode',
description: config.description ?? existing?.description ?? '',
group: parseSkillGroup(path),
};
const skills = get().skills;
const nextSkills = skills.some((skill) => skill.name === name)
? skills.map((skill) => (skill.name === name ? nextSkill : skill))
: [...skills, nextSkill];
set({ skills: nextSkills });
};
const removeSkillLocal = (
set: (state: Partial<SkillsStore>) => void,
get: () => SkillsStore,
name: string,
) => {
const nextState: Partial<SkillsStore> = {
skills: get().skills.filter((skill) => skill.name !== name),
};
if (get().selectedSkillName === name) {
nextState.selectedSkillName = null;
}
set(nextState);
};
const MAX_HEALTH_WAIT_MS = 20000;
const FAST_HEALTH_POLL_INTERVAL_MS = 300;
const FAST_HEALTH_POLL_ATTEMPTS = 4;
@@ -278,8 +317,6 @@ export const useSkillsStore = create<SkillsStore>()(
},
createSkill: async (config: SkillConfig) => {
startConfigUpdate("Creating skill...");
let requiresReload = false;
try {
const skillConfig: Record<string, unknown> = {
name: config.name,
@@ -306,10 +343,21 @@ export const useSkillsStore = create<SkillsStore>()(
throw new Error(message);
}
const needsReload = payload?.requiresReload ?? false;
invalidateSkillsLoadCache(currentDirectory);
if (needsReload) {
requiresReload = true;
if (payload?.requiresManualRestart) {
upsertSkillLocal(set, get, config.name, config);
return true;
}
if (noteDeferredRestartFromPayload(payload, 'skills', { id: config.name })) {
upsertSkillLocal(set, get, config.name, config);
emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE });
return true;
}
if (payload?.requiresReload) {
startConfigUpdate("Creating skill...");
await refreshSkillsAfterOpenCodeRestart({
message: payload?.message,
delayMs: payload?.reloadDelayMs,
@@ -324,16 +372,10 @@ export const useSkillsStore = create<SkillsStore>()(
return loaded;
} catch {
return false;
} finally {
if (!requiresReload) {
finishConfigUpdate();
}
}
},
updateSkill: async (name: string, config: Partial<SkillConfig>) => {
startConfigUpdate("Updating skill...");
let requiresReload = false;
try {
const skillConfig: Record<string, unknown> = {};
@@ -357,10 +399,21 @@ export const useSkillsStore = create<SkillsStore>()(
throw new Error(message);
}
const needsReload = payload?.requiresReload ?? false;
invalidateSkillsLoadCache(currentDirectory);
if (needsReload) {
requiresReload = true;
if (payload?.requiresManualRestart) {
upsertSkillLocal(set, get, name, config);
return true;
}
if (noteDeferredRestartFromPayload(payload, 'skills', { id: name })) {
upsertSkillLocal(set, get, name, config);
emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE });
return true;
}
if (payload?.requiresReload) {
startConfigUpdate("Updating skill...");
await refreshSkillsAfterOpenCodeRestart({
message: payload?.message,
delayMs: payload?.reloadDelayMs,
@@ -375,16 +428,10 @@ export const useSkillsStore = create<SkillsStore>()(
return loaded;
} catch {
return false;
} finally {
if (!requiresReload) {
finishConfigUpdate();
}
}
},
deleteSkill: async (name: string) => {
startConfigUpdate("Deleting skill...");
let requiresReload = false;
try {
const currentDirectory = getCurrentDirectory();
const queryParams = currentDirectory ? `?directory=${encodeURIComponent(currentDirectory)}` : '';
@@ -399,10 +446,21 @@ export const useSkillsStore = create<SkillsStore>()(
throw new Error(message);
}
const needsReload = payload?.requiresReload ?? false;
invalidateSkillsLoadCache(currentDirectory);
if (needsReload) {
requiresReload = true;
if (payload?.requiresManualRestart) {
removeSkillLocal(set, get, name);
return true;
}
if (noteDeferredRestartFromPayload(payload, 'skills', { id: name })) {
removeSkillLocal(set, get, name);
emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE });
return true;
}
if (payload?.requiresReload) {
startConfigUpdate("Deleting skill...");
await refreshSkillsAfterOpenCodeRestart({
message: payload?.message,
delayMs: payload?.reloadDelayMs,
@@ -422,10 +480,6 @@ export const useSkillsStore = create<SkillsStore>()(
return loaded;
} catch {
return false;
} finally {
if (!requiresReload) {
finishConfigUpdate();
}
}
},