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
@@ -4,7 +4,7 @@ import { Input } from '@/components/ui/input';
import { NumberInput } from '@/components/ui/number-input';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/components/ui';
import { useAgentsStore, type AgentConfig, type AgentScope } from '@/stores/useAgentsStore';
import { useAgentsStore, type AgentConfig, type AgentMutationResult, type AgentScope } from '@/stores/useAgentsStore';
import { useShallow } from 'zustand/react/shallow';
import { useDirectorySync } from '@/sync/sync-context';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
@@ -635,18 +635,22 @@ export const AgentsPage: React.FC = () => {
scope: isNewAgent ? draftScope : undefined,
};
let success: boolean;
let result: AgentMutationResult;
if (isNewAgent) {
success = await createAgent(config);
if (success) {
result = await createAgent(config);
if (result.ok) {
setAgentDraft(null); // Clear draft after successful creation
}
} else {
success = await updateAgent(agentName, config);
result = await updateAgent(agentName, config);
}
if (success) {
toast.success(isNewAgent ? t('settings.agents.page.toast.created') : t('settings.agents.page.toast.updated'));
if (result.ok) {
if (result.requiresManualRestart) {
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
} else {
toast.success(isNewAgent ? t('settings.agents.page.toast.created') : t('settings.agents.page.toast.updated'));
}
} else {
toast.error(isNewAgent ? t('settings.agents.page.toast.createFailed') : t('settings.agents.page.toast.updateFailed'));
}
@@ -182,10 +182,12 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
setIsConfirmActionPending(true);
try {
const success = await deleteAgent(confirmActionAgent.name, (confirmActionAgent as Agent & { scope?: AgentScope }).scope);
const result = await deleteAgent(confirmActionAgent.name, (confirmActionAgent as Agent & { scope?: AgentScope }).scope);
if (success) {
if (confirmActionType === 'delete') {
if (result.ok) {
if (result.requiresManualRestart) {
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
} else if (confirmActionType === 'delete') {
toast.success(t('settings.agents.sidebar.toast.agentDeleted', { name: confirmActionAgent.name }));
} else {
toast.success(t('settings.agents.sidebar.toast.agentReset', { name: confirmActionAgent.name }));
@@ -274,7 +276,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
? `${renameDialogAgent.model.providerID}/${renameDialogAgent.model.modelID}`
: null;
const renameExt = renameDialogAgent as Agent & { scope?: AgentScope; disable?: boolean };
const success = await createAgent({
const createResult = await createAgent({
name: sanitizedName,
description: renameDialogAgent.description,
model: renameModelStr,
@@ -288,11 +290,15 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
scope: renameExt.scope,
});
if (success) {
if (createResult.ok) {
// Delete old agent
const deleteSuccess = await deleteAgent(renameDialogAgent.name, renameExt.scope);
if (deleteSuccess) {
toast.success(`Agent renamed to "${sanitizedName}"`);
const deleteResult = await deleteAgent(renameDialogAgent.name, renameExt.scope);
if (deleteResult.ok) {
if (createResult.requiresManualRestart || deleteResult.requiresManualRestart) {
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
} else {
toast.success(t('settings.agents.sidebar.toast.agentRenamed', { name: sanitizedName }));
}
setSelectedAgent(sanitizedName);
} else {
toast.error(t('settings.agents.sidebar.toast.removeOldAfterRenameFailed'));