From 0ab406e5ccff1b9ec2fe923eaa9f61a59d00940f Mon Sep 17 00:00:00 2001 From: CleverThis Date: Sun, 19 Apr 2026 08:40:13 +0000 Subject: [PATCH 1/3] fix(tui): add ctrl+r reload and f2/ctrl+, settings key bindings - Added ctrl+r (reload), f2 (settings), and ctrl+, (settings) to BINDINGS in src/cleveragents/tui/app.py - Implemented action_reload() to reset session transcript and refresh the persona bar - Implemented action_settings() to toggle the SettingsScreen overlay - Created new src/cleveragents/tui/widgets/settings_screen.py with SettingsScreen widget - Added SettingsScreen to compose() method and on_mount() initialization in app.py - Added Behave scenarios for ctrl+r reload and f2/ctrl+, settings in features/tui_app_coverage.feature - Added step definitions for the new scenarios in features/steps/tui_app_coverage_steps.py - Updated BINDINGS count from 3 to 6 in existing test scenario ISSUES CLOSED: #10475 --- features/steps/tui_app_coverage_steps.py | 87 +++++++++++++++++++ features/tui_app_coverage.feature | 42 ++++++++- src/cleveragents/tui/app.py | 19 ++++ .../tui/widgets/settings_screen.py | 78 +++++++++++++++++ 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/cleveragents/tui/widgets/settings_screen.py diff --git a/features/steps/tui_app_coverage_steps.py b/features/steps/tui_app_coverage_steps.py index 560564656..b36a3e18f 100644 --- a/features/steps/tui_app_coverage_steps.py +++ b/features/steps/tui_app_coverage_steps.py @@ -10,6 +10,8 @@ These steps target uncovered lines in cleveragents/tui/app.py: - Lines 131-142: _refresh_persona_bar method - Lines 144-185: on_input_submitted (all branches) - Line 189: CleverAgentsTuiApp alias +- action_reload: ctrl+r binding +- action_settings: f2 / ctrl+, binding """ import importlib @@ -119,12 +121,14 @@ def _install_mock_textual(context): import cleveragents.tui.widgets.persona_bar as pb_mod import cleveragents.tui.widgets.prompt as prompt_mod import cleveragents.tui.widgets.reference_picker as rp_mod + import cleveragents.tui.widgets.settings_screen as ss_mod import cleveragents.tui.widgets.slash_command_overlay as sco_mod importlib.reload(hp_mod) importlib.reload(pb_mod) importlib.reload(prompt_mod) importlib.reload(rp_mod) + importlib.reload(ss_mod) importlib.reload(sco_mod) import cleveragents.tui.app as app_mod @@ -147,12 +151,14 @@ def _restore_modules(context): import cleveragents.tui.widgets.persona_bar as pb_mod import cleveragents.tui.widgets.prompt as prompt_mod import cleveragents.tui.widgets.reference_picker as rp_mod + import cleveragents.tui.widgets.settings_screen as ss_mod import cleveragents.tui.widgets.slash_command_overlay as sco_mod importlib.reload(hp_mod) importlib.reload(pb_mod) importlib.reload(prompt_mod) importlib.reload(rp_mod) + importlib.reload(ss_mod) importlib.reload(sco_mod) import cleveragents.tui.app as app_mod @@ -683,3 +689,84 @@ def step_slash_overlay_all_commands(context) -> None: # (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" + + +# --------------------------------------------------------------------------- +# action_reload (ctrl+r binding) +# --------------------------------------------------------------------------- +@when("I call action_reload on the app") +def step_call_action_reload(context): + context._tui_app.action_reload() + + +@then("the session transcript should be empty after reload") +def step_session_transcript_empty(context): + assert context._tui_app._session.transcript == [] + + +# --------------------------------------------------------------------------- +# action_settings (f2 / ctrl+, binding) +# --------------------------------------------------------------------------- +@when("I call action_settings on the app") +def step_call_action_settings(context): + context._tui_app.action_settings() + + +@then("the settings screen should be visible") +def step_settings_screen_visible(context): + from cleveragents.tui.widgets.settings_screen import SettingsScreen + + settings = context._tui_app.query_one("#settings-screen", SettingsScreen) + assert settings.visible is True + + +@then("the settings screen should be hidden") +def step_settings_screen_hidden(context): + from cleveragents.tui.widgets.settings_screen import SettingsScreen + + settings = context._tui_app.query_one("#settings-screen", SettingsScreen) + assert settings.visible is False + + +@then('the settings screen text should contain "{text}"') +def step_settings_screen_contains(context, text): + from cleveragents.tui.widgets.settings_screen import SettingsScreen + + settings = context._tui_app.query_one("#settings-screen", SettingsScreen) + assert text in settings._text, f"Expected '{text}' in '{settings._text}'" + + +# --------------------------------------------------------------------------- +# BINDINGS assertions +# --------------------------------------------------------------------------- +@then("the app bindings should include ctrl+r for reload") +def step_bindings_include_ctrl_r(context): + bindings = context._tui_app.BINDINGS + keys = [b[0] for b in bindings] + assert "ctrl+r" in keys, f"ctrl+r not found in bindings: {keys}" + reload_binding = next(b for b in bindings if b[0] == "ctrl+r") + assert reload_binding[1] == "reload", ( + f"Expected action 'reload', got '{reload_binding[1]}'" + ) + + +@then("the app bindings should include f2 for settings") +def step_bindings_include_f2(context): + bindings = context._tui_app.BINDINGS + keys = [b[0] for b in bindings] + assert "f2" in keys, f"f2 not found in bindings: {keys}" + settings_binding = next(b for b in bindings if b[0] == "f2") + assert settings_binding[1] == "settings", ( + f"Expected action 'settings', got '{settings_binding[1]}'" + ) + + +@then("the app bindings should include ctrl+comma for settings") +def step_bindings_include_ctrl_comma(context): + bindings = context._tui_app.BINDINGS + keys = [b[0] for b in bindings] + assert "ctrl+comma" in keys, f"ctrl+comma not found in bindings: {keys}" + settings_binding = next(b for b in bindings if b[0] == "ctrl+comma") + assert settings_binding[1] == "settings", ( + f"Expected action 'settings', got '{settings_binding[1]}'" + ) diff --git a/features/tui_app_coverage.feature b/features/tui_app_coverage.feature index f9ce5f258..7f8669ad1 100644 --- a/features/tui_app_coverage.feature +++ b/features/tui_app_coverage.feature @@ -41,13 +41,13 @@ Feature: TUI App Coverage And the app should store the command router And the app should store the persona state - # --- CSS_PATH and BINDINGS class variables (lines 84-89) --- + # --- CSS_PATH and BINDINGS class variables --- Scenario: The Textual TUI app has correct class variables Given a mock command router and persona state When I instantiate the Textual TUI app Then the app class should have CSS_PATH set to "cleveragents.tcss" - And the app class should have 4 key bindings + And the app class should have 7 key bindings # --- compose method (lines 102-112) --- @@ -269,3 +269,41 @@ Feature: TUI App Coverage 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 + + # --- action_reload method (ctrl+r binding) --- + + Scenario: action_reload clears the session transcript and refreshes the bar + 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 call action_reload on the app + Then the session transcript should be empty after reload + And the persona bar content should be refreshed + + # --- action_settings method (f2 / ctrl+, binding) --- + + Scenario: action_settings opens the settings screen + 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 call action_settings on the app + Then the settings screen should be visible + And the settings screen text should contain "Settings" + + Scenario: action_settings toggles the settings screen closed + 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 call action_settings on the app + And I call action_settings on the app + Then the settings screen should be hidden + + # --- BINDINGS class variable updated count --- + + Scenario: The Textual TUI app has 7 key bindings including reload and settings + Given a mock command router and persona state + When I instantiate the Textual TUI app + Then the app class should have 7 key bindings + And the app bindings should include ctrl+r for reload + And the app bindings should include f2 for settings + And the app bindings should include ctrl+comma for settings diff --git a/src/cleveragents/tui/app.py b/src/cleveragents/tui/app.py index 7b4cfdfcb..a465f4c33 100644 --- a/src/cleveragents/tui/app.py +++ b/src/cleveragents/tui/app.py @@ -30,6 +30,7 @@ from cleveragents.tui.widgets.help_panel_overlay import ( from cleveragents.tui.widgets.persona_bar import PersonaBar from cleveragents.tui.widgets.prompt import PromptInput from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay +from cleveragents.tui.widgets.settings_screen import SettingsScreen from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay if TYPE_CHECKING: @@ -274,6 +275,9 @@ if _TEXTUAL_AVAILABLE: ("f1", "help", "Help"), ("ctrl+t", "cycle_preset", "Cycle Preset"), ("escape", "escape", "Close Overlay"), + ("ctrl+r", "reload", "Reload"), + ("f2", "settings", "Settings"), + ("ctrl+comma", "settings", "Settings"), ] def __init__( @@ -305,6 +309,7 @@ if _TEXTUAL_AVAILABLE: yield ReferencePickerOverlay(id="reference-picker") yield SlashCommandOverlay(id="slash-overlay") yield ActorSelectionOverlay(id="actor-selection") + yield SettingsScreen(id="settings-screen") yield PromptInput( placeholder="Type message, /command, or !shell ...", id="prompt" ) @@ -323,6 +328,8 @@ if _TEXTUAL_AVAILABLE: slash = self.query_one("#slash-overlay", SlashCommandOverlay) slash.set_commands("", slash_command_specs()) actor_overlay = self.query_one("#actor-selection", ActorSelectionOverlay) + settings = self.query_one("#settings-screen", SettingsScreen) + settings.hide() if first_run: actor_overlay.show() else: @@ -387,6 +394,18 @@ if _TEXTUAL_AVAILABLE: if callable(focus_method): focus_method() + def action_reload(self) -> None: + """Reload/refresh the current session or conversation.""" + self._session = SessionView( + session_id=self._session.session_id, transcript=[] + ) + self._refresh_persona_bar() + + def action_settings(self) -> None: + """Open or close the Settings screen.""" + settings = self.query_one("#settings-screen", SettingsScreen) + settings.toggle() + def _refresh_persona_bar(self) -> None: persona = self._persona_state.active_persona(self._session.session_id) preset = self._persona_state.current_preset(self._session.session_id) diff --git a/src/cleveragents/tui/widgets/settings_screen.py b/src/cleveragents/tui/widgets/settings_screen.py new file mode 100644 index 000000000..6433730a5 --- /dev/null +++ b/src/cleveragents/tui/widgets/settings_screen.py @@ -0,0 +1,78 @@ +"""Settings screen overlay for the TUI.""" + +from __future__ import annotations + +import importlib +from typing import Any + + +def _load_static_base() -> type[Any]: + try: + return importlib.import_module("textual.widgets").Static + except Exception: # pragma: no cover - optional dependency + + class _FallbackStatic: + def __init__(self, *args: object, **kwargs: object) -> None: + self._text = "" + + def update(self, text: str) -> None: + self._text = text + + return _FallbackStatic + + +_StaticBase = _load_static_base() + +_SETTINGS_CONTENT = ( + "Settings\n" + "\n" + " Theme Light / Dark / Auto\n" + " Font size Small / Medium / Large\n" + " Wrap width 80 / 100 / 120 / Off\n" + " Timestamps On / Off\n" + " Compact mode On / Off\n" + "\n" + "─────────────────────────────────────────────────────\n" + "escape Close settings\n" +) + + +class SettingsScreen(_StaticBase): + """Overlay that displays application settings. + + Opened via the ``f2`` or ``ctrl+,`` key bindings. Dismissed with + ``escape``. + """ + + def __init__(self, *args: object, **kwargs: object) -> None: + super().__init__(*args, **kwargs) + self._visible: bool = False + self._text: str = "" + + @property + def visible(self) -> bool: + """Return whether the settings screen is currently visible.""" + return self._visible + + def show(self) -> None: + """Display the settings screen.""" + self._visible = True + self._text = _SETTINGS_CONTENT + self.update(self._text) + + def hide(self) -> None: + """Hide the settings screen and clear its content.""" + self._visible = False + self._text = "" + self.update("") + + def toggle(self) -> bool: + """Toggle the settings screen visibility. + + Returns ``True`` when the screen ends visible and ``False`` when hidden. + """ + if self._visible: + self.hide() + return False + self.show() + return True -- 2.52.0 From dd8b473b7478cfa57219bc556722da0aeed557b0 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Sun, 19 Apr 2026 13:18:04 +0000 Subject: [PATCH 2/3] fix(tui): export SettingsScreen from widgets package --- src/cleveragents/tui/widgets/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cleveragents/tui/widgets/__init__.py b/src/cleveragents/tui/widgets/__init__.py index 058aa200c..73a5573e0 100644 --- a/src/cleveragents/tui/widgets/__init__.py +++ b/src/cleveragents/tui/widgets/__init__.py @@ -10,6 +10,7 @@ from cleveragents.tui.widgets.permission_question import ( from cleveragents.tui.widgets.persona_bar import PersonaBar from cleveragents.tui.widgets.prompt import PromptInput, PromptSubmitted from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay +from cleveragents.tui.widgets.settings_screen import SettingsScreen from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay from cleveragents.tui.widgets.thought_block import ThoughtBlockWidget @@ -22,6 +23,7 @@ __all__ = [ "PromptInput", "PromptSubmitted", "ReferencePickerOverlay", + "SettingsScreen", "SlashCommandOverlay", "ThoughtBlockWidget", "render_permission_question", -- 2.52.0 From f15780c5e59ac89efa5d5b48b8730398b1a8f869 Mon Sep 17 00:00:00 2001 From: controller-ci-rerun Date: Sat, 13 Jun 2026 04:14:59 -0400 Subject: [PATCH 3/3] chore: re-trigger CI [controller] -- 2.52.0