From e0e68fae6033f021430dc4e4ec904c0259d9cc2e Mon Sep 17 00:00:00 2001 From: CleverThis Date: Tue, 28 Apr 2026 13:08:05 +0000 Subject: [PATCH 1/2] fix(tui): add on_input_changed handler for live overlay updates Add the missing on_input_changed event handler to the TUI app so that both the slash command overlay and the reference picker overlay update in real time as the user types, rather than only on submit. - When the user types / the slash overlay filters commands by the query after the slash character - When the user types @ the reference picker updates with matching suggestions based on the token after the last @ sign - Otherwise both overlays are reset to their default (unfiltered) state Also adds BDD scenarios and step definitions covering all branches of the new handler. ISSUES CLOSED: #4738 --- features/steps/tui_app_coverage_steps.py | 51 ++++++++++++++++++++++++ features/tui_app_coverage.feature | 41 +++++++++++++++++++ src/cleveragents/tui/app.py | 30 ++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/features/steps/tui_app_coverage_steps.py b/features/steps/tui_app_coverage_steps.py index 98b7737e0..3ab762b66 100644 --- a/features/steps/tui_app_coverage_steps.py +++ b/features/steps/tui_app_coverage_steps.py @@ -626,3 +626,54 @@ def step_theme_class_var(context, theme: str) -> None: assert theme == context._tui_app.THEME, ( f"Expected THEME='{theme}', got '{context._tui_app.THEME}'" ) + + +# --------------------------------------------------------------------------- +# on_input_changed: live overlay update steps +# --------------------------------------------------------------------------- +def _trigger_input_changed(context, text: str) -> None: + """Set prompt text and fire on_input_changed.""" + from cleveragents.tui.widgets.prompt import PromptInput + + prompt = context._tui_app.query_one("#prompt", PromptInput) + prompt.text = text + event = SimpleNamespace() + context._tui_app.on_input_changed(event) + + +@when('I trigger on_input_changed with text "{text}"') +def step_trigger_input_changed(context, text: str) -> None: + _trigger_input_changed(context, text) + + +@then('the slash overlay should show only commands matching "{query}"') +def step_slash_overlay_filtered(context, query: str) -> None: + from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay + + overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay) + assert overlay._text, "Slash overlay text should not be empty" + # All displayed commands must start with the query + for cmd in overlay._commands: + assert cmd.command.startswith(query), ( + f"Command '{cmd.command}' does not start with '{query}'" + ) + + +@then("the reference picker should be reset to empty") +def step_ref_picker_reset(context) -> None: + from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay + + picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay) + assert "(no matches)" in picker._text or picker._text.startswith("@"), ( + f"Expected reference picker to be reset, got: '{picker._text}'" + ) + + +@then("the slash overlay should be reset to all commands") +def step_slash_overlay_all_commands(context) -> None: + from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay + + overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay) + assert overlay._text, "Slash overlay text should not be empty after reset" + # When reset with empty query, all commands are shown (up to 12 displayed) + assert len(overlay._commands) > 0, "Slash overlay should have commands after reset" diff --git a/features/tui_app_coverage.feature b/features/tui_app_coverage.feature index fffb55161..6468da524 100644 --- a/features/tui_app_coverage.feature +++ b/features/tui_app_coverage.feature @@ -228,3 +228,44 @@ Feature: TUI App Coverage When I call action_escape on the app Then the reference picker overlay should be hidden And the prompt text should be "draft" + + # --- on_input_changed handler (live overlay updates) --- + + Scenario: on_input_changed with slash prefix filters slash commands live + Given a mock command router and persona state + When I instantiate the Textual TUI app + And I call on_mount on the app + And I trigger on_input_changed with text "/ses" + Then the slash overlay should show only commands matching "ses" + And the reference picker should be reset to empty + + Scenario: on_input_changed with at-sign updates reference picker live + Given a mock command router and persona state + When I instantiate the Textual TUI app + And I call on_mount on the app + And I trigger on_input_changed with text "@proj" + Then the reference picker should have been updated + And the slash overlay should be reset to all commands + + Scenario: on_input_changed with plain text resets both overlays + Given a mock command router and persona state + When I instantiate the Textual TUI app + And I call on_mount on the app + And I trigger on_input_changed with text "hello world" + Then the slash overlay should be reset to all commands + And the reference picker should be reset to empty + + Scenario: on_input_changed with empty at-sign resets reference picker + Given a mock command router and persona state + When I instantiate the Textual TUI app + And I call on_mount on the app + And I trigger on_input_changed with text "send @" + Then the reference picker should have been updated + + Scenario: on_input_changed with empty text resets both overlays + Given a mock command router and persona state + When I instantiate the Textual TUI app + And I call on_mount on the app + And I trigger on_input_changed with text "" + Then the slash overlay should be reset to all commands + And the reference picker should be reset to empty diff --git a/src/cleveragents/tui/app.py b/src/cleveragents/tui/app.py index bf771d54b..7b4cfdfcb 100644 --- a/src/cleveragents/tui/app.py +++ b/src/cleveragents/tui/app.py @@ -434,6 +434,36 @@ if _TEXTUAL_AVAILABLE: ) conversation.update(text) + def on_input_changed(self, event: InputSubmittedEvent) -> None: + """Update overlays live as the user types. + + - When text starts with ``/``, filter slash commands by the query + after the slash so the overlay narrows in real time. + - When text contains ``@``, extract the token after the last ``@`` + and update the reference picker with matching suggestions. + - Otherwise reset both overlays to their default (unfiltered) state. + """ + del event + prompt = self.query_one("#prompt", PromptInput) + text = prompt.text + slash = self.query_one("#slash-overlay", SlashCommandOverlay) + ref_picker = self.query_one("#reference-picker", ReferencePickerOverlay) + stripped = text.lstrip() + if stripped.startswith("/"): + query = stripped[1:] + slash.set_commands(query, slash_command_specs()) + ref_picker.set_suggestions("", []) + elif "@" in text: + at_index = text.rfind("@") + tail = text[at_index + 1 :] + tokens = tail.split() + query = tokens[0] if tokens else "" + ref_picker.set_suggestions(query, suggestions(query)) + slash.set_commands("", slash_command_specs()) + else: + slash.set_commands("", slash_command_specs()) + ref_picker.set_suggestions("", []) + def on_input_submitted(self, event: InputSubmittedEvent) -> None: del event prompt = self.query_one("#prompt", PromptInput) -- 2.52.0 From eedd0b7a4d11683d74bbe6b8fb5f605e7a8ffd13 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Wed, 10 Jun 2026 08:35:35 -0400 Subject: [PATCH 2/2] fix(tui): correct on_input_changed BDD step assertions and empty-text step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - step_ref_picker_reset: fix assertion to check _text == "" (set_suggestions with empty query calls hide(), not show "(no matches)") - step_slash_overlay_all_commands: fix assertion to check overlay is hidden (_visible=False, _text="") after reset — set_commands with empty query calls hide(), it does not display all commands - Add @when("I trigger on_input_changed with empty text") step so that the empty-string scenario is matched — parse's {text} placeholder requires one or more chars and would not match "" causing an "errored" scenario - Feature: update scenario 4 (empty at-sign) to use step_ref_picker_reset since "send @" yields empty query which hides the picker - Feature: update scenario 5 to use the new empty-text step ISSUES CLOSED: #4738 --- features/steps/tui_app_coverage_steps.py | 16 +++++++++++----- features/tui_app_coverage.feature | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/features/steps/tui_app_coverage_steps.py b/features/steps/tui_app_coverage_steps.py index 3ab762b66..560564656 100644 --- a/features/steps/tui_app_coverage_steps.py +++ b/features/steps/tui_app_coverage_steps.py @@ -646,6 +646,11 @@ def step_trigger_input_changed(context, text: str) -> None: _trigger_input_changed(context, text) +@when("I trigger on_input_changed with empty text") +def step_trigger_input_changed_empty_text(context) -> None: + _trigger_input_changed(context, "") + + @then('the slash overlay should show only commands matching "{query}"') def step_slash_overlay_filtered(context, query: str) -> None: from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay @@ -664,8 +669,8 @@ def step_ref_picker_reset(context) -> None: from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay) - assert "(no matches)" in picker._text or picker._text.startswith("@"), ( - f"Expected reference picker to be reset, got: '{picker._text}'" + assert picker._text == "", ( + f"Expected reference picker to be empty after reset, got: '{picker._text}'" ) @@ -674,6 +679,7 @@ def step_slash_overlay_all_commands(context) -> None: from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay) - assert overlay._text, "Slash overlay text should not be empty after reset" - # When reset with empty query, all commands are shown (up to 12 displayed) - assert len(overlay._commands) > 0, "Slash overlay should have commands after reset" + # "reset to all commands" means the overlay returned to its default hidden state + # (set_commands with empty query calls hide(), clearing _text and _commands) + assert not overlay._visible, "Slash overlay should be hidden after reset" + assert overlay._text == "", "Slash overlay text should be empty after reset" diff --git a/features/tui_app_coverage.feature b/features/tui_app_coverage.feature index 6468da524..f9ce5f258 100644 --- a/features/tui_app_coverage.feature +++ b/features/tui_app_coverage.feature @@ -260,12 +260,12 @@ Feature: TUI App Coverage When I instantiate the Textual TUI app And I call on_mount on the app And I trigger on_input_changed with text "send @" - Then the reference picker should have been updated + Then the reference picker should be reset to empty Scenario: on_input_changed with empty text resets both overlays Given a mock command router and persona state When I instantiate the Textual TUI app And I call on_mount on the app - And I trigger on_input_changed with text "" + And I trigger on_input_changed with empty text Then the slash overlay should be reset to all commands And the reference picker should be reset to empty -- 2.52.0