fix(agents): stop falsely reporting saved agent edits on external OpenCode; make model-selector shortcut customizable (#1839)

* feat(shortcuts): make 'Open model selector' shortcut customizable

Lets users remap the model selector shortcut (e.g. to Ctrl+M) via
Settings > OpenChamber > Shortcuts, matching OpenCode's quick
model-switch keybinding workflow.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(agents): surface manual-restart needed when on external OpenCode

Agent prompt/permission/settings edits are written to disk, but an
external OpenCode server (skip-start or auto-detected on the default
port) is not owned by OpenChamber and is only health-probed on config
change, so it keeps serving its startup-cached config until restarted.
The API previously claimed a successful reload, so the UI silently
reverted the edit to the stale/default value on refresh.

Now refreshOpenCodeAfterConfigChange reports whether a real reload
happened; agent routes return requiresManualRestart for external mode;
and the agents UI keeps the saved values and warns the user to restart
their OpenCode server instead of showing a false success. Managed mode
behavior is unchanged (process is restarted and reload is live).

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Serhii Dziupin
2026-06-26 19:37:45 +03:00
committed by GitHub
co-authored by Serhii Dziupin
parent 3e3cd82a47
commit f4e90ca232
15 changed files with 155 additions and 53 deletions
+50 -16
View File
@@ -115,6 +115,17 @@ export interface AgentConfig {
scope?: AgentScope;
}
/**
* Result of an agent config mutation.
* `requiresManualRestart` is true when the change was persisted to disk but the
* connected (external) OpenCode server could not be reloaded by OpenChamber, so
* the user must restart that server before the change takes effect.
*/
export interface AgentMutationResult {
ok: boolean;
requiresManualRestart?: boolean;
}
// Extended Agent type for API properties not in SDK types
export type AgentWithExtras = Agent & {
native?: boolean;
@@ -190,9 +201,9 @@ interface AgentsStore {
setSelectedAgent: (name: string | null) => void;
setAgentDraft: (draft: AgentDraft | null) => void;
loadAgents: () => Promise<boolean>;
createAgent: (config: AgentConfig) => Promise<boolean>;
updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<boolean>;
deleteAgent: (name: string, scope?: AgentScope) => Promise<boolean>;
createAgent: (config: AgentConfig) => Promise<AgentMutationResult>;
updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<AgentMutationResult>;
deleteAgent: (name: string, scope?: AgentScope) => Promise<AgentMutationResult>;
getAgentByName: (name: string) => Agent | undefined;
// Returns only visible agents (excludes hidden internal agents)
getVisibleAgents: () => Agent[];
@@ -361,8 +372,16 @@ export const useAgentsStore = create<AgentsStore>()(
throw new Error(message);
}
const needsReload = payload?.requiresReload ?? true;
invalidateAgentsLoadCache(configDirectory);
// External OpenCode server: persisted to disk but not reloaded.
// Skip the reload so the form keeps the just-saved values instead of
// reverting to the server's stale, startup-cached config.
if (payload?.requiresManualRestart) {
return { ok: true, requiresManualRestart: true };
}
const needsReload = payload?.requiresReload ?? true;
if (needsReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
@@ -371,17 +390,17 @@ export const useAgentsStore = create<AgentsStore>()(
scopes: ["agents"],
mode: "projects",
});
return true;
return { ok: true };
}
const loaded = await get().loadAgents();
if (loaded) {
emitConfigChange("agents", { source: CONFIG_EVENT_SOURCE });
}
return loaded;
return { ok: loaded };
} catch (error) {
console.error('Failed to create agent:', error);
return false;
return { ok: false };
} finally {
if (!requiresReload) {
finishConfigUpdate();
@@ -424,8 +443,16 @@ export const useAgentsStore = create<AgentsStore>()(
throw new Error(message);
}
const needsReload = payload?.requiresReload ?? true;
invalidateAgentsLoadCache(configDirectory);
// External OpenCode server: persisted to disk but not reloaded.
// Skip the reload so the form keeps the just-saved values instead of
// reverting to the server's stale, startup-cached config.
if (payload?.requiresManualRestart) {
return { ok: true, requiresManualRestart: true };
}
const needsReload = payload?.requiresReload ?? true;
if (needsReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
@@ -434,14 +461,14 @@ export const useAgentsStore = create<AgentsStore>()(
scopes: ["agents"],
mode: "projects",
});
return true;
return { ok: true };
}
const loaded = await get().loadAgents();
if (loaded) {
emitConfigChange("agents", { source: CONFIG_EVENT_SOURCE });
}
return loaded;
return { ok: loaded };
} catch (error) {
console.error('Failed to update agent:', error);
throw error;
@@ -475,8 +502,18 @@ export const useAgentsStore = create<AgentsStore>()(
throw new Error(message);
}
const needsReload = payload?.requiresReload ?? true;
invalidateAgentsLoadCache(configDirectory);
if (get().selectedAgentName === name) {
set({ selectedAgentName: null });
}
// External OpenCode server: persisted to disk but not reloaded.
if (payload?.requiresManualRestart) {
return { ok: true, requiresManualRestart: true };
}
const needsReload = payload?.requiresReload ?? true;
if (needsReload) {
requiresReload = true;
await refreshAfterOpenCodeRestart({
@@ -485,7 +522,7 @@ export const useAgentsStore = create<AgentsStore>()(
scopes: ["agents"],
mode: "projects",
});
return true;
return { ok: true };
}
const loaded = await get().loadAgents();
@@ -493,10 +530,7 @@ export const useAgentsStore = create<AgentsStore>()(
emitConfigChange("agents", { source: CONFIG_EVENT_SOURCE });
}
if (get().selectedAgentName === name) {
set({ selectedAgentName: null });
}
return loaded;
return { ok: loaded };
} catch (error) {
console.error('Failed to delete agent:', error);
throw error;