From ddb8ec31b5e2f8e7daa535b9ec371a9d4a468b2d Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Mon, 13 Jul 2026 08:49:34 +0000 Subject: [PATCH] fix: do not leak the action keyword into the v2 query search string When the user has typed only the action keyword, Flow Launcher sends {'search': '', 'trimmedQuery': ''}. The falsy-or fallback to trimmedQuery handed the keyword to the plugin's query handler, since trimmedQuery (and its obsolete alias rawQuery) always include the action keyword while Query.search never does. Claude-Session: https://claude.ai/code/session_016BXjCFiQ5jaCjkaFmCZGyF --- pyflowlauncher/launcher.py | 4 +++- tests/integration/test_v2_protocol.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pyflowlauncher/launcher.py b/pyflowlauncher/launcher.py index 13cd2ab..c7b4254 100644 --- a/pyflowlauncher/launcher.py +++ b/pyflowlauncher/launcher.py @@ -118,7 +118,9 @@ async def run(self, dispatch: Callable[[str, list], Awaitable[Any]]) -> None: # The host sends ("query", [query, Settings.Inner]). if len(params) > 1 and isinstance(params[1], dict): self._settings = params[1] - params = [params[0].get('search') or params[0].get('trimmedQuery', '')] + # Query.search excludes the action keyword; trimmedQuery/rawQuery + # include it, so they must never be used as the search string. + params = [params[0].get('search') or ''] elif method != 'context_menu' and len(params) == 1 and isinstance(params[0], list): # Actions: the host calls RPC.InvokeAsync(method, argument: Parameters), # which wraps the whole Parameters list as one positional argument diff --git a/tests/integration/test_v2_protocol.py b/tests/integration/test_v2_protocol.py index bbc6056..5a4369e 100644 --- a/tests/integration/test_v2_protocol.py +++ b/tests/integration/test_v2_protocol.py @@ -129,6 +129,25 @@ def test_empty_search_string(self): ]) assert query_response(responses, 1)['result']['result'][0]['Title'] == 'Hello, World!' + def test_action_keyword_not_leaked_into_search(self): + """Typing only the action keyword sends {'search': '', 'trimmedQuery': 'kw'}; + the handler must receive '' — not fall back to the keyword-bearing trimmedQuery.""" + received = [] + plugin = Plugin(launcher=FlowLauncherV2()) + + @plugin.on_method + def query(q: str): + received.append(q) + yield Result(title="ok") + + run(plugin, [ + {'id': 1, 'method': 'query', + 'params': [{'search': '', 'trimmedQuery': 'kw', 'rawQuery': 'kw', + 'actionKeyword': 'kw'}, {}]}, + {'id': 2, 'method': 'close', 'params': []}, + ]) + assert received == [''] + class TestV2Lifecycle: