feat: quick-search fill modes + editable custom-field combobox + JS build gate

Quick search (Ctrl+Shift+Q fill mode)
- Enter / left-click → full autofill (username + Tab + password), like
  Ctrl+Shift+L.
- Shift+Enter / right-click → username only (new Delphi username-only
  SendInput path via field=user; ExecuteAutofill AUsernameOnly param).
- Ctrl+Enter / Ctrl+click → password only.
- Copy mode (tray / palette) unchanged: Enter/left = password,
  Shift+Enter/right = username.
- Clipboard fix: copy-then-minimise no longer wipes the just-copied
  password — MinimizeToTray takes an AClearClipboard flag (False on the
  quick-search copy path, driven by app/minimize?keepclip=1). The 30s
  auto-clear still guards it.
- Right-click on a result row suppresses the native/custom context menu
  (preventDefault + stopPropagation).

Editable custom-field combobox
- Option-backed custom fields (card brand, expiry year/month, etc.) now
  render a custom editable combobox instead of a locked <select>: an
  arrow drops a menu of ALL options (a native <datalist> filtered to the
  typed text, which confused users), while the input stays freely
  typeable for values not in the list. Storage shape unchanged.
- Outside-click closes the menu via the existing slideover mousedown
  handler; item mousedown + preventDefault so blur doesn't race the pick.

Build safety
- BuildAssets.ps1 runs `node --check` on every embedded .js before
  generating assets.res. A syntax error now aborts the asset build
  (exit 1, file + line logged) instead of shipping a dead bundle that
  only surfaces after a full Delphi rebuild. Node is optional: absent →
  warn and continue.

Docs
- CODE_AUDIT.md: full static-analysis report (security, latent bugs,
  maintainability, future features, prioritized action plan).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-03 08:13:25 +01:00
parent 7440d07793
commit 3076fec710
7 changed files with 507 additions and 73 deletions
+26 -7
View File
@@ -129,8 +129,11 @@ type
public
constructor Create(AMainForm: TForm);
destructor Destroy; override;
// Hide main window and show tray icon.
procedure MinimizeToTray;
// Hide main window and show tray icon. AClearClipboard defaults to
// True (a manual minimise wipes any copied password immediately);
// the quick-search "copy then hide so I can paste" flow passes False
// so it doesn't nuke the password it just placed on the clipboard.
procedure MinimizeToTray(AClearClipboard: Boolean = True);
// Restore main window and remove tray icon.
procedure RestoreFromTray;
// Apply Windows dark-mode title bar to the main form. Win10 19044+
@@ -160,8 +163,10 @@ type
// If AUsername is empty, only the password is typed (no Tab) — matches
// the password-only hotkey path AND avoids spurious Tab on entries
// without a stored username.
// AUsernameOnly = True → type ONLY the username (no Tab, no password);
// used by the quick-search "autofill username" action.
procedure ExecuteAutofill(ATargetHWND: HWND;
const AUsername, APassword: string);
const AUsername, APassword: string; AUsernameOnly: Boolean = False);
property SecureClipboard: TSecureClipboard read FSecureClipboard;
property TrayAdded: Boolean read FTrayAdded;
property AutofillRegistered: Boolean read FAutofillRegistered;
@@ -551,7 +556,7 @@ begin
until LWnd = 0;
end;
procedure TPMBridge.MinimizeToTray;
procedure TPMBridge.MinimizeToTray(AClearClipboard: Boolean = True);
var
LFormHwnd, LAppHwnd: HWND;
begin
@@ -561,8 +566,12 @@ begin
// Extra safety: clear the clipboard immediately when the user minimizes,
// rather than waiting for the 30s auto-clear timer to fire. A password
// the user just copied shouldn't sit in the clipboard while the app is
// out of sight.
FSecureClipboard.Clear;
// 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.
if AClearClipboard then
FSecureClipboard.Clear;
LFormHwnd := MainFormHWND(FMainForm);
LAppHwnd := FindFMXAppWindow;
@@ -1115,7 +1124,7 @@ begin
end;
procedure TPMBridge.ExecuteAutofill(ATargetHWND: HWND;
const AUsername, APassword: string);
const AUsername, APassword: string; AUsernameOnly: Boolean = False);
const
MinimizeSettleMs = 80;
FocusSettleDelayMs = 120;
@@ -1135,6 +1144,16 @@ begin
WaitForModifierRelease(1000);
Sleep(FocusSettleDelayMs);
// Username-only: type just the username into the focused field, no Tab,
// no password. Used by the quick-search right-click / Shift+Enter path.
if AUsernameOnly then
begin
SendSelectAllAndDelete;
Sleep(60);
SendUnicodeString(AUsername);
Exit;
end;
if AUsername = '' then
begin
SendSelectAllAndDelete;
+11 -3
View File
@@ -99,6 +99,9 @@ type
// SendInput because Win10/11 anti-focus-stealing rules then refuse to
// hand focus to the target window.
FAutofillPendingHide: Boolean;
// True when the pending autofill should type ONLY the username (quick
// search "autofill username" — right-click / Shift+Enter in fill mode).
FAutofillPendingUserOnly: Boolean;
// Created dynamically in FormCreate so the directive can pick either
// TTMSFNCWebBrowser or TTMSFNCEdgeWebBrowser at compile time without
// needing two .fmx variants. Aligned to Client to fill the remaining
@@ -788,6 +791,7 @@ begin
FAutofillPendingPass := GetParam('password');
FAutofillPendingHWND := FAutofillTargetHWND;
FAutofillPendingHide := GetParam('hide_after') = '1';
FAutofillPendingUserOnly := GetParam('field') = 'user';
FAutofillTargetHWND := 0;
// Small timer so SetForegroundWindow has time to take effect before
@@ -823,7 +827,9 @@ begin
// from the tray menu, hide it again so the user can paste straight
// into the target app without alt-tabbing.
else if ACmd = 'app/minimize' then
FBridge.MinimizeToTray
// keepclip=1 → don't wipe the clipboard on minimise (quick-search
// copy-then-hide flow). Default clears it as before.
FBridge.MinimizeToTray(GetParam('keepclip') <> '1')
// Tray balloon notifications on/off. JS pushes the user setting at
// startup (settings_json sync) and whenever they flip the toggle.
@@ -1323,22 +1329,24 @@ procedure TMainForm.AutofillTimerTick(Sender: TObject);
var
TargetHwnd: HWND;
PendingUser, PendingPass: string;
HideAfter: Boolean;
HideAfter, UserOnly: Boolean;
ForegroundAfter: HWND;
begin
TargetHwnd := FAutofillPendingHWND;
PendingUser := FAutofillPendingUser;
PendingPass := FAutofillPendingPass;
HideAfter := FAutofillPendingHide;
UserOnly := FAutofillPendingUserOnly;
FAutofillPendingHWND := 0;
FAutofillPendingUser := '';
FAutofillPendingPass := '';
FAutofillPendingHide := False;
FAutofillPendingUserOnly := False;
TTimer(Sender).Enabled := False;
TTimer(Sender).Free;
FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass);
FBridge.ExecuteAutofill(TargetHwnd, PendingUser, PendingPass, UserOnly);
ForegroundAfter := GetForegroundWindow;
LogLine(Format('Autofill executed — target=%s, foreground_after=%s, match=%s',
[IntToHex(TargetHwnd, 8), IntToHex(ForegroundAfter, 8),
+27
View File
@@ -77,6 +77,33 @@ if ($files.Count -eq 0) {
Log "Embedding $($files.Count) file(s):"
$files | ForEach-Object { Log (" " + $_.UrlPath + " -> " + $_.ResName) }
# --- JS syntax gate -----------------------------------------------------------
# A syntax error in app.js parses fine here but kills the whole frontend at
# runtime (no event handlers → dead UI), and it's only caught after a full
# Delphi rebuild. Run `node --check` on every embedded .js so a broken bundle
# never makes it into assets.res. Node is optional: if it isn't installed we
# warn and continue rather than blocking the build on a machine without it.
$node = Get-Command node.exe -ErrorAction SilentlyContinue
if (-not $node) { $node = Get-Command node -ErrorAction SilentlyContinue }
$jsFiles = $files | Where-Object { $_.Relative -match '\.js$' }
if ($jsFiles) {
if ($node) {
foreach ($jf in $jsFiles) {
Log "Syntax check: $($jf.Relative)"
# --check prints errors to stderr and returns non-zero on failure.
$out = & $node.Source --check $jf.FullPath 2>&1
if ($LASTEXITCODE -ne 0) {
Log "JS SYNTAX ERROR in $($jf.Relative):"
Log ($out | Out-String)
throw "JS syntax check failed for $($jf.Relative) - aborting asset build."
}
}
Log "JS syntax OK."
} else {
Log "WARNING: node not found - skipping JS syntax check. Install Node to enable it."
}
}
# --- Generate assets.rc -------------------------------------------------------
$rc = New-Object System.Text.StringBuilder
[void]$rc.AppendLine('// Auto-generated by BuildAssets.ps1 - do not edit by hand.')
Binary file not shown.