Skip to content

fix(navigation): run the visit deadline only while a presenter is attached - #1814

Merged
matt2e merged 2 commits into
mainfrom
onboarding/01-navigation-presenter-deadline
Oct 28, 2026
Merged

matt2e merged 2 commits into
mainfrom
onboarding/01-navigation-presenter-deadline

Conversation

@matt2e

@matt2e matt2e commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Onboarding stack (merge in order, top to bottom):

  1. fix(navigation): run the visit deadline only while a presenter is attached #1814 navigation: visit deadline only while a presenter is attached ← this PR
  2. refactor(communities): extract the join owner and add the pre-join relay lookup #1815 communities: join owner and pre-join relay lookup
  3. feat(identity): observable nsec backup status with pass tickets, explicit lock release, and refused-recovery copy #1816 identity: native nsec backup status
  4. feat(builderlab): tenant table — a login origin per hosted relay, granted natively and accepted by the binding signer #1817 builderlab: tenant table and granted native origins
  5. feat(identity): first-run Builderlab account client — sign-in without a plugin session, kgoose reads, link/unlink, backup, saved communities — and its dev seed #1818 identity: first-run Builderlab account client and dev seed
  6. feat(identity): first-run setup store, step table and pages (composition follows in the next PR) #1819 identity: first-run setup store, step table and pages
  7. feat(identity): the first-run Welcome flow replaces the key-setup gate (FOUNDATION: App.tsx) #1820 identity: Welcome flow replaces the key-setup gate
  8. feat(settings): read-only Key backup row in Settings → Identity #1821 settings: read-only Key backup row
  9. [DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822 DO NOT MERGE: isolated debug Keychain slot (full stack for native testing)

Depends on: nothing. Only PR 07 (Welcome) depends on it.

Diff: production +38/−8; tests +41/−1; docs +3/−1

What this layer is

The navigation controller's 15 s visit deadline is armed only while at least one presenter is attached (attach() on the host object, returning a detach function); useAppNavigation attaches in a mount-time layout effect. On main today the startup home visit is created inside createServices() and times out if the key-setup screen sits idle for 15 s, so this is a standalone defect fix with no first-run dependency. Preserved: a mounted presenter that never finishes still times out 15 s after attaching; ingress failures and resolve() are untouched.

Review focus

  • controller.ts: arm() is the only place the timer starts; start(), attach() and the last detach are its three callers. Check that resolve() and fail() are unchanged.
  • src/app/navigation.ts: the layout effect attaches once per presenter mount and detaches on unmount; a StrictMode double mount restarts the deadline.
  • controller.test.ts: setup() now attaches by default so the existing timeout pins still prove the 15 s contract; the new case is the no-presenter one. The App-level proof through the first-run gate (first-run-navigation.test.tsx) lands in PR 07.

Checks run on this branch's tree

  • bin/pnpm tsc --noEmit ✓ (exit 0)
  • biome check on the 4 changed TS files ✓
  • bin/pnpm vitest run src/features/navigation src/app: 698 passed / 2 failed of 700 — both failures were 5 s timeouts under load in files this layer does not touch (App.profile.test.tsx, NotificationSettings.test.tsx); rerun alone: 2 files, 27/27 passed ✓

Native evidence

None on this tree. The native first-run recordings and the hands-on checks on the branch were made on the full tree, which only the DO-NOT-MERGE PR 09 reproduces (it adds the isolated debug Keychain slot). Treat every layer below 09 as unit/integration-tested only.

🤖 Generated with Claude Code

…ached

The navigation controller armed a pending visit's 15 s deadline the moment
the visit was created. The startup `home` visit is created inside
`createServices()` before React renders, so whenever no presenter exists
for 15 s — today, the key-setup screen left idle; soon, a first-run flow
that keeps `ConnectedApp` hidden for minutes — the visit is already
`failed`/`timeout` when the first presenter mounts, and the user's first
frame is "This destination couldn’t open … kept for retry".

The deadline exists to catch a presenter that never finishes; while no
presenter exists its expiry carries no information. The controller's host
object gains `attach()`, returning a detach function, and a pending visit's
timer is armed only while at least one presenter is attached: `start()`
arms at once when one is, `attach()` arms the active pending visit when the
first presenter arrives, and the last detach clears it. `useAppNavigation`
attaches in a mount-time layout effect, so the startup visit's deadline runs
from the moment `ConnectedApp` mounts. `provideNavigation` already spreads
the controller's host methods into `services.navigationHost`, so no
FOUNDATION file changes and no constant is lengthened: every visit opened
while the app is presenting still has exactly its 15 s.

Preserved: a visit a mounted presenter never finishes still times out 15 s
after that presenter attached (no retry, no loop); ingress/deep-link
failures (`fail()`) and `resolve()`'s "keep the original deadline" are
untouched; a StrictMode attach→detach→attach restarts the deadline at
mount; `attach()` after `dispose()` arms nothing.

Tests: `controller.test.ts` — `setup()` attaches a presenter by default, as
the app does, so the existing timeout pins hold; a new case shows the
startup visit outliving 60 s with no presenter, the deadline starting on
attach, standing while a second presenter remains, pausing when the last
leaves, and a later presenter measured from its own start. `service.test.ts`
attaches in its request()-driven setup. The App-level proof through the
real first-run gate (`src/app/first-run-navigation.test.tsx`) lands with
the Welcome flow later in this stack. docs/plugin-architecture.md says the
deadline runs only while a presenter is attached.

Stack: PR 01 of the onboarding stack, based on main; nothing depends on it
except 07 (Welcome), and it depends on nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Two small follow-ups: preserve the adjacent deadline regression test and name the identity gate precisely.

Comment thread src/features/navigation/controller.ts
Comment thread docs/plugin-architecture.md Outdated
…and name the identity gate

The ThreadPanel deadline regression test never attached a presenter, so the
presenter guard left its deadline unarmed and the test no longer protected
the 15-second contract. Attach one, as the service tests do.

The presenter is held back by IdentitySetup, not the first-run
CommunityLanding branch, so name that gate in the docs and comments.

Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e merged commit f5f3f53 into main Oct 28, 2026
69 of 71 checks passed
@matt2e
matt2e deleted the onboarding/01-navigation-presenter-deadline branch October 28, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants