From 342719dfff8ed0ee7eb57317862ff019be52f1e6 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:41:17 +1000 Subject: [PATCH 1/5] fix(opencode): image paste on Windows Terminal 1.25+ with kitty keyboard --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- .../cli/cmd/tui/component/prompt/index.tsx | 25 +++++++++++++++---- .../src/cli/cmd/tui/util/clipboard.ts | 9 +++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8bb17ff13362..2be67b795290 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -183,7 +183,7 @@ export function tui(input: { targetFps: 60, gatherStats: false, exitOnCtrlC: false, - useKittyKeyboard: {}, + useKittyKeyboard: { events: true }, 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 c85426cc2471..b5eac65da8a1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -17,7 +17,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" @@ -355,6 +355,20 @@ export function Prompt(props: PromptProps) { ] }) + // Windows Terminal 1.25+ with kitty keyboard swallows Ctrl+V press but + // leaks the release (CSI 118;modifier;3u). Detect it and probe clipboard. + 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 @@ -852,10 +866,8 @@ 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 handles the paste. + // On Windows most terminals consume Ctrl+V so this rarely fires. if (keybind.match("input_paste", e)) { const content = await Clipboard.read() if (content?.mime.startsWith("image/")) { @@ -938,6 +950,9 @@ export function Prompt(props: PromptProps) { // Replace CRLF first, then any remaining CR const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const pastedContent = normalizedText.trim() + + // Empty paste = image-only clipboard. Stable WT sends an empty + // bracketed paste for this; WT 1.25+ with kitty 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 85e13d313396..714a36fcf30d 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -28,6 +28,13 @@ export namespace Clipboard { mime: string } + // Checks clipboard for images first, then falls back to text. + // + // On Windows this is triggered from multiple paths in prompt/ because + // terminals handle Ctrl+V differently: + // 1. Ctrl+V keypress forwarded to app (rare, most terminals consume it) + // 2. Empty bracketed paste (stable WT without kitty sends this for image-only clipboard) + // 3. Kitty Ctrl+V release event (WT 1.25+ swallows the press but leaks the release) export async function read(): Promise { const os = platform() @@ -58,6 +65,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()) }" From 01c0902c96fbdfcf5a5571af690c2f3e58a07720 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 19 Mar 2026 09:31:12 +1000 Subject: [PATCH 2/5] fix(opencode): gate kitty keyboard events to windows --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 2be67b795290..3be638245a86 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -183,7 +183,7 @@ export function tui(input: { targetFps: 60, gatherStats: false, exitOnCtrlC: false, - useKittyKeyboard: { events: true }, + useKittyKeyboard: { events: process.platform === "win32" }, autoFocus: false, openConsoleOnError: false, consoleOptions: { From 601131c3b133eb8b6be5a4c674579e2b40be105f Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:19:16 +1000 Subject: [PATCH 3/5] tui: clarify Windows Terminal paste path comments --- .../src/cli/cmd/tui/component/prompt/index.tsx | 13 +++++++------ packages/opencode/src/cli/cmd/tui/util/clipboard.ts | 11 ++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) 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 c13d0eeb9700..257248c3c126 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -356,8 +356,8 @@ export function Prompt(props: PromptProps) { ] }) - // Windows Terminal 1.25+ with kitty keyboard swallows Ctrl+V press but - // leaks the release (CSI 118;modifier;3u). Detect it and probe clipboard. + // 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) => { @@ -864,8 +864,9 @@ export function Prompt(props: PromptProps) { e.preventDefault() return } - // Check clipboard for images before terminal handles the paste. - // On Windows most terminals consume Ctrl+V so this rarely fires. + // 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/")) { @@ -949,8 +950,8 @@ export function Prompt(props: PromptProps) { const normalizedText = decodePasteBytes(event.bytes).replace(/\r\n/g, "\n").replace(/\r/g, "\n") const pastedContent = normalizedText.trim() - // Empty paste = image-only clipboard. Stable WT sends an empty - // bracketed paste for this; WT 1.25+ with kitty does not. + // 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 714a36fcf30d..87c0a63abc82 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -30,11 +30,12 @@ export namespace Clipboard { // Checks clipboard for images first, then falls back to text. // - // On Windows this is triggered from multiple paths in prompt/ because - // terminals handle Ctrl+V differently: - // 1. Ctrl+V keypress forwarded to app (rare, most terminals consume it) - // 2. Empty bracketed paste (stable WT without kitty sends this for image-only clipboard) - // 3. Kitty Ctrl+V release event (WT 1.25+ swallows the press but leaks the release) + // 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 { const os = platform() From 18f96e2b6c2f1fa70beee48821930528341f6888 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:20:49 +1000 Subject: [PATCH 4/5] tui: remove invalid markdown color props --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 0d9ddc746c2a..4682c50df1ad 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1465,8 +1465,6 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess streaming={true} content={props.part.text.trim()} conceal={ctx.conceal()} - fg={theme.markdownText} - bg={theme.background} /> From a9dd7ac3b9da7a2bd588a80eda0993e56b540aea Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:27:09 +1000 Subject: [PATCH 5/5] tui: restore markdown color props --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 4682c50df1ad..0d9ddc746c2a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1465,6 +1465,8 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess streaming={true} content={props.part.text.trim()} conceal={ctx.conceal()} + fg={theme.markdownText} + bg={theme.background} />