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 = (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 = (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((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 }) } - const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText) + if (pasteMode(text) === "manual") { + put() + return + } + + 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" +}