From 3bead6ae2548e1e8f8e41480f6b8615c8968a590 Mon Sep 17 00:00:00 2001 From: Armand Lynch Date: Fri, 6 Mar 2026 12:35:26 -0500 Subject: [PATCH 1/3] fix(tui): support middle-click paste from X11 primary selection --- .../cli/cmd/tui/component/prompt/index.tsx | 22 +++++++++++++++++-- .../src/cli/cmd/tui/util/clipboard.ts | 18 +++++++++++++++ 2 files changed, 38 insertions(+), 2 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 382bd2806ec7..38a28efbf71a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,4 +1,14 @@ -import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes, t, dim, fg } from "@opentui/core" +import { + BoxRenderable, + TextareaRenderable, + MouseEvent, + MouseButton, + PasteEvent, + decodePasteBytes, + t, + dim, + fg, +} from "@opentui/core" import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" import "opentui-spinner/solid" import path from "path" @@ -1080,7 +1090,15 @@ export function Prompt(props: PromptProps) { input.cursorColor = theme.text }, 0) }} - onMouseDown={(r: MouseEvent) => r.target?.focus()} + onMouseDown={async (r: MouseEvent) => { + r.target?.focus() + if (r.button !== MouseButton.MIDDLE) return + if (props.disabled) return + r.preventDefault() + const text = await Clipboard.readPrimary() + if (!text || !input || input.isDestroyed) return + input.insertText(text) + }} focusedBackgroundColor={theme.backgroundElement} cursorColor={theme.text} syntaxStyle={syntax()} diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 87c0a63abc82..ac5bafcc66e6 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -101,6 +101,24 @@ export namespace Clipboard { } } + export async function readPrimary(): Promise { + const os = platform() + if (os === "linux") { + if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-paste")) { + const text = await $`wl-paste --primary --no-newline`.nothrow().text() + if (text) return text + } + if (Bun.which("xclip")) { + const text = await $`xclip -selection primary -o`.nothrow().text() + if (text) return text + } + if (Bun.which("xsel")) { + const text = await $`xsel --primary --output`.nothrow().text() + if (text) return text + } + } + } + const getCopyMethod = lazy(() => { const os = platform() From 82d54654d5a3d98565890268c8b7b57da7a5cb54 Mon Sep 17 00:00:00 2001 From: Armand Lynch Date: Mon, 16 Mar 2026 08:01:18 -0400 Subject: [PATCH 2/3] fix(tui): import $ from bun in clipboard --- packages/opencode/src/cli/cmd/tui/util/clipboard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index ac5bafcc66e6..7af171066831 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -1,3 +1,4 @@ +import { $ } from "bun" import { platform, release } from "os" import clipboardy from "clipboardy" import { lazy } from "../../../../util/lazy.js" From aed42472ac58121d50e16951ba255585b15e95d4 Mon Sep 17 00:00:00 2001 From: Armand Lynch Date: Sun, 22 Mar 2026 11:24:04 -0400 Subject: [PATCH 3/3] fix(tui): update clipboard and paste handling for opentui 0.1.88 --- .../src/cli/cmd/tui/util/clipboard.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 7af171066831..4668f168ca61 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -1,4 +1,3 @@ -import { $ } from "bun" import { platform, release } from "os" import clipboardy from "clipboardy" import { lazy } from "../../../../util/lazy.js" @@ -103,20 +102,18 @@ export namespace Clipboard { } export async function readPrimary(): Promise { - const os = platform() - if (os === "linux") { - if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-paste")) { - const text = await $`wl-paste --primary --no-newline`.nothrow().text() - if (text) return text - } - if (Bun.which("xclip")) { - const text = await $`xclip -selection primary -o`.nothrow().text() - if (text) return text - } - if (Bun.which("xsel")) { - const text = await $`xsel --primary --output`.nothrow().text() - if (text) return text - } + if (platform() !== "linux") return + if (process.env["WAYLAND_DISPLAY"] && which("wl-paste")) { + const result = await Process.text(["wl-paste", "--primary", "--no-newline"], { nothrow: true }) + if (result.text) return result.text + } + if (which("xclip")) { + const result = await Process.text(["xclip", "-selection", "primary", "-o"], { nothrow: true }) + if (result.text) return result.text + } + if (which("xsel")) { + const result = await Process.text(["xsel", "--primary", "--output"], { nothrow: true }) + if (result.text) return result.text } }