diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index f85175cb7909..adb707839de3 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -119,6 +119,17 @@ export const Info = Schema.Struct({ description: "Additional instruction files or patterns to include", }), layout: Schema.optional(ConfigLayoutV1.Layout).annotate({ description: "@deprecated Always uses stretch layout." }), + clipboard: Schema.optional( + Schema.Struct({ + linux: Schema.optional( + Schema.Struct({ + enablePrimaryCopy: Schema.optional(Schema.Boolean).annotate({ + description: "Copy to primary clipboard in addition to regular clipboard on Linux (Wayland/X11)", + }), + }), + ), + }), + ).annotate({ description: "Clipboard configuration" }), permission: Schema.optional(ConfigPermissionV1.Info), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), attachment: Schema.optional(ConfigAttachmentV1.Info).annotate({ diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 7cda89dbabb4..fd6f616cc9ad 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -185,8 +185,14 @@ export const TuiThreadCommand = cmd({ try { const { Effect } = await import("effect") + const { Config } = await import("@/config/config") + const { AppRuntime } = await import("@/effect/app-runtime") const { run } = await import("../tui/layer") const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime") + const clipboardConfig = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.get())).catch(() => ({})) + if (clipboardConfig.clipboard?.linux?.enablePrimaryCopy) { + process.env.OPENCODE_CLIPBOARD_PRIMARY_COPY = "true" + } await Effect.runPromise( run({ url: transport.url, diff --git a/packages/tui/src/clipboard.ts b/packages/tui/src/clipboard.ts index 08f86f9f7a97..4d1468dd5ee4 100644 --- a/packages/tui/src/clipboard.ts +++ b/packages/tui/src/clipboard.ts @@ -77,11 +77,14 @@ export function copyCommand( os: NodeJS.Platform, wayland: boolean, has: (name: string) => boolean, + primary?: boolean, ): string[] | undefined { if (os === "darwin" && has("osascript")) return ["osascript"] - if (os === "linux" && wayland && has("wl-copy")) return ["wl-copy"] - if (os === "linux" && has("xclip")) return ["xclip", "-selection", "clipboard"] - if (os === "linux" && has("xsel")) return ["xsel", "--clipboard", "--input"] + if (os === "linux" && wayland && has("wl-copy")) return primary ? ["wl-copy", "--primary"] : ["wl-copy"] + if (os === "linux" && has("xclip")) + return primary ? ["xclip", "-selection", "primary"] : ["xclip", "-selection", "clipboard"] + if (os === "linux" && has("xsel")) + return primary ? ["xsel", "--primary", "--input"] : ["xsel", "--clipboard", "--input"] if (os === "win32" && has("powershell.exe")) { return [ "powershell.exe", @@ -117,8 +120,18 @@ function getCopyMethod() { })()) } +async function writePrimary(text: string) { + const { which } = await import("@opencode-ai/core/util/which") + const native = copyCommand(platform(), Boolean(process.env.WAYLAND_DISPLAY), (name) => Boolean(which(name)), true) + if (!native) return + await command(native[0], native.slice(1), text).catch(() => undefined) +} + export async function write(text: string) { writeOsc52(text) const method = await getCopyMethod() await method(text) + if (process.env.OPENCODE_CLIPBOARD_PRIMARY_COPY === "true") { + await writePrimary(text) + } }