Commit Graph

31 Commits

Author SHA1 Message Date
r-zakarya 33d8df3dee fix(tray): give the foreground back to the application we took it from
Hiding a window hands the foreground to nobody, so we stayed foreground while
invisible. The next hotkey then captured our own window as its target and typed
the password into it, and Ctrl+Shift+A opened a New entry with an empty title —
the guard correctly refusing to prefill with "Password Manager".

Track foreground changes with an EVENT_SYSTEM_FOREGROUND hook and keep the last
window that is neither ours nor a shell one, then reactivate it just before
hiding. Sampling on demand cannot work: by then the user's application is two
foreground changes behind (their app, the taskbar they clicked, us), and
reactivating after the hide is silently refused since only the current
foreground owner may give focus away.

Autofill paths pass AReturnForeground=False — they already placed the
foreground on the window they filled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:10:22 +01:00
r-zakarya a9e56c52b5 fix(autofill): send the window back to the tray when the picker is dismissed
Cancelling left the window on the desktop, though it had only been restored to
ask which entry to fill. Quick-search already hides itself on Esc; this makes
the picker symmetric. Applies to all three hotkeys — the capture and the hide
both sit on the shared BridgeAutofillRequest / autofill/cancel paths, so full,
password-only and TOTP behave identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:11:43 +01:00
r-zakarya 3c84bdca49 fix(autofill): return to the tray after picking in the multi-match picker
Only the quick-search path captured whether the window was tray-hidden and fed
it back as hide_after; JS never sends that parameter from the autofill picker,
so Ctrl+Shift+L pulled the window out of the tray and left it on the desktop.

Capture it in BridgeAutofillRequest instead, when the hotkey fires — by execute
time the picker has already restored the window and the state is gone. Same
rule as quick-search: tray-hidden only, a taskbar-minimised window is not sent
to the tray it never came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:03:57 +01:00
r-zakarya e98acabbbd revert(winstate): drop the parked restored-size rect, it left a stub window
Widening rcNormalPosition while the window was down did remove the restore
flash, but SetWindowPlacement left a small stub frame in a corner of the screen
whenever the window was maximised — during the minimise animation and equally
once the window was already iconic. A permanent visible artefact is worse than
an ugly animation, so this goes back to the validated behaviour: correct final
state, flash still present.

The flash itself is an FMX bug, reproducible in an empty FMX app. Rationale and
the five rejected approaches are recorded in TEST_SCENARIOS.md I1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:51:28 +01:00
r-zakarya 17e817f8ab fix(winstate): park the restored-size rect so the FMX restore has nothing to grow
FMX drops the maximised state when Windows minimises the form externally —
reproducible in an empty FMX app. Windows then picks rcNormalPosition as the
restore target before it animates, so every reactive correction measured (queued,
inline in MINIMIZEEND, inside the restore's own WM_SIZE) landed after the frame
was already growing: the state ended correct and the flash remained.

Widen rcNormalPosition to the monitor work area while the window is down instead.
FMX still restores to normal, except normal is now full-screen, so nothing grows;
ReleaseNormalRect writes the real rectangle back once the window is maximised
again, keeping the user's own restored size intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:13:52 +01:00
r-zakarya b52cdc2d0b fix(winstate): apply the wanted window state inline, killing the restore flash
The MINIMIZEEND correction was posted through ForceQueue. Instrumenting the
hook showed the window state identical at the hook and one message-loop turn
later, so the queue guarded no race and only bought the normal-sized frame the
user saw. ApplyWantedWindowState now runs inline; the deferred pass stays as an
idempotent net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:39:23 +01:00
r-zakarya ca23b88582 docs: section L — Windows startup (--tray)
L1 hidden start with no taskbar button, L2 the button coming back on restore,
L3 the normal launch (validated). Records why every fix placed inside the form
was powerless: the FMX proxy is shown before FormCreate is reached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:12:31 +01:00
r-zakarya 087eb52d66 docs: F1b validated — overwrite restores the flags
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:27:40 +01:00
r-zakarya 220710174f docs: split F1 into F1a (delete + reimport) and F1b (overwrite)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:25:04 +01:00
r-zakarya af234ce049 fix(import): overwrite mode restores favorite and pinned too
F1 passed when the entry had been deleted first (INSERT path) but not in
overwrite mode: that path uses PUT, and PUT deliberately ignores
favorite/pinned so a SYNC update can never clobber the local flags — they are
device-local by design. A restore, however, must bring them back.

The overwrite loop now applies them through the dedicated toggle endpoints,
and only when they differ from the local entry. JS-only: PUT keeps ignoring
them, so sync behaviour is untouched.

Splits the F1 scenario into F1a (delete + reimport, already green) and F1b
(overwrite).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:24:38 +01:00
r-zakarya f198eaf6f3 docs: K1 — the clear-field toggle vs Tab-selects-all, intentional
Records why disabling "Clear the field before typing" still leaves the
password replaced: Tab selects the target field's content, so the keystrokes
overwrite it. Our code clears nothing — verified from the JS clear=0 through
both ClearFieldIfWanted guards. The toggle exists to avoid sending Ctrl+A
(terminals, remote desktops), not to preserve field content.

Filed as intentional so it isn't "fixed" later into appending a new password
onto the old one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 00:05:49 +01:00
r-zakarya e68536420f docs: J2-J4 validated — fill, copy and already-visible paths
Section J fully green: no double minimise on the fill path, the clipboard
survives the copy path, and an app already on screen is not sent to the tray.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:58:10 +01:00
r-zakarya 59090dcf67 docs: section J — quick search from the tray
J1 covers both bugs just fixed (lost keyboard focus after the tray restore,
and the coalesced cmd:// that swallowed the minimise). J2/J3 check the fill and
copy paths still return to the tray exactly once, J4 that an app which was
already on screen is not sent to the tray.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:39:56 +01:00
r-zakarya 610755e8e6 docs: I5/I6 — minimise-guard interaction with autofill and the tray
I5 exercises the path the guard fix addresses (autofill minimises, then the
window is hidden to the tray, so MINIMIZEEND may never arrive). I6 is the
non-regression check on the tray and focus paths the hook could have touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:08:09 +01:00
r-zakarya 7ae20aac43 docs: section I — window maximised-state scenarios
I1/I2 validated (both directions survive Show desktop), plus the tray path and
how to read the [winstate] diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:55:55 +01:00
r-zakarya 6518b59860 docs: H8 — capturing a combo owned by another app (validated)
Covers the swallowed-combo detection, the abandoned-capture timeout, and the
internal collision check that only works now that the capture releases our
own hotkeys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:38:26 +01:00
r-zakarya 3513e7b6e5 fix(ui): actionable toasts linger, and a dead hotkey shows in the sidebar
Every toast auto-dismissed after 2.8 s — fine for a success receipt, too
short to read AND act on an error. Errors and warnings now stay 10 s and can
be clicked away; success is unchanged.

The hotkey-conflict warning also needed a trace outside the toast: the ⚠ in
Settings only helps someone who opens the panel. A dot on the sidebar Settings
entry (plus a tooltip) is now the cue in the main UI that a combo is dead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:10:17 +01:00
r-zakarya feb956001e docs: H7 validated — persistent conflict marker confirmed
Also records the test pitfall: hook-based apps swallow the key without
failing RegisterHotKey, so they produce a dead hotkey with no marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:06:46 +01:00
r-zakarya 3d3ab3b02b fix(hotkeys): name the combo another app already owns
Two gaps when Windows refuses a RegisterHotKey:
- the toast said "one or both hotkeys" — wrong since there are three autofill
  combos, and it never said which one lost.
- Quick search and New entry failures were only written to the debug panel,
  which is off in production: the hotkey silently did nothing with no clue why.

Delphi now exposes the per-slot registration outcome and the host accumulates
the failed combo names across all five hotkeys, emitting one notification for
the batch. A combo the user deliberately cleared (vk=0) is never listed.
JS names them: "Already used by another app: 2FA code — pick another combo in
Settings".

Registration stays best-effort per slot: one clash doesn't disarm the others.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:46:33 +01:00
r-zakarya 4c745d2dd9 docs: section H — 2FA hotkey scenarios
H1 fill (confirmed working), H2 entry without TOTP, H3 picker eligibility,
H4 configurable combo + collision + reset, H5 locked vault, H6 code freshness
at period boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:42:27 +01:00
r-zakarya 2d6ba98073 docs: G1 also covers the deferred-focus steal on Fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:44:06 +01:00
r-zakarya 0184fc7cc2 docs: section G — Vault health scenarios (Fix button, cache freshness)
G1 (cancelled switch must not act on the still-open entry), G2 (Fix on an
entry deleted meanwhile), G3 (dashboard reflects import/sync/delete without
a manual Recompute).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:32:05 +01:00
r-zakarya 660f495649 docs: scenarios for the programmatic-click and round-trip fixes
B14 (native dialogs must not stack a discard prompt) and a new section F
covering the export/import round-trip: favorite + pinned restored, foreign
CSV defaults, sync staying device-local, custom-icon provenance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:19:23 +01:00
r-zakarya 30e37b3fbd fix(import): restore favorite + pinned on import/backup restore
A pinned entry came back unpinned after export→import. Wider than pinned:
`favorite` was WRITTEN into the export/backup/sync containers but nothing
ever read it back — not parseEntriesFromJSON, not encryptImportEntry, not
the server INSERTs. So it was a write-only field, and a restore silently
dropped both flags. `pinned` wasn't in any container at all.

Wired the full round-trip:
- containers: export + auto-backup now carry `pinned` (favorite was there).
- parseEntriesFromJSON reads both back (absent → 0, e.g. foreign CSV).
- encryptImportEntry passes them through to the body.
- server create + bulk-import INSERTs write favorite/pinned
  (GetValue<Boolean>(..., False) → 0 default). PUT still ignores them, so a
  sync update can't clobber local flags.

Sync stays device-local as decided 2026-07-11: buildSyncSnapshot now OMITS
favorite explicitly (it was dead weight before; with the passthrough live,
leaving it in would have started propagating it). Commented so the intent
isn't re-broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:06:05 +01:00
r-zakarya f45b96d147 docs: test scenarios for the auth / recovery / memory series
Section E (E1-E7) covers the fixes that had none: reauth inline retry,
recovery setup + redeem on argon2id-v2, recovery commit-after-save, the
sessionStorage key removal, and a slideover regression guard after deleting
the dead entry modal. Dropped the now-done "legacy entry modal" item from the
remaining list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:38:16 +01:00
r-zakarya de50a5f71f docs: A11 fixed, plus its false-positive and deleted-entry scenarios
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:45:10 +01:00
r-zakarya 4ceb7ab872 docs: scenarios for the Esc-chain and icon-refresh fixes
A10b (editor icon preview), B11 (Esc closes the top modal, not the editor,
across all 11 modals), B12 (a static modal survives its Esc), B13 (toast /
busy-overlay clicks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:19:49 +01:00
r-zakarya 47c29bd04e fix(esc): reauth / picker / quick-search were missing from the Esc chain
Three static modals had no entry in the fallback Esc handler, so Esc over
them fell through and closed the editor behind (or did nothing when no
editor was open — reauth and the autofill picker could only be dismissed by
mouse).

Also scopes the stray-modal catch added in a95becd to [data-dynamic]. As
written it matched ANY open .modal, so an Esc over reauth / quick-search /
the autofill picker would have removed those static nodes from the DOM for
good, breaking the feature until restart. Only the runtime-built folder
modal carries the marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:17:53 +01:00
r-zakarya a95becd582 fix(editor): Esc over the folder modal no longer closes the editor
The folder modal is built dynamically with no id in the fallback Esc
handler's modal list, so Esc fell straight through to the slideover close
and shut the editor behind it. The capture handler already bails on any
open .modal; mirror that in the fallback by closing a stray .modal before
reaching the slideover branch. Covers every dynamically-built modal, not
just this one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:17:45 +01:00
r-zakarya 6f4363bde6 fix(batch): move the selection bar into the title row
Selecting an entry inserted the batch bar as a block above the grid, pushing
every row down ~60px — the list jumped under the cursor mid-click. The bar
now renders inside .content-header, to the right of "All items", and the
header reserves its height (min-height) so appearing and disappearing shifts
nothing at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:59:53 +01:00
r-zakarya 9ea26be422 docs: manual test scenarios for the concurrent-write / editor bugs
Covers the four classes found this session (stale editor payload, editor
dismissed without the discard gate, missing withEncryptedMeta, view-blind
UI) with the surrounding cases, not just the ones reported.

Also fixes one more instance found while auditing: deleting a folder
reassigns its entries to All server-side, but an editor open on one of them
kept pointing at the deleted folder and re-created it on Save.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:51:12 +01:00