Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, " ")
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/paste.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Clipboard } from "../../util/clipboard"

export async function resolvePastedContent(
eventText: string,
readClipboard: () => Promise<Clipboard.Content | undefined>,
): Promise<string | undefined> {
// 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()
}
24 changes: 24 additions & 0 deletions packages/opencode/test/cli/tui/paste.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})