Skip to content

fix: handle text clipboard paste on Ctrl+V for Windows terminals#13871

Open
yxshee wants to merge 1 commit intoanomalyco:devfrom
yxshee:fix-windows-tui-ctrl-v-paste
Open

fix: handle text clipboard paste on Ctrl+V for Windows terminals#13871
yxshee wants to merge 1 commit intoanomalyco:devfrom
yxshee:fix-windows-tui-ctrl-v-paste

Conversation

@yxshee
Copy link
Contributor

@yxshee yxshee commented Feb 16, 2026

Fixes this issue #13800

Summary

On Windows 11, pressing Ctrl+V in the TUI prompt does not paste text. This PR fixes the issue by directly reading clipboard text when Ctrl+V is pressed, instead of relying solely on bracketed paste sequences that Windows terminals don't reliably emit.

Root Cause

The onKeyDown handler for input_paste (Ctrl+V) in prompt/index.tsx calls Clipboard.read(), which successfully reads both images and text from the clipboard. However, only the image/* case was handled — for text content, the handler fell through expecting the terminal to emit a bracketed paste sequence. Windows Terminal, cmd, and PowerShell do not reliably send bracketed paste sequences for Ctrl+V key presses, causing text paste to silently fail.

Fix

clipboard.ts: Added Clipboard.readText() — a dedicated text reader that uses clipboardy.read() and normalizes CRLF/CR line endings to LF.

prompt/index.tsx: Modified the Ctrl+V handler to also handle text/plain content from Clipboard.read():

  • Prevents the default event to avoid double-paste
  • Normalizes CRLF → LF (Windows clipboard often uses CRLF)
  • Routes through the same paste pipeline as onPaste: file path detection, image file handling, large paste summarization (≥3 lines or >150 chars), and direct insertion for short text
  • Falls through to bracketed paste only if clipboard is completely empty (no regression for terminals that do support it)

Testing

  • 18 new unit tests covering clipboard text reading, CRLF normalization, and paste pipeline logic
  • Full test suite: 909 pass, 0 regressions
  • No changes to image paste behavior
  • Bracketed paste (onPaste) path is fully preserved

Notes for Reviewers

  1. No regression on macOS/Linux: The text clipboard read path now runs on all platforms when Ctrl+V is pressed. On macOS/Linux where bracketed paste works, the Ctrl+V handler will read the same text that would have arrived via bracketed paste — the user sees the same result either way.
  2. Image paste preserved: Image check runs first (unchanged) — only if no image is found does the text path execute.
  3. Manual Windows verification: To fully validate on Windows 11, run the TUI and press Ctrl+V with text in the clipboard. Verify text appears in the prompt. Test with multiline text (should show [Pasted ~N lines] summary). Test with an image in the clipboard (should still paste as image).

Windows terminals (Windows Terminal, cmd, PowerShell) do not reliably
emit bracketed paste sequences when the user presses Ctrl+V. The
existing handler only checked for image content and fell through to
rely on bracketed paste for text, which silently failed on Windows.

- Add Clipboard.readText() with CRLF normalization
- Handle text/plain from Clipboard.read() in the Ctrl+V onKeyDown handler
- Route pasted text through the same pipeline as onPaste (file detection,
  image file paths, large paste summarization, direct insertion)
- Preserve image paste and bracketed paste behavior
- Add 18 unit tests for clipboard text reading and paste pipeline
Copilot AI review requested due to automatic review settings February 16, 2026 17:25
@github-actions
Copy link
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions
Copy link
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Duplicate Found

PR #13798: fix: Ctrl+V paste not working on Windows 11 TUI
#13798

This PR appears to be addressing the exact same issue as PR #13871 — fixing Ctrl+V paste not working on Windows 11 in the TUI. Both PRs target the same problem: Windows terminals not reliably emitting bracketed paste sequences for Ctrl+V.

Recommendation: Check if PR #13798 is still open and whether it's been superseded by the current PR, or if they need to be consolidated.


Related PRs (Historical Context)

These older PRs addressed similar clipboard/paste issues but on different aspects:

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Windows-specific issue where pressing Ctrl+V in the TUI prompt fails to paste text. The root cause is that Windows terminals (Windows Terminal, cmd, PowerShell) don't reliably emit bracketed paste sequences for Ctrl+V key presses. The fix adds direct clipboard text reading when Ctrl+V is pressed, routing it through the same paste pipeline as bracketed paste.

Changes:

  • Added Clipboard.readText() function that reads and normalizes clipboard text (CRLF → LF)
  • Modified Ctrl+V handler to directly handle text/plain clipboard content when bracketed paste isn't available
  • Added 18 unit tests covering clipboard text reading, CRLF normalization, and paste pipeline logic

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
packages/opencode/src/cli/cmd/tui/util/clipboard.ts Added readText() function for reading and normalizing clipboard text (unused in implementation)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx Extended Ctrl+V handler to directly process text clipboard content, duplicating logic from onPaste handler; includes unrelated formatting changes
packages/opencode/test/cli/tui/clipboard.test.ts Added comprehensive test coverage for clipboard operations and paste pipeline logic

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +75 to +90
/**
* Reads text from the clipboard, normalizing CRLF to LF.
* Returns undefined if the clipboard is empty, contains non-text, or on error.
* This is used by the Ctrl+V handler to directly paste text on platforms
* where bracketed paste is not reliably emitted (e.g. Windows terminals).
*/
export async function readText(): Promise<string | undefined> {
try {
const text = await clipboardy.read()
if (!text) return undefined
// Normalize Windows CRLF and stray CR to LF
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
} catch {
return undefined
}
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readText() function is defined but never used in the codebase. The Ctrl+V handler in prompt/index.tsx uses Clipboard.read() instead and checks for content?.mime === "text/plain". Consider either using readText() in the handler or removing this unused function to reduce code duplication and maintenance burden.

Suggested change
/**
* Reads text from the clipboard, normalizing CRLF to LF.
* Returns undefined if the clipboard is empty, contains non-text, or on error.
* This is used by the Ctrl+V handler to directly paste text on platforms
* where bracketed paste is not reliably emitted (e.g. Windows terminals).
*/
export async function readText(): Promise<string | undefined> {
try {
const text = await clipboardy.read()
if (!text) return undefined
// Normalize Windows CRLF and stray CR to LF
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
} catch {
return undefined
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +836 to +891
if (content?.mime === "text/plain" && content.data) {
e.preventDefault()
// Normalize CRLF → LF (Windows clipboard often has CRLF)
const normalizedText = content.data.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
const pastedContent = normalizedText.trim()
if (!pastedContent) return

// Check if pasted content is a file path (same logic as onPaste)
const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ")
const isUrl = /^(https?):\/\//.test(filepath)
if (!isUrl) {
try {
const file = Bun.file(filepath)
if (file.type === "image/svg+xml") {
const svgContent = await file.text().catch(() => { })
if (svgContent) {
pasteText(svgContent, `[SVG: ${file.name ?? "image"}]`)
return
}
}
if (file.type.startsWith("image/")) {
const imgContent = await file
.arrayBuffer()
.then((buffer) => Buffer.from(buffer).toString("base64"))
.catch(() => { })
if (imgContent) {
await pasteImage({
filename: file.name,
mime: file.type,
content: imgContent,
})
return
}
}
} catch { }
}

// Summarize large pastes (same threshold as onPaste)
const lineCount = (pastedContent.match(/\n/g)?.length ?? 0) + 1
if (
(lineCount >= 3 || pastedContent.length > 150) &&
!sync.data.config.experimental?.disable_paste_summary
) {
pasteText(pastedContent, `[Pasted ~${lineCount} lines]`)
return
}

// Short text: insert directly
input.insertText(pastedContent)
setTimeout(() => {
if (!input || input.isDestroyed) return
input.getLayoutNode().markDirty()
renderer.requestRender()
}, 0)
return
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is significant code duplication between the Ctrl+V handler (lines 836-891) and the onPaste handler (lines 952-1019). Both implement the same logic for: 1) normalizing CRLF to LF, 2) trimming content, 3) detecting file paths, 4) handling SVG files, 5) handling image files, 6) summarizing large pastes, and 7) inserting short text. This duplication increases maintenance burden and the risk of bugs when one handler is updated but not the other. Consider extracting this shared logic into a common helper function that both handlers can call.

Copilot uses AI. Check for mistakes.
],
})
.catch(() => {})
.catch(() => { })
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatting changes in this PR are inconsistent with the established codebase convention. The codebase consistently uses .catch(() => {}) with no space inside the braces (see clipboard.ts:69, auth.ts:254, import.ts:119, run.ts:303, and many other files). This PR changes it to .catch(() => { }) with a space. While this is a minor style issue, it creates inconsistency. Consider either reverting these formatting changes or applying them consistently across the entire codebase in a separate formatting PR.

Suggested change
.catch(() => { })
.catch(() => {})

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,138 @@
import { describe, expect, test, mock, beforeEach } from "bun:test"
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports mock and beforeEach are unused in this test file. Consider removing them to keep the imports clean and avoid confusion.

Suggested change
import { describe, expect, test, mock, beforeEach } from "bun:test"
import { describe, expect, test } from "bun:test"

Copilot uses AI. Check for mistakes.
Comment on lines +836 to +841
if (content?.mime === "text/plain" && content.data) {
e.preventDefault()
// Normalize CRLF → LF (Windows clipboard often has CRLF)
const normalizedText = content.data.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
const pastedContent = normalizedText.trim()
if (!pastedContent) return
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a logic issue with empty clipboard content handling. When content?.mime === "text/plain" and content.data is truthy (line 836), the code calls e.preventDefault() at line 837. However, if the content becomes empty after normalization and trim (checked at line 841), the code returns early without doing anything. This means the default paste behavior has already been prevented, so the user sees no paste operation occur at all. If the intention is to let the terminal handle whitespace-only clipboard content, the e.preventDefault() call should be moved after the empty check, or the check should happen before preventDefault().

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant