diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index dc052c4d2e04..11631d21139d 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -185,7 +185,7 @@ export function tui(input: { targetFps: 60, gatherStats: false, exitOnCtrlC: false, - useKittyKeyboard: {}, + useKittyKeyboard: { events: process.platform === "win32" }, autoFocus: false, openConsoleOnError: false, consoleOptions: { 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 c13b436514fc..257248c3c126 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -18,7 +18,7 @@ import { usePromptStash } from "./stash" import { DialogStash } from "../dialog-stash" import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { useCommandDialog } from "../dialog-command" -import { useRenderer } from "@opentui/solid" +import { useKeyboard, useRenderer } from "@opentui/solid" import { Editor } from "@tui/util/editor" import { useExit } from "../../context/exit" import { Clipboard } from "../../util/clipboard" @@ -356,6 +356,20 @@ export function Prompt(props: PromptProps) { ] }) + // Windows Terminal 1.25+ handles Ctrl+V on keydown when kitty events are + // enabled, but still reports the kitty key-release event. Probe on release. + if (process.platform === "win32") { + useKeyboard( + (evt) => { + if (!input.focused) return + if (evt.name === "v" && evt.ctrl && evt.eventType === "release") { + command.trigger("prompt.paste") + } + }, + { release: true }, + ) + } + const ref: PromptRef = { get focused() { return input.focused @@ -850,10 +864,9 @@ export function Prompt(props: PromptProps) { e.preventDefault() return } - // Handle clipboard paste (Ctrl+V) - check for images first on Windows - // This is needed because Windows terminal doesn't properly send image data - // through bracketed paste, so we need to intercept the keypress and - // directly read from clipboard before the terminal handles it + // Check clipboard for images before terminal-handled paste runs. + // This helps terminals that forward Ctrl+V to the app; Windows + // Terminal 1.25+ usually handles Ctrl+V before this path. if (keybind.match("input_paste", e)) { const content = await Clipboard.read() if (content?.mime.startsWith("image/")) { @@ -936,6 +949,9 @@ export function Prompt(props: PromptProps) { // Replace CRLF first, then any remaining CR const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n") const pastedContent = normalizedText.trim() + + // Windows Terminal <1.25 can surface image-only clipboard as an + // empty bracketed paste. Windows Terminal 1.25+ does not. if (!pastedContent) { command.trigger("prompt.paste") return diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 85e13d313396..87c0a63abc82 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -28,6 +28,14 @@ export namespace Clipboard { mime: string } + // Checks clipboard for images first, then falls back to text. + // + // On Windows prompt/ can call this from multiple paste signals because + // terminals surface image paste differently: + // 1. A forwarded Ctrl+V keypress + // 2. An empty bracketed-paste hint for image-only clipboard in Windows + // Terminal <1.25 + // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+ export async function read(): Promise { const os = platform() @@ -58,6 +66,8 @@ export namespace Clipboard { } } + // Windows/WSL: probe clipboard for images via PowerShell. + // Bracketed paste can't carry image data so we read it directly. if (os === "win32" || release().includes("WSL")) { const script = "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"