Commit Graph

69 Commits

Author SHA1 Message Date
r-zakarya da74a675be chore(autofill): mesurer ou passe le temps d'un remplissage
Un autofill lent apres une periode d'inactivite peut l'etre pour des
raisons sans rapport qui se ressemblent parfaitement de l'exterieur : le
moteur WebView2 qui se reveille, la prise du premier plan, ou l'attente
de la relache des modificateurs. Elles se corrigent a des endroits
opposes, donc on les separe au lieu de deviner laquelle accuser.

Trois marques : le temps que met la PAGE a repondre au raccourci, le
total, et le detail des deux attentes internes.

Premiere mesure : 13 ms de reponse apres 23 minutes en tray, total
635 ms. Le renderer ne dormait donc pas - l'hypothese la plus evidente
etait fausse - et 635 ms est le cout nominal de la sequence, faite de
pauses fixes (80 + 120 + 60 + 200 + 200). Le cas lent reste a attraper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:31:03 +01:00
r-zakarya 35d0d8ddf3 feat(autofill): la barrette revient allumee si elle l'etait
Pref DPAPI lazyOn, reprise par RestoreLazyBar depuis FormCreate - apres
le theme, parce que la barrette se peint elle-meme, et apres le cablage
de OnTrayRestore, dont depend son bouton Open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:26:18 +01:00
r-zakarya c1d0eb3891 feat(desktop): sync to a private Gitea repo, chosen by the URL
Same discriminant as the Android client - "/api/v1/repos/" in the URL - so one
address tells both ends which transport to speak, and nothing new appears in
Settings beyond a placeholder and a warning.

The bridge stays shared. Three things differ, and they are confined: the header
is "token <PAT>" rather than Basic (Gitea answers a silent 401 to the other),
a PUT carries a JSON envelope with the file base64-encoded inside it, and the
connection test uses GET because the contents API does not answer HEAD.

The sha does the ETag's whole job. It comes back from the pull, rides in the
PUT body instead of an If-Match, and a stale one earns a 409 - handled exactly
like WebDAV's 412, by re-running pull->merge->push so the other device's
changes are folded in rather than clobbered. On a first push `sha` is OMITTED
rather than sent empty: Gitea reads its presence as "replace that exact
version". `branch` is omitted too, so the repo's default is used instead of a
name we would be guessing.

A failing PUT now reads its response body: Gitea explains a 409 or a 422 there,
and without it the error was just a number.

The test button checks the SHAPE of what came back, not only the status. A URL
one path segment off answers 200 with valid JSON that is not a file - which
would otherwise surface much later as "wrong sync password" and send you
looking on the wrong side. Four tests cover that parsing, including the
line-wrapped base64 Gitea sends and the array an accidental directory URL
returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:37:39 +01:00
r-zakarya 5ce18e7ab5 fix(prefs): stop pulling secret prefs into the renderer just to render the UI
A renderer dump taken after a quick-unlock, with nothing opened, contained the auto-backup password in cleartext - inside an ExecuteJavaScript string and in the DevTools protocol buffers - and it survived the auto-lock that wipes the vault key.

New prefs/has answers set-or-not without moving the value. The settings panel uses that; only the paths that encrypt a backup, test the WebDAV connection or run a sync fetch the secrets themselves.
2026-08-02 23:21:03 +01:00
r-zakarya 4a78dc2e90 fix(hotkeys): find the native dialog by enumerating the thread windows
GetLastActivePopup returns the form itself - the common Save dialog is not an owned popup - so the code that raises it never ran. It does live on our thread with the standard #32770 class, which EnumThreadWindows finds regardless of its owner.

Logging confirmed two things worth keeping: WM_HOTKEY is dispatched during the dialog modal loop, and IsWindowEnabled detects the state correctly.
2026-08-01 19:03:03 +01:00
r-zakarya d6701a8994 fix(hotkeys): attach to the foreground thread to focus the buried dialog
The autofill helper attaches the caller to the TARGET thread, but the dialog runs on our own thread, so the attach was a no-op and Windows kept refusing the foreground change - the dialog rose without focus. Attaching to the current foreground thread instead is what grants the call.
2026-08-01 18:53:20 +01:00
r-zakarya c9c1e98470 fix(hotkeys): focus the buried dialog and say why the shortcut did nothing
SetForegroundWindow is refused when the hotkey comes from another application, so the dialog was raised but never focused; ForceForegroundWindow is now exposed by PM.Bridge for that. The explanation goes through a tray balloon because our window is disabled and behind the dialog, where no toast would be seen.
2026-08-01 18:49:03 +01:00
r-zakarya 1c0423680d fix(hotkeys): never touch the window while a native modal owns it
Ctrl+Shift+Q and Ctrl+Shift+A call RestoreWindowFromTray before JS can refuse, so a JS guard is too late by construction. Restoring the owner of a Save As dialog stranded it behind everything and the app looked frozen while it was only disabled.

NativeModalActive tests IsWindowEnabled - Windows disables the owner for the lifetime of the modal - and brings the dialog back to front via GetLastActivePopup instead of merely refusing.
2026-08-01 18:40:45 +01:00
r-zakarya 42d2d6fda5 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.
2026-08-01 18:30:00 +01:00
r-zakarya 80711272c4 fix(tray): a -tray launch does take the foreground, so give it back
FMX shows its taskbar proxy before we hide it, so the launching console was left inactive. GStartupForeground, captured in the unit initialization before any window of ours exists, holds it. A Windows-startup launch is unaffected: the foreground is a shell window there, which the capture filters out.
2026-07-28 19:38:02 +01:00
r-zakarya 2a1da8913b fix(tray): do not un-minimise the application we hand the foreground back to
ForceForegroundWindow restores an iconic target, so closing to the tray would pop back open a window the user had deliberately minimised. Skip the handback in that case and leave the choice to Windows. A -tray launch also skips it: it never took the foreground from anyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:14:12 +01:00
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 99b68c3e67 docs(startup): correct the rationale for the deferred server start
The comment justified deferring btnStartClick by the taskbar-proxy exposure
window, which the .dpr fix has since removed. Kept as-is per decision — it
works and reaching the tray sooner is desirable — but the comment now says
that, so nobody "fixes" it later against a premise that no longer holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:18:19 +01:00
r-zakarya 04f52851fc fix(startup): use ApplicationHWND + WS_EX_TOOLWINDOW, restore it on show
Adopts the approach that actually works: ApplicationHWND (FMX.Platform.Win) IS
the taskbar proxy, so there is nothing to hunt for and it is valid before the
form exists. And hiding it was never enough — the taskbar had already drawn
the button; clearing WS_EX_APPWINDOW and setting WS_EX_TOOLWINDOW is what
removes it, since a tool window is never listed.

That style is permanent, which is why the button never came back after a
restore. TrayProxyShowInTaskbar puts it back, called from
RestoreWindowFromTray — the single choke point every tray restore already goes
through — so it lands before RestoreFromTray shows the windows and the style
takes effect on that show.

Removes the scaffolding this chase accumulated and that the real fix makes
pointless: the EVENT_OBJECT_SHOW hook, the polling worker, FindOurFMXAppWindow,
TrayHideNow with its four probes, the one-shot OnActivate hide, and the [tray]
tracing. ~4.8 KB of dead code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:06:19 +01:00
r-zakarya ab8186ef75 fix(startup): start the proxy watcher from the .dpr, before the form exists
Decisive observation: the taskbar button is already up before FormCreate is
entered. Everything I had tried lived inside the form, so all of it ran after
the button was drawn — the watcher included, which is why moving it around
never helped.

StartTrayProxyHider is now exported from UMainForm and called from the .dpr
right after Application.Initialize, before CreateForm. It polls every 5 ms and
hides our TFMAppClass proxy unconditionally (the button can be up before
WS_VISIBLE reads as set, which the previous IsWindowVisible check would have
skipped), stopping as soon as the tray startup completes so a user restoring
from the tray is never fought.

Raw Win32 only — FindWindowEx + ShowWindow, no FMX object touched at that
point, which is what made the earlier .dpr attempt crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:15:32 +01:00
r-zakarya 4f2aff9de4 fix(startup): hide the taskbar proxy from a worker thread
Why every previous attempt could not work: the main thread is BUSY inside
FormCreate and pumps no message, and that is exactly when FMX creates and
shows its taskbar proxy. A WinEvent hook, a timer, a queued call — none of
them can run in that window. Deferring the server start shortened the window
but did not remove it, because the proxy is created regardless.

A worker thread is the one thing still running while the main thread blocks.
It polls for our TFMAppClass window (matched on our own process id) and hides
it the instant it appears; ShowWindow is safe cross-thread. It stops as soon
as the tray startup completes, with a 10 s ceiling so it can never linger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:37:51 +01:00
r-zakarya 17e48aa688 fix(startup): start the server AFTER the tray hide, not before
The visible thing is only the taskbar button — the form window is never shown
(form_visible_was=False throughout). That button belongs to the FMX proxy,
created partway through FormCreate, and FormCreate runs for ~1 s because it
opens the DB, binds the Indy server and kicks off WebView2. The thread pumps
no message during that second, so nothing could hide the proxy until it ended:
hooks and hide points alike were powerless by construction.

So stop trying to react faster and remove the wait instead: on a --tray launch
btnStartClick now runs inside the deferred block, after MinimizeToTray.
FormCreate returns in milliseconds, the proxy is hidden immediately, and the
server starts once nothing is on screen. FServer is constructed earlier
(line 295), so FServer.Active stays safe meanwhile. Normal launches keep the
original order untouched.

The EVENT_OBJECT_SHOW hook stays armed across the deferred start, so a proxy
re-shown during it is hidden again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:33:26 +01:00
r-zakarya 8fa956f07f fix(startup): hide the FMX taskbar proxy the instant it is created
The instrumented run pinned it down: the FORM is never visible
(form_visible_was=False at all four hide points). What shows is the FMX
taskbar proxy — app=0 at 20.145, app=854682 at 20.974 — so it is created
partway through FormCreate and stayed on screen for the best part of a second
until the first hide that could see it.

A WinEvent hook on EVENT_OBJECT_SHOW, active only for the --tray startup,
hides any top-level window of ours the moment Windows shows it, and is torn
down once MinimizeToTray has run.

Two details that would each have broken it:
- WINEVENT_INCONTEXT, not OUTOFCONTEXT: the latter queues the callback until
  the thread pumps messages, which does not happen during FormCreate — exactly
  when the proxy appears.
- The pending flag is a unit-level var, not a form field: CreateForm assigns
  the MainForm global only after the constructor returns, so a callback firing
  during FormCreate could not have reached the form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:28:29 +01:00
r-zakarya 283676224f fix(startup): hide at three points during FormCreate, and prove when it shows
The timeline says FormCreate itself runs for 1.02 s (45.509 → 46.532) while
the hide only landed at 46.663 — so if the window is visible during that
stretch, it accounts for exactly the second being reported.

TrayHideNow() hides the form AND the FMX taskbar proxy, and is now called at
three points: top of FormCreate, right after the browser is parented (which
forces the handle to exist, the likely moment FMX shows it), and at the end of
FormCreate — plus the existing on-activate one-shot.

It also logs whether the window WAS visible each time, which pins down exactly
where FMX turns it on instead of another guess.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:53:22 +01:00
r-zakarya 0506d757dd fix(startup): hide the taskbar proxy too, and trace the --tray timeline
The window still showed, taskbar entry included. Two things.

The taskbar button belongs to the FMX proxy window (TFMAppClass), a separate
top-level window from the form — hiding the form alone never removed it.
FindFMXAppWindow moves from private to public so the activate-hide can drop
both. (Guarded on Assigned(FBridge): the handler is installed at line 222,
the bridge is created at 268.)

And rather than guess a fourth time at FMX's startup order — the same trap as
the window-state saga, which only fell to instrumentation — this logs a
millisecond timeline: FormCreate begin/end, the activate-hide, and the
deferred MinimizeToTray. Whichever gap holds the second will be obvious.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:47:25 +01:00
r-zakarya eec7bc4f80 fix(startup): revert the .dpr hide (AV) and hide on first activate instead
The .dpr version crashed with an EAccessViolation writing to 000000CE —
touching MainForm.Visible/Handle between CreateForm and Run is evidently too
early for FMX. Reverted; the app builds and runs as before.

Replaced with a one-shot OnActivate handler installed in FormCreate only for
--tray: it fires the instant FMX shows the form, hides it (FMX + Win32) and
unhooks itself, so it can never fight the user later. That runs far earlier
than the deferred MinimizeToTray, which waits for the message loop and hence
for the DB open, server start and WebView2 init — the second the window used
to spend on screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:42:04 +01:00
r-zakarya e6a749465e fix(startup): --tray launch no longer flashes the window
With "Start with Windows" the window sat on screen for a second or two before
dropping to the tray. The hide was deferred with ForceQueue, which only runs
once the message loop is free — i.e. after the DB open, the HTTP server start
and the WebView2 init, all of which happen first.

Set Visible := False at the very top of FormCreate instead, before any of that
work, so there is nothing to show in the first place. The deferred
MinimizeToTray stays: it still adds the tray icon and drops the taskbar entry,
and doubles as the safety net if FMX shows the main form regardless — worst
case the behaviour is exactly what it is today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:29:01 +01:00
r-zakarya e88dbf3ec8 fix(quick-search): Esc from the tray now closes AND returns to the tray
Two bugs in the same flow, both on the tray-originated path.

1. Esc did nothing until the search box was clicked: after the tray restore
   the WebView2 child does not hold the OS keyboard focus, so no keystroke
   reached the page at all — a DOM .focus() cannot fix that. Both quick-search
   entry points now call WebBrowser.SetFocus after restoring.

2. Esc closed the modal but left the app on screen. In fill mode
   closeQuickSearchModal fires cancelAutofill and then minimizeToTray, and two
   back-to-back window.location.href assignments COALESCE — the minimise was
   dropped. The copy path was unaffected because it never calls cancelAutofill,
   which is why this only showed up here. Deferred by a setTimeout(0), the same
   remedy CLAUDE.md already documents for quickunlock/store + pin/clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:25:28 +01:00
r-zakarya 2773cc444b fix(window): route every tray restore through one guard-clearing helper
Checking the hook's blast radius surfaced a real interaction: ExecuteAutofill
genuinely minimises the window (SW_MINIMIZE) before typing, which raises the
minimise guard. If the window is then HIDDEN to the tray rather than
un-minimised — the quick-search hide-after flow does exactly that —
MINIMIZEEND may never fire and the guard would stay raised for the rest of the
session, silently freezing the tracked window state.

All six FBridge.RestoreFromTray call sites now go through
RestoreWindowFromTray, which clears the guard first. RestoreFromTray replays
its own saved placement, so nothing is left for the guard to protect there.

MinimizeToTray itself is unaffected: it uses SW_HIDE, which raises no minimise
event at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:04:37 +01:00
r-zakarya 63dd7a296a chore(window): trim the winstate diagnostics now the fix is confirmed
The traces did their job: hwnd=657868 vs form=657842 proved Win+D minimises
the FMX taskbar proxy, not the form — which is why the earlier HWND filter
swallowed every event. Two full cycles then showed minimise start/end firing
and the re-maximise landing, with no stray "user resize" during the restore.

Keeps the two lines worth having: the hook install result (a silent failure
would be baffling) and the correction itself, which is rare.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:54:30 +01:00
r-zakarya 72fe01f513 fix(window): don't filter the hook on the form HWND
The hook installed but never fired: an FMX app owns TWO top-level windows —
the form and the taskbar proxy (the one FindFMXAppWindow digs out in
PM.Bridge) — and Win+D minimises the proxy. Comparing the event HWND against
the form handle rejected every event.

Dropped the comparison; the hook is already scoped to our process and WebView2
lives in separate ones, so any window event arriving here is ours. Logs the
install result and each event's HWND so a silent failure is visible next time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:50:26 +01:00
r-zakarya 7a19bdc13b fix(window): observe the minimise itself via a WinEvent hook
Third discriminator measured and rejected: the log shows the restore's resize
arrives with fg=True (22:44:08), so foreground ownership no more separates it
from a user gesture than the placement flags did.

FMX simply exposes nothing usable — no resize while iconic, and the restore's
resize is indistinguishable from the user's own. So stop inferring and observe
the transition at the OS level: SetWinEventHook on EVENT_SYSTEM_MINIMIZESTART
/ MINIMIZEEND, scoped to our process, WINEVENT_OUTOFCONTEXT (no injection,
callback on our thread).

Minimise start raises a guard that makes the resize handler ignore everything
until the restore settles; minimise end re-applies the state the user actually
left, in both directions. Hook failure is non-fatal — only the preservation is
lost. Unhooked in FormDestroy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:47:52 +01:00
r-zakarya ca54c39c75 fix(window): use foreground ownership to tell a user resize from a restore
The log settled the previous attempt: WPF_RESTORETOMAXIMIZED (flags=2) is set
for ANY maximised window, not just one on its way back from minimised, and
showCmd is already 1/3 by the time the resize fires. Testing those classified
the user's own Maximize click as a restore, so the intent was never recorded
(want stayed False) and nothing could be re-applied afterwards.

Windows exposes no restore marker here. What does separate the two: a user
gesture happens while the window OWNS THE FOREGROUND; FMX's re-layout during a
restore (and during the startup burst) runs before it does. Resizes that
arrive without foreground ownership are no longer recorded as intent, and
re-apply a maximised state the user had left.

Placement is still read, for the log only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:42:05 +01:00
r-zakarya 03c638b2e1 fix(window): ask Windows whether a resize is part of a restore
The traces killed the previous discriminator: OnActivate fires constantly
(WebView2 focus churn) and even right after the user clicks Maximize, so
"resize followed by activate" wrongly classified a user maximise as a restore
and dropped the intent.

Windows itself knows: WPF_RESTORETOMAXIMIZED (or showCmd still
SW_SHOWMINIMIZED) marks the resize of a window on its way back from minimised.
That resize is now never recorded as intent, and triggers the re-maximise
directly — no dependency on activate ordering. Activate is demoted to a
guarded safety net.

Log line now dumps zoomed/showCmd/flags so the next run confirms the flag is
actually set on this machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 20:22:09 +01:00
r-zakarya 28f0d1c5bc fix(window): let an activate cancel the restore's own resize
The traces settled it: on a restore FMX fires its resize BEFORE the activate,
already reporting "not maximised" — so any snapshot taken at activate time was
too late. A genuine user resize, by contrast, is never followed by an activate
(the window is already active).

That is the discriminator. A resize no longer commits immediately: it queues,
and an activate arriving first cancels it, leaving the intent intact for the
correction to re-apply. Verified against every sequence in the log, including
the startup burst (three resizes then an activate), which must NOT be mistaken
for the user un-maximising.

Skips resizes that don't change the maximised state, so dragging the window
edge no longer floods the queue or the log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 20:16:47 +01:00
r-zakarya ab12f6bf66 fix(window): apply the restore correction after FMX finishes, with tracing
The FWasMinimized gate never armed — FMX evidently fires no resize while the
window is iconic — so activate bailed out and the maximised window still came
back normal.

Stop depending on that signal or on event ordering: snapshot the intent at
activate (before a restore-time resize can overwrite it) and apply the
correction from a queued call, which necessarily runs after FMX has finished
sizing the window. FRestoring keeps that resize out of the tracking, and the
intent is re-asserted afterwards since our own ShowWindow fires one too.

Adds [winstate] LogLine traces so the next iteration works from observed
event order instead of guesses (needs config.txt with debug=true).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 20:13:30 +01:00
r-zakarya 8204ccb316 fix(window): stop forcing maximised on a window the user left normal
The previous version over-corrected: a normal window came back maximised
after Win+D. Two causes, both fixed.

- It read FMX's WindowState, which does not reliably follow a native
  un-maximise, so FWantMaximized never cleared. Now reads Win32 IsZoomed /
  IsIconic, which is the actual truth.
- The restore fires its own resize, reporting whatever FMX decided — recording
  that erased the very intent we were preserving. FWasMinimized now gates the
  restore's resize out of the tracking.

Activate then corrects both directions: re-maximise when FMX dropped a
maximised window, restore when Windows brought a normal one back maximised.
Degrades safely — if the minimise resize never fires, it simply does nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 20:06:34 +01:00
r-zakarya 03a8a6f8a6 fix(window): re-apply maximised after an external minimise (Win+D)
The earlier fix only covered the tray restore path, but Show desktop /
taskbar restore never runs our code at all — it is pure Windows + FMX, and
FMX brings the window back "normal" after forgetting it was maximised.

Track the state the user actually settled on (OnResize, ignoring wsMinimized
since that is the state we must survive) and re-apply it on OnActivate, which
fires however the user returns: taskbar, tray or Alt+Tab. Gated on
FWantMaximized so a window the user deliberately un-maximised is left alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 19:58:27 +01:00
r-zakarya 4965ff2518 feat(hotkeys): persistent conflict marker in Settings
A toast is gone by the time the user opens Settings, so a hotkey Windows
refused looked perfectly configured while doing nothing. The offending row now
carries a ⚠ marker (amber border + tooltip explaining the combo is taken)
until a re-registration clears it.

Delphi reports slot ids instead of labels; HOTKEY_SLOTS maps id → button +
label in one place, so JS owns the wording and can flag the exact row. The
markers are re-applied by openSettings, surviving panel re-renders.

Also stop toasting success on the startup push (it fires on every unlock) —
autofillPushHotkeys(true). A clash still warns: that one is actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:52:05 +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 7c86f35fce feat(autofill): Ctrl+Shift+T types the 2FA code
Same workflow as the password-only hotkey: click the 2FA field, press the
combo, the current 6-digit code is typed in.

Reuses the whole autofill pipeline — no new native typing path. The code is
sent in the password slot with an empty username, so Delphi's existing
password-only branch types just the code.

- Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other
  two autofill combos; an unconfigured combo (vk=0) doesn't count as a
  registration failure.
- Matching filters on totp_secret BEFORE scoring, so the multi-match picker
  never offers an entry that has no code.
- Configurable + synced like the others (autofillHotkeyTotp), collision-checked
  against the other four slots, covered by Reset to defaults and the cheatsheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:53:00 +01:00
r-zakarya 0d40328a24 feat(hotkey): make the new-entry hotkey configurable + fix tray title capture
Part A — Ctrl+Shift+A (new entry from window title) is now editable like the
other 3 global hotkeys, following the same pattern end to end: state.newEntryHotkey
(synced), a capture button in Settings > Autofill, dup cross-check, reset-to-
defaults, autofillPushHotkeys, and Bridge.setAutofillHotkeys carrying ne_mods/
ne_vk. Delphi gets SetNewEntryHotkey + stores the live combo (FNewEntryHotkeyMods/
Vk) so ExecuteAutofill's post-fill re-register uses the user's combo instead of
resetting to Ctrl+Shift+A. UMainForm parses ne_* unconditionally (works with
autofill off).

Part B — new-entry from the tray pre-filled the title with "Password Manager":
GetForegroundWindow resolved to our own hidden window. Only capture the title
when the foreground window belongs to another process; otherwise pass '' (blank).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:49:48 +01:00
r-zakarya 3d217e82d4 fix(quick-search): Esc closes only the modal; cancel returns focus to target
- The qsInput Esc handler didn't stopPropagation, so the SAME keystroke fell
  through to the document-level Esc handlers which, seeing the modal now
  closed, also closed (or discard-prompted) the dirty slideover behind it.
- autofill/cancel now hands the foreground back to the saved target HWND:
  Esc after a fill-mode hotkey returns the user to the window they came
  from instead of leaving our app focused. Locked-vault path still ends
  focused on us (cancelAutofill runs before focusApp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:20:24 +01:00
r-zakarya 770504ea03 fix(quick-search): only tray-hidden windows go back to the tray on close
wasHidden counted IsIconic too, so a window merely minimised to the taskbar
("visible" to the user) was treated as tray-origin: Esc/pick sent it to the
TRAY, vanishing from the taskbar it came from. wasHidden is now strictly
"came from the tray" (Visible=false); an iconic window gets restored by the
hotkey and simply stays open when the modal closes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:12:41 +01:00
r-zakarya 72dcc3dd82 fix(autofill): kill residual hotkey chord; dedicated fail-balloon setting
Residual new-entry trigger (1 in 6): password chars go out as
KEYEVENTF_UNICODE (VK_PACKET, can't match a hotkey) — the real chord risk is
the Ctrl+A clear-field, which sends a real VK_A. If the user re-presses
Ctrl+Shift mid-sequence, that VK_A becomes physical Ctrl+Shift+A = our own
new-entry hotkey. ForceReleaseModifiers now runs inside
SendSelectAllAndDelete, at the risky instant, not just once up front.

Balloon: was gated by "Show tray notifications" (OFF for this user) — now
gated by its own synced setting "Tray alert when autofill is blocked"
(autofillFailBalloon, Settings > Autofill, default ON), carried as notify=0
on cmd://autofill/execute. ShowBalloon no longer gates internally; each
caller applies its own setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:23:02 +01:00
r-zakarya aab0b14be4 fix(autofill): force-release stuck modifiers; balloon when blocked from tray
Root cause of "Ctrl+Shift+P opened the new-entry modal": if the user still
holds Ctrl+Shift when WaitForModifierRelease times out (1s), every password
letter is typed as a Ctrl+Shift+<letter> chord — garbage in the field AND it
fires our own global hotkeys (a password containing 'a' triggers Ctrl+Shift+A
= new entry). ForceReleaseModifiers now injects KEYUP for any still-held
modifier before typing.

Also: when the fill is blocked (elevated target) while the window is hidden
in the tray, the in-app toast is invisible — show a tray balloon instead.
ShowFirstTimeBalloon generalized into ShowBalloon(title, text, warning),
gated by the existing "Show tray notifications" setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:08:50 +01:00
r-zakarya b869effee5 feat(autofill): "Clear the field before typing" setting (default ON)
The Ctrl+A + Del sent before each field misbehaves on targets where Ctrl+A
isn't select-all (terminals, some remote desktops). New synced setting
(autofillClearField, Settings > Autofill) gates it: JS appends clear=0 to
cmd://autofill/execute when off; ExecuteAutofill wraps the three
SendSelectAllAndDelete calls behind AClearFirst. Absent param = ON, so
existing behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:52:35 +01:00
r-zakarya 4a47caad55 fix(autofill): honest result reporting + restore maximized from tray
Bug 1: a maximized window trayed via the quick-search fill flow came back
"normal" on the next restore. ExecuteAutofill minimizes the window BEFORE
MinimizeToTray snapshots the placement, so the snapshot said SHOWMINIMIZED
and the never-restore-minimized guard forced SHOWNORMAL. Now honours
WPF_RESTORETOMAXIMIZED (Windows keeps the pre-minimize state in flags).

Bug 2: filling into an elevated app (admin Notepad) showed "password sent"
while UIPI silently discarded the keystrokes (SendInput even reports
success). ExecuteAutofill is now a function: it checks the target process
elevation up front (can't-open counts as elevated) and returns False without
typing. UMainForm feeds the result to JS via Bridge.onAutofillResult; the
quick-search success toast is deferred until Delphi confirms, and a failure
shows "Autofill blocked - the target window runs as administrator".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:19:34 +01:00
r-zakarya 3b32a621e8 fix(quick-search): stop teleporting + hiding an already-open window
Two bugs when Ctrl+Shift+Q fires while the app window is open beside the
target app:
- RestoreFromTray re-applied FSavedPlacement (captured at the LAST
  MinimizeToTray) to an already-visible window -> it jumped to a stale
  position. Now: visible and not iconic -> just SetForegroundWindow.
- ExecuteAutofill minimizes our window when it is foreground (the user just
  clicked the entry) and never brought it back when hide_after was false.
  New ARestoreAfter param (= not HideAfter): restore with SW_SHOWNOACTIVATE
  after the fill, so the window returns to its position without stealing
  focus from the freshly-filled target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:52:26 +01:00
r-zakarya 0925afb689 perf(autofill): keep WebView2 renderer responsive while in tray
Add --disable-renderer-backgrounding / -backgrounding-occluded-windows /
-background-timer-throttling to the WebView2 args. Without them Chromium
freezes a hidden renderer, so the first autofill hotkey after a tray-only
start (Start with Windows) waited 3-5s for onAutofillRequest to run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:32:14 +01:00