From 7e681b0bc02b98e932a51e5469bcaeb6649c5f75 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 20 Feb 2026 07:38:05 -0600 Subject: fix(app): large text pasted into prompt-input causes main thread lock --- .../app/src/components/prompt-input/attachments.ts | 22 +++++++++++++++++++++- .../src/components/prompt-input/editor-dom.test.ts | 22 ++++++++++++++++++++++ .../app/src/components/prompt-input/editor-dom.ts | 15 +++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) (limited to 'packages/app/src/components/prompt-input') diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index 9ea2e62a6..a9e4e4965 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -7,6 +7,19 @@ import { getCursorPosition } from "./editor-dom" export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] +const LARGE_PASTE_CHARS = 8000 +const LARGE_PASTE_BREAKS = 120 + +function largePaste(text: string) { + if (text.length >= LARGE_PASTE_CHARS) return true + let breaks = 0 + for (const char of text) { + if (char !== "\n") continue + breaks += 1 + if (breaks >= LARGE_PASTE_BREAKS) return true + } + return false +} type PromptAttachmentsInput = { editor: () => HTMLDivElement | undefined @@ -14,7 +27,7 @@ type PromptAttachmentsInput = { isDialogActive: () => boolean setDraggingType: (type: "image" | "@mention" | null) => void focusEditor: () => void - addPart: (part: ContentPart) => void + addPart: (part: ContentPart) => boolean readClipboardImage?: () => Promise } @@ -89,6 +102,13 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { } if (!plainText) return + + if (largePaste(plainText)) { + if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + input.focusEditor() + if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + } + const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText) if (inserted) return diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts index 15e759f44..3088522a5 100644 --- a/packages/app/src/components/prompt-input/editor-dom.test.ts +++ b/packages/app/src/components/prompt-input/editor-dom.test.ts @@ -24,6 +24,28 @@ describe("prompt-input editor dom", () => { expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") }) + test("createTextFragment avoids break-node explosion for large multiline content", () => { + const content = Array.from({ length: 220 }, () => "line").join("\n") + const fragment = createTextFragment(content) + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(1) + expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE) + expect(container.textContent).toBe(content) + }) + + test("createTextFragment keeps terminal break in large multiline fallback", () => { + const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n` + const fragment = createTextFragment(content) + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(2) + expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1)) + expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") + }) + test("length helpers treat breaks as one char and ignore zero-width chars", () => { const container = document.createElement("div") container.appendChild(document.createTextNode("ab\u200B")) diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts index 4850a26ec..8575140d7 100644 --- a/packages/app/src/components/prompt-input/editor-dom.ts +++ b/packages/app/src/components/prompt-input/editor-dom.ts @@ -1,5 +1,20 @@ +const MAX_BREAKS = 200 + export function createTextFragment(content: string): DocumentFragment { const fragment = document.createDocumentFragment() + let breaks = 0 + for (const char of content) { + if (char !== "\n") continue + breaks += 1 + if (breaks > MAX_BREAKS) { + const tail = content.endsWith("\n") + const text = tail ? content.slice(0, -1) : content + if (text) fragment.appendChild(document.createTextNode(text)) + if (tail) fragment.appendChild(document.createElement("br")) + return fragment + } + } + const segments = content.split("\n") segments.forEach((segment, index) => { if (segment) { -- cgit v1.2.3