fix(tui): add on_input_changed handler for live overlay updates #10918
@@ -626,3 +626,60 @@ 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)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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 picker._text == "", (
|
||||
f"Expected reference picker to be empty after 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)
|
||||
# "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"
|
||||
|
||||
@@ -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 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 empty 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