fix(desktop): draw the person page's waits as skeletons, never a false Someone or empty line

Merged
DA
7d ago
+177-19

PR 3 of 9 in the loading-states rollout (Loading states audit, plan Fix Inbox loading state, "Loading states rollout"): the Person page (desktop/person-page.tsx).

Summary

  • Corner spinners → skeleton sections. The bare 16px "Loading inbox" and "Loading automations" rings, which stacked in the column with no heading, are now placeholder sections: a heading-shaped block over placeholder rows. The Inbox uses InboxListSkeleton as it is (left unmodified, since PR 2 owns its row-count prop). Automations get a local RowsSkeleton built from @luke/ui's ListRow/Skeleton/SkeletonRegion: a tile, a name, and trigger text, with no time. AutomationRow is unchanged.
  • No "Someone · Not in this workspace" while the members load. A page opened by userId alone (⌘K People, an avatar) for a member with no task in the list used to resolve to SOMEONE/UNNAMED until the page's own MemberDirectory answered. Until the members and former-members reads have answered, it now shows a placeholder head (a 48px circle, a name line, a caption line) and a placeholder toolbar title. Only settled reads can now say Not found.
  • No "Nothing of theirs here yet." before the task list is read. The empty line is now gated on tasks.listRead. While the list is unread and nothing of theirs is listed yet, a placeholder tasks section stands in its place. A list read that failed says "Their tasks could not be read." with Retry (onRetryList), the same way Home does, so it doesn't pass for an empty page either.
  • The file's header comment is updated to describe the new loading behaviour.

Evidence

  • Platform-independent checks: pnpm check:affected passed, apart from two known sandbox quirks, each rerun alone and green: discarded-effect with a larger heap, and app-menu.test.ts after fetching Electron's binary.
  • person-page.test.ts has 4 new tests and 1 updated. All 5 fail with the person-page.tsx change stashed:
    • while the Inbox and the automations are read, placeholder rows stand for each, never a spinner
    • a page opened by a member's id alone stands a placeholder head while the members are read, never "Someone · Not in this workspace", and names them once the read answers
    • while the task list is read, placeholder rows stand for their tasks and the page is never called empty
    • a task list that could not be read says so and offers Retry, never calling the page empty
    • updated: "automations that could not be read leave the Inbox still being read…" now expects the Inbox placeholder instead of a spinner
  • Screenshots in the comment below.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run, or diff explained line by line). (CI)
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root AGENTS.md "Testing")
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-raw-async-primitives). The test's held reads use new Promise, as the suite's existing inboxHeld already does.
  • No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via pnpm discarded-effect)
  • Test count equals the pre-PR count or the delta is listed: +4 in person-page.test.ts (23 → 27).
  • Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root AGENTS.md's "The permanent adaptors" with the reason it stays.
  • Every CLAUDE.md/AGENTS.md sentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording)
  • PRIVACY.md unchanged, or the change is a product decision called out in the PR body.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI)
  • Barrel/door rule: no Node-reaching Effect module (@effect/platform-node, effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderer node: grep plus a new grep for those specifiers in renderer bundles)

Notes

  • Blockers or follow-up verification: useWorkspaceMembers/useFormerMembers swallow a refused read and stay undefined. If either read is refused, a person nothing else names keeps the placeholder head instead of the old false "Someone · Not in this workspace". Giving the member directory a failed state, so the head could offer Retry, would touch member-directory.tsx and use-workspace-members.ts, so it is left as a follow-up.

Made with Luke


Devin Review


Summary by cubic

Replaces the person page's loading waits with skeleton placeholders and stops the page from falsely naming someone as not in the workspace or declaring it empty while reads are pending.

  • While the Inbox, automations, members, or task list are being read, skeleton placeholder sections stand in their place instead of bare spinners.
  • A page opened by a member's id alone now shows a placeholder head until the member read answers, instead of "Someone · Not in this workspace".
  • The empty line "Nothing of theirs here yet" now appears only after every read has answered and the page names someone; a failed task list read shows an error with Retry.