From b32da3056de232a44bcfba1d679509b58e00ade1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 18:03:50 +0100 Subject: [PATCH] fix(app): hold the skills switch while setup applies its choices The Paseo tools and CLI switches were disabled while Continue or Finish applied them, but the skills switch only locked while a skills operation ran. With skills needing no change, it stayed editable during a pending tools update and the new value was silently dropped. --- packages/app/e2e/browser/onboarding.spec.ts | 13 +++++++++++++ .../e2e/support/helpers/daemon-websocket-gate.ts | 11 ++++++++--- packages/app/src/agent-skills/index.tsx | 10 +++++++--- .../internal/steps/orchestration-step.tsx | 7 ++++++- 4 files changed, 34 insertions(+), 7 deletions(-) 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 ? ( -