From 3822c21194f6750b79a0e8644f89c34698588580 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sun, 12 Jul 2026 11:20:49 +0100 Subject: [PATCH] fix(clipboard): auto-clear must not wipe content copied since our SetText Copy a password (30s auto-clear armed), then copy something else from another app before the timer fires: the tick emptied the clipboard anyway, destroying the user's newer content. Guard with the Win32 clipboard sequence number: SetText snapshots GetClipboardSequenceNumber, ClearIfOurs only empties when it hasn't moved. Applied to the auto-clear timer AND the clear-on-minimize path (same bug class); the explicit JS clipboard/clear command stays unconditional (user-initiated). Co-Authored-By: Claude Opus 4.8 --- delphi-backend/Source/PM.Bridge.pas | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/delphi-backend/Source/PM.Bridge.pas b/delphi-backend/Source/PM.Bridge.pas index d166fa1..402b730 100644 --- a/delphi-backend/Source/PM.Bridge.pas +++ b/delphi-backend/Source/PM.Bridge.pas @@ -41,6 +41,9 @@ type TSecureClipboard = class private FClearTimer: TTimer; + // Clipboard sequence number right after OUR SetText. If it moved, the + // user copied something else since — that content is theirs, not ours. + FSetSequence: DWORD; procedure ClearTimerTick(Sender: TObject); public constructor Create; @@ -50,6 +53,10 @@ type // AClearAfterMs = 0 disables auto-clear; default is 30 seconds. procedure SetText(const AText: string; AClearAfterMs: Integer = 30000); procedure Clear; + // Clear only if the clipboard still holds what WE put there (sequence + // unchanged). Auto-clear timer + minimize-to-tray use this so they never + // wipe something the user copied from another app in the meantime. + procedure ClearIfOurs; end; // Distinguishes the "fill everything" hotkey (Ctrl+Shift+L) from the @@ -341,7 +348,7 @@ end; procedure TSecureClipboard.ClearTimerTick(Sender: TObject); begin FClearTimer.Enabled := False; - Clear; + ClearIfOurs; end; procedure TSecureClipboard.SetText(const AText: string; AClearAfterMs: Integer); @@ -385,6 +392,10 @@ begin CloseClipboard; end; + // Snapshot the sequence AFTER our write — any later copy (ours or another + // app's) bumps it, which is exactly the "is it still ours?" signal. + FSetSequence := GetClipboardSequenceNumber; + if AClearAfterMs > 0 then begin FClearTimer.Interval := AClearAfterMs; @@ -402,6 +413,12 @@ begin end; end; +procedure TSecureClipboard.ClearIfOurs; +begin + if GetClipboardSequenceNumber = FSetSequence then + Clear; +end; + function TSecureClipboard.ReadText: string; var H: THandle; @@ -581,9 +598,10 @@ begin // out of sight. SKIPPED when AClearClipboard=False — the quick-search // copy-then-hide flow deliberately keeps the password on the clipboard // (the 30s auto-clear timer still guards it) so the user can paste it - // into their target app after we minimise. + // into their target app after we minimise. ClearIfOurs: never wipe + // content the user copied from another app since. if AClearClipboard then - FSecureClipboard.Clear; + FSecureClipboard.ClearIfOurs; LFormHwnd := MainFormHWND(FMainForm); LAppHwnd := FindFMXAppWindow;