diff --git a/CHANGELOG.md b/CHANGELOG.md index 201938958..f33d105be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **TUI Persona Cycling Keybinding** (#6425): Fixed TUI keyboard bindings to match the + specification. The `tab` key now cycles through personas in the configured cycle order + (ascending `cycle_order`, with `cycle_order == 0` personas last). The `ctrl+tab` key + cycles through the current persona's argument presets. Previously, only `ctrl+t` was + bound to preset cycling and persona cycling via `tab` was not implemented at all. + Added `PersonaState.cycle_persona()` with deterministic sort ordering and + `action_cycle_persona()` to the TUI app. Updated the help panel overlay to reflect + the new bindings. + - **Trusted Automation Profile Description** (#9156): Corrected the `trusted` built-in automation profile description from `"Auto for most, human for apply and revert"` to `"Auto-exec, manual apply. Day-to-day development"` to match the specification at line 17197. - **Atomic `load_from_metadata` for Autonomy Guardrails** (#7504): Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index eba36f3bd..264d99644 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,6 +3,7 @@ * Aditya Chhabra * Brent E. Edwards * HAL 9000 +* HAL9001 * Hamza Khyari * Jeffrey Phillips Freeman * Luis Mendes @@ -22,6 +23,7 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes. * HAL 9000 has contributed automated bug fixes, security improvements, and migration safety enhancements including the migration prompt safe-default fix (#7503). * HAL 9000 has contributed CONTRIBUTING.md compliance improvements to the agent-evolution-worker (#8370): replaced hardcoded milestone references with dynamic Forgejo API queries and standardised label usage to `Type/Task`. +* HAL9001 has contributed code review and quality assurance for the TUI persona cycling keybinding fix (PR #6726 / issue #6425). * This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc. * HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system. * HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559). diff --git a/features/steps/tui_app_coverage_steps.py b/features/steps/tui_app_coverage_steps.py index fcf125b6c..7002de6c0 100644 --- a/features/steps/tui_app_coverage_steps.py +++ b/features/steps/tui_app_coverage_steps.py @@ -388,6 +388,14 @@ def step_help_panel_contains(context, text): assert text in panel._text, f"Expected '{text}' in '{panel._text}'" +# --------------------------------------------------------------------------- +# action_cycle_persona +# --------------------------------------------------------------------------- +@when("I call action_cycle_persona on the app") +def step_call_action_cycle_persona(context): + context._tui_app.action_cycle_persona() + + # --------------------------------------------------------------------------- # action_cycle_preset (lines 127-129) # --------------------------------------------------------------------------- diff --git a/features/steps/tui_persona_system_steps.py b/features/steps/tui_persona_system_steps.py index 10a59a1dc..b3208d23f 100644 --- a/features/steps/tui_persona_system_steps.py +++ b/features/steps/tui_persona_system_steps.py @@ -125,6 +125,13 @@ def step_active_persona(context: Context, session_id: str, persona_name: str) -> assert persona.name == persona_name +@when('I cycle persona for session "{session_id}"') +def step_cycle_persona(context: Context, session_id: str) -> None: + if not hasattr(context, "tui_state"): + context.tui_state = PersonaState(registry=context.tui_registry) + context.tui_state.cycle_persona(session_id) + + @when('I cycle persona preset for session "{session_id}"') def step_cycle_preset(context: Context, session_id: str) -> None: if not hasattr(context, "tui_state"): diff --git a/features/tui_app_coverage.feature b/features/tui_app_coverage.feature index 2392dca85..fa9b5c73a 100644 --- a/features/tui_app_coverage.feature +++ b/features/tui_app_coverage.feature @@ -47,7 +47,7 @@ Feature: TUI App Coverage 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 3 key bindings + And the app class should have 4 key bindings # --- compose method (lines 102-112) --- @@ -102,6 +102,15 @@ Feature: TUI App Coverage And I call action_help on the app Then the help panel should be hidden on mount + # --- action_cycle_persona method --- + + Scenario: action_cycle_persona cycles the persona 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_cycle_persona on the app + Then the persona bar content should be refreshed + # --- action_cycle_preset method (lines 127-129) --- Scenario: action_cycle_preset cycles the preset and refreshes the bar diff --git a/features/tui_help_panel_overlay_coverage.feature b/features/tui_help_panel_overlay_coverage.feature index b7b2a23c4..468b8526e 100644 --- a/features/tui_help_panel_overlay_coverage.feature +++ b/features/tui_help_panel_overlay_coverage.feature @@ -29,9 +29,10 @@ Feature: TUI Help Panel Overlay Coverage Scenario: help panel Main Screen keybindings match actual app BINDINGS Given a fresh help panel overlay When I show help for context "Main Screen" - Then the standalone help panel text should contain "ctrl+t" - And the standalone help panel text should not contain "ctrl+tab" - And the standalone help panel text should not contain "tab" + Then the standalone help panel text should contain "tab" + And the standalone help panel text should contain "ctrl+tab" + And the standalone help panel text should contain "Cycle to next persona" + And the standalone help panel text should contain "Cycle to next argument preset" Scenario: help panel global keybindings match actual app BINDINGS Given a fresh help panel overlay diff --git a/features/tui_persona_system.feature b/features/tui_persona_system.feature index 95605e5ac..3345c7737 100644 --- a/features/tui_persona_system.feature +++ b/features/tui_persona_system.feature @@ -61,3 +61,16 @@ Feature: TUI persona system | c | | d | Then TUI concurrent update should complete without errors + + Scenario: Persona state cycles through personas in cycle order + Given a temporary TUI persona registry + And I save a TUI persona named "reviewer" with actor "local/mock-default" and cycle order 1 + And I save a TUI persona named "builder" with actor "local/mock-default" and cycle order 2 + And I save a TUI persona named "default" with actor "local/mock-default" + When I set active persona to "reviewer" for session "cycle-s1" + And I cycle persona for session "cycle-s1" + Then active persona for session "cycle-s1" should be "builder" + When I cycle persona for session "cycle-s1" + Then active persona for session "cycle-s1" should be "default" + When I cycle persona for session "cycle-s1" + Then active persona for session "cycle-s1" should be "reviewer" diff --git a/src/cleveragents/tui/app.py b/src/cleveragents/tui/app.py index ca663fa8f..7c947718d 100644 --- a/src/cleveragents/tui/app.py +++ b/src/cleveragents/tui/app.py @@ -92,7 +92,8 @@ if _TEXTUAL_AVAILABLE: BINDINGS: ClassVar[list[tuple[str, str, str]]] = [ ("ctrl+q", "quit", "Quit"), ("f1", "help", "Help"), - ("ctrl+t", "cycle_preset", "Cycle Preset"), + ("tab", "cycle_persona", "Cycle Persona"), + ("ctrl+tab", "cycle_preset", "Cycle Preset"), ] def __init__( @@ -148,6 +149,10 @@ if _TEXTUAL_AVAILABLE: context_name = resolve_help_context(prompt.text) help_panel.toggle(context_name) + def action_cycle_persona(self) -> None: + self._persona_state.cycle_persona(self._session.session_id) + self._refresh_persona_bar() + def action_cycle_preset(self) -> None: self._persona_state.cycle_preset(self._session.session_id) self._refresh_persona_bar() diff --git a/src/cleveragents/tui/persona/state.py b/src/cleveragents/tui/persona/state.py index c11fa3fcb..f5efd93e7 100644 --- a/src/cleveragents/tui/persona/state.py +++ b/src/cleveragents/tui/persona/state.py @@ -8,6 +8,18 @@ from cleveragents.tui.persona.registry import PersonaRegistry from cleveragents.tui.persona.schema import Persona +def _persona_sort_key(persona: Persona) -> tuple[int, int, str]: + """Return a sort key for persona cycle ordering. + + Personas with cycle_order > 0 come first, sorted ascending. + Personas with cycle_order == 0 (including "default") come last, + sorted by name. + """ + if persona.cycle_order > 0: + return (0, persona.cycle_order, persona.name) + return (1, 0, persona.name) + + @dataclass(slots=True) class PersonaState: """Tracks active persona per TUI session.""" @@ -63,6 +75,29 @@ class PersonaState: self.preset_by_session[session_id] = next_name return next_name + def cycle_persona(self, session_id: str) -> Persona: + """Cycle to the next persona in the configured cycle order. + + Personas are sorted by cycle_order (ascending, >0 first) then by name. + The default persona is always last in the cycle. After cycling, the + preset is reset to ``"default"``. + """ + all_personas = self.registry.list_personas() + if not all_personas: + return self.active_persona(session_id) + sorted_personas = sorted(all_personas, key=_persona_sort_key) + current_name = self.active_name(session_id) + names = [p.name for p in sorted_personas] + if current_name not in names: + next_persona = sorted_personas[0] + else: + idx = names.index(current_name) + next_persona = sorted_personas[(idx + 1) % len(sorted_personas)] + self.active_by_session[session_id] = next_persona.name + self.preset_by_session[session_id] = "default" + self.registry.set_last_persona(next_persona.name) + return next_persona + def effective_arguments(self, session_id: str) -> dict[str, object]: persona = self.active_persona(session_id) preset = self.current_preset(session_id) diff --git a/src/cleveragents/tui/widgets/help_panel_overlay.py b/src/cleveragents/tui/widgets/help_panel_overlay.py index a0351580b..41b511e2e 100644 --- a/src/cleveragents/tui/widgets/help_panel_overlay.py +++ b/src/cleveragents/tui/widgets/help_panel_overlay.py @@ -32,7 +32,8 @@ _GLOBAL_ITEMS = ( _CONTEXT_ITEMS: dict[str, tuple[tuple[str, str], ...]] = { "Main Screen": ( ("enter", "Submit prompt"), - ("ctrl+t", "Cycle to next argument preset"), + ("tab", "Cycle to next persona"), + ("ctrl+tab", "Cycle to next argument preset"), ("@", "Open Reference Picker overlay"), ("/", "Open Slash Command overlay"), ("! / $", "Activate shell mode"),