summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/prompt-input.tsx46
-rw-r--r--packages/app/src/components/prompt-input/attachments.ts22
-rw-r--r--packages/app/src/components/prompt-input/editor-dom.test.ts22
-rw-r--r--packages/app/src/components/prompt-input/editor-dom.ts15
4 files changed, 91 insertions, 14 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index b1c608ffc..adfd592f8 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -89,6 +89,8 @@ const EXAMPLES = [
"prompt.example.25",
] as const
+const NON_EMPTY_TEXT = /[^\s\u200B]/
+
export const PromptInput: Component<PromptInputProps> = (props) => {
const sdk = useSDK()
const sync = useSync()
@@ -636,7 +638,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
let buffer = ""
const flushText = () => {
- const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "")
+ let content = buffer
+ if (content.includes("\r")) content = content.replace(/\r\n?/g, "\n")
+ if (content.includes("\u200B")) content = content.replace(/\u200B/g, "")
buffer = ""
if (!content) return
parts.push({ type: "text", content, start: position, end: position + content.length })
@@ -714,10 +718,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const rawParts = parseFromDOM()
const images = imageAttachments()
const cursorPosition = getCursorPosition(editorRef)
- const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("")
- const trimmed = rawText.replace(/\u200B/g, "").trim()
+ const rawText =
+ rawParts.length === 1 && rawParts[0]?.type === "text"
+ ? rawParts[0].content
+ : rawParts.map((p) => ("content" in p ? p.content : "")).join("")
const hasNonText = rawParts.some((part) => part.type !== "text")
- const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0
+ const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0
if (shouldReset) {
closePopover()
@@ -757,19 +763,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
const addPart = (part: ContentPart) => {
+ if (part.type === "image") return false
+
const selection = window.getSelection()
- if (!selection || selection.rangeCount === 0) return
+ if (!selection) return false
- const cursorPosition = getCursorPosition(editorRef)
- const currentPrompt = prompt.current()
- const rawText = currentPrompt.map((p) => ("content" in p ? p.content : "")).join("")
- const textBeforeCursor = rawText.substring(0, cursorPosition)
- const atMatch = textBeforeCursor.match(/@(\S*)$/)
+ if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) {
+ editorRef.focus()
+ const cursor = prompt.cursor() ?? promptLength(prompt.current())
+ setCursorPosition(editorRef, cursor)
+ }
+
+ if (selection.rangeCount === 0) return false
+ const range = selection.getRangeAt(0)
+ if (!editorRef.contains(range.startContainer)) return false
if (part.type === "file" || part.type === "agent") {
+ const cursorPosition = getCursorPosition(editorRef)
+ const rawText = prompt
+ .current()
+ .map((p) => ("content" in p ? p.content : ""))
+ .join("")
+ const textBeforeCursor = rawText.substring(0, cursorPosition)
+ const atMatch = textBeforeCursor.match(/@(\S*)$/)
const pill = createPill(part)
const gap = document.createTextNode(" ")
- const range = selection.getRangeAt(0)
if (atMatch) {
const start = atMatch.index ?? cursorPosition - atMatch[0].length
@@ -784,8 +802,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
- } else if (part.type === "text") {
- const range = selection.getRangeAt(0)
+ }
+
+ if (part.type === "text") {
const fragment = createTextFragment(part.content)
const last = fragment.lastChild
range.deleteContents()
@@ -821,6 +840,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
handleInput()
closePopover()
+ return true
}
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
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<File | null>
}
@@ -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) {