summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-26 12:00:38 +1000
committerGitHub <[email protected]>2026-03-26 12:00:38 +1000
commit1a4a6eabe207156f5ee0584c48e04db7e556f10a (patch)
treeed4a06315f502375934c7cd5263904387b83e85a
parentba244a6e62dab9d95496527684dcb46cf939d087 (diff)
downloadopencode-1a4a6eabe207156f5ee0584c48e04db7e556f10a.tar.gz
opencode-1a4a6eabe207156f5ee0584c48e04db7e556f10a.zip
fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard (#17674)
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx26
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/clipboard.ts10
3 files changed, 32 insertions, 6 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 84050d220..4897cb7e8 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -186,7 +186,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 c13b43651..257248c3c 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 85e13d313..87c0a63ab 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<Content | undefined> {
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()) }"