Skip to content

feat: pretty-print JSON on arrival and make headers selectable - #10

Merged
tranphucbol merged 1 commit into
mainfrom
feature/pretty-json-and-selectable-headers
Sep 11, 2026
Merged

tranphucbol merged 1 commit into
mainfrom
feature/pretty-json-and-selectable-headers

Conversation

@tranphucbol

@tranphucbol tranphucbol commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

1. Purpose & Motivation

Description

Two things in the response pane cost the reader a step they should not have to take.

Every API preman is pointed at returns minified JSON, and the pretty-print toggle was one click away on every single response. A default in the wrong place. A body the toggle would have formatted is now formatted when it arrives.

Response header rows were also unreadable in two ways: a name and its value sit at opposite ends of a wide pane with nothing tying them together once the value wraps, and the app-wide select-none in app.css meant a header value could not be copied at all.

Context & Flow

response-body event → BodyViewer mounts (keyed on body.handle)
        │
        ├── [OLD] paints viewText(view) raw
        │            └── reader clicks the format toggle → engine "body-format" → pretty
        │
        └── [NEW] paints viewText(view) raw, and in the same tick asks formatAvailability
                     │
                     ├── not allowed (non-JSON, or over 2MB) → stays raw, toggle disabled with a reason
                     │
                     └── allowed → engine "body-format"
                                     │
                                     ├── ok → pretty, gutter on, range strip hidden
                                     └── failed → stays raw, silently; the toggle still reports

2. Key Changes

Description

BodyViewer gains one effect that requests the formatted body on mount when formatAvailability allows it, guarded by a ref so it fires once per response. The viewer is already keyed on body.handle, so a new response is a new decision, and turning the toggle back off does not hand the old one a second chance.

The toggle's tooltip is now state-dependent. It is lit before anyone has touched it, so while pretty it reads "Show the raw body" rather than "Pretty-print".

Response header rows in ResponsePane take hover:bg-hover and select-text. The band is what the eye follows from a name across the gap to a value that wraps to three lines, which access-control-allow-headers usually does.

Technical Decisions

The automatic pass reuses the toggle's own predicate and its own BODY_FORMAT_LIMIT_BYTES of two megabytes rather than a second, lower limit. A lower one is defensible on cost, since two megabytes now gets formatted, sent over the port and given a syntax tree without anyone asking. It was rejected for what it looks like from the outside: between the two numbers the reader finds a toggle that is offered, enabled, and did not fire, and no tooltip explains that state.

A JSON body therefore paints twice, raw then formatted a round trip later. Gating the first frame on a round trip would have cost every response, including every response that turns out not to be JSON, to avoid a repaint the body-enter fade mostly covers.

The automatic pass swallows its own errors where the click reports. The reader did not ask for it, so a red line under the toolbar would be the app complaining about something they did not do.

Rejected alternatives, recorded in full in ADR 053: a preference in Preferences, which holds things where two people genuinely want different answers and this is not one; and formatting in the renderer, which blocks the main thread past the interaction budgets and puts a second formatter next to the engine's.

select-text is a deliberate exception to the app-wide select-none. docs/design-system.md enumerated exactly two such exceptions, so that count is updated rather than left stale.

Critical Modules/Files

  • packages/desktop/src/renderer/panes/BodyViewer.tsx — the new effect. The ref guard and the handle key are what stop it looping or fighting the reader's toggle. Worth checking that a body arriving after the pane moved on cannot set state on the wrong response.
  • docs/decisions/053-a-json-body-arrives-pretty-printed.md — this narrows ADR 013 by name. The pane's "it will not hold the body" is now true with a stated exception rather than absolutely.
  • packages/desktop/src/renderer/panes/ResponsePane.tsx — small, but it changes an app-wide interaction rule for one table.

Recommended Review Order

  1. docs/decisions/053-a-json-body-arrives-pretty-printed.md — the decision and what it cost
  2. packages/desktop/src/renderer/panes/BodyViewer.tsx — the effect and the toggle label
  3. packages/desktop/src/renderer/model/body.ts — formatAvailability now answers two questions
  4. packages/desktop/src/renderer/panes/ResponsePane.tsx — the header rows
  5. test/renderer/response.test.ts — the truncated-body precondition
  6. docs/design-system.md, docs/decisions/README.md

3. Verification & Testing

Provide reproducible inputs or steps for each test case, followed by the result and supporting evidence.

Driven against a throwaway HTTP server on localhost returning minified JSON, HTML, and a 2,556,900-byte JSON body, from a scratch workspace in a temp directory. Nothing in these images is a real service or a real credential.

  • A JSON response arrives pretty-printed without clicking the format toggle, with line numbers and no range strip

    A 515-byte minified body, sent once and not touched afterwards. It is indented, the gutter is numbering real lines, and the format toggle is already lit.

    01-json-arrives-pretty.png

  • The format toggle turns a pretty body back to the raw bytes the server sent, and its tooltip reads "Show the raw body" while lit

    The same response after one click on the lit toggle: one line, no gutter, exactly what was on the wire. The accessibility tree reports the button as "Show the raw body" [pressed] while pretty and "Pretty-print" after the click.

    02-toggle-back-to-raw.png

  • A non-JSON response (HTML or plain text) arrives raw with the toggle disabled and a tooltip naming the reason

    text/html, left exactly as sent. The toggle's aria-label is Only JSON is pretty-printed. and it carries disabled.

    03-html-stays-raw.png

  • A JSON body above the 2MB limit arrives raw with the toggle disabled and a tooltip naming the size

    2.4 MB of JSON. Nothing was formatted, the range strip is back, and the windowing still reads 0 B – 64.0 KB of 2.4 MB. The disabled toggle says Too large to pretty-print (2.4 MB). The limit is 2.0 MB - search it instead.

    04-over-limit-stays-raw.png

  • Response header rows show a hover band across the full row width under the pointer

    The pointer is on access-control-allow-headers, which is the row the band is for: the value is long and the name is far away on the left.

    05-header-hover-band.png

  • A header name and value can be drag-selected and copied

    A drag across the strict-transport-security value. The highlight is visible, and window.getSelection().toString() returned max-age=15724800; includeSubDomains, max-age=31536000 — before this change the app-wide select-none made that string empty.

    06-header-value-selected.png

  • bun run typecheck, lint, format:check and test are clean (1954 passed)

    All four clean. One caveat worth knowing: givenThousandRequestWorkspace_whenBuildCatalog_thenUnderFourHundredMs is a wall-clock budget and fails in a full run if anything else is loading the machine — it passed once the Electron app used for these screenshots was closed. It passes in isolation in 7.5s.

4. Impact and Scope

Renderer only. Nothing under packages/core/ or packages/cli/ changes, so the CLI's output is untouched and no engine contract moves — body-format was already in the protocol and is now called on mount as well as on click.

Performance: the renderer now holds up to two megabytes of a body it was not asked to hold, on every JSON response, where before it held at most VIEWER_RETAINED_BYTES. The cap is the same cap the reader could always have reached with one click. Each JSON response adds one IPC round trip and one repaint.

A truncated JSON body under the limit costs one wasted 64KB window: the format request and window zero go out in the same tick and the format wins. Suppressing it would mean the window loader knowing what the formatter is about to do, and that coupling costs more than the bytes.

One test added, covering the precondition the effect depends on — that a truncated JSON body is decided at seed time, before any bytes arrive, because languageFor reads the content type rather than sniffing an empty sample.

No API, schema, configuration or security surface is affected.

The pretty-print toggle was one click away on every response, and every API
preman is pointed at returns minified JSON. A body the toggle would format is
now formatted when it arrives, bounded by the toggle's own two-megabyte limit
rather than a second threshold that would leave the control offered, enabled
and silent. ADR 053 records the repaint that costs and what it narrows in 013.

Response header rows take a hover band, because a name and its value sit at
opposite ends of a wide pane and a wrapped value leaves nothing tying them
together, and `select-text`, because a header value is a string the reader
wants in their clipboard.
@tranphucbol
tranphucbol merged commit 439364a into main Sep 11, 2026
3 checks passed
@tranphucbol
tranphucbol deleted the feature/pretty-json-and-selectable-headers branch September 11, 2026 18:05
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