Skip to content

Commit c7de318

Browse files
authored
fix(app): handle multiline web paste in prompt composer (anomalyco#17509)
1 parent dce4487 commit c7de318

4 files changed

Lines changed: 57 additions & 23 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useFilteredList } from "@opencode-ai/ui/hooks"
22
import { useSpring } from "@opencode-ai/ui/motion-spring"
33
import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
44
import { createStore } from "solid-js/store"
5-
import { createFocusSignal } from "@solid-primitives/active-element"
65
import { useLocal } from "@/context/local"
76
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
87
import {
@@ -411,7 +410,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
411410
}
412411
}
413412

414-
const isFocused = createFocusSignal(() => editorRef)
415413
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
416414

417415
const pick = () => fileInputRef?.click()
@@ -1014,7 +1012,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
10141012

10151013
const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
10161014
editor: () => editorRef,
1017-
isFocused,
10181015
isDialogActive: () => !!dialog.active,
10191016
setDraggingType: (type) => setStore("draggingType", type),
10201017
focusEditor: () => {

packages/app/src/components/prompt-input/attachments.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from "bun:test"
22
import { attachmentMime } from "./files"
3+
import { pasteMode } from "./paste"
34

45
describe("attachmentMime", () => {
56
test("keeps PDFs when the browser reports the mime", async () => {
@@ -22,3 +23,22 @@ describe("attachmentMime", () => {
2223
expect(await attachmentMime(file)).toBeUndefined()
2324
})
2425
})
26+
27+
describe("pasteMode", () => {
28+
test("uses native paste for short single-line text", () => {
29+
expect(pasteMode("hello world")).toBe("native")
30+
})
31+
32+
test("uses manual paste for multiline text", () => {
33+
expect(
34+
pasteMode(`{
35+
"ok": true
36+
}`),
37+
).toBe("manual")
38+
expect(pasteMode("a\r\nb")).toBe("manual")
39+
})
40+
41+
test("uses manual paste for large text", () => {
42+
expect(pasteMode("x".repeat(8000))).toBe("manual")
43+
})
44+
})

packages/app/src/components/prompt-input/attachments.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { useLanguage } from "@/context/language"
55
import { uuid } from "@/utils/uuid"
66
import { getCursorPosition } from "./editor-dom"
77
import { attachmentMime } from "./files"
8-
const LARGE_PASTE_CHARS = 8000
9-
const LARGE_PASTE_BREAKS = 120
8+
import { normalizePaste, pasteMode } from "./paste"
109

1110
function dataUrl(file: File, mime: string) {
1211
return new Promise<string>((resolve) => {
@@ -25,20 +24,8 @@ function dataUrl(file: File, mime: string) {
2524
})
2625
}
2726

28-
function largePaste(text: string) {
29-
if (text.length >= LARGE_PASTE_CHARS) return true
30-
let breaks = 0
31-
for (const char of text) {
32-
if (char !== "\n") continue
33-
breaks += 1
34-
if (breaks >= LARGE_PASTE_BREAKS) return true
35-
}
36-
return false
37-
}
38-
3927
type PromptAttachmentsInput = {
4028
editor: () => HTMLDivElement | undefined
41-
isFocused: () => boolean
4229
isDialogActive: () => boolean
4330
setDraggingType: (type: "image" | "@mention" | null) => void
4431
focusEditor: () => void
@@ -91,7 +78,6 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
9178
}
9279

9380
const handlePaste = async (event: ClipboardEvent) => {
94-
if (!input.isFocused()) return
9581
const clipboardData = event.clipboardData
9682
if (!clipboardData) return
9783

@@ -126,16 +112,23 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
126112

127113
if (!plainText) return
128114

129-
if (largePaste(plainText)) {
130-
if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
115+
const text = normalizePaste(plainText)
116+
117+
const put = () => {
118+
if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true
131119
input.focusEditor()
132-
if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return
120+
return input.addPart({ type: "text", content: text, start: 0, end: 0 })
121+
}
122+
123+
if (pasteMode(text) === "manual") {
124+
put()
125+
return
133126
}
134127

135-
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText)
128+
const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text)
136129
if (inserted) return
137130

138-
input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
131+
put()
139132
}
140133

141134
const handleGlobalDragOver = (event: DragEvent) => {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const LARGE_PASTE_CHARS = 8000
2+
const LARGE_PASTE_BREAKS = 120
3+
4+
function largePaste(text: string) {
5+
if (text.length >= LARGE_PASTE_CHARS) return true
6+
let breaks = 0
7+
for (const char of text) {
8+
if (char !== "\n") continue
9+
breaks += 1
10+
if (breaks >= LARGE_PASTE_BREAKS) return true
11+
}
12+
return false
13+
}
14+
15+
export function normalizePaste(text: string) {
16+
if (!text.includes("\r")) return text
17+
return text.replace(/\r\n?/g, "\n")
18+
}
19+
20+
export function pasteMode(text: string) {
21+
if (largePaste(text)) return "manual"
22+
if (text.includes("\n") || text.includes("\r")) return "manual"
23+
return "native"
24+
}

0 commit comments

Comments
 (0)