Repository navigation
feat(identity): first-run setup store, step table and pages (composition follows in the next PR) - #1819
Conversation
f5752d2 to
d35338d
Compare
b202f48 to
6e41931
Compare
d35338d to
4972a16
Compare
4972a16 to
5c9a654
Compare
6e41931 to
1d5b1b1
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account: two changes needed, detailed inline—preserve the portable-avatar guard during community restoration, and distinguish partial backup recovery from a missing linked-key backup. One optional focus improvement is also inline.
Rocket, Mantis and Groot completed independent lanes; I verified and integrated their findings. Head 5c9a65404d7d0dd929650f6f7098e7aa5936841d, base 1d5b1b1e8f8278405bda3f883ea0483a7fb94526. Source only: no tests, builds, app execution or CI assessment; native recovery, rendered focus and #1820’s composition remain unverified.
| try { | ||
| communities.joined( | ||
| { id: destination.id, name: destination.host }, | ||
| read.exists ? read.profile : client.profile, |
There was a problem hiding this comment.
🤖 P2 — Keep protected community avatars out of the device default. On a fresh device with an empty local profile name, communities.joined seeds the default from this argument (service.ts:524). Passing read.profile directly also copies an avatar under this community’s /media/ path. CommunityDialog then offers that default when joining another community, where saving can publish a protected, unusable avatar URL. This bypasses the explicit portability rule in docs/communities.md and the existing completeJoin path. Apply profileDefault(next, client.profile, destination.id) before joined, keeping the original read in the outcome. Add a restore case with a community-hosted avatar and verify the previous local picture is retained.
| <p role="alert">{recovery.error}</p> | ||
| ) : mismatched ? ( | ||
| <p role="alert"> | ||
| None of your backups holds the key linked to this account, so nothing |
There was a problem hiding this comment.
🤖 P3 — Preserve retry when recovery is partial. With a non-matching recovered candidate and failedCount > 0, the linked key may belong to a backup that failed to recover. This branch nevertheless says no backup holds it, and the action branch offers import instead of retry. Distinguish incomplete recovery from confirmed absence and offer retry alongside import. Wire retry through a successful cancel followed by beginRecovery in KeyRestore: merely exposing the existing callback is insufficient because service.ts refuses a new attempt while raw candidates remain. Cover this mixed success/failure case, including that cancellation failure does not start another attempt. This also follows DESIGN.md’s rule that failed reads do not prove absence.
| type="submit" | ||
| variant="prominent" | ||
| loading={busy} | ||
| disabled={!canContinue} |
There was a problem hiding this comment.
🤖 Optional P3 — Preserve focus during a failed profile submission. canContinue includes !busy, so pending Continue receives both loading and disabled. That bypasses shared Button’s loading && !disabled focus-preservation behavior; Skip likewise becomes non-focusable. Consider keeping the active action focusable while guarding repeated submission, or restoring a deliberate focus target on failure. Keep the busy guard in form submission. This is source inference; verify the failed Continue/Skip keyboard paths in the rendered host before choosing the repair (DESIGN.md, “Loading without flicker”).
1d5b1b1 to
8eefb4a
Compare
5c9a654 to
32731cd
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account: no new findings in this rebase, but the two previously reported changes are still needed: P2 avatar portability and P3 partial-recovery retry. The existing repair advice remains applicable; the optional profile-submit focus suggestion remains optional.
The feature patch differs from the previously reviewed revision only in comments; we checked its affected integration with the changed base. Rocket, Mantis and Groot completed independent lanes, and I verified their findings against the pinned source. Head 32731cd75ccc4cacfea3249f9016fc894435747f, base 8eefb4a0d909841610f87eed10bf65f615e2908c. This is a follow-up of the prior findings and affected integration, not a review of every intervening base change.
Source only: no tests, builds, app execution or CI assessment. Native recovery, rendered focus/accessibility, performance and #1820’s composition remain unverified. The public description and commit metadata were inspected; no attached images were present. Reported author test results were not independently verified.
…ion follows in the next PR)
The stateless building blocks of the first-run flow, reviewable as
components against DESIGN.md and Kenny's onboarding layouts. Nothing
imports them until PR 07 (`Welcome.tsx`); the repo has no unused-export
check, so lint and typecheck pass with them standing alone.
Setup store and steps (`setup.ts`): `createSetup()` with `start(steps,
then)`, `retry()` (re-runs only the failed step, then the rest) and
`reset()`, over snapshots `idle` / `running{label}` / `paused{at}` /
`failed{error, retryable, skipsLink}` / `done`; a step's `run` may resolve
to a resting state that overrides the caller's `then`. Step factories:
`checkAccount(account, onLinked, onKnown)` (the linked-key and
known-communities reads in parallel under one label; the list read never
fails setup), `checkBackup(account, onListed)` (never fails setup),
`createKey`, `importKey`, `linkKey`, `unlinkKey(account, old, new)`,
`restoreCommunities(known, communities, decide)` and `finish()`.
`restore-communities.ts`: `restoreKnown(known, communities, { concurrency:
3, startBy: 45_000 })` — the same signed profile read the Join Community
page uses, at most three at once, nothing started after 45 s, each admitted
destination saved through `communities.joined`; outcomes `admitted` /
`denied` / `unavailable` in list order; never throws. `key-import.ts`:
`classifyKeyImport` (empty / `ncryptsec` refused with a clear message /
`nsec` with its npub preview / invalid) and `readKeyFile` (1 KiB cap, first
non-blank line).
Pages (all `src/features/identity/`, stateless, props-driven):
`CommunityUrlField` (bare host, `wss://`/`https://`, or an invite link —
`https://<host>/invite/<code>`, `buzz://join?relay=…&code=…` — via
`enteredCommunity`; Continue stays enabled while empty/invalid and shows one
inline error), `InviteCodeField`, `JoinCommunity` (owned rows named "Join
{host}" for assistive technology, the URL field, the disabled "Scan a QR
code" / "Create a community — Coming soon" buttons, on the onboarding
layout), `KeyChoice` (Create my key / I already have a key / Use a
different account, the signed-in address, the backed-up-key sentence only
when the lookup listed none), `KeyImport` (masked field, Reveal/Hide, file
drop/picker, npub preview, `expectedPubkey` mismatch refused in the page,
"Importing your key…" on the same screen), `KeyLost`, `KeyRestore` (the K3
column; "Your key is stored securely on this device." only while the
restore may still install the key; the unavailable/refused page),
`BackupOffer`, `ConnectChoice` ("Connect to your community" with the host
on its own mono line), `WelcomeBack` (the linked npub with Copy, Restore
from backup when listed, Enter your private key, I lost this key, Use a
different account), `PublicKeyPanel`, `SetupProgress`, `Wireframe`
(`WireframeScreen` on Kenny's frame, `WireframePanel` as the one stacking
group), `ProfileStep` (a pure form over `ProfileFields`: Continue hands the
trimmed draft out, Skip for now leaves). Every `h1` carries `tabIndex={-1}`
and the Back triggers `data-return-focus`, for the heading-focus handoff
`Welcome` performs. Shared owners only: `Button`, `Field`, `Input`,
`Checkbox`, `Avatar`, `LoadingRegion`, `OnboardingScreen` and Kenny's
`IdentitySetup.module.css` classes (unchanged); role colours and type roles
only, no new tokens or variants.
Tests: `setup.test.ts` (retry/rest/idle semantics, `checkAccount` and
`checkBackup` never failing setup, `unlinkKey` sending nothing while the
old key is on the device), `restore-communities.test.ts` (three reads at
once, outcomes in list order, dedupe and insecure skip, throwing `joined`,
the 45 s cutoff under fake timers), `key-import.test.ts`,
`CommunityUrlField.test.ts` (8 accepted and 10 refused forms),
`ProfileStep.test.tsx`. The pages' rendered checks (Chromium/WebKit, light/
dark, 480/1100 px, 150 % text) were done on the branch in scratch fixtures
and are recorded in the branch's commit history; mount a page with stub
props to repeat them.
Stack: on the first-run account client; uses the merged `destination`,
`find-community` types, `api`, `ProfileFields autoFocus`, `service`,
`RestoreIdentity` `expectedPubkey`/`onImport` and `BackupError`, plus the
account client's `builderlab-account` types. The Welcome flow composes these.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
32731cd to
235e33e
Compare
8eefb4a to
40812cc
Compare
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated follow-up source review via Wes’s account: no new findings in this limited rebase follow-up. The previously reported P2 protected-avatar restoration issue and P3 partial-backup recovery issue remain unchanged and still need the repairs described there. The earlier focus suggestion remains optional; existing inline feedback is not duplicated.
Reviewed head 235e33ead40427dcf9d905783e33105c9894a2b5 against base 40812ccf4c000363b47d30fce9e4a028b5cfcf50. The feature patch is byte-identical to the patch reviewed at 32731cd75ccc4cacfea3249f9016fc894435747f; the endpoint delta consists only of the inherited Claude spare-process guard and its regression test (20 additions, 1 deletion). This qualifies for the limited-follow-up solo exception; Star Lord performed this pass without new teammate lanes.
Source-only: inspected that delta, its process lifecycle/callers, prior findings, and public PR description/commit material (no attached images). No new disclosure issue found. Tests, app/native flows, rendered accessibility, focus behavior, performance, and CI were not exercised or verified. This is a COMMENT review, not approval or merge authorization.
Onboarding stack (merge in order, top to bottom):
Depends on: the merged #1815 (
destination,find-communitytypes,api,ProfileFields autoFocus) and #1816 (service,recoveryOff,BackupError); this layer addsRestoreIdentity's optionalexpectedPubkey/onImportprops with their only caller,KeyRestore, and uses #1818'sbuilderlab-accounttypes. #1820 composes these; nothing imports them until then.Diff: production +1590/−3; tests +636/−0
What this layer is
The stateless building blocks of first run, reviewable as components against DESIGN.md and Kenny's onboarding layouts.
setup.ts:createSetup(start/retry/reset;running/paused{at}/failed{retryable, skipsLink}/done/idle), the step factories (checkAccount,checkBackup,createKey,importKey,linkKey,unlinkKey,restoreCommunities) andfinish();restore-communities.ts(restoreKnown, 3-wide, 45 s cutoff);key-import.ts(classifyKeyImport,readKeyFile); and the pagesCommunityUrlField,InviteCodeField,JoinCommunity,KeyChoice,KeyImport,KeyLost,KeyRestore,BackupOffer,ConnectChoice,WelcomeBack,PublicKeyPanel,SetupProgress,Wireframe,ProfileStep. Dead code for exactly one PR by design (the repo has no unused-export check), so that #1820 is the composition and the product flow alone.Review focus
setup.ts:retry()re-runs only the failed step; a step's resting answer overrides the caller'sthen;failed.skipsLinkis derived from the steps left.checkAccount/checkBackupnever fail setup on the list/lookup reads.restore-communities.ts: concurrency 3, nothing started after 45 s, outcomes in list order, never throws;joinedonly for admitted destinations.Button/Field/Input/Checkbox/Avatar/LoadingRegion/OnboardingScreen, role colours and type roles only, Kenny's CSS module classes reused unchanged;tabIndex={-1}headings anddata-return-focustriggers serve the focus handoffWelcomeperforms. Rendered checks were done on the branch in scratch fixtures (recorded in its history); mount a page with stub props to repeat them.KeyImport: a mismatched key (expectedPubkey) is refused in the page and never reachesidentity_import; the key lives only in component state.CommunityUrlField: Continue stays enabled while empty/invalid (Enter is handled by the input, not a native form submit).Checks run on this branch's tree
9818f778ewithout feat(builderlab): tenant table — a login origin per hosted relay, granted natively and accepted by the binding signer #1817 (2026-10-30).tsc --noEmitis clean at each of 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–feat(settings): read-only Key backup row in Settings → Identity #1821. At the stack head5daa01ffd([DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822; same tree that was tested): fullvitest run13,915 passed, 1 failed (codex-sh.test.ts, outside the stack, passes 3/3 alone);biome checkon the changed files and the design push check pass.--no-verify: the pre-push unit check failed only on 5 s timeouts in files the stack doesn't touch while the machine was at load ≈45; all 7 of those files pass when run alone. Rust is unchanged below [DO NOT MERGE] chore(identity): DO NOT MERGE — isolated debug Keychain slot (full stack for native testing) #1822 and was not re-run locally.Native evidence
None on this tree. Native first-run runs use the full tree in the DO-NOT-MERGE #1822 (it adds the isolated debug Keychain slot), and none has been made since the rebase. Treat every PR before #1822 as unit/integration-tested only.
🤖 Generated with Claude Code