fix(tui): fix persona cycling to use ctrl+p keybinding (#6425)
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 27s
CI / helm (pull_request) Successful in 23s
CI / build (pull_request) Successful in 27s
CI / quality (pull_request) Successful in 34s
CI / push-validation (pull_request) Successful in 37s
CI / typecheck (pull_request) Successful in 1m7s
CI / security (pull_request) Successful in 1m7s
CI / e2e_tests (pull_request) Successful in 3m51s
CI / integration_tests (pull_request) Successful in 4m9s
CI / unit_tests (pull_request) Successful in 5m28s
CI / docker (pull_request) Successful in 1m19s
CI / coverage (pull_request) Successful in 10m47s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 58m12s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 27s
CI / helm (pull_request) Successful in 23s
CI / build (pull_request) Successful in 27s
CI / quality (pull_request) Successful in 34s
CI / push-validation (pull_request) Successful in 37s
CI / typecheck (pull_request) Successful in 1m7s
CI / security (pull_request) Successful in 1m7s
CI / e2e_tests (pull_request) Successful in 3m51s
CI / integration_tests (pull_request) Successful in 4m9s
CI / unit_tests (pull_request) Successful in 5m28s
CI / docker (pull_request) Successful in 1m19s
CI / coverage (pull_request) Successful in 10m47s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Successful in 58m12s
Align TUI persona bindings with specification by cycling personas on tab and presets on ctrl+tab. Update persona state, help panel guidance, and Behave coverage to reflect new shortcuts.\n\nISSUES CLOSED: #6425
This commit is contained in:
@@ -5,8 +5,8 @@ These steps target uncovered lines in cleveragents/tui/app.py:
|
||||
- Lines 81-100: _TextualCleverAgentsTuiApp class definition + __init__
|
||||
- Lines 102-112: compose method
|
||||
- Lines 114-121: on_mount method
|
||||
- Lines 123-125: action_help method
|
||||
- Lines 127-129: action_cycle_preset method
|
||||
- Lines 123-131: action_help and action_cycle_persona methods
|
||||
- Lines 132-134: action_cycle_preset method
|
||||
- Lines 131-142: _refresh_persona_bar method
|
||||
- Lines 144-185: on_input_submitted (all branches)
|
||||
- Line 189: CleverAgentsTuiApp alias
|
||||
@@ -404,6 +404,11 @@ def step_persona_bar_refreshed(context):
|
||||
assert bar._text # non-empty after refresh
|
||||
|
||||
|
||||
@when("I call action_cycle_persona on the app")
|
||||
def step_call_action_cycle_persona(context):
|
||||
context._tui_app.action_cycle_persona()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _refresh_persona_bar (lines 131-142)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.tui.widgets.help_panel_overlay import (
|
||||
@@ -42,14 +44,16 @@ def step_toggle_help_for_context(context, context_name):
|
||||
|
||||
@then('the standalone help panel text should contain "{text}"')
|
||||
def step_standalone_help_panel_text_contains(context, text):
|
||||
assert text in context.help_panel._text, (
|
||||
pattern = rf"(?:^|\W){re.escape(text)}(?:\W|$)"
|
||||
assert re.search(pattern, context.help_panel._text), (
|
||||
f"Expected {text!r} in help panel text, got:\n{context.help_panel._text}"
|
||||
)
|
||||
|
||||
|
||||
@then('the standalone help panel text should not contain "{text}"')
|
||||
def step_standalone_help_panel_text_not_contains(context, text):
|
||||
assert text not in context.help_panel._text, (
|
||||
pattern = rf"(?:^|\W){re.escape(text)}(?:\W|$)"
|
||||
assert not re.search(pattern, context.help_panel._text), (
|
||||
f"Expected {text!r} NOT in help panel text, but found it in:\n{context.help_panel._text}"
|
||||
)
|
||||
|
||||
|
||||
@@ -132,6 +132,13 @@ def step_cycle_preset(context: Context, session_id: str) -> None:
|
||||
context.tui_state.cycle_preset(session_id)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@then('current preset for session "{session_id}" should be "{preset_name}"')
|
||||
def step_current_preset(context: Context, session_id: str, preset_name: str) -> None:
|
||||
assert context.tui_state.current_preset(session_id) == preset_name
|
||||
|
||||
@@ -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 (lines 126-130) ---
|
||||
|
||||
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,9 @@ 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 not contain "ctrl+t"
|
||||
|
||||
Scenario: help panel global keybindings match actual app BINDINGS
|
||||
Given a fresh help panel overlay
|
||||
|
||||
@@ -35,6 +35,21 @@ Feature: TUI persona system
|
||||
When I cycle persona preset for session "s1"
|
||||
Then current preset for session "s1" should be "default"
|
||||
|
||||
Scenario: Persona state cycles through personas using configured order
|
||||
Given a temporary TUI persona registry
|
||||
And I save a TUI persona named "builder" with actor "local/mock-default" and cycle order 2
|
||||
And I save a TUI persona named "reviewer" with actor "local/mock-default" and cycle order 1
|
||||
And I save a TUI persona named "support" with actor "local/mock-default"
|
||||
When I set active persona to "reviewer" for session "s1"
|
||||
And I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "builder"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "support"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "default"
|
||||
When I cycle persona for session "s1"
|
||||
Then active persona for session "s1" should be "reviewer"
|
||||
|
||||
Scenario: Malformed persona YAML does not break registry operations
|
||||
Given a temporary TUI persona registry
|
||||
And I save a TUI persona named "good" with actor "local/mock-default"
|
||||
|
||||
@@ -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.value)
|
||||
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()
|
||||
|
||||
@@ -43,6 +43,45 @@ class PersonaState:
|
||||
self.preset_by_session[session_id] = "default"
|
||||
return persona
|
||||
|
||||
def cycle_persona(self, session_id: str) -> Persona:
|
||||
"""Advance the active persona for *session_id* and return it.
|
||||
|
||||
Personas are cycled according to their configured ``cycle_order``.
|
||||
Entries with a ``cycle_order`` value greater than zero are ordered
|
||||
ascending by that value. Remaining personas retain a deterministic
|
||||
order by name. The cycle always includes the default persona to
|
||||
ensure the user can return to it, even if no explicit personas have
|
||||
been configured.
|
||||
"""
|
||||
|
||||
personas = self.registry.list_personas()
|
||||
if not personas:
|
||||
personas = [self.registry.ensure_default()]
|
||||
|
||||
# Guarantee the default persona participates in the cycle.
|
||||
default_persona = self.registry.ensure_default()
|
||||
persona_by_name = {persona.name: persona for persona in personas}
|
||||
persona_by_name.setdefault(default_persona.name, default_persona)
|
||||
|
||||
def _sort_key(item: Persona) -> tuple[int, int, str]:
|
||||
if item.cycle_order > 0:
|
||||
return (0, item.cycle_order, item.name.lower())
|
||||
is_default = 1 if item.name == "default" else 0
|
||||
return (1, is_default, item.name.lower())
|
||||
|
||||
ordered = sorted(persona_by_name.values(), key=_sort_key)
|
||||
|
||||
active_name = self.active_name(session_id)
|
||||
names = [persona.name for persona in ordered]
|
||||
if active_name not in names:
|
||||
next_persona = ordered[0]
|
||||
else:
|
||||
next_persona = ordered[(names.index(active_name) + 1) % len(ordered)]
|
||||
|
||||
persona = self.set_active_persona(session_id, next_persona.name)
|
||||
self.preset_by_session[session_id] = "default"
|
||||
return persona
|
||||
|
||||
def current_preset(self, session_id: str) -> str:
|
||||
if session_id not in self.preset_by_session:
|
||||
self.preset_by_session[session_id] = "default"
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user