Repository: Flow-Launcher/Flow.Launcher
Pull request #3500: Support Multiple Topmost Records
Author: login: Jack251970
Labels: name: enhancement
description: New feature or request
Created: 2025-05-01T05:28:22Z | Merged: 2025-05-14T03:18:00Z

# Support Multiple Topmost Records

Currently, for one query, Flow can only support one topmost record. We should support multiple topmost records because users may want to set many records to be topmost.

The latter record is, the topper it be.

# Test

- Old data `TopMostRecord.json` will be converted into new data `MultipleTopMostRecord.json`.
- Flow can display multiple topmost records.
- Latter records are topper.

e.g.
MultipleTopMostRecord.json:
```
{
  "records": {
    "vs": [
      {
        "Title": "Visual Studio 2022",
        "SubTitle": "",
        "PluginID": "791FC278BA414111B8D1886DFE447410",
        "RecordKey": null
      },
      {
        "Title": "Visual Studio Installer",
        "SubTitle": "",
        "PluginID": "791FC278BA414111B8D1886DFE447410",
        "RecordKey": null
      },
      // This is the latest record and it should be the topmost
      {
        "Title": "Visual Studio Code",
        "SubTitle": "",
        "PluginID": "791FC278BA414111B8D1886DFE447410",
        "RecordKey": null
      }
    ]
  }
}
```

Screenshot:
![image](https://github.com/user-attachments/assets/cd0d634d-a755-4436-8dc5-0202dab66ee5)


Diff:
diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
index 0b10382ee9b..c7eba05fd5c 100644
--- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
@@ -45,6 +45,22 @@ public JsonStorage(string filePath)
             FilesFolders.ValidateDirectory(DirectoryPath);
         }
 
+        public bool Exists()
+        {
+            return File.Exists(FilePath);
+        }
+
+        public void Delete()
+        {
+            foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath })
+            {
+                if (File.Exists(path))
+                {
+                    File.Delete(path);
+                }
+            }
+        }
+
         public async Task<T> LoadAsync()
         {
             if (Data != null)
diff --git a/Flow.Launcher/Storage/TopMostRecord.cs b/Flow.Launcher/Storage/TopMostRecord.cs
index 7f35904a520..d75e9cb7908 100644
--- a/Flow.Launcher/Storage/TopMostRecord.cs
+++ b/Flow.Launcher/Storage/TopMostRecord.cs
@@ -1,14 +1,115 @@
-﻿using System.Collections.Concurrent;
+﻿using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
 using System.Text.Json.Serialization;
+using Flow.Launcher.Infrastructure.Storage;
 using Flow.Launcher.Plugin;
 
 namespace Flow.Launcher.Storage
 {
-    public class TopMostRecord
+    public class FlowLauncherJsonStorageTopMostRecord
+    {
+        private readonly FlowLauncherJsonStorage<MultipleTopMostRecord> _topMostRecordStorage;
+        private readonly MultipleTopMostRecord _topMostRecord;
+
+        public FlowLauncherJsonStorageTopMostRecord()
+        {
+#pragma warning disable CS0618 // Type or member is obsolete
+            // Get old data & new data
+            var topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
+#pragma warning restore CS0618 // Type or member is obsolete
+            _topMostRecordStorage = new FlowLauncherJsonStorage<MultipleTopMostRecord>();
+
+            // Check if data exist
+            var oldDataExist = topMostRecordStorage.Exists();
+            var newDataExist = _topMostRecordStorage.Exists();
+
+            // If new data exist, it means we have already migrated the old data
+            // So we can safely delete the old data and load the new data
+            if (newDataExist)
+            {
+                try
+                {
+                    topMostRecordStorage.Delete();
+                }
+                catch
+                {
+                    // Ignored - Flow will delete the old data during next startup
+                }
+                _topMostRecord = _topMostRecordStorage.Load();
+            }
+            // If new data does not exist and old data exist, we need to migrate the old data to the new data
+            else if (oldDataExist)
+            {
+                // Migrate old data to new data
+                _topMostRecord = _topMostRecordStorage.Load();
+                var oldTopMostRecord = topMostRecordStorage.Load();
+                if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return;
+                foreach (var record in oldTopMostRecord.records)
+                {
+                    var newValue = new ConcurrentQueue<Record>();
+                    newValue.Enqueue(record.Value);
+                    _topMostRecord.records.AddOrUpdate(record.Key, newValue, (key, oldValue) =>
+                    {
+                        oldValue.Enqueue(record.Value);
+                        return oldValue;
+                    });
+                }
+
+                // Delete old data and save the new data
+                try
+                {
+                    topMostRecordStorage.Delete();
+                }
+                catch
+                {
+                    // Ignored - Flow will delete the old data during next startup
+                }
+                Save();
+            }
+            // If both data do not exist, we just need to create a new data
+            else
+            {
+                _topMostRecord = _topMostRecordStorage.Load();
+            }
+        }
+
+        public void Save()
+        {
+            _topMostRecordStorage.Save();
+        }
+
+        public bool IsTopMost(Result result)
+        {
+            return _topMostRecord.IsTopMost(result);
+        }
+
+        public int GetTopMostIndex(Result result)
+        {
+            return _topMostRecord.GetTopMostIndex(result);
+        }
+
+        public void Remove(Result result)
+        {
+            _topMostRecord.Remove(result);
+        }
+
+        public void AddOrUpdate(Result result)
+        {
+            _topMostRecord.AddOrUpdate(result);
+        }
+    }
+
+    /// <summary>
+    /// Old data structure to support only one top most record for the same query
+    /// </summary>
+    [Obsolete("Use MultipleTopMostRecord instead. This class will be removed in future versions.")]
+    internal class TopMostRecord
     {
         [JsonInclude]
-        public ConcurrentDictionary<string, Record> records { get; private set; } = new ConcurrentDictionary<string, Record>();
+        public ConcurrentDictionary<string, Record> records { get; private set; } = new();
 
         internal bool IsTopMost(Result result)
         {
@@ -56,12 +157,145 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentQueueConverter))]
+        public ConcurrentDictionary<string, ConcurrentQueue<Record>> records { get; private set; } = new();
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal int GetTopMostIndex(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return -1;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            // since the latter items should be more recent, we should return the smaller index for score to subtract
+            // which can make them more topmost
+            // A, B, C => 2, 1, 0 => (max - 2), (max - 1), (max - 0)
+            var index = 0;
+            foreach (var record in value)
+            {
+                if (record.Equals(result))
+                {
+                    return value.Count - 1 - index;
+                }
+                index++;
+            }
+            return -1;
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the queue
+            var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result)));
+            if (queue.IsEmpty)
+            {
+                // if the queue is empty, remove the queue from the dictionary
+                records.TryRemove(result.OriginQuery.RawQuery, out _);
+            }
+            else
+            {
+                // change the queue in the dictionary
+                records[result.OriginQuery.RawQuery] = queue;
+            }
+        }
+
+        internal void AddOrUpdate(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to add or update the record
+            if (result.OriginQuery == null)
+            {
+                return;
+            }
+
+            var record = new Record
+            {
+                PluginID = result.PluginID,
+                Title = result.Title,
+                SubTitle = result.SubTitle,
+                RecordKey = result.RecordKey
+            };
+            if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                // create a new queue if it does not exist
+                value = new ConcurrentQueue<Record>();
+                value.Enqueue(record);
+                records.TryAdd(result.OriginQuery.RawQuery, value);
+            }
+            else
+            {
+                // add or update the record in the queue
+                var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates
+                queue.Enqueue(record);
+                records[result.OriginQuery.RawQuery] = queue;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Because ConcurrentQueue does not support serialization, we need to convert it to a List
+    /// </summary>
+    internal class ConcurrentDictionaryConcurrentQueueConverter : JsonConverter<ConcurrentDictionary<string, ConcurrentQueue<Record>>>
+    {
+        public override ConcurrentDictionary<string, ConcurrentQueue<Record>> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            var dictionary = JsonSerializer.Deserialize<Dictionary<string, List<Record>>>(ref reader, options);
+            var concurrentDictionary = new ConcurrentDictionary<string, ConcurrentQueue<Record>>();
+            foreach (var kvp in dictionary)
+            {
+                concurrentDictionary.TryAdd(kvp.Key, new ConcurrentQueue<Record>(kvp.Value));
+            }
+            return concurrentDictionary;
+        }
+
+        public override void Write(Utf8JsonWriter writer, ConcurrentDictionary<string, ConcurrentQueue<Record>> value, JsonSerializerOptions options)
+        {
+            var dict = new Dictionary<string, List<Record>>();
+            foreach (var kvp in value)
+            {
+                dict.Add(kvp.Key, kvp.Value.ToList());
+            }
+            JsonSerializer.Serialize(writer, dict, options);
+        }
+    }
+
+    internal class Record
     {
-        public string Title { get; set; }
-        public string SubTitle { get; set; }
-        public string PluginID { get; set; }
-        public string RecordKey { get; set; }
+        public string Title { get; init; }
+        public string SubTitle { get; init; }
+        public string PluginID { get; init; }
+        public string RecordKey { get; init; }
 
         public bool Equals(Result r)
         {
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index c0b74dc6870..f92a8ed0a5f 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -39,11 +39,10 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable
 
         private readonly FlowLauncherJsonStorage<History> _historyItemsStorage;
         private readonly FlowLauncherJsonStorage<UserSelectedRecord> _userSelectedRecordStorage;
-        private readonly FlowLauncherJsonStorage<TopMostRecord> _topMostRecordStorage;
+        private readonly FlowLauncherJsonStorageTopMostRecord _topMostRecord;
         private readonly History _history;
         private int lastHistoryIndex = 1;
         private readonly UserSelectedRecord _userSelectedRecord;
-        private readonly TopMostRecord _topMostRecord;
 
         private CancellationTokenSource _updateSource; // Used to cancel old query flows
         private CancellationToken _updateToken; // Used to avoid ObjectDisposedException of _updateSource.Token
@@ -143,10 +142,9 @@ public MainViewModel()
 
             _historyItemsStorage = new FlowLauncherJsonStorage<History>();
             _userSelectedRecordStorage = new FlowLauncherJsonStorage<UserSelectedRecord>();
-            _topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
+            _topMostRecord = new FlowLauncherJsonStorageTopMostRecord();
             _history = _historyItemsStorage.Load();
             _userSelectedRecord = _userSelectedRecordStorage.Load();
-            _topMostRecord = _topMostRecordStorage.Load();
 
             ContextMenu = new ResultsViewModel(Settings)
             {
@@ -1805,7 +1803,7 @@ public void Save()
         {
             _historyItemsStorage.Save();
             _userSelectedRecordStorage.Save();
-            _topMostRecordStorage.Save();
+            _topMostRecord.Save();
         }
 
         /// <summary>
@@ -1838,9 +1836,12 @@ public void UpdateResultView(ICollection<ResultsForUpdate> resultsForUpdates)
             {
                 foreach (var result in metaResults.Results)
                 {
-                    if (_topMostRecord.IsTopMost(result))
+                    var deviationIndex = _topMostRecord.GetTopMostIndex(result);
+                    if (deviationIndex != -1)
                     {
-                        result.Score = Result.MaxScore;
+                        // Adjust the score based on the result's position in the top-most list.
+                        // A lower deviationIndex (closer to the top) results in a higher score.
+                        result.Score = Result.MaxScore - deviationIndex;
                     }
                     else
                     {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the topmost record storage to support multiple topmost records while preserving the original order. Key changes include:
- Replacing the single-record storage with a new FlowLauncherJsonStorageTopMostRecord class.
- Migrating old topmost record data to a new structure supporting multiple records.
- Extending JsonStorage functionality with Exists and Delete methods.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                                                     | Description                                                         |
| -------------------------------------------------------- | ------------------------------------------------------------------- |
| Flow.Launcher/ViewModel/MainViewModel.cs                 | Updated to use FlowLauncherJsonStorageTopMostRecord for topmost data. |
| Flow.Launcher/Storage/TopMostRecord.cs                     | Introduced a new storage class with migration logic and multiple records handling. |
| Flow.Launcher.Infrastructure/Storage/JsonStorage.cs        | Added utility methods for file existence check and deletion.         |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:29:03Z
user: login: Jack251970
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T05:30:17Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>Flow.Launcher.Infrastructure/Storage/JsonStorage.cs (1)</summary><blockquote>

`48-52`: **Consider broadening “existence” semantics**  

`Exists()` currently returns `File.Exists(FilePath)` only.  
In day-to-day use we normally only care about the primary json, but the consumer of this API may interpret “storage exists” as “any artefact exists”. If a previous run crashed while writing, only the `.bak` or `.tmp` file may be present, yet `Exists()` will misleadingly return false.  

Either (a) clarify the XML-doc comment to state that only the main file is checked, or (b) extend the check to the backup / temp paths so that migration code can make a more reliable decision.

</blockquote></details>
<details>
<summary>Flow.Launcher/ViewModel/MainViewModel.cs (3)</summary><blockquote>

`40-44`: **Field naming can be misleading**  

`_topMostRecord` actually holds the *storage wrapper* (`FlowLauncherJsonStorageTopMostRecord`), not the record collection itself.  
Because the same identifier is later used for model operations (`AddOrUpdate`, `IsTopMost`) it still works, but the mental model is harder to follow.  
A name such as `_topMostRecordStorage` (old name) or `_topMostRecordManager` would better convey responsibility.

---

`1613-1614`: **Missing await/async variant of `Save()`**  

`JsonStorage.Save()` is synchronous and can block the UI thread when Flow is closed via UI. You already have `SaveAsync()` upstream – consider exposing an async counterpart in `FlowLauncherJsonStorageTopMostRecord` and calling it from an async `SaveAsync` in `MainViewModel`, or invoking the sync save on a background thread.

---

`1646-1651`: **`IsTopMost` invoked per-result may impact large result sets**  

`UpdateResultView` iterates every incoming `Result` and calls `_topMostRecord.IsTopMost(result)`; under the new design this does an `O(n)` bag scan per call.  
In the worst case (large multi-plugin query + many top-most records) this becomes quadratic.  

An inexpensive optimisation is to cache `_topMostRecord` look-ups once per query key:  
```csharp
var isTopMostCache = new Dictionary<string, bool>();
...
var key = result.OriginQuery?.RawQuery;
if (key != null && isTopMostCache.TryGetValue(key, out var isTop))
{
    if (isTop) result.Score = Result.MaxScore;
}
else
{
    var top = _topMostRecord.IsTopMost(result);
    if (key != null) isTopMostCache[key] = top;
    if (top) result.Score = Result.MaxScore;
}
```

</blockquote></details>
<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (2)</summary><blockquote>

`252-277`: **Custom converter should null-check & reuse options**  

`Read()` assumes `dictionary` is non-null. A corrupted file will throw NRE instead of propagating `JsonException`, losing diagnostic information.  
Add a guard:

```csharp
if (dictionary == null)
    return new();
```  

Also, consider passing the existing `options` when re-serialising in `Write()` to preserve caller settings (e.g., indentation).

---

`279-299`: **`Record.Equals` lacks `GetHashCode` override**  

`Equals` is overridden for logical comparison but `GetHashCode` isn’t, breaking hash-based collections, and may lead to subtle bugs if a `Record` is ever placed in a `HashSet`/dictionary. Either add `GetHashCode` or seal the class and document the behaviour.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 950a7f5f2b8b53e8b850cda7d86a95c3050246ff and 0673d07c2a14a6595d4c495c1ab7b3fde3395782.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `Flow.Launcher.Infrastructure/Storage/JsonStorage.cs` (1 hunks)
* `Flow.Launcher/Storage/TopMostRecord.cs` (2 hunks)
* `Flow.Launcher/ViewModel/MainViewModel.cs` (3 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: gitStream workflow automation
* GitHub Check: gitStream.cm
* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>Flow.Launcher/ViewModel/MainViewModel.cs (1)</summary>

`136-138`: **Constructor no longer pre-loads data – confirm this is intended**  

Old code loaded records eagerly (`_topMostRecordStorage.Load()`); the new wrapper loads lazily inside its own ctor.  
That is fine, but be aware it now occurs *before* `MainViewModel`’s constructor body runs only if the wrapper’s constructor does synchronous IO. If startup performance matters, consider lazy-loading on first use.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:31:16Z
user: login: Jack251970
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T05:33:17Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:33:33Z
user: login: Jack251970
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T05:34:59Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:35:23Z
user: login: Jack251970
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T05:37:15Z
user: login: Jack251970
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-01T05:37:57Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:38:26Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:38:51Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces support for multiple topmost records by migrating from a single-record storage mechanism to a new storage system that allows multiple records per query. Key changes include refactoring the storage API in MainViewModel, implementing a new data structure (MultipleTopMostRecord) with conversion logic from the old format, and adding file existence and deletion functionality in JsonStorage.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File | Description |
| ---- | ----------- |
| Flow.Launcher/ViewModel/MainViewModel.cs | Migrates the topmost record storage to the new FlowLauncherJsonStorageTopMostRecord implementation. |
| Flow.Launcher/Storage/TopMostRecord.cs | Refactors the storage mechanism to support multiple topmost records and introduces a new data structure with migration logic. |
| Flow.Launcher/Infrastructure/Storage/JsonStorage.cs | Adds helper methods (Exists and Delete) for file management, supporting the migration process. |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**Flow.Launcher/Storage/TopMostRecord.cs:90**
* [nitpick] The internal class name 'TopMostRecord' may cause confusion with the new multi-record storage and the legacy naming in the migration logic. Consider renaming it to a name that clearly indicates its role as the legacy data structure.
```
internal class TopMostRecord
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:40:05Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces support for multiple topmost records by migrating from a single-record storage design to a multiple-record model.  
- Replaces the old FlowLauncherJsonStorage<TopMostRecord> with a new FlowLauncherJsonStorageTopMostRecord in MainViewModel.  
- Implements migration logic and new data structures (using ConcurrentBag) in the storage layer to accommodate multiple records.  
- Adds file existence and deletion capabilities in JsonStorage to support the migration process.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File | Description |
| ---- | ----------- |
| Flow.Launcher/ViewModel/MainViewModel.cs | Updated storage field and usage to support multiple topmost records. |
| Flow.Launcher/Storage/TopMostRecord.cs | Introduced new classes for multiple topmost records and provided migration logic from old data. |
| Flow.Launcher/Infrastructure/Storage/JsonStorage.cs | Added methods to check file existence and delete files for storage management. |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:44:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (1)</summary><blockquote>

`146-148`: **ConcurrentBag usage is appropriate for this implementation**

Using a `ConcurrentBag` for storing multiple records per query fits the requirements, as the retrieved learnings confirm that sequence order preservation is not needed in this context. This implementation choice appropriately balances thread safety with performance.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (1)</summary><blockquote>

`196-199`: **Check for empty bag may be redundant**

The check for `value.IsEmpty` after already rebuilding the bag seems redundant since we're replacing the entire bag with filtered elements on line 193. The bag would be empty only if it contained just the one element being removed.

```diff
-    // if the bag is empty, remove the bag from the dictionary
-    if (value.IsEmpty)
-    {
-        records.TryRemove(result.OriginQuery.RawQuery, out _);
-    }
+    // if the bag is empty, remove the bag from the dictionary
+    if (records[result.OriginQuery.RawQuery].IsEmpty)
+    {
+        records.TryRemove(result.OriginQuery.RawQuery, out _);
+    }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4ecef470e82c07c611044db777675e5205bede6d and c49b3b7cba86ffadaec56e748751041f5e5c47c1.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `Flow.Launcher/Storage/TopMostRecord.cs` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (2)</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:35:21.827Z
Learning: ConcurrentBag.TryTake(out T) in C# removes an arbitrary item from the bag, not necessarily the one matching the out parameter. This can lead to removing the wrong element when trying to remove a specific item. A safer approach is to reconstruct the collection using filtering or use an ordered collection with proper removal methods.
```

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:145-149
Timestamp: 2025-05-01T05:38:25.645Z
Learning: For the MultipleTopMostRecord implementation in Flow.Launcher, sequence order of records in the ConcurrentBag does not need to be preserved, as confirmed by the developer. The unordered nature of ConcurrentBag is acceptable for this implementation.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (2)</summary><blockquote>

<details>
<summary>Flow.Launcher.Infrastructure/Storage/JsonStorage.cs (3)</summary>

* `Exists` (48-51)
* `Delete` (53-62)
* `Save` (202-213)

</details>
<details>
<summary>Flow.Launcher/ViewModel/MainViewModel.cs (3)</summary>

* `Save` (1609-1614)
* `Result` (1372-1410)
* `Result` (1412-1438)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (5)</summary>

`12-85`: **Good implementation of the new storage class for managing topmost records**

The new `FlowLauncherJsonStorageTopMostRecord` class provides a clean facade that handles migration between the old single-record format and the new multiple-records format. The constructor logic properly addresses all possible scenarios (new data exists, only old data exists, neither exists) with appropriate exception handling during file deletion operations.

---

`191-194`: **Good fix for the ConcurrentBag.TryTake issue**

This implementation correctly addresses the issue with `ConcurrentBag.TryTake()` removing arbitrary items. Instead of using `TryTake()`, the code creates a new bag from filtered elements, ensuring the exact matching record is removed.

---

`229-233`: **Good fix for record updates using filtered approach**

Similar to the `Remove` method, the `AddOrUpdate` method correctly handles updating records by creating a new collection from filtered elements rather than attempting to use `TryTake()`, which would remove arbitrary items. This implementation ensures that the specific record is properly updated.

---

`240-262`: **Well-implemented JSON converter for concurrent collections**

The `ConcurrentDictionaryConcurrentBagConverter` properly handles the serialization and deserialization of the concurrent collections by converting them to standard collections that can be serialized. This is a good practice for working with concurrent collections that don't have built-in serialization support.

---

`266-269`: **Good choice to make Record properties immutable**

Using `init` accessors instead of `set` makes the `Record` properties immutable after initialization, which is a good practice for value objects. This prevents accidental modification of records after creation and helps maintain data integrity.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:45:21Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR enables Flow to support multiple topmost records by migrating existing single-record data and updating the storage logic.  
- Refactors storage for topmost records to use a new JSON storage class and data structure.  
- Adds migration logic to convert old topmost records into a new multiple-record format.  
- Enhances JSON storage by adding existence checks and file deletion support.

### Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

| File                                                        | Description                                                                                                          |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Flow.Launcher/ViewModel/MainViewModel.cs                    | Updates references to topmost record storage to use the new storage class for multiple topmost records.               |
| Flow.Launcher/Storage/TopMostRecord.cs                        | Introduces the new storage class (FlowLauncherJsonStorageTopMostRecord) and data structure (MultipleTopMostRecord) for migration. |
| Flow.Launcher/Infrastructure/Storage/JsonStorage.cs         | Adds new utility methods to check file existence and delete storage files.                                           |


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**Flow.Launcher/Infrastructure/Storage/JsonStorage.cs:57**
* Wrap the File.Delete call in a try-catch block to gracefully handle possible exceptions during file deletion and enhance fault tolerance.
```
if (File.Exists(path))
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:49:17Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR updates Flow to support multiple topmost records by migrating from a legacy single-record structure to a new data structure that supports multiple records and adjusts record ranking accordingly. Key changes include:
- Replacing the single topmost record storage with a new FlowLauncherJsonStorageTopMostRecord class.
- Updating the scoring logic in MainViewModel to factor in record positioning.
- Implementing migration logic in the storage layer to convert old data to the new format.

### Reviewed Changes

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

| File                                                         | Description                                                          |
| ------------------------------------------------------------ | -------------------------------------------------------------------- |
| Flow.Launcher/ViewModel/MainViewModel.cs                     | Updates to use the new topmost record structure and scoring adjustment. |
| Flow.Launcher/Storage/TopMostRecord.cs                         | Introduces the new FlowLauncherJsonStorageTopMostRecord and migration logic. |
| Flow.Launcher/Infrastructure/Storage/JsonStorage.cs            | Adds supportive file-check and deletion methods.                     |




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T02:30:55Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (3)</summary><blockquote>

`46-47`: **Add logging for old data migration.**

Consider adding a log message here to indicate that an old data migration is being performed. This would improve maintainability by providing better visibility into the migration process.

```diff
-                // Migrate old data to new data
+                // Migrate old data to new data
+                System.Diagnostics.Debug.WriteLine("Migrating from TopMostRecord to MultipleTopMostRecord format.");
```

---

`167-167`: **Consider if ConcurrentQueue is the right data structure for user expectations.**

The PR objectives mention that users expect the most recently pinned item to appear first (i.e., at the very top). However, `ConcurrentQueue` is FIFO (First In, First Out), meaning the oldest items will be enumerated first.

This may create a user experience issue where the most recently pinned item appears at the bottom of the topmost group, not at the top as users would expect when they pin an item.



Do you want to reconsider using a data structure that naturally places the most recent items first, such as `ConcurrentStack` (LIFO), or manually reversing the collection during enumeration?

---

`271-280`: **Consider preserving order for user experience.**

When deserializing the records, the order within each queue is preserved. However, if order is important for user experience (especially for the multiple topmost items feature), consider adding a comment that explains how ordering works in the deserialized data.

This is particularly important since there was a discussion in the PR about the appropriate ordering of topmost items.

```diff
         public override ConcurrentDictionary<string, ConcurrentQueue<Record>> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
             var dictionary = JsonSerializer.Deserialize<Dictionary<string, List<Record>>>(ref reader, options);
             var concurrentDictionary = new ConcurrentDictionary<string, ConcurrentQueue<Record>>();
             foreach (var kvp in dictionary)
             {
+                // Preserve the order of records as they were stored in the JSON
                 concurrentDictionary.TryAdd(kvp.Key, new ConcurrentQueue<Record>(kvp.Value));
             }
             return concurrentDictionary;
         }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c49b3b7cba86ffadaec56e748751041f5e5c47c1 and b25777b73457d872637ad594cad571d65fdea43c.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `Flow.Launcher/Storage/TopMostRecord.cs` (2 hunks)
* `Flow.Launcher/ViewModel/MainViewModel.cs` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* Flow.Launcher/ViewModel/MainViewModel.cs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (2)</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:35:21.827Z
Learning: ConcurrentBag.TryTake(out T) in C# removes an arbitrary item from the bag, not necessarily the one matching the out parameter. This can lead to removing the wrong element when trying to remove a specific item. A safer approach is to reconstruct the collection using filtering or use an ordered collection with proper removal methods.
```

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:145-149
Timestamp: 2025-05-01T05:38:25.645Z
Learning: For the MultipleTopMostRecord implementation in Flow.Launcher, sequence order of records in the ConcurrentBag does not need to be preserved, as confirmed by the developer. The unordered nature of ConcurrentBag is acceptable for this implementation.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>Flow.Launcher/Storage/TopMostRecord.cs (1)</summary>

`183-207`: **Consider clarifying the topmost index calculation logic.**

The current implementation assumes newer items (at the end of the queue) should have higher priority, which conflicts with the PR objective that states "the most recently pinned item should appear first."

In the comments discussion, users expect that when clicking "Top Most" on an item, it should move to the first position, which is intuitive. The current logic might not align with this expectation.

```diff
-            // since the latter items should be more recent, we should return the smaller index for score to subtract
-            // which can make them more topmost
-            // A, B, C => 2, 1, 0 => (max - 2), (max - 1), (max - 0)
+            // Items are stored in queue order, but for display we need to consider user expectations
+            // Users expect their most recently pinned item to appear first (at the top)
+            // The topmost index affects result ordering, with smaller values appearing higher
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T02:31:43Z
user: login: Jack251970
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T02:33:10Z
user: login: onesounds
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T02:52:11Z

Review comments:
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Copilot
body: Using ConcurrentBag here does not guarantee preserving the original sequence order of records as required. Consider switching to a thread-safe collection that maintains order, such as ConcurrentQueue or a thread-safe list.
```suggestion
        [JsonConverter(typeof(ConcurrentDictionaryConcurrentQueueConverter))]
        public ConcurrentDictionary<string, ConcurrentQueue<Record>> records { get; private set; } = new();
```
created_at: 2025-05-01T05:29:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Jack251970
body: We do not need to consider sequence here
created_at: 2025-05-01T05:30:17Z
author_association: MEMBER
diff_hunk: @@ -45,6 +45,27 @@ public JsonStorage(string filePath)
             FilesFolders.ValidateDirectory(DirectoryPath);
         }
 
+        public bool Exists()
+        {
+            return File.Exists(FilePath);
+        }
+
+        public void Delete()
+        {
+            if (File.Exists(FilePath))
+            {
+                File.Delete(FilePath);
+            }
+            if (File.Exists(BackupFilePath))
+            {
+                File.Delete(BackupFilePath);
+            }
+            if (File.Exists(TempFilePath))
+            {
+                File.Delete(TempFilePath);
+            }
+        }
path: Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`Delete()` is not exception-safe & may leave corrupted state**  

1. All three `File.Delete` calls are executed individually. If the second call throws (e.g. sharing violation) the first file has been removed but the third call is never reached, leaving the storage partly deleted.  
2. No error handling is present, so the caller must wrap this every time or crash.  
3. Deleting files that are hard-linked (`File.Replace` may create them) can unexpectedly remove shared data.

A minimal improvement is to wrap the whole section in a single `try/catch (IOException|UnauthorizedAccessException)` and log; even better, return a boolean indicating success.  
```diff
public bool Delete()
{
-    if (File.Exists(FilePath)) File.Delete(FilePath);
-    ...
+    try
+    {
+        foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath })
+            if (File.Exists(path))
+                File.Delete(path);
+        return true;
+    }
+    catch (IOException ex)
+    {
+        Log.Exception(ClassName, "Failed to delete storage files", ex);
+        return false;
+    }
}
```  
  


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:31:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`TryTake` may remove the wrong element**  

`ConcurrentBag.TryTake(out recordToRemove)` removes an *arbitrary* item; it ignores the candidate you supply.  
Therefore the element taken may not equal `recordToRemove`, leaving the matching record inside the bag and silently deleting another one.

Safer pattern:
```csharp
var bag = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result)));
records[result.OriginQuery.RawQuery] = new ConcurrentBag<Record>(bag);
```
(or switch to ordered collection and use `Remove`/`RemoveAll` under lock).

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:31:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**ConcurrentBag does not preserve order – breaks “maintain original sequence” requirement**  

PR objective explicitly states that the original order of multiple top-most records must be kept.  
`ConcurrentBag<T>` is *unordered*; enumeration order is deliberately undefined and changes as items are added/removed. This will surface as apparently random ordering when the bag is serialized or iterated.  

If order matters, prefer:
* `ConcurrentQueue<T>` (FIFO) – still thread-safe, preserves insertion order; or
* `List<T>` protected by a lock / `ConcurrentDictionary<string, List<Record>>`.

Switching to an ordered collection now prevents flaky behaviour later.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:31:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
+            // if the bag is empty, remove the bag from the dictionary
+            if (value.IsEmpty)
+            {
+                records.TryRemove(result.OriginQuery.RawQuery, out _);
+            }
+        }
+
+        internal void AddOrUpdate(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to add or update the record
+            if (result.OriginQuery == null)
+            {
+                return;
+            }
+
+            var record = new Record
+            {
+                PluginID = result.PluginID,
+                Title = result.Title,
+                SubTitle = result.SubTitle,
+                RecordKey = result.RecordKey
+            };
+            if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                // create a new bag if it does not exist
+                value = new ConcurrentBag<Record>()
+                {
+                    record
+                };
+                records.TryAdd(result.OriginQuery.RawQuery, value);
+            }
+            else
+            {
+                // add or update the record in the bag
+                if (value.Any(r => r.Equals(result)))
+                {
+                    // update the record
+                    var recordToUpdate = value.FirstOrDefault(r => r.Equals(result));
+                    if (recordToUpdate != null)
+                    {
+                        value.TryTake(out recordToUpdate);
+                        value.Add(record);
+                    }
+                }
+                else
+                {
+                    // add the record
+                    value.Add(record);
+                }
+            }
+        }
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Update path suffers from same `TryTake` limitation & potential duplicates**  

When the record already exists, the code:

```csharp
value.TryTake(out recordToUpdate);
value.Add(record);
```

1. `TryTake` may remove *another* element (see previous comment).  
2. If `TryTake` happens to remove a different item, the old record remains and the new one is appended, causing duplicates.  
3. Even in the happy path the replacement record is appended, shifting its position relative to others and defeating deterministic ordering.

Recommend replacing the bag with an ordered/synchronised list and updating in-place.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-01T05:31:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,6 +45,27 @@ public JsonStorage(string filePath)
             FilesFolders.ValidateDirectory(DirectoryPath);
         }
 
+        public bool Exists()
+        {
+            return File.Exists(FilePath);
+        }
+
+        public void Delete()
+        {
+            if (File.Exists(FilePath))
+            {
+                File.Delete(FilePath);
+            }
+            if (File.Exists(BackupFilePath))
+            {
+                File.Delete(BackupFilePath);
+            }
+            if (File.Exists(TempFilePath))
+            {
+                File.Delete(TempFilePath);
+            }
+        }
path: Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
user: login: Jack251970
body: Good
created_at: 2025-05-01T05:33:17Z
author_association: MEMBER
diff_hunk: @@ -45,6 +45,27 @@ public JsonStorage(string filePath)
             FilesFolders.ValidateDirectory(DirectoryPath);
         }
 
+        public bool Exists()
+        {
+            return File.Exists(FilePath);
+        }
+
+        public void Delete()
+        {
+            if (File.Exists(FilePath))
+            {
+                File.Delete(FilePath);
+            }
+            if (File.Exists(BackupFilePath))
+            {
+                File.Delete(BackupFilePath);
+            }
+            if (File.Exists(TempFilePath))
+            {
+                File.Delete(TempFilePath);
+            }
+        }
path: Flow.Launcher.Infrastructure/Storage/JsonStorage.cs
user: login: coderabbitai[bot]
body: @Jack251970 Glad you found the feedback helpful! Let me know if you need any help with implementing these changes. I'm here to assist if you have any questions.

🐰✨


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Jack251970 Glad you found the feedback helpful! Let me know if you need any help with implementing these changes. I'm here to assist if you have any questions.

🐰✨</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:33:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Jack251970
body: Good
created_at: 2025-05-01T05:34:59Z
author_association: MEMBER
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: @Jack251970 Great! Let me know if you need any further assistance with implementing the fix for the `TryTake` issue or have any other questions.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:35:21.827Z
Learning: ConcurrentBag.TryTake(out T) in C# removes an arbitrary item from the bag, not necessarily the one matching the out parameter. This can lead to removing the wrong element when trying to remove a specific item. A safer approach is to reconstruct the collection using filtering or use an ordered collection with proper removal methods.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Jack251970 Great! Let me know if you need any further assistance with implementing the fix for the `TryTake` issue or have any other questions.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:35:21.827Z
Learning: ConcurrentBag.TryTake(out T) in C# removes an arbitrary item from the bag, not necessarily the one matching the out parameter. This can lead to removing the wrong element when trying to remove a specific item. A safer approach is to reconstruct the collection using filtering or use an ordered collection with proper removal methods.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:35:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Jack251970
body: I do not think we need to do that. TryRemove gets the correct element reference.
created_at: 2025-05-01T05:37:15Z
author_association: MEMBER
diff_hunk: @@ -56,12 +138,150 @@
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Jack251970
body: We do not need to consider sequence here
created_at: 2025-05-01T05:37:57Z
author_association: MEMBER
diff_hunk: @@ -56,12 +138,150 @@
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: @Jack251970 Thanks for the clarification. I understand that the sequence order isn't important for the topmost records implementation, so using `ConcurrentBag` is appropriate for this use case.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:145-149
Timestamp: 2025-05-01T05:38:25.645Z
Learning: For the MultipleTopMostRecord implementation in Flow.Launcher, sequence order of records in the ConcurrentBag does not need to be preserved, as confirmed by the developer. The unordered nature of ConcurrentBag is acceptable for this implementation.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Jack251970 Thanks for the clarification. I understand that the sequence order isn't important for the topmost records implementation, so using `ConcurrentBag` is appropriate for this use case.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:145-149
Timestamp: 2025-05-01T05:38:25.645Z
Learning: For the MultipleTopMostRecord implementation in Flow.Launcher, sequence order of records in the ConcurrentBag does not need to be preserved, as confirmed by the developer. The unordered nature of ConcurrentBag is acceptable for this implementation.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:38:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: 

<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@Jack251970 I understand your thinking, but there might be a misunderstanding about how `ConcurrentBag.TryTake` works in C#. 

The `TryTake(out T result)` method in `ConcurrentBag` doesn't take a parameter that specifies *which* element to remove. The `out` parameter is only used to return whatever arbitrary element was removed.

From Microsoft's documentation:
> "Attempts to remove and return an object from the ConcurrentBag<T>."

When you do:
```csharp
var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
value.TryTake(out recordToRemove);
```

The `TryTake` call doesn't remove the specific `recordToRemove` item - it removes any random item from the bag and assigns it to your variable. This means you could end up removing the wrong record.

If you want to specifically remove the matching record, you'd need to recreate the collection without that element, as I suggested earlier, or use a different collection type that supports removing specific elements.



---

🏁 Script executed:
```
#!/bin/bash
# Search for documentation on ConcurrentBag.TryTake to confirm its behavior
echo "Let's examine the ConcurrentBag.TryTake documentation to verify its behavior:"
rg -A 10 "TryTake.*ConcurrentBag" --type=csharp || echo "No direct documentation found in the codebase"

# Run a simple test using a C# script to demonstrate ConcurrentBag.TryTake behavior
cat > /tmp/concurrentbag_test.cs << 'EOF'
using System;
using System.Collections.Concurrent;
using System.Linq;

class Program
{
    static void Main()
    {
        var bag = new ConcurrentBag<TestItem>();
        
        var item1 = new TestItem { Id = 1, Name = "Item 1" };
        var item2 = new TestItem { Id = 2, Name = "Item 2" };
        var item3 = new TestItem { Id = 3, Name = "Item 3" };
        
        bag.Add(item1);
        bag.Add(item2);
        bag.Add(item3);
        
        Console.WriteLine("Bag contents before TryTake:");
        foreach (var item in bag)
        {
            Console.WriteLine($"  - Item {item.Id}: {item.Name}");
        }
        
        // Try to take a specific item
        var itemToTake = bag.FirstOrDefault(i => i.Id == 2);
        Console.WriteLine($"\nTrying to take: Item {itemToTake.Id}: {itemToTake.Name}");
        
        TestItem takenItem;
        bool success = bag.TryTake(out takenItem);
        
        Console.WriteLine($"\nTryTake result: {success}");
        Console.WriteLine($"Taken item: Item {takenItem.Id}: {takenItem.Name}");
        
        Console.WriteLine("\nBag contents after TryTake:");
        foreach (var item in bag)
        {
            Console.WriteLine($"  - Item {item.Id}: {item.Name}");
        }
    }
}

class TestItem
{
    public int Id { get; set; }
    public string Name { get; set; }
}
EOF

# Try to compile and run the C# script to demonstrate the behavior
echo -e "\nCompiling and running test script to demonstrate ConcurrentBag.TryTake behavior:"
dotnet script /tmp/concurrentbag_test.cs || echo "Unable to run dotnet script. This would demonstrate that TryTake removes an arbitrary item, not the one referenced by the output parameter."

# Let's also check the MSDN documentation description for ConcurrentBag.TryTake
echo -e "\nHere's the expected behavior of ConcurrentBag.TryTake according to Microsoft docs:"
echo "ConcurrentBag<T>.TryTake Method: Attempts to remove and return an object from the ConcurrentBag<T>."
echo "The 'out T result' parameter is for the object removed from the bag, if successful."
echo "The method does NOT take an input parameter to specify which item to remove."
echo "Source: https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentbag-1.trytake"
```

Length of output: 1893

---



</details>

@Jack251970 I think there might be a misunderstanding about which method I was referring to. You're absolutely right that `records.TryRemove(result.OriginQuery.RawQuery, out _)` correctly removes the element with the specified key from the dictionary.

However, my comment was about this line:
```csharp
value.TryTake(out recordToRemove);
```

This is using `ConcurrentBag.TryTake`, not `ConcurrentDictionary.TryRemove`. The `TryTake` method on `ConcurrentBag` doesn't remove a specific element - it removes a random element from the bag regardless of what's in the `out` parameter.

So even though you've identified the correct record with `FirstOrDefault`, the `TryTake` method won't necessarily remove that specific record, potentially removing a different record instead.

That's why I suggested recreating the collection without the specific element as a safer alternative.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets from `Flow.Launcher/Storage/TopMostRecord.cs` (lines 1-190)

The entire file content is relevant as it defines the classes and methods related to managing "top most" records, including the `Remove` method in both `TopMostRecord` and `MultipleTopMostRecord` classes, which is the likely context of the user comment about `TryRemove` and element references.

Key relevant parts include:

- `TopMostRecord.Remove(Result result)` (lines ~56-66):
  ```csharp
  internal void Remove(Result result)
  {
      // origin query is null when user select the context menu item directly of one item from query list
      // in this case, we do not need to remove the record
      if (result.OriginQuery == null)
      {
          return;
      }

      records.Remove(result.OriginQuery.RawQuery, out _);
  }
  ```

- `MultipleTopMostRecord.Remove(Result result)` (lines ~111-134):
  ```csharp
  internal void Remove(Result result)
  {
      // origin query is null when user select the context menu item directly of one item from query list
      // in this case, we do not need to remove the record
      if (result.OriginQuery == null ||
          !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
      {
          return;
      }

      // remove the record from the bag
      var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
      if (recordToRemove != null)
      {
          value.TryTake(out recordToRemove);
      }

      // if the bag is empty, remove the bag from the dictionary
      if (value.IsEmpty)
      {
          records.TryRemove(result.OriginQuery.RawQuery, out _);
      }
  }
  ```

- The `AddOrUpdate` methods in both classes, which show how records are added or updated.

- The `IsTopMost` methods, which check if a result is considered top most.

- The `ConcurrentDictionaryConcurrentBagConverter` class (lines ~140-170), which handles JSON serialization of the `ConcurrentDictionary<string, ConcurrentBag<Record>>` used in `MultipleTopMostRecord`.

- The `Record` class and its `Equals(Result r)` method (lines ~172-190), which defines equality logic used in `Remove` and `AddOrUpdate`.

---

### Relevant snippets from `Flow.Launcher.Infrastructure/Storage/JsonStorage.cs`

- `Exists()` method (lines 48-51):
  ```csharp
  public bool Exists()
  {
      return File.Exists(FilePath);
  }
  ```

- `Delete()` method (lines 53-67):
  ```csharp
  public void Delete()
  {
      if (File.Exists(FilePath))
      {
          File.Delete(FilePath);
      }
      if (File.Exists(BackupFilePath))
      {
          File.Delete(BackupFilePath);
      }
      if (File.Exists(TempFilePath))
      {
          File.Delete(TempFilePath);
      }
  }
  ```

- `Save()` method (lines 207-218):
  ```csharp
  public void Save()
  {
      // User may delete the directory, so we need to check it
      FilesFolders.ValidateDirectory(DirectoryPath);

      var serialized = JsonSerializer.Serialize(Data,
          new JsonSerializerOptions { WriteIndented = true });

      File.WriteAllText(TempFilePath, serialized);

      AtomicWriteSetting();
  }
  ```

These methods are relevant because `FlowLauncherJsonStorageTopMostRecord` uses `FlowLauncherJsonStorage<T>` for persistence, and calls `Save()` and `Delete()` on these storage objects.

---

### Relevant snippets from `Flow.Launcher/ViewModel/MainViewModel.cs`

- `Save()` method (lines 1609-1614):
  ```csharp
  public void Save()
  {
      _historyItemsStorage.Save();
      _userSelectedRecordStorage.Save();
      _topMostRecord.Save();
  }
  ```

- `ContextMenuTopMost(Result result)` method (lines 1372-1410):
  ```csharp
  private Result ContextMenuTopMost(Result result)
  {
      Result menu;
      if (_topMostRecord.IsTopMost(result))
      {
          menu = new Result
          {
              Title = App.API.GetTranslation("cancelTopMostInThisQuery"),
              IcoPath = "Images\\down.png",
              PluginDirectory = Constant.ProgramDirectory,
              Action = _ =>
              {
                  _topMostRecord.Remove(result);
                  App.API.ShowMsg(App.API.GetTranslation("success"));
                  App.API.ReQuery();
                  return false;
              }
          };
      }
      else
      {
          menu = new Result
          {
              Title = App.API.GetTranslation("setAsTopMostInThisQuery"),
              IcoPath = "Images\\up.png",
              Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xeac2"),
              PluginDirectory = Constant.ProgramDirectory,
              Action = _ =>
              {
                  _topMostRecord.AddOrUpdate(result);
                  App.API.ShowMsg(App.API.GetTranslation("success"));
                  App.API.ReQuery();
                  return false;
              }
          };
      }

      return menu;
  }
  ```

This snippet shows how the `Remove` method is used in the UI context menu action, which is likely related to the user comment about `TryRemove` and element references.

---

### Summary

- The `Remove` method in `MultipleTopMostRecord` uses `TryRemove` on the dictionary only if the `ConcurrentBag` is empty after removing an element.
- The user comment "TryRemove gets the correct element reference" likely refers to this logic.
- The `Remove` method in `TopMostRecord` uses `Remove` on the dictionary directly.
- The `AddOrUpdate` and `IsTopMost` methods provide context on how records are managed.
- The storage class `FlowLauncherJsonStorage<T>` methods `Save`, `Delete`, and `Exists` are relevant for understanding persistence.
- The `ContextMenuTopMost` method in `MainViewModel` shows how these methods are invoked in the UI.

These snippets provide comprehensive context for understanding the behavior and usage of the `Remove` method and related data structures.

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:38:49.973Z
Learning: TryTake method in ConcurrentBag ignores the initial value of the output parameter and removes an arbitrary item from the bag, not the specific item referenced by the output parameter.
```

---

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:38:49.973Z
Learning: There's an important distinction between ConcurrentDictionary.TryRemove(key, out value) which removes the element with the specified key, and ConcurrentBag.TryTake(out result) which removes an arbitrary element from the bag without considering any input parameter.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Jack251970
PR: Flow-Launcher/Flow.Launcher#3500
File: Flow.Launcher/Storage/TopMostRecord.cs:191-197
Timestamp: 2025-05-01T05:35:21.827Z
Learning: ConcurrentBag.TryTake(out T) in C# removes an arbitrary item from the bag, not necessarily the one matching the out parameter. This can lead to removing the wrong element when trying to remove a specific item. A safer approach is to reconstruct the collection using filtering or use an ordered collection with proper removal methods.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-01T05:38:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,150 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var recordToRemove = value.FirstOrDefault(r => r.Equals(result));
+            if (recordToRemove != null)
+            {
+                value.TryTake(out recordToRemove);
+            }
+
+            // if the bag is empty, remove the bag from the dictionary
+            if (value.IsEmpty)
+            {
+                records.TryRemove(result.OriginQuery.RawQuery, out _);
+            }
+        }
+
+        internal void AddOrUpdate(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to add or update the record
+            if (result.OriginQuery == null)
+            {
+                return;
+            }
+
+            var record = new Record
+            {
+                PluginID = result.PluginID,
+                Title = result.Title,
+                SubTitle = result.SubTitle,
+                RecordKey = result.RecordKey
+            };
+            if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                // create a new bag if it does not exist
+                value = new ConcurrentBag<Record>()
+                {
+                    record
+                };
+                records.TryAdd(result.OriginQuery.RawQuery, value);
+            }
+            else
+            {
+                // add or update the record in the bag
+                if (value.Any(r => r.Equals(result)))
+                {
+                    // update the record
+                    var recordToUpdate = value.FirstOrDefault(r => r.Equals(result));
+                    if (recordToUpdate != null)
+                    {
+                        value.TryTake(out recordToUpdate);
+                        value.Add(record);
+                    }
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Copilot
body: Using ConcurrentBag for updating a specific record may not reliably remove the intended item because TryTake removes an arbitrary element. Consider using a different collection type that supports removal of a specific element if record order or accuracy is critical.
```suggestion
                // add or update the record in the list
                var recordToUpdate = value.FirstOrDefault(r => r.Equals(result));
                if (recordToUpdate != null)
                {
                    // update the record
                    value.Remove(recordToUpdate);
                    value.Add(record);
```
created_at: 2025-05-01T05:40:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +138,135 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentBagConverter))]
+        public ConcurrentDictionary<string, ConcurrentBag<Record>> records { get; private set; } = new();
+
+        internal void Add(TopMostRecord topMostRecord)
+        {
+            if (topMostRecord == null || topMostRecord.records.IsEmpty)
+            {
+                return;
+            }
+
+            foreach (var record in topMostRecord.records)
+            {
+                records.AddOrUpdate(record.Key, new ConcurrentBag<Record> { record.Value }, (key, oldValue) =>
+                {
+                    oldValue.Add(record.Value);
+                    return oldValue;
+                });
+            }
+        }
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the bag
+            var bag = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result)));
+            records[result.OriginQuery.RawQuery] = new ConcurrentBag<Record>(bag);
+
+            // if the bag is empty, remove the bag from the dictionary
+            if (value.IsEmpty)
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Copilot
body: After replacing the original bag with a new filtered bag, the emptiness check should be performed on the newly created bag instead of the old value. Consider updating the condition to check the new bag (e.g., if (bag.IsEmpty)).
```suggestion
            if (bag.IsEmpty)
```
created_at: 2025-05-01T05:44:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,14 +1,96 @@
-﻿using System.Collections.Concurrent;
+﻿using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
 using System.Text.Json.Serialization;
+using Flow.Launcher.Infrastructure.Storage;
 using Flow.Launcher.Plugin;
 
 namespace Flow.Launcher.Storage
 {
-    public class TopMostRecord
+    public class FlowLauncherJsonStorageTopMostRecord
+    {
+        private readonly FlowLauncherJsonStorage<MultipleTopMostRecord> _topMostRecordStorage;
+        private readonly MultipleTopMostRecord _topMostRecord;
+
+        public FlowLauncherJsonStorageTopMostRecord()
+        {
+            // Get old data & new data
+            var topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
+            _topMostRecordStorage = new FlowLauncherJsonStorage<MultipleTopMostRecord>();
+
+            // Check if data exist
+            var oldDataExist = topMostRecordStorage.Exists();
+            var newDataExist = _topMostRecordStorage.Exists();
+
+            // If new data exist, it means we have already migrated the old data
+            // So we can safely delete the old data and load the new data
+            if (newDataExist)
+            {
+                try
+                {
+                    topMostRecordStorage.Delete();
+                }
+                catch
+                {
+                    // Ignored - Flow will delete the old data during next startup
+                }
+                _topMostRecord = _topMostRecordStorage.Load();
+            }
+            // If new data does not exist and old data exist, we need to migrate the old data to the new data
+            else if (oldDataExist)
+            {
+                // Migrate old data to new data
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Copilot
body: [nitpick] Consider adding a log message or inline comment here to indicate that an old data migration is being performed. This would improve maintainability by clarifying the migration flow.
```suggestion
                // Migrate old data to new data
                Console.WriteLine("Migrating old data to new data format."); // Log the migration process
```
created_at: 2025-05-01T05:49:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1645,9 +1643,10 @@ public void UpdateResultView(ICollection<ResultsForUpdate> resultsForUpdates)
             {
                 foreach (var result in metaResults.Results)
                 {
-                    if (_topMostRecord.IsTopMost(result))
+                    var deviationIndex = _topMostRecord.GetTopMostIndex(result);
+                    if (deviationIndex != -1)
                     {
path: Flow.Launcher/ViewModel/MainViewModel.cs
user: login: Copilot
body: [nitpick] Consider adding an inline comment to clarify the rationale behind subtracting deviationIndex from Result.MaxScore, so future maintainers can easily understand the scoring adjustment logic.
```suggestion
                    {
                        // Adjust the score based on the result's position in the top-most list.
                        // A lower deviationIndex (closer to the top) results in a higher score.
```
created_at: 2025-05-02T02:30:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,14 +1,115 @@
-﻿using System.Collections.Concurrent;
+﻿using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
 using System.Text.Json.Serialization;
+using Flow.Launcher.Infrastructure.Storage;
 using Flow.Launcher.Plugin;
 
 namespace Flow.Launcher.Storage
 {
-    public class TopMostRecord
+    public class FlowLauncherJsonStorageTopMostRecord
+    {
+        private readonly FlowLauncherJsonStorage<MultipleTopMostRecord> _topMostRecordStorage;
+        private readonly MultipleTopMostRecord _topMostRecord;
+
+        public FlowLauncherJsonStorageTopMostRecord()
+        {
+#pragma warning disable CS0618 // Type or member is obsolete
+            // Get old data & new data
+            var topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
+#pragma warning restore CS0618 // Type or member is obsolete
+            _topMostRecordStorage = new FlowLauncherJsonStorage<MultipleTopMostRecord>();
+
+            // Check if data exist
+            var oldDataExist = topMostRecordStorage.Exists();
+            var newDataExist = _topMostRecordStorage.Exists();
+
+            // If new data exist, it means we have already migrated the old data
+            // So we can safely delete the old data and load the new data
+            if (newDataExist)
+            {
+                try
+                {
+                    topMostRecordStorage.Delete();
+                }
+                catch
+                {
+                    // Ignored - Flow will delete the old data during next startup
+                }
+                _topMostRecord = _topMostRecordStorage.Load();
+            }
+            // If new data does not exist and old data exist, we need to migrate the old data to the new data
+            else if (oldDataExist)
+            {
+                // Migrate old data to new data
+                _topMostRecord = _topMostRecordStorage.Load();
+                var oldTopMostRecord = topMostRecordStorage.Load();
+                if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return;
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Copilot
body: Returning early when legacy data is empty bypasses further migration; consider explicitly initializing _topMostRecord or adding a clarifying comment to confirm that this behavior is intentional.
```suggestion
                if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty)
                {
                    // Initialize _topMostRecord to ensure it is not null
                    _topMostRecord = new MultipleTopMostRecord();
                    return;
                }
```
created_at: 2025-05-02T02:30:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,12 +157,145 @@ internal void AddOrUpdate(Result result)
         }
     }
 
-    public class Record
+    /// <summary>
+    /// New data structure to support multiple top most records for the same query
+    /// </summary>
+    internal class MultipleTopMostRecord
+    {
+        [JsonInclude]
+        [JsonConverter(typeof(ConcurrentDictionaryConcurrentQueueConverter))]
+        public ConcurrentDictionary<string, ConcurrentQueue<Record>> records { get; private set; } = new();
+
+        internal bool IsTopMost(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return false;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            return value.Any(record => record.Equals(result));
+        }
+
+        internal int GetTopMostIndex(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to check if the result is top most
+            if (records.IsEmpty || result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return -1;
+            }
+
+            // since this dictionary should be very small (or empty) going over it should be pretty fast.
+            // since the latter items should be more recent, we should return the smaller index for score to subtract
+            // which can make them more topmost
+            // A, B, C => 2, 1, 0 => (max - 2), (max - 1), (max - 0)
+            var index = 0;
+            foreach (var record in value)
+            {
+                if (record.Equals(result))
+                {
+                    return value.Count - 1 - index;
+                }
+                index++;
+            }
+            return -1;
+        }
+
+        internal void Remove(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to remove the record
+            if (result.OriginQuery == null ||
+                !records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                return;
+            }
+
+            // remove the record from the queue
+            var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result)));
+            if (queue.IsEmpty)
+            {
+                // if the queue is empty, remove the queue from the dictionary
+                records.TryRemove(result.OriginQuery.RawQuery, out _);
+            }
+            else
+            {
+                // change the queue in the dictionary
+                records[result.OriginQuery.RawQuery] = queue;
+            }
+        }
+
+        internal void AddOrUpdate(Result result)
+        {
+            // origin query is null when user select the context menu item directly of one item from query list
+            // in this case, we do not need to add or update the record
+            if (result.OriginQuery == null)
+            {
+                return;
+            }
+
+            var record = new Record
+            {
+                PluginID = result.PluginID,
+                Title = result.Title,
+                SubTitle = result.SubTitle,
+                RecordKey = result.RecordKey
+            };
+            if (!records.TryGetValue(result.OriginQuery.RawQuery, out var value))
+            {
+                // create a new queue if it does not exist
+                value = new ConcurrentQueue<Record>();
+                value.Enqueue(record);
+                records.TryAdd(result.OriginQuery.RawQuery, value);
+            }
+            else
+            {
+                // add or update the record in the queue
+                var queue = new ConcurrentQueue<Record>(value.Where(r => !r.Equals(result))); // make sure we don't have duplicates
+                queue.Enqueue(record);
+                records[result.OriginQuery.RawQuery] = queue;
+            }
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Update logic may not match expected user behavior.**

When a user re-pins an already topmost item, they likely expect it to move to the front of the topmost items. However, the current implementation removes and re-adds the item to the end of the queue, which may not satisfy this expectation if items are displayed in queue order.

This is related to the concern raised in the PR discussion about the current implementation behaving more like "add to top-group" rather than a true "pin to top."


If you want the most recently pinned item to appear first, consider one of these approaches:
1. Use a different collection type like `LinkedList<T>` with a lock that allows for easy repositioning
2. Maintain a timestamp field in the `Record` class to allow sorting by recency
3. Use a data structure that naturally supports the desired ordering behavior

```csharp
// Example approach using a timestamp field
var record = new Record
{
    PluginID = result.PluginID,
    Title = result.Title,
    SubTitle = result.SubTitle,
    RecordKey = result.RecordKey,
    LastPinnedTimestamp = DateTime.UtcNow // New field
};
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-02T02:31:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,14 +1,115 @@
-﻿using System.Collections.Concurrent;
+﻿using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
 using System.Text.Json.Serialization;
+using Flow.Launcher.Infrastructure.Storage;
 using Flow.Launcher.Plugin;
 
 namespace Flow.Launcher.Storage
 {
-    public class TopMostRecord
+    public class FlowLauncherJsonStorageTopMostRecord
+    {
+        private readonly FlowLauncherJsonStorage<MultipleTopMostRecord> _topMostRecordStorage;
+        private readonly MultipleTopMostRecord _topMostRecord;
+
+        public FlowLauncherJsonStorageTopMostRecord()
+        {
+#pragma warning disable CS0618 // Type or member is obsolete
+            // Get old data & new data
+            var topMostRecordStorage = new FlowLauncherJsonStorage<TopMostRecord>();
+#pragma warning restore CS0618 // Type or member is obsolete
+            _topMostRecordStorage = new FlowLauncherJsonStorage<MultipleTopMostRecord>();
+
+            // Check if data exist
+            var oldDataExist = topMostRecordStorage.Exists();
+            var newDataExist = _topMostRecordStorage.Exists();
+
+            // If new data exist, it means we have already migrated the old data
+            // So we can safely delete the old data and load the new data
+            if (newDataExist)
+            {
+                try
+                {
+                    topMostRecordStorage.Delete();
+                }
+                catch
+                {
+                    // Ignored - Flow will delete the old data during next startup
+                }
+                _topMostRecord = _topMostRecordStorage.Load();
+            }
+            // If new data does not exist and old data exist, we need to migrate the old data to the new data
+            else if (oldDataExist)
+            {
+                // Migrate old data to new data
+                _topMostRecord = _topMostRecordStorage.Load();
+                var oldTopMostRecord = topMostRecordStorage.Load();
+                if (oldTopMostRecord == null || oldTopMostRecord.records.IsEmpty) return;
path: Flow.Launcher/Storage/TopMostRecord.cs
user: login: Jack251970
body: No need
created_at: 2025-05-02T02:33:10Z
author_association: MEMBER

PR comments:
user: login: gitstream-cm[bot]
created_at: 2025-05-01T05:28:26Z
body: 
🚨 **gitStream Monthly Automation Limit Reached** 🚨

Your organization has exceeded the number of pull requests allowed for automation with gitStream.
**Monthly PRs automated:** 274/250

To continue automating your PR workflows and unlock additional features, please [contact LinearB](https://linearb.io/book-a-demo).

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-01T05:29:25Z
body: 
# @check-spelling-bot Report
## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770410730/job/41469424237#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770410730/attempts/1#summary-41469424237) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE
user: login: gitstream-cm[bot]
created_at: 2025-05-01T05:30:22Z
body: 🥷 **Code experts: no user but you matched threshold 10** 
 
Jack251970 has most 👩‍💻 **activity** in the files. 
Jack251970 has most 🧠 **knowledge** in the files. 
 <details>
 <summary>See details</summary>


`Flow.Launcher.Infrastructure/Storage/JsonStorage.cs` 
 

Activity based on git-commit: 

 |  | Jack251970 |  
 | --- | --- | 
| MAY |   |  
| APR | 17 additions & 6 deletions |  
| MAR |   |  
| FEB | 1 additions & 1 deletions |  
| JAN |   |  
| DEC |   |  
 

Knowledge based on git-blame: 
 Jack251970: 8% 

`Flow.Launcher/Storage/TopMostRecord.cs` 
 

Activity based on git-commit: 

 |  | Jack251970 |  
 | --- | --- | 
| MAY |   |  
| APR |   |  
| MAR |   |  
| FEB |   |  
| JAN | 30 additions & 9 deletions |  
| DEC | 3 additions & 2 deletions |  
 

Knowledge based on git-blame: 
 Jack251970: 47% 

`Flow.Launcher/ViewModel/MainViewModel.cs` 
 

Activity based on git-commit: 

 |  | Jack251970 |  
 | --- | --- | 
| MAY |   |  
| APR | 35 additions & 28 deletions |  
| MAR | 695 additions & 628 deletions |  
| FEB | 63 additions & 21 deletions |  
| JAN | 17 additions & 21 deletions |  
| DEC | 59 additions & 63 deletions |  
 

Knowledge based on git-blame: 
 Jack251970: 30% 

</details>
 
 
 
 
To learn more about /:\ gitStream - [Visit our Docs](https://docs.gitstream.cm/) 
 
 

<automation id="gitstream/explain_code_experts"/>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-01T05:31:12Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

"""
## Walkthrough

This update introduces new methods to the generic JSON storage class for existence checking and deletion of storage files. It adds a new class, `FlowLauncherJsonStorageTopMostRecord`, to handle migration and management of top-most records, supporting multiple records per query and providing migration from the old single-record format. Serialization support for complex concurrent collections is implemented via a custom JSON converter. The main view model is updated to use the new top-most record storage class, reflecting the migration and new data structure.

## Changes

| File(s)                                                        | Change Summary |
|---------------------------------------------------------------|----------------|
| Flow.Launcher.Infrastructure/Storage/JsonStorage.cs           | Added `Exists()` and `Delete()` public methods to check for file existence and delete storage/backup/temp files if present. |
| Flow.Launcher/Storage/TopMostRecord.cs                        | Introduced `FlowLauncherJsonStorageTopMostRecord` for migration and management of top-most records, supporting multiple records per query. Added `MultipleTopMostRecord` and a custom JSON converter for concurrent collections. Updated `Record` class to use immutable properties and enhanced equality logic. Retained old `TopMostRecord` class as obsolete for migration. |
| Flow.Launcher/ViewModel/MainViewModel.cs                      | Replaced usage of generic top-most record storage with the new specialized class. Updated field, constructor, save method, and result scoring logic accordingly. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant MainViewModel
    participant TopMostRecordStorage
    participant JsonStorage

    MainViewModel->>TopMostRecordStorage: Instantiate FlowLauncherJsonStorageTopMostRecord
    TopMostRecordStorage->>JsonStorage: Check if old data exists (Exists())
    alt Old data exists
        TopMostRecordStorage->>JsonStorage: Load old TopMostRecord
        TopMostRecordStorage->>TopMostRecordStorage: Migrate to MultipleTopMostRecord
        TopMostRecordStorage->>JsonStorage: Delete old data (Delete())
    else New data exists
        TopMostRecordStorage->>JsonStorage: Load MultipleTopMostRecord
        TopMostRecordStorage->>JsonStorage: Delete old data if any (Delete())
    end
    MainViewModel->>TopMostRecordStorage: Save()
    TopMostRecordStorage->>JsonStorage: Save new MultipleTopMostRecord
```

## Possibly related PRs

- Flow-Launcher/Flow.Launcher#3194: Introduced `RecordKey` property in `Result` class and integrated it for unique record identification, closely related to the equality and migration logic in this PR.

## Suggested reviewers

- taooceros
- onesounds

## Poem

> 🐇 In burrows deep where data hides,  
> New records dance in multiple strides.  
> Old files vanish, migration’s done,  
> Scores adjust, the race is won.  
> JSON hops with queues in tow,  
> A rabbit’s code, swift as it goes! 🥕✨
"""

</details>

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1cf264a3a9c5d519482f5585e7fb54ad23ce9dfe and c64f3dfb303f6b2bc261644e5b2adb46408bfb16.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `Flow.Launcher/ViewModel/MainViewModel.cs` (4 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* Flow.Launcher/ViewModel/MainViewModel.cs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Flow-Launcher/Flow.Launcher&utm_content=3500):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-01T05:39:16Z
body: 
# @check-spelling-bot Report
## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770519846/job/41469685557#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770519846/attempts/1#summary-41469685557) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-01T05:40:09Z
body: > [!NOTE]
> Generated docstrings for this pull request at https://github.com/Flow-Launcher/Flow.Launcher/pull/3501
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-01T05:43:42Z
body: 
# @check-spelling-bot Report
## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770566559/job/41469798445#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770566559/attempts/1#summary-41469798445) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-01T05:48:26Z
body: 
# @check-spelling-bot Report
## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770617766/job/41469919387#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14770617766/attempts/1#summary-41469919387) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE
user: login: onesounds
created_at: 2025-05-01T06:25:51Z
body: Just in case, I’ll mention this now — I haven’t tested this PR and I'm not fully aware of its intent. But from a UX perspective, if there’s already a top-most item A and you add another one B, the display order should be B, then A.

When there are three top-most item — A, B, and C — the one that was activated most recently should be displayed in front. Otherwise, users may feel like their top-most action didn’t work, and they’ll be forced to deactivate other top-most item to perform their intended task.
author_association: CONTRIBUTOR
user: login: Jack251970
created_at: 2025-05-01T09:32:57Z
body: > Just in case, I’ll mention this now — I haven’t tested this PR and I'm not fully aware of its intent. But from a UX perspective, if there’s already a top-most item A and you add another one B, the display order should be B, then A.
> 
> When there are three top-most item — A, B, and C — the one that was activated most recently should be displayed in front. Otherwise, users may feel like their top-most action didn’t work, and they’ll be forced to deactivate other top-most item to perform their intended task.

Yeah, that makes sense. But I think the impact may be very small. Users typically will not set so much records as topmost, so ABC, CBA really does differ too much. The interface still allows them to select the correct option easily.
author_association: MEMBER
user: login: onesounds
created_at: 2025-05-01T12:09:24Z
body: Noooo~ ABC and CBA is different! (I actually tested this PR.) I use the Top Most feature all the time, and since our search often doesn’t show the best results first, a true “pin to top” is essential. This PR isn’t Top Most—it’s more like “add to top-group.” If there’s already a Top Most item, clicking Top Most again doesn’t move it to the first position, so you’d have to rename the menu—and it’s not intuitive.

Isn’t it just a matter of sorting by insertion order? If I pin A, then B, then C, C was last, so it should appear first—C, B, A. That’s trivial. Items with TopMost = true will still be grouped at the top, matching your PR’s intention. We can’t be sure there won’t be requests like “Please allow ordering within the top group.” If someone files a bug titled “I clicked Top Most but it sorted to second place,” replying “Oh, unpin the first item” would be ridiculous. Users won’t remember what they pinned—they have no way to know unless they try—so when they click Top Most, their only expectation is “show this at the very top.” It’s fine to group all pinned items together, but that group’s order needs to reflect the user’s intent (most recently pinned first).

Anyway, this implementation fails to meet the functional expectation of “Top Most,” so it can’t stay like this. If you’re going to add this feature, please change it so that items are displayed in the order in which “Top Most” was applied—i.e., the item most recently marked as Top Most should appear first. 
author_association: CONTRIBUTOR
user: login: Jack251970
created_at: 2025-05-02T01:51:21Z
body: > Noooo~ ABC and CBA is different! (I actually tested this PR.) I use the Top Most feature all the time, and since our search often doesn’t show the best results first, a true “pin to top” is essential. This PR isn’t Top Most—it’s more like “add to top-group.” If there’s already a Top Most item, clicking Top Most again doesn’t move it to the first position, so you’d have to rename the menu—and it’s not intuitive.
> 
> Isn’t it just a matter of sorting by insertion order? If I pin A, then B, then C, C was last, so it should appear first—C, B, A. That’s trivial. Items with TopMost = true will still be grouped at the top, matching your PR’s intention. We can’t be sure there won’t be requests like “Please allow ordering within the top group.” If someone files a bug titled “I clicked Top Most but it sorted to second place,” replying “Oh, unpin the first item” would be ridiculous. Users won’t remember what they pinned—they have no way to know unless they try—so when they click Top Most, their only expectation is “show this at the very top.” It’s fine to group all pinned items together, but that group’s order needs to reflect the user’s intent (most recently pinned first).
> 
> Anyway, this implementation fails to meet the functional expectation of “Top Most,” so it can’t stay like this. If you’re going to add this feature, please change it so that items are displayed in the order in which “Top Most” was applied—i.e., the item most recently marked as Top Most should appear first.

Well, I get it.
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-02T02:29:44Z
body: 
# @check-spelling-bot Report
## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14787457863/job/41518324460#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14787457863/attempts/1#summary-41518324460) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-02T02:33:08Z
body: 
# @check-spelling-bot Report
## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14787499503/job/41518447153#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14787499503/attempts/1#summary-41518447153) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-10T02:35:00Z
body: 
# @check-spelling-bot Report

## :red_circle: Please review
### See the [:open_file_folder: files](https://github.com/Flow-Launcher/Flow.Launcher/pull/3500/files/) view, the [:scroll:action log](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14941026229/job/41978039722#step:4:1),  or [:memo: job summary](https://github.com/Flow-Launcher/Flow.Launcher/actions/runs/14941026229/attempts/1#summary-41978039722) for details.

[:x: Errors and Warnings](https://docs.check-spelling.dev/Event-descriptions) | Count
-|-
[:x: forbidden-pattern](https://docs.check-spelling.dev/Event-descriptions#forbidden-pattern) | 22
[:warning: non-alpha-in-dictionary](https://docs.check-spelling.dev/Event-descriptions#non-alpha-in-dictionary) | 19

See [:x: Event descriptions](https://docs.check-spelling.dev/Event-descriptions) for more information.

<!-- Because only_check_changed_files is active, checking for obsolete items cannot be performed-->

<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->
<details><summary>If the flagged items are :exploding_head: false positives</summary>

If items relate to a ...
* binary file (or some other file you wouldn't want to check at all).

  Please add a file path to the `excludes.txt` file matching the containing file.

  File paths are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.

  `^` refers to the file's path from the root of the repository, so `^README\.md$` would exclude [README.md](
../tree/HEAD/README.md) (on whichever branch you're using).

* well-formed pattern.

  If you can write a [pattern](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns) that would match it,
  try adding it to the `patterns.txt` file.

  Patterns are Perl 5 Regular Expressions - you can [test](
https://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.

  Note that patterns can't match multiline strings.

</details>

author_association: NONE