From 84456970b2b91be7d488650daef81e4999a90c68 Mon Sep 17 00:00:00 2001 From: Silvio Fernando <50349062+sfcdsfcd@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:46:33 -0300 Subject: [PATCH 1/2] feat: improve scoring with multi-token AND and phrase match bonus --- BrowserSearch/Main.cs | 41 +++++++++++++++++++------ CLAUDE.md | 69 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 CLAUDE.md diff --git a/BrowserSearch/Main.cs b/BrowserSearch/Main.cs index f75b21c..c511b73 100644 --- a/BrowserSearch/Main.cs +++ b/BrowserSearch/Main.cs @@ -267,17 +267,40 @@ public List Query(Query query) private int CalculateScore(string query, string title, string url) { // Since PT Run's FuzzySearch is too slow, and the history usually has a lot of entries, - // lets calculate the scores manually using a faster (but less accurate) method - float titleScore = title.Contains(query, StringComparison.InvariantCultureIgnoreCase) - ? (query.Length / (float)title.Length * 100f) - : 0; - float urlScore = url.Contains(query, StringComparison.InvariantCultureIgnoreCase) - ? (query.Length / (float)url.Length * 100f) - : 0; + // lets calculate the scores manually using a faster (but less accurate) method. + // Multi-token: every whitespace-separated token must appear in title or url (AND). + string[] tokens = query.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length == 0) + { + return 0; + } - float score = new[] { titleScore, urlScore }.Max(); - score += _defaultBrowser!.CalculateExtraScore(query, title, url); + float score = 0; + foreach (string token in tokens) + { + bool inTitle = title.Contains(token, StringComparison.InvariantCultureIgnoreCase); + bool inUrl = url.Contains(token, StringComparison.InvariantCultureIgnoreCase); + if (!inTitle && !inUrl) + { + return 0; + } + + float titleContrib = inTitle ? token.Length / (float)title.Length * 100f : 0f; + float urlContrib = inUrl ? token.Length / (float)url.Length * 100f : 0f; + score += Math.Max(titleContrib, urlContrib); + } + // Bonus when the whole query appears contiguously, so phrase matches still outrank scattered tokens + if (title.Contains(query, StringComparison.InvariantCultureIgnoreCase)) + { + score += query.Length / (float)title.Length * 100f; + } + else if (url.Contains(query, StringComparison.InvariantCultureIgnoreCase)) + { + score += query.Length / (float)url.Length * 100f; + } + + score += _defaultBrowser!.CalculateExtraScore(query, title, url); return (int)score; } } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ace7d04 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +PowerToys Run plugin (C#, .NET 9, WPF, AnyCPU) that searches the default browser's history. Activated in PT Run via the `b?` action keyword. Distributed as a folder dropped into `%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\`. + +## Build / publish + +The project depends on four PowerToys assemblies that are NOT in NuGet and must be supplied manually before any build will succeed. Copy them from an installed PowerToys into `BrowserSearch/libs/`: + +- `Wox.Plugin.dll` +- `Wox.Infrastructure.dll` +- `Microsoft.Data.Sqlite.dll` +- `PowerToys.Settings.UI.Lib.dll` + +The README says to copy from `%ProgramFiles%\PowerToys\` (system-wide install). On this machine PowerToys is installed per-user at `%LOCALAPPDATA%\PowerToys\` instead — check both locations before assuming the DLLs are missing. + +Build commands: +- Debug: build via Visual Studio, then copy `BrowserSearch/bin/Debug/` into the PT Run plugins folder. +- Release: `pwsh ./publish.ps1` from the repo root. The script runs `dotnet publish BrowserSearch -c Release -o ./PublishOutput`, deletes two stray Win SDK DLLs, then replaces `%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\BrowserSearch\` with the publish output. PowerToys must be exited first or the file copy will fail on locked DLLs. + +There are no tests, no linter config, and no CI in this repo. + +`BrowserSearch.csproj` reads the version directly from `plugin.json` via a `File.ReadAllText().Split(',')[5]` expression — it depends on the field order in `plugin.json`. If you reorder keys in `plugin.json`, the build version will silently become wrong. + +## Architecture + +Entry point is `BrowserSearch/Main.cs` — implements `IPlugin`, `ISettingProvider`, `IReloadable` from `Wox.Plugin`. Lifecycle: + +1. `Init` / `ReloadData` calls `InitDefaultBrowser`, which uses `Wox.Plugin.Common.DefaultBrowserInfo` to detect the default browser. `BrowserInfo.UpdateIfTimePassed()` is async, so the code polls `BrowserInfo.Name` for up to 500 ms before giving up. +2. A large `switch (BrowserInfo.Name)` maps the friendly name (e.g. `"Brave Beta"`, `"Microsoft Edge Canary"`, `"NAVER Whale"`) to either a `Chromium` or `Firefox` instance, passing one or more candidate user-data directories. Adding a new browser = adding a `case` here. +3. `Query` runs on every keystroke. It loads cached history from the active `IBrowser`, scores entries with `CalculateScore`, sorts, and truncates to `_maxResults` (settings option, default 15; `-1` = unlimited). + +Scoring is intentionally NOT PT Run's `FuzzySearch` — that was found to be too slow over thousands of history rows. `Main.CalculateScore` does a case-insensitive `Contains` against title and url, computes `query.Length / target.Length * 100`, takes the max, and adds `IBrowser.CalculateExtraScore` on top. When changing scoring, preserve the "fast `Contains` path" — fuzzy matching here will tank query latency. + +### Browser abstraction (`Browsers/`) + +`IBrowser` has three methods: `Init`, `GetHistory`, `CalculateExtraScore`. Two concrete implementations: + +- **`Chromium`** — handles all Chromium-based browsers. Reads `User Data/Local State` JSON to discover profiles in `info_cache`, then for each profile copies two SQLite files to `%TEMP%` (the originals are locked while the browser runs) and reads them: `History` (table `urls`, ordered by `visit_count DESC`) and `Network Action Predictor` (used to boost score for query→URL pairs the user has previously selected). +- **`Firefox`** — copies `places.sqlite`, reads `moz_places` (`url`, `title`, `frecency`). Frecency is used as the extra-score component, divided by 1000. The history list is `Reverse()`d after loading so highest-frecency entries land at the *end*; `Query` returns `TakeLast(N)` for the empty-search case to surface the most-used pages. +- **`OperaGX`** — subclasses `Chromium` and overrides `CreateProfiles` because Opera GX has no `Local State` file; it hard-codes a single `"default"` profile pointed at the user-data dir directly. Multi-profile is unsupported. + +The `Chromium` constructor accepts an *array* of candidate user-data paths and picks the first that exists — this is how multiple installation variants (e.g. Opera Stable vs Opera Developer, or Firefox/Zen using the same handler) are supported behind one `case` in `Main.cs`. Note this means if a user has both variants installed, only one is loaded (mentioned in README as a known limitation). + +Profiles dictionaries are keyed by lowercase name. `Chromium.CreateProfiles` indexes each profile under multiple keys (directory name + each of `gaia_given_name`, `gaia_name`, `name`, `shortcut_name`) so the user's `SingleProfile` setting can match any of them. `Firefox.CreateProfiles` strips the random prefix from the `.` directory layout. + +History DB connections are opened, used, then closed/disposed inside `Init` — the in-memory `_history` list is the only thing kept alive. `_initialized` guards against re-running on `ReloadData` storms (PT Run calls `ReloadData` multiple times when toggling the plugin; `Main` additionally throttles to once per 300 ms via `_lastUpdateTickCount`). + +### Settings + +Two `PluginAdditionalOption`s declared in `Main.AdditionalOptions`: +- `MaxResults` (Numberbox, default 15, `-1` = all) — applied in `Query`. +- `SingleProfile` (Textbox, empty = all profiles) — passed into the browser implementation's constructor and matched (lowercased) against the profile dictionary at `Init` time. Changing this requires a `ReloadData` cycle to take effect. + +`CreateSettingPanel` throws `NotImplementedException` — settings are surfaced via PT Run's built-in additional-options UI, not a custom panel. Don't try to "fix" the throw. + +## Conventions + +- Nullable reference types are enabled. Honor it. +- `Log.Info`/`Log.Warn`/`Log.Error` from `Wox.Plugin.Logger` is the logging API; pass `typeof(SomeClass)` as the second arg for the source. +- User-facing errors that block functionality use `MessageBox.Show(..., "BrowserSearch")`. +- Plugin GUID is `E5A9FC7A3F7F4320BE612DA95C57C32D` (in both `plugin.json` and `Main.PluginID`); never change it — PT Run identifies the plugin by this ID. + +## Commit style + +Per global rules (`~/.claude/rules/no-coauthor.md`): never include a `Co-Authored-By: Claude ...` trailer in commits in any project, including this one. From fc0679c8a3ff4523bad2269eda945076645552c0 Mon Sep 17 00:00:00 2001 From: Silvio Fernando <50349062+sfcdsfcd@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:06:14 -0300 Subject: [PATCH 2/2] removing claude.md --- CLAUDE.md | 69 ------------------------------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index ace7d04..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,69 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What this is - -PowerToys Run plugin (C#, .NET 9, WPF, AnyCPU) that searches the default browser's history. Activated in PT Run via the `b?` action keyword. Distributed as a folder dropped into `%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\`. - -## Build / publish - -The project depends on four PowerToys assemblies that are NOT in NuGet and must be supplied manually before any build will succeed. Copy them from an installed PowerToys into `BrowserSearch/libs/`: - -- `Wox.Plugin.dll` -- `Wox.Infrastructure.dll` -- `Microsoft.Data.Sqlite.dll` -- `PowerToys.Settings.UI.Lib.dll` - -The README says to copy from `%ProgramFiles%\PowerToys\` (system-wide install). On this machine PowerToys is installed per-user at `%LOCALAPPDATA%\PowerToys\` instead — check both locations before assuming the DLLs are missing. - -Build commands: -- Debug: build via Visual Studio, then copy `BrowserSearch/bin/Debug/` into the PT Run plugins folder. -- Release: `pwsh ./publish.ps1` from the repo root. The script runs `dotnet publish BrowserSearch -c Release -o ./PublishOutput`, deletes two stray Win SDK DLLs, then replaces `%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins\BrowserSearch\` with the publish output. PowerToys must be exited first or the file copy will fail on locked DLLs. - -There are no tests, no linter config, and no CI in this repo. - -`BrowserSearch.csproj` reads the version directly from `plugin.json` via a `File.ReadAllText().Split(',')[5]` expression — it depends on the field order in `plugin.json`. If you reorder keys in `plugin.json`, the build version will silently become wrong. - -## Architecture - -Entry point is `BrowserSearch/Main.cs` — implements `IPlugin`, `ISettingProvider`, `IReloadable` from `Wox.Plugin`. Lifecycle: - -1. `Init` / `ReloadData` calls `InitDefaultBrowser`, which uses `Wox.Plugin.Common.DefaultBrowserInfo` to detect the default browser. `BrowserInfo.UpdateIfTimePassed()` is async, so the code polls `BrowserInfo.Name` for up to 500 ms before giving up. -2. A large `switch (BrowserInfo.Name)` maps the friendly name (e.g. `"Brave Beta"`, `"Microsoft Edge Canary"`, `"NAVER Whale"`) to either a `Chromium` or `Firefox` instance, passing one or more candidate user-data directories. Adding a new browser = adding a `case` here. -3. `Query` runs on every keystroke. It loads cached history from the active `IBrowser`, scores entries with `CalculateScore`, sorts, and truncates to `_maxResults` (settings option, default 15; `-1` = unlimited). - -Scoring is intentionally NOT PT Run's `FuzzySearch` — that was found to be too slow over thousands of history rows. `Main.CalculateScore` does a case-insensitive `Contains` against title and url, computes `query.Length / target.Length * 100`, takes the max, and adds `IBrowser.CalculateExtraScore` on top. When changing scoring, preserve the "fast `Contains` path" — fuzzy matching here will tank query latency. - -### Browser abstraction (`Browsers/`) - -`IBrowser` has three methods: `Init`, `GetHistory`, `CalculateExtraScore`. Two concrete implementations: - -- **`Chromium`** — handles all Chromium-based browsers. Reads `User Data/Local State` JSON to discover profiles in `info_cache`, then for each profile copies two SQLite files to `%TEMP%` (the originals are locked while the browser runs) and reads them: `History` (table `urls`, ordered by `visit_count DESC`) and `Network Action Predictor` (used to boost score for query→URL pairs the user has previously selected). -- **`Firefox`** — copies `places.sqlite`, reads `moz_places` (`url`, `title`, `frecency`). Frecency is used as the extra-score component, divided by 1000. The history list is `Reverse()`d after loading so highest-frecency entries land at the *end*; `Query` returns `TakeLast(N)` for the empty-search case to surface the most-used pages. -- **`OperaGX`** — subclasses `Chromium` and overrides `CreateProfiles` because Opera GX has no `Local State` file; it hard-codes a single `"default"` profile pointed at the user-data dir directly. Multi-profile is unsupported. - -The `Chromium` constructor accepts an *array* of candidate user-data paths and picks the first that exists — this is how multiple installation variants (e.g. Opera Stable vs Opera Developer, or Firefox/Zen using the same handler) are supported behind one `case` in `Main.cs`. Note this means if a user has both variants installed, only one is loaded (mentioned in README as a known limitation). - -Profiles dictionaries are keyed by lowercase name. `Chromium.CreateProfiles` indexes each profile under multiple keys (directory name + each of `gaia_given_name`, `gaia_name`, `name`, `shortcut_name`) so the user's `SingleProfile` setting can match any of them. `Firefox.CreateProfiles` strips the random prefix from the `.` directory layout. - -History DB connections are opened, used, then closed/disposed inside `Init` — the in-memory `_history` list is the only thing kept alive. `_initialized` guards against re-running on `ReloadData` storms (PT Run calls `ReloadData` multiple times when toggling the plugin; `Main` additionally throttles to once per 300 ms via `_lastUpdateTickCount`). - -### Settings - -Two `PluginAdditionalOption`s declared in `Main.AdditionalOptions`: -- `MaxResults` (Numberbox, default 15, `-1` = all) — applied in `Query`. -- `SingleProfile` (Textbox, empty = all profiles) — passed into the browser implementation's constructor and matched (lowercased) against the profile dictionary at `Init` time. Changing this requires a `ReloadData` cycle to take effect. - -`CreateSettingPanel` throws `NotImplementedException` — settings are surfaced via PT Run's built-in additional-options UI, not a custom panel. Don't try to "fix" the throw. - -## Conventions - -- Nullable reference types are enabled. Honor it. -- `Log.Info`/`Log.Warn`/`Log.Error` from `Wox.Plugin.Logger` is the logging API; pass `typeof(SomeClass)` as the second arg for the source. -- User-facing errors that block functionality use `MessageBox.Show(..., "BrowserSearch")`. -- Plugin GUID is `E5A9FC7A3F7F4320BE612DA95C57C32D` (in both `plugin.json` and `Main.PluginID`); never change it — PT Run identifies the plugin by this ID. - -## Commit style - -Per global rules (`~/.claude/rules/no-coauthor.md`): never include a `Co-Authored-By: Claude ...` trailer in commits in any project, including this one.