Skip to content

Commit 342719d

Browse files
committed
fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard
1 parent f847564 commit 342719d

3 files changed

Lines changed: 30 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
@@ -183,7 +183,7 @@ export function tui(input: {
183183
targetFps: 60,
184184
gatherStats: false,
185185
exitOnCtrlC: false,
186-
useKittyKeyboard: {},
186+
useKittyKeyboard: { events: true },
187187
autoFocus: false,
188188
openConsoleOnError: false,
189189
consoleOptions: {

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

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

358+
// Windows Terminal 1.25+ with kitty keyboard swallows Ctrl+V press but
359+
// leaks the release (CSI 118;modifier;3u). Detect it and probe clipboard.
360+
if (process.platform === "win32") {
361+
useKeyboard(
362+
(evt) => {
363+
if (!input.focused) return
364+
if (evt.name === "v" && evt.ctrl && evt.eventType === "release") {
365+
command.trigger("prompt.paste")
366+
}
367+
},
368+
{ release: true },
369+
)
370+
}
371+
358372
const ref: PromptRef = {
359373
get focused() {
360374
return input.focused
@@ -852,10 +866,8 @@ export function Prompt(props: PromptProps) {
852866
e.preventDefault()
853867
return
854868
}
855-
// Handle clipboard paste (Ctrl+V) - check for images first on Windows
856-
// This is needed because Windows terminal doesn't properly send image data
857-
// through bracketed paste, so we need to intercept the keypress and
858-
// directly read from clipboard before the terminal handles it
869+
// Check clipboard for images before terminal handles the paste.
870+
// On Windows most terminals consume Ctrl+V so this rarely fires.
859871
if (keybind.match("input_paste", e)) {
860872
const content = await Clipboard.read()
861873
if (content?.mime.startsWith("image/")) {
@@ -938,6 +950,9 @@ export function Prompt(props: PromptProps) {
938950
// Replace CRLF first, then any remaining CR
939951
const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
940952
const pastedContent = normalizedText.trim()
953+
954+
// Empty paste = image-only clipboard. Stable WT sends an empty
955+
// bracketed paste for this; WT 1.25+ with kitty does not.
941956
if (!pastedContent) {
942957
command.trigger("prompt.paste")
943958
return

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

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

31+
// Checks clipboard for images first, then falls back to text.
32+
//
33+
// On Windows this is triggered from multiple paths in prompt/ because
34+
// terminals handle Ctrl+V differently:
35+
// 1. Ctrl+V keypress forwarded to app (rare, most terminals consume it)
36+
// 2. Empty bracketed paste (stable WT without kitty sends this for image-only clipboard)
37+
// 3. Kitty Ctrl+V release event (WT 1.25+ swallows the press but leaks the release)
3138
export async function read(): Promise<Content | undefined> {
3239
const os = platform()
3340

@@ -58,6 +65,8 @@ export namespace Clipboard {
5865
}
5966
}
6067

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