- {section}
- {isAgentsSectionActive && isRefreshingAgents ? (
-
- ) : null}
+
+
+ {section}
+ {isAgentsSectionActive && isRefreshingAgents ? (
+
+ ) : null}
+
+ {/* Floats over the scroll area rather than living in the section's
+ flow, so it keeps its position and state across agent-section
+ remounts (`key={activeSection}`). */}
+ {isMachineScopedSection ?
: null}
)
}
diff --git a/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.test.tsx b/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.test.tsx
index 0c2abb3ab..eecce6ce8 100644
--- a/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.test.tsx
+++ b/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.test.tsx
@@ -17,7 +17,16 @@ const statusesState = {
const settingsState = {
providerOrder: [] as string[],
+ machineScopeModes: {
+ providerOrder: "synced" as const,
+ hiddenModels: "synced" as const,
+ disabledAgents: "synced" as const,
+ },
+ machineSettings: {} as Record
,
setProviderOrder: vi.fn<(order: string[]) => void>(),
+ setMachineScopeMode: vi.fn<(domain: string, mode: string) => void>(),
+ setMachineProviderOrder: vi.fn<(machineId: string, order: string[]) => void>(),
+ lockProviderOrderToMachine: vi.fn<(machineId: string) => void>(),
acpRegistryInstalledAgents: {} as Record,
syncAcpRegistryInstalledAgents: vi.fn<(installed: InstalledAcpRegistryAgent[]) => void>(),
};
@@ -67,6 +76,12 @@ vi.mock("@/renderer/bridge", () => ({
readBridge: () => bridge,
}));
+vi.mock("@/renderer/state/remoteServersStore", () => ({
+ useRemoteServersStore: (
+ selector: (state: { servers: never[]; runtime: Record }) => unknown,
+ ) => selector({ servers: [], runtime: {} }),
+}));
+
vi.mock("@heroui/react", () => ({
toast: toastMock,
Button: (props: {
@@ -100,6 +115,9 @@ vi.mock("@dnd-kit/react/sortable", () => ({
vi.mock("@/renderer/components/common", () => ({
PixelLoader: () => ,
+ ToggleSwitch: (props: { "aria-label"?: string; isSelected?: boolean }) => (
+
+ ),
}));
vi.mock("@/renderer/components/providers/ProviderIcon", () => ({
@@ -147,7 +165,7 @@ describe("ModelOrderSection provider updates", () => {
expect(screen.getByText("—")).toBeTruthy();
});
- it("breaks the version out per environment only when they disagree", async () => {
+ it("scopes each provider row's version to the selected machine", async () => {
statusesState.agentStatuses = [
makeStatus({ kind: "claude", label: "Claude Code", version: "1.2.3", envKind: "windows" }),
makeStatus({ kind: "codex", label: "Codex", version: "1.0.0", envKind: "windows" }),
@@ -171,11 +189,11 @@ describe("ModelOrderSection provider updates", () => {
render();
- expect(await screen.findByText("Windows v1.2.3 · WSL (Ubuntu) v1.1.0")).toHaveClass(
- "max-w-[45%]",
- "truncate",
- );
+ // The default scope is the local machine, so only its versions render —
+ // the WSL copy belongs to the "local/wsl:Ubuntu" machine.
+ expect(await screen.findByText("v1.2.3")).toBeTruthy();
expect(screen.getByText("v1.0.0")).toBeTruthy();
+ expect(screen.queryByText(/WSL/)).toBeNull();
});
it("offers a per-provider update when the published version is newer", async () => {
@@ -388,10 +406,12 @@ describe("ModelOrderSection provider updates", () => {
});
});
- it("updates each outdated environment of a provider once", async () => {
+ it("updates only the selected machine's environment", async () => {
statusesState.agentStatuses = [
makeStatus({ kind: "claude", label: "Claude Code", version: "1.2.3", envKind: "windows" }),
];
+ // A WSL copy belongs to another machine and must not be touched by an
+ // update-all run scoped to the local machine.
statusesState.wslAgentStatuses = [
makeStatus({
kind: "claude",
@@ -407,10 +427,9 @@ describe("ModelOrderSection provider updates", () => {
fireEvent.click(await screen.findByRole("button", { name: "Update all" }));
- await waitFor(() => expect(bridge.updateAgentBinary).toHaveBeenCalledTimes(2));
+ await waitFor(() => expect(bridge.updateAgentBinary).toHaveBeenCalledTimes(1));
expect(bridge.updateAgentBinary.mock.calls.map(([payload]) => payload)).toEqual([
{ agentKind: "claude", envKind: "windows" },
- { agentKind: "claude", envKind: "wsl", wslDistro: "Ubuntu" },
]);
});
@@ -433,34 +452,20 @@ describe("ModelOrderSection provider updates", () => {
expect(toastMock.success).not.toHaveBeenCalled();
});
- it("still updates the remaining environments when one environment fails", async () => {
+ it("reports a failed machine update without claiming success", async () => {
statusesState.agentStatuses = [
makeStatus({ kind: "claude", label: "Claude Code", version: "1.2.3", envKind: "windows" }),
];
- statusesState.wslAgentStatuses = [
- makeStatus({
- kind: "claude",
- label: "Claude Code",
- version: "1.1.0",
- envKind: "wsl",
- envDistro: "Ubuntu",
- }),
- ];
bridge.getLatestAgentVersion.mockResolvedValue({ source: "npm", version: "1.3.0" });
- bridge.updateAgentBinary.mockImplementation(({ envKind }) =>
- Promise.resolve(
- envKind === "windows" ? { ok: true } : { ok: false, output: "npm ERR! EACCES" },
- ),
- );
+ bridge.updateAgentBinary.mockResolvedValue({ ok: false, output: "npm ERR! EACCES" });
render();
fireEvent.click(await screen.findByRole("button", { name: "Update Claude Code to v1.3.0" }));
- await waitFor(() => expect(bridge.updateAgentBinary).toHaveBeenCalledTimes(2));
+ await waitFor(() => expect(bridge.updateAgentBinary).toHaveBeenCalledTimes(1));
expect(bridge.updateAgentBinary.mock.calls.map(([payload]) => payload)).toEqual([
{ agentKind: "claude", envKind: "windows" },
- { agentKind: "claude", envKind: "wsl", wslDistro: "Ubuntu" },
]);
expect(toastMock.danger).toHaveBeenCalledWith("Unable to update Claude Code: npm ERR! EACCES");
expect(toastMock.success).not.toHaveBeenCalled();
diff --git a/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.tsx b/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.tsx
index 057b9b613..dfc365cc2 100644
--- a/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.tsx
+++ b/src/renderer/views/SettingsOverlay/parts/ModelOrderSection.tsx
@@ -7,10 +7,13 @@ import type { AgentStatus } from "@/shared/contracts";
import { useSharedSettings } from "@/renderer/state/sharedSettingsStore";
import { useAgentStatusesStore } from "@/renderer/state/agentStatusesStore";
import { getSettingsInstalledAgents } from "@/shared/agentStatus";
-import { PixelLoader } from "@/renderer/components/common";
+import { PixelLoader, ToggleSwitch } from "@/renderer/components/common";
import { ProviderIcon } from "@/renderer/components/providers/ProviderIcon";
import { getProviderModelPickerRank } from "@/renderer/components/providers/providerManifest";
import { useProviderUpdates, type ProviderUpdateEntry } from "./useProviderUpdates";
+import { machineIdForStatus, useMachines, useSelectedMachine } from "@/renderer/state/machines";
+import { useRemoteServersStore } from "@/renderer/state/remoteServersStore";
+import { effectiveProviderOrder } from "@/shared/machineSettings";
function resolveDisplayedKinds(
installed: readonly AgentStatus[],
@@ -144,17 +147,56 @@ export function ModelOrderSection() {
const agentStatuses = useAgentStatusesStore((s) => s.agentStatuses);
const wslAgentStatuses = useAgentStatusesStore((s) => s.wslAgentStatuses);
const providerOrder = useSharedSettings((s) => s.providerOrder);
+ const machineScopeModes = useSharedSettings((s) => s.machineScopeModes);
+ const machineSettings = useSharedSettings((s) => s.machineSettings);
const setProviderOrder = useSharedSettings((s) => s.setProviderOrder);
+ const setMachineScopeMode = useSharedSettings((s) => s.setMachineScopeMode);
+ const setMachineProviderOrder = useSharedSettings((s) => s.setMachineProviderOrder);
+ const lockProviderOrderToMachine = useSharedSettings((s) => s.lockProviderOrderToMachine);
- const installedAgents = getSettingsInstalledAgents(agentStatuses, wslAgentStatuses);
- const displayedKinds = resolveDisplayedKinds(installedAgents, providerOrder);
+ const machines = useMachines();
+ const selectedMachine = useSelectedMachine();
+ const isRemoteMachine = selectedMachine.ref.host === "remote";
+ const remoteRuntime = useRemoteServersStore((s) =>
+ selectedMachine.desktopId ? s.runtime[selectedMachine.desktopId] : undefined,
+ );
+ const orderLocked = machineScopeModes.providerOrder === "synced";
+ const activeOrder = effectiveProviderOrder(
+ { machineScopeModes, machineSettings, providerOrder },
+ selectedMachine.id,
+ );
+
+ // The list shows the selected machine's providers: local machines filter the
+ // local status stores by machine; remote machines read the paired host's
+ // already-collected statuses.
+ const installedAgents = isRemoteMachine
+ ? getSettingsInstalledAgents(
+ selectedMachine.ref.env.kind === "native"
+ ? (remoteRuntime?.agentStatuses?.windows ?? [])
+ : [],
+ (remoteRuntime?.agentStatuses?.wsl ?? []).filter(
+ (a) => a.envDistro === selectedMachine.wslDistro,
+ ),
+ )
+ : getSettingsInstalledAgents(
+ agentStatuses.filter((a) => machineIdForStatus(a) === selectedMachine.id),
+ wslAgentStatuses.filter((a) => machineIdForStatus(a) === selectedMachine.id),
+ );
+ const displayedKinds = resolveDisplayedKinds(installedAgents, activeOrder);
const byKind = new Map(installedAgents.map((a) => [a.kind, a]));
const orderedAgents = displayedKinds
.map((kind) => byKind.get(kind))
.filter((a): a is AgentStatus => a !== undefined);
- const isCustomized = providerOrder.length > 0;
- const updates = useProviderUpdates(orderedAgents);
+ const isCustomized = orderLocked
+ ? providerOrder.length > 0
+ : (machineSettings[selectedMachine.id]?.providerOrder?.length ?? 0) > 0;
+ const applyOrder = (next: string[]) => {
+ if (orderLocked) setProviderOrder(next);
+ else setMachineProviderOrder(selectedMachine.id, next);
+ };
+ // Update actions run on the local supervisor only.
+ const updates = useProviderUpdates(isRemoteMachine ? [] : orderedAgents, selectedMachine.id);
const updateAllProgress = updates.updateAllProgress;
const updateAllProgressLabel = updateAllProgress
? updateAllProgress.phase === "probing"
@@ -173,7 +215,7 @@ export function ModelOrderSection() {
const [moved] = next.splice(fromIndex, 1);
if (!moved) return;
next.splice(toIndex, 0, moved);
- setProviderOrder(next);
+ applyOrder(next);
}
if (orderedAgents.length === 0) return null;
@@ -191,7 +233,7 @@ export function ModelOrderSection() {
{isCustomized ? (