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.
This commit is contained in:
Mohamed Boudra
2026-11-02 18:03:50 +01:00
parent 23d928ad10
commit b32da3056d
4 changed files with 34 additions and 7 deletions
@@ -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 {
@@ -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();
+7 -3
View File
@@ -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({
>
<View style={styles.controls}>
{needsUpdate ? (
<Button variant="outline" onPress={handleUpdate} disabled={isWorking}>
<Button variant="outline" onPress={handleUpdate} disabled={disabled || isWorking}>
{isWorking
? t("settings.host.skills.actions.working")
: t("settings.host.skills.actions.update")}
@@ -160,14 +162,16 @@ export function AgentSkillsRow({
variant="ghost"
leftIcon={chooseIcon}
onPress={handleOpen}
disabled={status === null || isWorking}
disabled={disabled || status === null || isWorking}
accessibilityLabel={t("settings.host.skills.choose")}
/>
<Switch
value={value}
onValueChange={onValueChange}
// Nothing picked means nothing to install; the gear is the way forward.
disabled={status === null || isWorking || (!value && !hasSelectedSkills(status))}
disabled={
disabled || status === null || isWorking || (!value && !hasSelectedSkills(status))
}
accessibilityLabel={t("settings.host.skills.title")}
/>
</View>
@@ -15,7 +15,12 @@ export function OrchestrationStep({ setup }: { setup: OrchestrationSetupControll
<SettingsCard>
<PaseoToolsRow value={choices.tools} onValueChange={setTools} disabled={isApplying} />
{offered.skills ? (
<AgentSkillsRow skills={setup.skills} value={choices.skills} onValueChange={setSkills} />
<AgentSkillsRow
skills={setup.skills}
value={choices.skills}
onValueChange={setSkills}
disabled={isApplying}
/>
) : null}
{offered.cli ? (
<CommandLineRow value={choices.cli} onValueChange={setCli} disabled={isApplying} />