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
- On X11, install xclip. It doesn't matter whether xsel is installed, since copyCommand checks xclip first.
- Start opencode and copy a message.
- Paste somewhere. The text is there, so the copy worked.
- Run pgrep -ax xclip. The xclip process is still running.
- Run ss -xp | grep xclip. Its fd 1 is still paired to the opencode process.
- 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
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
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