diff --git a/packages/ui/src/components/sections/agents/AgentsPage.tsx b/packages/ui/src/components/sections/agents/AgentsPage.tsx index 5d7b182a..277fe1e6 100644 --- a/packages/ui/src/components/sections/agents/AgentsPage.tsx +++ b/packages/ui/src/components/sections/agents/AgentsPage.tsx @@ -578,6 +578,7 @@ export const AgentsPage: React.FC = () => { try { const trimmedModel = model.trim(); + const trimmedPrompt = prompt.trim(); const permissionConfig = buildPermissionConfigWithGlobal(globalPermission, permissionRules); const config: AgentConfig = { name: agentName, @@ -586,7 +587,7 @@ export const AgentsPage: React.FC = () => { model: trimmedModel === '' ? null : trimmedModel, temperature, top_p: topP, - prompt: prompt.trim() || undefined, + prompt: trimmedPrompt || (isNewAgent ? undefined : null), permission: permissionConfig, scope: isNewAgent ? draftScope : undefined, }; diff --git a/packages/ui/src/components/update/OpenCodeUpdateToast.tsx b/packages/ui/src/components/update/OpenCodeUpdateToast.tsx index f6deda11..7a4a321c 100644 --- a/packages/ui/src/components/update/OpenCodeUpdateToast.tsx +++ b/packages/ui/src/components/update/OpenCodeUpdateToast.tsx @@ -38,7 +38,7 @@ export const OpenCodeUpdateToast: React.FC = () => { message: t('opencodeUpdate.toast.reload.message'), mode: 'projects', scopes: ['all'], - }); + }).catch(() => undefined); }, [t]); const runUpgrade = React.useCallback(async () => { diff --git a/packages/ui/src/components/views/SettingsView.tsx b/packages/ui/src/components/views/SettingsView.tsx index b11bafa7..a07d43ff 100644 --- a/packages/ui/src/components/views/SettingsView.tsx +++ b/packages/ui/src/components/views/SettingsView.tsx @@ -749,7 +749,7 @@ export const SettingsView: React.FC = ({ onClose, forceMobile 'text-sm font-semibold text-sidebar-foreground/90', '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)} > {t('settings.view.actions.reloadOpenCode')} diff --git a/packages/ui/src/stores/useAgentsStore.ts b/packages/ui/src/stores/useAgentsStore.ts index 0f4fe455..aa083528 100644 --- a/packages/ui/src/stores/useAgentsStore.ts +++ b/packages/ui/src/stores/useAgentsStore.ts @@ -11,10 +11,10 @@ import { } from "@/lib/configUpdate"; import { getSafeStorage } from "./utils/safeStorage"; import { useConfigStore } from "@/stores/useConfigStore"; -import { useCommandsStore } from "@/stores/useCommandsStore"; +import { invalidateCommandsLoadCache, useCommandsStore } from "@/stores/useCommandsStore"; import { useProjectsStore } from "@/stores/useProjectsStore"; import { useSkillsCatalogStore } from "@/stores/useSkillsCatalogStore"; -import { useSkillsStore } from "@/stores/useSkillsStore"; +import { invalidateSkillsLoadCache, useSkillsStore } from "@/stores/useSkillsStore"; import { runtimeFetch } from "@/lib/runtime-fetch"; // Note: useDirectoryStore cannot be imported at top level to avoid circular dependency @@ -106,7 +106,7 @@ export interface AgentConfig { model?: string | null; temperature?: number; top_p?: number; - prompt?: string; + prompt?: string | null; mode?: "primary" | "subagent" | "all"; permission?: PermissionConfig | null; @@ -364,7 +364,7 @@ export const useAgentsStore = create()( message: payload?.message, delayMs: payload?.reloadDelayMs, scopes: ["agents"], - mode: "active", + mode: "projects", }); return true; } @@ -426,7 +426,7 @@ export const useAgentsStore = create()( message: payload?.message, delayMs: payload?.reloadDelayMs, scopes: ["agents"], - mode: "active", + mode: "projects", }); return true; } @@ -473,7 +473,7 @@ export const useAgentsStore = create()( message: payload?.message, delayMs: payload?.reloadDelayMs, scopes: ["agents"], - mode: "active", + mode: "projects", }); return true; } @@ -648,18 +648,21 @@ async function performConfigRefresh(options: { uiRefreshTasks.push(agentConfigStore.loadAgents().then(() => undefined)); } if (refreshCommands) { + invalidateCommandsLoadCache(currentDirectory); uiRefreshTasks.push(commandsStore.loadCommands().then(() => undefined)); } if (refreshSkills) { + invalidateSkillsLoadCache(currentDirectory); uiRefreshTasks.push(skillsStore.loadSkills().then(() => undefined)); - uiRefreshTasks.push(skillsCatalogStore.loadCatalog().then(() => undefined)); + uiRefreshTasks.push(skillsCatalogStore.loadCatalog({ refresh: true }).then(() => undefined)); } updateConfigUpdateMessage("Refreshing configuration…"); await Promise.all([...sdkRefreshTasks, ...uiRefreshTasks]); - } catch { + } catch (error) { updateConfigUpdateMessage("OpenCode refresh failed. Please retry."); await sleep(1500); + throw error; } finally { finishConfigUpdate(); } diff --git a/packages/ui/src/stores/useCommandsStore.ts b/packages/ui/src/stores/useCommandsStore.ts index 92306b55..a3accc94 100644 --- a/packages/ui/src/stores/useCommandsStore.ts +++ b/packages/ui/src/stores/useCommandsStore.ts @@ -47,6 +47,10 @@ const getCommandsCacheKey = (directory: string | null): string => { return directory?.trim() || DEFAULT_COMMANDS_CACHE_KEY; }; +export const invalidateCommandsLoadCache = (directory: string | null = getRequestDirectory()) => { + commandsLastLoadedAt.delete(getCommandsCacheKey(directory)); +}; + const buildCommandsSignature = (commands: Command[]): string => { return commands .map((command) => [ @@ -277,6 +281,7 @@ export const useCommandsStore = create()( console.log('[CommandsStore] Command created successfully'); const needsReload = payload?.requiresReload ?? true; + invalidateCommandsLoadCache(directory); if (needsReload) { requiresReload = true; await performFullConfigRefresh({ @@ -338,6 +343,7 @@ export const useCommandsStore = create()( console.log('[CommandsStore] Command updated successfully'); const needsReload = payload?.requiresReload ?? true; + invalidateCommandsLoadCache(directory); if (needsReload) { requiresReload = true; await performFullConfigRefresh({ @@ -384,6 +390,7 @@ export const useCommandsStore = create()( console.log('[CommandsStore] Command deleted successfully'); const needsReload = payload?.requiresReload ?? true; + invalidateCommandsLoadCache(directory); if (needsReload) { requiresReload = true; await performFullConfigRefresh({ @@ -495,6 +502,7 @@ async function performFullConfigRefresh(options: { message?: string; delayMs?: n const commandsStore = useCommandsStore.getState(); + invalidateCommandsLoadCache(); await commandsStore.loadCommands(); 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); updateConfigUpdateMessage("OpenCode refresh failed. Please retry refreshing configuration manually."); await sleep(1500); + throw error; } finally { finishConfigUpdate(); } diff --git a/packages/ui/src/stores/useSkillsCatalogStore.ts b/packages/ui/src/stores/useSkillsCatalogStore.ts index 9097f905..139348fb 100644 --- a/packages/ui/src/stores/useSkillsCatalogStore.ts +++ b/packages/ui/src/stores/useSkillsCatalogStore.ts @@ -13,7 +13,7 @@ import type { SkillsCatalogSourceResponse, } from '@/lib/api/types'; -import { refreshSkillsAfterOpenCodeRestart, useSkillsStore } from '@/stores/useSkillsStore'; +import { invalidateSkillsLoadCache, refreshSkillsAfterOpenCodeRestart, useSkillsStore } from '@/stores/useSkillsStore'; import { opencodeClient } from '@/lib/opencode/client'; import { startConfigUpdate, finishConfigUpdate, updateConfigUpdateMessage } from '@/lib/configUpdate'; import { runtimeFetch } from '@/lib/runtime-fetch'; @@ -423,6 +423,7 @@ export const useSkillsCatalogStore = create()( }); } else { updateConfigUpdateMessage(payload.message || 'Refreshing skills…'); + invalidateSkillsLoadCache(currentDirectory); void useSkillsStore.getState().loadSkills(); } diff --git a/packages/ui/src/stores/useSkillsStore.ts b/packages/ui/src/stores/useSkillsStore.ts index a7c8da91..b59ec55c 100644 --- a/packages/ui/src/stores/useSkillsStore.ts +++ b/packages/ui/src/stores/useSkillsStore.ts @@ -168,6 +168,10 @@ const getSkillsCacheKey = (directory: string | null): string => { 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 FAST_HEALTH_POLL_INTERVAL_MS = 300; const FAST_HEALTH_POLL_ATTEMPTS = 4; @@ -302,6 +306,7 @@ export const useSkillsStore = create()( } const needsReload = payload?.requiresReload ?? false; + invalidateSkillsLoadCache(currentDirectory); if (needsReload) { requiresReload = true; await refreshSkillsAfterOpenCodeRestart({ @@ -352,6 +357,7 @@ export const useSkillsStore = create()( } const needsReload = payload?.requiresReload ?? false; + invalidateSkillsLoadCache(currentDirectory); if (needsReload) { requiresReload = true; await refreshSkillsAfterOpenCodeRestart({ @@ -393,6 +399,7 @@ export const useSkillsStore = create()( } const needsReload = payload?.requiresReload ?? false; + invalidateSkillsLoadCache(currentDirectory); if (needsReload) { requiresReload = true; await refreshSkillsAfterOpenCodeRestart({ @@ -554,13 +561,15 @@ export async function refreshSkillsAfterOpenCodeRestart(options?: { message?: st await waitForOpenCodeConnection(options?.delayMs); updateConfigUpdateMessage("Refreshing skills…"); const skillsStore = useSkillsStore.getState(); + invalidateSkillsLoadCache(); const loaded = await skillsStore.loadSkills(); if (loaded) { emitConfigChange("skills", { source: CONFIG_EVENT_SOURCE }); } - } catch { + } catch (error) { updateConfigUpdateMessage("OpenCode refresh failed. Please retry."); await sleep(1500); + throw error; } finally { finishConfigUpdate(); } diff --git a/packages/vscode/src/opencodeConfig.ts b/packages/vscode/src/opencodeConfig.ts index 80c79a4b..5b6e28cc 100644 --- a/packages/vscode/src/opencodeConfig.ts +++ b/packages/vscode/src/opencodeConfig.ts @@ -1666,6 +1666,39 @@ export const updateAgent = (agentName: string, updates: Record, for (const [field, value] of Object.entries(updates || {})) { 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; + const current = agentMap[agentName] as Record | 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); if (mdExists || creatingNewMd) { diff --git a/packages/vscode/webview/main.tsx b/packages/vscode/webview/main.tsx index 1756fe9c..8434d7f6 100644 --- a/packages/vscode/webview/main.tsx +++ b/packages/vscode/webview/main.tsx @@ -1354,7 +1354,7 @@ onCommand('showSettings', () => { // and refreshes config/data. Triggered by the "Restart API Connection" command. onCommand('reloadOpenCode', () => { void import('@openchamber/ui/stores/useAgentsStore').then(({ reloadOpenCodeConfiguration }) => { - void reloadOpenCodeConfiguration(); + void reloadOpenCodeConfiguration().catch(() => undefined); }); }); diff --git a/packages/web/server/lib/opencode/agents.js b/packages/web/server/lib/opencode/agents.js index 06932df0..c2a25106 100644 --- a/packages/web/server/lib/opencode/agents.js +++ b/packages/web/server/lib/opencode/agents.js @@ -449,6 +449,39 @@ function updateAgent(agentName, updates, workingDirectory) { for (const [field, value] of Object.entries(updates)) { 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)); if (mdExists || creatingNewMd) {