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}
)}