fix(tui): bind tab to persona cycling and ctrl+tab to preset cycling
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 1m2s
CI / helm (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 1m28s
CI / quality (pull_request) Successful in 1m23s
CI / push-validation (pull_request) Successful in 42s
CI / security (pull_request) Successful in 1m33s
CI / build (pull_request) Successful in 55s
CI / integration_tests (pull_request) Successful in 4m59s
CI / unit_tests (pull_request) Failing after 6m47s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Successful in 11m18s
CI / e2e_tests (pull_request) Failing after 16m54s
CI / status-check (pull_request) Has been cancelled

Fixes #6425. Implements spec-required TUI keybindings:
- tab: cycles through personas in configured cycle order
- ctrl+tab: cycles through current persona's argument presets

Changes:
- Add PersonaState.cycle_persona() with deterministic sort ordering
  (cycle_order > 0 ascending first, then cycle_order == 0 by name)
- Add action_cycle_persona() to TUI app
- Update BINDINGS: tab -> cycle_persona, ctrl+tab -> cycle_preset
- Update help panel overlay to reflect new bindings
- Update BDD tests to match new binding count (4) and new scenarios
- Add CHANGELOG entry under [Unreleased] -> Fixed
- Add HAL9001 to CONTRIBUTORS.md
This commit is contained in:
2026-05-05 18:20:27 +00:00
parent 3975ad244c
commit 3fe278ded4
10 changed files with 96 additions and 6 deletions
+9
View File
@@ -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
+2
View File
@@ -3,6 +3,7 @@
* Aditya Chhabra <aditya.chhabra@cleverthis.com>
* Brent E. Edwards <brent.edwards@cleverthis.com>
* HAL 9000 <hal9000@cleverthis.com>
* HAL9001 <hal9001@cleverthis.com>
* Hamza Khyari <hamza.khyari@cleverthis.com>
* Jeffrey Phillips Freeman <jeffrey.freeman@syncleus.com>
* Luis Mendes <luis.p.mendes@gmail.com>
@@ -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).
+8
View File
@@ -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)
# ---------------------------------------------------------------------------
@@ -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"):
+10 -1
View File
@@ -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
@@ -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
+13
View File
@@ -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"
+6 -1
View File
@@ -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()
+35
View File
@@ -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)
@@ -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"),