Collapse very long lines in response viewers - #532
Conversation
A response body on one enormous line, typically base64 image data in JSON, stalls the UI. Cost tracks the longest line rather than the document size, because soft wrap has to measure a line end to end to find its break points. Read-only editors now collapse over-long lines to a placeholder that opens the full value in a paged dialog. Tokens over 5k chars collapse individually where there is a grammar, so a minified body keeps its keys visible; anything still past column 10k is collapsed too, which needs no grammar. The document is untouched, so copy, filter and save still see the full text. Also replaces the whole-document md5 behind the editor state cache with a sampled fingerprint. It ran on every editor update and again on restore. Measured in WKWebView, per render pass: 1MB single line 221ms -> 51ms 3MB single line 968ms -> 138ms 3MB switch away and back 265ms -> 37ms
Greptile SummaryThe PR collapses exceptionally long response lines in read-only CodeMirror viewers and replaces full-document cache hashing with mode-sensitive fingerprints.
Confidence Score: 3/5The PR is not yet safe to merge because keyboard users still cannot activate collapsed values and read-only document updates can remain undoable after the editor becomes editable. The copy control is still a non-focusable span without keyboard activation, and read-only mode is reconfigured without clearing history while programmatic content replacements remain recorded, leaving both previously reported failures outstanding. Files Needing Attention: apps/yaak-client/components/core/Editor/largeValues.ts and apps/yaak-client/components/core/Editor/Editor.tsx
|
| Filename | Overview |
|---|---|
| apps/yaak-client/components/core/Editor/largeValues.ts | Implements long-line replacement decorations and hidden-text copying, but the previously reported keyboard-access failure remains. |
| apps/yaak-client/components/core/Editor/Editor.tsx | Integrates mode-sensitive cache fingerprints; exact editable fingerprints fix stale cache restoration, while the previously reported in-memory read-only history issue remains. |
| apps/yaak-client/lib/docFingerprint.ts | Adds exact and sampled fingerprint modes, with editable editors now protected by a full-document hash. |
| apps/yaak-client/components/core/Editor/extensions.ts | Enables large-value collapsing only through the read-only extension compartment. |
| apps/yaak-client/components/core/Editor/largeValues.test.ts | Covers token and column collapsing, document preservation, multiline behavior, and recomputation. |
| apps/yaak-client/lib/docFingerprint.test.ts | Documents and tests the collision properties of sampled fingerprints and the guarantees of exact mode. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Read-only editor receives response] --> B{Line over 10k characters?}
B -- No --> C[Render normally]
B -- Yes --> D[Collapse quoted tokens over 5k]
D --> E[Collapse remaining content past visible limit]
E --> F[Render placeholder with copy action]
G[Editor state update] --> H{Editable?}
H -- Yes --> I[Store exact document fingerprint]
H -- No --> J[Store sampled document fingerprint]
Reviews (6): Last reviewed commit: "Make the tag action styling generic" | Re-trigger Greptile
- Quoted values now collapse whole, so a line reads `"image": "999.8 KB hidden…"` with its key and quotes intact, and the dialog shows the bare value - Undelimited tokens fall back to the column cut. The text grammar parses a whole line as one token, and collapsing that left nothing visible at all - Tag shares .template-tag's shape via a common selector, with its own colors, and is named large-value-tag to match existing widget classes - Label uses formatSize with the exact character count in the tooltip
Paging through a megabyte of base64 was the least useful thing the dialog could offer, and it is the part a richer viewer would replace anyway. The tag now carries a copy button instead, which copies exactly what the tag hides and toasts on success. Clicking the tag itself does nothing. The icon is Lucide's `copy` inlined as SVG rather than the React component, because the widget builds its DOM synchronously and a React root would leave it empty while CodeMirror measures line heights.
The multi-value test assumed parsing always finishes inside PARSE_TIMEOUT_MS. On a slower runner it doesn't, the column cut takes over as designed, and the test failed on CI. Shrunk the body so the parse completes either way; the fallback itself is still covered by the plain-text tests. Editable documents now get an exact document hash. A sampled fingerprint collision there would restore undo history belonging to other content, which an undo could then write into the body. Read-only documents keep the sampled hash, since that is where the megabyte-sized bodies are, their history can never be applied, and a collision can only misplace a fold or the cursor. Both cases are now pinned by tests, including the same-length change sampling deliberately misses. The copy button also acts on click rather than mousedown, so it works when reached by keyboard and not just by mouse.
A response can hold many of these, and Tab is already contested in the editor. Nothing is stranded by it: the text never leaves the document, so the pane's own copy and save buttons, or simply selecting the body, still reach everything the tag hides. It stays a button so it keeps a role and an accessible name.
Template tags and path parameters were spans with click listeners, so they had no role or accessible name and nothing could focus them. They are buttons now, matching the large-value tag, and kept out of the tab order for the same reason: one field can hold many, and Tab already means something in the editor. Font and alignment are pinned in CSS rather than relying on preflight, since a button would otherwise pick up the user agent's font inside the editor.
The tag had no theme scope, so its tokens resolved against the editor's ambient palette where a tag-sized border is meant to be near invisible. It now uses the same neutral tag styling a path parameter does, which resolves against the tag palette instead. That makes a dedicated class unnecessary: template-tag is the generic style for these already, with two users before this one. Only the copy icon needs its own rule now, so Editor.css keeps its original .template-tag block untouched. Also reverts the widgets to spans. A button's box model made the line taller, and the role and name it bought can be set on a span instead.
Nothing about it was copy-specific, so it lives inside .template-tag alongside .fn as an icon button any tag can carry, rather than a class of its own.
Reading a model imperatively and writing it back is the ordinary idiom (patchModelById merges into whatever it reads), but the store only advances when the backend echoes a write back. Debouncing patches meant any such read during the pending window saw stale fields. handleRenamePathPlaceholder is the sharp case: it derives the new URL by substituting into the value it read, so a pending URL patch made it write a URL the user never typed while still renaming the parameter, which is the placeholder/value mismatch #528 fixed. useImportCurl has the milder version, where the pending patch flushes afterwards and resurrects the values the import replaced. getModel and getAnyModel now read through the pending patch, and patchModelById consumes it since prev already carries it. The reactive path is deliberately left lagging, since not re-rendering per keystroke is the point. Also: - Correct the editor-state cache comment. #532 only samples read-only documents, so editable ones still pay an exact md5 per update; the full-rope serialize is paid by both. Measured: 0.44 ms/update at 200 KB, 2.3 ms at 1 MB. - Drop the saver from stateSavers on unmount, since a pending one retains the last EditorState and therefore the whole document. - Use the new visibleItems memo for keyboard nav instead of getValidSelectableItems, which duplicated it and rescanned every item on each arrow keypress. - Cover debounce flush/cancel, which sends rely on to not lose edits.
Reading a model imperatively and writing it back is the ordinary idiom (patchModelById merges into whatever it reads), but the store only advances when the backend echoes a write back. Debouncing patches meant any such read during the pending window saw stale fields. handleRenamePathPlaceholder is the sharp case: it derives the new URL by substituting into the value it read, so a pending URL patch made it write a URL the user never typed while still renaming the parameter, which is the placeholder/value mismatch #528 fixed. useImportCurl has the milder version, where the pending patch flushes afterwards and resurrects the values the import replaced. getModel and getAnyModel now read through the pending patch, and patchModelById consumes it since prev already carries it. The reactive path is deliberately left lagging, since not re-rendering per keystroke is the point. Also: - Correct the editor-state cache comment. #532 only samples read-only documents, so editable ones still pay an exact md5 per update; the full-rope serialize is paid by both. Measured: 0.44 ms/update at 200 KB, 2.3 ms at 1 MB. - Drop the saver from stateSavers on unmount, since a pending one retains the last EditorState and therefore the whole document. - Use the new visibleItems memo for keyboard nav instead of getValidSelectableItems, which duplicated it and rescanned every item on each arrow keypress. - Cover debounce flush/cancel, which sends rely on to not lose edits.
Summary
A response body on one enormous line, usually base64 image data in JSON, stalls the UI because Codemirror is not good at handling this case. The response viewers now collapse long lines to a placeholder containing a copy button.
Separately, the editor state cache hashed the whole document on every update and again on restore. That is now a sampled fingerprint, roughly 4ms per MB down to constant.
Related
Known gap
Search still matches inside collapsed ranges, so a Cmd+F hit can highlight something invisible. Not addressed here.