Commit Graph

556 Commits

Author SHA1 Message Date
r-zakarya e919a79522 docs(claude): document the multiline Notes field and the custom-field row 2026-08-01 14:00:45 +01:00
r-zakarya 4936aa78ca refactor(entries): drop the cyan secret marker on the reveal eye
The eye only renders on a secret field, so its presence already carries the state.
2026-08-01 12:47:59 +01:00
r-zakarya a8c1a88ec4 refactor(entries): drop the inline copy button, the row menu carries it
Row is down to label | value (eye inside) | menu.
2026-08-01 12:05:44 +01:00
r-zakarya e5758f984c fix(entries): keep the reveal eye clear of the multiline scrollbar
scrollbar-gutter reserves the track whether or not it is showing, so the eye keeps the same position once the content passes the auto-grow cap.
2026-08-01 12:04:46 +01:00
r-zakarya a2cd08c676 feat(entries): move the reveal eye inside the custom-field value
Same placement as the TOTP field, which buys back a column on a dense row: label | value (eye inside) | copy | menu. Pinned top-right on a multiline field so it stays clear of the resize grip.
2026-08-01 11:57:56 +01:00
r-zakarya c71ea643e7 feat(entries): add Copy value to the custom-field row menu
The inline copy button stays: copy is the frequent action and burying it costs a click every time. Unlike the old lock-versus-eye pair, two ways to copy are not confusable.
2026-08-01 11:55:29 +01:00
r-zakarya a0a069c7f6 refactor(entries): fold the secret toggle into a per-field row menu
The lock button sat next to the eye and both masked the value, so they read as the same control. The eye now carries the state - cyan ring when the field is stored as secret - and flipping the flag moved into a row menu alongside Remove field, one deliberate step away from the everyday reveal. The row drops from six columns to five.

The menu reuses the card kebab classes, so closing it on an outside click is already handled.
2026-08-01 11:47:25 +01:00
r-zakarya 6705a8b478 fix(entries): toggle the secret flag in place instead of rebuilding every row
rerender() wipes the whole custom-fields list, which reads as the panel flashing and also drops focus and the auto-sized height of a multiline field.
2026-08-01 11:06:59 +01:00
r-zakarya 4b7f5ff599 docs(entries): spell out the difference between the reveal eye and the secret lock
Both toggles mask the value, so they read as duplicates. Tooltips now say which one is temporary and which one changes what is stored.
2026-08-01 10:34:44 +01:00
r-zakarya 9dc9f625d6 fix(entries): size the multiline field from its content instead of a fixed height
A manual resize was lost on reopen: the row is rebuilt on every open, and remembering the height would not survive a restart either since localStorage is wiped when the ephemeral port changes the origin. Deriving it from the content removes the state entirely - the field is always the right size.
2026-07-31 18:31:26 +01:00
r-zakarya 2cab20eb76 docs(tests): add F6 for the multiline Notes field 2026-07-28 23:21:21 +01:00
r-zakarya d8643b96d1 feat(entries): multiline Notes field for any entry, masked by default
The note body of a note entry is the encrypted_password column, so logins had no way to carry notes. Rather than a dedicated encrypted column and the seven call sites that come with it, this rides on custom_fields, which rotation, duplicate, export/import and sync already carry.

Both the open-time normalisation and the save-time rebuild drop any key they do not name, so multiline is carried explicitly in each - otherwise the field silently reopens single-line.
2026-07-28 23:06:39 +01:00
r-zakarya 415dc6c8ba docs(claude): record that pre-login settings must live in DPAPI prefs 2026-07-28 22:51:00 +01:00
r-zakarya 67107fba37 fix(unlock): persist unlockMode in DPAPI so a cold start still asks for the PIN
unlockMode lived only in localStorage, wiped on every launch since the ephemeral port changes the origin, and in settings_json, which needs a session to read. The auth screen therefore always fell back to the master password on a cold start even though the PIN blob was there. Mirror it to a DPAPI pref and read it back in showAuth, like rememberedUsername.
2026-07-28 22:50:28 +01:00
r-zakarya 52af21f917 docs(tests): mark B1-B13 validated 2026-07-28 20:07:49 +01:00
r-zakarya 83a0e08df8 docs(tests): fix B6, a trashed entry can never be open in the editor
openTrashActions is an empty stub and the trash kebab has no Edit item, so the scenario as written was not reachable. Rewritten around what is: the purge must leave an unrelated open editor alone, plus the one remote path that can actually trash an entry while it is being edited.
2026-07-28 19:54:37 +01:00
r-zakarya 4499c8c570 docs(tests): mark H11/H12 validated 2026-07-28 19:44:13 +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 7c08a69bb2 docs(tests): mark H13 validated, Ctrl+Shift+P confirmed on Reddit 2026-07-28 15:50:25 +01:00
r-zakarya 16577a22be docs(tests): record why a full fill cannot work on the Reddit login form 2026-07-28 15:48:49 +01:00
r-zakarya 38ba208455 fix(autofill): stop sending Ctrl+A after the Tab in a full fill
Tabbing into an input already selects its whole content, so the second clear was redundant. Where Tab lands on something that is not editable - a link, a reveal button, the document, as on the Reddit login form - Ctrl+A selected the entire page instead and the password went nowhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:32:25 +01:00
r-zakarya 40e2054496 docs(tests): add H11/H12 for the foreground-handback guards 2026-07-27 23:17:52 +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 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 8d03b74b6a chore: drop the -tray debugger run parameter from the project file
Committed by accident while testing the tray launch. Left in, every F9 from
the IDE would start hidden in the tray — the exact confusion the parameter is
worth warning about. It is a local debugging preference, not project config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:09:29 +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 169cdddf9d fix(startup): hide the --tray window from the .dpr, where it sticks
Setting Visible := False in FormCreate wasn't enough — FMX re-shows the main
form after the constructor returns, so the window still appeared for ~1 s
during the Windows logon.

Do it in the .dpr instead, between CreateForm and Run: that is after FMX's own
show and before any message is pumped, so nothing ever paints. Backed by a
Win32 ShowWindow(SW_HIDE) so it holds whatever FMX decides. The handle is
still created, so WebView2 initialises normally and the vault is ready as soon
as the user clicks the tray icon.

The taskbar entry can still appear briefly; the deferred MinimizeToTray drops
it, along with adding the tray icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:35:58 +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 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 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 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