fix(desktop): say a refused members read on the Person page, with Retry

Merged
DA
16d ago
+172-65

Summary

Follow-up PR A of the loading-states rollout. Context: the Loading states audit and the plan Fix Inbox loading state, "Loading states rollout". It picks up the issue Devin raised on #3626 and deferred here: if the members read or the former-members read was refused, the Person page head stayed a placeholder forever.

  • use-workspace-members.ts: both reads now go through one private useCountedRead. It re-reads when the count changes or on retry, and records whether the read was refused. A new useFormerMembersRead returns { former, refused, retry }, matching useWorkspaceMembersRead (#3640). useWorkspaceMembers and useFormerMembers are still thin wrappers, so the version history and ⌘K are unchanged.
  • MemberDirectory: now reads through both …Read hooks. Its context exposes reading, refused, and retry beside the members. retry only re-asks a read that was refused, so the read that answered stays drawn. Callers that only read names are unchanged.
  • Person page: when either read is refused and nothing else names the person, the placeholder head becomes ErrorState with Retry: "Their profile could not be read." ("Your…" on the reader's own page), worded like the page's other read failures. The toolbar title says "Profile" in place of its placeholder. A successful Retry shows the person.

Person page with a refused members read

Other places MemberDirectory names people (scope item 3)

None of these needed a change:

  • Bylines (edit-ref.tsx), answers (inbox-pane.tsx), starters on Inbox rows and group headers (inbox-list.tsx), and the version history (its own useWorkspaceMembers and useFormerMembers): with no members, personActor names the person by the name the wire carries, with a letter where their picture would be. That is exactly what they show while the read is still loading, and it makes no false claim. Someone is marked as having left only once the members read has answered.
  • One soft spot, not fixed: a task whose wire names its creator by ownerId alone (an older service) is drawn as "Someone" on Home rows and the task card until the members read answers, and a refused read leaves it that way. The @-mention picker also offers no people while the read is refused. Neither has room for an error and Retry, so it's not a small change. Leaving both for the tasks-view follow-up or later.

Evidence

  • Platform-independent checks: pnpm check:affected passed (exit 0). discarded-effect was run alone under NODE_OPTIONS=--max-old-space-size=6144, a known sandbox heap limit.
  • person-page.test.ts has two new tests, and both fail without the change:
    • A refused members read shows the error and Retry, and Retry shows the member.
    • A refused read of who left shows the error, and Retry shows them as a former member.

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)
  • 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. (+2 in person-page.test.ts)
  • 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: None. The tasks view's read statuses are out of scope and handled in a separate follow-up.

Made with Luke


Devin Review


Summary by cubic

Shows the Person page an error with Retry when reading the workspace's members or former members is refused, instead of leaving a never-resolving placeholder head.

Refactor

  • MemberDirectory now exposes reading, refused, and retry beside the members; retry only re-asks a read that was refused.
  • useWorkspaceMembers and useFormerMembers now share a single useCountedRead hook, and a new useFormerMembersRead matches useWorkspaceMembersRead.
  • The page's "Profile" placeholder label is a shared constant.
  • Callers that only read names are unchanged.