From 49aefb235fa5e5176e651c57532fd56830946aa4 Mon Sep 17 00:00:00 2001 From: JIANZHOU Date: Sat, 16 May 2026 20:18:49 +0800 Subject: [PATCH] fix clipboard copy failure reporting --- .../src/cli/cmd/tui/util/clipboard.ts | 8 +-- .../test/cli/cmd/tui/clipboard.test.ts | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/test/cli/cmd/tui/clipboard.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index be3cec14c6a2..d211e1610da9 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -12,11 +12,11 @@ import * as Process from "../../../../util/process" const writeWithStdin = (cmd: string[], text: string): Promise => Effect.runPromise( AppProcess.Service.use((svc) => svc.run(ChildProcess.make(cmd[0]!, cmd.slice(1)), { stdin: text })).pipe( + Effect.flatMap(AppProcess.requireSuccess), Effect.provide(AppProcess.defaultLayer), - Effect.catch(() => Effect.void), Effect.asVoid, ), - ).catch(() => undefined) + ) // Lazy load which and clipboardy to avoid expensive execa/which/isexe chain at startup const getWhich = lazy(async () => { @@ -130,7 +130,7 @@ const getCopyMethod = lazy(async () => { console.log("clipboard: using osascript") return async (text: string) => { const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') - await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true }) + await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`]) } } @@ -168,7 +168,7 @@ const getCopyMethod = lazy(async () => { console.log("clipboard: no native support") return async (text: string) => { const clipboardy = await getClipboardy() - await clipboardy.write(text).catch(() => {}) + await clipboardy.write(text) } }) diff --git a/packages/opencode/test/cli/cmd/tui/clipboard.test.ts b/packages/opencode/test/cli/cmd/tui/clipboard.test.ts new file mode 100644 index 000000000000..fad105f0ce78 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/clipboard.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test" +import { chmod, mkdtemp, rm, writeFile } from "fs/promises" +import { tmpdir } from "os" +import path from "path" +import { pathToFileURL } from "url" + +describe("TUI clipboard", () => { + test("rejects when the selected native clipboard command fails", async () => { + const directory = await mkdtemp(path.join(tmpdir(), "opencode-clipboard-")) + + try { + const xclip = path.join(directory, "xclip") + await writeFile(xclip, "#!/bin/sh\nexit 7\n") + await chmod(xclip, 0o755) + + const clipboard = path.resolve(import.meta.dir, "../../../../src/cli/cmd/tui/util/clipboard.ts") + const script = path.join(directory, "copy-fails.mjs") + await writeFile( + script, + ` +import * as Clipboard from ${JSON.stringify(pathToFileURL(clipboard).href)} + +try { + await Clipboard.copy("hello") + console.error("copy resolved") + process.exit(1) +} catch { + process.exit(0) +} +`.trimStart(), + ) + + const proc = Bun.spawn([process.execPath, script], { + env: { + ...process.env, + PATH: `${directory}${path.delimiter}${process.env.PATH ?? ""}`, + WAYLAND_DISPLAY: "", + }, + stdout: "pipe", + stderr: "pipe", + }) + const [stdout, stderr, code] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]) + + expect(`${stdout}${stderr}`).not.toContain("copy resolved") + expect(code).toBe(0) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) +})