Skip to content

Conversation

dcog989
Copy link
Contributor

@dcog989 dcog989 commented Sep 25, 2025

Old / New Plugin Comparison

Feature Legacy Version Current Version
Architecture Tightly coupled, monolithic loaders Decoupled, service-oriented
Performance Synchronous, blocking operations Fully asynchronous, non-blocking, atomic data updates
Favicon Support Local database extraction only Multi-tiered retrieval (Cache, DB, Web) with parsing, scoring, and optimization
Reliability Basic file watching, potential race conditions Thread-safe file watching, robust refresh logic, improved error handling
Code Maintainability Low; difficult to test and modify High; clear separation of concerns facilitates testing and extension
User Experience Functional, with potential for UI lag Responsive UI, timely refreshes, ~100% favicon retrieval

@prlabeler prlabeler bot added the enhancement New feature or request label Sep 25, 2025
@github-actions github-actions bot added this to the 2.1.0 milestone Sep 25, 2025
Copy link

gitstream-cm bot commented Sep 25, 2025

🥷 Code experts: Jack251970, Yusyuriv

Jack251970, jjw24 have most 👩‍💻 activity in the files.
Yusyuriv, Jack251970 have most 🧠 knowledge in the files.

See details

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj

Activity based on git-commit:

Jack251970 jjw24
SEP 2 additions & 2 deletions 4 additions & 3 deletions
AUG 23 additions & 14 deletions
JUL 1 additions & 1 deletions
JUN 36 additions & 35 deletions
MAY
APR 2 additions & 2 deletions

Knowledge based on git-blame:
Jack251970: 38%
Yusyuriv: 7%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL 4 additions & 1 deletions
JUN 3 additions & 1 deletions 2 additions & 2 deletions
MAY
APR

Knowledge based on git-blame:
Jack251970: 14%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL 5 additions & 8 deletions
JUN 18 additions & 18 deletions
MAY 3 additions & 2 deletions
APR 17 additions & 17 deletions

Knowledge based on git-blame:
Yusyuriv: 55%
Jack251970: 17%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN
MAY
APR

Knowledge based on git-blame:
Yusyuriv: 68%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN 25 additions & 7 deletions
MAY
APR

Knowledge based on git-blame:
Yusyuriv: 42%
Jack251970: 40%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN 3 additions & 1 deletions
MAY
APR

Knowledge based on git-blame:
Yusyuriv: 56%
Jack251970: 11%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN 4 additions & 3 deletions
MAY
APR 18 additions & 18 deletions

Knowledge based on git-blame:
Jack251970: 11%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN
MAY
APR

Knowledge based on git-blame:
Yusyuriv: 73%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN 10 additions & 2 deletions
MAY
APR

Knowledge based on git-blame:
Jack251970: 83%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN 5 additions & 6 deletions
MAY
APR 2 additions & 7 deletions

Knowledge based on git-blame:
Yusyuriv: 63%
Jack251970: 6%

Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json

Activity based on git-commit:

Jack251970 jjw24
SEP
AUG
JUL
JUN
MAY 1 additions & 1 deletions
APR

Knowledge based on git-blame:

✨ Comment /gs review for LinearB AI review. Learn how to automate it here.

Copy link

gitstream-cm bot commented Sep 25, 2025

Be a legend 🏆 by adding a before and after screenshot of the changes you made, especially if they are around UI/UX.

Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Replaced legacy synchronous bookmark loaders and helpers with an async, service-oriented implementation: new async bookmark loaders, favicon extraction/conversion/caching, file-watching, browser detection, observable settings, MVVM UI changes, and Main refactored to IAsyncReloadable with asynchronous reloads and lifecycle management. (50 words)

Changes

Cohort / File(s) Summary
Orchestration / Main
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
Reworked plugin entry to implement IAsyncReloadable: async reloads, temp/cache lifecycle, instantiates and wires BookmarkLoaderService, FaviconService, BookmarkWatcherService, Firefox polling timer, query/result mapping, settings integration, and disposal.
Removed legacy loaders & helpers
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs, .../ChromiumBookmarkLoader.cs, .../EdgeBookmarkLoader.cs, .../FirefoxBookmarkLoader.cs, .../CustomChromiumBookmarkLoader.cs, .../CustomFirefoxBookmarkLoader.cs, .../Commands/BookmarkLoader.cs, .../Helper/FaviconHelper.cs, .../IBookmarkLoader.cs
Deleted old synchronous Chromium/Chrome/Edge/Firefox loader implementations, bookmark aggregation helper, legacy favicon helper, and legacy IBookmarkLoader interface.
Async services & loaders
.../Services/BookmarkLoaderService.cs, .../Services/ChromiumBookmarkLoader.cs, .../Services/FirefoxBookmarkLoader.cs, .../Services/IBookmarkLoader.cs, .../Services/BookmarkWatcherService.cs, .../Services/BrowserDetector.cs
Added BookmarkLoaderService orchestrator, new async IBookmarkLoader interface and implementations for Chromium/Firefox, BrowserDetector utilities, and a debounced file-watcher service BookmarkWatcherService.
Favicon pipeline & helpers
.../Services/FaviconService.cs, .../Services/FaviconWebClient.cs, .../Services/HtmlFaviconParser.cs, .../Services/ImageConverter.cs, .../Services/LocalFaviconExtractor.cs
Added FaviconService (cache, failure tracking, concurrency), HTTP client for favicons, HTML favicon parser, image conversion pipeline (ImageConverter), and local DB-backed favicon extractor.
Firefox profile utilities
.../Services/FirefoxProfileFinder.cs
Added FirefoxProfileFinder to locate places.sqlite across MSI/MSIX installs and parse profiles.ini.
Models & Settings
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs, .../Models/CustomBrowser.cs, .../Models/Settings.cs
Bookmark now includes ProfilePath and updated equality; CustomBrowser default BrowserTypeUnknown and removed AllBrowserTypes; Settings refactored to observable properties, renamed CustomChromiumBrowsersCustomBrowsers, and added favicon flags.
UI / MVVM
.../ViewModels/CustomBrowserSettingViewModel.cs, .../Views/CustomBrowserSetting.xaml, .../Views/CustomBrowserSetting.xaml.cs, .../Views/SettingsControl.xaml, .../Views/SettingsControl.xaml.cs
Added CustomBrowserSettingViewModel and MVVM-backed CustomBrowserSetting window; updated SettingsControl to bind to new settings and CustomBrowsers, replaced event handlers with command bindings.
Localization & metadata
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml, Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json
Updated localization keys for copy-URL, favicon options, engine detection and custom browser UI; plugin description and author updated.
Project file
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
Non-semantic formatting change: hidden/zero-width character at file start.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Flow as FlowLauncher
  participant Plugin as BrowserBookmark.Main
  participant LoaderSvc as BookmarkLoaderService
  participant Chromium as ChromiumBookmarkLoader
  participant Firefox as FirefoxBookmarkLoader
  participant Favicon as FaviconService
  participant Watcher as BookmarkWatcherService

  Flow->>Plugin: Init(context, settings)
  Plugin->>Plugin: Setup temp/cache & services
  Plugin->>LoaderSvc: LoadBookmarksAsync(ct)
  par loaders
    LoaderSvc->>Chromium: GetBookmarksAsync()
    Chromium-->>LoaderSvc: bookmarks (async stream)
    LoaderSvc->>Firefox: GetBookmarksAsync()
    Firefox-->>LoaderSvc: bookmarks (async stream)
  end
  LoaderSvc-->>Plugin: (Bookmarks, DiscoveredFiles)
  Plugin->>Favicon: ProcessBookmarkFavicons(bookmarks, ct)
  Plugin->>Watcher: UpdateWatchers(discoveredFiles)
  User->>Plugin: Query(q)
  Plugin-->>User: Results
  Watcher-->>Plugin: OnBookmarkFileChanged
  Plugin->>Plugin: ReloadDataAsync()
Loading
sequenceDiagram
  autonumber
  participant Favicon as FaviconService
  participant Local as LocalFaviconExtractor
  participant Web as FaviconWebClient
  participant Html as HtmlFaviconParser
  participant Img as ImageConverter
  participant Cache as FaviconCache

  Favicon->>Local: GetFaviconDataAsync(bookmark)
  alt local DB hit
    Local-->>Favicon: bytes
    Favicon->>Img: ToPngAsync(stream)
    Img-->>Favicon: png + size
    Favicon->>Cache: Save PNG
  else local miss & web enabled
    Favicon->>Web: GetHtmlHeadAsync(pageUri)
    Web-->>Favicon: html + base
    Favicon->>Html: Parse(html, base)
    Html-->>Favicon: candidates
    Favicon->>Web: DownloadFaviconAsync(candidate)
    Web-->>Favicon: stream
    Favicon->>Img: ToPngAsync(stream)
    Img-->>Favicon: png + size
    Favicon->>Cache: Save PNG
  end
  Favicon-->>Favicon: Set bookmark.FaviconPath
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • jjw24
  • onesounds

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title “BrowserBookmark plugin, advanced” is too generic and does not clearly convey the primary changes introduced by the pull request, such as its shift to an asynchronous, service-oriented architecture or the expanded favicon support. Its use of the word “advanced” is vague and does not summarize the main technical enhancements. Consider renaming the pull request title to more explicitly describe the key change, for example: “Refactor BrowserBookmark plugin to async service-oriented architecture with multi-tier favicon support.”
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed The pull request description presents a clear feature comparison between the legacy and current implementations, directly relating to the architectural, performance, and feature enhancements introduced by the changeset.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (34)
Scripts/BuilderToolbox.ps1 (6)

230-234: Dispose event jobs and process to avoid leaks.

Unregister-Event does not remove the event jobs. Add Remove-Job and dispose the process.

     Unregister-Event -SubscriptionId $stdOutEvent.Id
     Unregister-Event -SubscriptionId $stdErrEvent.Id
-    
-    return ($process.ExitCode -eq 0 -or $IgnoreErrors)
+    try { Remove-Job -Id $stdOutEvent.Id -Force -ErrorAction SilentlyContinue } catch {}
+    try { Remove-Job -Id $stdErrEvent.Id -Force -ErrorAction SilentlyContinue } catch {}
+    try { $process.Close() } catch {}
+    try { $process.Dispose() } catch {}
+    return ($process.ExitCode -eq 0 -or $IgnoreErrors)

327-333: Pass self-contained as a canonical boolean string.

dotnet CLI expects true/false; avoid relying on PowerShell’s boolean stringification.

-    $arguments = "publish `"$($global:mainProjectFile)`" -c Release -r $($global:publishRuntimeId) --self-contained $IsSelfContained -o `"$PublishDir`""
+    $sc = $(if ($IsSelfContained) { 'true' } else { 'false' })
+    $arguments = "publish `"$($global:mainProjectFile)`" -c Release -r $($global:publishRuntimeId) --self-contained $sc -o `"$PublishDir`""

380-382: Fix quoting for git pretty format on Windows.

Use double quotes for the format string; single quotes can be unreliable on Windows.

-        $gitLogCommand = "log $commitRange --pretty=format:'* %h - %s (%an)'"
+        $gitLogCommand = "log $commitRange --pretty=format:`"* %h - %s (%an)`""
         $changelogContent = (git -C $global:solutionRoot $gitLogCommand)

655-663: Prefer JSON parsing to remove createdump from deps.json instead of regex.

Text regex is brittle and risks corrupting JSON. Parse and edit the JSON, then serialize back.

If desired, I can provide a JSON-editing implementation tailored to the deps.json structure used here.


136-161: Use dotnet tool update for VPK instead of uninstall/install.

Simpler and preserves settings; still pins to the required version.

-        Invoke-DotnetCommand -Command "tool" -Arguments "uninstall -g vpk" -IgnoreErrors $true
-        if (-not (Invoke-DotnetCommand -Command "tool" -Arguments "install -g vpk --version $requiredVpkVersion")) {
+        if (-not (Invoke-DotnetCommand -Command "tool" -Arguments "update -g vpk --version $requiredVpkVersion")) {
             Write-Host "Failed to install required VPK version." -ForegroundColor Red
             return $false
         }

113-133: Read SDK version from global.json and verify SolutionAssemblyInfo.cs path

  • In Scripts/BuilderToolbox.ps1, replace
    $global:requiredDotNetVersion = "9"
    with logic to parse .sdk.version from the repo’s global.json (falling back as needed).
  • Immediately after
    $global:assemblyInfoPath = Join-Path $solutionRoot "SolutionAssemblyInfo.cs"
    add a Test-Path check and error out if the file doesn’t exist.
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1)

46-59: Avoid rebuilding the entire HTML string each loop.

Calling contentBuilder.ToString() on every iteration re-materializes the full head (up to ~500 KB) and makes the loop O(n²). Scan only the recent tail instead.

-            var contentBuilder = new StringBuilder();
+            const string HeadCloseTag = "</head>";
+            var contentBuilder = new StringBuilder();
             var buffer = new char[4096];
             int charsRead;
             var totalCharsRead = 0;
             const int maxCharsToRead = 500 * 1024;

             while (!token.IsCancellationRequested && (charsRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0 && totalCharsRead < maxCharsToRead)
             {
                 contentBuilder.Append(buffer, 0, charsRead);
                 totalCharsRead += charsRead;

-                if (contentBuilder.ToString().Contains("</head>", StringComparison.OrdinalIgnoreCase))
-                {
-                    break;
-                }
+                var scanLength = Math.Min(contentBuilder.Length, buffer.Length + HeadCloseTag.Length);
+                if (scanLength >= HeadCloseTag.Length)
+                {
+                    var segment = contentBuilder.ToString(contentBuilder.Length - scanLength, scanLength);
+                    if (segment.Contains(HeadCloseTag, StringComparison.OrdinalIgnoreCase))
+                    {
+                        break;
+                    }
+                }
             }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (2)

57-60: Ensure temp directory exists and avoid duplicate entries after fallback

  • Create the temp folder before copying.
  • Clear partially-read results before re-reading from the copy to avoid duplicates.

Apply this diff:

-                tempDbPath = Path.Combine(_tempPath, $"ff_places_{Guid.NewGuid()}.sqlite");
-                File.Copy(_placesPath, tempDbPath, true);
-                await ReadBookmarksFromDb(tempDbPath, bookmarks, cancellationToken);
+                tempDbPath = Path.Combine(_tempPath, $"ff_places_{Guid.NewGuid()}.sqlite");
+                Directory.CreateDirectory(_tempPath);
+                File.Copy(_placesPath, tempDbPath, true);
+                bookmarks.Clear(); // avoid duplicates if direct-read partially succeeded
+                await ReadBookmarksFromDb(tempDbPath, bookmarks, cancellationToken);

95-104: Use typed getters for SQLite columns

Typed access reduces overhead and avoids string dictionary lookups.

Apply this diff:

-            var title = reader["title"]?.ToString() ?? string.Empty;
-            var url = reader["url"]?.ToString();
+            var url = !reader.IsDBNull(0) ? reader.GetString(0) : null;
+            var title = !reader.IsDBNull(1) ? reader.GetString(1) : string.Empty;
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (3)

24-31: Unsubscribe events before disposing watchers to prevent handler leaks

Dispose alone doesn’t detach delegates; unsubscribe explicitly.

Apply this diff:

-        foreach (var watcher in _watchers)
-        {
-            watcher.EnableRaisingEvents = false;
-            watcher.Dispose();
-        }
+        foreach (var watcher in _watchers)
+        {
+            watcher.EnableRaisingEvents = false;
+            watcher.Changed -= OnFileChanged;
+            watcher.Created -= OnFileChanged;
+            watcher.Deleted -= OnFileChanged;
+            watcher.Renamed -= OnFileRenamed;
+            watcher.Dispose();
+        }

19-20: Guard timer callback after disposal

Race: a scheduled callback could fire after Dispose. Check _disposed inside the callback.

Apply this diff:

-        _debounceTimer = new Timer(_ => OnBookmarkFileChanged?.Invoke(), null, Timeout.Infinite, Timeout.Infinite);
+        _debounceTimer = new Timer(_ =>
+        {
+            lock (_lock)
+            {
+                if (_disposed) return;
+            }
+            OnBookmarkFileChanged?.Invoke();
+        }, null, Timeout.Infinite, Timeout.Infinite);

41-46: Increase FileSystemWatcher buffer to reduce overflow under bursty writes

Default buffer (8 KB) can overflow easily for frequent bookmark updates.

Apply this diff:

             var watcher = new FileSystemWatcher(directory)
             {
                 Filter = fileName,
-                NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size
+                NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size,
+                InternalBufferSize = 64 * 1024
             };
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1)

72-79: Optional: also require a non-empty name to enable Save

Prevents saving nameless entries.

If desired, introduce a computed property and update the command:

-    [RelayCommand(CanExecute = nameof(IsValidPath))]
+    [RelayCommand(CanExecute = nameof(CanSave))]
     private void Save()
     {
         _originalBrowser.Name = EditableBrowser.Name;
         _originalBrowser.DataDirectoryPath = EditableBrowser.DataDirectoryPath;
         _originalBrowser.BrowserType = EditableBrowser.BrowserType;
         _closeAction(true);
     }
+
+    public bool CanSave => IsValidPath && !string.IsNullOrWhiteSpace(EditableBrowser.Name);

Also call SaveCommand.NotifyCanExecuteChanged() when Name changes (subscribe similarly to DataDirectoryPath).

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1)

57-57: Optional: use class name as log tag

Improves log filtering consistency.

Apply this diff:

-            _context.API.LogDebug(nameof(Parse), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");
+            _context.API.LogDebug(nameof(HtmlFaviconParser), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)

33-39: Use a single source of truth for the selected item

Mixing SelectedCustomBrowser and CustomBrowsers.SelectedItem is inconsistent. Prefer the bound property and fall back to control only if necessary.

Apply this diff:

-        if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser)
-        {
-            Settings.CustomBrowsers.Remove(selectedCustomBrowser);
-        }
+        var selected = SelectedCustomBrowser ?? CustomBrowsers.SelectedItem as CustomBrowser;
+        if (selected is not null)
+        {
+            Settings.CustomBrowsers.Remove(selected);
+        }

Ensure SelectedCustomBrowser is bound to the list’s SelectedItem in XAML.


65-67: Optional: set window owner for modality/focus

Helps keep the dialog centered and modal to Settings.

Apply this diff:

-        var window = new CustomBrowserSetting(SelectedCustomBrowser);
+        var window = new CustomBrowserSetting(SelectedCustomBrowser) { Owner = Window.GetWindow(this) };
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (2)

17-18: Remove redundant ternary.

GetPlacesPathFromProfileDir already returns null when not found; return it directly.

-        return !string.IsNullOrEmpty(placesPath) ? placesPath : null;
+        return placesPath;

53-64: Limit the [Install...] parsing to its section to avoid cross‑section matches.

Current regex can capture Default from later sections under rare INI layouts. Parse the [Install...] block first, then read Default within it.

-            var installMatch = Regex.Match(iniContent, @"^\[Install[^\]]+\](?:.|\n|\r)*?^Default\s*=\s*(.+)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
-            if (installMatch.Success)
+            var installSection = Regex.Match(iniContent, @"^\[Install[^\]]+\](?:.|\n|\r)*?(?=\r?\n\[|$)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
+            if (installSection.Success)
             {
-                var path = installMatch.Groups[1].Value.Trim();
+                var defaultMatch = Regex.Match(installSection.Value, @"^Default\s*=\s*(.+)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
+                if (defaultMatch.Success)
+                {
+                    var path = defaultMatch.Groups[1].Value.Trim();
                 // This path is typically relative, e.g., "Profiles/xyz.default-release"
                 var profilePath = Path.Combine(profileFolderPath, path.Replace('/', Path.DirectorySeparatorChar));
                 var placesDb = Path.Combine(profilePath, "places.sqlite");
                 if (File.Exists(placesDb))
                 {
                     return placesDb;
                 }
+                }
             }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)

53-58: Preserve aspect ratio for SVG scaling.

Non‑uniform x/y scaling can distort icons. Use uniform scale and center.

-                using var bitmap = new SKBitmap(TargetIconSize, TargetIconSize);
-                using var canvas = new SKCanvas(bitmap);
-                canvas.Clear(SKColors.Transparent);
-                var scaleMatrix = SKMatrix.CreateScale((float)TargetIconSize / svg.Picture.CullRect.Width, (float)TargetIconSize / svg.Picture.CullRect.Height);
-                canvas.DrawPicture(svg.Picture, in scaleMatrix);
+                var w = svg.Picture.CullRect.Width;
+                var h = svg.Picture.CullRect.Height;
+                var scale = MathF.Min(TargetIconSize / w, TargetIconSize / h);
+                var tx = (TargetIconSize - w * scale) / 2f;
+                var ty = (TargetIconSize - h * scale) / 2f;
+
+                using var bitmap = new SKBitmap(TargetIconSize, TargetIconSize);
+                using var canvas = new SKCanvas(bitmap);
+                canvas.Clear(SKColors.Transparent);
+                canvas.Translate(tx, ty);
+                canvas.Scale(scale);
+                canvas.DrawPicture(svg.Picture);

23-44: Honor cancellation between conversion attempts.

Check the token before each attempt to bail out quickly.

         var (pngData, size) = TryConvertSvgToPng(ms);
         if (pngData is not null) return (pngData, size);
 
+        token.ThrowIfCancellationRequested();
         ms.Position = 0;
         (pngData, size) = TryConvertIcoToPng(ms);
         if (pngData is not null) return (pngData, size);
         
+        token.ThrowIfCancellationRequested();
         ms.Position = 0;
         (pngData, size) = TryConvertBitmapToPng(ms);
         return (pngData, size);
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)

137-140: Avoid duplicate Firefox loaders when MSI and MSIX point to the same places.sqlite.

Guard against yielding both if paths are equal.

-            if (!string.IsNullOrEmpty(msixPlacesPath))
+            if (!string.IsNullOrEmpty(msixPlacesPath) && !string.Equals(msixPlacesPath, placesPath, StringComparison.OrdinalIgnoreCase))
             {
                 yield return new FirefoxBookmarkLoader("Firefox (Store)", msixPlacesPath, _tempPath, logAction);
             }

53-54: Align dedup logic with desired equality
Distinct() uses Bookmark’s record-generated Equals (all four properties) and its custom GetHashCode (Name + Url). If you intend to dedupe by Url only, switch to bookmarks.DistinctBy(b => b.Url) or supply an IEqualityComparer<Bookmark> that compares Url.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (2)

49-62: Broaden error handling to avoid aborting on IO issues.

File.OpenRead and ParseAsync can throw IOException/UnauthorizedAccessException; catch them per profile to continue.

-            catch (JsonException ex)
+            catch (JsonException ex)
             {
                 _logException(nameof(ChromiumBookmarkLoader), $"Failed to parse bookmarks file for {_browserName}: {bookmarkPath}", ex);
             }
+            catch (IOException ex)
+            {
+                _logException(nameof(ChromiumBookmarkLoader), $"IO error reading bookmarks file for {_browserName}: {bookmarkPath}", ex);
+            }
+            catch (UnauthorizedAccessException ex)
+            {
+                _logException(nameof(ChromiumBookmarkLoader), $"Access denied reading bookmarks file for {_browserName}: {bookmarkPath}", ex);
+            }

93-118: Optional: be cancellation-friendly in deep trees.

Check cancellation periodically within the child traversal to improve responsiveness.

Would you like a patch adding token checks inside the recursion?

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml (2)

129-129: Update binding to push changes as the user types.

Enable immediate VM updates for name input.

-                            Text="{Binding EditableBrowser.Name}" />
+                            Text="{Binding EditableBrowser.Name, UpdateSourceTrigger=PropertyChanged}" />

156-156: Update binding to push path changes as the user types.

Helps live engine detection feedback.

-                                Text="{Binding EditableBrowser.DataDirectoryPath}" />
+                                Text="{Binding EditableBrowser.DataDirectoryPath, UpdateSourceTrigger=PropertyChanged}" />
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1)

23-30: Do not infer Firefox by substring; detect by DB presence (or explicit type).

Relying on Source.Contains("Firefox") misclassifies custom Firefox-based browsers (e.g., Librewolf, Waterfox) and silently falls back to the Chromium query path, lowering local favicon hit rate. Prefer checking for the presence of favicons.sqlite/Favicons in the profile path (and/or an explicit BrowserType on Bookmark).

Apply this diff to make detection robust and case-insensitive with DB presence fallback:

-    public async Task<byte[]?> GetFaviconDataAsync(Bookmark bookmark, CancellationToken token)
-    {
-        return bookmark.Source switch
-        {
-            var s when s.Contains("Firefox") => await GetFirefoxFaviconAsync(bookmark, token),
-            _ => await GetChromiumFaviconAsync(bookmark, token) // Default to Chromium
-        };
-    }
+    public Task<byte[]?> GetFaviconDataAsync(Bookmark bookmark, CancellationToken token)
+    {
+        var firefoxDb = Path.Combine(bookmark.ProfilePath, "favicons.sqlite");
+        var chromiumDb = Path.Combine(bookmark.ProfilePath, "Favicons");
+
+        if (File.Exists(firefoxDb))
+            return GetFirefoxFaviconAsync(bookmark, token);
+        if (File.Exists(chromiumDb))
+            return GetChromiumFaviconAsync(bookmark, token);
+
+        // Heuristic fallback if neither DB is present
+        if (!string.IsNullOrEmpty(bookmark.Source) &&
+            bookmark.Source.Contains("Firefox", StringComparison.OrdinalIgnoreCase))
+            return GetFirefoxFaviconAsync(bookmark, token);
+
+        return GetChromiumFaviconAsync(bookmark, token);
+    }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

100-107: Cache key mismatch: per-URL check vs per-host write. Unify to host-level.

ProcessBookmarkFavicons checks cache with a URL-hash, but FetchAndCacheFaviconAsync writes per-host. This causes repeated work on every run for bookmarks without local favicons.

Apply this diff to check/write with a host-based key from the start:

-            var cachePath = GetCachePath(bookmark.Url, _faviconCacheDir);
+            var cacheKey = Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var uriForKey)
+                ? uriForKey.GetLeftPart(UriPartial.Authority)
+                : bookmark.Url;
+            var cachePath = GetCachePath(cacheKey, _faviconCacheDir);
             if (File.Exists(cachePath))
             {
                 bookmark.FaviconPath = cachePath;
                 return;
             }

This also ensures local DB extractions are stored under the same per-host cache as web fetches.


232-240: Minor: clarify SelectBestFavicon default path.

The Size default is 0, so icoResult is chosen even when both are “no data.” It’s harmless because you check PngData later, but consider using -1 as the default Size to make intent explicit.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml (2)

8-8: Minor: description clarity.

Consider clarifying that web retrieval is optional and local DB extraction is attempted first, e.g., “Search your browser bookmarks; load favicons from your browser (and optionally the web).”


16-17: Nit: “Others...” vs “Other browsers…”

If this toggles custom browsers, “Other browsers…” may be clearer. Optional.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (3)

73-74: Use Volatile.Read for the bookmarks snapshot.

To pair with Volatile.Write in ReloadDataAsync and avoid visibility issues under concurrency, read with Volatile.Read.

Apply this diff:

-        var bookmarks = _bookmarks; // use a local copy
+        var bookmarks = Volatile.Read(ref _bookmarks); // atomic snapshot

52-68: Temp directory setup fallback.

If Temp deletion/creation fails, subsequent operations may break. Consider falling back to Path.GetTempPath() on failure.


236-261: Context menu: dual icon sources.

You set both Glyph and IcoPath. If both are supported, fine; otherwise prefer one to avoid UI inconsistencies.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e068104 and 50c9f4b.

📒 Files selected for processing (35)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomFirefoxBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs (0 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Languages/en.xaml (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/BaseModel.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml (8 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml (5 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (3 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json (1 hunks)
  • Scripts/BuilderToolbox.ps1 (1 hunks)
💤 Files with no reviewable changes (9)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomChromiumBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromiumBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Helper/FaviconHelper.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/CustomFirefoxBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ChromeBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/EdgeBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Commands/BookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/IBookmarkLoader.cs
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-06-06T05:26:32.163Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3664
File: Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs:326-326
Timestamp: 2025-06-06T05:26:32.163Z
Learning: In the Firefox bookmark loader (FirefoxBookmarkLoader.cs), using hardcoded Windows line endings ("\r\n") for splitting profiles.ini content is acceptable and platform-agnostic line splitting is not required.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs
📚 Learning: 2024-10-08T15:52:58.573Z
Learnt from: taooceros
PR: Flow-Launcher/Flow.Launcher#2616
File: Flow.Launcher/Flow.Launcher.csproj:7-7
Timestamp: 2024-10-08T15:52:58.573Z
Learning: In the Flow Launcher project, the version number in the `Flow.Launcher.csproj` file is dynamically updated during the CI/CD process.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
📚 Learning: 2025-09-13T06:04:26.977Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3593
File: Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml.cs:143-165
Timestamp: 2025-09-13T06:04:26.977Z
Learning: In Plugins/Flow.Launcher.Plugin.WebSearch/SettingsControl.xaml, the ListView_SizeChanged handler in the C# file assumes 5 GridViewColumns but the XAML may have more columns. The user Jack251970 indicated this mismatch doesn't need to be fixed when pointed out during PR #3593 review.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml
📚 Learning: 2025-09-11T08:30:29.487Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3593
File: Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml:0-0
Timestamp: 2025-09-11T08:30:29.487Z
Learning: In Flow.Launcher's SettingsPaneTheme.xaml theme editor panel, the inner ui:ScrollViewerEx within the Border (Grid.Column="1") is intentionally kept because its height cannot be expanded to the whole page due to layout constraints. This serves a different scrolling purpose than the outer ScrollViewerEx and is necessary for the theme editor panel functionality.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml
🧬 Code graph analysis (17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (2)
  • CustomBrowserSettingViewModel (14-101)
  • CustomBrowserSettingViewModel (43-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (7-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)
  • BrowserType (29-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2)
  • OnBookmarkFileChanged (138-141)
  • Dispose (265-274)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (3)
  • Task (85-105)
  • FirefoxBookmarkLoader (13-106)
  • FirefoxBookmarkLoader (31-37)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (2)
  • ChromiumBookmarkLoader (14-120)
  • ChromiumBookmarkLoader (23-29)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (4)
  • FirefoxProfileFinder (9-114)
  • GetFirefoxPlacesPath (11-18)
  • GetFirefoxMsixPlacesPath (20-40)
  • GetPlacesPathFromProfileDir (42-113)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • IBookmarkLoader (152-165)
  • Task (26-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-30)
  • Task (32-41)
  • Task (43-53)
  • Task (55-91)
  • Task (93-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs (1)
  • FirefoxBookmarkLoaderBase (14-204)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
  • Task (75-92)
  • Task (94-132)
  • Task (134-150)
  • Task (163-230)
  • Task (242-263)
  • Task (265-278)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (85-105)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (7)
  • Task (75-92)
  • Task (94-132)
  • Task (134-150)
  • Task (163-230)
  • Task (242-263)
  • Task (265-278)
  • Dispose (280-288)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1)
  • Task (23-44)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)
  • FaviconService (17-289)
  • FaviconService (36-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • BookmarkLoaderService (13-166)
  • BookmarkLoaderService (19-24)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (3)
  • FaviconService (17-289)
  • FaviconService (36-54)
  • Dispose (280-288)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Flow.Launcher.Core/Plugin/PluginManager.cs (5)
  • List (488-515)
  • Task (94-112)
  • Task (114-122)
  • Task (124-131)
  • Task (133-140)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (9-68)
  • SettingsControl (14-18)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (2)
  • FirefoxProfileFinder (9-114)
  • GetPlacesPathFromProfileDir (42-113)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1)
  • IBookmarkLoader (152-165)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (7-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (2)
  • CustomBrowserSetting (11-37)
  • CustomBrowserSetting (15-24)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-136)
  • LocalFaviconExtractor (17-21)
  • Task (23-30)
  • Task (32-41)
  • Task (43-53)
  • Task (55-91)
  • Task (93-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-131)
  • FaviconWebClient (19-32)
  • Task (34-74)
  • Task (76-124)
  • Dispose (126-130)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (110-121)
  • Task (161-217)
  • List (70-92)
  • List (234-263)
  • Result (94-108)
  • Dispose (265-274)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
  • Task (75-92)
  • Task (94-132)
  • Task (134-150)
  • Task (163-230)
  • Task (242-263)
  • Task (265-278)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (2)
  • Task (34-74)
  • Task (76-124)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (4)
  • IBookmarkLoader (152-165)
  • IEnumerable (56-59)
  • IEnumerable (61-106)
  • IEnumerable (108-150)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserDetector (9-50)
  • IEnumerable (11-27)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/BaseModel.cs (2)
  • BaseModel (8-16)
  • OnPropertyChanged (12-15)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (7-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (7-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserType (29-49)
  • BrowserDetector (9-50)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/BaseModel.cs (1)
  • OnPropertyChanged (12-15)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs

[warning] 128-128:
msix is not a recognized word. (unrecognized-spelling)


[warning] 139-139:
msix is not a recognized word. (unrecognized-spelling)


[warning] 137-137:
msix is not a recognized word. (unrecognized-spelling)


[warning] 131-131:
msix is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs

[warning] 246-246:
copylink is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json

[warning] 6-6:
dcog is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 143-143:
Cooldown is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs

[warning] 3-3:
Skia is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: gitStream.cm
  • GitHub Check: build
🔇 Additional comments (17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj (2)

107-109: LGTM on the ProjectReference regrouping.

Thanks for keeping the project reference intact while reorganizing the item groups; this remains correct.


86-99: Icon loading unaffected by build-action change
Plugin code and plugin.json reference Images\bookmark.png and Images\copylink.png via file paths (IcoPath) and rely on CopyToOutputDirectory, not XAML pack URIs. Switching to <None Update="…"> has no impact on runtime loading.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/plugin.json (1)

5-12: Manifest updates look good.

New description, author, and language metadata all align with the revamped plugin surface.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)

11-47: Defaulting to Unknown browser type is sensible.

Letting detection/UI drive the browser choice avoids accidentally treating every new custom entry as Chromium.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Bookmark.cs (1)

6-19: New bookmark shape looks consistent.

Adding ProfilePath while basing equality on Name+Url keeps deduping behaviour stable and gives the loaders the extra context they need.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)

7-11: Interface contract reads clean.

Name plus async enumeration is exactly the surface the loader service needs—nice.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)

14-48: Property change notifications wired correctly

The backing fields plus OnPropertyChanged pattern keep the UI bindings reactive as we toggle the various loader/favicon options. Nice cleanup.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/BaseModel.cs (1)

7-16: Shared INotifyPropertyChanged base is welcome

This lightweight base keeps the models aligned with the new MVVM flow without dragging in extra dependencies. Looks good.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml (1)

103-117: Favicon options binding matches the new settings

Binding FetchMissingFavicons’ IsEnabled to Settings.EnableFavicons gives the right UX and keeps everything in sync with the settings model.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)

11-49: Centralized detection logic looks solid

The staged checks for Chromium Bookmarks and Firefox places.sqlite cover the common cases cleanly and reuse the existing Firefox finder—nice addition.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (2)

14-23: No action required: GeneratedRegex is supported under net9.0-windows. The BrowserBookmark plugin targets net9.0-windows, which fully supports the GeneratedRegex attribute.


64-67: No change required: API uses LogWarn
The IPublicAPI interface and PublicAPIInstance implement LogWarn(), so LogWarning is not defined.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)

69-101: Verify WPF IconBitmapDecoder usage off the UI thread.

WPF imaging types can require STA; this code may run on background thread. Confirm this path is safe in your hosting context or guard with a fallback.

Would you like me to provide a STA-dispatch wrapper or an alternative ICO decode path (Skia-only)?


11-21: No field name mismatch
rg search shows only _imageConverter in the codebase; there are no _image_converter references.

Likely an incorrect or invalid review comment.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml (1)

1-18: Confirm runtime DataContext initialization.

Design-time context is set; ensure code-behind or caller sets the VM at runtime.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1)

93-106: LGTM: gzip favicon handling.

GZip header detection and decompression path look correct; exceptions bubble to caller and are logged there.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

216-217: Good: favicon processing limited to the delta.

Processing only the newly loaded Firefox bookmarks reduces work and IO.

@Jack251970 Jack251970 requested a review from Copilot September 26, 2025 07:55
@Jack251970 Jack251970 added the review in progress Indicates that a review is in progress for this PR label Sep 26, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR completely rewrites the Browser Bookmark plugin with an advanced, service-oriented architecture that significantly improves performance, reliability, and user experience. The changes replace the legacy monolithic design with a modern, decoupled system featuring comprehensive favicon support and robust error handling.

  • Replaced synchronous, tightly-coupled loaders with asynchronous, service-based architecture
  • Added multi-tiered favicon retrieval system with local database extraction, web fetching, and intelligent caching
  • Implemented thread-safe file watching with debounced refresh logic and periodic Firefox bookmark updates

Reviewed Changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Main.cs Complete rewrite with async/await patterns, service injection, and improved lifecycle management
Services/*.cs New service layer with specialized classes for bookmark loading, favicon processing, and file watching
ViewModels/CustomBrowserSettingViewModel.cs New MVVM pattern implementation with automatic browser type detection
Models/*.cs Enhanced models with nullable annotations and improved property change notifications
Views/*.cs Updated UI components with cleaner data binding and simplified code-behind
Comments suppressed due to low confidence (1)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs:1

  • The exception handling in the Dispose method swallows the exception from line 74, and the timer operations on lines 75-76 are outside the catch block. Move the timer cleanup inside a finally block or handle the exception appropriately.
using System;

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)

51-62: Avoid breaking persisted settings: assign explicit enum values.

Reordering the enum to insert Unknown first changes underlying numeric values. If settings serialize enums as numbers (default for System.Text.Json without JsonStringEnumConverter), existing persisted Chromium(0)/Firefox(1) values would deserialize to Unknown/Chromium respectively. Assign explicit values to preserve compatibility.

Apply:

 [EnumLocalize]
 public enum BrowserType
 {
-    [EnumLocalizeValue("Unknown")]
-    Unknown,
-
-    [EnumLocalizeValue("Chromium")]
-    Chromium,
-
-    [EnumLocalizeValue("Firefox")]
-    Firefox,
+    [EnumLocalizeValue("Unknown")]
+    Unknown = 2,
+
+    [EnumLocalizeValue("Chromium")]
+    Chromium = 0,
+
+    [EnumLocalizeValue("Firefox")]
+    Firefox = 1,
 }

Additionally, consider migrating settings to string‑based enum serialization to avoid future reorder hazards.

🧹 Nitpick comments (8)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1)

86-110: Optional: Prefer a true folder picker for UX.

If acceptable to add a dependency, consider CommonOpenFileDialog (Windows API Code Pack) for a proper folder picker instead of the “fake file name” trick.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (2)

51-60: Avoid O(n^2) string growth when searching for .

contentBuilder.ToString() each loop re-materializes the full string. Scan only the newly appended segment.

Apply this diff:

-                if (contentBuilder.ToString().Contains("</head>", StringComparison.OrdinalIgnoreCase))
-                {
-                    break;
-                }
+                // Search only the recent segment (with overlap) to avoid O(n^2) behavior.
+                var start = Math.Max(0, contentBuilder.Length - buffer.Length - 16);
+                var len = contentBuilder.Length - start;
+                if (len > 0 && contentBuilder.ToString(start, len)
+                        .IndexOf("</head>", StringComparison.OrdinalIgnoreCase) >= 0)
+                {
+                    break;
+                }

82-87: Gate downloads by Content-Type to skip HTML and other non-images.

Preemptively reject non-image payloads.

Apply this diff:

             if (!response.IsSuccessStatusCode)
                 return null;

+            var mediaType = response.Content.Headers.ContentType?.MediaType;
+            if (!string.IsNullOrEmpty(mediaType) &&
+                !mediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) &&
+                !string.Equals(mediaType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1)

113-119: Dispose the MemoryStream created from localData.

Prevent unnecessary allocations lingering in parallel loops.

Apply this diff:

-                var (pngData, _) = await _imageConverter.ToPngAsync(new MemoryStream(localData), token);
+                using var ms = new MemoryStream(localData);
+                var (pngData, _) = await _imageConverter.ToPngAsync(ms, token);
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)

44-46: Make “Default” comparison case-insensitive.

Be robust against casing differences when labeling the source.

-            var source = _browserName + (Path.GetFileName(profilePath) == "Default" ? "" : $" ({Path.GetFileName(profilePath)})");
+            var profileName = Path.GetFileName(profilePath);
+            var source = _browserName + (string.Equals(profileName, "Default", StringComparison.OrdinalIgnoreCase) ? "" : $" ({profileName})");

92-111: Guard JsonElement.GetString() with ValueKind checks.

Non-conforming data can make ValueKind != String and GetString() will throw. Defensive checks keep the async stream resilient.

-            if (subElement.TryGetProperty("type", out var type))
+            if (subElement.TryGetProperty("type", out var type) && type.ValueKind == JsonValueKind.String)
             {
-                switch (type.GetString())
+                var typeStr = type.GetString();
+                switch (typeStr)
                 {
                     case "folder":
                     case "workspace": // Edge Workspace
                         EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
                         break;
                     case "url":
-                        if (subElement.TryGetProperty("name", out var name) &&
-                            subElement.TryGetProperty("url", out var url) &&
-                            !string.IsNullOrEmpty(name.GetString()) &&
-                            !string.IsNullOrEmpty(url.GetString()))
+                        var hasName = subElement.TryGetProperty("name", out var name) && name.ValueKind == JsonValueKind.String;
+                        var hasUrl = subElement.TryGetProperty("url", out var url) && url.ValueKind == JsonValueKind.String;
+                        if (hasName && hasUrl)
                         {
-                            bookmarks.Add(new Bookmark(name.GetString()!, url.GetString()!, source, profilePath));
+                            var nameStr = name.GetString();
+                            var urlStr = url.GetString();
+                            if (!string.IsNullOrEmpty(nameStr) && !string.IsNullOrEmpty(urlStr))
+                            {
+                                bookmarks.Add(new Bookmark(nameStr!, urlStr!, source, profilePath));
+                            }
                         }
                         break;
                 }
             }
             else
             {
                 _logException(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.", null);
             }

63-67: Optional: stream bookmarks as you parse to reduce memory.

Yield bookmarks directly during traversal instead of buffering the whole list per profile. This lowers peak memory and time-to-first-result.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (1)

24-36: Mark handled to avoid double execution on Enter/Escape.

Without e.Handled = true, default button bindings may also fire, causing duplicate Save/Cancel.

     private void WindowKeyDown(object sender, KeyEventArgs e)
     {
         if (e.Key == Key.Enter)
         {
             if (_viewModel.SaveCommand.CanExecute(null))
             {
                 _viewModel.SaveCommand.Execute(null);
+                e.Handled = true;
             }
         }
         else if (e.Key == Key.Escape)
         {
             _viewModel.CancelCommand.Execute(null);
+            e.Handled = true;
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88535f7 and d28213b.

📒 Files selected for processing (10)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (3 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-06T05:26:32.163Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3664
File: Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs:326-326
Timestamp: 2025-06-06T05:26:32.163Z
Learning: In the Firefox bookmark loader (FirefoxBookmarkLoader.cs), using hardcoded Windows line endings ("\r\n") for splitting profiles.ini content is acceptable and platform-agnostic line splitting is not required.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
📚 Learning: 2024-11-03T07:34:24.926Z
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:34:24.926Z
Learning: In Windows Forms dialogs, Windows handles invalid paths and prevents the user from clicking "Ok" if the path is incorrect, so additional path validation is unnecessary.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs
📚 Learning: 2024-11-03T07:40:11.014Z
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs
🧬 Code graph analysis (10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2)
  • Task (107-118)
  • Task (158-214)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
  • Task (75-92)
  • Task (94-132)
  • Task (134-159)
  • Task (172-239)
  • Task (251-272)
  • Task (274-287)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (2)
  • Task (86-107)
  • CleanupTempFiles (109-136)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • IBookmarkLoader (152-165)
  • Task (26-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1)
  • IAsyncEnumerable (30-68)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (6)
  • CleanupTempFiles (114-141)
  • Task (23-30)
  • Task (32-41)
  • Task (43-53)
  • Task (55-97)
  • Task (99-112)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (3)
  • Task (107-118)
  • Task (158-214)
  • Dispose (262-271)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (7)
  • Task (75-92)
  • Task (94-132)
  • Task (134-159)
  • Task (172-239)
  • Task (251-272)
  • Task (274-287)
  • Dispose (289-297)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-30)
  • Task (32-41)
  • Task (43-53)
  • Task (55-97)
  • Task (99-112)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1)
  • Task (23-44)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (2)
  • CustomBrowserSettingViewModel (13-111)
  • CustomBrowserSettingViewModel (42-53)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)
  • BrowserType (29-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (2)
  • CustomBrowserSetting (9-38)
  • CustomBrowserSetting (13-22)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (7)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • BookmarkLoaderService (13-166)
  • BookmarkLoaderService (19-24)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (9)
  • FaviconService (17-298)
  • FaviconService (36-54)
  • Task (75-92)
  • Task (94-132)
  • Task (134-159)
  • Task (172-239)
  • Task (251-272)
  • Task (274-287)
  • Dispose (289-297)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (86-107)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-30)
  • Task (32-41)
  • Task (43-53)
  • Task (55-97)
  • Task (99-112)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (8-67)
  • SettingsControl (13-17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-142)
  • LocalFaviconExtractor (17-21)
  • Task (23-30)
  • Task (32-41)
  • Task (43-53)
  • Task (55-97)
  • Task (99-112)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (107-118)
  • Task (158-214)
  • List (67-89)
  • List (231-260)
  • Result (91-105)
  • Dispose (262-271)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserType (29-49)
  • BrowserDetector (9-50)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (4)
  • IBookmarkLoader (152-165)
  • IEnumerable (56-59)
  • IEnumerable (61-106)
  • IEnumerable (108-150)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • IAsyncEnumerable (41-84)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserDetector (9-50)
  • IEnumerable (11-27)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs

[warning] 70-70:
wal is not a recognized word. (unrecognized-spelling)


[warning] 67-67:
shm is not a recognized word. (unrecognized-spelling)


[warning] 67-67:
shm is not a recognized word. (unrecognized-spelling)


[warning] 66-66:
wal is not a recognized word. (unrecognized-spelling)


[warning] 66-66:
wal is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 142-142:
Cooldown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1)

86-110: Folder selection via WPF OpenFileDialog looks good; no disposal needed.

This uses Microsoft.Win32.OpenFileDialog (not IDisposable) with CheckPathExists, which is sufficient. Prior note about disposing FolderBrowserDialog doesn’t apply here.

Based on learnings

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1)

22-27: TLS validation restored — thank you.

Removal of DangerousAcceptAnyServerCertificateValidator fixes the security hole.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)

22-30: Dialog flow for adding custom browsers looks good.

Adds on OK without forcing a reload here; Main listens and reloads.


59-66: Verify SelectedCustomBrowser binding.

Ensure XAML binds the list’s SelectedItem to SelectedCustomBrowser so Edit flows work after focus changes.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1)

134-159: Cancellation-aware shared fetch is correct now.

Awaiting the shared task with WaitAsync(token) preserves sharing while honoring cancellation.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (3)

9-9: Default Unknown is appropriate for detection flow.

Using Unknown as the initial state aligns with BrowserDetector’s contract and the view-model validation.


54-56: Verify localization for “Unknown”.

Ensure “Unknown” is localized across plugin language resources to match existing Chromium/Firefox entries.


51-62: Confirm enum serialization mode.

If enums are serialized numerically, reordering can break compatibility. If string-based serialization is already configured, the risk is mitigated.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1)

113-116: Good fix: removed unsafe GetString() in no-type branch.

This addresses the prior exception risk noted in earlier review.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (1)

26-36: Good fix: respect CanExecute before Save on Enter.

This resolves the earlier issue where invalid paths could be saved via Enter.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (2)

45-61: Avoid O(n²) string allocations in the head-scan loop.

Calling contentBuilder.ToString() on every iteration is expensive. Scan the buffer incrementally for "" instead.

Apply this diff:

-            var contentBuilder = new StringBuilder();
-            var buffer = new char[4096];
-            int charsRead;
-            var totalCharsRead = 0;
-            const int maxHeadChars = 150 * 1024;
-
-            while (!token.IsCancellationRequested && (charsRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0 && totalCharsRead < maxHeadChars)
-            {
-                contentBuilder.Append(buffer, 0, charsRead);
-                totalCharsRead += charsRead;
-
-                if (contentBuilder.ToString().Contains("</head>", StringComparison.OrdinalIgnoreCase))
-                {
-                    break;
-                }
-            }
+            var contentBuilder = new StringBuilder();
+            var buffer = new char[4096];
+            int charsRead;
+            var totalCharsRead = 0;
+            const int maxHeadChars = 150 * 1024;
+            ReadOnlySpan<char> pattern = "</head>".AsSpan();
+            var matchIndex = 0;
+            var foundHead = false;
+
+            while (!token.IsCancellationRequested &&
+                   (charsRead = await reader.ReadAsync(buffer, 0, buffer.Length)) > 0 &&
+                   totalCharsRead < maxHeadChars)
+            {
+                contentBuilder.Append(buffer, 0, charsRead);
+                totalCharsRead += charsRead;
+
+                for (var i = 0; i < charsRead; i++)
+                {
+                    var c = char.ToLowerInvariant(buffer[i]);
+                    var p = char.ToLowerInvariant(pattern[matchIndex]);
+                    if (c == p)
+                    {
+                        matchIndex++;
+                        if (matchIndex == pattern.Length)
+                        {
+                            foundHead = true;
+                            break;
+                        }
+                    }
+                    else
+                    {
+                        matchIndex = c == char.ToLowerInvariant(pattern[0]) ? 1 : 0;
+                    }
+                }
+                if (foundHead) break;
+            }

88-107: Pre-size MemoryStream to reduce allocations and honor Content-Length.

Small win on large icons and avoids re-alloc churn.

Apply this diff:

-            await using var contentStream = await response.Content.ReadAsStreamAsync(token);
-            var memoryStream = new MemoryStream();
+            await using var contentStream = await response.Content.ReadAsStreamAsync(token);
+            var contentLength = response.Content.Headers.ContentLength;
+            var capacity = (int)Math.Min(contentLength.GetValueOrDefault(MaxFaviconBytes), MaxFaviconBytes);
+            var memoryStream = new MemoryStream(capacity > 0 ? capacity : 0);
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (2)

117-144: Deduplicate cleanup logic via shared helper.

This CleanupTempFiles duplicates LocalFaviconExtractor’s method. Consider sharing a small utility to reduce duplication.


41-92: Stream results instead of buffering whole list (optional).

Yield as you read to cut memory and surface results sooner; only fallback path needs a retry branch.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

100-131: Guard against rare concurrent writes to the same cache file.

Parallel processing could race on identical URLs. Use CreateNew and fall back if already written.

Apply this diff:

-                    await File.WriteAllBytesAsync(cachePath, pngData, token);
-                    bookmark.FaviconPath = cachePath;
+                    try
+                    {
+                        await using var fs = new FileStream(cachePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
+                        await fs.WriteAsync(pngData, token);
+                        bookmark.FaviconPath = cachePath;
+                    }
+                    catch (IOException)
+                    {
+                        // Someone else created it; just use it.
+                        if (File.Exists(cachePath))
+                            bookmark.FaviconPath = cachePath;
+                    }
                     return;

36-54: Add note: cache eviction is out of scope but advisable.

Consider a size-/age-based cleanup (LRU by last access) capped to a few hundred MB to prevent unbounded growth.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

44-47: Fire-and-forget without logging can hide failures (optional).

Wrap background calls with a try/catch logger to avoid unobserved exceptions.

Also applies to: 135-139, 120-127

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15a7a58 and 31fd6fd.

📒 Files selected for processing (6)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-06T05:26:32.163Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3664
File: Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs:326-326
Timestamp: 2025-06-06T05:26:32.163Z
Learning: In the Firefox bookmark loader (FirefoxBookmarkLoader.cs), using hardcoded Windows line endings ("\r\n") for splitting profiles.ini content is acceptable and platform-agnostic line splitting is not required.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
🧬 Code graph analysis (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (7)
  • Task (75-92)
  • Task (94-132)
  • Task (134-159)
  • Task (172-239)
  • Task (255-276)
  • Task (278-291)
  • Dispose (293-301)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1)
  • Task (23-44)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (8)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • BookmarkLoaderService (13-166)
  • BookmarkLoaderService (19-24)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (9)
  • FaviconService (17-302)
  • FaviconService (36-54)
  • Task (75-92)
  • Task (94-132)
  • Task (134-159)
  • Task (172-239)
  • Task (255-276)
  • Task (278-291)
  • Dispose (293-301)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (94-115)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)
  • BrowserType (29-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (8-67)
  • SettingsControl (13-17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-150)
  • LocalFaviconExtractor (17-21)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (107-118)
  • Task (158-214)
  • List (67-89)
  • List (231-260)
  • Result (91-105)
  • Dispose (262-271)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • IBookmarkLoader (152-165)
  • Task (26-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (6)
  • CleanupTempFiles (122-149)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 142-142:
Cooldown is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs

[warning] 66-66:
shm is not a recognized word. (unrecognized-spelling)


[warning] 65-65:
wal is not a recognized word. (unrecognized-spelling)


[warning] 65-65:
wal is not a recognized word. (unrecognized-spelling)


[warning] 64-64:
wal is not a recognized word. (unrecognized-spelling)


[warning] 61-61:
shm is not a recognized word. (unrecognized-spelling)


[warning] 60-60:
wal is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1)

22-31: TLS validation is now honored — good fix.

The insecure ServerCertificateCustomValidationCallback override is gone. This resolves the prior MITM risk.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)

22-31: SQL join/filters look correct now.

Using IS NOT NULL and guarding url aligns with SQLite syntax and avoids nulls.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (3)

134-159: Cancellation handling in shared web fetch is correct.

Awaiting the shared task with WaitAsync(token) and not canceling the producer fixes prior cancellation gap.


161-170: Good: deterministic cache filenames via SHA-256.

Stable hashing avoids path issues and collisions.


241-253: Best-result selection now respects non-null PngData.

Fix addresses prior bug where a larger “empty” result could win.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

67-85: Volatile.Read in Query — good.

Prevents stale reads when swapping the list.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)

41-94: Consider optimizing the fallback logic to avoid unnecessary copies.

Currently, the code always attempts a direct read first and falls back to copying if the database is locked. When Firefox is known to be running (which is common), this results in a predictable failure followed by file copying on every load.

Consider checking if Firefox is running before attempting the direct read to skip the failed attempt when unnecessary.

 public async IAsyncEnumerable<Bookmark> GetBookmarksAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
 {
     if (string.IsNullOrEmpty(_placesPath) || !File.Exists(_placesPath))
         yield break;

     var bookmarks = new List<Bookmark>();
     string? tempDbPath = null;
+    bool firefoxIsRunning = IsFirefoxRunning(); // Helper to check if Firefox process is active

     try
     {
-        // First, try to read directly from the source to avoid a slow file copy
-        await ReadBookmarksFromDb(_placesPath, bookmarks, cancellationToken);
+        if (!firefoxIsRunning)
+        {
+            // Try to read directly from the source when Firefox is not running
+            await ReadBookmarksFromDb(_placesPath, bookmarks, cancellationToken);
+        }
+        else
+        {
+            // Firefox is running, go straight to fallback copy
+            throw new SqliteException("Firefox is running", 5);
+        }
     }

This optimization would improve performance in the common case where Firefox is open by avoiding the predictable direct read failure.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

100-107: Unify cache keying (URL vs origin) to avoid duplicate writes and extra work.

You cache web results by origin but check/save local results by full URL. This causes redundant lookups/writes and inconsistent paths.

Apply this diff to prefer origin cache when available and write local conversions to the same path:

-            var cachePath = GetCachePath(bookmark.Url, _faviconCacheDir);
-            if (File.Exists(cachePath))
-            {
-                bookmark.FaviconPath = cachePath;
-                return;
-            }
+            var urlCachePath = GetCachePath(bookmark.Url, _faviconCacheDir);
+            string? originCachePath = null;
+            if (Uri.TryCreate(bookmark.Url, UriKind.Absolute, out var parsed))
+                originCachePath = GetCachePath(parsed.GetLeftPart(UriPartial.Authority), _faviconCacheDir);
+            var existingPath = originCachePath != null && File.Exists(originCachePath)
+                ? originCachePath
+                : (File.Exists(urlCachePath) ? urlCachePath : null);
+            if (existingPath != null)
+            {
+                bookmark.FaviconPath = existingPath;
+                return;
+            }
@@
-                var (pngData, _) = await _imageConverter.ToPngAsync(new MemoryStream(localData), token);
+                var (pngData, _) = await _imageConverter.ToPngAsync(new MemoryStream(localData), token);
                 if (pngData != null)
                 {
-                    await File.WriteAllBytesAsync(cachePath, pngData, token);
-                    bookmark.FaviconPath = cachePath;
+                    var destPath = originCachePath ?? urlCachePath;
+                    await File.WriteAllBytesAsync(destPath, pngData, token);
+                    bookmark.FaviconPath = destPath;
                     return;
                 }

No change needed here; just noting the mismatch source:

-        var cachePath = GetCachePath(urlString, _faviconCacheDir);
+        var cachePath = GetCachePath(urlString, _faviconCacheDir); // origin-based

Also applies to: 113-119, 172-177


41-47: Future: add eviction for orphaned favicons.

You create/cache indefinitely under FaviconCache but don’t prune. Consider periodic cleanup keyed to current bookmarks or LRU with cap.

I can draft a lightweight cleanup that deletes cache files not referenced by current bookmarks and older than N days. Want a patch?

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2)

213-221: Avoid duplicate bookmarks on periodic Firefox reload.

Use Distinct on the newly loaded Firefox set and the final concat to prevent dupes.

Apply this diff:

-            var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
+            var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).Distinct().ToList();
@@
-            var newBookmarkList = otherBookmarks.Concat(newFirefoxBookmarks).Distinct().ToList();
+            var newBookmarkList = otherBookmarks.Concat(newFirefoxBookmarks).Distinct().ToList();

282-289: Unsubscribe the file-change handler on dispose.

Prevents dangling delegate references and eases GC.

Apply this diff:

     public void Dispose()
     {
         _settings.PropertyChanged -= OnSettingsPropertyChanged;
         _settings.CustomBrowsers.CollectionChanged -= OnCustomBrowsersChanged;
+        _bookmarkWatcher.OnBookmarkFileChanged -= OnBookmarkFileChanged;
         _firefoxBookmarkTimer?.Dispose();
         _cancellationTokenSource.Cancel();
         _cancellationTokenSource.Dispose();
         _faviconService.Dispose();
         _bookmarkWatcher.Dispose();
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31fd6fd and 3094249.

📒 Files selected for processing (3)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-06T05:26:32.163Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3664
File: Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs:326-326
Timestamp: 2025-06-06T05:26:32.163Z
Learning: In the Firefox bookmark loader (FirefoxBookmarkLoader.cs), using hardcoded Windows line endings ("\r\n") for splitting profiles.ini content is acceptable and platform-agnostic line splitting is not required.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
🧬 Code graph analysis (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • IBookmarkLoader (152-165)
  • Task (26-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (6)
  • CleanupTempFiles (122-149)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-150)
  • LocalFaviconExtractor (17-21)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (5)
  • Task (108-127)
  • Task (167-232)
  • List (68-90)
  • List (249-278)
  • Dispose (280-289)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (7)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • BookmarkLoaderService (13-166)
  • BookmarkLoaderService (19-24)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (9)
  • FaviconService (19-302)
  • FaviconService (36-54)
  • Task (75-92)
  • Task (94-132)
  • Task (134-159)
  • Task (172-239)
  • Task (255-276)
  • Task (278-291)
  • Dispose (293-301)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (96-117)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (8-67)
  • SettingsControl (13-17)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs

[warning] 66-66:
shm is not a recognized word. (unrecognized-spelling)


[warning] 65-65:
wal is not a recognized word. (unrecognized-spelling)


[warning] 65-65:
wal is not a recognized word. (unrecognized-spelling)


[warning] 64-64:
wal is not a recognized word. (unrecognized-spelling)


[warning] 61-61:
shm is not a recognized word. (unrecognized-spelling)


[warning] 60-60:
wal is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 142-142:
Cooldown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (4)

22-31: Good job fixing the SQL syntax issue!

The query now correctly uses IS NOT NULL in the WHERE clause instead of the invalid NOT NULL syntax in JOIN conditions. This ensures proper SQL execution and filtering of null values.


56-77: Good implementation of WAL/SHM support and duplicate prevention!

The implementation correctly:

  1. Copies WAL/SHM files alongside the main database to preserve recent changes when Firefox is running
  2. Clears the bookmarks collection before fallback read to prevent duplicates

This ensures bookmark data consistency during the fallback scenario.


96-100: ProfilePath correctly uses the original Firefox profile directory.

The implementation properly derives the profile path from _placesPath (the original location) rather than dbPath (which could be a temp copy). This ensures favicon extraction works correctly even when reading from a temporary database copy.


119-146: CleanupTempFiles implementation is robust and matches LocalFaviconExtractor pattern.

The cleanup method properly handles all associated database files (WAL/SHM) using a wildcard pattern and includes appropriate error handling. The implementation is consistent with the same pattern used in LocalFaviconExtractor.cs.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (3)

134-159: Cancellation honored correctly for shared fetch.

Using WaitAsync(token) with try/catch cleanly detaches caller cancellation without killing the shared task. LGTM.


161-169: Good: secure, stable cache keys.

Switched to SHA-256 for deterministic filenames. This addresses weak-hash feedback.


241-253: Fixed: selection now ignores null candidates.

The selector prefers non-null PngData and size appropriately. Resolves the earlier drop of valid favicons.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

71-71: Correct memory barriers for bookmark swap.

Using Volatile.Read matches the Volatile.Write in reload and avoids stale reads.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1)

41-47: Future: Cache eviction strategy.

Consider size/time-based eviction for FaviconCache and periodic cleanup of FaviconFails.json entries older than the cooldown window.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

144-147: Optional: Fire-and-forget error surfacing.

Consider logging failures from ReloadDataAsync to avoid silent task faults.

Apply pattern:

-        _ = ReloadDataAsync();
+        _ = ReloadDataAsync().ContinueWith(t => Context.API.LogException(nameof(Main), "ReloadDataAsync failed.", t.Exception!), TaskContinuationOptions.OnlyOnFaulted);

Also applies to: 45-48, 131-136, 140-142

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3094249 and 2cfc1a2.

📒 Files selected for processing (2)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (7)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • BookmarkLoaderService (13-166)
  • BookmarkLoaderService (19-24)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (9)
  • FaviconService (19-305)
  • FaviconService (36-54)
  • Task (75-95)
  • Task (97-135)
  • Task (137-162)
  • Task (175-242)
  • Task (258-279)
  • Task (281-294)
  • Dispose (296-304)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (96-117)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (8-67)
  • SettingsControl (13-17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-150)
  • LocalFaviconExtractor (17-21)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (108-127)
  • Task (173-238)
  • List (68-90)
  • List (255-284)
  • Result (92-106)
  • Dispose (286-295)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 145-145:
Cooldown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (9)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (5)

17-17: Good: FaviconCandidate moved to namespace scope.

Unblocks unqualified references from other types (e.g., HtmlFaviconParser).


164-173: Good: SHA-256 for cache hashing.

Avoids weak SHA-1 and ensures deterministic, collision-resistant filenames.


244-256: Good: Selection now ignores null PngData.

Fixes prior bug dropping valid favicons.


50-54: Security: Ensure HttpClient uses default TLS validation.

Prior review flagged DangerousAcceptAnyServerCertificateValidator in FaviconWebClient. Please confirm it’s removed.

#!/bin/bash
# Search for insecure TLS overrides
rg -nP -C2 '(DangerousAcceptAnyServerCertificateValidator|ServerCertificateCustomValidationCallback)' --type=cs

137-162: Approve code changes: Web fetch now honors cancellation via Task.WaitAsync(token), preventing stuck callers while preserving the shared fetch.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (4)

68-90: Good: Volatile.Read paired with Volatile.Write.

Prevents stale reads during atomic swaps in Query.


108-127: Good: Serialized reloads via _reloadGate.

Avoids overlapping swaps and watcher races.


156-170: Good: Hardened PeriodicTimer loop.

Local snapshot and exception handling prevent disposal races.


219-233: Good: Targeted Firefox reload without blocking full list.

Merges results by source and kicks off favicon processing for just the new set.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

124-126: Minor: Dispose the transient MemoryStream.

Use a using-var to avoid a tiny allocation leak and signal intent.

Apply this diff:

-                var (pngData, _) = await _imageConverter.ToPngAsync(new MemoryStream(localData), token);
+                using var ms = new MemoryStream(localData);
+                var (pngData, _) = await _imageConverter.ToPngAsync(ms, token);

320-329: Dispose owned components if they implement IDisposable.

_localExtractor, _htmlParser, and _imageConverter may hold unmanaged resources. Dispose them if applicable.

Consider:

public void Dispose()
{
    _cts.Cancel();
    _ongoingFetches.Clear();

    (_localExtractor as IDisposable)?.Dispose();
    (_htmlParser as IDisposable)?.Dispose();
    (_imageConverter as IDisposable)?.Dispose();
    _webClient.Dispose();
    _fileLock.Dispose();
    _cts.Dispose();
    GC.SuppressFinalize(this);
}

Please confirm whether these types implement IDisposable. If not, ignore.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cfc1a2 and fa9c7c2.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 162-162:
Cooldown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (7)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (7)

17-17: Resolved: FaviconCandidate promoted to namespace scope.

This addresses the earlier compile-visibility concern. Looks good.


75-96: Resolved: SaveFailedFetchesAsync now swallows ObjectDisposedException.

The added ODE catch prevents shutdown-time task faults. File lock handling is correct.


104-120: Unified host/page cache key checks are correct.

Preferring a host-level cache while still honoring preexisting page-level files avoids duplication and improves hit rate.


154-179: Resolved: Web fetch honors cancellation without canceling the shared task.

Using WaitAsync(token) with OCE handling matches the intended behavior.


181-190: Good: Cache path uses SHA‑256.

Secure, collision‑resistant hashing for filenames addresses prior SHA1 concern.


268-280: Resolved: Selection guards against null PngData.

Selection logic now prefers available data and avoids dropping valid results.


50-54: FaviconWebClient enforces default TLS certificate validation.
No custom ServerCertificateCustomValidationCallback is set; HttpClientHandler uses strict validation by default.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (5)

106-142: Avoid races and small memory leak in local-cache write path

  • The MemoryStream instance isn’t disposed.
  • Writes can still race; prefer atomic temp-write + move.

Apply this diff:

-                var (pngData, _) = await _imageConverter.ToPngAsync(new MemoryStream(localData), token);
+                using var ms = new MemoryStream(localData, writable: false);
+                var (pngData, _) = await _imageConverter.ToPngAsync(ms, token);
                 if (pngData != null)
                 {
-                    var path = hostCachePath;
-                    try
-                    {
-                        await File.WriteAllBytesAsync(path, pngData, token);
-                    }
-                    catch (IOException)
-                    {
-                        // Another thread may have created it concurrently.
-                    }
+                    var path = hostCachePath;
+                    var tmp = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
+                    try
+                    {
+                        await File.WriteAllBytesAsync(tmp, pngData, token);
+                        try { File.Move(tmp, path, overwrite: false); }
+                        catch (IOException)
+                        {
+                            // Another thread may have created it concurrently.
+                        }
+                    }
+                    finally
+                    {
+                        try { if (File.Exists(tmp)) File.Delete(tmp); } catch { /* best effort */ }
+                    }
                     if (File.Exists(path))
                     {
                         bookmark.FaviconPath = path;
                         return;
                     }
                     // If write failed and file still doesn't exist, fall through to web fallback.
                 }

98-105: Parallelism constant: consider making it configurable or CPU-aware

MaxDegreeOfParallelism = 8 is reasonable, but making it configurable or using Environment.ProcessorCount could better fit diverse machines.


196-253: Make web-cache writes atomic to eliminate race and partial-file risks

Current direct writes may race; prefer temp-write + atomic move, mirroring the local path suggestion.

Apply this diff:

-            if (bestResult.PngData != null)
+            if (bestResult.PngData != null)
             {
                 try
                 {
-                    await File.WriteAllBytesAsync(cachePath, bestResult.PngData, _cts.Token);
+                    var tmp = cachePath + "." + Guid.NewGuid().ToString("N") + ".tmp";
+                    try
+                    {
+                        await File.WriteAllBytesAsync(tmp, bestResult.PngData, _cts.Token);
+                        try { File.Move(tmp, cachePath, overwrite: false); }
+                        catch (IOException)
+                        {
+                            // Another thread may have created it concurrently.
+                        }
+                    }
+                    finally
+                    {
+                        try { if (File.Exists(tmp)) File.Delete(tmp); } catch { /* best effort */ }
+                    }
                 }
                 catch (IOException)
                 {
                     // Another thread may have created it concurrently.
                 }
                 if (File.Exists(cachePath))
                 {
                     _context.API.LogDebug(nameof(FaviconService), $"Favicon for {urlString} cached successfully.");
                     if (_failedFetches.TryRemove(urlString, out _))
                     {
                         _ = SaveFailedFetchesAsync();
                     }
                     return cachePath;
                 }
                 // Do not treat as success; let finally record failure if needed.
             }

266-271: Optional: prune aged failure entries on load/save

To keep FaviconFails.json small, consider dropping entries older than FailedFaviconCooldown during LoadFailedFetches or just before Save.


328-336: Optional: best‑effort flush before cancel on dispose

If you want to persist last updates, consider a best‑effort SaveFailedFetchesAsync using CancellationToken.None before calling _cts.Cancel().

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa9c7c2 and fdfee20.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-150)
  • LocalFaviconExtractor (17-21)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (108-127)
  • Task (173-238)
  • List (68-90)
  • List (255-284)
  • Result (92-106)
  • Dispose (286-295)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 166-166:
Cooldown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (5)

17-17: FaviconCandidate promoted to namespace scope — LGTM

This resolves the earlier visibility/qualification issue with HtmlFaviconParser.


75-96: Graceful shutdown: SaveFailedFetchesAsync handles ODE — LGTM

Catching ObjectDisposedException (and OCE) avoids unobserved task faults during shutdown.


158-183: Cancellation honored for shared web fetch — LGTM

Using WaitAsync(token) with per-caller cancellation while keeping the shared fetch alive addresses the earlier cancellation gap.


185-194: Secure hashing for cache keys — LGTM

SHA-256 hashing for deterministic cache paths addresses the weak-hash concern.


276-288: Best-result selection fixed to avoid null PngData — LGTM

Null checks before size comparison prevent dropping valid favicons.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

172-178: Consider "cooldown" spelling in comments

The static analysis tool flags "Cooldown" as unrecognized. While this is commonly used in technical contexts, consider using "cool-down period" or "backoff period" for formal documentation if style guidelines require it.


98-163: Extract MaxDegreeOfParallelism into a configurable setting
Move the hard-coded MaxDegreeOfParallelism = 8 in FaviconService.cs (line 102) into a new Settings.MaxFaviconParallelism property (default 8) so users can tune concurrency for I/O-heavy favicon fetching.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdfee20 and 43934c4.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-51)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-150)
  • LocalFaviconExtractor (17-21)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (10-105)
  • HtmlFaviconParser (25-28)
  • List (30-81)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (5)
  • Task (108-127)
  • Task (173-238)
  • List (68-90)
  • List (255-284)
  • Dispose (286-295)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 173-173:
Cooldown is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (8)

17-17: Move FaviconCandidate to namespace scope

HtmlFaviconParser references it unqualified, but it's currently nested inside FaviconService.

Based on previous review, this was already flagged. The FaviconCandidate is now at the correct namespace scope, so this is resolved.


56-73: LGTM!

The LoadFailedFetches method correctly loads the failed fetches from JSON and handles exceptions appropriately. The use of case-insensitive StringComparer is good for URL authorities.


75-96: LGTM!

Good fix - the method now properly handles ObjectDisposedException during disposal races, as recommended in previous reviews.


194-201: LGTM: Using SHA256 provides better security than SHA1

The code correctly uses SHA256.HashData() for generating cache keys, which is the recommended choice over SHA-1 due to security considerations. Microsoft recommends SHA-256 or better due to collision problems with SHA-1. The static HashData method is also more efficient than creating and managing a HashAlgorithm instance.


104-163: Improved cache handling with atomic writes

The ProcessBookmarkFavicons method now implements the cache key unification and atomic writes that were suggested in previous reviews. The logic correctly:

  1. Prefers host-based cache paths for consistency
  2. Falls back to page-based cache paths for legacy compatibility
  3. Uses atomic temp file writes with proper cleanup
  4. Only sets FaviconPath when the file actually exists

165-190: LGTM: Proper cancellation handling in web fetch

The GetFaviconFromWebAsync method correctly implements cancellation handling as suggested in previous reviews. It now honors the cancellation token via WaitAsync() while keeping the shared fetch task alive for other callers.


240-280: LGTM: Fixed cache existence verification

The FetchAndCacheFaviconAsync method correctly implements the fixes from previous reviews:

  1. Only logs success and returns cache path if the file actually exists
  2. Proper error handling for concurrent writes
  3. Failure tracking only occurs when fetch was attempted and no cache file exists

283-295: LGTM: Fixed favicon selection logic

The SelectBestFavicon method correctly implements the null check fixes from previous reviews, ensuring that results with null PngData are not selected over valid alternatives.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43934c4 and ef6e339.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

@Jack251970 Jack251970 requested a review from Copilot September 28, 2025 10:26
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


namespace Flow.Launcher.Plugin.BrowserBookmark.Services;

public readonly record struct FaviconCandidate(string Url, int Score);
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The FaviconCandidate record is defined inside the service file but is used by other services like HtmlFaviconParser. Consider moving this to a separate Models file or namespace to improve code organization and reusability.

Copilot uses AI. Check for mistakes.

Comment on lines +13 to +14
[GeneratedRegex("<link[^>]+?>", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")]
private static partial Regex LinkTagRegex();
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using multiple compiled regexes can impact startup performance. Consider combining related patterns or using a single regex with named groups to reduce the compilation overhead during service initialization.

Copilot uses AI. Check for mistakes.

{
if (!_settings.EnableFavicons) return;

var options = new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = cancellationToken };
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The MaxDegreeOfParallelism value of 8 is hardcoded. Consider making this configurable through settings or determining it dynamically based on system capabilities (e.g., Environment.ProcessorCount) to optimize performance across different hardware configurations.

Suggested change
var options = new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = cancellationToken };
var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = cancellationToken };

Copilot uses AI. Check for mistakes.

private List<Bookmark> _bookmarks = [];
private readonly CancellationTokenSource _cancellationTokenSource = new();
private PeriodicTimer? _firefoxBookmarkTimer;
private static readonly TimeSpan FirefoxPollingInterval = TimeSpan.FromHours(3);
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The Firefox polling interval is hardcoded to 3 hours. Consider making this configurable through settings to allow users to adjust the refresh frequency based on their usage patterns.

Suggested change
private static readonly TimeSpan FirefoxPollingInterval = TimeSpan.FromHours(3);
private TimeSpan FirefoxPollingInterval => TimeSpan.FromHours(_settings.FirefoxPollingIntervalHours);

Copilot uses AI. Check for mistakes.

lock (_lock)
{
if (_disposed) return;
_debounceTimer?.Change(2000, Timeout.Infinite);
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The debounce delay of 2000ms is hardcoded. Consider making this configurable or define it as a named constant to improve maintainability and allow for future adjustments.

Copilot uses AI. Check for mistakes.

{
private readonly HttpClient _httpClient;
private readonly PluginInitContext _context;
private const int MaxFaviconBytes = 250 * 1024;
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider making the MaxFaviconBytes limit configurable through settings. Some high-quality favicons may exceed 250KB, and users might want to adjust this limit based on their preferences and bandwidth considerations.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (2)

50-51: Open with broader FileShare to read while the browser holds the file.

Chromium may keep Bookmarks open/locked. Use ReadWrite|Delete sharing to reduce IO errors and retries.

-                await using var stream = File.OpenRead(bookmarkPath);
+                await using var stream = new FileStream(
+                    bookmarkPath, FileMode.Open, FileAccess.Read,
+                    FileShare.ReadWrite | FileShare.Delete);

47-57: Stream results directly; drop the intermediate List allocation.

Yield directly from the JSON traversal to lower memory and start producing results earlier.

-            var bookmarks = new List<Bookmark>();
             try
             {
                 await using var stream = File.OpenRead(bookmarkPath);
                 using var jsonDocument = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
 
                 if (jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
                 {
-                    bookmarks.AddRange(EnumerateBookmarks(rootElement, source, profilePath));
+                    foreach (var b in EnumerateBookmarks(rootElement, source, profilePath))
+                        yield return b;
                 }
             }
@@
-            foreach (var bookmark in bookmarks)
-            {
-                yield return bookmark;
-            }

And convert the helpers to iterators:

-    private IEnumerable<Bookmark> EnumerateBookmarks(JsonElement rootElement, string source, string profilePath)
-    {
-        var bookmarks = new List<Bookmark>();
-        foreach (var folder in rootElement.EnumerateObject())
-        {
-            if (folder.Value.ValueKind != JsonValueKind.Object)
-                continue;
-
-            // Fix for Opera. It stores bookmarks slightly different than chrome.
-            if (folder.Name == "custom_root")
-                bookmarks.AddRange(EnumerateBookmarks(folder.Value, source, profilePath));
-            else
-                EnumerateFolderBookmark(folder.Value, bookmarks, source, profilePath);
-        }
-        return bookmarks;
-    }
+    private IEnumerable<Bookmark> EnumerateBookmarks(JsonElement rootElement, string source, string profilePath)
+    {
+        foreach (var folder in rootElement.EnumerateObject())
+        {
+            if (folder.Value.ValueKind != JsonValueKind.Object)
+                continue;
+
+            if (folder.Name == "custom_root")
+            {
+                foreach (var b in EnumerateBookmarks(folder.Value, source, profilePath))
+                    yield return b;
+            }
+            else
+            {
+                foreach (var b in EnumerateFolderBookmark(folder.Value, source, profilePath))
+                    yield return b;
+            }
+        }
+    }
@@
-    private void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks, string source, string profilePath)
+    private IEnumerable<Bookmark> EnumerateFolderBookmark(JsonElement folderElement, string source, string profilePath)
     {
         if (!folderElement.TryGetProperty("children", out var childrenElement))
-            return;
+            yield break;
 
         foreach (var subElement in childrenElement.EnumerateArray())
         {
-            if (subElement.TryGetProperty("type", out var type))
+            if (subElement.TryGetProperty("type", out var type))
             {
                 switch (type.GetString())
                 {
                     case "folder":
                     case "workspace": // Edge Workspace
-                        EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
+                        foreach (var b in EnumerateFolderBookmark(subElement, source, profilePath))
+                            yield return b;
                         break;
                     case "url":
                         if (subElement.TryGetProperty("name", out var name) &&
                             subElement.TryGetProperty("url", out var url) &&
                             !string.IsNullOrEmpty(name.GetString()) &&
                             !string.IsNullOrEmpty(url.GetString()))
                         {
-                            bookmarks.Add(new Bookmark(name.GetString()!, url.GetString()!, source, profilePath));
+                            yield return new Bookmark(name.GetString()!, url.GetString()!, source, profilePath);
                         }
                         break;
                 }
             }
             else
             {
                 _logException(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.", null);
             }
         }
     }

Note: If you also apply the ValueKind guards above, mirror them here.

Also applies to: 75-79, 82-97, 99-130

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)

16-17: Guard directory enumeration and use case-insensitive Distinct on Windows.

Avoid rare UnauthorizedAccess/IO issues and normalize duplicates on case-insensitive filesystems.

-        var profileDirs = Directory.EnumerateDirectories(basePath, "Profile *").ToList();
+        List<string> profileDirs;
+        try
+        {
+            profileDirs = Directory.EnumerateDirectories(basePath, "Profile *").ToList();
+        }
+        catch
+        {
+            // Fall back to base path only.
+            return new[] { basePath };
+        }
@@
-        return profileDirs.Distinct();
+        return profileDirs.Distinct(StringComparer.OrdinalIgnoreCase);

Also applies to: 26-26

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (2)

24-31: Detach handlers before disposing old watchers.

Avoid lingering delegates and reduce GC pressure.

-        foreach (var watcher in _watchers)
-        {
-            watcher.EnableRaisingEvents = false;
-            watcher.Dispose();
-        }
+        foreach (var watcher in _watchers)
+        {
+            watcher.EnableRaisingEvents = false;
+            watcher.Changed -= OnFileChanged;
+            watcher.Created -= OnFileChanged;
+            watcher.Deleted -= OnFileChanged;
+            watcher.Renamed -= OnFileRenamed;
+            watcher.Dispose();
+        }

22-53: Synchronize mutations of _watchers.

Dispose/update can race if called from different threads during shutdown. Use a dedicated lock.

     private readonly object _lock = new();
+    private readonly object _watchersLock = new();
@@
-    public void UpdateWatchers(IEnumerable<string> filePaths)
+    public void UpdateWatchers(IEnumerable<string> filePaths)
     {
-        // Dispose old watchers
-        foreach (var watcher in _watchers)
-        {
-            watcher.EnableRaisingEvents = false;
-            watcher.Dispose();
-        }
-        _watchers.Clear();
+        lock (_watchersLock)
+        {
+            // Dispose old watchers
+            foreach (var watcher in _watchers)
+            {
+                watcher.EnableRaisingEvents = false;
+                watcher.Changed -= OnFileChanged;
+                watcher.Created -= OnFileChanged;
+                watcher.Deleted -= OnFileChanged;
+                watcher.Renamed -= OnFileRenamed;
+                watcher.Dispose();
+            }
+            _watchers.Clear();
+        }
@@
-            _watchers.Add(watcher);
+            lock (_watchersLock)
+            {
+                _watchers.Add(watcher);
+            }
         }
     }
@@
-        foreach (var watcher in _watchers)
-        {
-            watcher.EnableRaisingEvents = false;
-            watcher.Changed -= OnFileChanged;
-            watcher.Created -= OnFileChanged;
-            watcher.Deleted -= OnFileChanged;
-            watcher.Renamed -= OnFileRenamed;
-            watcher.Dispose();
-        }
+        lock (_watchersLock)
+        {
+            foreach (var watcher in _watchers)
+            {
+                watcher.EnableRaisingEvents = false;
+                watcher.Changed -= OnFileChanged;
+                watcher.Created -= OnFileChanged;
+                watcher.Deleted -= OnFileChanged;
+                watcher.Renamed -= OnFileRenamed;
+                watcher.Dispose();
+            }
+            _watchers.Clear();
+        }

Also applies to: 76-93

Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1)

41-52: Unsubscribe from EditableBrowser.PropertyChanged on close.

Avoid retaining the VM via the event if a consumer mistakenly holds the dialog open or reuses the VM.

     public CustomBrowserSettingViewModel(CustomBrowser browser, Action<bool> closeAction)
     {
@@
         EditableBrowser.PropertyChanged += EditableBrowser_PropertyChanged;
         DetectEngineType();
     }
@@
     private void Save()
     {
         _originalBrowser.Name = EditableBrowser.Name;
         _originalBrowser.DataDirectoryPath = EditableBrowser.DataDirectoryPath;
         _originalBrowser.BrowserType = EditableBrowser.BrowserType;
+        EditableBrowser.PropertyChanged -= EditableBrowser_PropertyChanged;
         _closeAction(true);
     }
@@
     private void Cancel()
     {
+        EditableBrowser.PropertyChanged -= EditableBrowser_PropertyChanged;
         _closeAction(false);
     }

Also applies to: 70-77, 79-83

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (3)

22-31: Filter to bookmark rows only (type = 1).

Prevents folders/separators/queries slipping through and reduces post‑processing.

         INNER JOIN moz_bookmarks
             ON moz_bookmarks.fk = moz_places.id
-        WHERE moz_bookmarks.fk IS NOT NULL
+        WHERE moz_bookmarks.fk IS NOT NULL
           AND moz_bookmarks.title IS NOT NULL
           AND moz_places.url IS NOT NULL
+          AND moz_bookmarks.type = 1
         ORDER BY moz_places.visit_count DESC

41-94: Consider streaming results to reduce memory and latency.

Yield as you read to avoid building an intermediate list, keeping the fallback path intact by restricting the first attempt to a local iterator.

I can provide a minimal refactor that preserves the fallback semantics while yielding incrementally.


96-117: Bookmark equality excludes Source/ProfilePath
The Bookmark record overrides Equals/GetHashCode to use only Name and Url, so .Distinct() will collapse entries from different browsers or profiles. If you need to treat bookmarks from separate Source (or ProfilePath) as distinct, include those fields in the equality implementation or supply a dedicated IEqualityComparer.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (2)

56-57: Use class name as log tag for consistency.

Aligns with other services.

-            _context.API.LogDebug(nameof(Parse), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");
+            _context.API.LogDebug(nameof(HtmlFaviconParser), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");
@@
-                _context.API.LogDebug(nameof(Parse), $"Found suitable favicon candidate (score: {score}). Halting further HTML parsing.");
+                _context.API.LogDebug(nameof(HtmlFaviconParser), $"Found suitable favicon candidate (score: {score}). Halting further HTML parsing.");

Also applies to: 74-75


87-98: Parse all sizes and pick the largest, not the first.

Rel values often include multiple sizes; choosing the max improves icon quality.

-        var sizesMatch = SizesAttributeRegex().Match(linkTag);
-        if (sizesMatch.Success)
-        {
-            var sizesValue = sizesMatch.Groups["v"].Value.ToUpperInvariant();
-            if (sizesValue == "ANY") return 1000;
-
-            var firstSizePart = sizesValue.Split(' ')[0];
-            if (int.TryParse(firstSizePart.Split('X')[0], out var size))
-            {
-                return size;
-            }
-        }
+        var sizesMatch = SizesAttributeRegex().Match(linkTag);
+        if (sizesMatch.Success)
+        {
+            var sizesValue = sizesMatch.Groups["v"].Value.ToUpperInvariant();
+            if (sizesValue == "ANY") return 1000;
+
+            var maxSide = 0;
+            foreach (var token in sizesValue.Split(new[] { ' ', ',', ';' }, StringSplitOptions.RemoveEmptyEntries))
+            {
+                var xIdx = token.IndexOf('X');
+                if (xIdx > 0 && int.TryParse(token.AsSpan(0, xIdx), out var side))
+                    maxSide = Math.Max(maxSide, side);
+            }
+            if (maxSide > 0) return maxSide;
+        }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (3)

41-54: Propagate cancellation after aggregation to avoid partial swaps on shutdown.

Currently each loader swallows OperationCanceledException; add a token check after Task.WhenAll to surface cancellation to callers (so Main can skip watcher updates/favicons).

         await Task.WhenAll(tasks);
+        cancellationToken.ThrowIfCancellationRequested();
 
         return (bookmarks.Distinct().ToList(), discoveredBookmarkFiles.Distinct().ToList());

99-106: Skip creating Firefox loaders when places.sqlite isn’t found.

Avoid yielding loaders that will immediately yield-break by returning null from the factory and filtering at the call site.

-        foreach (var browser in _settings.CustomBrowsers.Where(b => b.BrowserType == BrowserType.Firefox))
+        foreach (var browser in _settings.CustomBrowsers.Where(b => b.BrowserType == BrowserType.Firefox))
         {
             if (string.IsNullOrEmpty(browser.Name) || string.IsNullOrEmpty(browser.DataDirectoryPath) || !Directory.Exists(browser.DataDirectoryPath))
                 continue;
 
-            yield return CreateCustomFirefoxLoader(browser.Name, browser.DataDirectoryPath);
+            var loader = CreateCustomFirefoxLoader(browser.Name, browser.DataDirectoryPath);
+            if (loader != null)
+                yield return loader;
         }
@@
-    private IBookmarkLoader CreateCustomFirefoxLoader(string name, string dataDirectoryPath)
+    private IBookmarkLoader? CreateCustomFirefoxLoader(string name, string dataDirectoryPath)
     {
         var logAction = (string tag, string msg, Exception? ex) => _context.API.LogException(tag, msg, ex);
         // Custom Firefox paths might point to the root profile dir (e.g. ...\Mozilla\Firefox)
         var placesPath = FirefoxProfileFinder.GetPlacesPathFromProfileDir(dataDirectoryPath);
         if (string.IsNullOrEmpty(placesPath))
         {
             // Or they might point directly to a profile folder (e.g. ...\Profiles\xyz.default-release)
             placesPath = Path.Combine(dataDirectoryPath, "places.sqlite");
         }
 
-        // Do not add Firefox places.sqlite to the watcher as it's updated constantly for history.
-        return new FirefoxBookmarkLoader(name, placesPath, _tempPath, logAction);
+        if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
+        {
+            _context.API.LogWarn(nameof(BookmarkLoaderService), $"No places.sqlite under {dataDirectoryPath} for '{name}', skipping.");
+            return null;
+        }
+        // Do not add Firefox places.sqlite to the watcher as it's updated constantly for history.
+        return new FirefoxBookmarkLoader(name, placesPath, _tempPath, logAction);
     }

Also applies to: 152-165


66-97: Optional: Add default detection for other Chromium browsers (Brave/Opera).

Consider adding Brave and Opera default paths to improve OOTB coverage; custom browsers are a fallback.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

113-122: Guard side-effects if cancellation was requested mid‑reload.

Avoid updating watchers and starting favicon work during shutdown by bailing out after load completes.

             var (bookmarks, discoveredFiles) = await _bookmarkLoader.LoadBookmarksAsync(_cancellation_tokenSource.Token);
+            if (_cancellationTokenSource.IsCancellationRequested)
+                return;
 
             // Atomically swap the list. This prevents the Query method from seeing a partially loaded list.
             Volatile.Write(ref _bookmarks, bookmarks);
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

192-201: Micro-opt: Faster hex formatting.

Use Convert.ToHexString to reduce allocations and simplify code.

-        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(url));
-        var sb = new StringBuilder(hash.Length * 2);
-        foreach (byte b in hash)
-        {
-            sb.Append(b.ToString("x2"));
-        }
-        return Path.Combine(cacheDir, sb.ToString() + ".png");
+        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(url));
+        var hex = Convert.ToHexString(hash).ToLowerInvariant();
+        return Path.Combine(cacheDir, hex + ".png");

335-343: Offer: Add optional cache eviction for orphaned favicons.

On full reload, compute live host keys from current bookmarks and delete stale files older than N days from FaviconCache. Happy to draft a small CleanupCacheAsync(bookmarks) for this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fce1f2 and 54b35d1.

📒 Files selected for processing (15)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-06T05:26:32.163Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3664
File: Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs:326-326
Timestamp: 2025-06-06T05:26:32.163Z
Learning: In the Firefox bookmark loader (FirefoxBookmarkLoader.cs), using hardcoded Windows line endings ("\r\n") for splitting profiles.ini content is acceptable and platform-agnostic line splitting is not required.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
📚 Learning: 2024-11-03T07:34:24.926Z
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:34:24.926Z
Learning: In Windows Forms dialogs, Windows handles invalid paths and prevents the user from clicking "Ok" if the path is incorrect, so additional path validation is unnecessary.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs
📚 Learning: 2024-11-03T07:40:11.014Z
Learnt from: Yusyuriv
PR: Flow-Launcher/Flow.Launcher#3057
File: Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs:0-0
Timestamp: 2024-11-03T07:40:11.014Z
Learning: In Flow Launcher, when using Windows Forms dialogs (e.g., in `JsonRPCPluginSettings.cs`), path validation is enabled by default in `OpenFileDialog` and `FolderBrowserDialog`, preventing users from selecting invalid paths, but it's possible to opt out of this validation on individual dialogs.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs
🧬 Code graph analysis (14)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (3)
  • IEnumerable (56-59)
  • IEnumerable (61-106)
  • IEnumerable (108-150)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (2)
  • FirefoxProfileFinder (9-114)
  • GetPlacesPathFromProfileDir (42-113)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • IBookmarkLoader (152-165)
  • Task (26-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (6)
  • CleanupTempFiles (122-149)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (4)
  • IBookmarkLoader (152-165)
  • IEnumerable (56-59)
  • IEnumerable (61-106)
  • IEnumerable (108-150)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (10-10)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserDetector (9-50)
  • IEnumerable (11-27)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
  • Task (75-96)
  • Task (98-163)
  • Task (165-190)
  • Task (203-281)
  • Task (297-318)
  • Task (320-333)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/CustomBrowserSetting.xaml.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (2)
  • CustomBrowserSettingViewModel (12-108)
  • CustomBrowserSettingViewModel (41-52)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2)
  • OnBookmarkFileChanged (144-147)
  • Dispose (286-295)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1)
  • IBookmarkLoader (152-165)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/CustomBrowser.cs (1)
  • CustomBrowser (5-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserType (29-49)
  • BrowserDetector (9-50)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-150)
  • LocalFaviconExtractor (17-21)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-130)
  • FaviconWebClient (19-31)
  • Task (33-73)
  • Task (75-123)
  • Dispose (125-129)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (3)
  • HtmlFaviconParser (9-104)
  • HtmlFaviconParser (24-27)
  • List (29-80)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (6)
  • ImageConverter (13-129)
  • ImageConverter (18-21)
  • Task (23-44)
  • PngData (46-67)
  • PngData (69-102)
  • PngData (104-128)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (108-127)
  • Task (173-238)
  • List (68-90)
  • List (255-284)
  • Result (92-106)
  • Dispose (286-295)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
  • Task (75-96)
  • Task (98-163)
  • Task (165-190)
  • Task (203-281)
  • Task (297-318)
  • Task (320-333)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (2)
  • Task (96-117)
  • CleanupTempFiles (119-146)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1)
  • Task (23-44)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (7)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (3)
  • BookmarkLoaderService (13-166)
  • BookmarkLoaderService (19-24)
  • Task (26-54)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (9)
  • FaviconService (19-344)
  • FaviconService (36-54)
  • Task (75-96)
  • Task (98-163)
  • Task (165-190)
  • Task (203-281)
  • Task (297-318)
  • Task (320-333)
  • Dispose (335-343)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (96-117)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (23-38)
  • Task (40-49)
  • Task (51-61)
  • Task (63-105)
  • Task (107-120)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (8-67)
  • SettingsControl (13-17)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (4)
  • Task (108-127)
  • Task (173-238)
  • List (68-90)
  • List (255-284)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (3)
  • Task (96-117)
  • FirefoxBookmarkLoader (13-147)
  • FirefoxBookmarkLoader (33-39)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • IEnumerable (11-27)
  • BrowserType (29-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)
  • IEnumerable (82-97)
  • ChromiumBookmarkLoader (13-131)
  • ChromiumBookmarkLoader (22-28)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (4)
  • FirefoxProfileFinder (9-114)
  • GetFirefoxPlacesPath (11-18)
  • GetFirefoxMsixPlacesPath (20-40)
  • GetPlacesPathFromProfileDir (42-113)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs

[warning] 66-66:
shm is not a recognized word. (unrecognized-spelling)


[warning] 65-65:
wal is not a recognized word. (unrecognized-spelling)


[warning] 65-65:
wal is not a recognized word. (unrecognized-spelling)


[warning] 64-64:
wal is not a recognized word. (unrecognized-spelling)


[warning] 61-61:
shm is not a recognized word. (unrecognized-spelling)


[warning] 60-60:
wal is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 173-173:
Cooldown is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs

[warning] 139-139:
msix is not a recognized word. (unrecognized-spelling)


[warning] 137-137:
msix is not a recognized word. (unrecognized-spelling)


[warning] 131-131:
msix is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (14)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)

58-69: Good: IO and access errors are now handled.

Adding IOException/UnauthorizedAccessException (plus generic) resolves the earlier gap when opening/reading the Bookmarks file.


125-128: Good: removed unsafe GetString() usage in the no-type branch.

The log message no longer calls GetString() on a non-string JsonElement.


44-45: No action needed: discovered files are deduplicated downstream
The BookmarkLoaderService.LoadBookmarksAsync call applies .Distinct().ToList() to discoveredBookmarkFiles before returning them, so duplicates cannot propagate into the watcher service.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)

7-11: Interface looks right for async, cancellable streaming.

Contract is minimal and sufficient.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)

29-49: LGTM overall.

Browser type detection logic is clear and conservative.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (2)

14-90: Property‑changed pattern looks good.

Backed fields with change guards and OnPropertyChanged across flags are consistent.


92-105: net9.0-windows defaults to C# 12+, so [] collection expressions are supported
All projects target net9.0-windows without an explicit <LangVersion>, and the default C# compiler version (≥ 12) includes collection expressions. No changes required.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/ViewModels/CustomBrowserSettingViewModel.cs (1)

88-107: Dispose FolderBrowserDialog to release native handles.

Duplicate of earlier feedback; use a using statement.

-        var dialog = new System.Windows.Forms.FolderBrowserDialog
-        {
-            Description = "Select the browser's profile data directory.",
-            UseDescriptionForTitle = true
-        };
+        using var dialog = new System.Windows.Forms.FolderBrowserDialog
+        {
+            Description = "Select the browser's profile data directory.",
+            UseDescriptionForTitle = true
+        };

Based on learnings.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2)

71-72: LGTM: Volatile.Read used for safe publication in Query.

This pairs correctly with Volatile.Write in reload paths.


156-170: LGTM: PeriodicTimer loop hardened against disposal races.

Local snapshot + catching ObjectDisposedException/OperationCanceledException is correct.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (3)

104-161: LGTM: Host-level caching + atomic temp-to-final writes.

Good dedup by authority, safe concurrent writes via temp file + move, and early local DB path before web fetch.


50-53: Security: Ensure HttpClient validates TLS certs.

FaviconWebClient previously disabled cert validation. Confirm it now uses default validation and sane timeouts; otherwise revert the override.


165-190: Task.WaitAsync support confirmed: all projects target net9.0-windows, so Task.WaitAsync is available—no changes required.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1)

53-54: Bookmark equality confirmed Bookmark is a record that overrides Equals(Bookmark?) and GetHashCode() to compare Name and Url, so Distinct() will correctly dedupe on those fields.

Comment on lines +219 to +229
var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
var successfulLoaderNames = successfulResults.Select(r => r.Loader.Name).ToHashSet();

public void ReloadData()
{
ReloadAllBookmarks();
var currentBookmarks = Volatile.Read(ref _bookmarks);

var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Any(name => b.Source.StartsWith(name, StringComparison.OrdinalIgnoreCase)));

var newBookmarkList = otherBookmarks.Concat(newFirefoxBookmarks).Distinct().ToList();

Volatile.Write(ref _bookmarks, newBookmarkList);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix: Don’t use StartsWith when removing old Firefox bookmarks.

Using StartsWith can wrongly drop “Firefox (Store)” when only “Firefox” was reloaded. Normalize source to its base name and compare by set membership.

-            var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
-            var successfulLoaderNames = successfulResults.Select(r => r.Loader.Name).ToHashSet();
+            var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
+            var successfulLoaderNames = successfulResults.Select(r => r.Loader.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
@@
-            var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Any(name => b.Source.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
+            var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Contains(GetBaseSource(b.Source)));
@@
             var newBookmarkList = otherBookmarks.Concat(newFirefoxBookmarks).Distinct().ToList();

Add helper:

@@
     }
 
+    private static string GetBaseSource(string source)
+    {
+        var idx = source.IndexOf(" (", StringComparison.Ordinal);
+        return idx >= 0 ? source.Substring(0, idx) : source;
+    }
+
     private async Task ReloadFirefoxBookmarksAsync()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
var successfulLoaderNames = successfulResults.Select(r => r.Loader.Name).ToHashSet();
public void ReloadData()
{
ReloadAllBookmarks();
var currentBookmarks = Volatile.Read(ref _bookmarks);
var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Any(name => b.Source.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
var newBookmarkList = otherBookmarks.Concat(newFirefoxBookmarks).Distinct().ToList();
Volatile.Write(ref _bookmarks, newBookmarkList);
var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
var successfulLoaderNames = successfulResults
.Select(r => r.Loader.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var currentBookmarks = Volatile.Read(ref _bookmarks);
var otherBookmarks = currentBookmarks
.Where(b => !successfulLoaderNames.Contains(GetBaseSource(b.Source)));
var newBookmarkList = otherBookmarks
.Concat(newFirefoxBookmarks)
.Distinct()
.ToList();
Volatile.Write(ref _bookmarks, newBookmarkList);
Suggested change
var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
var successfulLoaderNames = successfulResults.Select(r => r.Loader.Name).ToHashSet();
public void ReloadData()
{
ReloadAllBookmarks();
var currentBookmarks = Volatile.Read(ref _bookmarks);
var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Any(name => b.Source.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
var newBookmarkList = otherBookmarks.Concat(newFirefoxBookmarks).Distinct().ToList();
Volatile.Write(ref _bookmarks, newBookmarkList);
private static string GetBaseSource(string source)
{
var idx = source.IndexOf(" (", StringComparison.Ordinal);
return idx >= 0
? source.Substring(0, idx)
: source;
}
🤖 Prompt for AI Agents
In Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs around lines 219 to 229,
avoid using StartsWith to filter out old Firefox bookmarks because it can
incorrectly remove sources like "Firefox (Store)"; instead normalize each
Loader.Name to its base name, build a HashSet of those base names, and filter
currentBookmarks by checking whether the base name of b.Source (normalized the
same way) is contained in that set; add a small helper method to extract the
base name from a source string (e.g., trim and drop parenthetical suffixes) and
use it for both creating successfulLoaderNames and comparing b.Source so
comparisons are exact set membership rather than prefix matching.

Comment on lines +17 to +21
public BookmarkWatcherService()
{
_debounceTimer = new Timer(_ => OnBookmarkFileChanged?.Invoke(), null, Timeout.Infinite, Timeout.Infinite);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard timer callback; unhandled exceptions can tear down the process.

Wrap the timer callback in try/catch and no-op when disposed. Invoke subscribers outside the lock.

Apply:

-    public BookmarkWatcherService()
-    {
-        _debounceTimer = new Timer(_ => OnBookmarkFileChanged?.Invoke(), null, Timeout.Infinite, Timeout.Infinite);
-    }
+    public BookmarkWatcherService()
+    {
+        _debounceTimer = new Timer(DebounceCallback, null, Timeout.Infinite, Timeout.Infinite);
+    }
+
+    private void DebounceCallback(object? _)
+    {
+        Action? handler;
+        lock (_lock)
+        {
+            if (_disposed) return;
+            handler = OnBookmarkFileChanged;
+        }
+        try { handler?.Invoke(); }
+        catch (Exception ex)
+        {
+            // Swallow to prevent process crash; route through Flow's logger if available.
+            System.Diagnostics.Debug.WriteLine(ex);
+        }
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public BookmarkWatcherService()
{
_debounceTimer = new Timer(_ => OnBookmarkFileChanged?.Invoke(), null, Timeout.Infinite, Timeout.Infinite);
}
public BookmarkWatcherService()
{
_debounceTimer = new Timer(DebounceCallback, null, Timeout.Infinite, Timeout.Infinite);
}
private void DebounceCallback(object? _)
{
Action? handler;
lock (_lock)
{
if (_disposed) return;
handler = OnBookmarkFileChanged;
}
try
{
handler?.Invoke();
}
catch (Exception ex)
{
// Swallow to prevent process crash; route through Flow's logger if available.
System.Diagnostics.Debug.WriteLine(ex);
}
}
🤖 Prompt for AI Agents
In
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs
around lines 17-21, the Timer callback is unguarded: wrap the callback body in a
try/catch to swallow/log exceptions so they cannot bubble out and crash the
process, check a disposed/cancelled flag (or Timer state) at the start and
return no-op if the service is disposed, and ensure OnBookmarkFileChanged
subscribers are invoked outside any locks (capture the delegate into a local
variable then invoke it after releasing locks) to avoid deadlocks and exceptions
propagating from subscriber code.

Comment on lines 106 to 128
if (subElement.TryGetProperty("type", out var type))
{
switch (type.GetString())
{
case "folder":
case "workspace": // Edge Workspace
EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
break;
case "url":
if (subElement.TryGetProperty("name", out var name) &&
subElement.TryGetProperty("url", out var url) &&
!string.IsNullOrEmpty(name.GetString()) &&
!string.IsNullOrEmpty(url.GetString()))
{
bookmarks.Add(new Bookmark(name.GetString()!, url.GetString()!, source, profilePath));
}
break;
}
}
else
{
_logException(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.", null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden JSON parsing: guard GetString() with ValueKind checks to avoid InvalidOperationException on malformed nodes.

type.GetString(), name.GetString(), and url.GetString() can throw when the JSON value isn’t a string (corrupt file, extensions, experiments). This will bubble into the catch block and drop the entire profile’s bookmarks. Guard the ValueKind and log once per offending node.

Apply this diff:

-            if (subElement.TryGetProperty("type", out var type))
+            if (subElement.TryGetProperty("type", out var typeElem))
             {
-                switch (type.GetString())
+                var typeStr = typeElem.ValueKind == JsonValueKind.String ? typeElem.GetString() : null;
+                if (string.IsNullOrEmpty(typeStr))
+                {
+                    _logException(nameof(ChromiumBookmarkLoader), "type property is not a string in bookmark node.", null);
+                    continue;
+                }
+                switch (typeStr)
                 {
                     case "folder":
                     case "workspace": // Edge Workspace
                         EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
                         break;
                     case "url":
-                        if (subElement.TryGetProperty("name", out var name) &&
-                            subElement.TryGetProperty("url", out var url) &&
-                            !string.IsNullOrEmpty(name.GetString()) &&
-                            !string.IsNullOrEmpty(url.GetString()))
+                        var hasName = subElement.TryGetProperty("name", out var nameElem);
+                        var hasUrl = subElement.TryGetProperty("url", out var urlElem);
+                        var nameStr = hasName && nameElem.ValueKind == JsonValueKind.String ? nameElem.GetString() : null;
+                        var urlStr = hasUrl && urlElem.ValueKind == JsonValueKind.String ? urlElem.GetString() : null;
+                        if (!string.IsNullOrEmpty(nameStr) && !string.IsNullOrEmpty(urlStr))
                         {
-                            bookmarks.Add(new Bookmark(name.GetString()!, url.GetString()!, source, profilePath));
+                            bookmarks.Add(new Bookmark(nameStr!, urlStr!, source, profilePath));
                         }
                         break;
                 }
             }
             else
             {
                 _logException(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.", null);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (subElement.TryGetProperty("type", out var type))
{
switch (type.GetString())
{
case "folder":
case "workspace": // Edge Workspace
EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
break;
case "url":
if (subElement.TryGetProperty("name", out var name) &&
subElement.TryGetProperty("url", out var url) &&
!string.IsNullOrEmpty(name.GetString()) &&
!string.IsNullOrEmpty(url.GetString()))
{
bookmarks.Add(new Bookmark(name.GetString()!, url.GetString()!, source, profilePath));
}
break;
}
}
else
{
_logException(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.", null);
}
if (subElement.TryGetProperty("type", out var typeElem))
{
// Safely extract the 'type' as string, or log & skip if it's not a string
var typeStr = typeElem.ValueKind == JsonValueKind.String ? typeElem.GetString() : null;
if (string.IsNullOrEmpty(typeStr))
{
_logException(
nameof(ChromiumBookmarkLoader),
"type property is not a string in bookmark node.",
null
);
continue;
}
switch (typeStr)
{
case "folder":
case "workspace": // Edge Workspace
EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
break;
case "url":
// Safely extract 'name' and 'url' as strings
var hasName = subElement.TryGetProperty("name", out var nameElem);
var hasUrl = subElement.TryGetProperty("url", out var urlElem);
var nameStr = hasName && nameElem.ValueKind == JsonValueKind.String
? nameElem.GetString() : null;
var urlStr = hasUrl && urlElem.ValueKind == JsonValueKind.String
? urlElem.GetString() : null;
if (!string.IsNullOrEmpty(nameStr) && !string.IsNullOrEmpty(urlStr))
{
bookmarks.Add(new Bookmark(
nameStr!,
urlStr!,
source,
profilePath
));
}
break;
}
}
else
{
_logException(
nameof(ChromiumBookmarkLoader),
"type property not found in bookmark node.",
null
);
}
🤖 Prompt for AI Agents
In
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs
around lines 106 to 128, the code calls GetString() on JSON properties without
checking ValueKind which can throw for non-string nodes; update the switch and
bookmark-add logic to first verify property.ValueKind == JsonValueKind.String
for type, name and url before calling GetString(), skip and call _logException
once for that offending node when any required property is missing or not a
string, and ensure you continue processing other nodes instead of throwing.

Comment on lines +63 to +69
private async Task<byte[]?> GetFaviconFromDbAsync(Bookmark bookmark, string dbFileName, string query,
Func<byte[], CancellationToken, Task<byte[]>>? postProcessor, CancellationToken token)
{
var dbPath = Path.Combine(bookmark.ProfilePath, dbFileName);
if (!File.Exists(dbPath))
return null;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against missing profile paths before hitting the DB

When bookmark.ProfilePath is null or empty (e.g., legacy entries or loaders that haven't populated it yet), this call to Path.Combine throws ArgumentNullException, crashing the favicon pass for every bookmark sourced from that browser. We should bail out early when the profile root is unavailable instead of attempting a DB read.

Apply this diff to harden the method:

-        var dbPath = Path.Combine(bookmark.ProfilePath, dbFileName);
+        var profilePath = bookmark.ProfilePath;
+        if (string.IsNullOrWhiteSpace(profilePath) || !Directory.Exists(profilePath))
+            return null;
+
+        var dbPath = Path.Combine(profilePath, dbFileName);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async Task<byte[]?> GetFaviconFromDbAsync(Bookmark bookmark, string dbFileName, string query,
Func<byte[], CancellationToken, Task<byte[]>>? postProcessor, CancellationToken token)
{
var dbPath = Path.Combine(bookmark.ProfilePath, dbFileName);
if (!File.Exists(dbPath))
return null;
private async Task<byte[]?> GetFaviconFromDbAsync(Bookmark bookmark, string dbFileName, string query,
Func<byte[], CancellationToken, Task<byte[]>>? postProcessor, CancellationToken token)
{
var profilePath = bookmark.ProfilePath;
if (string.IsNullOrWhiteSpace(profilePath) || !Directory.Exists(profilePath))
return null;
var dbPath = Path.Combine(profilePath, dbFileName);
if (!File.Exists(dbPath))
return null;
// …rest of the method…
}
🤖 Prompt for AI Agents
In
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs
around lines 63 to 69, Path.Combine is called with bookmark.ProfilePath which
can be null/empty and will throw ArgumentNullException; guard by checking if
string.IsNullOrWhiteSpace(bookmark?.ProfilePath) (or bookmark.ProfilePath ==
null) at method start and return null immediately before calling Path.Combine,
so we bail out early for missing profile roots and avoid attempting a DB read.

@Jack251970 Jack251970 requested a review from Copilot September 28, 2025 10:43
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

[RelayCommand]
private void BrowseDataDirectory()
{
var dialog = new System.Windows.Forms.FolderBrowserDialog
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Windows Forms dialogs in a WPF application is not recommended. Consider using a WPF-native folder picker or the newer Windows API Code Pack FolderBrowserDialog for better consistency with the application's UI framework.

Copilot uses AI. Check for mistakes.

AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli
};
_httpClient = new HttpClient(handler);
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6409.0 Safari/537.36");
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The hardcoded User-Agent string may become outdated and could be flagged by some servers. Consider making this configurable or use a more generic, current User-Agent string.

Copilot uses AI. Check for mistakes.

Comment on lines +132 to +134
catch (IOException)
{
// Another thread may have created it concurrently.
Copy link
Preview

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The catch block silently ignores IOException without logging. Consider adding debug logging to help troubleshoot concurrent file operations.

Suggested change
catch (IOException)
{
// Another thread may have created it concurrently.
catch (IOException ex)
{
// Another thread may have created it concurrently.
System.Diagnostics.Debug.WriteLine($"IOException during File.Move: {ex.Message}\n{ex.StackTrace}");

Copilot uses AI. Check for mistakes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (15)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)

26-26: Use case-insensitive distinct for Windows paths.

Avoid duplicate paths caused by casing differences.

-        return profileDirs.Distinct();
+        return profileDirs.Distinct(System.StringComparer.OrdinalIgnoreCase);

16-16: Harden directory enumeration against IO/ACL exceptions.

Enumerating subfolders can throw; ignore and proceed.

-        var profileDirs = Directory.EnumerateDirectories(basePath, "Profile *").ToList();
+        var profileDirs = new List<string>();
+        try
+        {
+            profileDirs.AddRange(Directory.EnumerateDirectories(basePath, "Profile *"));
+        }
+        catch (System.UnauthorizedAccessException) { /* ignore */ }
+        catch (IOException) { /* ignore */ }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1)

45-76: Stream bookmarks instead of buffering per profile.

Avoids a temporary list and reduces peak memory.

-            var bookmarks = new List<Bookmark>();
             try
             {
                 await using var stream = File.OpenRead(bookmarkPath);
                 using var jsonDocument = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
 
                 if (jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
                 {
-                    bookmarks.AddRange(EnumerateBookmarks(rootElement, source, profilePath));
+                    foreach (var b in EnumerateBookmarks(rootElement, source, profilePath))
+                    {
+                        yield return b;
+                    }
                 }
             }
             catch (IOException ex)
             {
                 Main.Context.API.LogException(nameof(ChromiumBookmarkLoader), $"IO error reading {_browserName} bookmarks: {bookmarkPath}", ex);
             }
             catch (UnauthorizedAccessException ex)
             {
                 Main.Context.API.LogException(nameof(ChromiumBookmarkLoader), $"Unauthorized to read {_browserName} bookmarks: {bookmarkPath}", ex);
             }
             catch (JsonException ex)
             {
                 Main.Context.API.LogException(nameof(ChromiumBookmarkLoader), $"Failed to parse bookmarks file for {_browserName}: {bookmarkPath}", ex);
             }
             catch (Exception ex)
             {
                 Main.Context.API.LogException(nameof(ChromiumBookmarkLoader), $"Unexpected error loading {_browserName} bookmarks: {bookmarkPath}", ex);
             }
-
-            foreach (var bookmark in bookmarks)
-            {
-                yield return bookmark;
-            }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)

54-56: Avoid O(n^2) scans of the growing StringBuilder.

Check only the tail for "" after each append.

-                if (contentBuilder.ToString().Contains("</head>", StringComparison.OrdinalIgnoreCase))
+                var tailLen = Math.Min(contentBuilder.Length, 2048);
+                var start = contentBuilder.Length - tailLen;
+                if (tailLen > 0 && contentBuilder.ToString(start, tailLen).IndexOf("</head>", StringComparison.OrdinalIgnoreCase) >= 0)
                 {
                     break;
                 }

73-79: Ignore non-HTTP(S) favicon URIs early.

Prevents unsupported-scheme requests.

     public async Task<MemoryStream?> DownloadFaviconAsync(Uri faviconUri, CancellationToken token)
     {
         try
         {
+            if (faviconUri.Scheme != Uri.UriSchemeHttp && faviconUri.Scheme != Uri.UriSchemeHttps)
+                return null;

86-88: Pre-allocate MemoryStream when Content-Length is known.

Reduces reallocations on larger icons.

-            await using var contentStream = await response.Content.ReadAsStreamAsync(token);
-            var memoryStream = new MemoryStream();
+            await using var contentStream = await response.Content.ReadAsStreamAsync(token);
+            var capacity = response.Content.Headers.ContentLength is long len ? (int)Math.Min(len, MaxFaviconBytes) : 0;
+            var memoryStream = capacity > 0 ? new MemoryStream(capacity) : new MemoryStream();

25-29: Prefer HTTP/2+ where available.

Small perf gains and better TLS/HPACK behavior.

         _httpClient = new HttpClient(handler);
+        _httpClient.DefaultRequestVersion = System.Net.HttpVersion.Version20;
+        _httpClient.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;

13-128: Favicon cache eviction/cleanup (operational).

Introduce size/time-based eviction and orphan cleanup (remove cache entries for URLs no longer in bookmarks). Hook into periodic maintenance or on successful reload. I can draft a lightweight job and wire it to Settings.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)

63-96: Consider decoding ICO directly to target size to avoid double transcode.

You transcode ICO→PNG with WPF, then decode back with Skia to resize and encode again. If staying with Skia, you could decode the largest ICO frame via IconBitmapDecoder and resize it using TransformedBitmap before PNG encode, or decode directly with Skia when feasible. This reduces an extra decode/encode cycle and allocations. Not blocking.


98-122: Early bounds check before full bitmap decode (perf/robustness).

For very large rasters, SKBitmap.Decode(stream) will load the entire image. Consider using SKCodec to read bounds first and bail out (or downscale) when dimensions are extreme (e.g., > 2048px), aligning with favicon expectations.

Would you like a small helper using SKCodec to probe dimensions and enforce a max width/height before decoding?

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1)

34-35: Redundant cancellation wiring on async enumeration.

You already pass cancellationToken to GetBookmarksAsync. Adding .WithCancellation(cancellationToken) is redundant and can be removed for clarity.

-                await foreach (var bookmark in loader.GetBookmarksAsync(cancellationToken).WithCancellation(cancellationToken))
+                await foreach (var bookmark in loader.GetBookmarksAsync(cancellationToken))
                 {
                     bookmarks.Add(bookmark);
                 }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1)

51-54: Be explicit about string comparison for protocol‑relative URLs.

Use StartsWith("//", StringComparison.Ordinal) for clarity and micro‑perf.

-            if (href.StartsWith("//"))
+            if (href.StartsWith("//", StringComparison.Ordinal))
             {
                 href = effectiveBaseUri.Scheme + ":" + href;
             }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (1)

28-29: Make Firefox polling interval configurable.

Hardcoding 3 hours may not fit all users; consider a setting or Environment.ProcessorCount‑aware default.

-    private static readonly TimeSpan FirefoxPollingInterval = TimeSpan.FromHours(3);
+    private static readonly TimeSpan FirefoxPollingInterval = TimeSpan.FromHours(3); // TODO: read from Settings (e.g., Settings.FirefoxPollingIntervalHours)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (2)

100-103: Parallelism: scale to hardware or settings.

Use CPU‑aware or setting‑driven MaxDegreeOfParallelism.

-        var options = new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = cancellationToken };
+        var degree = Math.Max(1, Math.Min(Environment.ProcessorCount, 8)); // or from Settings
+        var options = new ParallelOptions { MaxDegreeOfParallelism = degree, CancellationToken = cancellationToken };

39-46: Plan cache eviction/cleanup to prevent unbounded growth.

Consider a periodic task to prune:

  • files older than N days without matching bookmarks,
  • keep‑under size cap with LRU,
  • remove entries for domains in FailedFetches beyond cooldown.

Happy to sketch an implementation if useful.

Also applies to: 201-279

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54b35d1 and 3ef2c4e.

📒 Files selected for processing (12)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-06T05:26:32.163Z
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3664
File: Plugins/Flow.Launcher.Plugin.BrowserBookmark/FirefoxBookmarkLoader.cs:326-326
Timestamp: 2025-06-06T05:26:32.163Z
Learning: In the Firefox bookmark loader (FirefoxBookmarkLoader.cs), using hardcoded Windows line endings ("\r\n") for splitting profiles.ini content is acceptable and platform-agnostic line splitting is not required.

Applied to files:

  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
  • Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs
🧬 Code graph analysis (9)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (5)
  • Task (108-127)
  • Task (173-238)
  • List (68-90)
  • List (255-284)
  • Main (16-296)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (3)
  • Task (94-115)
  • FirefoxBookmarkLoader (13-145)
  • FirefoxBookmarkLoader (32-37)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • IEnumerable (11-27)
  • BrowserType (29-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)
  • IEnumerable (80-95)
  • ChromiumBookmarkLoader (13-129)
  • ChromiumBookmarkLoader (21-26)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (4)
  • FirefoxProfileFinder (9-114)
  • GetFirefoxPlacesPath (11-18)
  • GetFirefoxMsixPlacesPath (20-40)
  • GetPlacesPathFromProfileDir (42-113)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (7)
  • LocalFaviconExtractor (12-148)
  • LocalFaviconExtractor (16-19)
  • Task (21-36)
  • Task (38-47)
  • Task (49-59)
  • Task (61-103)
  • Task (105-118)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (5)
  • FaviconWebClient (13-128)
  • FaviconWebClient (18-29)
  • Task (31-71)
  • Task (73-121)
  • Dispose (123-127)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (2)
  • HtmlFaviconParser (9-97)
  • List (22-73)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (5)
  • ImageConverter (13-123)
  • Task (17-38)
  • PngData (40-61)
  • PngData (63-96)
  • PngData (98-122)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (6)
  • Task (108-127)
  • Task (173-238)
  • Main (16-296)
  • List (68-90)
  • List (255-284)
  • Dispose (286-295)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (3)
  • List (68-90)
  • List (255-284)
  • Main (16-296)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (1)
  • ImageConverter (13-123)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (4)
  • IBookmarkLoader (147-159)
  • IEnumerable (54-57)
  • IEnumerable (59-103)
  • IEnumerable (105-145)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (11-11)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (2)
  • BrowserDetector (9-50)
  • IEnumerable (11-27)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (4)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)
  • IBookmarkLoader (147-159)
  • Task (24-52)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/IBookmarkLoader.cs (1)
  • IAsyncEnumerable (11-11)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (5)
  • List (68-90)
  • List (255-284)
  • Main (16-296)
  • Task (108-127)
  • Task (173-238)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (6)
  • CleanupTempFiles (120-147)
  • Task (21-36)
  • Task (38-47)
  • Task (49-59)
  • Task (61-103)
  • Task (105-118)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxProfileFinder.cs (2)
  • FirefoxProfileFinder (9-114)
  • GetPlacesPathFromProfileDir (42-113)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (7)
  • Task (73-94)
  • Task (96-161)
  • Task (163-188)
  • Task (201-279)
  • Task (295-316)
  • Task (318-331)
  • Dispose (333-341)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ImageConverter.cs (2)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (6)
  • Task (73-94)
  • Task (96-161)
  • Task (163-188)
  • Task (201-279)
  • Task (295-316)
  • Task (318-331)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (4)
  • Task (21-36)
  • Task (38-47)
  • Task (49-59)
  • Task (61-103)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (8)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Models/Settings.cs (1)
  • Settings (5-106)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (3)
  • BookmarkLoaderService (13-160)
  • BookmarkLoaderService (18-22)
  • Task (24-52)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (9)
  • FaviconService (19-342)
  • FaviconService (35-52)
  • Task (73-94)
  • Task (96-161)
  • Task (163-188)
  • Task (201-279)
  • Task (295-316)
  • Task (318-331)
  • Dispose (333-341)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkWatcherService.cs (4)
  • BookmarkWatcherService (7-94)
  • BookmarkWatcherService (17-20)
  • UpdateWatchers (22-53)
  • Dispose (76-93)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)
  • Task (94-115)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/LocalFaviconExtractor.cs (5)
  • Task (21-36)
  • Task (38-47)
  • Task (49-59)
  • Task (61-103)
  • Task (105-118)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs (1)
  • BrowserType (29-49)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Views/SettingsControl.xaml.cs (2)
  • SettingsControl (8-67)
  • SettingsControl (13-17)
🪛 GitHub Check: Check Spelling
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs

[warning] 134-134:
msix is not a recognized word. (unrecognized-spelling)


[warning] 132-132:
msix is not a recognized word. (unrecognized-spelling)


[warning] 126-126:
msix is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs

[warning] 171-171:
Cooldown is not a recognized word. (unrecognized-spelling)

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs

[warning] 64-64:
shm is not a recognized word. (unrecognized-spelling)


[warning] 63-63:
wal is not a recognized word. (unrecognized-spelling)


[warning] 63-63:
wal is not a recognized word. (unrecognized-spelling)


[warning] 62-62:
wal is not a recognized word. (unrecognized-spelling)


[warning] 59-59:
shm is not a recognized word. (unrecognized-spelling)


[warning] 58-58:
wal is not a recognized word. (unrecognized-spelling)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: gitStream.cm
  • GitHub Check: build
🔇 Additional comments (11)
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs (3)

56-71: Good: IO/security/JSON/unknown exceptions now handled.

This addresses prior review feedback on file-read robustness.


104-121: Harden JSON string extraction (avoid InvalidOperationException on non-strings).

Guard type/name/url before GetString(). This was flagged previously.

-            if (subElement.TryGetProperty("type", out var type))
+            if (subElement.TryGetProperty("type", out var typeElem))
             {
-                switch (type.GetString())
+                var typeStr = typeElem.ValueKind == JsonValueKind.String ? typeElem.GetString() : null;
+                if (string.IsNullOrEmpty(typeStr))
+                {
+                    Main.Context.API.LogWarn(nameof(ChromiumBookmarkLoader), "type property is not a string in bookmark node.");
+                    continue;
+                }
+                switch (typeStr)
                 {
                     case "folder":
                     case "workspace": // Edge Workspace
                         EnumerateFolderBookmark(subElement, bookmarks, source, profilePath);
                         break;
                     case "url":
-                        if (subElement.TryGetProperty("name", out var name) &&
-                            subElement.TryGetProperty("url", out var url) &&
-                            !string.IsNullOrEmpty(name.GetString()) &&
-                            !string.IsNullOrEmpty(url.GetString()))
+                        var hasName = subElement.TryGetProperty("name", out var nameElem);
+                        var hasUrl = subElement.TryGetProperty("url", out var urlElem);
+                        var nameStr = hasName && nameElem.ValueKind == JsonValueKind.String ? nameElem.GetString() : null;
+                        var urlStr = hasUrl && urlElem.ValueKind == JsonValueKind.String ? urlElem.GetString() : null;
+                        if (!string.IsNullOrEmpty(nameStr) && !string.IsNullOrEmpty(urlStr))
                         {
-                            bookmarks.Add(new Bookmark(name.GetString()!, url.GetString()!, source, profilePath));
+                            bookmarks.Add(new Bookmark(nameStr!, urlStr!, source, profilePath));
                         }
                         break;
                 }
             }
             else
             {
-                Main.Context.API.LogException(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.", null);
+                Main.Context.API.LogWarn(nameof(ChromiumBookmarkLoader), "type property not found in bookmark node.");
             }

80-128: No additional unsafe GetString() usages found — all occurrences are confined to ChromiumBookmarkLoader.cs and already guarded by TryGetProperty and null/empty checks.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconWebClient.cs (1)

20-24: Good: TLS cert validation restored.

Removing the dangerous callback closes a critical security hole.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BookmarkLoaderService.cs (2)

96-103: Defensive checks for custom Chromium paths look good.

Validates name/path and leverages BrowserDetector.GetChromiumProfileDirectories, which already includes the base path to cover direct-profile inputs.

If you want Brave/Vivaldi/Opera supported out‑of‑the‑box (without custom entries), I can add their known default paths in a follow‑up.


51-52: Confirm deduplication criteria in Bookmark.Equals
Bookmark already overrides equality to compare only Name and Url, ignoring Source and ProfilePath. Verify that deduplication via Distinct() should indeed be based solely on those two properties.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs (1)

11-20: Good call using GeneratedRegex over runtime‑compiled regex.

This addresses startup/perf concerns from previous reviews about multiple compiled regex instances.

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs (1)

21-30: Great fixes: corrected SQL, WAL/SHM snapshot, duplicate‑safe fallback, and stable ProfilePath.

  • Query now uses IS NOT NULL and proper JOIN/WHERE.
  • Fallback copies -wal/-shm to preserve recent writes.
  • Clearing partially read results avoids duplicates.
  • ProfilePath derives from original _placesPath, not temp copy.

All align with prior feedback. Based on learnings

Also applies to: 57-66, 67-70, 96-99

Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs (2)

71-72: Concurrency and visibility look solid.

Volatile.Read/Write on _bookmarks and the SemaphoreSlim gate prevent torn reads and overlapping reloads; PeriodicTimer loop is hardened.

Also applies to: 108-127, 158-170


219-226: Bug: StartsWith can drop the wrong sources (e.g., “Firefox (Store)”). Use exact set membership on normalized names.

Prefix matching may remove unrelated sources. Normalize base names and compare via HashSet.

Apply this diff:

-            var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
-            var successfulLoaderNames = successfulResults.Select(r => r.Loader.Name).ToHashSet();
+            var newFirefoxBookmarks = successfulResults.SelectMany(r => r.Bookmarks).ToList();
+            var successfulLoaderNames = successfulResults
+                .Select(r => GetBaseSource(r.Loader.Name))
+                .ToHashSet(StringComparer.OrdinalIgnoreCase);

-            var currentBookmarks = Volatile.Read(ref _bookmarks);
-
-            var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Any(name => b.Source.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
+            var currentBookmarks = Volatile.Read(ref _bookmarks);
+            var otherBookmarks = currentBookmarks.Where(b => !successfulLoaderNames.Contains(GetBaseSource(b.Source)));

Add helper (anywhere in the class, e.g., above ReloadFirefoxBookmarksAsync):

+    private static string GetBaseSource(string source)
+    {
+        var idx = source.IndexOf(" (", StringComparison.Ordinal);
+        return idx >= 0 ? source[..idx] : source;
+    }
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FaviconService.cs (1)

96-161: Favicon pipeline hardening looks excellent.

  • Parallel.ForEachAsync with cancellation honored in GetFaviconFromWebAsync.
  • Host/page cache unification + atomic temp‑file writes.
  • SHA‑256 cache keys.
  • Selection logic guards against null PngData.
  • Failure cooldown persisted; SaveFailedFetchesAsync handles ODE.

Also applies to: 163-188, 190-199, 201-279, 281-293

Comment on lines +13 to +14
if (!Directory.Exists(basePath))
return [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix C# 12-only collection expression to avoid build breaks on older LangVersion.

return []; requires C# 12. Use Array.Empty<string>() to be safe.

Apply this diff:

-        if (!Directory.Exists(basePath))
-            return [];
+        if (!Directory.Exists(basePath))
+            return System.Array.Empty<string>();

Run to verify LangVersion and other usages:


🏁 Script executed:

#!/bin/bash
# Check declared C# language versions
fd -t f -a -g "*.csproj" | xargs -I{} sh -c 'echo "== {} =="; rg -n "<LangVersion>" "{}" || echo "(no explicit LangVersion)"'
# Search for other C# 12 collection expressions that may break older compilers
rg -nP --type=cs -C2 $'return\\s*\\[\\s*\\];|=\\s*\\[\\s*\\];'

Length of output: 6877


Replace C# 12 collection expressions to avoid build breaks

  • No <LangVersion> is set in any .csproj; the [] collection expression requires C# 12.
  • Multiple occurrences found (e.g. BrowserDetector.cs, BookmarkWatcherService.cs, Main.cs, Settings.cs, Internationalization.cs, Calculator/Main.cs, Explorer/Settings.cs).
  • Replace
    • return [];System.Array.Empty<T>() (or Enumerable.Empty<T>())
    • = [];new List<T>() or new ObservableCollection<T>()
🤖 Prompt for AI Agents
In Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/BrowserDetector.cs
around lines 13 to 14, the code uses the C# 12 collection expression `return
[];` which breaks builds because no <LangVersion> is set; replace this with a
non-C#12 equivalent such as System.Array.Empty<T>() or Enumerable.Empty<T>()
(use the appropriate T for the method's return type). Also scan other reported
files (BookmarkWatcherService.cs, Main.cs, Settings.cs, Internationalization.cs,
Calculator/Main.cs, Explorer/Settings.cs) and replace `= [];` with a concrete
constructor like new List<T>() or new ObservableCollection<T>() as appropriate
and change any other `return [];` to the array/enumerable empty helper.

Comment on lines +99 to +103
if (!folderElement.TryGetProperty("children", out var childrenElement))
return;

foreach (var subElement in childrenElement.EnumerateArray())
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard children ValueKind before EnumerateArray() to avoid InvalidOperationException.

Corrupt/extension-modified bookmark trees can set non-array values.

-        foreach (var subElement in childrenElement.EnumerateArray())
+        if (childrenElement.ValueKind != JsonValueKind.Array)
+        {
+            Main.Context.API.LogWarn(nameof(ChromiumBookmarkLoader), "children property is not an array in bookmark node.");
+            return;
+        }
+        foreach (var subElement in childrenElement.EnumerateArray())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!folderElement.TryGetProperty("children", out var childrenElement))
return;
foreach (var subElement in childrenElement.EnumerateArray())
{
if (!folderElement.TryGetProperty("children", out var childrenElement))
return;
if (childrenElement.ValueKind != JsonValueKind.Array)
{
Main.Context.API.LogWarn(nameof(ChromiumBookmarkLoader), "children property is not an array in bookmark node.");
return;
}
foreach (var subElement in childrenElement.EnumerateArray())
{
🤖 Prompt for AI Agents
In
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/ChromiumBookmarkLoader.cs
around lines 99 to 103, the code calls childrenElement.EnumerateArray() without
verifying that childrenElement is actually a JSON array; add a guard that checks
childrenElement.ValueKind == JsonValueKind.Array (or not JsonValueKind.Null)
before calling EnumerateArray and return or skip processing when it is not an
array to avoid InvalidOperationException from corrupt or extension-modified
bookmark trees.

Comment on lines +76 to +79
catch (Exception e)
{
Main.Context.API.LogException(nameof(FirefoxBookmarkLoader), $"Failed to load {_browserName} bookmarks: {_placesPath}", e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t log cancellation as an error; handle OperationCanceledException explicitly.

Currently the generic catch will swallow OCE and log an error. Treat it as a normal shutdown path.

Apply this diff:

-        catch (Exception e)
+        catch (OperationCanceledException)
+        {
+            yield break;
+        }
+        catch (Exception e)
         {
             Main.Context.API.LogException(nameof(FirefoxBookmarkLoader), $"Failed to load {_browserName} bookmarks: {_placesPath}", e);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
catch (Exception e)
{
Main.Context.API.LogException(nameof(FirefoxBookmarkLoader), $"Failed to load {_browserName} bookmarks: {_placesPath}", e);
}
catch (OperationCanceledException)
{
yield break;
}
catch (Exception e)
{
Main.Context.API.LogException(
nameof(FirefoxBookmarkLoader),
$"Failed to load {_browserName} bookmarks: {_placesPath}",
e);
}
🤖 Prompt for AI Agents
In
Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/FirefoxBookmarkLoader.cs
around lines 76 to 79, the generic catch is currently logging
OperationCanceledException as an error; add a specific catch for
OperationCanceledException before the general catch and do not call
Main.Context.API.LogException for it—either swallow or preferably rethrow
(throw;) to preserve the cancellation signal so shutdown is handled normally,
then keep the existing generic Exception catch to log genuine errors.

Comment on lines +49 to +59
Main.Context.API.LogDebug(nameof(Parse), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");

if (href.StartsWith("//"))
{
href = effectiveBaseUri.Scheme + ":" + href;
}

if (!Uri.TryCreate(effectiveBaseUri, href, out var fullUrl))
{
Main.Context.API.LogWarn(nameof(Parse), $"Failed to create a valid URI from href: '{href}' and base URI: '{effectiveBaseUri}'");
continue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use class name in logs instead of method name for consistency.

LogDebug/LogWarn are called with nameof(Parse), producing “Parse” as the source. Prefer nameof(HtmlFaviconParser) to align with other logs.

-            Main.Context.API.LogDebug(nameof(Parse), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");
+            Main.Context.API.LogDebug(nameof(HtmlFaviconParser), $"Found potential favicon link. Raw tag: '{linkTag}', Extracted href: '{href}', Base URI: '{effectiveBaseUri}'");
...
-                Main.Context.API.LogWarn(nameof(Parse), $"Failed to create a valid URI from href: '{href}' and base URI: '{effectiveBaseUri}'");
+                Main.Context.API.LogWarn(nameof(HtmlFaviconParser), $"Failed to create a valid URI from href: '{href}' and base URI: '{effectiveBaseUri}'");
...
-                Main.Context.API.LogDebug(nameof(Parse), $"Found suitable favicon candidate (score: {score}). Halting further HTML parsing.");
+                Main.Context.API.LogDebug(nameof(HtmlFaviconParser), $"Found suitable favicon candidate (score: {score}). Halting further HTML parsing.");

Also applies to: 65-68

🤖 Prompt for AI Agents
In Plugins/Flow.Launcher.Plugin.BrowserBookmark/Services/HtmlFaviconParser.cs
around lines 49-59 (and also apply the same change at 65-68), the
LogDebug/LogWarn calls use nameof(Parse) as the log source; update those calls
to use nameof(HtmlFaviconParser) instead so logs show the class name
consistently with other code. Replace the first argument in each logging call
from nameof(Parse) to nameof(HtmlFaviconParser) for all occurrences in these
ranges and keep the existing message text unchanged.

Comment on lines +15 to +23
public const int TargetIconSize = 48;

public async Task<(byte[]? PngData, int Size)> ToPngAsync(Stream stream, CancellationToken token)
{
token.ThrowIfCancellationRequested();

await using var ms = new MemoryStream();
await stream.CopyToAsync(ms, token);
ms.Position = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against oversized/hostile inputs when copying streams (potential OOM).

ToPngAsync copies the entire input to memory without a cap. Since inputs can come from the web, a large/hostile image could spike memory and stall the UI process.

Add a conservative limit (e.g., 5 MB) and short‑circuit when exceeded. Also avoid CopyToAsync so you can enforce the cap while reading.

 public class ImageConverter
 {
     public const int TargetIconSize = 48;
+    private const int MaxInputBytes = 5 * 1024 * 1024; // ~5 MB safeguard

     public async Task<(byte[]? PngData, int Size)> ToPngAsync(Stream stream, CancellationToken token)
     {
         token.ThrowIfCancellationRequested();

-        await using var ms = new MemoryStream();
-        await stream.CopyToAsync(ms, token);
+        await using var ms = new MemoryStream(capacity: 64 * 1024);
+        if (stream.CanSeek && stream.Length > MaxInputBytes)
+            return (null, 0);
+        var buffer = new byte[8192];
+        int read;
+        long total = 0;
+        while ((read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token)) > 0)
+        {
+            total += read;
+            if (total > MaxInputBytes)
+                return (null, 0);
+            await ms.WriteAsync(buffer.AsMemory(0, read), token);
+        }
         ms.Position = 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public const int TargetIconSize = 48;
public async Task<(byte[]? PngData, int Size)> ToPngAsync(Stream stream, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await using var ms = new MemoryStream();
await stream.CopyToAsync(ms, token);
ms.Position = 0;
public const int TargetIconSize = 48;
private const int MaxInputBytes = 5 * 1024 * 1024; // ~5 MB safeguard
public async Task<(byte[]? PngData, int Size)> ToPngAsync(Stream stream, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await using var ms = new MemoryStream(capacity: 64 * 1024);
if (stream.CanSeek && stream.Length > MaxInputBytes)
return (null, 0);
var buffer = new byte[8192];
int read;
long total = 0;
while ((read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), token)) > 0)
{
total += read;
if (total > MaxInputBytes)
return (null, 0);
await ms.WriteAsync(buffer.AsMemory(0, read), token);
}
ms.Position = 0;

Copy link
Member

@Jack251970 Jack251970 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionality works well here and I think this is good to go.

However, there are still two things we need to address before merging:

  1. Please resolve all reviews from AI.
  2. In FaviconWebClient, I noticed you have used a new HttpClient.cs instance. But I think it is better to use the one in Http.cs which allows Flow to use proxy set by users. So could you please use http API functions (HttpGetStringAsync, HttpGetStreamAsync, HttpDownloadAsync) instead. If you think these api functions are not enough, could you please create a new http api function?

@jjw24
Copy link
Member

jjw24 commented Sep 30, 2025

it would be trivially easy to extend this plugin to also index the user's browser history. Should that be added to future work?

@dcog989 no because this plugin is responsible for handling bookmarks.

If the indexing mechanism is better and can be used by other plugins like the Program plugin, then consider turning it into API methods available for others to adopt.

@Jack251970 Jack251970 linked an issue Oct 3, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request review in progress Indicates that a review is in progress for this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Display icon for browser bookmarks
3 participants