Skip to content

Commit de37fa2

Browse files
Apply PR #17674: fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard
2 parents 1d721c3 + a9dd7ac commit de37fa2

File tree

3 files changed

+32
-6
lines changed

3 files changed

+32
-6
lines changed

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"
@@ -368,6 +368,20 @@ export function Prompt(props: PromptProps) {
368368
]
369369
})
370370

371+
// Windows Terminal 1.25+ handles Ctrl+V on keydown when kitty events are
372+
// enabled, but still reports the kitty key-release event. Probe on release.
373+
if (process.platform === "win32") {
374+
useKeyboard(
375+
(evt) => {
376+
if (!input.focused) return
377+
if (evt.name === "v" && evt.ctrl && evt.eventType === "release") {
378+
command.trigger("prompt.paste")
379+
}
380+
},
381+
{ release: true },
382+
)
383+
}
384+
371385
const ref: PromptRef = {
372386
get focused() {
373387
return input.focused
@@ -862,10 +876,9 @@ export function Prompt(props: PromptProps) {
862876
e.preventDefault()
863877
return
864878
}
865-
// Handle clipboard paste (Ctrl+V) - check for images first on Windows
866-
// This is needed because Windows terminal doesn't properly send image data
867-
// through bracketed paste, so we need to intercept the keypress and
868-
// directly read from clipboard before the terminal handles it
879+
// Check clipboard for images before terminal-handled paste runs.
880+
// This helps terminals that forward Ctrl+V to the app; Windows
881+
// Terminal 1.25+ usually handles Ctrl+V before this path.
869882
if (keybind.match("input_paste", e)) {
870883
const content = await Clipboard.read()
871884
if (content?.mime.startsWith("image/")) {
@@ -948,6 +961,9 @@ export function Prompt(props: PromptProps) {
948961
// Replace CRLF first, then any remaining CR
949962
const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n")
950963
const pastedContent = normalizedText.trim()
964+
965+
// Windows Terminal <1.25 can surface image-only clipboard as an
966+
// empty bracketed paste. Windows Terminal 1.25+ does not.
951967
if (!pastedContent) {
952968
command.trigger("prompt.paste")
953969
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)