fix: route clipboard writes through Electron IPC to prevent NSPasteboard contention (#83)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a4ef06b18f
commit
a0d8ba1e18
|
|
@ -272,10 +272,27 @@ app.whenReady().then(() => {
|
|||
warmSystemFontFamilies()
|
||||
setupAutoUpdater(mainWindow, { onBeforeQuit: () => store?.flush() })
|
||||
|
||||
// Clipboard: read text via Electron's native clipboard module so the
|
||||
// renderer can bypass Chromium's clipboard pipeline (which holds
|
||||
// NSPasteboard references and causes contention with CLI tools like Codex).
|
||||
// Clipboard: route all clipboard operations through Electron's native
|
||||
// clipboard module so the renderer bypasses Chromium's clipboard pipeline.
|
||||
// Chromium holds NSPasteboard references during format conversion and
|
||||
// change-detection polling, which causes concurrent clipboard reads by CLI
|
||||
// tools (e.g. Codex checking for images on Enter) to fail intermittently.
|
||||
ipcMain.handle('clipboard:readText', () => clipboard.readText())
|
||||
ipcMain.handle('clipboard:writeText', (_event, text: string) => clipboard.writeText(text))
|
||||
|
||||
// Deny clipboard-read permission to the renderer so Chromium does not
|
||||
// autonomously poll NSPasteboard (e.g. for Edit-menu state or async
|
||||
// Clipboard API calls). All clipboard access now goes through the IPC
|
||||
// handlers above, eliminating contention with CLI subprocesses.
|
||||
mainWindow.webContents.session.setPermissionRequestHandler(
|
||||
(_webContents, permission, callback) => {
|
||||
if (permission === 'clipboard-read' || permission === 'clipboard-sanitized-write') {
|
||||
callback(false)
|
||||
return
|
||||
}
|
||||
callback(true)
|
||||
}
|
||||
)
|
||||
|
||||
// Updater IPC
|
||||
ipcMain.handle('updater:getStatus', () => getUpdateStatus())
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ type UIApi = {
|
|||
onOpenSettings: (callback: () => void) => () => void
|
||||
onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void
|
||||
readClipboardText: () => Promise<string>
|
||||
writeClipboardText: (text: string) => Promise<void>
|
||||
onFileDrop: (callback: (data: { path: string }) => void) => () => void
|
||||
getZoomLevel: () => number
|
||||
setZoomLevel: (level: number) => void
|
||||
|
|
|
|||
|
|
@ -256,6 +256,8 @@ const api = {
|
|||
return () => ipcRenderer.removeListener('terminal:zoom', listener)
|
||||
},
|
||||
readClipboardText: (): Promise<string> => ipcRenderer.invoke('clipboard:readText'),
|
||||
writeClipboardText: (text: string): Promise<void> =>
|
||||
ipcRenderer.invoke('clipboard:writeText', text),
|
||||
onFileDrop: (callback: (data: { path: string }) => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, data: { path: string }) => callback(data)
|
||||
ipcRenderer.on('terminal:file-drop', listener)
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export default function MonacoEditor({
|
|||
<DropdownMenuContent sideOffset={0} align="start">
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigator.clipboard.writeText(`${filePath}#L${gutterMenuLine}`)
|
||||
window.api.ui.writeClipboardText(`${filePath}#L${gutterMenuLine}`)
|
||||
}}
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5 mr-1.5" />
|
||||
|
|
@ -195,7 +195,7 @@ export default function MonacoEditor({
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigator.clipboard.writeText(`${relativePath}#L${gutterMenuLine}`)
|
||||
window.api.ui.writeClipboardText(`${relativePath}#L${gutterMenuLine}`)
|
||||
}}
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5 mr-1.5" />
|
||||
|
|
|
|||
|
|
@ -81,7 +81,9 @@ export function FileResultItem({
|
|||
</button>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={() => navigator.clipboard.writeText(fileResult.relativePath)}>
|
||||
<ContextMenuItem
|
||||
onClick={() => window.api.ui.writeClipboardText(fileResult.relativePath)}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
Copy Path
|
||||
</ContextMenuItem>
|
||||
|
|
@ -151,7 +153,7 @@ export function MatchItem({
|
|||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(`${relativePath}#L${match.line}`)}
|
||||
onClick={() => window.api.ui.writeClipboardText(`${relativePath}#L${match.line}`)}
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
Copy Line Path
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const WorktreeContextMenu = React.memo(function WorktreeContextMenu({ worktree,
|
|||
}, [worktree.path])
|
||||
|
||||
const handleCopyPath = useCallback(() => {
|
||||
navigator.clipboard.writeText(worktree.path)
|
||||
window.api.ui.writeClipboardText(worktree.path)
|
||||
}, [worktree.path])
|
||||
|
||||
const handleToggleRead = useCallback(() => {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export default function EditorFileTab({
|
|||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigator.clipboard.writeText(file.filePath)
|
||||
window.api.ui.writeClipboardText(file.filePath)
|
||||
}}
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5 mr-1.5" />
|
||||
|
|
@ -123,7 +123,7 @@ export default function EditorFileTab({
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigator.clipboard.writeText(file.relativePath)
|
||||
window.api.ui.writeClipboardText(file.relativePath)
|
||||
}}
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5 mr-1.5" />
|
||||
|
|
@ -134,7 +134,7 @@ export default function EditorFileTab({
|
|||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigator.clipboard.writeText(`${file.filePath}#L${cursorLine}`)
|
||||
window.api.ui.writeClipboardText(`${file.filePath}#L${cursorLine}`)
|
||||
}}
|
||||
>
|
||||
<MapPin className="w-3.5 h-3.5 mr-1.5" />
|
||||
|
|
@ -142,7 +142,7 @@ export default function EditorFileTab({
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigator.clipboard.writeText(`${file.relativePath}#L${cursorLine}`)
|
||||
window.api.ui.writeClipboardText(`${file.relativePath}#L${cursorLine}`)
|
||||
}}
|
||||
>
|
||||
<MapPin className="w-3.5 h-3.5 mr-1.5" />
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function useTerminalPaneContextMenu({
|
|||
}
|
||||
const selection = pane.terminal.getSelection()
|
||||
if (selection) {
|
||||
await navigator.clipboard.writeText(selection)
|
||||
await window.api.ui.writeClipboardText(selection)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue