fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard (#17674)

pull/19210/head
Luke Parker 2026-03-26 12:00:38 +10:00 committed by GitHub
parent ba244a6e62
commit 1a4a6eabe2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 32 additions and 6 deletions

View File

@ -186,7 +186,7 @@ export function tui(input: {
targetFps: 60, targetFps: 60,
gatherStats: false, gatherStats: false,
exitOnCtrlC: false, exitOnCtrlC: false,
useKittyKeyboard: {}, useKittyKeyboard: { events: process.platform === "win32" },
autoFocus: false, autoFocus: false,
openConsoleOnError: false, openConsoleOnError: false,
consoleOptions: { consoleOptions: {

View File

@ -18,7 +18,7 @@ import { usePromptStash } from "./stash"
import { DialogStash } from "../dialog-stash" import { DialogStash } from "../dialog-stash"
import { type AutocompleteRef, Autocomplete } from "./autocomplete" import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command" import { useCommandDialog } from "../dialog-command"
import { useRenderer } from "@opentui/solid" import { useKeyboard, useRenderer } from "@opentui/solid"
import { Editor } from "@tui/util/editor" import { Editor } from "@tui/util/editor"
import { useExit } from "../../context/exit" import { useExit } from "../../context/exit"
import { Clipboard } from "../../util/clipboard" 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 = { const ref: PromptRef = {
get focused() { get focused() {
return input.focused return input.focused
@ -850,10 +864,9 @@ export function Prompt(props: PromptProps) {
e.preventDefault() e.preventDefault()
return return
} }
// Handle clipboard paste (Ctrl+V) - check for images first on Windows // Check clipboard for images before terminal-handled paste runs.
// This is needed because Windows terminal doesn't properly send image data // This helps terminals that forward Ctrl+V to the app; Windows
// through bracketed paste, so we need to intercept the keypress and // Terminal 1.25+ usually handles Ctrl+V before this path.
// directly read from clipboard before the terminal handles it
if (keybind.match("input_paste", e)) { if (keybind.match("input_paste", e)) {
const content = await Clipboard.read() const content = await Clipboard.read()
if (content?.mime.startsWith("image/")) { if (content?.mime.startsWith("image/")) {
@ -936,6 +949,9 @@ export function Prompt(props: PromptProps) {
// Replace CRLF first, then any remaining CR // Replace CRLF first, then any remaining CR
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n") const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
const pastedContent = normalizedText.trim() 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) { if (!pastedContent) {
command.trigger("prompt.paste") command.trigger("prompt.paste")
return return

View File

@ -28,6 +28,14 @@ export namespace Clipboard {
mime: string 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<Content | undefined> { export async function read(): Promise<Content | undefined> {
const os = platform() 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")) { if (os === "win32" || release().includes("WSL")) {
const script = 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()) }" "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()) }"