diff --git a/src/main/window/clipboard-text-publisher.test.ts b/src/main/window/clipboard-text-publisher.test.ts index bc3ca7111a6..e9ce5a06d05 100644 --- a/src/main/window/clipboard-text-publisher.test.ts +++ b/src/main/window/clipboard-text-publisher.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ProcessResult, ProcessSpec } from '@orca/process-host/process-spec' import { assertClipboardTextWriteWithinLimitWithYield, + CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS, CLIPBOARD_TEXT_WRITE_MAX_BYTES } from '../../shared/clipboard-text' import { createClipboardTextPublisher } from './clipboard-text-publisher' @@ -127,6 +128,37 @@ describe('terminal clipboard writes', () => { } ) + it.each([false, true])( + 'keeps a newer copy after yielding validation (Wayland=%s)', + async (wayland) => { + vi.useFakeTimers() + try { + const copy = writer(wayland) + const large = 'x'.repeat(CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS * 2) + const first = copy(large, 'terminal') + const second = copy('newer', 'ordinary') + + await vi.runAllTimersAsync() + await Promise.all([first, second]) + + if (wayland) { + expect(run.mock.calls.map(([spec]) => spec.input === large)).toEqual([true, false]) + expect(run.mock.calls[1]?.[0].input).toBe('newer') + expect(write).not.toHaveBeenCalled() + } else { + expect(write.mock.calls.map(([text, kind]) => [text === large, kind])).toEqual([ + [true, 'terminal'], + [false, 'ordinary'] + ]) + expect(write.mock.calls[1]?.[0]).toBe('newer') + expect(run).not.toHaveBeenCalled() + } + } finally { + vi.useRealTimers() + } + } + ) + it('preserves request order across yielding validation and resumes after failure', async () => { let release: (text: string) => void = () => undefined const validate = vi.fn(async (text: string) => text) diff --git a/src/main/window/clipboard-text-publisher.ts b/src/main/window/clipboard-text-publisher.ts index c665b61354c..99ccafbae96 100644 --- a/src/main/window/clipboard-text-publisher.ts +++ b/src/main/window/clipboard-text-publisher.ts @@ -40,21 +40,25 @@ export function createClipboardTextPublisher( if (text.length > CLIPBOARD_TEXT_WRITE_MAX_BYTES) { return Promise.reject(new Error(CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR)) } - if (!deps.isWayland()) { - return deps.validate(text).then((safeText) => deps.write(safeText, kind)) + const wayland = deps.isWayland() + if (wayland) { + if ( + count >= MAX_PENDING_CLIPBOARD_WRITES || + text.length > CLIPBOARD_TEXT_WRITE_MAX_BYTES - queuedCodeUnits + ) { + return Promise.reject(new Error('Too many pending clipboard writes')) + } + count += 1 + queuedCodeUnits += text.length } - if ( - count >= MAX_PENDING_CLIPBOARD_WRITES || - text.length > CLIPBOARD_TEXT_WRITE_MAX_BYTES - queuedCodeUnits - ) { - return Promise.reject(new Error('Too many pending clipboard writes')) - } - count += 1 - queuedCodeUnits += text.length // Validate inside the queue: large copies yield and must not overwrite a later request. const next = pending .then(async () => { const safeText = await deps.validate(text) + if (!wayland) { + deps.write(safeText, kind) + return + } const result = await deps.run({ program: await deps.helperPath(), input: safeText, @@ -68,8 +72,10 @@ export function createClipboardTextPublisher( deps.clearCache() }) .finally(() => { - count -= 1 - queuedCodeUnits -= text.length + if (wayland) { + count -= 1 + queuedCodeUnits -= text.length + } }) pending = next.catch(() => undefined) return next