diff --git a/packages/app/e2e/browser/onboarding.spec.ts b/packages/app/e2e/browser/onboarding.spec.ts index fdb57741e9..df5ad71c05 100644 --- a/packages/app/e2e/browser/onboarding.spec.ts +++ b/packages/app/e2e/browser/onboarding.spec.ts @@ -8,6 +8,7 @@ import { expectSetupStep, finishSetup, goToSetupStep, + hostGivesAgentsPaseoTools, openAppAsNewUser, openRelayPairingFromSetup, closeRelayPairing, @@ -233,3 +234,35 @@ test("setup choices carry over to Settings and the phone step pairs over relay", await host.close(); } }); + +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"); + // 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({ + timeout: 60_000, + }); + const primary = (label: string) => page.getByRole("button", { name: label, exact: true }); + + try { + await openAppAsNewUser(page, host); + await step("Welcome to Paseo"); + await primary("Continue").click(); + await step("Your agents"); + await primary("Continue").click(); + await step("Add your projects"); + await primary("Continue").click(); + + // The setup host starts with Paseo tools off; the switch starts on. + expect(await hostGivesAgentsPaseoTools(host)).toBe(false); + await step("Let agents use Paseo"); + await expect(paseoToolsSwitch(page)).toBeChecked(); + await primary("Finish").click(); + + await expect.poll(() => hostGivesAgentsPaseoTools(host), { timeout: 30_000 }).toBe(true); + await expect(page.getByRole("heading", { name: "Let agents use Paseo" })).toHaveCount(0); + } finally { + await host.close(); + } +}); diff --git a/packages/app/e2e/support/helpers/onboarding.ts b/packages/app/e2e/support/helpers/onboarding.ts index 4183fc2a59..6f1d4c53b0 100644 --- a/packages/app/e2e/support/helpers/onboarding.ts +++ b/packages/app/e2e/support/helpers/onboarding.ts @@ -3,7 +3,9 @@ import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { expect, type Locator, type Page } from "@playwright/test"; +import type { DaemonClient as InternalDaemonClient } from "@getpaseo/client/internal/daemon-client"; import { addProjectFlow, addProjectFlowInput, chooseAddProjectMethod } from "./add-project-flow"; +import { connectDaemonClient } from "./daemon-client-loader"; import { buildCreateAgentPreferences, buildSeededHost } from "./daemon-registry"; import { startIsolatedHostDaemon, type IsolatedHostDaemonOptions } from "./isolated-host-daemon"; @@ -80,6 +82,18 @@ export function paseoToolsSwitch(scope: Page | Locator): Locator { return scope.getByRole("switch", { name: "Inject Paseo tools", exact: true }); } +/** Whether the host gives its agents Paseo tools, read from the host itself. */ +export async function hostGivesAgentsPaseoTools(host: { port: number }): Promise { + const client = await connectDaemonClient< + Pick + >({ clientIdPrefix: "app-e2e-onboarding", port: host.port }); + try { + return (await client.getDaemonConfig()).config.mcp.injectIntoAgents !== false; + } finally { + await client.close(); + } +} + export async function finishSetup(page: Page): Promise { await setupModal(page).getByRole("button", { name: "Finish", exact: true }).click(); await expect(setupModal(page)).toHaveCount(0); diff --git a/packages/app/src/agent-skills/use-agent-skills.ts b/packages/app/src/agent-skills/use-agent-skills.ts index dcfec21756..836cdb73dd 100644 --- a/packages/app/src/agent-skills/use-agent-skills.ts +++ b/packages/app/src/agent-skills/use-agent-skills.ts @@ -10,6 +10,7 @@ import { useToast } from "@/contexts/toast-context"; import { useFetchQuery } from "@/data/query"; import { useHostFeature } from "@/runtime/host-features"; import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime"; +import { confirmDialog } from "@/utils/confirm-dialog"; export type AgentSkillsController = ReturnType; @@ -94,14 +95,21 @@ export function useAgentSkills(serverId: string) { ), [reconcileAsync], ); - const runUninstall = useCallback( - () => - uninstallAsync().then( - (status) => status.state === "not-installed", - () => false, - ), - [uninstallAsync], - ); + // Uninstalling deletes whole skill folders, including anything the user added inside them, so + // it always asks first. Declining resolves false, as a failure does. + const runUninstall = useCallback(async () => { + const confirmed = await confirmDialog({ + title: t("settings.host.skills.uninstallTitle"), + message: t("settings.host.skills.uninstallMessage"), + confirmLabel: t("settings.host.skills.actions.uninstall"), + destructive: true, + }); + if (!confirmed) return false; + return uninstallAsync().then( + (status) => status.state === "not-installed", + () => false, + ); + }, [t, uninstallAsync]); const saveSelection = useCallback( (selection: AgentSkillSelection, confirmedRemovals: readonly string[] = []) => saveAsync({ selection, confirmedRemovals }), diff --git a/packages/app/src/components/community-links.tsx b/packages/app/src/components/community-links.tsx index 1ce65bb6e1..1c177da4d8 100644 --- a/packages/app/src/components/community-links.tsx +++ b/packages/app/src/components/community-links.tsx @@ -56,6 +56,9 @@ export function CommunityLinks() { const styles = StyleSheet.create(() => ({ row: { flexDirection: "row", + // Shrinks and wraps rather than running off a narrow phone screen. + flexShrink: 1, + flexWrap: "wrap", justifyContent: "center", alignItems: "center", gap: 0, diff --git a/packages/app/src/components/user-message/index.tsx b/packages/app/src/components/user-message/index.tsx index 1d2e67b277..e7bef2d177 100644 --- a/packages/app/src/components/user-message/index.tsx +++ b/packages/app/src/components/user-message/index.tsx @@ -41,6 +41,7 @@ export function MessagePreview({ children }: { children: ReactNode }) { {overflowing ? (