From 04bcafc1b69ab3d5656912bfb9011123977cb175 Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Sat, 30 May 2026 03:20:20 +0200 Subject: [PATCH] fix(clipboard): route terminal copy through main process for Wayland navigator.clipboard.writeText is gated on focus/user-activation and silently fails on Linux/Wayland (Ozone). Replace with a main-process clipboard.writeText via a new clipboard-write-text IPC. Also register an xterm OSC 52 handler so programs inside the terminal (notably Claude Code's copy-to-clipboard) can set the system clipboard; xterm doesn't wire OSC 52 itself. Port of upstream PR doctly/switchboard#55 (ymajoros). Closes upstream issue #54 from our fork's perspective; the upstream PR will close it fully when merged. --- main.js | 10 +++++++++- preload.js | 1 + public/terminal-manager.js | 20 +++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 8f688cdc..b863e7cb 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } = require('electron'); +const { app, BrowserWindow, clipboard, dialog, ipcMain, Menu, screen, shell } = require('electron'); const { Worker } = require('worker_threads'); const { execFile } = require('child_process'); const path = require('path'); @@ -471,6 +471,14 @@ ipcMain.handle('open-external', (_event, url) => { if (/^https?:\/\//i.test(url)) return shell.openExternal(url); }); +// --- IPC: clipboard write --- +// The renderer's navigator.clipboard.writeText is gated on focus/user-activation and +// is flaky-to-dead on Linux/Wayland (Ozone). The main-process clipboard has no such +// strings attached, so all terminal copies go through here. +ipcMain.handle('clipboard-write-text', (_event, text) => { + if (typeof text === 'string') clipboard.writeText(text); +}); + // --- IPC: MCP bridge --- ipcMain.on('mcp-diff-response', (_event, sessionId, diffId, action, editedContent) => { resolvePendingDiff(sessionId, diffId, action, editedContent); diff --git a/preload.js b/preload.js index c5163363..4b24ef02 100644 --- a/preload.js +++ b/preload.js @@ -47,6 +47,7 @@ contextBridge.exposeInMainWorld('api', { deleteWorktree: (worktreePath) => ipcRenderer.invoke('delete-worktree', worktreePath), worktreeStatus: (worktreePath) => ipcRenderer.invoke('worktree-status', worktreePath), openExternal: (url) => ipcRenderer.invoke('open-external', url), + writeClipboard: (text) => ipcRenderer.invoke('clipboard-write-text', text), // Send (fire-and-forget) sendInput: (id, data) => ipcRenderer.send('terminal-input', id, data), diff --git a/public/terminal-manager.js b/public/terminal-manager.js index 6863b226..ea401cae 100644 --- a/public/terminal-manager.js +++ b/public/terminal-manager.js @@ -63,7 +63,7 @@ function setupTerminalKeyBindings(terminal, container, getSessionId, { onFind } if (!isMac && e.key === 'c' && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { if (terminal.hasSelection()) { if (e.type === 'keydown') { - navigator.clipboard.writeText(terminal.getSelection()).catch(() => {}); + window.api.writeClipboard(terminal.getSelection()); } return false; } @@ -191,6 +191,24 @@ function createTerminalEntry(session) { }, }); + // OSC 52 — let the program inside the terminal set the system clipboard (this is how + // Claude Code copies). xterm doesn't wire this up itself, so we do. Payload is + // ";" (or ";?" for a read-back query, which we ignore). + // Route through the main process — see writeClipboard — because the renderer clipboard + // is unreliable on Wayland. + terminal.parser.registerOscHandler(52, (payload) => { + const sep = payload.indexOf(';'); + const b64 = sep === -1 ? payload : payload.slice(sep + 1); + if (!b64 || b64 === '?') return true; + try { + const bytes = Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0)); + window.api.writeClipboard(new TextDecoder().decode(bytes)); + } catch { + return false; + } + return true; + }); + const fitAddon = new FitAddon.FitAddon(); terminal.loadAddon(fitAddon); terminal.loadAddon(new WebLinksAddon.WebLinksAddon((_event, url) => {