fix(members): serve the Crew Members page from the React Query cache (#9418) - #9458
fix(members): serve the Crew Members page from the React Query cache (#9418)#9458NicholasRBowers wants to merge 1 commit into
Conversation
|
Intent: Move the Crew Members page's five fetch sites (roster, thread get-or-create, drawer activity, wake sources, star write) onto React Query so cached data serves instantly on return, the app's existing invalidation reaches the roster, and failed reads are retryable. |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Moves page state onto the caches that invalidation already reaches — root-cause fix for the flash, the latched failure, and the stale roster, with each shared-key shape verified. I checked the load-bearing claims: [DESIGN-REVIEWED] d60e31e |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Verify the one unverifiable manual claim: that capture frame 07 and the frame-01 label check fail identically on pristine main (pre-existing drift). Not justified as shipped
What this change shipsIntent: stop the Crew Members page re-fetching from scratch on every visit by moving its five fetch sites onto React Query — a FIX (issue #9418; mandated by
Claims checked: [FIRST-PRINCIPLES-REVIEWED] d60e31e |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of I have everything I need: the diff is confined to UX-Verdict: CONCERNS The refactor's only visible deltas — four ErrorNotice states and the kill-switch "(paused)" marker — appear in no screenshot, so nobody has seen them rendered. Watch
Evidence gaps
[UX-REVIEWED] d60e31e |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
…9418) The page hand-rolled its data layer with useState+useEffect, so every visit refetched everything from scratch: the roster blanked then repopulated, the drawer re-skeletoned data shown seconds ago, and a failed wake-sources fetch was latched for the page's life. The roster also lived outside the query cache, so the invalidation the WebSocket refresh frames and the crew editor already issue could never reach it. Move the five fetch sites onto React Query: - roster: ['kirocrew-agents', 'members-roster'] — a prefix child of the key useWebSocket already invalidates, so crews created or renamed elsewhere now reach this page with zero new invalidation calls - thread get-or-create POST: a cached query keyed by exact member name, staleTime 0 — a cached thread mounts instantly on return while the idempotent POST repairs in the background - drawer activity: per-member cached query - wake sources: the shared ['cron-jobs'] / crewWebhooksQueryKey / ['default-agent'] entries, deduping against the Schedule page and the crew editor; failures are now retryable via React Query - star toggle: optimistic flip via queryClient.setQueryData Non-fetch effects (unread drain, countdown tick, URL-to-member sync) are unchanged. Closes #9418
fbc6267 to
d60e31e
Compare
|
self-added: yes
|
|
self-added: yes
|
|
self-added: yes
|
|
self-added: no
|
|
🤖 Kiro Crew Auto-Pipeline [operator: NicholasRBowers#a942f9ca] — REVIEW-READY at head d60e31e: all 65 checks green, PR Readiness passed, GPT + Opus clean, Design + First Principles PASS, UX CONCERNS answered with a recorded disposition, 0 unresolved threads. Single commit, mergeable. Awaiting human maintainer review; auto-merge not armed. |
|
🤖 Kiro Crew Auto-Pipeline — closing as superseded. @CrysisDeu's #9442 merged the same React Query migration for this page ( One hazard this PR's review rounds hardened that the merged version does not carry: No hard feelings on the race — the goal was the fix, and it shipped. |
Problem / Motivation
Every visit to the Crew Members page starts from scratch. The member list goes blank and then fills back in, the DM column shows its "Opening thread…" placeholder again, and the drawer's Recent activity and Wake sources blocks re-skeleton for data that was on screen seconds ago. If a wake-sources fetch fails once, the block stays failed until a full remount — it is never retried. And a crew created or renamed anywhere else in the dashboard does not appear on this page until it fully remounts, because the roster lives in component state that the app's cache invalidation cannot reach.
Why it matters
The page hand-rolls its data layer with
useState+useEffect, whichwebsite/AGENTS.mdforbids ("Data fetching is React Query"). Beyond the rule: users see reload flashes on every visit, duplicate requests go out for data other pages already cache under['cron-jobs']/['webhooks', …], and the roster silently disagrees with the crew editor after a save.What changed (motivation → approach → change)
The page's five fetch sites move onto React Query. The roster is cached under
['kirocrew-agents', 'members-roster']. That key is a child of the key the WebSocket refresh frames already invalidate, and invalidation matches by prefix, so crews created or renamed elsewhere now reach this page with zero new invalidation calls. It cannot share the bare key: that entry stores a different response shape, and two queryFns under one key let whichever mounts first decide the other's shape.The thread get-or-create POST becomes a cached query keyed by exact member name with
staleTime: 0. A cached thread mounts instantly when you return to a member, and the idempotent POST re-runs behind it to repair whatever the backend lost since — every open still goes through the endpoint, as the page's identity rules require. The drawer's activity block gets a per-member cached query. The wake-sources block reads the same three cache entries the Schedule page and the crew editor already keep (['cron-jobs'], the crew editor's['webhooks', 'crew-editor']entry, and the shared['default-agent']definition), so opening the drawer dedupes against them, and a failed read is retryable instead of latched forever. The star toggle becomes auseMutationwith the full optimistic protocol: cancel the in-flight roster refetch before the flip, revert per-name on error, reconcile with an invalidation on settle.Two behaviors were preserved deliberately. The roster's display order is committed per membership, not per refetch — the roster now refetches on every refresh frame, and re-sorting on a
last_active_tsadvance would move rows under the cursor mid-click, opening the wrong member's durable DM thread. And every drawer block keeps its failed-vs-empty distinction: a failed refetch keeps showing the last good data rather than replacing it with an error banner (data === undefined && isError, the same spelling the patrol block already uses). Error surfaces this diff touched now render throughErrorNotice(wake, activity, roster load, thread open), withaskAgentoff only above the DM composer, which may hold an unsent draft. Non-fetch effects — the unread drain, the countdown tick, the URL-to-member sync — are unchanged.One small visible behavior change beyond the flash removal, called out on purpose: a webhook row under "Wake sources" now shows the "(paused)" marker when the store-wide webhook kill switch is off, not only when the token's own switch is. The row's silenced state calls the shared
webhookCanCallInpredicate (wakesCrew.ts), which encodes both switches — the same rule the Webhooks page and the crew editor already apply — so this drawer stops contradicting them about whether an external webhook can call in. A test pins it.flowchart LR subgraph Before WS1[WS refresh frame]:::ctx --> K1["invalidate ['kirocrew-agents']"]:::ctx K1 -.-> X1[roster in useState<br/>unreachable]:::removed end subgraph After WS2[WS refresh frame]:::ctx --> K2["invalidate ['kirocrew-agents']"]:::ctx K2 --> R2["roster cache<br/>['kirocrew-agents','members-roster']"]:::added R2 --> O2[order pinned to membership]:::added end classDef added fill:#DCFCE7,stroke:#16A34A,color:#14532D,stroke-width:2px classDef removed fill:#FEE2E2,stroke:#DC2626,color:#7F1D1D,stroke-dasharray:4 3 classDef ctx fill:#E0F2FE,stroke:#0284C7,color:#0C4A6E linkStyle 1 stroke:#DC2626,stroke-dasharray:4 3 linkStyle 3,4 stroke:#16A34A,stroke-width:2px🟩 added · 🟥 removed · 🟦 unchanged
The refresh frame's invalidation now reaches the roster, and a refetch updates row content without moving rows.
Tests
serves the roster from cache on remount— rows render synchronously at the remount's first paint; the blank-then-repopulate flash is the bug this locks out.invalidating the ['kirocrew-agents'] prefix refetches the roster in place— the WebSocket refresh path reaches the roster's child key, and existing rows never blank during the refetch.a refresh-frame refetch never reorders the roster; a membership change re-sorts it— row content updates in place, order holds; adding a crew re-sorts by recency.a failed wake-sources fetch recovers on the next retry signal— the old one-shot fetch latched its failure for the page's life.a failed wake refetch keeps the known-good list on screen— a transient blip on a shared key must not replace correct data with an error banner.the store-wide webhook kill switch marks bound tokens "(paused)"— pins the one declared visible behavior change, through the sharedwebhookCanCallInpredicate.reopening the drawer revalidates activity— cached entries render instantly, and the background refetch lands newly recorded activity (nothing invalidates this key, so the reopen must revalidate).useMutationprotocol: the write dispatches async, the flip is asserted optimistic while the write is still unsettled, and the settle-side reconciling refetch is served post-write server truth.Manual verification
Full frontend suite green (30,407 passed),
npx tsc -band eslint clean. Screenshots below captured through the existingcapture-members-page.mjsharness against this branch; its frames 01–06 verified their asserted states (frame 07 and one frame-01 label check fail identically on pristine main — pre-existing capture drift, not this diff).Screenshots / video
Visually unchanged except two things: reload flashes are gone, and a webhook row under Wake sources shows "(paused)" when the store-wide kill switch is off (declared and tested above). Layout, spacing, and copy are untouched.
More surfaces (mobile, light, wide, driving sessions)
Related Issues
Fixes #9418
Pattern harvest
Rule candidate: review-prompt
Pattern: a page-level
useStatefetch cache is unreachable byqueryClient.invalidateQueries— any surface holding server state outside React Query silently diverges from every invalidation site.Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)