diff --git a/features/steps/tdd_prompt_input_textarea_steps.py b/features/steps/tdd_prompt_input_textarea_steps.py new file mode 100644 index 000000000..bba3850db --- /dev/null +++ b/features/steps/tdd_prompt_input_textarea_steps.py @@ -0,0 +1,95 @@ +"""Step definitions for tdd_prompt_input_textarea.feature. + +Tests for: +- PromptInput inherits from TextArea (not Input) +- PromptInput.consume_text() returns PromptSubmitted with correct text +- PromptInput text is cleared after consume_text() +""" + +from __future__ import annotations + +from behave import then, when +from behave.runner import Context + +import cleveragents.tui.widgets.prompt as _prompt_mod +from cleveragents.tui.widgets.prompt import PromptInput, PromptSubmitted + + +@when("I inspect the PromptInput base class") +def step_inspect_prompt_input_base(context: Context) -> None: + """Load the PromptInput class for inspection.""" + context.prompt_input_cls = PromptInput + + +@then("PromptInput should be a subclass of TextArea") +def step_prompt_input_is_textarea(context: Context) -> None: + """Verify PromptInput inherits from textual.widgets.TextArea.""" + try: + import textual.widgets as tw + + assert issubclass(context.prompt_input_cls, tw.TextArea), ( + f"PromptInput must inherit from TextArea, " + f"but its MRO is: {[c.__name__ for c in context.prompt_input_cls.__mro__]}" + ) + except ImportError: # pragma: no cover + # Textual not installed — check the fallback base name + base_name = context.prompt_input_cls.__bases__[0].__name__ + assert "TextArea" in base_name, ( + f"PromptInput fallback base must be TextArea-like, got: {base_name}" + ) + + +@then("PromptInput should not be a subclass of Input") +def step_prompt_input_not_input(context: Context) -> None: + """Verify PromptInput does NOT inherit from textual.widgets.Input.""" + try: + import textual.widgets as tw + + assert not issubclass(context.prompt_input_cls, tw.Input), ( + "PromptInput must NOT inherit from Input (single-line widget). " + "Use TextArea for multi-line support." + ) + except ImportError: # pragma: no cover + # Textual not installed — check the fallback base name + base_name = context.prompt_input_cls.__bases__[0].__name__ + assert "Input" not in base_name or "TextArea" in base_name, ( + f"PromptInput fallback base must not be Input-like, got: {base_name}" + ) + + +@when("I create a PromptInput instance") +def step_create_prompt_input(context: Context) -> None: + """Create a PromptInput instance using the fallback (no Textual needed).""" + # Create a PromptInput instance directly + # The PromptInput class is already loaded with the appropriate base class + context.prompt_input = _prompt_mod.PromptInput() + context.prompt_submitted: PromptSubmitted | None = None + + +@when('I set the PromptInput text to "{text}"') +def step_set_prompt_input_text(context: Context, text: str) -> None: + """Set the text on the PromptInput instance.""" + context.prompt_input.text = text + + +@when("I call consume_text on the PromptInput") +def step_call_consume_text(context: Context) -> None: + """Call consume_text() and store the result.""" + context.prompt_submitted = context.prompt_input.consume_text() + + +@then('the PromptSubmitted text should be "{expected}"') +def step_prompt_submitted_text(context: Context, expected: str) -> None: + """Verify the PromptSubmitted payload has the expected text.""" + assert context.prompt_submitted is not None, "consume_text() returned None" + assert context.prompt_submitted.text == expected, ( + f"Expected text '{expected}', got '{context.prompt_submitted.text}'" + ) + + +@then("the PromptInput text should be empty after consume") +def step_prompt_input_empty_after_consume(context: Context) -> None: + """Verify the PromptInput text is cleared after consume_text().""" + assert context.prompt_input.text == "", ( + f"Expected empty text after consume_text(), got '{context.prompt_input.text}'" + ) diff --git a/features/steps/tui_app_coverage_steps.py b/features/steps/tui_app_coverage_steps.py index ad9227bd2..20e13c2b7 100644 --- a/features/steps/tui_app_coverage_steps.py +++ b/features/steps/tui_app_coverage_steps.py @@ -82,20 +82,21 @@ def _build_mock_textual(): def update(self, text): self._text = text - class MockInput: - """Minimal Input stand-in for the Textual base class.""" + class MockTextArea: + class MockTextArea: + """Minimal TextArea stand-in for the Textual base class.""" - value = "" + text = "" def __init__(self, *args, **kwargs): - self.value = "" + self.text = "" mock_textual_app.App = MockApp mock_textual_containers.Vertical = MockVertical mock_textual_widgets.Header = MockHeader mock_textual_widgets.Footer = MockFooter mock_textual_widgets.Static = MockStatic - mock_textual_widgets.Input = MockInput + mock_textual_widgets.TextArea = MockTextArea return { "textual": mock_textual, @@ -114,7 +115,7 @@ def _install_mock_textual(context): for key, mod in mocks.items(): sys.modules[key] = mod - # Reload widget modules so they pick up the mock Static/Input base class + # Reload widget modules so they pick up the mock Static/TextArea base class import cleveragents.tui.widgets.help_panel_overlay as hp_mod import cleveragents.tui.widgets.persona_bar as pb_mod import cleveragents.tui.widgets.prompt as prompt_mod @@ -142,7 +143,7 @@ def _restore_modules(context): else: sys.modules[key] = val - # Reload widget modules so they pick up the real Static/Input base class again + # Reload widget modules so they pick up the real Static/TextArea base class again import cleveragents.tui.widgets.help_panel_overlay as hp_mod import cleveragents.tui.widgets.persona_bar as pb_mod import cleveragents.tui.widgets.prompt as prompt_mod @@ -370,7 +371,7 @@ def step_set_prompt_text(context, text): from cleveragents.tui.widgets.prompt import PromptInput prompt = context._tui_app.query_one("#prompt", PromptInput) - prompt.value = text + prompt.text = text @then('the conversation widget should contain "{text}"') @@ -429,7 +430,7 @@ def _submit_text(context, text): from cleveragents.tui.widgets.prompt import PromptInput prompt = context._tui_app.query_one("#prompt", PromptInput) - prompt.value = text + prompt.text = text event = SimpleNamespace() context._tui_app.on_input_submitted(event) diff --git a/features/tdd_prompt_input_textarea.feature b/features/tdd_prompt_input_textarea.feature new file mode 100644 index 000000000..3f3719511 --- /dev/null +++ b/features/tdd_prompt_input_textarea.feature @@ -0,0 +1,23 @@ +@tdd_issue @tdd_issue_10411 +Feature: TDD Issue #10411 - PromptInput uses single-line Input instead of multi-line TextArea + As a CleverAgents TUI user + I want the PromptInput widget to use TextArea (multi-line) + So that I can enter multi-line prompts in the TUI + + This test captures the bug described in issue #10411. + PromptInput was inheriting from textual.widgets.Input (single-line) + instead of textual.widgets.TextArea (multi-line). + + @tdd_issue @tdd_issue_10411 + Scenario: PromptInput inherits from TextArea not Input + When I inspect the PromptInput base class + Then PromptInput should be a subclass of TextArea + And PromptInput should not be a subclass of Input + + @tdd_issue @tdd_issue_10411 + Scenario: PromptInput consume_text returns PromptSubmitted with text + When I create a PromptInput instance + And I set the PromptInput text to "hello world" + And I call consume_text on the PromptInput + Then the PromptSubmitted text should be "hello world" + And the PromptInput text should be empty after consume diff --git a/src/cleveragents/tui/app.py b/src/cleveragents/tui/app.py index 4a8c66dcf..ca663fa8f 100644 --- a/src/cleveragents/tui/app.py +++ b/src/cleveragents/tui/app.py @@ -145,7 +145,7 @@ if _TEXTUAL_AVAILABLE: def action_help(self) -> None: prompt = self.query_one("#prompt", PromptInput) help_panel = self.query_one("#help-panel", HelpPanelOverlay) - context_name = resolve_help_context(prompt.value) + context_name = resolve_help_context(prompt.text) help_panel.toggle(context_name) def action_cycle_preset(self) -> None: diff --git a/src/cleveragents/tui/widgets/prompt.py b/src/cleveragents/tui/widgets/prompt.py index f7d1943b1..e902af86b 100644 --- a/src/cleveragents/tui/widgets/prompt.py +++ b/src/cleveragents/tui/widgets/prompt.py @@ -7,21 +7,21 @@ from dataclasses import dataclass from typing import Any -def _load_input_base() -> type[Any]: +def _load_textarea_base() -> type[Any]: try: - return importlib.import_module("textual.widgets").Input + return importlib.import_module("textual.widgets").TextArea except Exception: # pragma: no cover - class _FallbackInput: - value = "" + class _FallbackTextArea: + text = "" def __init__(self, *args: object, **kwargs: object) -> None: - self.value = "" + self.text = "" - return _FallbackInput + return _FallbackTextArea -_InputBase = _load_input_base() +_TextAreaBase = _load_textarea_base() @dataclass(slots=True, frozen=True) @@ -31,10 +31,10 @@ class PromptSubmitted: text: str -class PromptInput(_InputBase): - """Input widget wrapper with helper methods.""" +class PromptInput(_TextAreaBase): + """TextArea widget wrapper with helper methods.""" def consume_text(self) -> PromptSubmitted: - text = self.value - self.value = "" + text = self.text + self.text = "" return PromptSubmitted(text=text)