Skip to content

Collapse very long lines in response viewers - #532

Merged
gschier merged 8 commits into
mainfrom
claude/hopeful-cannon-1ad43f
Aug 13, 2026
Merged

Collapse very long lines in response viewers#532
gschier merged 8 commits into
mainfrom
claude/hopeful-cannon-1ad43f

Conversation

@gschier

@gschier gschier commented Aug 12, 2026

Copy link
Copy Markdown
Member

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.

image

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.

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
@gschier
gschier marked this pull request as ready for review August 12, 2026 22:38
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Greptile Summary

The PR collapses exceptionally long response lines in read-only CodeMirror viewers and replaces full-document cache hashing with mode-sensitive fingerprints.

  • Adds token-aware and column-based replacement decorations for oversized lines.
  • Adds a copy affordance for hidden ranges and styling for the new widget.
  • Uses exact fingerprints for editable documents and sampled fingerprints for read-only documents.
  • Adds unit coverage for collapsing behavior and fingerprint modes.

Confidence Score: 3/5

The 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

Important Files Changed

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]
Loading

Reviews (6): Last reviewed commit: "Make the tag action styling generic" | Re-trigger Greptile

Comment thread apps/yaak-client/lib/docFingerprint.ts
Comment thread apps/yaak-client/components/core/Editor/largeValues.ts Outdated
- 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.
Comment thread apps/yaak-client/components/core/Editor/Editor.tsx
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.
@gschier
gschier merged commit 0e4c355 into main Aug 13, 2026
9 checks passed
@gschier
gschier deleted the claude/hopeful-cannon-1ad43f branch August 13, 2026 15:40
gschier added a commit that referenced this pull request Aug 13, 2026
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.
gschier added a commit that referenced this pull request Aug 13, 2026
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.
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