Parcourir la source

fix(app): handle multiline web paste in prompt composer (#17509)

Shoubhit Dash il y a 1 mois
Parent
commit
689d9e14ea

+ 0 - 3
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: () => {

+ 20 - 0
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")
+  })
+})

+ 13 - 20
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) => {

+ 24 - 0
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"
+}