fix: save agent prompt and reload changes reliably

Agent settings now correctly save an emptied system prompt instead of silently keeping the old text.

After saving agent, command, or skill settings, OpenChamber refreshes the updated configuration instead of showing stale values from a short-lived cache.

Reload actions now surface refresh failures consistently while avoiding noisy background errors.
This commit is contained in:
Bohdan Triapitsyn
2026-06-08 13:43:45 +03:00
parent b2b71198ca
commit ef71a997a8
10 changed files with 103 additions and 14 deletions
@@ -578,6 +578,7 @@ export const AgentsPage: React.FC = () => {
try { try {
const trimmedModel = model.trim(); const trimmedModel = model.trim();
const trimmedPrompt = prompt.trim();
const permissionConfig = buildPermissionConfigWithGlobal(globalPermission, permissionRules); const permissionConfig = buildPermissionConfigWithGlobal(globalPermission, permissionRules);
const config: AgentConfig = { const config: AgentConfig = {
name: agentName, name: agentName,
@@ -586,7 +587,7 @@ export const AgentsPage: React.FC = () => {
model: trimmedModel === '' ? null : trimmedModel, model: trimmedModel === '' ? null : trimmedModel,
temperature, temperature,
top_p: topP, top_p: topP,
prompt: prompt.trim() || undefined, prompt: trimmedPrompt || (isNewAgent ? undefined : null),
permission: permissionConfig, permission: permissionConfig,
scope: isNewAgent ? draftScope : undefined, scope: isNewAgent ? draftScope : undefined,
}; };
@@ -38,7 +38,7 @@ export const OpenCodeUpdateToast: React.FC = () => {
message: t('opencodeUpdate.toast.reload.message'), message: t('opencodeUpdate.toast.reload.message'),
mode: 'projects', mode: 'projects',
scopes: ['all'], scopes: ['all'],
}); }).catch(() => undefined);
}, [t]); }, [t]);
const runUpgrade = React.useCallback(async () => { const runUpgrade = React.useCallback(async () => {
@@ -749,7 +749,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
'text-sm font-semibold text-sidebar-foreground/90', 'text-sm font-semibold text-sidebar-foreground/90',
'hover:text-sidebar-foreground hover:bg-interactive-hover', 'hover:text-sidebar-foreground hover:bg-interactive-hover',
)} )}
onClick={() => void reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] })} onClick={() => void reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] }).catch(() => undefined)}
> >
<Icon name="restart" className="h-4 w-4 shrink-0" /> <Icon name="restart" className="h-4 w-4 shrink-0" />
<span>{t('settings.view.actions.reloadOpenCode')}</span> <span>{t('settings.view.actions.reloadOpenCode')}</span>
+11 -8
View File
@@ -11,10 +11,10 @@ import {
} from "@/lib/configUpdate"; } from "@/lib/configUpdate";
import { getSafeStorage } from "./utils/safeStorage"; import { getSafeStorage } from "./utils/safeStorage";
import { useConfigStore } from "@/stores/useConfigStore"; import { useConfigStore } from "@/stores/useConfigStore";
import { useCommandsStore } from "@/stores/useCommandsStore"; import { invalidateCommandsLoadCache, useCommandsStore } from "@/stores/useCommandsStore";
import { useProjectsStore } from "@/stores/useProjectsStore"; import { useProjectsStore } from "@/stores/useProjectsStore";
import { useSkillsCatalogStore } from "@/stores/useSkillsCatalogStore"; import { useSkillsCatalogStore } from "@/stores/useSkillsCatalogStore";
import { useSkillsStore } from "@/stores/useSkillsStore"; import { invalidateSkillsLoadCache, useSkillsStore } from "@/stores/useSkillsStore";
import { runtimeFetch } from "@/lib/runtime-fetch"; import { runtimeFetch } from "@/lib/runtime-fetch";
// Note: useDirectoryStore cannot be imported at top level to avoid circular dependency // Note: useDirectoryStore cannot be imported at top level to avoid circular dependency
@@ -106,7 +106,7 @@ export interface AgentConfig {
model?: string | null; model?: string | null;
temperature?: number; temperature?: number;
top_p?: number; top_p?: number;
prompt?: string; prompt?: string | null;
mode?: "primary" | "subagent" | "all"; mode?: "primary" | "subagent" | "all";
permission?: PermissionConfig | null; permission?: PermissionConfig | null;
@@ -364,7 +364,7 @@ export const useAgentsStore = create<AgentsStore>()(
message: payload?.message, message: payload?.message,
delayMs: payload?.reloadDelayMs, delayMs: payload?.reloadDelayMs,
scopes: ["agents"], scopes: ["agents"],
mode: "active", mode: "projects",
}); });
return true; return true;
} }
@@ -426,7 +426,7 @@ export const useAgentsStore = create<AgentsStore>()(
message: payload?.message, message: payload?.message,
delayMs: payload?.reloadDelayMs, delayMs: payload?.reloadDelayMs,
scopes: ["agents"], scopes: ["agents"],
mode: "active", mode: "projects",
}); });
return true; return true;
} }
@@ -473,7 +473,7 @@ export const useAgentsStore = create<AgentsStore>()(
message: payload?.message, message: payload?.message,
delayMs: payload?.reloadDelayMs, delayMs: payload?.reloadDelayMs,
scopes: ["agents"], scopes: ["agents"],
mode: "active", mode: "projects",
}); });
return true; return true;
} }
@@ -648,18 +648,21 @@ async function performConfigRefresh(options: {
uiRefreshTasks.push(agentConfigStore.loadAgents().then(() => undefined)); uiRefreshTasks.push(agentConfigStore.loadAgents().then(() => undefined));
} }
if (refreshCommands) { if (refreshCommands) {
invalidateCommandsLoadCache(currentDirectory);
uiRefreshTasks.push(commandsStore.loadCommands().then(() => undefined)); uiRefreshTasks.push(commandsStore.loadCommands().then(() => undefined));
} }
if (refreshSkills) { if (refreshSkills) {
invalidateSkillsLoadCache(currentDirectory);
uiRefreshTasks.push(skillsStore.loadSkills().then(() => undefined)); uiRefreshTasks.push(skillsStore.loadSkills().then(() => undefined));
uiRefreshTasks.push(skillsCatalogStore.loadCatalog().then(() => undefined)); uiRefreshTasks.push(skillsCatalogStore.loadCatalog({ refresh: true }).then(() => undefined));
} }
updateConfigUpdateMessage("Refreshing configuration…"); updateConfigUpdateMessage("Refreshing configuration…");
await Promise.all([...sdkRefreshTasks, ...uiRefreshTasks]); await Promise.all([...sdkRefreshTasks, ...uiRefreshTasks]);
} catch { } catch (error) {
updateConfigUpdateMessage("OpenCode refresh failed. Please retry."); updateConfigUpdateMessage("OpenCode refresh failed. Please retry.");
await sleep(1500); await sleep(1500);
throw error;
} finally { } finally {
finishConfigUpdate(); finishConfigUpdate();
} }
@@ -47,6 +47,10 @@ const getCommandsCacheKey = (directory: string | null): string => {
return directory?.trim() || DEFAULT_COMMANDS_CACHE_KEY; return directory?.trim() || DEFAULT_COMMANDS_CACHE_KEY;
}; };
export const invalidateCommandsLoadCache = (directory: string | null = getRequestDirectory()) => {
commandsLastLoadedAt.delete(getCommandsCacheKey(directory));
};
const buildCommandsSignature = (commands: Command[]): string => { const buildCommandsSignature = (commands: Command[]): string => {
return commands return commands
.map((command) => [ .map((command) => [
@@ -277,6 +281,7 @@ export const useCommandsStore = create<CommandsStore>()(
console.log('[CommandsStore] Command created successfully'); console.log('[CommandsStore] Command created successfully');
const needsReload = payload?.requiresReload ?? true; const needsReload = payload?.requiresReload ?? true;
invalidateCommandsLoadCache(directory);
if (needsReload) { if (needsReload) {
requiresReload = true; requiresReload = true;
await performFullConfigRefresh({ await performFullConfigRefresh({
@@ -338,6 +343,7 @@ export const useCommandsStore = create<CommandsStore>()(
console.log('[CommandsStore] Command updated successfully'); console.log('[CommandsStore] Command updated successfully');
const needsReload = payload?.requiresReload ?? true; const needsReload = payload?.requiresReload ?? true;
invalidateCommandsLoadCache(directory);
if (needsReload) { if (needsReload) {
requiresReload = true; requiresReload = true;
await performFullConfigRefresh({ await performFullConfigRefresh({
@@ -384,6 +390,7 @@ export const useCommandsStore = create<CommandsStore>()(
console.log('[CommandsStore] Command deleted successfully'); console.log('[CommandsStore] Command deleted successfully');
const needsReload = payload?.requiresReload ?? true; const needsReload = payload?.requiresReload ?? true;
invalidateCommandsLoadCache(directory);
if (needsReload) { if (needsReload) {
requiresReload = true; requiresReload = true;
await performFullConfigRefresh({ await performFullConfigRefresh({
@@ -495,6 +502,7 @@ async function performFullConfigRefresh(options: { message?: string; delayMs?: n
const commandsStore = useCommandsStore.getState(); const commandsStore = useCommandsStore.getState();
invalidateCommandsLoadCache();
await commandsStore.loadCommands(); await commandsStore.loadCommands();
emitConfigChange("commands", { source: CONFIG_EVENT_SOURCE }); emitConfigChange("commands", { source: CONFIG_EVENT_SOURCE });
@@ -502,6 +510,7 @@ async function performFullConfigRefresh(options: { message?: string; delayMs?: n
console.error("[CommandsStore] Failed to refresh configuration after OpenCode restart:", error); console.error("[CommandsStore] Failed to refresh configuration after OpenCode restart:", error);
updateConfigUpdateMessage("OpenCode refresh failed. Please retry refreshing configuration manually."); updateConfigUpdateMessage("OpenCode refresh failed. Please retry refreshing configuration manually.");
await sleep(1500); await sleep(1500);
throw error;
} finally { } finally {
finishConfigUpdate(); finishConfigUpdate();
} }
@@ -13,7 +13,7 @@ import type {
SkillsCatalogSourceResponse, SkillsCatalogSourceResponse,
} from '@/lib/api/types'; } from '@/lib/api/types';
import { refreshSkillsAfterOpenCodeRestart, useSkillsStore } from '@/stores/useSkillsStore'; import { invalidateSkillsLoadCache, refreshSkillsAfterOpenCodeRestart, useSkillsStore } from '@/stores/useSkillsStore';
import { opencodeClient } from '@/lib/opencode/client'; import { opencodeClient } from '@/lib/opencode/client';
import { startConfigUpdate, finishConfigUpdate, updateConfigUpdateMessage } from '@/lib/configUpdate'; import { startConfigUpdate, finishConfigUpdate, updateConfigUpdateMessage } from '@/lib/configUpdate';
import { runtimeFetch } from '@/lib/runtime-fetch'; import { runtimeFetch } from '@/lib/runtime-fetch';
@@ -423,6 +423,7 @@ export const useSkillsCatalogStore = create<SkillsCatalogState>()(
}); });
} else { } else {
updateConfigUpdateMessage(payload.message || 'Refreshing skills…'); updateConfigUpdateMessage(payload.message || 'Refreshing skills…');
invalidateSkillsLoadCache(currentDirectory);
void useSkillsStore.getState().loadSkills(); void useSkillsStore.getState().loadSkills();
} }
+10 -1
View File
@@ -168,6 +168,10 @@ const getSkillsCacheKey = (directory: string | null): string => {
return directory?.trim() || DEFAULT_SKILLS_CACHE_KEY; return directory?.trim() || DEFAULT_SKILLS_CACHE_KEY;
}; };
export const invalidateSkillsLoadCache = (directory: string | null = getCurrentDirectory()) => {
skillsLastLoadedAt.delete(getSkillsCacheKey(directory));
};
const MAX_HEALTH_WAIT_MS = 20000; const MAX_HEALTH_WAIT_MS = 20000;
const FAST_HEALTH_POLL_INTERVAL_MS = 300; const FAST_HEALTH_POLL_INTERVAL_MS = 300;
const FAST_HEALTH_POLL_ATTEMPTS = 4; const FAST_HEALTH_POLL_ATTEMPTS = 4;
@@ -302,6 +306,7 @@ export const useSkillsStore = create<SkillsStore>()(
} }
const needsReload = payload?.requiresReload ?? false; const needsReload = payload?.requiresReload ?? false;
invalidateSkillsLoadCache(currentDirectory);
if (needsReload) { if (needsReload) {
requiresReload = true; requiresReload = true;
await refreshSkillsAfterOpenCodeRestart({ await refreshSkillsAfterOpenCodeRestart({
@@ -352,6 +357,7 @@ export const useSkillsStore = create<SkillsStore>()(
} }
const needsReload = payload?.requiresReload ?? false; const needsReload = payload?.requiresReload ?? false;
invalidateSkillsLoadCache(currentDirectory);
if (needsReload) { if (needsReload) {
requiresReload = true; requiresReload = true;
await refreshSkillsAfterOpenCodeRestart({ await refreshSkillsAfterOpenCodeRestart({
@@ -393,6 +399,7 @@ export const useSkillsStore = create<SkillsStore>()(
} }
const needsReload = payload?.requiresReload ?? false; const needsReload = payload?.requiresReload ?? false;
invalidateSkillsLoadCache(currentDirectory);
if (needsReload) { if (needsReload) {
requiresReload = true; requiresReload = true;
await refreshSkillsAfterOpenCodeRestart({ await refreshSkillsAfterOpenCodeRestart({
@@ -554,13 +561,15 @@ export async function refreshSkillsAfterOpenCodeRestart(options?: { message?: st
await waitForOpenCodeConnection(options?.delayMs); await waitForOpenCodeConnection(options?.delayMs);
updateConfigUpdateMessage("Refreshing skills…"); updateConfigUpdateMessage("Refreshing skills…");
const skillsStore = useSkillsStore.getState(); const skillsStore = useSkillsStore.getState();
invalidateSkillsLoadCache();
const loaded = await skillsStore.loadSkills(); const loaded = await skillsStore.loadSkills();
if (loaded) { if (loaded) {
emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE }); emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE });
} }
} catch { } catch (error) {
updateConfigUpdateMessage("OpenCode refresh failed. Please retry."); updateConfigUpdateMessage("OpenCode refresh failed. Please retry.");
await sleep(1500); await sleep(1500);
throw error;
} finally { } finally {
finishConfigUpdate(); finishConfigUpdate();
} }
+33
View File
@@ -1666,6 +1666,39 @@ export const updateAgent = (agentName: string, updates: Record<string, unknown>,
for (const [field, value] of Object.entries(updates || {})) { for (const [field, value] of Object.entries(updates || {})) {
if (field === 'prompt') { if (field === 'prompt') {
if (value === null) {
if (mdExists || creatingNewMd) {
if (mdData) {
mdData.body = '';
mdModified = true;
}
continue;
}
if (isPromptFileReference(jsonSection?.prompt)) {
const promptFilePath = resolvePromptFilePath(jsonSection.prompt);
if (!promptFilePath) throw new Error(`Invalid prompt file reference for agent ${agentName}`);
writePromptFile(promptFilePath, '');
continue;
}
if (config.agent) {
const agentMap = config.agent as Record<string, unknown>;
const current = agentMap[agentName] as Record<string, unknown> | undefined;
if (current) {
delete current.prompt;
if (Object.keys(current).length === 0) {
delete agentMap[agentName];
}
if (Object.keys(agentMap).length === 0) {
delete config.agent;
}
jsonModified = true;
}
}
continue;
}
const normalizedValue = typeof value === 'string' ? value : value == null ? '' : String(value); const normalizedValue = typeof value === 'string' ? value : value == null ? '' : String(value);
if (mdExists || creatingNewMd) { if (mdExists || creatingNewMd) {
+1 -1
View File
@@ -1354,7 +1354,7 @@ onCommand('showSettings', () => {
// and refreshes config/data. Triggered by the "Restart API Connection" command. // and refreshes config/data. Triggered by the "Restart API Connection" command.
onCommand('reloadOpenCode', () => { onCommand('reloadOpenCode', () => {
void import('@openchamber/ui/stores/useAgentsStore').then(({ reloadOpenCodeConfiguration }) => { void import('@openchamber/ui/stores/useAgentsStore').then(({ reloadOpenCodeConfiguration }) => {
void reloadOpenCodeConfiguration(); void reloadOpenCodeConfiguration().catch(() => undefined);
}); });
}); });
@@ -449,6 +449,39 @@ function updateAgent(agentName, updates, workingDirectory) {
for (const [field, value] of Object.entries(updates)) { for (const [field, value] of Object.entries(updates)) {
if (field === 'prompt') { if (field === 'prompt') {
if (value === null) {
if (mdExists || creatingNewMd) {
if (mdData) {
mdData.body = '';
mdModified = true;
}
continue;
}
if (isPromptFileReference(jsonSection?.prompt)) {
const promptFilePath = resolvePromptFilePath(jsonSection.prompt);
if (!promptFilePath) {
throw new Error(`Invalid prompt file reference for agent ${agentName}`);
}
writePromptFile(promptFilePath, '');
continue;
}
if (config.agent?.[agentName]) {
delete config.agent[agentName].prompt;
if (Object.keys(config.agent[agentName]).length === 0) {
delete config.agent[agentName];
}
if (Object.keys(config.agent).length === 0) {
delete config.agent;
}
jsonModified = true;
}
continue;
}
const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value)); const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
if (mdExists || creatingNewMd) { if (mdExists || creatingNewMd) {