feat: Enhance response viewer behavior when receives large response content#9740
feat: Enhance response viewer behavior when receives large response content#9740
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves UI responsiveness when rendering very large response bodies by truncating extremely long individual lines in read-only CodeMirror viewers and providing an inline control to expand them on demand.
Changes:
- Adds a
truncateLongLinesprop toCodeEditorand enables it for response viewers and MCP event preview. - Implements long-line truncation (>10,000 chars) with an inline “Show full value” bookmark widget to restore the full line.
- Tweaks CodeMirror configuration with
maxHighlightLengthto reduce highlight work on long lines.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/insomnia/src/ui/components/viewers/response-viewer.tsx | Enables long-line truncation for JSON/source response viewing. |
| packages/insomnia/src/ui/components/mcp/event-view.tsx | Enables long-line truncation for MCP message previews. |
| packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx | Adds truncation implementation + new prop; adjusts CodeMirror options. |
Comments suppressed due to low confidence (1)
packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx:773
useImperativeHandleis created with an empty dependency array, but the exposedsetValuenow depends onshouldTruncateLongLines(derived from props). IfreadOnly/truncateLongLineschanges for an existing mounted editor, the imperativesetValuewill keep using the initial value and behave incorrectly. IncludeshouldTruncateLongLines(and any other referenced values) in the dependency array, or wrap it via a ref (e.g.useLatest) to avoid stale closures.
useImperativeHandle(
ref,
() => ({
setValue: value =>
shouldTruncateLongLines
? setEditorValueWithTruncation(codeMirror.current, value)
: codeMirror.current?.setValue(value || ''),
getValue: () => codeMirror.current?.getValue() || '',
selectAll: () =>
codeMirror.current?.setSelection({ line: 0, ch: 0 }, { line: codeMirror.current.lineCount(), ch: 0 }),
focus: () => codeMirror.current?.focus(),
scrollToSelection: (chStart: number, chEnd: number, lineStart: number, lineEnd: number) => {
codeMirror.current?.setSelection({ line: lineStart, ch: chStart }, { line: lineEnd, ch: chEnd });
// If sizing permits, position selection just above center
codeMirror.current?.scrollIntoView({ line: lineStart, ch: chStart }, window.innerHeight / 2 - 100);
},
focusEnd: () => {
if (codeMirror.current && !codeMirror.current.hasFocus()) {
codeMirror.current.focus();
}
codeMirror.current?.getDoc()?.setCursor(codeMirror.current.getDoc().lineCount(), 0);
},
getCursor: () => {
return codeMirror.current?.getCursor();
},
setCursorLine: (lineNumber: number) => {
codeMirror.current?.setCursor(lineNumber);
},
tryToSetOption,
hasFocus: () => codeMirror.current?.hasFocus() as boolean,
indexFromPos: (pos?: CodeMirror.Position) => (pos ? codeMirror.current?.indexFromPos(pos) || 0 : 0),
getDoc: () => codeMirror.current?.getDoc(),
}),
[],
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx
Outdated
Show resolved
Hide resolved
packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx
Outdated
Show resolved
Hide resolved
✅ Circular References ReportGenerated at: 2026-03-25T02:53:27.333Z Summary
Click to view all circular references in PR (73)Click to view all circular references in base branch (73)Analysis✅ No Change: This PR does not introduce or remove any circular references. This report was generated automatically by comparing against the |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
packages/insomnia/src/ui/components/.client/codemirror/code-editor.tsx:777
useImperativeHandleis created with an empty dependency array, but thesetValueimplementation now depends onshouldTruncateLongLines. IfreadOnly/truncateLongLineschanges after mount, the exposedsetValuewill keep using the initial value and may not truncate when it should (or vice versa). Consider either removing the deps array (recompute each render) or includingshouldTruncateLongLines(and any other referenced values you expect to vary) in the deps list.
useImperativeHandle(
ref,
() => ({
setValue: value =>
shouldTruncateLongLines
? setEditorValueWithTruncation(codeMirror.current, value)
: codeMirror.current?.setValue(value || ''),
getValue: () => codeMirror.current?.getValue() || '',
selectAll: () =>
codeMirror.current?.setSelection({ line: 0, ch: 0 }, { line: codeMirror.current.lineCount(), ch: 0 }),
focus: () => codeMirror.current?.focus(),
scrollToSelection: (chStart: number, chEnd: number, lineStart: number, lineEnd: number) => {
codeMirror.current?.setSelection({ line: lineStart, ch: chStart }, { line: lineEnd, ch: chEnd });
// If sizing permits, position selection just above center
codeMirror.current?.scrollIntoView({ line: lineStart, ch: chStart }, window.innerHeight / 2 - 100);
},
focusEnd: () => {
if (codeMirror.current && !codeMirror.current.hasFocus()) {
codeMirror.current.focus();
}
codeMirror.current?.getDoc()?.setCursor(codeMirror.current.getDoc().lineCount(), 0);
},
getCursor: () => {
return codeMirror.current?.getCursor();
},
setCursorLine: (lineNumber: number) => {
codeMirror.current?.setCursor(lineNumber);
},
tryToSetOption,
hasFocus: () => codeMirror.current?.hasFocus() as boolean,
indexFromPos: (pos?: CodeMirror.Position) => (pos ? codeMirror.current?.indexFromPos(pos) || 0 : 0),
getDoc: () => codeMirror.current?.getDoc(),
}),
[],
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const length = Math.max(activeResponse.bytesContent, activeResponse.bytesRead); | ||
| const isOversizedResponse = length > 5 * 1024 * 1024; // 5MB | ||
| const isOversizedResponse = length > LARGE_RESPONSE_MB * 1024 * 1024; | ||
| // Oversized repsonses are handled in the response-viewer.tsx for now |
There was a problem hiding this comment.
Spelling typo in comment: "repsonses" → "responses".
| // Oversized repsonses are handled in the response-viewer.tsx for now | |
| // Oversized responses are handled in the response-viewer.tsx for now |
| // Perform the editor update in a single operation to minimize reflows y | ||
| editor.operation(() => { | ||
| editor.setValue(lines.join('\n')); | ||
| Object.keys(longLinesMap).forEach(lineStr => { |
There was a problem hiding this comment.
lines.join('\n') will normalize all line endings to LF in truncate mode, even when no line actually exceeds the threshold. That can change the displayed/copy-pasted content for many responses >10k chars. Consider only doing the split/join path when at least one long line is detected (otherwise setValue(fullText)), or preserve the original newline sequence when rebuilding.
| if (lines[i].length > threshold) { | ||
| // save the original long line in a map for later restoration | ||
| longLinesMap[i] = lines[i]; | ||
| // truncate the line, preserving both the start and end | ||
| lines[i] = lines[i].slice(0, LONG_LINE_VISIBLE_CHARS) + lines[i].slice(-LONG_LINE_VISIBLE_CHARS); | ||
| } |
There was a problem hiding this comment.
Truncating the line by concatenating prefix + suffix means the editor text becomes a different value with no in-text delimiter; since CodeMirror bookmarks/widgets are not part of the document text, copying the truncated view can yield a misleading string with the middle silently removed. Consider inserting a literal placeholder (e.g. an ellipsis with a byte/char count) into the line text itself, and placing the widget after that placeholder.
| const makeToggleWidget = (lineNum: number, originalText: string): HTMLSpanElement => { | ||
| const el = document.createElement('span'); | ||
| el.className = 'line-collapse-widget'; | ||
| el.style.cssText = | ||
| 'display:inline-block;padding:0 6px;margin:0 2px;background:var(--hl-md);color:var(--color-font);' + | ||
| 'border-radius:3px;font-size:0.85em;cursor:pointer;vertical-align:baseline;'; | ||
|
|
||
| el.textContent = '\u2026 Show full value \u2026'; | ||
| el.title = 'Expanding long values can affect performance'; | ||
| el.setAttribute('aria-label', 'Show full value'); | ||
| el.onclick = () => { | ||
| // clear the marker and restore the original long line when the widget is clicked | ||
| markerMap.get(lineNum)?.clear(); | ||
| editor.replaceRange( | ||
| originalText, | ||
| { line: lineNum, ch: 0 }, | ||
| { line: lineNum, ch: editor.getLine(lineNum).length }, | ||
| ); | ||
| }; |
There was a problem hiding this comment.
The “Show full value” widget is a clickable <span> with only onclick. It’s not keyboard-focusable and won’t be operable via Enter/Space, which is an accessibility issue. Consider using a <button type="button"> for the widget, or add role="button", tabIndex={0}, and key handlers (Enter/Space) plus focus styling.
| return el; | ||
| }; | ||
|
|
||
| // Perform the editor update in a single operation to minimize reflows y |
There was a problem hiding this comment.
Typo in comment: trailing "y" in "minimize reflows y".
| // Perform the editor update in a single operation to minimize reflows y | |
| // Perform the editor update in a single operation to minimize reflows |
| useImperativeHandle( | ||
| ref, | ||
| () => ({ | ||
| setValue: value => codeMirror.current?.setValue(value), | ||
| setValue: value => | ||
| shouldTruncateLongLines | ||
| ? setEditorValueWithTruncation(codeMirror.current, value) | ||
| : codeMirror.current?.setValue(value || ''), |
There was a problem hiding this comment.
useImperativeHandle(..., [],) now captures shouldTruncateLongLines but never updates if readOnly/truncateLongLines props change after mount, so ref.setValue could apply the wrong behavior. Include shouldTruncateLongLines (and any other referenced values) in the dependency array, or compute the decision inside the callback from current props/refs.
Background:
When viewing a large API response (eg: MCP app resources contains the entire DOM tree), the response editor would hang or become unresponsive on load due to CodeMirror performance issue.
Changes:
INS-2250