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:
2026-04-28 13:08:05 +00:00
committed by Forgejo
parent 1129ea5121
commit e0e68fae60
3 changed files with 122 additions and 0 deletions
+51
View File
@@ -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"
+41
View File
@@ -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
+30
View File
@@ -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)