Skip to content

feat: add per-tracker SortTorrents queue ordering#64

Open
Feramance wants to merge 8 commits intomasterfrom
feat/per-tracker-sort-torrents
Open

feat: add per-tracker SortTorrents queue ordering#64
Feramance wants to merge 8 commits intomasterfrom
feat/per-tracker-sort-torrents

Conversation

@Feramance
Copy link
Copy Markdown
Owner

@Feramance Feramance commented Mar 24, 2026

Add tracker-level SortTorrents config across backend and WebUI, and reorder qBittorrent queues by effective tracker priority during processing when enabled. This preserves existing behavior by default and documents/tests the new setting end-to-end.

Summary

Provide a short overview of the change and why it is needed.

Testing

Describe how you validated this change.

  • Added or updated automated tests
  • Performed manual verification (describe below)
  • Not applicable (explain why)

Manual test notes:

Checklist

  • Linked the related issue or discussed scope with maintainers
  • Ran dotnet format and npm run lint (if touching frontend)
  • Updated documentation or release notes where it makes sense
  • Confirmed there are no sensitive secrets or credentials committed

Additional Notes

Optional context for reviewers (follow-up work, screenshots, rollout plan, etc.).


Note

Medium Risk
Medium risk because it changes torrent-processing behavior by issuing qBittorrent queue reordering calls and introduces new CORS configuration that can affect browser access if misconfigured.

Overview
Adds a new tracker-level SortTorrents flag (default false) that, when enabled, globally reorders the qBittorrent queue by effective tracker Priority during processing using topPrio, with locking/throttling to avoid repeated sorts.

Extends config parsing/saving and WebUI config editor/summary/tooltips to support SortTorrents, and adds tests covering TOML round-tripping and UI summary text.

Introduces optional WebUI.CorsAllowedOrigins configuration (defaults to allow any origin) and wires it into both Host and WebUI CORS policies, plus adds a SECURITY.md policy and updates docs to clarify /web/* vs /api/* exposure and warn/confirm when disabling auth.

Written by Cursor Bugbot for commit 929f04a. This will update automatically on new commits. Configure here.

Add tracker-level SortTorrents config across backend and WebUI, and reorder qBittorrent queues by effective tracker priority during processing when enabled. This preserves existing behavior by default and documents/tests the new setting end-to-end.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Batch topPrio call ignores hash parameter ordering
    • The sorting routine now issues topPrio per hash in computed order so qBittorrent batch reordering no longer discards tracker-priority intent.
Preview (3cd21b5bb1)
diff --git a/config.example.toml b/config.example.toml
--- a/config.example.toml
+++ b/config.example.toml
@@ -123,6 +123,13 @@
 # Global tracker configurations (inherited by all Arr instances)
 Trackers = []
 
+# Example per-tracker override:
+# [[qBit.Trackers]]
+# Name = "Private Tracker"
+# URI = "https://tracker.example.com/announce"
+# Priority = 10
+# SortTorrents = false
+
 [qBit.CategorySeeding]
 # Download rate limit per torrent in KB/s (-1 = unlimited)
 DownloadRateLimitPerTorrent = -1

diff --git a/docs/configuration/seeding.md b/docs/configuration/seeding.md
--- a/docs/configuration/seeding.md
+++ b/docs/configuration/seeding.md
@@ -437,6 +437,23 @@
 
 ---
 
+#### SortTorrents
+
+```toml
+SortTorrents = false
+```
+
+**Type:** Boolean
+**Default:** `false`
+
+When enabled for a tracker, torrents matching that tracker are reordered in the qBittorrent queue by tracker `Priority` during processing.
+
+- Higher `Priority` trackers are pushed toward the top of the queue.
+- This setting only affects torrents whose effective tracker configuration has `SortTorrents = true`.
+- qBittorrent **Torrent Queuing** must be enabled for queue ordering effects to be visible.
+
+---
+
 #### URI
 
 ```toml

diff --git a/docs/webui/config-editor.md b/docs/webui/config-editor.md
--- a/docs/webui/config-editor.md
+++ b/docs/webui/config-editor.md
@@ -445,6 +445,7 @@
 - **Name**: Tracker name (for display purposes)
 - **URI**: Tracker URL (used for matching)
 - **Priority**: Tracker priority (higher = preferred)
+- **Sort Torrents**: Reorder matching torrents in qBittorrent queue by tracker priority
 - **Maximum ETA (s)**: Override global ETA limit for this tracker
 - **Download Rate Limit**: Override global download limit
 - **Upload Rate Limit**: Override global upload limit
@@ -461,6 +462,7 @@
 Name = "Premium Tracker"
 URI = "https://premium.tracker.com/announce"
 Priority = 10
+SortTorrents = false
 MaximumETA = 86400
 DownloadRateLimit = -1
 UploadRateLimit = -1

diff --git a/src/Torrentarr.Core/Configuration/ConfigurationLoader.cs b/src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
--- a/src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
+++ b/src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
@@ -789,6 +789,10 @@
         }
 
         // --- HnR defaults on CategorySeeding and Tracker sections ---
+        var trackerDefaults = new Dictionary<string, object>
+        {
+            ["SortTorrents"] = false
+        };
         var hnrDefaults = new Dictionary<string, object>
         {
             ["HitAndRunMode"] = "disabled",
@@ -823,6 +827,14 @@
             {
                 foreach (var trackerTable in GetTrackerTables(trObj))
                 {
+                    foreach (var (field, defaultVal) in trackerDefaults)
+                    {
+                        if (!trackerTable.ContainsKey(field))
+                        {
+                            trackerTable[field] = defaultVal;
+                            changed = true;
+                        }
+                    }
                     foreach (var (field, defaultVal) in hnrDefaults)
                     {
                         if (!trackerTable.ContainsKey(field))
@@ -840,6 +852,14 @@
             {
                 foreach (var trackerTable in GetTrackerTables(atrObj))
                 {
+                    foreach (var (field, defaultVal) in trackerDefaults)
+                    {
+                        if (!trackerTable.ContainsKey(field))
+                        {
+                            trackerTable[field] = defaultVal;
+                            changed = true;
+                        }
+                    }
                     foreach (var (field, defaultVal) in hnrDefaults)
                     {
                         if (!trackerTable.ContainsKey(field))
@@ -977,6 +997,9 @@
         if (table.TryGetValue("Priority", out var priority))
             tracker.Priority = Convert.ToInt32(priority);
 
+        if (table.TryGetValue("SortTorrents", out var sortTorrents))
+            tracker.SortTorrents = Convert.ToBoolean(sortTorrents);
+
         if (table.TryGetValue("MaxUploadRatio", out var maxRatio))
             tracker.MaxUploadRatio = Convert.ToDouble(maxRatio);
 
@@ -1773,6 +1796,7 @@
                     sb.AppendLine($"Name = \"{EscapeTomlString(tracker.Name)}\"");
                 sb.AppendLine($"URI = \"{tracker.Uri}\"");
                 sb.AppendLine($"Priority = {tracker.Priority}");
+                sb.AppendLine($"SortTorrents = {tracker.SortTorrents.ToString().ToLower()}");
                 sb.AppendLine($"MaximumETA = {tracker.MaxETA ?? -1}");
                 sb.AppendLine($"DownloadRateLimit = {tracker.DownloadRateLimit ?? -1}");
                 sb.AppendLine($"UploadRateLimit = {tracker.UploadRateLimit ?? -1}");

diff --git a/src/Torrentarr.Core/Configuration/TorrentarrConfig.cs b/src/Torrentarr.Core/Configuration/TorrentarrConfig.cs
--- a/src/Torrentarr.Core/Configuration/TorrentarrConfig.cs
+++ b/src/Torrentarr.Core/Configuration/TorrentarrConfig.cs
@@ -107,6 +107,7 @@
     public string? Name { get; set; } // Human-readable tracker name
     public string Uri { get; set; } = "";
     public int Priority { get; set; } = 0;
+    public bool SortTorrents { get; set; } = false;
     public double? MaxUploadRatio { get; set; }
     public int? MaxSeedingTime { get; set; }
     public int? RemoveTorrent { get; set; }

diff --git a/src/Torrentarr.Infrastructure/ApiClients/QBittorrent/QBittorrentClient.cs b/src/Torrentarr.Infrastructure/ApiClients/QBittorrent/QBittorrentClient.cs
--- a/src/Torrentarr.Infrastructure/ApiClients/QBittorrent/QBittorrentClient.cs
+++ b/src/Torrentarr.Infrastructure/ApiClients/QBittorrent/QBittorrentClient.cs
@@ -452,6 +452,23 @@
     }
 
     /// <summary>
+    /// Move torrents to the top of the queue priority list.
+    /// POST /api/v2/torrents/topPrio
+    /// </summary>
+    public async Task<bool> TopPriorityAsync(List<string> hashes, CancellationToken ct = default)
+    {
+        if (hashes == null || hashes.Count == 0)
+            return true;
+
+        var request = new RestRequest("api/v2/torrents/topPrio", Method.Post);
+        AddAuthCookie(request);
+        request.AddParameter("hashes", string.Join("|", hashes));
+
+        var response = await _client.ExecuteAsync(request, ct);
+        return response.IsSuccessful;
+    }
+
+    /// <summary>
     /// Create a new category
     /// </summary>
     public async Task<bool> CreateCategoryAsync(string name, string? savePath = null, CancellationToken ct = default)

diff --git a/src/Torrentarr.Infrastructure/Services/TorrentProcessor.cs b/src/Torrentarr.Infrastructure/Services/TorrentProcessor.cs
--- a/src/Torrentarr.Infrastructure/Services/TorrentProcessor.cs
+++ b/src/Torrentarr.Infrastructure/Services/TorrentProcessor.cs
@@ -94,6 +94,8 @@
             }
             _logger.LogDebug("Found {Count} torrents in category {Category}", torrents.Count, category);
 
+            await SortTorrentsByTrackerPriorityAsync(torrents, cancellationToken);
+
             var stats = new TorrentProcessingStats
             {
                 TotalTorrents = torrents.Count
@@ -587,6 +589,83 @@
         }
     }
 
+    /// <summary>
+    /// Reorder qBittorrent queue by tracker priority for torrents whose effective tracker
+    /// config has SortTorrents=true. Runs once per processing cycle per category.
+    /// </summary>
+    private async Task SortTorrentsByTrackerPriorityAsync(
+        List<TorrentInfo> torrents,
+        CancellationToken ct)
+    {
+        if (_seedingService == null || torrents.Count == 0)
+            return;
+
+        // Fast-path: if no configured tracker enables sorting, skip per-torrent tracker API lookups.
+        var hasSortTorrentsEnabled = _config.QBitInstances.Values.Any(q =>
+            q.Trackers.Any(t => t.SortTorrents))
+            || _config.ArrInstances.Values.Any(a =>
+                a.Torrent.Trackers.Any(t => t.SortTorrents));
+
+        if (!hasSortTorrentsEnabled)
+            return;
+
+        var sortableByInstance = new Dictionary<string, List<(TorrentInfo Torrent, int Priority)>>(StringComparer.OrdinalIgnoreCase);
+
+        foreach (var torrent in torrents)
+        {
+            try
+            {
+                var trackerConfig = await _seedingService.GetTrackerConfigAsync(torrent, ct);
+                if (trackerConfig?.SortTorrents != true)
+                    continue;
+
+                if (!sortableByInstance.TryGetValue(torrent.QBitInstanceName, out var list))
+                {
+                    list = new List<(TorrentInfo Torrent, int Priority)>();
+                    sortableByInstance[torrent.QBitInstanceName] = list;
+                }
+
+                list.Add((torrent, trackerConfig.Priority));
+            }
+            catch (Exception ex)
+            {
+                _logger.LogDebug(ex, "Skipping sort evaluation for torrent {Hash}", torrent.Hash);
+            }
+        }
+
+        foreach (var (instanceName, sortable) in sortableByInstance)
+        {
+            if (sortable.Count == 0)
+                continue;
+
+            var client = _qbitManager.GetClient(instanceName);
+            if (client == null)
+                continue;
+
+            try
+            {
+                // qB topPrio moves torrents to the top of the queue.
+                // Apply reverse order of descending priority one hash at a time so each later
+                // call is placed above the previous one (highest priority ends up at the top).
+                var ordered = sortable
+                    .OrderByDescending(t => t.Priority)
+                    .ThenBy(t => t.Torrent.AddedOn)
+                    .Select(t => t.Torrent.Hash)
+                    .Reverse()
+                    .ToList();
+
+                foreach (var hash in ordered)
+                {
+                    await client.TopPriorityAsync(new List<string> { hash }, ct);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogWarning(ex, "Failed to sort torrent queue for qBit instance {Instance}", instanceName);
+            }
+        }
+    }
+
     // ========================================================================================
     // PRE-STEP: leave_alone resolution (qBitrr: _should_leave_alone — arss.py:5804-5892)
     // ========================================================================================

diff --git a/tests/Torrentarr.Core.Tests/Configuration/ConfigurationLoaderTests.cs b/tests/Torrentarr.Core.Tests/Configuration/ConfigurationLoaderTests.cs
--- a/tests/Torrentarr.Core.Tests/Configuration/ConfigurationLoaderTests.cs
+++ b/tests/Torrentarr.Core.Tests/Configuration/ConfigurationLoaderTests.cs
@@ -593,4 +593,78 @@
         config.WebUI.AuthDisabled.Should().BeFalse("new installs get auth enabled by default");
         config.WebUI.LocalAuthEnabled.Should().BeTrue("new installs get local auth enabled by default");
     }
+
+    [Fact]
+    public void Load_ParsesTrackerSortTorrents_DefaultsFalseWhenMissing()
+    {
+        WriteToml("""
+            [qBit]
+            Host = "localhost"
+
+            [Radarr-Movies]
+            URI = "http://radarr:7878"
+            APIKey = "key"
+            Category = "radarr"
+
+            [[Radarr-Movies.Torrent.Trackers]]
+            URI = "https://tracker.example.com/announce"
+            Priority = 10
+            """);
+
+        var config = new ConfigurationLoader(_tempFilePath).Load();
+
+        config.ArrInstances["Radarr-Movies"].Torrent.Trackers.Should().HaveCount(1);
+        config.ArrInstances["Radarr-Movies"].Torrent.Trackers[0].SortTorrents.Should().BeFalse();
+    }
+
+    [Fact]
+    public void Save_WritesTrackerSortTorrents()
+    {
+        WriteToml("""
+            [qBit]
+            Host = "localhost"
+
+            [Radarr-Movies]
+            URI = "http://radarr:7878"
+            APIKey = "key"
+            Category = "radarr"
+            """);
+
+        var loader = new ConfigurationLoader(_tempFilePath);
+        var config = loader.Load();
+        config.ArrInstances["Radarr-Movies"].Torrent.Trackers.Add(new TrackerConfig
+        {
+            Uri = "https://tracker.example.com/announce",
+            Priority = 10,
+            SortTorrents = true
+        });
+
+        loader.SaveConfig(config);
+        var content = File.ReadAllText(_tempFilePath);
+
+        content.Should().Contain("SortTorrents = true");
+    }
+
+    [Fact]
+    public void Load_ParsesTrackerSortTorrents_TrueWhenSet()
+    {
+        WriteToml("""
+            [qBit]
+            Host = "localhost"
+
+            [Radarr-Movies]
+            URI = "http://radarr:7878"
+            APIKey = "key"
+            Category = "radarr"
+
+            [[Radarr-Movies.Torrent.Trackers]]
+            URI = "https://tracker.example.com/announce"
+            Priority = 10
+            SortTorrents = true
+            """);
+
+        var config = new ConfigurationLoader(_tempFilePath).Load();
+
+        config.ArrInstances["Radarr-Movies"].Torrent.Trackers[0].SortTorrents.Should().BeTrue();
+    }
 }

diff --git a/tests/Torrentarr.Core.Tests/Configuration/TorrentarrConfigDefaultsTests.cs b/tests/Torrentarr.Core.Tests/Configuration/TorrentarrConfigDefaultsTests.cs
--- a/tests/Torrentarr.Core.Tests/Configuration/TorrentarrConfigDefaultsTests.cs
+++ b/tests/Torrentarr.Core.Tests/Configuration/TorrentarrConfigDefaultsTests.cs
@@ -101,6 +101,7 @@
     {
         var tracker = new TrackerConfig();
 
+        tracker.SortTorrents.Should().BeFalse();
         tracker.HitAndRunMode.Should().BeNull(); // string? defaults to null
         tracker.MinSeedRatio.Should().BeNull();
         tracker.MinSeedingTimeDays.Should().BeNull();

diff --git a/webui/src/__tests__/config/torrentHandlingSummary.test.ts b/webui/src/__tests__/config/torrentHandlingSummary.test.ts
new file mode 100644
--- /dev/null
+++ b/webui/src/__tests__/config/torrentHandlingSummary.test.ts
@@ -1,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import {
+  getArrTorrentHandlingSummary,
+  getQbitTorrentHandlingSummary,
+} from "../../config/torrentHandlingSummary";
+
+describe("torrentHandlingSummary", () => {
+  it("includes queue sorting line for Arr tracker when SortTorrents is enabled", () => {
+    const summary = getArrTorrentHandlingSummary({
+      Torrent: {
+        Trackers: [
+          {
+            Name: "Private",
+            HitAndRunMode: "disabled",
+            SortTorrents: true,
+          },
+        ],
+      },
+    } as never);
+
+    expect(summary).toContain("Queue sorting is enabled for this tracker.");
+  });
+
+  it("does not include queue sorting line when SortTorrents is absent", () => {
+    const summary = getQbitTorrentHandlingSummary({
+      Trackers: [
+        {
+          Name: "Private",
+          HitAndRunMode: "disabled",
+        },
+      ],
+    } as never);
+
+    expect(summary).not.toContain("Queue sorting is enabled for this tracker.");
+  });
+});

diff --git a/webui/src/config/tooltips.ts b/webui/src/config/tooltips.ts
--- a/webui/src/config/tooltips.ts
+++ b/webui/src/config/tooltips.ts
@@ -200,6 +200,8 @@
     "Ignore torrents younger than this many seconds in managed categories when evaluating failures.",
   HitAndRunMode:
     "Hit and Run protection mode for this tracker: 'and' requires both ratio and time, 'or' clears on either, 'disabled' turns off HnR.",
+  SortTorrents:
+    "When enabled, torrents matching this tracker are moved to the top of the qBittorrent queue by tracker priority. Requires qBittorrent Torrent Queuing to be enabled.",
   MinSeedRatio:
     "Minimum seed ratio before HnR obligation is cleared (e.g. 1.0 for 1:1 ratio).",
   MinSeedingTimeDays:

diff --git a/webui/src/config/torrentHandlingSummary.ts b/webui/src/config/torrentHandlingSummary.ts
--- a/webui/src/config/torrentHandlingSummary.ts
+++ b/webui/src/config/torrentHandlingSummary.ts
@@ -245,6 +245,7 @@
       const t = raw as Record<string, unknown>;
       const name = String(t.Name ?? "Tracker").trim() || "Tracker";
       const mode = resolveHnrMode(t.HitAndRunMode);
+      const sortTorrents = Boolean(t.SortTorrents);
       const tMinRatio = Number(t.MinSeedRatio ?? 1);
       const tMinDays = Number(t.MinSeedingTimeDays ?? 0);
       const ratioStr = Number.isFinite(tMinRatio) ? tMinRatio : 1;
@@ -256,12 +257,12 @@
       );
       if (mode === "disabled") {
         blocks.push(
-          `- **${name}** \u2014 HnR is off. The torrent may be removed as soon as the seeding rules above are met.`,
+          `- **${name}** \u2014 HnR is off. The torrent may be removed as soon as the seeding rules above are met.${sortTorrents ? " Queue sorting is enabled for this tracker." : ""}`,
         );
       } else {
         const both = mode === "and" ? "both required" : "either allows removal";
         blocks.push(
-          `- **${name}** \u2014 HnR is on (${both}). The torrent will **not** be removed until it has reached ratio ${ratioStr} and been seeding for ${daysVal} ${daysLabel}. Until then, it is protected from removal even if stalled or if the global seeding limit would allow removal.`,
+          `- **${name}** \u2014 HnR is on (${both}). The torrent will **not** be removed until it has reached ratio ${ratioStr} and been seeding for ${daysVal} ${daysLabel}. Until then, it is protected from removal even if stalled or if the global seeding limit would allow removal.${sortTorrents ? " Queue sorting is enabled for this tracker." : ""}`,
         );
       }
     });
@@ -398,6 +399,7 @@
       const t = raw as Record<string, unknown>;
       const name = String(t.Name ?? "Tracker").trim() || "Tracker";
       const mode = resolveHnrMode(t.HitAndRunMode);
+      const sortTorrents = Boolean(t.SortTorrents);
       const tMinRatio = Number(t.MinSeedRatio ?? 1);
       const tMinDays = Number(t.MinSeedingTimeDays ?? 0);
       const ratioStr = Number.isFinite(tMinRatio) ? tMinRatio : 1;
@@ -409,7 +411,7 @@
       );
       if (mode === "disabled") {
         blocks.push(
-          `- **${name}** \u2014 HnR is off. The torrent may be removed as soon as the seeding rules above are met (e.g. after max time or when max ratio is reached).`,
+          `- **${name}** \u2014 HnR is off. The torrent may be removed as soon as the seeding rules above are met (e.g. after max time or when max ratio is reached).${sortTorrents ? " Queue sorting is enabled for this tracker." : ""}`,
         );
       } else {
         const both = mode === "and" ? "both required" : "either allows removal";
@@ -418,7 +420,7 @@
             ? `until it has reached ratio ${ratioStr} and been seeding for ${daysVal} ${daysLabel}`
             : `until it has reached ratio ${ratioStr} or been seeding for ${daysVal} ${daysLabel}`;
         blocks.push(
-          `- **${name}** \u2014 HnR is on (${both}). The torrent will **not** be removed ${until}. Until then, it is protected from removal even if stalled or if the global seeding time would allow removal.`,
+          `- **${name}** \u2014 HnR is on (${both}). The torrent will **not** be removed ${until}. Until then, it is protected from removal even if stalled or if the global seeding time would allow removal.${sortTorrents ? " Queue sorting is enabled for this tracker." : ""}`,
         );
       }
     });

diff --git a/webui/src/pages/ConfigView.tsx b/webui/src/pages/ConfigView.tsx
--- a/webui/src/pages/ConfigView.tsx
+++ b/webui/src/pages/ConfigView.tsx
@@ -1148,6 +1148,11 @@
     },
   },
   {
+    label: "Sort Torrents",
+    path: ["SortTorrents"],
+    type: "checkbox",
+  },
+  {
     label: "Maximum ETA",
     path: ["MaxETA"],
     type: "duration",
@@ -3187,6 +3192,7 @@
           Name: "",
           Uri: "",
           Priority: 0,
+          SortTorrents: false,
           RemoveIfExists: false,
           SuperSeedMode: false,
           AddTags: [],

cursoragent and others added 3 commits March 24, 2026 09:53
Document latest-version-only security support, add configurable CORS allowlist, and add a confirmation warning before disabling auth so users keep intended behavior while getting a clear internet-exposure warning.
Assert on the rendered movie row value instead of the column header text so the test waits for loaded data and remains stable on slower Linux/macOS runners.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

{
_logger.LogDebug(ex, "Skipping sort evaluation for torrent {Hash}", torrent.Hash);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Global sort makes sequential API call per torrent

Low Severity

SortTorrentsByTrackerPriorityAsync fetches ALL torrents across every qBit instance, then calls GetTrackerConfigAsync (which internally issues an HTTP GetTorrentTrackersAsync call) sequentially for each one, followed by individual TopPriorityAsync HTTP calls per sortable hash. For large libraries this is O(N + M) sequential HTTP round-trips, which can block the processing worker for a significant period. The debounce and opt-in activation mitigate repeated hits, but a single cycle can still stall processing when N is large.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants