anomalyco / anomalyco/opencode

clipboard.write() never settles on X11 when xclip is the backend

Open
#43,697 3 comments 0 reactions 1 assignee View on GitHub

@simonklee is already working on this.

Since Aug 20, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

Description

On X11 with xclip installed, copying in the TUI puts the text on the clipboard but clipboard.write() never settles. The copy itself works. The promise just sits there. I watched one stay pending for over eight minutes after a single copy, and it only cleared when another program took the X clipboard selection.

The cause is that xclip has to stay alive to own the X selection, which is how X selections work. It forks a background process and the original exits. That background process inherits the stdout pipe opencode gave it and keeps it open. command() in packages/tui/src/clipboard.ts resolves on the child's close event, and close waits for both the process to exit and its stdio to close. The stdio never closes, so it never fires.

Here is the proof from my machine, well after the copy finished. xclip's stdout is still paired to the opencode process and still established.

u_str ESTAB * 1733272 * 1733273 users:(("opencode",pid=269746,fd=64))
u_str ESTAB * 1733273 * 1733272 users:(("xclip",pid=272586,fd=1))

This is the default path on X11. copyCommand checks xclip before xsel, so xclip wins whenever it's installed, and the troubleshooting page tells people to install it. xsel does not behave this way. clipboardy has a comment in its linux backend saying it uses xsel instead of xclip for exactly this reason.

Two places it shows up in the UI. dialog-message.tsx awaits clipboard.write?.(text) and then calls dialog.clear(), so the dialog can sit open. error-component.tsx runs .then(() => setCopied(true)), so you get no confirmation even though the copy worked.

Worth flagging for #41924. That fix doesn't reach this. A pending promise never rejects, so there's no error to surface.

The fix is to stop piping stdout on the write path. command() always passes stdio: [..., "pipe", "ignore"], and stdout is only needed for reads. Using "ignore" for writes made it resolve in 3ms for me. Adding -i to the xclip arguments does not help, which is worth saying because that was part of #35289.

Plugins

None

OpenCode version

1.18.19

Steps to reproduce

  1. On X11, install xclip. It doesn't matter whether xsel is installed, since copyCommand checks xclip first.
  2. Start opencode and copy a message.
  3. Paste somewhere. The text is there, so the copy worked.
  4. Run pgrep -ax xclip. The xclip process is still running.
  5. Run ss -xp | grep xclip. Its fd 1 is still paired to the opencode process.
  6. Copy something in any other program. xclip exits and the promise settles.

If driving the TUI is awkward, this reproduces it with command() copied straight out of clipboard.ts.

import { spawn } from "node:child_process"

function command(cmd, args = [], input, stdoutMode = "pipe") {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { stdio: [input === undefined ? "ignore" : "pipe", stdoutMode, "ignore"] })
const out = []
child.on("error", reject)
child.stdout?.on("data", (c) => out.push(c))
child.on("close", (code) => (code === 0 ? resolve(Buffer.concat(out)) : reject(new Error("code " + code))))
if (input !== undefined) child.stdin?.end(input)
})
}

const run = (mode) =>
Promise.race([
command("xclip", ["-selection", "clipboard"], "payload", mode).then(() => "resolved"),
new Promise((r) => setTimeout(() => r("HUNG"), 5000)),
])

console.log("stdout piped :", await run("pipe")) // HUNG
console.log("stdout ignored:", await run("ignore")) // resolved
process.exit(0)

Screenshot and/or share link

No response

Operating System

Kubuntu 24.04, X11, xclip 0.13-3

Terminal

Konsole 23.08.5

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.