diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 1463c813e8a..4b2f21cc516 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -30,6 +30,7 @@ import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" import { useKV } from "../../context/kv" import { useTextareaKeybindings } from "../textarea-keybindings" +import { resolvePastedContent } from "./paste" export type PromptProps = { sessionID?: string @@ -861,13 +862,15 @@ export function Prompt(props: PromptProps) { // Normalize line endings at the boundary // Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste // Replace CRLF first, then any remaining CR - const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n") - const pastedContent = normalizedText.trim() - if (!pastedContent) { + const resolvedContent = await resolvePastedContent(event.text, Clipboard.read) + + if (!resolvedContent) { command.trigger("prompt.paste") return } + const pastedContent = resolvedContent + // trim ' from the beginning and end of the pasted content. just // ' and nothing else const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ") diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/paste.ts b/packages/opencode/src/cli/cmd/tui/component/prompt/paste.ts new file mode 100644 index 00000000000..166e6056a92 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/paste.ts @@ -0,0 +1,17 @@ +import { Clipboard } from "../../util/clipboard" + +export async function resolvePastedContent( + eventText: string, + readClipboard: () => Promise, +): Promise { + // Normalize and use paste payload when terminals supply text directly + const normalized = eventText.replace(/\r\n/g, "\n").replace(/\r/g, "\n") + const text = normalized.trim() + if (text) return text + + // Finder copy often yields empty paste events; fallback to clipboard text + const clipboardContent = await readClipboard() + const clipboardText = clipboardContent?.mime.startsWith("text/") ? clipboardContent.data : undefined + if (!clipboardText) return + return clipboardText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim() +} diff --git a/packages/opencode/test/cli/tui/paste.test.ts b/packages/opencode/test/cli/tui/paste.test.ts new file mode 100644 index 00000000000..d1a230859cd --- /dev/null +++ b/packages/opencode/test/cli/tui/paste.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, mock, test } from "bun:test" +import { resolvePastedContent } from "../../../src/cli/cmd/tui/component/prompt/paste" + +describe("resolvePastedContent", () => { + test("returns normalized event text when present", async () => { + const readClipboard = mock(async () => undefined) + const result = await resolvePastedContent(" Hello\r\nWorld ", readClipboard) + expect(result).toBe("Hello\nWorld") + expect(readClipboard.mock.calls.length).toBe(0) + }) + + test("falls back to clipboard text when event text is empty", async () => { + const readClipboard = mock(async () => ({ data: " clipboard text ", mime: "text/plain" })) + const result = await resolvePastedContent("", readClipboard) + expect(result).toBe("clipboard text") + expect(readClipboard.mock.calls.length).toBe(1) + }) + + test("returns undefined when clipboard lacks text content", async () => { + const readClipboard = mock(async () => ({ data: "ignored", mime: "image/png" })) + const result = await resolvePastedContent("", readClipboard) + expect(result).toBeUndefined() + }) +})