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:
committed by
GitHub
co-authored by
Serhii Dziupin
parent
3e3cd82a47
commit
f4e90ca232
@@ -26,6 +26,30 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
||||
expandSnippets,
|
||||
} = dependencies;
|
||||
|
||||
// Build the response for a config mutation based on whether OpenCode actually
|
||||
// reloaded the change. When connected to an external OpenCode server that
|
||||
// OpenChamber cannot restart, the change is persisted to disk but the running
|
||||
// server will not serve it until the user restarts that server. We must not
|
||||
// report a clean "reloading" success in that case, otherwise the UI silently
|
||||
// reverts the edit to the stale value on the next refresh.
|
||||
const buildConfigMutationResponse = (refreshResult, { liveMessage, manualRestartMessage }) => {
|
||||
if (refreshResult && refreshResult.external) {
|
||||
return {
|
||||
success: true,
|
||||
requiresReload: false,
|
||||
requiresManualRestart: true,
|
||||
message: manualRestartMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
requiresReload: true,
|
||||
message: liveMessage,
|
||||
reloadDelayMs: clientReloadDelayMs,
|
||||
};
|
||||
};
|
||||
|
||||
const completeMcpMutation = async (res, action, name, applyChange) => {
|
||||
applyChange();
|
||||
|
||||
@@ -104,16 +128,14 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
||||
console.log('[Server] Scope:', scope, 'Working directory:', directory);
|
||||
|
||||
createAgent(agentName, config, directory, scope);
|
||||
await refreshOpenCodeAfterConfigChange('agent creation', {
|
||||
const refreshResult = await refreshOpenCodeAfterConfigChange('agent creation', {
|
||||
agentName
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
requiresReload: true,
|
||||
message: `Agent ${agentName} created successfully. Reloading interface…`,
|
||||
reloadDelayMs: clientReloadDelayMs,
|
||||
});
|
||||
res.json(buildConfigMutationResponse(refreshResult, {
|
||||
liveMessage: `Agent ${agentName} created successfully. Reloading interface…`,
|
||||
manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to create agent:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to create agent' });
|
||||
@@ -134,16 +156,14 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
||||
console.log('[Server] Working directory:', directory);
|
||||
|
||||
updateAgent(agentName, updates, directory);
|
||||
await refreshOpenCodeAfterConfigChange('agent update');
|
||||
const refreshResult = await refreshOpenCodeAfterConfigChange('agent update');
|
||||
|
||||
console.log(`[Server] Agent ${agentName} updated successfully`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
requiresReload: true,
|
||||
message: `Agent ${agentName} updated successfully. Reloading interface…`,
|
||||
reloadDelayMs: clientReloadDelayMs,
|
||||
});
|
||||
res.json(buildConfigMutationResponse(refreshResult, {
|
||||
liveMessage: `Agent ${agentName} updated successfully. Reloading interface…`,
|
||||
manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[Server] Failed to update agent:', error);
|
||||
console.error('[Server] Error stack:', error.stack);
|
||||
@@ -161,14 +181,12 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
||||
|
||||
const scope = req.body?.scope;
|
||||
deleteAgent(agentName, directory, scope);
|
||||
await refreshOpenCodeAfterConfigChange('agent deletion');
|
||||
const refreshResult = await refreshOpenCodeAfterConfigChange('agent deletion');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
requiresReload: true,
|
||||
message: `Agent ${agentName} deleted successfully. Reloading interface…`,
|
||||
reloadDelayMs: clientReloadDelayMs,
|
||||
});
|
||||
res.json(buildConfigMutationResponse(refreshResult, {
|
||||
liveMessage: `Agent ${agentName} deleted successfully. Reloading interface…`,
|
||||
manualRestartMessage: `Agent ${agentName} deleted. Restart your connected OpenCode server to apply the change.`,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to delete agent:', error);
|
||||
res.status(500).json({ error: error.message || 'Failed to delete agent' });
|
||||
|
||||
@@ -753,12 +753,22 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
||||
|
||||
await restartOpenCode();
|
||||
|
||||
// A managed OpenCode process is restarted (and thus re-reads config from
|
||||
// disk) by restartOpenCode(). An external OpenCode server is NOT owned by
|
||||
// OpenChamber: restartOpenCode() only re-probes its health, so the freshly
|
||||
// written config is on disk but the running server keeps serving its old,
|
||||
// startup-cached config until the user restarts it themselves. Report this
|
||||
// honestly so callers don't claim the change is live.
|
||||
const external = state.isExternalOpenCode === true;
|
||||
|
||||
try {
|
||||
await waitForOpenCodeReady();
|
||||
state.isOpenCodeReady = true;
|
||||
state.openCodeNotReadySince = 0;
|
||||
|
||||
if (agentName) {
|
||||
// Waiting for the agent to appear only makes sense when we actually
|
||||
// reloaded config. An external server will never surface it here.
|
||||
if (agentName && !external) {
|
||||
await waitForAgentPresence(agentName);
|
||||
}
|
||||
|
||||
@@ -770,6 +780,8 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
||||
console.error(`Failed to refresh OpenCode after ${reason}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { reloaded: !external, external };
|
||||
};
|
||||
|
||||
const bootstrapOpenCodeAtStartup = async () => {
|
||||
|
||||
Reference in New Issue
Block a user