Accumulate OpenCode settings restarts behind Apply & Restart
Defer OpenCode reloads after settings mutations, track pending changes, and expose a top-right Apply & Restart OpenCode action with a counter so sessions stay available until the user explicitly applies. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
775da6e9f4
commit
57819dd164
@@ -243,6 +243,8 @@ export const AgentsPage: React.FC = () => {
|
||||
if (result.ok) {
|
||||
if (result.requiresManualRestart) {
|
||||
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
|
||||
} else if (result.restartDeferred) {
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} else {
|
||||
toast.success(isNewAgent ? t('settings.agents.page.toast.created') : t('settings.agents.page.toast.updated'));
|
||||
}
|
||||
|
||||
@@ -188,6 +188,8 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
||||
if (result.ok) {
|
||||
if (result.requiresManualRestart) {
|
||||
toast.warning(t('settings.agents.page.toast.savedManualRestart'));
|
||||
} else if (result.restartDeferred) {
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} else if (confirmActionType === 'delete') {
|
||||
toast.success(t('settings.agents.sidebar.toast.agentDeleted', { name: confirmActionAgent.name }));
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@/lib/responseStyle';
|
||||
import type { DesktopSettings } from '@/lib/desktop';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import { noteDeferredRestartFromPayload, recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
||||
import {
|
||||
SettingsSection,
|
||||
@@ -242,13 +242,18 @@ export const BehaviorPage: React.FC = () => {
|
||||
throw new Error(await readApiError(response, t('settings.behavior.page.toast.saveFailed')));
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!noteDeferredRestartFromPayload(payload, 'behavior', { id: 'agents-md' })) {
|
||||
recordDeferredOpenCodeRestart('behavior', { id: 'agents-md' });
|
||||
}
|
||||
|
||||
await saveBehaviorSetting({
|
||||
globalBehaviorPrompt: content,
|
||||
}, t('settings.behavior.page.toast.saveFailed'));
|
||||
|
||||
setPrompt(content);
|
||||
setInitialPrompt(content);
|
||||
toast.success(t('settings.behavior.page.toast.saved'));
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} catch (error) {
|
||||
console.error('Failed to save behavior:', error);
|
||||
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
|
||||
@@ -266,19 +271,8 @@ export const BehaviorPage: React.FC = () => {
|
||||
t('settings.behavior.page.toast.saveFailed'),
|
||||
);
|
||||
setInitialOptimizeSystemPrompt(optimizeSystemPrompt);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
|
||||
toast.error(message);
|
||||
setIsApplyingPromptOptimization(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await reloadOpenCodeConfiguration({
|
||||
message: t('settings.behavior.page.systemPromptOptimization.restarting'),
|
||||
mode: 'projects',
|
||||
scopes: ['all'],
|
||||
});
|
||||
recordDeferredOpenCodeRestart('behavior', { id: 'optimize-system-prompt' });
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
|
||||
toast.error(message);
|
||||
@@ -317,7 +311,7 @@ export const BehaviorPage: React.FC = () => {
|
||||
>
|
||||
{isApplyingPromptOptimization
|
||||
? t('settings.common.actions.saving')
|
||||
: t('settings.openchamber.opencodeCli.actions.saveAndReload')}
|
||||
: t('settings.common.actions.saveChanges')}
|
||||
</Button>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
@@ -930,6 +930,8 @@ export const McpPage: React.FC = () => {
|
||||
: t('settings.mcp.page.toast.savedReloadFailed')), {
|
||||
description: result.warning || t('settings.mcp.page.toast.retryRefreshHint'),
|
||||
});
|
||||
} else if (result.restartDeferred) {
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} else {
|
||||
toast.success(result.message || (isNewServer
|
||||
? t('settings.mcp.page.toast.serverCreatedReloading')
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
} from '@/components/sections/shared/SettingsSection';
|
||||
import { isDesktopShell, requestFileAccess } from '@/lib/desktop';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import { recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { isWindowsArm64 } from '@/lib/platform';
|
||||
import { toast } from '@/components/ui';
|
||||
|
||||
export const OpenCodeCliSettings: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
@@ -89,11 +90,8 @@ export const OpenCodeCliSettings: React.FC = () => {
|
||||
? trimmed.slice(1, -1).trim()
|
||||
: trimmed;
|
||||
await updateDesktopSettings({ opencodeBinary: unquoted });
|
||||
await reloadOpenCodeConfiguration({
|
||||
message: t('settings.openchamber.opencodeCli.actions.restartingOpenCode'),
|
||||
mode: 'projects',
|
||||
scopes: ['all'],
|
||||
});
|
||||
recordDeferredOpenCodeRestart('cli', { id: 'opencode-binary' });
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -179,7 +177,7 @@ export const OpenCodeCliSettings: React.FC = () => {
|
||||
disabled={isLoading || isSaving}
|
||||
className="shrink-0 !font-normal"
|
||||
>
|
||||
{isSaving ? t('settings.common.actions.saving') : t('settings.openchamber.opencodeCli.actions.saveAndReload')}
|
||||
{isSaving ? t('settings.common.actions.saving') : t('settings.common.actions.saveChanges')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsInset>
|
||||
|
||||
@@ -109,7 +109,11 @@ export const AddPluginDialog: React.FC<AddPluginDialogProps> = ({
|
||||
});
|
||||
}
|
||||
if (result.ok) {
|
||||
toast.success(result.message || t('settings.plugins.toast.created'));
|
||||
if (result.restartDeferred) {
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} else {
|
||||
toast.success(result.message || t('settings.plugins.toast.created'));
|
||||
}
|
||||
if (result.reloadFailed) {
|
||||
toast.warning(t('settings.plugins.toast.reloadFailed'));
|
||||
}
|
||||
|
||||
@@ -183,6 +183,8 @@ export const PluginsPage: React.FC = () => {
|
||||
result.message || t('settings.plugins.toast.reloadFailed'),
|
||||
{ description: result.warning },
|
||||
);
|
||||
} else if (result.restartDeferred) {
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} else {
|
||||
toast.success(result.message || t('settings.plugins.toast.updated'));
|
||||
}
|
||||
@@ -299,6 +301,8 @@ export const PluginsPage: React.FC = () => {
|
||||
result.message || t('settings.plugins.toast.reloadFailed'),
|
||||
{ description: result.warning },
|
||||
);
|
||||
} else if (result.restartDeferred) {
|
||||
toast.success(t('settings.view.pendingRestart.saved'));
|
||||
} else {
|
||||
toast.success(result.message || t('settings.plugins.toast.updated'));
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { toast } from '@/components/ui';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import type { IconName } from "@/components/icon/icons";
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import { noteDeferredRestartFromPayload, recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { openExternalUrl } from '@/lib/url';
|
||||
@@ -351,7 +351,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
|
||||
toast.success(t('settings.providers.page.toast.apiKeySaved'));
|
||||
setApiKeyInputs((prev) => ({ ...prev, [providerId]: '' }));
|
||||
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
|
||||
recordDeferredOpenCodeRestart('providers', { id: providerId });
|
||||
setSelectedProvider(providerId);
|
||||
} catch (error) {
|
||||
console.error('Failed to save API key:', error);
|
||||
@@ -444,7 +444,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
toast.success(t('settings.providers.page.toast.oauthCompleted'));
|
||||
setOauthCodes((prev) => ({ ...prev, [codeKey]: '' }));
|
||||
setPendingOAuth(null);
|
||||
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
|
||||
recordDeferredOpenCodeRestart('providers', { id: providerId });
|
||||
setSelectedProvider(providerId);
|
||||
} catch (error) {
|
||||
console.error('Failed to complete OAuth flow:', error);
|
||||
@@ -490,7 +490,9 @@ export const ProvidersPage: React.FC = () => {
|
||||
}
|
||||
|
||||
toast.success(t('settings.providers.page.toast.providerDisconnected'));
|
||||
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
|
||||
if (!noteDeferredRestartFromPayload(payload, 'providers', { id: providerId })) {
|
||||
recordDeferredOpenCodeRestart('providers', { id: providerId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect provider:', error);
|
||||
toast.error(t('settings.providers.page.toast.providerDisconnectFailed'));
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Icon } from '@/components/icon/Icon';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { applyPendingOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||
import {
|
||||
selectPendingOpenCodeRestartCount,
|
||||
usePendingOpenCodeRestartStore,
|
||||
} from '@/stores/usePendingOpenCodeRestartStore';
|
||||
import { toast } from '@/components/ui';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type PendingOpenCodeRestartActionProps = {
|
||||
className?: string;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export const PendingOpenCodeRestartAction: React.FC<PendingOpenCodeRestartActionProps> = ({
|
||||
className,
|
||||
compact = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const pendingCount = usePendingOpenCodeRestartStore(selectPendingOpenCodeRestartCount);
|
||||
const isApplying = usePendingOpenCodeRestartStore((state) => state.isApplying);
|
||||
|
||||
const handleApply = React.useCallback(async () => {
|
||||
try {
|
||||
const result = await applyPendingOpenCodeRestart({
|
||||
message: t('settings.view.pendingRestart.applying'),
|
||||
});
|
||||
if (result.requiresManualRestart) {
|
||||
toast.warning(t('settings.view.pendingRestart.manualRestartRequired'));
|
||||
return;
|
||||
}
|
||||
if (result.ok) {
|
||||
toast.success(t('settings.view.pendingRestart.applied'));
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.message
|
||||
? error.message
|
||||
: t('settings.view.pendingRestart.applyFailed');
|
||||
toast.error(message);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
if (pendingCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltip = pendingCount === 1
|
||||
? t('settings.view.actions.applyAndRestartOpenCodeTooltipSingle')
|
||||
: t('settings.view.actions.applyAndRestartOpenCodeTooltipPlural', { count: pendingCount });
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
size={compact ? 'xs' : 'sm'}
|
||||
disabled={isApplying}
|
||||
title={tooltip}
|
||||
aria-label={tooltip}
|
||||
onClick={() => void handleApply()}
|
||||
className={cn('gap-1.5 shadow-sm', className)}
|
||||
>
|
||||
<Icon name="restart" className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">
|
||||
{isApplying
|
||||
? t('settings.view.pendingRestart.applying')
|
||||
: t('settings.view.actions.applyAndRestartOpenCode')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-5 items-center justify-center rounded-md px-1.5 typography-micro font-semibold',
|
||||
'bg-primary-foreground/15 text-primary-foreground',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{pendingCount}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -49,6 +49,11 @@ import { Icon } from "@/components/icon/Icon";
|
||||
import type { IconName } from "@/components/icon/icons";
|
||||
import { McpIcon } from '@/components/icons/McpIcon';
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import { PendingOpenCodeRestartAction } from '@/components/views/PendingOpenCodeRestartAction';
|
||||
import {
|
||||
selectPendingOpenCodeRestartCount,
|
||||
usePendingOpenCodeRestartStore,
|
||||
} from '@/stores/usePendingOpenCodeRestartStore';
|
||||
import {
|
||||
SETTINGS_PAGE_METADATA,
|
||||
getSettingsPageMeta,
|
||||
@@ -235,6 +240,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
const { t } = useI18n();
|
||||
const deviceInfo = useDeviceInfo();
|
||||
const isMobile = forceMobile ?? deviceInfo.isMobile;
|
||||
const pendingRestartCount = usePendingOpenCodeRestartStore(selectPendingOpenCodeRestartCount);
|
||||
|
||||
const settingsPageRaw = useUIStore((state) => state.settingsPage);
|
||||
const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen);
|
||||
@@ -974,7 +980,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
{/* Footer */}
|
||||
<div className="overflow-hidden transition-opacity duration-150 opacity-100">
|
||||
<div className="border-t border-border bg-background px-4 py-1 space-y-0.5 sm:bg-sidebar">
|
||||
{!runtimeCtx.isVSCode && (
|
||||
{!runtimeCtx.isVSCode && pendingRestartCount <= 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@@ -984,7 +990,20 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ 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'] }).catch(() => undefined)}
|
||||
onClick={() => {
|
||||
void (async () => {
|
||||
try {
|
||||
await reloadOpenCodeConfiguration({
|
||||
message: t('settings.view.pendingRestart.applying'),
|
||||
mode: 'projects',
|
||||
scopes: ['all'],
|
||||
});
|
||||
usePendingOpenCodeRestartStore.getState().clear();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<Icon name="restart" className="h-4 w-4 shrink-0" />
|
||||
<span>{t('settings.view.actions.reloadOpenCode')}</span>
|
||||
@@ -1113,6 +1132,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
</button>
|
||||
)}
|
||||
|
||||
<PendingOpenCodeRestartAction compact className="max-w-[min(100%,14rem)]" />
|
||||
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1140,8 +1161,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onClose && (
|
||||
<div className={cn('absolute right-0.5 z-50', isWindowed ? 'top-0.5' : 'top-1')}>
|
||||
<div className={cn('absolute right-0.5 z-50 flex items-center gap-1.5', isWindowed ? 'top-0.5' : 'top-1')}>
|
||||
<PendingOpenCodeRestartAction compact className="max-w-[min(100%,16rem)]" />
|
||||
{onClose && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -1151,8 +1173,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
>
|
||||
<Icon name="close" className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user