Hand off to a daemon after app setup - #63
Open
boudra wants to merge 2 commits into
Open
Conversation
App setup used to end on the dashboard, leaving the operator with no idea that Hub needs a daemon to run anything. A new step now names the exact command to run in a terminal and waits for the daemon it connects. App onboarding is marked complete on the server before this step renders, so the browser tab the CLI opens for authorization reaches CLI login instead of being sent back into setup. The step itself is a client-side phase, not a gate: it lives in the tab that finished app setup, adds no durable state, and a reload lands on the dashboard exactly as before. The command is derived from the address the operator is already looking at. Only a Hub served from the origin `paseo hub login` already defaults to may omit the argument, so no deployment flag is introduced. Success and "Do this later" both continue to the dashboard and the default project that instance setup already provisioned.
Both ways out of the handoff hid the phase and revealed the project list, leaving the operator to pick the only entry on it. The accepted journey ends inside the project instance setup already provisioned, so "Continue" and "Do this later" now navigate there using the organization slug the handoff was already given. Nothing is queried and no project is created. The route is committed before the phase is dropped. Dropping it first renders the dashboard at whatever URL onboarding is standing on, which is what flashed the project list on the way past. The E2E now asserts the canonical Default-project URL and its Overview immediately after the exit, instead of clicking a project link and performing the navigation the product owes the operator. Shared app-setup helpers follow the true destination; the handoff journey runs embedded, so it needs no PostgreSQL container of its own.
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
App setup used to end on the dashboard, leaving a new operator with a Hub that cannot run anything and no idea that a daemon is what runs it. This is the browser handoff phase of zero-friction Hub onboarding: after app setup, the operator gets one exact command to paste into a terminal, Hub watches for the daemon that command connects, and both ways out land them directly in the Default project their instance was provisioned with.
The intended journey is
npx @getpaseo/hub→ operator setup → app setup → daemon handoff →paseo hub login→ connect → initialize and deploy.Goals
paseo hub login <current-origin>self-hosted,paseo hub loginon the official hosted Hub.Non-goals
Why
App onboarding completes before the handoff renders
The onboarding guard is server-side: an instance operator whose app onboarding is incomplete gets
appSetupRequiredfor every authenticated route, including/cli-login. The terminal command on this screen opens a browser tab to approve a CLI login, so if the handoff were a gate, the flow would deadlock — the screen telling you to run the command would be the same screen blocking the command from finishing.So leaving app setup is a server fact.
completeAppSetupruns first; the moment it succeeds,/cli-loginis reachable in any tab. The handoff itself is a client-only phase held inAccountApp, not a route and not a database row. It lives in the tab that finished app setup. A reload lands on the dashboard exactly as before, which is also why no migration or onboarding column is needed.The command names the address the operator is already looking at
paseo hub loginwith no argument resolves to the CLI's own default origin. Any other Hub has to be named, and the only address certainly reachable from the operator's machine is the one their browser is on. So the rule is one comparison against that single origin — no deployment flag, no per-instance configuration, nothing new to set.paseo hub login http://localhost:3000(the live browser origin, exactly).paseo hub login.Polling states
The handoff subscribes to the existing organization daemon query and maps it to four named states outside React, so each one has a test:
checkingwaitingfailedlinkedA daemon counts as linked when it is active and connected. A connection is never taken back because a later poll failed —
linkedoutranksfailed.Both exits land in the Default project
Instance setup already provisions a
Defaultproject, so ending onboarding on a project list with one entry asks the operator to pick the only choice.ContinueandDo this laterboth navigate to/o/<organization-slug>/projects/default/overview, reusing the organization slug already passed into the handoff. Nothing is queried and no project is created.The route is committed before the client phase is dropped. Dropping it first renders the dashboard at whatever URL onboarding happens to be standing on, which flashes the project list on the way past.
Related work
🚫 Rollout is blocked
Do not merge this PR until getpaseo/paseo#3651 is merged and released.
This screen tells the operator that their terminal "signs you in, connects this daemon, and offers to set up a starter workflow". Today's released CLI authenticates only — connect, initialize, and deploy are added by #3651. Shipping the browser instructions first would promise a flow the CLI does not yet offer.
Test harness correction
Enrolling a daemon in the E2E harness used to reach the database directly with
pg.Client, which only works against PostgreSQL and forced the journey to pay for a container. Issuing the enrollment token now goes through the application-owned runtime boundary — the child process that owns the database answers adaemon-enrollment-tokencommand — so it works on either runtime.That removed the only runtime asymmetry in the journey, and the core handoff spec now runs fully on embedded PGlite with no PostgreSQL container at all. The same journey was also run forced onto PostgreSQL to confirm equivalence, and the affected shared suites still run against PostgreSQL as before.
Verification
npx vitest run src/daemons/handoff.test.tsxnpx playwright test e2e/daemon-handoff.spec.ts --project=desktop-chromium(embedded PGlite)npx vitest run src/instance-setup/browser-claim.integration.test.ts(PostgreSQL, testcontainers)npx playwright test e2e/apps.spec.ts e2e/first-run.spec.ts --project=desktop-chromiumnpx playwright test e2e/apps-mobile.spec.ts --project=mobile-chromiumnpm run release:checknpm pack --dry-run)The E2E journey proves, in one causal run: app-setup completion reaches the handoff; the self-hosted command contains the current origin exactly and copies to the clipboard;
/cli-loginrenders its verification-code form in a second tab while the handoff is displayed; a newly enrolled and connected daemon advances to the success state; and both exits assert the canonical Default-project URL and its Overview immediately after the click, rather than clicking a project link. The screen is axe-clean.Risk surface
AccountAppgained one client-side phase check ahead of the existing status branches. Worth confirming the handoff cannot capture a session that is signed out or has no organization — it renders only forappSetupRequiredandactive.appSetupRequirednow declaresorganization.slugrequired instead of optional. The server has always sent the same resolved membership as the active state; the integration test asserts both states carry the same slug.AppSetupSurface.leave()andskipAppSetup()now walk through the handoff and end on the project Overview, so every journey that passes through app setup changed destination. Assertions were updated only where the new true destination required it.