Skip to content

Support Assignment of One Action Keyword with Multiple Plugins#4295

Open
Jack251970 wants to merge 15 commits intodevfrom
action_keyword
Open

Support Assignment of One Action Keyword with Multiple Plugins#4295
Jack251970 wants to merge 15 commits intodevfrom
action_keyword

Conversation

@Jack251970
Copy link
Member

@Jack251970 Jack251970 commented Feb 26, 2026

CHANGES

  • Allows multiple plugins to share the same action keyword, removing the one-to-one limitation and improving plugin flexibility.

  • Updated IPublicAPI documentation and implementation to clarify that ActionKeywordAssigned is now only for compatibility with old plugins, and suppressed obsolete warnings in its usage.

TEST

Hello World Python & Plugin Indicator have the same action keyword ?.

image

Mark ActionKeywordRegistered as obsolete and always return false, reflecting support for multiple plugins per action keyword. Update IPublicAPI docs to clarify ActionKeywordAssigned is for legacy compatibility.
Expanded using directives for .NET collections and diagnostics.
Added ActionKeywordAssigned to PublicAPIInstance, using obsolete PluginManager.ActionKeywordRegistered with warning suppression.
Refactor non-global plugin storage to allow multiple plugins to share the same action keyword by using ConcurrentDictionary<string, List<PluginPair>>. Update all relevant methods to handle lists of plugins, ensure thread safety, and adjust the QueryBuilder logic accordingly. This change improves extensibility and flexibility in plugin management.
Copilot AI review requested due to automatic review settings February 26, 2026 08:56
@github-actions github-actions bot added this to the 2.2.0 milestone Feb 26, 2026
@Jack251970 Jack251970 changed the title Support Assign One Action Keyword with Multiple Plugins Support Assigning One Action Keyword with Multiple Plugins Feb 26, 2026
@prlabeler prlabeler bot added the enhancement New feature or request label Feb 26, 2026
@gitstream-cm
Copy link

gitstream-cm bot commented Feb 26, 2026

🥷 Code experts: jjw24

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

See details

Flow.Launcher.Core/Plugin/PluginManager.cs

Activity based on git-commit:

Jack251970 jjw24
FEB 12 additions & 0 deletions
JAN
DEC
NOV 3 additions & 3 deletions
OCT 130 additions & 31 deletions 8 additions & 8 deletions
SEP 1009 additions & 66 deletions

Knowledge based on git-blame:
Jack251970: 99%
jjw24: 1%

Flow.Launcher.Core/Plugin/QueryBuilder.cs

Activity based on git-commit:

Jack251970 jjw24
FEB
JAN
DEC
NOV 9 additions & 8 deletions
OCT
SEP

Knowledge based on git-blame:
jjw24: 86%
Jack251970: 14%

Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs

Activity based on git-commit:

Jack251970 jjw24
FEB
JAN
DEC
NOV
OCT 11 additions & 2 deletions
SEP 632 additions & 0 deletions

Knowledge based on git-blame:
Jack251970: 100%

Flow.Launcher/PublicAPIInstance.cs

Activity based on git-commit:

Jack251970 jjw24
FEB
JAN
DEC
NOV
OCT 4 additions & 1 deletions 23 additions & 12 deletions
SEP 654 additions & 28 deletions 630 additions & 0 deletions

Knowledge based on git-blame:
Jack251970: 98%

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

@gitstream-cm
Copy link

gitstream-cm bot commented Feb 26, 2026

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

@Jack251970 Jack251970 changed the title Support Assigning One Action Keyword with Multiple Plugins Support Assignment of One Action Keyword with Multiple Plugins Feb 26, 2026
@Jack251970 Jack251970 changed the title Support Assignment of One Action Keyword with Multiple Plugins Support Assignment of One Action Keyword with Multiple Plugins Feb 26, 2026
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Flow.Launcher.Core/Plugin/PluginManager.cs">

<violation number="1" location="Flow.Launcher.Core/Plugin/PluginManager.cs:391">
P2: `plugins` is a mutable List<PluginPair> that is modified under locks elsewhere, but here it’s enumerated without synchronization. Concurrent updates can throw during query execution. Take a snapshot of the list under the same lock before filtering.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates Flow Launcher’s action keyword routing to support multiple plugins sharing the same non-global action keyword, and keeps the old “keyword already assigned” check only for backward compatibility.

Changes:

  • Switch non-global action keyword storage from a single PluginPair to List<PluginPair> per keyword.
  • Update query parsing/selection logic to treat an action keyword as valid if any plugin for that keyword is enabled.
  • Mark the old registration check as obsolete and suppress obsolete warnings where it’s used via IPublicAPI.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
Flow.Launcher/PublicAPIInstance.cs Suppresses obsolete warning when calling the legacy action keyword assignment check.
Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs Adds remarks clarifying ActionKeywordAssigned is now for compatibility, but needs doc cleanup.
Flow.Launcher.Core/Plugin/QueryBuilder.cs Updates query-building to use keyword→multiple-plugins mapping and checks enabled plugins via LINQ.
Flow.Launcher.Core/Plugin/PluginManager.cs Implements keyword→multiple-plugins mapping and updates keyword add/remove/uninstall logic accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 211eb8a and baa3a69.

📒 Files selected for processing (3)
  • Flow.Launcher.Core/Plugin/PluginManager.cs
  • Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
  • Flow.Launcher.Test/QueryBuilderTest.cs

📝 Walkthrough

Walkthrough

Switched non-global plugin storage to allow multiple plugins per action keyword (values now List), updated registration/lookup/removal/uninstall paths, adapted QueryBuilder and tests to the new shape, and marked ActionKeywordRegistered/ActionKeywordAssigned obsolete for backward compatibility.

Changes

Cohort / File(s) Summary
Plugin Manager Core
Flow.Launcher.Core/Plugin/PluginManager.cs
Replaced _nonGlobalPlugins with ConcurrentDictionary<string, List<PluginPair>>. Updated Add/Remove/Register/Uninstall logic to manage per-key lists with per-key locking, null guards, metadata updates, and marked ActionKeywordRegistered obsolete.
Query Builder & Tests
Flow.Launcher.Core/Plugin/QueryBuilder.cs, Flow.Launcher.Test/QueryBuilderTest.cs
Changed QueryBuilder.Build(...) signature to accept Dictionary<string, List<PluginPair>>; added CheckPlugin(List<PluginPair>) to determine if any plugin is enabled; updated tests to supply List<PluginPair> values.
Public API surfaces
Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs, Flow.Launcher/PublicAPIInstance.cs
Annotated ActionKeywordAssigned(string)/ActionKeywordRegistered as [Obsolete(...)] with XML remarks; added pragmas to suppress obsolete warnings around calls.
Call-site updates
various callers (...)
Updated callers of GetNonGlobalPlugins() and related APIs to handle Dictionary<string, List<PluginPair>>, copy lists under lock, and iterate per-key lists rather than single PluginPair values.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant PM as PluginManager
  participant QB as QueryBuilder
  participant Plugin as Plugin(s)
  rect rgba(200,200,255,0.5)
  User->>PM: Register plugin with action keyword
  PM->>PM: Add PluginPair to _nonGlobalPlugins[key] (lock per-key)
  end
  rect rgba(200,255,200,0.5)
  User->>QB: Issue query with action keyword
  QB->>PM: GetNonGlobalPlugins()
  PM-->>QB: Dictionary<string, List<PluginPair>>
  QB->>QB: CheckPlugin(list) -> select enabled plugin(s)
  QB->>Plugin: Invoke selected plugin(s)
  Plugin-->>User: Return results
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • taooceros
  • jjw24
  • onesounds
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: enabling multiple plugins to share one action keyword, which aligns directly with the primary objective of this PR.
Description check ✅ Passed The description accurately explains the key changes including multi-plugin support for action keywords, updates to IPublicAPI documentation, and includes a concrete test case demonstrating the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch action_keyword

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
Flow.Launcher.Core/Plugin/PluginManager.cs (1)

1067-1073: Consider pruning empty keyword buckets during uninstall cleanup.

After removals, empty lists remain in _nonGlobalPlugins. Removing empty keys would keep lookups cleaner and avoid stale buckets over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Flow.Launcher.Core/Plugin/PluginManager.cs` around lines 1067 - 1073, After
removing plugin entries from each bucket in entriesToUpdate, prune any buckets
that become empty by removing their key from _nonGlobalPlugins; inside the loop
that iterates entriesToUpdate (where you currently call entry.Value.RemoveAll(p
=> p.Metadata.ID == plugin.ID)), check if entry.Value.Count == 0 and if so
remove the corresponding key from _nonGlobalPlugins (use the appropriate
synchronization used for _nonGlobalPlugins—e.g., lock(_nonGlobalPlugins) or its
concurrent remove method—to avoid race conditions). Ensure you reference the
same bucket via entry.Key when removing the key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Flow.Launcher.Core/Plugin/PluginManager.cs`:
- Around line 757-769: The add/update logic for plugin keywords in PluginManager
(_nonGlobalPlugins) prevents duplicate plugin entries but still appends
duplicate keywords into plugin metadata, causing drift; update the AddOrUpdate
lambdas (the block adding to _nonGlobalPlugins and the similar block around
lines 803-809) to deduplicate metadata when adding (e.g., check existing
metadata before appending or use a HashSet-style temporary collection) and to
clean up metadata when removing a plugin (remove only the plugin's keyword and
avoid leaving stale duplicates), preserving the existing lock on the list to
remain thread-safe and returning the updated collection.
- Around line 383-395: When fetching keyword matches from _nonGlobalPlugins,
filter out disabled plugins immediately so disabled siblings are never returned;
modify the branch that handles the TryGetValue result to set plugins =
plugins.Where(p => !PluginModified(p.Metadata.ID)) before applying the
dialogJump filter, then if dialogJump further restrict to p.Plugin is
IAsyncDialogJump and finally return the filtered collection (same pattern
applies to the GetGlobalPlugins() early-return branches—ensure they also include
!PluginModified(p.Metadata.ID) when building their result).
- Around line 589-592: GetNonGlobalPlugins currently returns a shallow copy of
_nonGlobalPlugins exposing the original mutable List<PluginPair> instances,
which can be concurrently mutated and cause InvalidOperationException; fix it by
taking a lock on the backing collection (_nonGlobalPlugins) while enumerating
and construct a new Dictionary where each value is a fresh snapshot list (e.g.,
new List<PluginPair>(existingList) or existingList.ToList()) so callers receive
independent lists; update the GetNonGlobalPlugins method to lock
_nonGlobalPlugins during the copy and return those cloned lists (referencing
GetNonGlobalPlugins, _nonGlobalPlugins, and PluginPair to locate the change).

In `@Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs`:
- Around line 296-299: The XML <remarks> text in IPublicAPI.cs is unclear and
grammatically incorrect; replace it with a concise, polished legacy-only
statement (update the <remarks> block for the affected method in the IPublicAPI
interface) such as: "Flow now allows a single action keyword to be associated
with multiple plugins; this method is retained only for legacy compatibility and
may be removed in a future release." Ensure the new wording explicitly states
legacy-only intent and is grammatically correct.

---

Nitpick comments:
In `@Flow.Launcher.Core/Plugin/PluginManager.cs`:
- Around line 1067-1073: After removing plugin entries from each bucket in
entriesToUpdate, prune any buckets that become empty by removing their key from
_nonGlobalPlugins; inside the loop that iterates entriesToUpdate (where you
currently call entry.Value.RemoveAll(p => p.Metadata.ID == plugin.ID)), check if
entry.Value.Count == 0 and if so remove the corresponding key from
_nonGlobalPlugins (use the appropriate synchronization used for
_nonGlobalPlugins—e.g., lock(_nonGlobalPlugins) or its concurrent remove
method—to avoid race conditions). Ensure you reference the same bucket via
entry.Key when removing the key.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10fcbb9 and b8bc7fa.

📒 Files selected for processing (4)
  • Flow.Launcher.Core/Plugin/PluginManager.cs
  • Flow.Launcher.Core/Plugin/QueryBuilder.cs
  • Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
  • Flow.Launcher/PublicAPIInstance.cs

Jack251970 and others added 6 commits February 26, 2026 17:05
GetNonGlobalPlugins now returns a new dictionary with copied lists,
preventing external modification of the internal _nonGlobalPlugins
collection. This change improves encapsulation and safeguards
plugin manager state integrity.
Replaced direct _nonGlobalPlugins field access with the GetNonGlobalPlugins() method to improve encapsulation and ensure up-to-date plugin data is used when retrieving non-global plugins by action keyword. No other logic was changed.
Wrap kvp.Value access in a lock when copying to nonGlobalPlugins
to prevent race conditions and ensure thread safety during
concurrent access.
Prevent duplicate action keywords by checking for existence before adding. Remove all instances of an old action keyword instead of just the first. Ensures action keyword lists remain unique and consistent.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Previously, the code only excluded modified plugins from the valid plugin list. This update adds an additional check to also exclude plugins marked as disabled in their metadata, ensuring that disabled plugins are not considered valid or processed further.
@coderabbitai coderabbitai bot removed the enhancement New feature or request label Feb 26, 2026
After removing plugins, also remove dictionary entries if their
associated lists become empty. This prevents unused empty lists
from accumulating and keeps the plugin manager's state clean.
Refactored QueryBuilderTest.cs to use Dictionary<string, List<PluginPair>> for nonGlobalPlugins, updating test cases to use lists of PluginPair. Also replaced empty dictionary instantiation with shorthand [] where appropriate.
@coderabbitai coderabbitai bot added the enhancement New feature or request label Feb 26, 2026
Extract plugin enabled check into a new CheckPlugin method, adding a lock for thread safety. Update the if statement to use this method instead of inline logic.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
Flow.Launcher.Test/QueryBuilderTest.cs (1)

34-49: ⚠️ Potential issue | 🟡 Minor

Test logic inconsistency: plugin is not actually disabled.

The test name ExclusivePluginQueryIgnoreDisabledTest suggests it should verify that disabled plugins are ignored, but the PluginMetadata created on line 39 doesn't set Disabled = true. The default value of Disabled is typically false, so the plugin is enabled and its action keyword should be recognized.

The test assertion on line 47 expects ActionKeyword to NOT equal ">", which would only be true if the plugin were disabled. Either the test data or the test assertions need correction.

💚 Proposed fix to actually disable the plugin
         var nonGlobalPlugins = new Dictionary<string, List<PluginPair>>
         {
-            { ">", new List<PluginPair>(){ new() { Metadata = new PluginMetadata { ActionKeywords = [">"] } } } }
+            { ">", new List<PluginPair>(){ new() { Metadata = new PluginMetadata { ActionKeywords = [">"], Disabled = true } } } }
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Flow.Launcher.Test/QueryBuilderTest.cs` around lines 34 - 49, The test
ExclusivePluginQueryIgnoreDisabledTest is currently creating a PluginMetadata
without marking it disabled, so the plugin is treated as enabled; update the
test data to set PluginMetadata.Disabled = true for the PluginPair (or otherwise
supply a disabled plugin) so QueryBuilder.Build sees the plugin as disabled and
the assertion ClassicAssert.AreNotEqual(">", q.ActionKeyword) becomes valid;
locate the PluginPair instantiation in the test and add Disabled = true to the
PluginMetadata before calling QueryBuilder.Build (verify QueryBuilder.Build and
Query properties like ActionKeyword, Search, TrimmedQuery, SearchTerms,
SecondToEndSearch behave as asserted).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Flow.Launcher.Test/QueryBuilderTest.cs`:
- Around line 34-49: The test ExclusivePluginQueryIgnoreDisabledTest is
currently creating a PluginMetadata without marking it disabled, so the plugin
is treated as enabled; update the test data to set PluginMetadata.Disabled =
true for the PluginPair (or otherwise supply a disabled plugin) so
QueryBuilder.Build sees the plugin as disabled and the assertion
ClassicAssert.AreNotEqual(">", q.ActionKeyword) becomes valid; locate the
PluginPair instantiation in the test and add Disabled = true to the
PluginMetadata before calling QueryBuilder.Build (verify QueryBuilder.Build and
Query properties like ActionKeyword, Search, TrimmedQuery, SearchTerms,
SecondToEndSearch behave as asserted).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2529efe and 17d675e.

📒 Files selected for processing (2)
  • Flow.Launcher.Core/Plugin/PluginManager.cs
  • Flow.Launcher.Test/QueryBuilderTest.cs

@Jack251970 Jack251970 requested a review from Copilot February 26, 2026 09:26
@coderabbitai coderabbitai bot removed the enhancement New feature or request label Feb 26, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Flow.Launcher.Core/Plugin/QueryBuilder.cs`:
- Around line 64-70: Remove the redundant lock in CheckPlugin: the method
currently locks on pluginPairs but callers use lists returned from
PluginManager.GetNonGlobalPlugins(), which already makes locked copies, so
thread safety is ensured at the source; update CheckPlugin (used by
QueryBuilder.Build) to simply return pluginPairs.Any(plugin =>
!plugin.Metadata.Disabled) without the lock, leaving synchronization
responsibility to PluginManager.GetNonGlobalPlugins().

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17d675e and 211eb8a.

📒 Files selected for processing (1)
  • Flow.Launcher.Core/Plugin/QueryBuilder.cs

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Jack251970 and others added 3 commits February 26, 2026 17:37
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
ExclusivePluginQueryIgnoreDisabledTest now sets the ">" plugin's Disabled property to true in PluginMetadata. This verifies that QueryBuilder correctly ignores disabled exclusive plugins.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Code Refactor enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants