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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user