From f63a4c8ca2a79612fde61facfd25ed04f7dd780f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 25 Dec 2025 08:15:35 +0000 Subject: [PATCH] fix(#903): Optimistic toggle with sync indicator for AI commands The toggle now: 1. Immediately shows the desired state after clicking (optimistic update) 2. Displays a spinning sync icon while waiting for agent confirmation 3. Disables the toggle during sync to prevent double-clicks 4. Reverts to original state if the API call fails 5. Clears sync state once agent reports the expected value This provides clear visual feedback about the async nature of remote config without requiring users to understand the underlying polling mechanism. --- .../src/components/Settings/UnifiedAgents.tsx | 86 ++++++++++++++++--- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/frontend-modern/src/components/Settings/UnifiedAgents.tsx b/frontend-modern/src/components/Settings/UnifiedAgents.tsx index 0b28ca544..30bec78be 100644 --- a/frontend-modern/src/components/Settings/UnifiedAgents.tsx +++ b/frontend-modern/src/components/Settings/UnifiedAgents.tsx @@ -111,6 +111,8 @@ export const UnifiedAgents: Component = () => { const [insecureMode, setInsecureMode] = createSignal(false); // For self-signed certificates (issue #806) const [enableCommands, setEnableCommands] = createSignal(false); // Enable AI command execution (issue #903) const [customAgentUrl, setCustomAgentUrl] = createSignal(''); + // Track pending command config changes: hostId -> desired commandsEnabled value + const [pendingCommandConfig, setPendingCommandConfig] = createSignal>({}); createEffect(() => { if (requiresToken()) { @@ -425,15 +427,47 @@ export const UnifiedAgents: Component = () => { }; const handleToggleCommands = async (hostId: string, enabled: boolean) => { + // Set optimistic/pending state immediately + setPendingCommandConfig(prev => ({ ...prev, [hostId]: enabled })); + try { await MonitoringAPI.updateHostAgentConfig(hostId, { commandsEnabled: enabled }); - notificationStore.success(`AI command execution ${enabled ? 'enabled' : 'disabled'}. Agent will apply change on next report.`); + notificationStore.success(`AI command execution ${enabled ? 'enabled' : 'disabled'}. Syncing with agent...`); } catch (err) { + // On error, clear the pending state so toggle reverts + setPendingCommandConfig(prev => { + const next = { ...prev }; + delete next[hostId]; + return next; + }); logger.error('Failed to toggle AI commands', err); notificationStore.error('Failed to update agent configuration'); } }; + // Clear pending state when agent reports matching the expected value + createEffect(() => { + const pending = pendingCommandConfig(); + const hosts = state.hosts || []; + + // Check if any pending config now matches the reported state + let updated = false; + const newPending = { ...pending }; + + for (const hostId of Object.keys(pending)) { + const host = hosts.find(h => h.id === hostId); + if (host && host.commandsEnabled === pending[hostId]) { + // Agent confirmed the change + delete newPending[hostId]; + updated = true; + } + } + + if (updated) { + setPendingCommandConfig(newPending); + } + }); + return (
@@ -915,19 +949,43 @@ export const UnifiedAgents: Component = () => { } > - + {(() => { + // Use pending state if set, otherwise use agent-reported state + const pending = pendingCommandConfig(); + const isPending = agent.id in pending; + const effectiveEnabled = isPending ? pending[agent.id] : agent.commandsEnabled; + + return ( +
+ + + + + + + +
+ ); + })()}