Skip to content

Commit 1a4a6ea

Browse files
authored
fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard (#17674)
1 parent ba244a6 commit 1a4a6ea

3 files changed

Lines changed: 32 additions & 6 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export function tui(input: {
186186
targetFps: 60,
187187
gatherStats: false,
188188
exitOnCtrlC: false,
189-
useKittyKeyboard: {},
189+
useKittyKeyboard: { events: process.platform === "win32" },
190190
autoFocus: false,
191191
openConsoleOnError: false,
192192
consoleOptions: {

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { usePromptStash } from "./stash"
1818
import { DialogStash } from "../dialog-stash"
1919
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
2020
import { useCommandDialog } from "../dialog-command"
21-
import { useRenderer } from "@opentui/solid"
21+
import { useKeyboard, useRenderer } from "@opentui/solid"
2222
import { Editor } from "@tui/util/editor"
2323
import { useExit } from "../../context/exit"
2424
import { Clipboard } from "../../util/clipboard"
@@ -356,6 +356,20 @@ export function Prompt(props: PromptProps) {
356356
]
357357
})
358358

359+
// Windows Terminal 1.25+ handles Ctrl+V on keydown when kitty events are
360+
// enabled, but still reports the kitty key-release event. Probe on release.
361+
if (process.platform === "win32") {
362+
useKeyboard(
363+
(evt) => {
364+
if (!input.focused) return
365+
if (evt.name === "v" && evt.ctrl && evt.eventType === "release") {
366+
command.trigger("prompt.paste")
367+
}
368+
},
369+
{ release: true },
370+
)
371+
}
372+
359373
const ref: PromptRef = {
360374
get focused() {
361375
return input.focused
@@ -850,10 +864,9 @@ export function Prompt(props: PromptProps) {
850864
e.preventDefault()
851865
return
852866
}
853-
// Handle clipboard paste (Ctrl+V) - check for images first on Windows
854-
// This is needed because Windows terminal doesn't properly send image data
855-
// through bracketed paste, so we need to intercept the keypress and
856-
// directly read from clipboard before the terminal handles it
867+
// Check clipboard for images before terminal-handled paste runs.
868+
// This helps terminals that forward Ctrl+V to the app; Windows
869+
// Terminal 1.25+ usually handles Ctrl+V before this path.
857870
if (keybind.match("input_paste", e)) {
858871
const content = await Clipboard.read()
859872
if (content?.mime.startsWith("image/")) {
@@ -936,6 +949,9 @@ export function Prompt(props: PromptProps) {
936949
// Replace CRLF first, then any remaining CR
937950
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
938951
const pastedContent = normalizedText.trim()
952+
953+
// Windows Terminal <1.25 can surface image-only clipboard as an
954+
// empty bracketed paste. Windows Terminal 1.25+ does not.
939955
if (!pastedContent) {
940956
command.trigger("prompt.paste")
941957
return

packages/opencode/src/cli/cmd/tui/util/clipboard.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ export namespace Clipboard {
2828
mime: string
2929
}
3030

31+
// Checks clipboard for images first, then falls back to text.
32+
//
33+
// On Windows prompt/ can call this from multiple paste signals because
34+
// terminals surface image paste differently:
35+
// 1. A forwarded Ctrl+V keypress
36+
// 2. An empty bracketed-paste hint for image-only clipboard in Windows
37+
// Terminal <1.25
38+
// 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
3139
export async function read(): Promise<Content | undefined> {
3240
const os = platform()
3341

@@ -58,6 +66,8 @@ export namespace Clipboard {
5866
}
5967
}
6068

69+
// Windows/WSL: probe clipboard for images via PowerShell.
70+
// Bracketed paste can't carry image data so we read it directly.
6171
if (os === "win32" || release().includes("WSL")) {
6272
const script =
6373
"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()) }"

0 commit comments

Comments
 (0)