From 3f7659412c373866c20ec7fed79345d124c391e3 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Sun, 24 May 2026 14:19:42 -0700 Subject: [PATCH] feat(settings): report actual version change in agent update toast Compare the freshly-detected agent version against the pre-update version to accurately report whether the agent was updated or is already up to date. - Update SingleAgentSettings to compare new and old version post-update - Add unit tests verifying successful update and already up-to-date toasts --- .../parts/SingleAgentSettings.test.tsx | 49 +++++++++++++++++-- .../parts/SingleAgentSettings.tsx | 35 ++++++++----- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.test.tsx b/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.test.tsx index 1e5e62e1..b58b092f 100644 --- a/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.test.tsx +++ b/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.test.tsx @@ -191,9 +191,15 @@ vi.mock("@/renderer/state/appStore", () => ({ })); vi.mock("@/renderer/state/agentStatusesStore", () => ({ - useAgentStatusesStore: ( - selector: (state: { agentStatuses: AgentStatus[]; wslAgentStatuses: AgentStatus[] }) => unknown, - ) => selector(statusesState), + useAgentStatusesStore: Object.assign( + ( + selector: (state: { + agentStatuses: AgentStatus[]; + wslAgentStatuses: AgentStatus[]; + }) => unknown, + ) => selector(statusesState), + { getState: () => statusesState }, + ), })); vi.mock("@/renderer/state/sharedSettingsStore", () => ({ @@ -971,6 +977,43 @@ describe("SingleAgentSettings", () => { platformSpy.mockRestore(); }); + it("reports the new version in the toast after a successful update", async () => { + statusesState.agentStatuses = [ + makeStatus("claude", { label: "Claude Code", version: "1.0.0" }), + ]; + getLatestAgentVersionMock.mockResolvedValueOnce({ version: "1.1.0", source: "npm" }); + refreshAgentStatusesMock.mockImplementation(async () => { + statusesState.agentStatuses = [ + makeStatus("claude", { label: "Claude Code", version: "1.1.0" }), + ]; + }); + + render(); + fireEvent.click( + await screen.findByRole("button", { name: /Update to v1\.1\.0 for Claude Code/ }), + ); + + await waitFor(() => + expect(toastMock.success).toHaveBeenCalledWith("Claude Code updated to v1.1.0."), + ); + }); + + it("reports up-to-date when the update command leaves the version unchanged", async () => { + statusesState.agentStatuses = [ + makeStatus("claude", { label: "Claude Code", version: "1.0.0" }), + ]; + getLatestAgentVersionMock.mockResolvedValueOnce({ version: "1.1.0", source: "npm" }); + + render(); + fireEvent.click( + await screen.findByRole("button", { name: /Update to v1\.1\.0 for Claude Code/ }), + ); + + await waitFor(() => + expect(toastMock.success).toHaveBeenCalledWith("Claude Code is already up to date."), + ); + }); + it("shows an error toast when ACP agent-owned auth fails", async () => { authenticateAcpAgentMock.mockRejectedValueOnce(new Error("browser closed")); statusesState.agentStatuses = [ diff --git a/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.tsx b/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.tsx index 595b33f4..84875f4c 100644 --- a/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.tsx +++ b/src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.tsx @@ -605,7 +605,6 @@ export function SingleAgentSettings(props: { agentKind: string }) { latestNpmEntry && latestNpmEntry.agentKind === props.agentKind ? latestNpmEntry.version : undefined; - const latestVersionProbeDone = latestNpmEntry?.agentKind === props.agentKind; const newestInstalledVersion = installedStatuses.reduce((latest, status) => { const version = status.version; if (!version) return latest; @@ -936,6 +935,7 @@ export function SingleAgentSettings(props: { agentKind: string }) { const scope = statusUpdateScope(status); const envKey = statusEnvKey(status); const envSuffix = envLabel(status) ? ` (${envLabel(status)})` : ""; + const previousVersion = status.version; setBinaryUpdatePendingEnvKey(envKey); readBridge() .updateAgentBinary({ @@ -945,8 +945,6 @@ export function SingleAgentSettings(props: { agentKind: string }) { }) .then(async (result) => { if (result.ok) { - const targetSuffix = latestNpmVersion ? ` to v${latestNpmVersion}` : ""; - toast.success(`${agent.label}${envSuffix} updated${targetSuffix}.`); // Switch the row to a loader while we wait for the supervisor to // re-detect the new installed version. The store updates via the // `agent-status-updated` events emitted during refresh; once the row @@ -960,6 +958,24 @@ export function SingleAgentSettings(props: { agentKind: string }) { } finally { setRedetectingEnvKey(undefined); } + // Many built-in updaters exit 0 even when there's nothing to do. + // Compare the freshly-detected version to the pre-update value so + // the toast reflects what actually happened. + const store = useAgentStatusesStore.getState(); + const pool = status.envKind === "wsl" ? store.wslAgentStatuses : store.agentStatuses; + const newVersion = pool.find( + (entry) => + entry.kind === props.agentKind && + entry.envKind === status.envKind && + entry.envDistro === status.envDistro, + )?.version; + if (newVersion && newVersion === previousVersion) { + toast.success(`${agent.label}${envSuffix} is already up to date.`); + } else if (newVersion) { + toast.success(`${agent.label}${envSuffix} updated to v${newVersion}.`); + } else { + toast.success(`${agent.label}${envSuffix} updated.`); + } return; } const detail = result.output?.trim(); @@ -1022,17 +1038,12 @@ export function SingleAgentSettings(props: { agentKind: string }) { ? newestInstalledVersion : undefined; const targetVersion = registryTargetVersion ?? peerTargetVersion; - const updateLabel = targetVersion - ? `Update to v${targetVersion}` - : "Check for updates"; + const updateLabel = targetVersion ? `Update to v${targetVersion}` : ""; const showUpdateButton = !isRedetecting && acpInstanceId === undefined && row.status.installed && - (targetVersion !== undefined || - (row.status.update?.builtIn !== undefined && - latestVersionProbeDone && - latestNpmVersion === undefined)); + targetVersion !== undefined; // Resolve the update command client-side from the same shared // module the supervisor uses, so the tooltip always has the // exact command we're about to run — no extra IPC roundtrip @@ -1094,9 +1105,7 @@ export function SingleAgentSettings(props: { agentKind: string }) { ) : ( - {targetVersion - ? `Update ${agent.label} to v${targetVersion}` - : `Check ${agent.label} for updates`} + Update {agent.label} to v{targetVersion} )}