From 902d43da7c76989cebe90c3de5cde9dde76757a1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 5 Nov 2026 16:40:53 +0700 Subject: [PATCH] chore(release): reconcile v0.15.2 into main (#8845) * fix(app): hold the loading screen until first-run setup is decided A new user saw the empty home, the sidebar and a logo jump before setup popped in. Startup now keeps the splash while setup may still open over the empty home, for at most 3 s and only while the setup host is reachable. Remembered-workspace restore does not wait. Web dialogs now fade in as they already fade out. (cherry picked from commit a0a7620b6289808847ca55f4c2f053acee287467) (cherry picked from commit 72d7c0bf262dde715045dbfe2382271313842060) * fix(app): fade the dialog card in and land the dim at once (cherry picked from commit 6ef05705a07bd2ae770d53ae9ba5f325473de9f0) (cherry picked from commit 71c28b431c9e537639b984326bb44466611a6951) * fix(app): keep the loading screen up while startup redirects (cherry picked from commit 0b28d4cbff4853ff831f482e415035bfa510fca0) (cherry picked from commit 13cc485e6fa412ef0439d791f581f119d5800b35) * test(app): cover a setup host that drops while startup waits (cherry picked from commit 2a906ef83e6ed6ffe833398d32271d21c26d847c) (cherry picked from commit 7b6e21207ae2b625e4a97e0deb2898766df2a070) * fix(app): type the web-only dialog enter animation style The CSS animation props are not part of ViewStyle. CI typechecks without the generated expo-env.d.ts, which declares them, so the cast is required there. (cherry picked from commit b887d979d27dabc7c39d10f39b08ddb89d0c2a35) * fix(app): keep the setup window mounted when the theme changes The shell's appearance boundary remounts its dialog hosts whenever a theme color changes, so picking a theme in setup, or a system light/dark switch, replaced the open setup window and replayed its fade. Setup draws colors through CSS variables and themed props, which update in place, and changes no font size, so it now sits outside that remount. The dialog enter fade is skipped with reduced motion, and its style is typed for CI, which typechecks without the generated expo-env.d.ts. (cherry picked from commit b24c6eacf8693985d914f2b5d61b3df05feb20b6) (cherry picked from commit cb0e9907f54f7e75b3328d42216a082cc16f0f6f) * docs(changelog): prepare 0.15.2 (cherry picked from commit 5af1a8ff66642b08d1c84b3aaecdaacba1cc2f30) * chore(release): cut 0.15.2 (cherry picked from commit e896c05b09707e711a2324fcab7da84a0ae57291) --- CHANGELOG.md | 17 +++ CHANGELOG.zh-CN.md | 17 +++ .../android/en-US/changelogs/150021.txt | 8 + .../android/en-US/changelogs/150022.txt | 8 + .../android/en-US/changelogs/150023.txt | 8 + .../android/en-US/changelogs/150024.txt | 8 + package-lock.json | 56 +++---- package.json | 2 +- packages/app/e2e/browser/onboarding.spec.ts | 137 ++++++++++++++++++ .../app/e2e/support/helpers/onboarding.ts | 48 ++++++ packages/app/package.json | 2 +- packages/app/src/app/_layout.tsx | 40 ++++- packages/app/src/app/index.tsx | 12 +- .../src/components/adaptive-modal-sheet.tsx | 23 +++ .../navigation/host-runtime-bootstrap.test.ts | 23 +++ .../src/navigation/host-runtime-bootstrap.ts | 8 + packages/app/src/onboarding/index.ts | 6 +- .../onboarding/use-setup-decision-pending.ts | 24 +++ packages/cli/package.json | 8 +- packages/client/package.json | 6 +- packages/desktop/package.json | 2 +- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/plugin/package.json | 10 +- packages/protocol/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 12 +- packages/website/package.json | 2 +- plugins/package.json | 2 +- 29 files changed, 434 insertions(+), 63 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/150021.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/150022.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/150023.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/150024.txt create mode 100644 packages/app/src/onboarding/use-setup-decision-pending.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b08b0f50..10cfa1ef09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.15.2 - 2026-11-05 + +### Added + +- Added a first-run setup on Paseo Desktop and web that walks new users through theme, providers, projects, agent orchestration and the mobile app, with Settings → General → Show setup again to reopen it ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- Added project suggestions from the folders where you ran Claude Code, Codex, OpenCode or Pi, shown in the setup Projects step ([#8363](https://github.com/getpaseo/paseo/pull/8363)) + +### Improved + +- Changed the theme picker to a grid of tiles with a preview of each theme ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- Replaced the "Loading…" text in Providers settings with skeleton rows ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- Changed controls to two sizes: default controls are 32px tall on desktop and 44px on phones, and compact controls are 28px ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- Changed first launch to go from the loading screen straight into setup instead of showing the empty home first ([#8824](https://github.com/getpaseo/paseo/pull/8824)) +- Changed dialogs on desktop and web to fade in over about 160ms ([#8824](https://github.com/getpaseo/paseo/pull/8824)) +- Changed modal and sheet backgrounds to match the screen background ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- Changed documentation links in Settings sections to a `Label ↗` link beside the section title ([#8363](https://github.com/getpaseo/paseo/pull/8363)) + ## 0.15.1 - 2026-11-05 ### Fixed diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 09e516a704..5f0156eb0e 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -1,5 +1,22 @@ # Changelog +## 0.15.2 - 2026-11-05 + +### 新增 + +- 在 Paseo 桌面版和网页版新增首次运行引导,依次带新用户设置主题、提供商、项目、Agent 编排和移动端应用,并可通过“设置 → 通用 → Show setup again”重新打开 ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- 在引导的“项目”步骤中,根据你运行过 Claude Code、Codex、OpenCode 或 Pi 的文件夹推荐项目 ([#8363](https://github.com/getpaseo/paseo/pull/8363)) + +### 改进 + +- 将主题选择器改为带各主题预览的图块网格 ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- 将“提供商”设置中的“Loading…”文字替换为骨架占位行 ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- 将控件统一为两种尺寸:默认控件在桌面端高 32px、在手机上高 44px,紧凑控件高 28px ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- 将首次启动改为从加载界面直接进入引导,不再先显示空白的主页 ([#8824](https://github.com/getpaseo/paseo/pull/8824)) +- 将桌面端和网页版的对话框改为约 160 毫秒的淡入 ([#8824](https://github.com/getpaseo/paseo/pull/8824)) +- 将弹窗和面板的背景改为与屏幕背景一致 ([#8363](https://github.com/getpaseo/paseo/pull/8363)) +- 将“设置”各分区的文档链接改为分区标题旁的 `Label ↗` 链接 ([#8363](https://github.com/getpaseo/paseo/pull/8363)) + ## 0.15.1 - 2026-11-05 ### 修复 diff --git a/fastlane/metadata/android/en-US/changelogs/150021.txt b/fastlane/metadata/android/en-US/changelogs/150021.txt new file mode 100644 index 0000000000..1c9864a4ad --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/150021.txt @@ -0,0 +1,8 @@ +Added +- Added a first-run setup on Paseo Desktop and web that walks new users through theme, providers, projects, agent orchestration and the mobile app, with Settings → General → Show setup again to reopen it +- Added project suggestions from the folders where you ran Claude Code, Codex, OpenCode or Pi, shown in the setup Projects step + +Improved +- Changed the theme picker to a grid of tiles with a preview of each theme + +Full notes: https://paseo.sh/changelog diff --git a/fastlane/metadata/android/en-US/changelogs/150022.txt b/fastlane/metadata/android/en-US/changelogs/150022.txt new file mode 100644 index 0000000000..1c9864a4ad --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/150022.txt @@ -0,0 +1,8 @@ +Added +- Added a first-run setup on Paseo Desktop and web that walks new users through theme, providers, projects, agent orchestration and the mobile app, with Settings → General → Show setup again to reopen it +- Added project suggestions from the folders where you ran Claude Code, Codex, OpenCode or Pi, shown in the setup Projects step + +Improved +- Changed the theme picker to a grid of tiles with a preview of each theme + +Full notes: https://paseo.sh/changelog diff --git a/fastlane/metadata/android/en-US/changelogs/150023.txt b/fastlane/metadata/android/en-US/changelogs/150023.txt new file mode 100644 index 0000000000..1c9864a4ad --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/150023.txt @@ -0,0 +1,8 @@ +Added +- Added a first-run setup on Paseo Desktop and web that walks new users through theme, providers, projects, agent orchestration and the mobile app, with Settings → General → Show setup again to reopen it +- Added project suggestions from the folders where you ran Claude Code, Codex, OpenCode or Pi, shown in the setup Projects step + +Improved +- Changed the theme picker to a grid of tiles with a preview of each theme + +Full notes: https://paseo.sh/changelog diff --git a/fastlane/metadata/android/en-US/changelogs/150024.txt b/fastlane/metadata/android/en-US/changelogs/150024.txt new file mode 100644 index 0000000000..1c9864a4ad --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/150024.txt @@ -0,0 +1,8 @@ +Added +- Added a first-run setup on Paseo Desktop and web that walks new users through theme, providers, projects, agent orchestration and the mobile app, with Settings → General → Show setup again to reopen it +- Added project suggestions from the folders where you ran Claude Code, Codex, OpenCode or Pi, shown in the setup Projects step + +Improved +- Changed the theme picker to a grid of tiles with a preview of each theme + +Full notes: https://paseo.sh/changelog diff --git a/package-lock.json b/package-lock.json index db6aab7828..e4e82efad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.15.1", + "version": "0.15.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.15.1", + "version": "0.15.2", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ @@ -37468,7 +37468,7 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.15.1", + "version": "0.15.2", "dependencies": { "@codemirror/commands": "6.10.4", "@codemirror/language": "6.12.4", @@ -39637,13 +39637,13 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.15.1", - "@getpaseo/protocol": "0.15.1", - "@getpaseo/server": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/protocol": "0.15.2", + "@getpaseo/server": "0.15.2", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -39890,11 +39890,11 @@ }, "packages/client": { "name": "@getpaseo/client", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { - "@getpaseo/protocol": "0.15.1", - "@getpaseo/relay": "0.15.1", + "@getpaseo/protocol": "0.15.2", + "@getpaseo/relay": "0.15.2", "ws": "^8.20.0", "zod": "^4.4.3" }, @@ -39906,7 +39906,7 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "@getpaseo/cli": "*", @@ -40149,7 +40149,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.15.1", + "version": "0.15.2", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.14", @@ -40195,7 +40195,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "@codemirror/language": "6.12.4", @@ -40225,7 +40225,7 @@ }, "packages/plugin": { "name": "@getpaseo/plugin", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.4.0", @@ -40233,8 +40233,8 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.15.1", - "@getpaseo/protocol": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/protocol": "0.15.2", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.2.3", @@ -40244,8 +40244,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.15.1", - "@getpaseo/protocol": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/protocol": "0.15.2", "react": ">=19.1.0 <20", "react-native": ">=0.81.5", "zod": "^4.4.3" @@ -40267,7 +40267,7 @@ }, "packages/protocol": { "name": "@getpaseo/protocol", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "semver": "^7.8.5", @@ -40295,7 +40295,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "^2.4.0", @@ -40517,17 +40517,17 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.15.1", - "@getpaseo/highlight": "0.15.1", - "@getpaseo/plugin": "0.15.1", - "@getpaseo/protocol": "0.15.1", - "@getpaseo/relay": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/highlight": "0.15.2", + "@getpaseo/plugin": "0.15.2", + "@getpaseo/protocol": "0.15.2", + "@getpaseo/relay": "0.15.2", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", @@ -41474,7 +41474,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.15.1", + "version": "0.15.2", "dependencies": { "@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/workers-types": "^4.20260317.1", @@ -43084,7 +43084,7 @@ }, "plugins": { "name": "@getpaseo/builtin-plugins", - "version": "0.15.1", + "version": "0.15.2", "dependencies": { "@getpaseo/plugin": "*", "zod": "^4.4.3" diff --git a/package.json b/package.json index 86b21bee50..b5fd31ae8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.15.1", + "version": "0.15.2", "private": true, "description": "Paseo: voice-controlled development environment for local AI coding agents", "keywords": [ diff --git a/packages/app/e2e/browser/onboarding.spec.ts b/packages/app/e2e/browser/onboarding.spec.ts index 68d54687a9..cf9ab06442 100644 --- a/packages/app/e2e/browser/onboarding.spec.ts +++ b/packages/app/e2e/browser/onboarding.spec.ts @@ -12,11 +12,15 @@ import { openAppAsNewUser, openRelayPairingFromSetup, closeRelayPairing, + emptyHome, + loadingScreen, openTailscaleDocsFromSetup, paseoToolsSwitch, pickTheme, projectSuggestion, projectSuggestionRow, + readFirstLaunch, + recordFirstLaunch, setupModal, setupPrimaryButton, showSetupAgainFromSettings, @@ -99,6 +103,139 @@ test("a new user is walked through setup once", async ({ page }, testInfo) => { } }); +test("a new user goes from the loading screen straight into setup", async ({ page }) => { + const host = await startSetupHost("first-launch"); + + try { + await recordFirstLaunch(page); + await openAppAsNewUser(page, host); + await expectSetupStep(page, "Welcome to Paseo"); + + const launch = await readFirstLaunch(page); + expect(launch.appBeforeSetup).toBe(false); + expect(launch.setupOpacityOnArrival).toBeLessThan(1); + await expect(setupModal(page)).toHaveCSS("opacity", "1"); + } finally { + await host.close(); + } +}); + +test("with reduced motion, setup appears without fading in", async ({ page }) => { + const host = await startSetupHost("reduced-motion"); + + try { + await page.emulateMedia({ reducedMotion: "reduce" }); + await recordFirstLaunch(page); + await openAppAsNewUser(page, host); + await expectSetupStep(page, "Welcome to Paseo"); + + expect((await readFirstLaunch(page)).setupOpacityOnArrival).toBe(1); + } finally { + await host.close(); + } +}); + +test("changing the theme keeps the setup window in place on every step", async ({ page }) => { + const host = await startSetupHost("theme-change"); + + try { + await openAppAsNewUser(page, host); + await expectSetupStep(page, "Welcome to Paseo"); + const setupWindow = await setupModal(page).elementHandle(); + + for (const title of [ + "Welcome to Paseo", + "Your agents", + "Add your projects", + "Let agents use Paseo", + "Take your agents with you", + ]) { + if (title !== "Welcome to Paseo") await continueSetup(page); + await expectSetupStep(page, title); + const heading = setupModal(page).getByRole("heading", { name: title }); + const headingNode = await heading.elementHandle(); + const lightColor = await heading.evaluate((node) => getComputedStyle(node).color); + await page.emulateMedia({ colorScheme: "dark" }); + await expect(heading).not.toHaveCSS("color", lightColor); + await page.emulateMedia({ colorScheme: "light" }); + await expect(heading).toHaveCSS("color", lightColor); + expect(await setupWindow?.evaluate((node) => node.isConnected), title).toBe(true); + expect(await headingNode?.evaluate((node) => node.isConnected), title).toBe(true); + } + + await goToSetupStep(page, "Appearance"); + await pickTheme(page, "Midnight"); + await pickTheme(page, "Light"); + expect(await setupWindow?.evaluate((node) => node.isConnected)).toBe(true); + } finally { + await host.close(); + } +}); + +test("the loading screen stays up while the setup host loads its workspaces", async ({ page }) => { + const host = await startSetupHost("slow-workspaces"); + const gate = await installDaemonWebSocketGate(page, { port: host.port }); + + try { + await recordFirstLaunch(page); + gate.holdNextClientRequest("fetch_workspaces_request"); + await openAppAsNewUser(page, host); + await gate.waitForHeldClientRequest(); + await expect(loadingScreen(page)).toBeVisible(); + await expect(emptyHome(page)).toHaveCount(0); + + gate.releaseHeldClientRequest(); + await expectSetupStep(page, "Welcome to Paseo"); + expect((await readFirstLaunch(page)).appBeforeSetup).toBe(false); + } finally { + await host.close(); + } +}); + +test("a setup host that never loads its workspaces still lands on the home", async ({ page }) => { + const host = await startSetupHost("stuck-workspaces"); + const gate = await installDaemonWebSocketGate(page, { port: host.port }); + + try { + gate.holdNextClientRequest("fetch_workspaces_request"); + await openAppAsNewUser(page, host); + await gate.waitForHeldClientRequest(); + await expect(emptyHome(page)).toBeVisible({ timeout: 15_000 }); + await expect(setupModal(page)).toHaveCount(0); + + // Setup still opens once the host answers. + gate.releaseHeldClientRequest(); + await expectSetupStep(page, "Welcome to Paseo"); + } finally { + await host.close(); + } +}); + +test("a setup host that drops while loading still lands on the home", async ({ page }) => { + const host = await startSetupHost("dropped-workspaces"); + const gate = await installDaemonWebSocketGate(page, { port: host.port }); + + try { + gate.holdNextClientRequest("fetch_workspaces_request"); + await openAppAsNewUser(page, host); + await gate.waitForHeldClientRequest(); + await gate.drop(); + await expect(emptyHome(page)).toBeVisible({ timeout: 15_000 }); + await expect(setupModal(page)).toHaveCount(0); + } finally { + await host.close(); + } +}); + +test("an unreachable setup host lands on the home without setup", async ({ page }) => { + const host = await startSetupHost("unreachable"); + await host.close(); + + await openAppAsNewUser(page, host); + await expect(emptyHome(page)).toBeVisible({ timeout: 15_000 }); + await expect(setupModal(page)).toHaveCount(0); +}); + test("setup suggests the repos the user already ran agents in", async ({ page }, testInfo) => { const repo = await createTempGitRepo("onboarding-history-"); const repoName = path.basename(repo.path); diff --git a/packages/app/e2e/support/helpers/onboarding.ts b/packages/app/e2e/support/helpers/onboarding.ts index 6f1d4c53b0..4226c1c27e 100644 --- a/packages/app/e2e/support/helpers/onboarding.ts +++ b/packages/app/e2e/support/helpers/onboarding.ts @@ -34,6 +34,54 @@ export async function openAppAsNewUser( await page.goto("/"); } +interface FirstLaunchRecord { + appBeforeSetup: boolean; + setupOpacityOnArrival: number | null; +} + +/** + * Watches the page from its first paint: whether the app's sidebar or home showed before setup, + * and how opaque setup was when it arrived. Call before `openAppAsNewUser`. + */ +export async function recordFirstLaunch(page: Page): Promise { + await page.addInitScript(() => { + const record: FirstLaunchRecord = { appBeforeSetup: false, setupOpacityOnArrival: null }; + Object.assign(window, { __firstLaunch: record }); + const observer = new MutationObserver(() => { + const setup = document.querySelector('[data-testid="onboarding-modal"] [role="dialog"]'); + if (setup) { + record.setupOpacityOnArrival = Number(getComputedStyle(setup).opacity); + observer.disconnect(); + return; + } + if ( + document.querySelector( + '[data-testid="open-project-submit"], [data-testid="sidebar-settings"]', + ) + ) { + record.appBeforeSetup = true; + } + }); + observer.observe(document, { childList: true, subtree: true }); + }); +} + +export async function readFirstLaunch(page: Page): Promise { + return page.evaluate( + () => (window as unknown as { __firstLaunch: FirstLaunchRecord }).__firstLaunch, + ); +} + +/** The loading screen the app shows while it starts. */ +export function loadingScreen(page: Page): Locator { + return page.getByTestId("startup-splash"); +} + +/** The empty home a host with no projects lands on. */ +export function emptyHome(page: Page): Locator { + return page.getByTestId("open-project-submit"); +} + export function setupModal(page: Page): Locator { return page.getByTestId("onboarding-modal").getByRole("dialog"); } diff --git a/packages/app/package.json b/packages/app/package.json index 3fa80be49a..2c897be9d4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/app", - "version": "0.15.1", + "version": "0.15.2", "private": true, "main": "index.ts", "scripts": { diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index d5b6794c03..8548b4bb76 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -46,7 +46,7 @@ import { DownloadToast } from "@/components/download-toast"; import { QuittingOverlay } from "@/components/quitting-overlay"; import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog"; import { ChangelogHost } from "@/changelog"; -import { OnboardingHost } from "@/onboarding"; +import { OnboardingHost, useIsSetupDecisionPending } from "@/onboarding"; import { AppDiagnosticHost } from "@/components/app-diagnostic-host"; import { AppearanceStyleBoundary } from "@/components/appearance-style-boundary"; import { LeftSidebar } from "@/components/left-sidebar"; @@ -151,6 +151,7 @@ export interface HostRuntimeBootstrapState { splashError: string | null; retry: () => void; hasGivenUpWaitingForHost: boolean; + isWaitingForSetup: boolean; storeReady: boolean; startupBlocker: StartupBlocker; } @@ -159,6 +160,7 @@ const HostRuntimeBootstrapContext = createContext({ splashError: null, retry: () => {}, hasGivenUpWaitingForHost: false, + isWaitingForSetup: false, storeReady: false, startupBlocker: { kind: "none" }, }); @@ -255,6 +257,9 @@ function useDaemonStartIsRunning(): boolean { } const STARTUP_GIVE_UP_TIMEOUT_MS = 5_000; +// How long the splash waits on top of host startup to learn whether setup opens. A slow host +// lands on the home instead, and setup opens over it once it is decided. +const STARTUP_SETUP_WAIT_TIMEOUT_MS = 3_000; async function shouldStartBuiltInDaemon(): Promise { if (!shouldUseDesktopDaemon()) { @@ -316,6 +321,23 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) { }; }, [shouldRunGiveUpTimer]); + const isSetupDecisionPending = useIsSetupDecisionPending(); + const [hasGivenUpWaitingForSetup, setHasGivenUpWaitingForSetup] = useState(false); + const isWaitingForSetup = isSetupDecisionPending && !hasGivenUpWaitingForSetup; + const shouldRunSetupWaitTimer = isWaitingForSetup && startupBlocker.kind === "none"; + + useEffect(() => { + if (!shouldRunSetupWaitTimer) { + return; + } + const handle = setTimeout(() => { + setHasGivenUpWaitingForSetup(true); + }, STARTUP_SETUP_WAIT_TIMEOUT_MS); + return () => { + clearTimeout(handle); + }; + }, [shouldRunSetupWaitTimer]); + const retry = useCallback(() => { const daemonStartService = getDaemonStartService({ store: getHostRuntimeStore() }); void daemonStartService.startIfEnabled({ shouldStart: shouldStartBuiltInDaemon }); @@ -326,8 +348,15 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) { const storeReady = resolveStartupNavigationReady({ startupBlocker }); const state = useMemo( - () => ({ splashError, retry, hasGivenUpWaitingForHost, storeReady, startupBlocker }), - [splashError, retry, hasGivenUpWaitingForHost, storeReady, startupBlocker], + () => ({ + splashError, + retry, + hasGivenUpWaitingForHost, + isWaitingForSetup, + storeReady, + startupBlocker, + }), + [splashError, retry, hasGivenUpWaitingForHost, isWaitingForSetup, storeReady, startupBlocker], ); return ( @@ -529,7 +558,6 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon - @@ -540,6 +568,10 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon + {/* Setup changes the theme while it is open, so it stays out of the remount above. Its + colors are CSS variables and themed props, which update in place, and setup changes no + font size. */} + ); diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index e0649f65a1..f6850a2a57 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -45,11 +45,19 @@ export default function Index() { }), isWorkspaceSelectionLoaded, hasGivenUpWaitingForHost: bootstrapState.hasGivenUpWaitingForHost, + isWaitingForSetup: bootstrapState.isWaitingForSetup, }); + const splash = ; if (startupRoute.kind === "redirect") { - return ; + // The loading screen stays up until the target route paints; a redirect alone renders nothing. + return ( + <> + {splash} + + + ); } - return ; + return splash; } diff --git a/packages/app/src/components/adaptive-modal-sheet.tsx b/packages/app/src/components/adaptive-modal-sheet.tsx index bff7fe34f6..7e28f3d81a 100644 --- a/packages/app/src/components/adaptive-modal-sheet.tsx +++ b/packages/app/src/components/adaptive-modal-sheet.tsx @@ -232,6 +232,28 @@ const styles = StyleSheet.create((theme) => ({ })); const WEB_EXIT_DURATION_MS = 160; +const WEB_ENTER_DURATION_MS = 160; +const WEB_ENTER_ANIMATION_NAME = "paseo-modal-enter"; + +// The desktop card fades in; the dim lands at once, so the screen behind is never seen undimmed +// while the dialog arrives. A CSS animation runs from the card's first paint, so a dialog that opens +// on its own, like first-run setup, never pops in. Closing fades the whole overlay. With reduced +// motion the keyframes are never defined, so the card appears at once. +function ensureWebEnterKeyframes() { + if (!isWeb || typeof document === "undefined") return; + if (document.getElementById(WEB_ENTER_ANIMATION_NAME)) return; + const style = document.createElement("style"); + style.id = WEB_ENTER_ANIMATION_NAME; + style.textContent = `@media (prefers-reduced-motion: no-preference) { @keyframes ${WEB_ENTER_ANIMATION_NAME} { from { opacity: 0; } to { opacity: 1; } } }`; + document.head.appendChild(style); +} +ensureWebEnterKeyframes(); +// The CSS animation props are not part of ViewStyle without the generated expo-env.d.ts. +const WEB_ENTER_STYLE = { + animationName: WEB_ENTER_ANIMATION_NAME, + animationDuration: `${WEB_ENTER_DURATION_MS}ms`, + animationTimingFunction: "ease-out", +} as unknown as ViewStyle; /** The compact sheet's surface: Gorhom's `backgroundComponent` for every bottom sheet. */ export function SheetBackground({ style }: BottomSheetBackgroundProps) { @@ -662,6 +684,7 @@ export function AdaptiveModalSheet({ styles.desktopCard, desktopHeight != null && { height: desktopHeight }, desktopMaxWidth != null && { maxWidth: desktopMaxWidth }, + isWeb && WEB_ENTER_STYLE, ], [desktopMaxWidth, desktopHeight], ); diff --git a/packages/app/src/navigation/host-runtime-bootstrap.test.ts b/packages/app/src/navigation/host-runtime-bootstrap.test.ts index 6e1e130090..fe7687886c 100644 --- a/packages/app/src/navigation/host-runtime-bootstrap.test.ts +++ b/packages/app/src/navigation/host-runtime-bootstrap.test.ts @@ -177,6 +177,7 @@ describe("resolveStartupRoute", () => { workspaceSelectionStatus: "unknown" as const, isWorkspaceSelectionLoaded: true, hasGivenUpWaitingForHost: false, + isWaitingForSetup: false, }; const baseHostInput = { route: { kind: "host" as const, serverId: "server-saved" }, @@ -278,6 +279,28 @@ describe("resolveStartupRoute", () => { ).toEqual({ kind: "redirect", href: "/h/srv-desktop" }); }); + it("holds the splash over the empty home while setup may still open", () => { + expect( + resolveStartupRoute({ + ...baseIndexInput, + hosts: [{ serverId: "srv-desktop" }], + anyOnlineHostServerId: "srv-desktop", + isWaitingForSetup: true, + }), + ).toEqual({ kind: "splash" }); + }); + + it("restores a saved workspace without waiting for setup", () => { + expect( + resolveStartupRoute({ + ...baseIndexInput, + hosts: [{ serverId: "server-1" }], + workspaceSelection: { serverId: "server-1", workspaceId: "workspace-a" }, + isWaitingForSetup: true, + }), + ).toEqual({ kind: "redirect", href: "/h/server-1" }); + }); + it("keeps a known connecting host in app-owned routing instead of showing welcome", () => { expect( resolveStartupRoute({ diff --git a/packages/app/src/navigation/host-runtime-bootstrap.ts b/packages/app/src/navigation/host-runtime-bootstrap.ts index 8ee1f7bfb2..c78b40c1cc 100644 --- a/packages/app/src/navigation/host-runtime-bootstrap.ts +++ b/packages/app/src/navigation/host-runtime-bootstrap.ts @@ -117,6 +117,8 @@ export interface ResolveIndexStartupRouteInput extends ResolveStartupRouteBaseIn workspaceSelectionStatus: WorkspaceSelectionStatus; isWorkspaceSelectionLoaded: boolean; hasGivenUpWaitingForHost: boolean; + /** Setup may still open over the empty home; see `useIsSetupDecisionPending`. */ + isWaitingForSetup: boolean; } export interface ResolveHostStartupRouteInput extends ResolveStartupRouteBaseInput { @@ -198,6 +200,12 @@ function resolveReadyIndexStartupRoute(input: ResolveIndexStartupRouteInput): St }; } + // Without a workspace to restore, startup lands on the empty home. A new user's setup opens + // over it, so hold the splash until that is known instead of flashing the home first. + if (input.isWaitingForSetup) { + return { kind: "splash" }; + } + if (input.anyOnlineHostServerId) { return { kind: "redirect", href: buildHostRootRoute(input.anyOnlineHostServerId) }; } diff --git a/packages/app/src/onboarding/index.ts b/packages/app/src/onboarding/index.ts index 68c80d8f0d..78d3da6016 100644 --- a/packages/app/src/onboarding/index.ts +++ b/packages/app/src/onboarding/index.ts @@ -2,8 +2,10 @@ * First-run setup: a one-time modal that walks a new user through the surfaces Settings * already has — appearance, providers, projects, Paseo tools and phone pairing. * - * The app shell mounts the host; Settings reopens it. Whether it opens, which steps it has, - * and how completion is remembered stay inside; import from `@/onboarding` only. + * The app shell mounts the host, startup waits for its decision, and Settings reopens it. Whether + * it opens, which steps it has, and how completion is remembered stay inside; import from + * `@/onboarding` only. */ export { OnboardingHost } from "./onboarding-host"; export { useOpenOnboarding } from "./use-open-onboarding"; +export { useIsSetupDecisionPending } from "./use-setup-decision-pending"; diff --git a/packages/app/src/onboarding/use-setup-decision-pending.ts b/packages/app/src/onboarding/use-setup-decision-pending.ts new file mode 100644 index 0000000000..5918833ce6 --- /dev/null +++ b/packages/app/src/onboarding/use-setup-decision-pending.ts @@ -0,0 +1,24 @@ +import { useSyncExternalStore } from "react"; +import { isNative } from "@/constants/platform"; +import { useHostRuntimeConnectionStatus } from "@/runtime/host-runtime"; +import { getOnboardingModel } from "./internal/onboarding-instance"; +import { useSetupHostServerId } from "./internal/use-setup-host"; + +/** + * True while this launch may still open setup: it has not been decided, and the setup host is + * connecting, or connected and loading its workspaces. Startup keeps its loading screen up + * meanwhile, so a new user lands on setup rather than on the empty home first. False on phones, + * where setup never opens, when there is no setup host, and while the host is unreachable. + */ +export const useIsSetupDecisionPending: () => boolean = isNative + ? () => false + : useIsSetupDecisionPendingOnDesktop; + +function useIsSetupDecisionPendingOnDesktop(): boolean { + const model = getOnboardingModel(); + const state = useSyncExternalStore(model.subscribe, model.getState, model.getState); + const serverId = useSetupHostServerId(); + const connectionStatus = useHostRuntimeConnectionStatus(serverId ?? ""); + if (state.status !== "deciding" || !serverId) return false; + return connectionStatus !== "offline" && connectionStatus !== "error"; +} diff --git a/packages/cli/package.json b/packages/cli/package.json index bd3b13a4f1..9248988869 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.15.1", + "version": "0.15.2", "description": "Paseo CLI - control your AI coding agents from the command line", "license": "Apache-2.0", "bin": { @@ -29,9 +29,9 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/client": "0.15.1", - "@getpaseo/protocol": "0.15.1", - "@getpaseo/server": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/protocol": "0.15.2", + "@getpaseo/server": "0.15.2", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/client/package.json b/packages/client/package.json index 3f6edeaf18..62e5b8c664 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/client", - "version": "0.15.1", + "version": "0.15.2", "description": "Paseo client SDK package", "license": "Apache-2.0", "files": [ @@ -48,8 +48,8 @@ "test": "vitest run" }, "dependencies": { - "@getpaseo/protocol": "0.15.1", - "@getpaseo/relay": "0.15.1", + "@getpaseo/protocol": "0.15.2", + "@getpaseo/relay": "0.15.2", "ws": "^8.20.0", "zod": "^4.4.3" }, diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 788716cef7..7e3b01dfaa 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.15.1", + "version": "0.15.2", "private": true, "description": "Paseo desktop app (Electron wrapper)", "homepage": "https://paseo.sh", diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index df44ab2a92..b9c1190375 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.15.1", + "version": "0.15.2", "description": "Native module for two way audio streaming", "keywords": [ "ExpoTwoWayAudio", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 5f40283e3c..85d9c90471 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.15.1", + "version": "0.15.2", "license": "Apache-2.0", "files": [ "dist", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 262d57381e..ef537c35e3 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/plugin", - "version": "0.15.1", + "version": "0.15.2", "description": "Paseo plugin SDK package", "license": "Apache-2.0", "files": [ @@ -78,8 +78,8 @@ "use-sync-external-store": "^1.6.0" }, "devDependencies": { - "@getpaseo/client": "0.15.1", - "@getpaseo/protocol": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/protocol": "0.15.2", "@types/node": "^20.9.0", "@types/react": "~19.2.0", "react": "19.2.3", @@ -89,8 +89,8 @@ "zod": "^4.4.3" }, "peerDependencies": { - "@getpaseo/client": "0.15.1", - "@getpaseo/protocol": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/protocol": "0.15.2", "react": ">=19.1.0 <20", "react-native": ">=0.81.5", "zod": "^4.4.3" diff --git a/packages/protocol/package.json b/packages/protocol/package.json index d802f6afe3..4667c9abe1 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/protocol", - "version": "0.15.1", + "version": "0.15.2", "description": "Paseo shared protocol schemas and wire types", "license": "Apache-2.0", "files": [ diff --git a/packages/relay/package.json b/packages/relay/package.json index d4f7af0c0c..6f17d20847 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.15.1", + "version": "0.15.2", "description": "Paseo relay for bridging daemon and client connections", "license": "Apache-2.0", "files": [ diff --git a/packages/server/package.json b/packages/server/package.json index 2c9615da7a..471e558b77 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.15.1", + "version": "0.15.2", "description": "Paseo backend server", "license": "Apache-2.0", "files": [ @@ -108,11 +108,11 @@ "@agentclientprotocol/sdk": "^0.17.1", "@anthropic-ai/claude-agent-sdk": "0.3.246", "@anthropic-ai/sdk": "^0.104.2", - "@getpaseo/client": "0.15.1", - "@getpaseo/highlight": "0.15.1", - "@getpaseo/plugin": "0.15.1", - "@getpaseo/protocol": "0.15.1", - "@getpaseo/relay": "0.15.1", + "@getpaseo/client": "0.15.2", + "@getpaseo/highlight": "0.15.2", + "@getpaseo/plugin": "0.15.2", + "@getpaseo/protocol": "0.15.2", + "@getpaseo/relay": "0.15.2", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "1.29.0", "@opencode-ai/sdk": "1.14.46", diff --git a/packages/website/package.json b/packages/website/package.json index 3d9aa70ff8..696ebb8a51 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.15.1", + "version": "0.15.2", "private": true, "type": "module", "scripts": { diff --git a/plugins/package.json b/plugins/package.json index 829209bd2a..1e56036650 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/builtin-plugins", - "version": "0.15.1", + "version": "0.15.2", "private": true, "type": "module", "scripts": {