Skip to content

fix(cli, react): honour --json on react/vitals; classify opaque-Promise client suspensions as client-hook#1350

Open
gaojude wants to merge 2 commits into
vercel-labs:mainfrom
gaojude:fix/react-classifier-and-json-flag
Open

fix(cli, react): honour --json on react/vitals; classify opaque-Promise client suspensions as client-hook#1350
gaojude wants to merge 2 commits into
vercel-labs:mainfrom
gaojude:fix/react-classifier-and-json-flag

Conversation

@gaojude
Copy link
Copy Markdown

@gaojude gaojude commented May 13, 2026

Two small fixes for the React features shipped in #1257. Pushed as draft so we can iterate on the description / scope.

1. --json is silently stripped from react and vitals subcommands

File: cli/src/commands.rs

--json is registered as a global boolean in clean_args's GLOBAL_BOOL_FLAGS, so it's removed from the args slice before parse_command sees them. The per-command parsers for react <sub> and vitals then check rest.contains(\"--json\") on the already-stripped args, so the flag silently no-ops and the action returns the formatted report instead of the raw structured payload (boundaries for suspense, the inspector JSON for the others).

The fix reads flags.json — which parse_flags populates from the pre-strip args — in both parsers. The existing rest.contains check is kept as a fallback so the unit tests that call parse_command directly with --json in the args slice continue to work.

Repro before the fix:

```shell
$ agent-browser react suspense --json | jq 'keys'
[
"report" # expected: "boundaries"
]
```

A regression test asserts that with `flags.json = true` and `--json` absent from `rest` (the real call path), all four react subcommands emit `cmd.json = true`.

2. `use(opaquePromise)` in a client component lands on `Unknown` instead of `ClientHook`

File: `cli/src/native/react/suspense.rs`

A client component calling `use(somePromise)` where the promise is an opaque user-created `Promise` (e.g. `fetch().then(r => r.json())` stashed in `useState`) was classified as `BlockerKind::Unknown`. React reports the suspender as `awaited.name = "Promise"` with no other identifying signal in the name, so none of the existing name-based checks fire — yet this is the prototypical "client-hook" scenario the classifier advertises.

The discriminator I landed on is `env`. React tags server-tracked awaits with `env="Server"` (the suspenders named `rsc stream`, `_Response.json`, anonymous fetches, etc. that come back from RSC), while client-side suspensions arrive with `env` unset or `"Client"`. Adding a fallback that reclassifies opaque-Promise suspenders as `ClientHook` when `env` is not explicitly `"Server"` resolves the case without stealing classifications from server-side awaits.

Verified against a Next.js App Router demo with two Suspense boundaries:

Boundary Before After
`` (server component awaiting `fetch`) `rsc stream (stream)` `rsc stream (stream)` (unchanged)
`` (client component awaiting `use(promise)`) `Promise (unknown)` `Promise (client-hook)`

Tests cover: client-side opaque Promise → ClientHook, explicit `env="Client"` Promise → ClientHook, `env="Server"` Promise → Unknown (no regression on server awaits), and existing named-hook handling.

Test plan

  • `cargo test` — 722 passed, 0 failed, 70 ignored (locally on `stable-aarch64-apple-darwin`)
  • End-to-end smoke test against a Next.js 16 / React 19 App Router demo (`use(fetch().then(json))` client component + `await fetch` server component), verified before/after classifications match the table above
  • CI

Out of scope

While verifying, I noticed that `agent-browser react suspense` (no `--json`) prints `✓ Done` instead of the formatted report — the daemon returns `{"report": "..."}` but the response printer doesn't render the `report` key for these subcommands. Separate bug; happy to follow up if you'd like it in this PR or a separate one.

🤖 Generated with Claude Code

gaojude and others added 2 commits May 12, 2026 21:03
`--json` is registered as a global boolean flag in `clean_args`, so it's
stripped from the args before `parse_command` sees them. The per-command
parsers for `react <sub>` and `vitals` then check `rest.contains("--json")`
on the already-stripped args and silently drop the flag — `cmd.json` is
never set, so the action returns the formatted `report` instead of the
raw `boundaries` payload.

Read `flags.json` (populated by `parse_flags` from the pre-strip args) in
both parsers. The `rest.contains` check is kept as a fallback so the
existing unit tests that call `parse_command` directly with `--json` in
the args slice continue to work.

Regression test asserts that with `flags.json = true` and `--json`
absent from `rest` (the real call path), all four react subcommands
emit `cmd.json = true`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A client component using `use(somePromise)` where the promise is an
opaque user-created `Promise` (e.g. `fetch().then(r => r.json())` stashed
in `useState`) was landing on `BlockerKind::Unknown`. React reports the
suspender as `awaited.name = "Promise"` with no other identifying
signal in the name, so none of the existing name-based checks fire — yet
this is the prototypical "client-hook" scenario the classifier advertises.

The discriminator is `env`: React tags server-tracked awaits with
`env="Server"` (the suspenders named "rsc stream", "_Response.json",
anonymous fetches, etc. that come back from RSC), while client-side
suspensions have `env` unset or "Client". Reclassify opaque-Promise
suspenders that aren't explicitly server-side as `ClientHook`.

Verified against a Next.js App Router demo with two Suspense boundaries:
a server component awaiting `fetch()` (still classified as `Stream` /
`ServerFetch` by name) and a client component awaiting `use(promise)`
(was `Unknown`, now `ClientHook`).

Tests cover: client-side opaque Promise -> ClientHook, explicit
`env="Client"` Promise -> ClientHook, `env="Server"` Promise -> Unknown
(no regression on server awaits), and existing named-hook handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 13, 2026

@gaojude is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@gaojude gaojude marked this pull request as ready for review May 13, 2026 01:06
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.

1 participant