Compare commits

...

2 Commits

Author SHA1 Message Date
cc1e853f89 Merge branch 'paste-buffer-large'
Some checks failed
Publish Docker Image / build-and-push (push) Failing after 4s
Buffers large pastes outside the textarea to prevent the
multi-second UI freeze that happens when megabytes of text get
committed to a <textarea> in one shot. Threshold 256 KB; first 50
lines + a banner render into the textarea, full content uploads
on Save.
2026-05-06 19:18:31 +00:00
0282ab594e fix: buffer large pastes outside the textarea to prevent UI freeze
Pasting (or drag-loading, or file-uploading) several MB of text into
the <textarea> would lock up the browser for seconds while it laid
out millions of characters. The bottleneck is the textarea itself,
not the filter pipeline (filters only run on Save).

Hold above-threshold (256 KB) content in a JS-side `bufferedContent`
variable and render only the first 50 lines + a banner into the
textarea. The Save path uses `bufferedContent` when set, so the
upload sees the full content. The textarea becomes read-only while
the buffer is active; user-typed input invalidates the buffer
automatically (the textarea is the source of truth in normal mode).

A new `loadContent(text)` helper is the single entry point: it
chooses textarea vs. buffer based on length. All call sites
(clipboard read, paste-event for text, file load, text drop) route
through it. Threshold is 256 KB (above which textarea rendering
visibly stutters); preview is 50 lines (enough to recognise the log
without choking the renderer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:18:31 +00:00

View File

@@ -7,8 +7,27 @@ const fileSelectButton = document.getElementById('paste-select-file');
const pasteClipboardButton = document.getElementById('paste-clipboard');
const pasteError = document.getElementById('paste-error');
/*
* Large-paste buffering. <textarea> rendering is the bottleneck once content
* crosses a few hundred KB; the browser locks up while it lays out millions
* of characters. For pastes / file loads above the threshold, we hold the
* full content in `bufferedContent` and only render a small preview into the
* textarea. The save path uses `bufferedContent` when set.
*/
const PASTE_BUFFER_THRESHOLD_BYTES = 256 * 1024; // 256 KB
const PASTE_PREVIEW_LINES = 50;
let bufferedContent = null;
pasteArea.focus();
pasteArea.addEventListener('input', reevaluateContentStatus);
pasteArea.addEventListener('input', () => {
// User-typed input invalidates the buffer (the textarea is now the
// source of truth). The buffer is only set programmatically, so a
// user-driven `input` event always means edit-from-preview.
if (bufferedContent !== null) {
clearBuffer();
}
reevaluateContentStatus();
});
pasteArea.addEventListener('paste', handlePasteEvent);
pasteSaveButtons.forEach(button => button.addEventListener('click', sendLog));
fileSelectButton.addEventListener('click', selectLogFile);
@@ -39,7 +58,7 @@ async function sendLog() {
pasteSaveButtons.forEach(button => button.classList.add("btn-working"));
try {
let log = pasteArea.value;
let log = bufferedContent ?? pasteArea.value;
log = applyFilters(log);
const bodyData = {
@@ -149,13 +168,56 @@ async function pasteFromClipboard() {
showError("Clipboard is empty.");
return;
}
pasteArea.value = content;
reevaluateContentStatus();
loadContent(content);
} catch (err) {
showError("Clipboard is empty or not accessible.");
}
}
/*
* Single entry point for "place this string in the editor."
* Routes large content into the buffer + preview; small content goes into
* the textarea normally so the user can keep editing it.
*/
function loadContent(text) {
if (text.length > PASTE_BUFFER_THRESHOLD_BYTES) {
loadIntoBuffer(text);
} else {
clearBuffer();
pasteArea.value = text;
reevaluateContentStatus();
}
}
function loadIntoBuffer(text) {
bufferedContent = text;
const lines = text.split('\n');
const sizeMb = (text.length / 1024 / 1024).toFixed(2);
if (lines.length <= PASTE_PREVIEW_LINES) {
pasteArea.value = text;
} else {
const preview = lines.slice(0, PASTE_PREVIEW_LINES).join('\n');
const remaining = lines.length - PASTE_PREVIEW_LINES;
pasteArea.value =
`[Large paste buffered: ${lines.length.toLocaleString()} lines, ${sizeMb} MB.\n` +
` Full content uploads on Save. Edit this textarea to clear the buffer.]\n` +
`\n` +
`--- preview: first ${PASTE_PREVIEW_LINES} of ${lines.length.toLocaleString()} lines ---\n` +
preview +
`\n--- ${remaining.toLocaleString()} more lines hidden ---\n`;
}
pasteArea.readOnly = true;
reevaluateContentStatus();
}
function clearBuffer() {
if (bufferedContent === null) {
return;
}
bufferedContent = null;
pasteArea.readOnly = false;
}
function reevaluateContentStatus() {
clearError();
if (pasteArea.value.length > 0) {
@@ -184,6 +246,14 @@ async function handlePasteEvent(e) {
if (e.clipboardData?.files?.length > 0) {
e.preventDefault();
await loadFileContents(e.clipboardData.files[0]);
return;
}
// Intercept large text pastes before the browser commits them to the
// textarea — assigning multi-MB strings to a <textarea> freezes the page.
const text = e.clipboardData?.getData('text');
if (text && text.length > PASTE_BUFFER_THRESHOLD_BYTES) {
e.preventDefault();
loadIntoBuffer(text);
}
}
@@ -220,8 +290,7 @@ async function loadFileContents(file) {
return;
}
pasteArea.value = new TextDecoder().decode(content);
reevaluateContentStatus();
loadContent(new TextDecoder().decode(content));
}
function selectLogFile() {
@@ -355,8 +424,7 @@ async function handleDropEvent(e) {
let files = e.dataTransfer.files;
if (files.length !== 1) {
if (Array.from(e.dataTransfer.types).includes('text/plain')) {
pasteArea.value = e.dataTransfer.getData('text/plain');
reevaluateContentStatus();
loadContent(e.dataTransfer.getData('text/plain'));
}
return;
}