fix(desktop): say a refused members read on the Person page, with Retry
Merged
DA
16d ago+172+172+012345678901234567890123456789-65-65-01234567890123456789
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 privateuseCountedRead. It re-reads when the count changes or on retry, and records whether the read was refused. A newuseFormerMembersReadreturns{ former, refused, retry }, matchinguseWorkspaceMembersRead(#3640).useWorkspaceMembersanduseFormerMembersare still thin wrappers, so the version history and ⌘K are unchanged.MemberDirectory: now reads through both…Readhooks. Its context exposesreading,refused, andretrybeside the members.retryonly 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
ErrorStatewith 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.

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 ownuseWorkspaceMembersanduseFormerMembers): with no members,personActornames 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
ownerIdalone (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:affectedpassed (exit 0).discarded-effectwas run alone underNODE_OPTIONS=--max-old-space-size=6144, a known sandbox heap limit. person-page.test.tshas 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.shgreen. (CI) - JSON Schema goldens unchanged (
LUKE_UPDATE_FIXTURESnot 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
astype assertion outside the allowlisted files. (CI via anti-slop) - No
Effect.runPromise/runSync/runForkoutside the runtime edges and the shimstools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-run-promise-outside-edges) - No new
setTimeout/setInterval/new Promise/AbortController/fs.watchoutside the filestools/anti-slop/effect-edges.jsonrecords. (CI viaanti-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.mdsentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording) -
PRIVACY.mdunchanged, or the change is a product decision called out in the PR body. - Package
package.jsondeps match what the sources import by bare specifier;effectviacatalog:. (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 renderernode: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
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
MemberDirectorynow exposesreading,refused, andretrybeside the members;retryonly re-asks a read that was refused.useWorkspaceMembersanduseFormerMembersnow share a singleuseCountedReadhook, and a newuseFormerMembersReadmatchesuseWorkspaceMembersRead.- The page's "Profile" placeholder label is a shared constant.
- Callers that only read names are unchanged.