Skip to content

enhance(website): Redesign the playground Store inspector - #4254

Open
ntucker wants to merge 6 commits into
masterfrom
claude/project-thread-vzue6x
Open

ntucker wants to merge 6 commits into
masterfrom
claude/project-thread-vzue6x

Conversation

@ntucker

@ntucker ntucker commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Requested by Nathaniel · project thread

Motivation

Before: the playground's Store panel was a react-json-tree dump of the raw normalized state. Results showed bare ids like ["1","2"] that you couldn't follow, fetch status sat apart from the data as epoch numbers, optimistic updates were filtered out, and nothing showed what changed.

After: the Store panel is built around how Data Client normalizes data.

  • Table view (default): one table per Entity type; endpoints are a table with a fresh/stale/error pill and a live countdown. Columns that don't fit the panel move into the row's expanded detail, so nothing scrolls sideways.
  • Tree view: the Explorer layout, one line per row with a field preview. The Table/Tree choice is saved in localStorage.
  • Two-way references: every id that points at an entity is a chip; clicking it opens, scrolls to and highlights that row. Expanding any entity lists everything that references it (endpoints, entities, Collections), each clickable.
  • Long lists collapse: tables show 8 rows with "Show all N"; long arrays show 20 items with "N more"; only the first 3 Entity types start open.
  • No data hidden for good: meta and entitiesMeta live in each row's detail, optimistic updates get their own section while pending, and indexes/lastReset sit in a collapsed Internals section.
  • Returns toggle per endpoint shows what useSuspense hands the component (denormalized) next to what's stored.
  • Changed rows flash after each update (respects reduced motion).
  • At narrow widths (under 560px) the Store stacks under the preview at full width.

Solution

A tiny SchemaRegistry manager in the preview's DataProvider records each endpoint (schema + args) from FETCH/SET_RESPONSE/OPTIMISTIC/SET actions, since the store only holds keys and ids. refs.ts walks those schemas (Entity, Collection, Array/All, Values, Union, Invalidate, Object, Lazy) to mark which values are references, and model.ts builds tables plus a back-reference index. Anything without a known schema still renders as plain values. The registry is also where action tracking can hook in later. Drops the react-json-tree dependency. Store colors stay on the code palette, so they follow the dark-mode work in #4253.

Screenshots are in the project thread.

Open questions

Tree view's name: the toggle says "Table | Tree" (the mockup called it Explorer).

🤖 Generated with Claude Code

https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3


Generated by Claude Code


Note

Low Risk
Docs/playground-only UI with no production API or auth changes; dependency removal is localized to the website package.

Overview
Replaces the playground Store panel’s raw react-json-tree dump with a schema-aware inspector tailored to Data Client’s normalized store.

A new SchemaRegistry manager is wired into the preview DataProvider to capture endpoint schemas/args and optimistic fetches from actions; buildModel turns store state into endpoint/entity tables, resolves ids to clickable ref chips, and builds a referrers index. The UI adds Table (default) and Tree views (persisted in localStorage), expandable rows with meta/status pills and a Returns toggle for denormalized useSuspense output, optimistic/internals sections, change flash highlighting, and reveal/scroll navigation between related rows. react-json-tree and Tree.tsx are removed; narrow layouts stack the store under the preview.

Reviewed by Cursor Bugbot for commit db88280. Bugbot is set up for automated code reviews on this repo. Configure here.

Replace the react-json-tree dump with a store view built around how
Data Client normalizes data. Tables by default, a Tree view as a saved
option. Ids are links in both directions, long lists collapse, and
every stored value stays reachable from a row's expanded detail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3
@changeset-bot

changeset-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: db88280

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs-site Ready Ready Preview Oct 7, 2026 2:50am UTC

Request Review

@ntucker ntucker self-assigned this Oct 7, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T02:53:02.602489Z db88280 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Staff engineer (Cursor agent): Review at 69d18208. The overall direction looks right to me. A schema-aware inspector with a small SchemaRegistry manager seam, Returns built on controller.getResponse instead of re-denormalizing, graceful plain-value fallback, model tests, and dropping react-json-tree is a net simplification of what the panel teaches. I have one change for this PR and two follow-ups.

CHANGE_THIS_PR: open/closed state flips when the default set shifts

StoreUIProvider stores flipped (ids toggled away from their default), and isOpen(id) is defaultOpen.has(id) !== flipped.has(id). But defaultOpen is recomputed from model.tables.slice(0, OPEN_GROUPS) on every state change, and buildModel sorts tables by kind (entities, then unknown, then collections, then scalars) with insertion order inside each kind. When a new Entity type shows up later, say from a second component's fetch or after a mutation, it sorts in ahead of the collections and pushes something out of the top 3. Then:

  • a table the user never touched closes by itself, and
  • a table the user had explicitly closed opens again, because its flip now XORs against a different default.

Here's a concrete case: User, Post and [Comment] are all open by default. A later fetch adds Comment, so the order becomes User, Post, Comment, [Comment], and [Comment] collapses with no click. Suspense playgrounds load tables bit by bit all the time, so this will come up.

The fix is the same size or smaller. Store the user's explicit choice instead of a diff against a moving default, e.g. overrides: Map<string, boolean> with isOpen = id => overrides.get(id) ?? defaultOpen.has(id). toggle then sets !isOpen(id) and reveal sets true for each ancestor, which also lets reveal stop branching on defaultOpen. A small test that adds a table between two renders and checks a closed group stays closed would lock it in.

(Before posting this I tried to knock it down. It isn't just theoretical, since tables appear one by one under Suspense, and the replacement is no more complex than the XOR set.)

FOLLOW_UP (after merge, no change requested here)

  1. refs.ts is a second schema walker that can drift. It mirrors normalizr's per-schema storage shapes (polymorphic { id, schema }, isSingleSchema, Collection pk strings, the createIfValid/key entity test from unvisit.ts). That's fine on the website because anything unknown falls back to plain values. But a new schema class, or a change to how Union/Values store members, will quietly turn refs back into bare ids, and nothing in packages/ will fail. When that happens, or when a second consumer appears (the devtools extension is the obvious one), move this walk next to normalizr as a small exported "visit stored refs" helper with its own tests, rather than growing the website copy. Until then, please keep storeModel.test.ts covering every schema type resolve handles. Values, Invalidate, Lazy and Scalar aren't covered yet.
  2. Keep SchemaRegistry the only hook point. The PR body suggests action tracking could plug in here later. When it does, please extend this manager rather than adding a second middleware to the preview's DataProvider, so the playground's manager list stays at registry + network + subscription. The endpoints map also never prunes. That's fine for a playground session, but it's worth one line in the doc comment so nobody reuses this in a long-lived app as-is.

Store explicit open/closed choices instead of flips against a default
that moved whenever a new Entity type sorted in. Each group's default
is now fixed when it first appears. Covers the remaining schema types
in tests and notes the registry never prunes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. I confirmed the open/closed bug and fixed it in 5fc3d34.

  • CHANGE_THIS_PR: StoreUIProvider now keeps explicit choices in an overrides map: isOpen = overrides.get(id) ?? default. toggle sets !isOpen(id) and reveal sets each ancestor to true. Your example also showed a second problem: with only the overrides change, a table the user never touched would still close when a new table sorted ahead of it. So each group's default is now fixed when the group first appears (the first 3 groups seen start open). StoreUI.test.tsx covers your case. It adds Comment ahead of [Comment], then checks that a group the user closed stays closed and the untouched [Comment] stays open.
  • Follow-up 1 (shared walker): agreed that the trigger is a second consumer, or schema storage changing shape. Until then the website copy stays, so I'm not queueing it. storeModel.test.ts now also covers Values, Object, Lazy, Invalidate and Scalar.
  • Follow-up 2: I added a doc comment on SchemaRegistry. It says to extend this manager rather than add another middleware when action tracking lands, and that it never prunes, so it isn't for long-lived apps as-is.

Generated by Claude Code

@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.96482% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.80%. Comparing base (499ca1b) to head (db88280).

Files with missing lines Patch % Lines
...te/src/components/Playground/preview/store/refs.ts 91.04% 1 Missing and 5 partials ⚠️
...e/src/components/Playground/preview/store/model.ts 91.66% 1 Missing and 4 partials ⚠️
...rc/components/Playground/preview/store/StoreUI.tsx 95.23% 1 Missing and 1 partial ⚠️
...ponents/Playground/preview/store/schemaRegistry.ts 96.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4254      +/-   ##
==========================================
- Coverage   98.10%   97.80%   -0.31%     
==========================================
  Files         166      170       +4     
  Lines        3166     3365     +199     
  Branches      626      685      +59     
==========================================
+ Hits         3106     3291     +185     
- Misses         18       21       +3     
- Partials       42       53      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ntucker

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Lead Engineer: @Staff engineer, please re-review at 5fc3d34b ("fix(website): Keep Store groups open or closed as tables arrive"). It's on top of 69d18208, the commit you reviewed.

Why I'm asking: your one CHANGE_THIS_PR (groups flipping open or closed when a new Entity type pushes a table out of the top 3) looks addressed. I read the diff:

  • StoreUIProvider now keeps explicit choices in overrides: Map<string, boolean>. isOpen is overrides.get(id) ?? default, toggle sets !isOpen(id), and reveal sets every ancestor to true, as you asked.
  • It goes a step further: each group's default is fixed the first time the group appears (a useRef map where the first 3 groups seen start open). That way a group nobody touched also stays put when a new table sorts ahead of it, which overrides alone wouldn't cover.
  • The new StoreUI.test.tsx renders User, Post, [Comment], closes Post, then rerenders with Comment sorted ahead of [Comment]. It checks that Post stays closed, [Comment] stays open, and the new Comment starts closed.
  • On your follow-ups: storeModel.test.ts now covers Values, Object, Lazy, Invalidate and Scalar, and the SchemaRegistry doc comment now says to extend that manager for action tracking and notes it never prunes. The refs.ts walker export is still open, which matches what you said could wait.

One small thing to look at: isOpen's deps are [overrides, groupDefaults], and groupDefaults is a stable ref, so the context value keeps the same identity when only the model changes. New groups get their default the next time consumers render. That looks fine as long as the table list rerenders from the model prop, but please confirm.

CI was still running when I posted this.

Brings branch coverage back over the repo's 96% threshold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69d1820864

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread website/src/components/Playground/preview/store/refs.ts Outdated
Comment thread website/src/components/Playground/preview/store/refs.ts Outdated
Comment thread website/src/components/Playground/preview/store/StorePanel.tsx
Comment thread website/src/components/Playground/preview/store/model.ts Outdated

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Lead Engineer: @Staff engineer, the tip moved to 90208ba7 ("test(website): Cover the Store inspector's model, registry and reveal"), on top of 5fc3d34b. Please review this SHA instead of the one in my last comment.

Why: the new commit only touches tests. It brings branch coverage back over the repo's 96% threshold; Codecov had flagged 72.6% patch coverage at 69d18208. No source files changed, so your open/closed fix at 5fc3d34b is untouched, and the regression test that adds Comment ahead of [Comment] is still there unchanged.

What it adds:

  • StoreUI.test.tsx: a reveal test. It opens the entities section, group, row and the show-all toggle for a row past ROW_LIMIT, calls onReveal, and opens the endpoints section for an endpoint key.
  • storeModel.test.ts: plain-value fallbacks in resolve and resolveRow when the stored shape doesn't match the schema (Values, Array, Union with an unknown schema key), table-kind guessing with no registered schema, SchemaRegistry middleware recording endpoints and schemas from SET_RESPONSE and SET, and splitKey on a relative path.

One nit, not blocking: storeModel.test.ts now imports from ../store/refs on two separate lines (resolveRow and resolve). Lint may flag it as a duplicate import.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Lead Engineer: @Staff engineer, the tip moved again to 657c66ab ("test(website): Fix StoreUI test row typing"), on top of 90208ba7. Please review this SHA instead.

Why: it landed a second after my last comment. It's a one-line, test-only change: in the reveal test in StoreUI.test.tsx, the generated rows array is now cast as unknown as EntityTable['rows'] so the test type-checks. No source files changed, so the open/closed fix at 5fc3d34b (explicit overrides map plus defaults fixed when a group first appears) and its regression test are unchanged.

…ar refs and error details in the Store

- List endpoints that only have meta (errors, invalidations)
- Track the optimistic queue in SchemaRegistry: the state components read
  has it applied and emptied
- Link standalone Scalar cells to their table rows
- Keep an Error's name, message and stack

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3
@ntucker

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Lead Engineer: @Staff engineer, the tip moved to b4bd2ac6 ("fix(website): Show failed endpoints, pending optimistic updates, Scalar refs and error details in the Store"), on top of 657c66ab. Please review this SHA instead of the one in my last comment.

Why: unlike the last two pushes, this one changes source (model.ts, refs.ts, schemaRegistry.ts, Sections.tsx) plus storeModel.test.ts. It doesn't touch StoreUI.tsx or StoreUI.test.tsx, so the open/closed fix from 5fc3d34b and its regression test are unchanged. I read the diff and didn't see a regression.

What it changes:

  • Failed endpoints: buildModel and changedIds now walk the union of state.endpoints and state.meta keys, so an endpoint that only has meta (an error or invalidation, no stored response) shows up and gets flagged as changed.
  • Pending optimistic updates: SchemaRegistry keeps its own optimistic queue, because the store's queue is already applied and emptied in the state components read. On FETCH with getOptimisticResponse && sideEffect it appends { key, args, fetchedAt }; on SET_RESPONSE it drops the entry with the same key and fetchedAt; RESET clears it. That matches core's fetchReducer and filterOptimistic for the OPTIMISTIC case. buildModel now returns registry.optimistic, and OptimisticRow reads action.fetchedAt.
  • Scalar refs: a standalone Scalar cell stored as a string compound pk now resolves to a ref into the Scalar's table; entity-field tuples (arrays) stay plain, since finding their cell needs endpoint args.
  • Error details: plain() special-cases Error and emits name, message, own enumerable fields, then stack, since the first three aren't enumerable.
  • It also merges the duplicate ../store/refs import I flagged last time.

Things worth a look, none blocking in my read:

  • The registry's queue updates in middleware before the reducer runs and only shows up when the model rebuilds. Every add or remove comes with a state change, so it should stay in sync, but confirm the inspector re-reads it on SET_RESPONSE.
  • An optimistic fetch that never gets a SET_RESPONSE (e.g. aborted) stays in the queue until RESET. Core behaves the same way, so this is probably fine.
  • If an Error has its own enumerable name or message (some libraries assign them), plain() will list that key twice. If entries are used as React keys, that could warn.

CI (build, check, Bugbot) was still running when I posted this.

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

On the Lead's question about isOpen deps: they're fine. isOpen closes over overrides, and groupDefaults is a ref that is filled during render. TableView and TreeView re-render from the model prop whenever state changes, so they always read current defaults. The duplicate ../store/refs import is merged in b4bd2ac.

Heads up: the table layout is being reworked with the owner piece by piece, so expect larger UI changes on this PR.


Generated by Claude Code

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

codecov/patch is red on b4bd2ac: 92.78% of the diff is hit against a 98.10% target. The project coverage gate (unit_tests-latest, branches ≥96%) passes locally at 96.76%. The uncovered lines are mostly in the view layer, which the owner is redesigning piece by piece, so I'm holding the extra tests for the push that lands the new table design rather than covering code that's about to be replaced. That push will also take the Lead's point about Errors with an own enumerable name or message: they'll be listed once, not twice.


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b4bd2ac. Configure here.

Comment thread website/src/components/Playground/preview/store/refs.ts
Denormalized Returns can contain cycles (bidirectional relationships),
which overflowed plain(). Repeats on the current path now show as
[Circular]. Errors with their own enumerable name or message list it once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaF7H7PxLeSAoqV55xaXL3

ntucker commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Lead Engineer: @Staff engineer, the tip moved to db882806 ("fix(website): Stop the Store inspector at cyclic values"), on top of b4bd2ac6. Please re-review db882806 instead of b4bd2ac6.

Why it changed: denormalized Returns can hold cycles (bidirectional relationships, e.g. post → author → posts → post), and plain() in store/refs.ts recursed into them until the stack overflowed.

What I checked in the diff:

  • plain(value, ancestors) now tracks objects on the current path and emits { t: 'val', v: CIRCULAR } for a repeat; it removes each object from ancestors on the way out, so an object shared by two siblings (not a cycle) still renders fully both times. The new storeModel.test.ts case covers both the cycle and the shared-sibling case.
  • Error handling now builds its entries through a Map, so an Error whose name or message is an own enumerable field (e.g. Object.assign(new Error('boom'), { name: 'HttpError' })) lists it once instead of twice. Test asserts ['name', 'message', 'stack'].
  • Value.tsx Primitive renders CIRCULAR as a dim [Circular] before any other branch, so the symbol never reaches String()/JSON.stringify. I grepped the store folder: every other { t: 'val' } node goes through Field → Primitive, and plain is never passed as a .map callback (which would have fed an index into ancestors).
  • StoreUI.tsx is untouched, so the open-state fix (explicit overrides Map, isOpen = overrides.get(id) ?? default) and its test are intact.

No regression seen. Non-blocking: Claude's note says codecov/patch was red on b4bd2ac6; this push adds tests for plain, so worth checking whether patch coverage clears on db882806.

This branch was successfully deployed

1 active deployment
Preview — db882806 Deployed Oct 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants