summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-03-14 22:51:45 +0530
committerGitHub <[email protected]>2026-03-14 22:51:45 +0530
commit689d9e14eade9001568c46c602092eb01fe7e746 (patch)
treec4f4e4740526915f59b0f34e3f260d1745b30827
parent66e8c57ed1077814c9a150b858a53fdd7c758c0f (diff)
downloadopencode-689d9e14eade9001568c46c602092eb01fe7e746.tar.gz
opencode-689d9e14eade9001568c46c602092eb01fe7e746.zip
fix(app): handle multiline web paste in prompt composer (#17509)
-rw-r--r--packages/app/src/components/prompt-input.tsx3
-rw-r--r--packages/app/src/components/prompt-input/attachments.test.ts20
-rw-r--r--packages/app/src/components/prompt-input/attachments.ts33
-rw-r--r--packages/app/src/components/prompt-input/paste.ts24
4 files changed, 57 insertions, 23 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 9048fa895..b2553e4c0 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -2,7 +2,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
import { createStore } from "solid-js/store"
-import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
import {
@@ -411,7 +410,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
- const isFocused = createFocusSignal(() => editorRef)
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
const pick = () => fileInputRef?.click()
@@ -1014,7 +1012,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
editor: () => editorRef,
- isFocused,
isDialogActive: () => !!dialog.active,
setDraggingType: (type) => setStore("draggingType", type),
focusEditor: () => {
diff --git a/packages/app/src/components/prompt-input/attachments.test.ts b/packages/app/src/components/prompt-input/attachments.test.ts
index d8ae43d13..43f7d425b 100644
--- a/packages/app/src/components/prompt-input/attachments.test.ts
+++ b/packages/app/src/components/prompt-input/attachments.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import { attachmentMime } from "./files"
+import { pasteMode } from "./paste"
describe("attachmentMime", () => {
test("keeps PDFs when the browser reports the mime", async () => {
@@ -22,3 +23,22 @@ describe("attachmentMime", () => {
expect(await attachmentMime(file)).toBeUndefined()
})
})
+
+describe("pasteMode", () => {
+ test("uses native paste for short single-line text", () => {
+ expect(pasteMode("hello world")).toBe("native")
+ })
+
+ test("uses manual paste for multiline text", () => {
+ expect(
+ pasteMode(`{
+ "ok": true
+}`),
+ ).toBe("manual")
+ expect(pasteMode("a\r\nb")).toBe("manual")
+ })
+
+ test("uses manual paste for large text", () => {
+ expect(pasteMode("x".repeat(8000))).toBe("manual")
+ })
+})
diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts
index b465ea5db..eca508c6c 100644
--- a/packages/app/src/components/prompt-input/attachments.ts
+++ b/packages/app/src/components/prompt-input/attachments.ts
@@ -5,8 +5,7 @@ import { useLanguage } from "@/context/language"
import { uuid } from "@/utils/uuid"
import { getCursorPosition } from "./editor-dom"
import { attachmentMime } from "./files"
-const LARGE_PASTE_CHARS = 8000
-const LARGE_PASTE_BREAKS = 120
+import { normalizePaste, pasteMode } from "./paste"
function dataUrl(file: File, mime: string) {
return new Promise<string>((resolve) => {
@@ -25,20 +24,8 @@ function dataUrl(file: File, mime: string) {
})
}
-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
- isFocused: () => boolean
isDialogActive: () => boolean
setDraggingType: (type: "image" | "@mention" | null) => void
focusEditor: () => void
@@ -91,7 +78,6 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
}
const handlePaste = async (event: ClipboardEvent) => {
- if (!input.isFocused()) return
const clipboardData = event.clipboardData
if (!clipboardData) return
@@ -126,16 +112,23 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
if (!plainText) return
- if (largePaste(plainText)) {
- if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
+ const text = normalizePaste(plainText)
+
+ const put = () => {
+ if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true
input.focusEditor()
- if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
+ return input.addPart({ type: "text", content: text, start: 0, end: 0 })
+ }
+
+ if (pasteMode(text) === "manual") {
+ put()
+ return
}
- const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText)
+ const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text)
if (inserted) return
- input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
+ put()
}
const handleGlobalDragOver = (event: DragEvent) => {
diff --git a/packages/app/src/components/prompt-input/paste.ts b/packages/app/src/components/prompt-input/paste.ts
new file mode 100644
index 000000000..6787d5030
--- /dev/null
+++ b/packages/app/src/components/prompt-input/paste.ts
@@ -0,0 +1,24 @@
+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
+}
+
+export function normalizePaste(text: string) {
+ if (!text.includes("\r")) return text
+ return text.replace(/\r\n?/g, "\n")
+}
+
+export function pasteMode(text: string) {
+ if (largePaste(text)) return "manual"
+ if (text.includes("\n") || text.includes("\r")) return "manual"
+ return "native"
+}