Skip to content

fix(tui): cap rendered request/response body lines - #2480

Merged
cliffhall merged 2 commits into
v2/mainfrom
v2/fix/2407-tui-body-line-cap
Sep 24, 2026
Merged

cliffhall merged 2 commits into
v2/mainfrom
v2/fix/2407-tui-body-line-cap

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2407

What changed

The TUI rendered an HTTP request/response body as one Ink <Box> per pretty-printed line, with no limit on the number of lines. The same code existed in four places: the App details view (App.tsx) and the Requests tab (RequestsTab.tsx), each for the request body and the response body. A large tool result became thousands of components in one render pass.

  • clients/tui/src/utils/bodyLines.ts (new, pure): layoutBody() pretty-prints JSON (anything else passes through as is), keeps the first 500 lines, clips each line to 2,000 characters, and reports how much it cut.
  • clients/tui/src/components/BodyLines.tsx (new): renders that layout, followed by … N more lines not shown (M total) when lines were dropped. All four call sites now use it, replacing four copies of the same inline code.

Why there's also a per-line cap: an embedded resource, one of the payloads the issue names, pretty-prints as a single enormous base64 line. A line-count cap alone does nothing for that, so the line is cut and marked … (+N chars).

The issue's "e.g." also mentions a "show more" affordance. I left that out: the cap with an explicit note is what bounds the render, and "show more" would be a new interaction (a keybinding plus paging state) beyond what the bug needs. The one other change in behavior is that a non-JSON body now renders line by line through the same capped path, where before it was a single uncapped <Text>.

Evidence

A frame from the real RequestsTab, rendered with a 20,000-character argument in the request and a 5,000-file listing (20,008 pretty-printed lines) in the response. It's the Ink frame as text, which is the TUI's actual output. The … (…) … marker lines were added when excerpting.

                                           │
 Network (1)                               │ POST http://localhost:3000/mcp
                                           │
 ▶ MCP  POST 200 42ms                      │ Category: transport
                                           │
                                           │ Status: 200 OK
                                           │
                                           │ 9:00:00 PM (42ms)
                                           │
                                           │ Request Headers:
                                           │   content-type: application/json
                                           │
                                           │ Request Body:
                                           │
                                           │   {
                                           │     "jsonrpc": "2.0",
   … (blob line continues, wrapped) …
                                           │   DQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQ
                                           │   UJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQUJDQ…
                                           │   (+18020 chars)
                                           │       }
                                           │     }
                                           │   }
                                           │
                                           │ Response Body:
                                           │
                                           │   {
                                           │     "jsonrpc": "2.0",
                                           │     "id": 7,
                                           │     "result": {
                                           │       "files": [
   … (response lines 7-495) …
                                           │           "name": "file-122.txt",
                                           │           "size": 122
                                           │         },
                                           │         {
                                           │           "name": "file-123.txt",
                                           │           "size": 123
                                           │   … 19508 more lines not shown (20008 total)
                                           │

Before this change, the same response rendered all 20,008 lines as separate <Box> components.

Tests

clients/tui/__tests__/BodyLines.test.tsx covers the util (JSON vs raw, line cap, per-line clip, caller-supplied caps) and the component (no note under the cap; exactly the cap plus an accurate note over it). Both new files are at 100% on all four coverage dimensions. The existing RequestsTab and App tests cover the swapped call sites, including the non-JSON fallback. npm run local:gate passes.

🤖 Generated with Claude Code

The TUI rendered an HTTP body as one Ink <Box> per pretty-printed line,
with no bound, in four copies (App.tsx details and RequestsTab, request
and response each). A large tool result became thousands of components
in one render pass.

Add a pure layoutBody() in utils/bodyLines.ts that pretty-prints JSON,
caps the body at 500 lines and each line at 2000 characters, and reports
what it cut. Render it through one BodyLines component used by all four
sites; the note under a truncated body says how many lines were hidden.
The per-line character cap is needed because an embedded resource
pretty-prints as a single enormous base64 line that a line cap alone
leaves untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Sep 24, 2026
@cliffhall
cliffhall requested a balanced review from Copilot September 24, 2026 05:07

Copilot AI 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.

Copilot review overview

🟢 Approval recommended

The bounded shared renderer addresses the reported component explosion and has appropriate focused coverage.

Review effort: Balanced
Findings: None

What changed in this PR

Bounds TUI request/response body rendering to prevent large payloads from overwhelming Ink.

Changes:

  • Adds shared formatting with line-count and per-line limits.
  • Replaces four duplicated render paths with BodyLines.
  • Adds focused utility and component tests.
File Description
clients/​tui/​src/​utils/​bodyLines.ts Formats and bounds body content.
clients/​tui/​src/​components/​BodyLines.tsx Renders bounded lines and truncation notices.
clients/​tui/​src/​components/​RequestsTab.tsx Uses the shared body renderer.
clients/​tui/​src/​App.tsx Uses the shared renderer in request details.
clients/​tui/​__tests__/​BodyLines.test.tsx Tests formatting, limits, and rendering.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review closed after round 1: the round was clean. No inline comments, the headline reports "Findings: None", and there's no Suppressed comments block. Per pr-flow step 7c, one clean round ends the loop.

@cliffhall
cliffhall merged commit b436906 into v2/main Sep 24, 2026
4 checks passed
@cliffhall
cliffhall deleted the v2/fix/2407-tui-body-line-cap branch September 24, 2026 05:28
@cliffhall cliffhall linked an issue Sep 24, 2026 that may be closed by this pull request
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

2 participants