fix(autofill): refusing a request must not move any window

The busy guard cancelled through the path that hands the foreground back to the captured target. During an export that pushed the native Save dialog behind the other application, leaving the app disabled by a modal the user could no longer find. refocus=0 separates refusing a request from ending a user interaction.
This commit is contained in:
r-zakarya
2026-08-01 18:30:00 +01:00
parent 03cc7adc25
commit 42d2d6fda5
4 changed files with 25 additions and 7 deletions
+7 -1
View File
@@ -175,7 +175,13 @@ plus bas.
- **Pourquoi les hotkeys échappent aux gardes UI** : ils arrivent de Win32, **hors de la fenêtre**. L'overlay qui couvre l'application n'intercepte rien : le panneau s'ouvrait derrière lui, inaccessible. - **Pourquoi les hotkeys échappent aux gardes UI** : ils arrivent de Win32, **hors de la fenêtre**. L'overlay qui couvre l'application n'intercepte rien : le panneau s'ouvrait derrière lui, inaccessible.
- **Où est posé le garde** : dans `openSlideOver` et `openQuickSearchModal`, pas sur chaque déclencheur — c'est le point de passage de *tous* les appelants (hotkey, palette de commandes, bouton *Fix* du vault health). Sûr parce qu'**aucun chemin de sauvegarde n'affiche l'overlay** : rien de légitime n'ouvre l'éditeur pendant une opération bloquante. - **Où est posé le garde** : dans `openSlideOver` et `openQuickSearchModal`, pas sur chaque déclencheur — c'est le point de passage de *tous* les appelants (hotkey, palette de commandes, bouton *Fix* du vault health). Sûr parce qu'**aucun chemin de sauvegarde n'affiche l'overlay** : rien de légitime n'ouvre l'éditeur pendant une opération bloquante.
- **ACTION 2 — autofill** : pendant l'overlay, `Ctrl+Shift+L`, `Ctrl+Shift+P`, `Ctrl+Shift+T`, avec un titre à **un seul** candidat puis à **plusieurs**. - **ACTION 2 — autofill** : pendant l'overlay, `Ctrl+Shift+L`, `Ctrl+Shift+P`, `Ctrl+Shift+T`, avec un titre à **un seul** candidat puis à **plusieurs**.
- **ATTENDU** : rien ne se remplit, aucun picker, même toast. Le garde est dans `autofillHandleRequest`, en amont du choix match-unique / picker, donc il couvre les trois hotkeys et les deux issues. Il appelle `Bridge.cancelAutofill()` pour que Delphi ne garde pas la fenêtre cible capturée en attente d'un remplissage qui ne viendra jamais. - **ATTENDU** : rien ne se remplit, aucun picker, même toast. Le garde est dans `autofillHandleRequest`, en amont du choix match-unique / picker, donc il couvre les trois hotkeys et les deux issues. Il appelle `Bridge.cancelAutofill(false)` pour que Delphi ne garde pas la fenêtre cible capturée en attente d'un remplissage qui ne viendra jamais.
### B17 · hotkeys pendant la boîte « Enregistrer sous » de l'export
- **ACTION** : lancer un *Export* → à l'apparition du dialogue natif **Enregistrer sous**, basculer sur une autre application (VS Code) → presser les hotkeys d'autofill → revenir à l'app.
- **ATTENDU** : le dialogue est toujours là, au premier plan de l'application, et répond. L'app n'est cliquable qu'à travers lui — c'est normal, une boîte native modale désactive sa fenêtre propriétaire tant qu'on n'a pas répondu.
- **Historique** : le refus « busy » appelait `cancelAutofill()`, dont le chemin Delphi fait un `SetForegroundWindow` vers la fenêtre capturée. Le dialogue passait **derrière** l'autre application, et l'app paraissait figée : bloquée par une modale devenue introuvable. `refocus=0` distingue désormais **refuser** une demande (ne rien déplacer) de **terminer** une interaction utilisateur (rendre le focus à la cible).
- **Généralisation** : un refus ne doit jamais déplacer une fenêtre. Tout nouveau garde qui annule une action doit passer par ce chemin.
### B14 · dialogues natifs ouverts par un clic programmatique ### B14 · dialogues natifs ouverts par un clic programmatique
- **ACTION** : `A` ouverte + **dirty** → (a) Settings → *Import vault* ; (b) télécharger une pièce jointe (fallback web) ; (c) *Upload icon* ; (d) joindre un fichier. - **ACTION** : `A` ouverte + **dirty** → (a) Settings → *Import vault* ; (b) télécharger une pièce jointe (fallback web) ; (c) *Upload icon* ; (d) joindre un fichier.
+7 -2
View File
@@ -1093,13 +1093,18 @@ begin
// foreground process at that moment, so plain SetForegroundWindow works. // foreground process at that moment, so plain SetForegroundWindow works.
else if ACmd = 'autofill/cancel' then else if ACmd = 'autofill/cancel' then
begin begin
if FAutofillTargetHWND <> 0 then // refocus=0 — JS is REFUSING the request, not ending a user interaction:
// nothing must move on screen. During an export a native Save dialog owns
// the foreground, and pushing it behind leaves the app disabled by a modal
// the user can no longer find.
var LRefocus := GetParam('refocus') <> '0';
if LRefocus and (FAutofillTargetHWND <> 0) then
SetForegroundWindow(FAutofillTargetHWND); SetForegroundWindow(FAutofillTargetHWND);
FAutofillTargetHWND := 0; FAutofillTargetHWND := 0;
// The window only came out of the tray to show the picker, so dismissing it // The window only came out of the tray to show the picker, so dismissing it
// puts it back — same as quick-search, which hides on Esc too. Done after // puts it back — same as quick-search, which hides on Esc too. Done after
// handing focus to the target above. // handing focus to the target above.
if FAutofillHotkeyWasHidden then if FAutofillHotkeyWasHidden and LRefocus then
begin begin
FAutofillHotkeyWasHidden := False; FAutofillHotkeyWasHidden := False;
// Foreground already handed to the target above. // Foreground already handed to the target above.
+4 -1
View File
@@ -242,7 +242,10 @@ async function autofillHandleRequest(windowTitle, kind) {
// window and hand the foreground away mid-operation. cancelAutofill so // window and hand the foreground away mid-operation. cancelAutofill so
// Delphi doesn't keep the captured target HWND for a fill that never comes. // Delphi doesn't keep the captured target HWND for a fill that never comes.
if (typeof isBusy === 'function' && isBusy()) { if (typeof isBusy === 'function' && isBusy()) {
Bridge.cancelAutofill(); // refocus=false: refusing must not move any window. During an export a
// native Save dialog holds the foreground, and shoving it behind leaves
// the app disabled by a modal the user can no longer find.
Bridge.cancelAutofill(false);
toast('Busy — try again in a moment', 'warning'); toast('Busy — try again in a moment', 'warning');
return; return;
} }
+7 -3
View File
@@ -177,10 +177,14 @@ const Bridge = (() => {
cmd('cmd://app/theme?mode=' + (mode === 'light' ? 'light' : 'dark')); cmd('cmd://app/theme?mode=' + (mode === 'light' ? 'light' : 'dark'));
}, },
// Tell Delphi we couldn't find a match / user cancelled. // Tell Delphi we couldn't find a match / user cancelled. It normally
cancelAutofill() { // hands the foreground back to the captured target — pass false when
// merely REFUSING a request, so nothing on screen moves. A native Save
// dialog owns the foreground during an export, and pushing it behind
// leaves the app disabled by a modal the user can no longer see.
cancelAutofill(refocus = true) {
if (!active) return; if (!active) return;
cmd('cmd://autofill/cancel'); cmd('cmd://autofill/cancel' + (refocus ? '' : '?refocus=0'));
}, },
// Sync the hotkey registration state with this device's preference. // Sync the hotkey registration state with this device's preference.