diff --git a/packages/app/e2e/browser/onboarding.spec.ts b/packages/app/e2e/browser/onboarding.spec.ts
index df5ad71c05..ebe088b7f7 100644
--- a/packages/app/e2e/browser/onboarding.spec.ts
+++ b/packages/app/e2e/browser/onboarding.spec.ts
@@ -23,6 +23,7 @@ import {
skipSetup,
startSetupHost,
} from "../support/helpers/onboarding";
+import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
import { enableRelayAndExpectOffer } from "../support/helpers/pair-device";
import { seedWorkspace } from "../support/helpers/seed-client";
import { openSettingsHostSection } from "../support/helpers/settings";
@@ -238,6 +239,7 @@ test("setup choices carry over to Settings and the phone step pairs over relay",
test("in the compact layout, Finish on Orchestration applies its switches", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
const host = await startSetupHost("compact");
+ const gate = await installDaemonWebSocketGate(page, { port: host.port });
// The compact sheet is the only surface on screen, and Orchestration is its last step.
const step = (title: string) =>
expect(page.getByRole("heading", { name: title, exact: true })).toBeVisible({
@@ -258,8 +260,19 @@ test("in the compact layout, Finish on Orchestration applies its switches", asyn
expect(await hostGivesAgentsPaseoTools(host)).toBe(false);
await step("Let agents use Paseo");
await expect(paseoToolsSwitch(page)).toBeChecked();
+ // Skills are not installed, so leaving them off has nothing to apply.
+ const skillsSwitch = page.getByRole("switch", { name: "Orchestration skills", exact: true });
+ await skillsSwitch.click();
+ await expect(skillsSwitch).not.toBeChecked();
+ gate.holdNextClientRequest("set_daemon_config_request");
await primary("Finish").click();
+ // While the choices apply, none of them can change.
+ await gate.waitForHeldClientRequest();
+ await expect(paseoToolsSwitch(page)).toBeDisabled();
+ await expect(skillsSwitch).toBeDisabled();
+ gate.releaseHeldClientRequest();
+
await expect.poll(() => hostGivesAgentsPaseoTools(host), { timeout: 30_000 }).toBe(true);
await expect(page.getByRole("heading", { name: "Let agents use Paseo" })).toHaveCount(0);
} finally {
diff --git a/packages/app/e2e/support/helpers/daemon-websocket-gate.ts b/packages/app/e2e/support/helpers/daemon-websocket-gate.ts
index bff52fc3f7..c33976879f 100644
--- a/packages/app/e2e/support/helpers/daemon-websocket-gate.ts
+++ b/packages/app/e2e/support/helpers/daemon-websocket-gate.ts
@@ -1,5 +1,5 @@
import type { Page, WebSocketRoute } from "@playwright/test";
-import { daemonWsRoutePattern } from "./daemon-port";
+import { daemonWsRoutePattern, wsRoutePatternForPort } from "./daemon-port";
export interface DirectoryBootstrapCounts {
agents: number;
@@ -294,7 +294,8 @@ function recordClientRequest(
directoryStarts.total[directory] += 1;
}
-export async function installDaemonWebSocketGate(page: Page) {
+/** Gates the shared E2E daemon, or the daemon on `port` for a spec that runs its own. */
+export async function installDaemonWebSocketGate(page: Page, options: { port?: number } = {}) {
let acceptingConnections = true;
let reconnectWithFreshClient = false;
let suppressAgentStream = false;
@@ -445,7 +446,11 @@ export async function installDaemonWebSocketGate(page: Page) {
return true;
};
- await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
+ const routePattern =
+ options.port === undefined
+ ? daemonWsRoutePattern()
+ : wsRoutePatternForPort(String(options.port));
+ await page.routeWebSocket(routePattern, (ws) => {
if (!acceptingConnections) {
blockedConnectionCount += 1;
for (const resolve of blockedConnectionWaiters) resolve();
diff --git a/packages/app/src/agent-skills/index.tsx b/packages/app/src/agent-skills/index.tsx
index 6cc27f55cf..dfc98649b6 100644
--- a/packages/app/src/agent-skills/index.tsx
+++ b/packages/app/src/agent-skills/index.tsx
@@ -107,10 +107,12 @@ export function AgentSkillsRow({
skills,
value,
onValueChange,
+ disabled = false,
}: {
skills: AgentSkillsController;
value: boolean;
onValueChange: (next: boolean) => void;
+ disabled?: boolean;
}) {
const { t } = useTranslation();
const { status, isWorking, reconcile, saveSelection } = skills;
@@ -150,7 +152,7 @@ export function AgentSkillsRow({
>
{needsUpdate ? (
-
diff --git a/packages/app/src/onboarding/internal/steps/orchestration-step.tsx b/packages/app/src/onboarding/internal/steps/orchestration-step.tsx
index da2249b0cd..da41f591b9 100644
--- a/packages/app/src/onboarding/internal/steps/orchestration-step.tsx
+++ b/packages/app/src/onboarding/internal/steps/orchestration-step.tsx
@@ -15,7 +15,12 @@ export function OrchestrationStep({ setup }: { setup: OrchestrationSetupControll
{offered.skills ? (
-
+
) : null}
{offered.cli ? (