Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand Down Expand Up @@ -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(<SingleAgentSettings agentKind="claude" />);
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(<SingleAgentSettings agentKind="claude" />);
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 = [
Expand Down
35 changes: 22 additions & 13 deletions src/renderer/views/SettingsOverlay/parts/SingleAgentSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined>((latest, status) => {
const version = status.version;
if (!version) return latest;
Expand Down Expand Up @@ -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({
Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1094,9 +1105,7 @@ export function SingleAgentSettings(props: { agentKind: string }) {
</div>
) : (
<span className="text-[11px]">
{targetVersion
? `Update ${agent.label} to v${targetVersion}`
: `Check ${agent.label} for updates`}
Update {agent.label} to v{targetVersion}
</span>
)}
</Tooltip.Content>
Expand Down