Skip to content

feat(client-devtools): close the gap to the panel design - #102

Merged
juicycleff merged 2 commits into
mainfrom
feat/devtools-panel-parity
Sep 8, 2026
Merged

juicycleff merged 2 commits into
mainfrom
feat/devtools-panel-parity

Conversation

@juicycleff

Copy link
Copy Markdown
Contributor

The last pass shipped the structural half and I called it finished, which it
was not. This is the rest of it, or nearly.

The launcher grew the two things it was drawn with. Vitals slide out on hover,
so the resting state is still one tile, and pend only appears when there is
something pending: a permanent 0 pend teaches you to stop reading it. The
ring goes amber while the overlay stack is non-empty. A stuck optimistic write
is a bug you want to see and a request in flight is Tuesday, so pending
outranks fetching and a real error outranks both.

It also gets out of the way now. If a framework's dev badge already owns the
bottom-right corner the launcher lifts itself above it, which is the collision
that started this whole piece of work. Matched by element name rather than by
position, because measuring layout on mount is how a devtools panel starts
causing the reflows it exists to help you find. An explicit offset beats
anything it guessed.

The trace flags near misses now, and here is the threshold. Per raised tag, not
per cause. A tag that reached no query and closely resembles one somebody
carries gets a banner even when the same cause reached something through a
different tag, because the tag that missed is still wrong and the one that
landed is what hides it. orderCreate invalidating Order:{res.id} against a
list carrying Order[] is the case, and it is the defect this package exists
to explain. A tag that reached nothing and resembles nothing stays quiet.

The vitals strip is back to what StoreSnapshot actually holds. Tombstones and
stamped tags are both bounded caches, so a number that climbs is the shape of a
leak, and neither was visible anywhere.

Network rows draw a bar. Two segments and only two, wire and backoff, because
those are the two the transport measures. It does not report when a response
started decoding or when the store committed it, and a bar that split those out
would be drawing a shape nobody measured.

You can act on the overlay stack: roll a layer back, promote one by hand, and
read the base record beside what the fold makes of it, changed fields only.

Holding a query in loading or error is panel-local and writes nothing. The row
says held, the pane says held, the trace says you did it, and release is a map
delete. A faked state has to be impossible to mistake for a real one and
reversible without the cache unwinding anything.

Keyboard: escape, 1 to 9, cmd-shift-F, f for freeze, space, and r and i for the
selected row. Nothing fires while you are typing. A panel that eats a 3 out of
its own filter box is worse than one with no shortcuts.

Every query pane has a why button that opens Explain already pointed at it,
instead of asking you to type an exact key from memory into a filter box.

The panel opened on the queries tab while the tab list started with the trace.
A test caught it. It opens on the trace now.

Budgets re-baselined once rather than nudged a fourth time. The reasoning is in
the test: the panel is one closure over shared UI state and there is nothing a
consumer could decline to import, so splitting would cost more than it saves.
The parts that could be declined were split out already and they are
./requests and ./control.

Still missing, and deliberately: copy as curl and replay, which need headers
and a transport reference the request log does not keep on purpose; per-phase
timings, which need more transport events; a command palette, which duplicates
the filter box.

The last pass shipped the structural half and I called it finished, which it
was not. This is the rest of it, or nearly.

The launcher grew the two things it was drawn with. Vitals slide out on hover,
so the resting state is still one tile, and `pend` only appears when there is
something pending: a permanent `0 pend` teaches you to stop reading it. The
ring goes amber while the overlay stack is non-empty. A stuck optimistic write
is a bug you want to see and a request in flight is Tuesday, so pending
outranks fetching and a real error outranks both.

It also gets out of the way now. If a framework's dev badge already owns the
bottom-right corner the launcher lifts itself above it, which is the collision
that started this whole piece of work. Matched by element name rather than by
position, because measuring layout on mount is how a devtools panel starts
causing the reflows it exists to help you find. An explicit `offset` beats
anything it guessed.

The trace flags near misses now, and here is the threshold. Per raised tag, not
per cause. A tag that reached no query and closely resembles one somebody
carries gets a banner even when the same cause reached something through a
different tag, because the tag that missed is still wrong and the one that
landed is what hides it. `orderCreate` invalidating `Order:{res.id}` against a
list carrying `Order[]` is the case, and it is the defect this package exists
to explain. A tag that reached nothing and resembles nothing stays quiet.

The vitals strip is back to what StoreSnapshot actually holds. Tombstones and
stamped tags are both bounded caches, so a number that climbs is the shape of a
leak, and neither was visible anywhere.

Network rows draw a bar. Two segments and only two, wire and backoff, because
those are the two the transport measures. It does not report when a response
started decoding or when the store committed it, and a bar that split those out
would be drawing a shape nobody measured.

You can act on the overlay stack: roll a layer back, promote one by hand, and
read the base record beside what the fold makes of it, changed fields only.

Holding a query in loading or error is panel-local and writes nothing. The row
says held, the pane says held, the trace says you did it, and release is a map
delete. A faked state has to be impossible to mistake for a real one and
reversible without the cache unwinding anything.

Keyboard: escape, 1 to 9, cmd-shift-F, f for freeze, space, and r and i for the
selected row. Nothing fires while you are typing. A panel that eats a 3 out of
its own filter box is worse than one with no shortcuts.

Every query pane has a why button that opens Explain already pointed at it,
instead of asking you to type an exact key from memory into a filter box.

The panel opened on the queries tab while the tab list started with the trace.
A test caught it. It opens on the trace now.

Budgets re-baselined once rather than nudged a fourth time. The reasoning is in
the test: the panel is one closure over shared UI state and there is nothing a
consumer could decline to import, so splitting would cost more than it saves.
The parts that could be declined were split out already and they are
./requests and ./control.

Still missing, and deliberately: copy as curl and replay, which need headers
and a transport reference the request log does not keep on purpose; per-phase
timings, which need more transport events; a command palette, which duplicates
the filter box.
@vercel

vercel Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated
forge Ready Ready Preview Sep 8, 2026 12:35pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Conventional Commits Validation

PR Title: valid
Commits: all 1 follow conventional format

The last of it. What is left out at the end is left out for a reason, not
because I ran out of road.

Editing an entity field pushes a patch onto the overlay stack. This is the
argument the whole state-manipulation section rested on and it was the one
piece of it I had not built. A devtools edit now rides the same machinery a
pending optimistic mutation does, so it never writes the base store, undo is
removing the entry rather than applying an inverse, and an evicting stream
frame takes it with it exactly as it would take a real optimistic write. A
merge over a base record that is gone patches nothing, and that rule was
already in OverlayStack.

RestTransport closes the credential refresh now. `refresh` had no matching
event, so a watcher could say a request hit the refresh but not how long it sat
there, which is the only number that tells an auth stall apart from a slow
server. The waterfall draws that wait in violet, beside the wire and the
backoff.

Force stale marks one query behind the server without asking for it again.
Invalidate raises its tags and reaches everything sharing them; this reaches
one, and leaves the request for whenever something next needs it. That is the
state you want when you are trying to see what a stale read looks like.

Curl on the request pane. The credential is a placeholder because the log never
keeps the real one, which is the reason it is cheap enough to leave running.

The shortcuts are finished: slash and cmd-K into the filter box, cmd-Z pops the
top overlay, F11 for fullscreen, enter explains the selected row. The mock drew
a command palette and it is a filter box instead, because that box already
parses status:4xx, >100ms and tag:, and a palette would be a second front door
to the same room.

Budgets: the inspection entry went to 6.5 kB and the combined one to 10 kB. The
action layer grew from six calls to eleven, and the panel needs all five of the
new ones.

Three things are still not here. Replay needs a transport reference the log
does not hold. Mocking a response needs the same. Queued, decode and commit
timings need boundaries nobody reports: they happen in the generated client and
in the cache, not in the transport, so drawing them would mean inventing a
shape nobody measured.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Conventional Commits Validation

PR Title: valid
Commits: all 2 follow conventional format

@github-actions github-actions Bot added feature and removed feature labels Sep 8, 2026

// JSON first so a number stays a number, falling back to the raw string
// for the common case of typing a word rather than a quoted one.
let parsed: unknown = value.value;
@juicycleff
juicycleff merged commit a4c5975 into main Sep 8, 2026
35 checks passed

This branch was successfully deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant