feat(tui): implement help panel (F1) with context-sensitive help
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 18s
CI / helm (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 3m20s
CI / quality (pull_request) Successful in 3m44s
CI / typecheck (pull_request) Successful in 3m58s
CI / security (pull_request) Successful in 4m10s
CI / unit_tests (pull_request) Successful in 9m2s
CI / docker (pull_request) Successful in 1m30s
CI / coverage (pull_request) Successful in 9m3s
CI / e2e_tests (pull_request) Successful in 22m5s
CI / integration_tests (pull_request) Successful in 24m37s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (pull_request) Successful in 55m59s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 18s
CI / helm (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 3m20s
CI / quality (pull_request) Successful in 3m44s
CI / typecheck (pull_request) Successful in 3m58s
CI / security (pull_request) Successful in 4m10s
CI / unit_tests (pull_request) Successful in 9m2s
CI / docker (pull_request) Successful in 1m30s
CI / coverage (pull_request) Successful in 9m3s
CI / e2e_tests (pull_request) Successful in 22m5s
CI / integration_tests (pull_request) Successful in 24m37s
CI / status-check (pull_request) Successful in 2s
CI / benchmark-regression (pull_request) Successful in 55m59s
Add a dedicated TUI help overlay toggled by F1 and resolve its content from the current prompt mode for main-screen, slash-command, reference, and shell contexts. Extend Behave and Robot coverage for the new help-panel widget, app wiring, context switching, and toggle behavior. ISSUES CLOSED: #1013
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added a context-sensitive TUI help panel overlay toggled by `F1`, with
|
||||
help content that varies for main-screen, slash-command, reference, and
|
||||
shell prompt modes. Updated Behave and Robot coverage for help-panel
|
||||
rendering and mode switching. (#1013)
|
||||
|
||||
- Expanded the TUI slash command overlay catalog to include 67 commands across
|
||||
14 groups, aligned with the specification command reference for session,
|
||||
persona, scope, plan, project, registry/config, context, and utility flows.
|
||||
|
||||
@@ -115,11 +115,13 @@ def _install_mock_textual(context):
|
||||
sys.modules[key] = mod
|
||||
|
||||
# Reload widget modules so they pick up the mock Static/Input 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
|
||||
import cleveragents.tui.widgets.reference_picker as rp_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)
|
||||
@@ -141,11 +143,13 @@ def _restore_modules(context):
|
||||
sys.modules[key] = val
|
||||
|
||||
# Reload widget modules so they pick up the real Static/Input 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
|
||||
import cleveragents.tui.widgets.reference_picker as rp_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)
|
||||
@@ -327,6 +331,15 @@ def step_persona_bar_has_content(context):
|
||||
assert bar._text # non-empty
|
||||
|
||||
|
||||
@then("the help panel should be hidden on mount")
|
||||
def step_help_panel_hidden_on_mount(context):
|
||||
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
||||
|
||||
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
|
||||
assert panel.visible is False
|
||||
assert panel._text == ""
|
||||
|
||||
|
||||
@then("the reference picker should have suggestions initialised")
|
||||
def step_ref_picker_initialised(context):
|
||||
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
||||
@@ -352,6 +365,14 @@ def step_call_action_help(context):
|
||||
context._tui_app.action_help()
|
||||
|
||||
|
||||
@when('I set the prompt text to "{text}"')
|
||||
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
|
||||
|
||||
|
||||
@then('the conversation widget should contain "{text}"')
|
||||
def step_conversation_contains(context, text):
|
||||
MockStatic = context._tui_mock_static
|
||||
@@ -359,6 +380,14 @@ def step_conversation_contains(context, text):
|
||||
assert text in conv._text, f"Expected '{text}' in '{conv._text}'"
|
||||
|
||||
|
||||
@then('the help panel text should contain "{text}"')
|
||||
def step_help_panel_contains(context, text):
|
||||
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
||||
|
||||
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
|
||||
assert text in panel._text, f"Expected '{text}' in '{panel._text}'"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# action_cycle_preset (lines 127-129)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Step definitions for tui_help_panel_overlay_coverage.feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.tui.widgets.help_panel_overlay import (
|
||||
HelpPanelOverlay,
|
||||
resolve_help_context,
|
||||
)
|
||||
|
||||
|
||||
@when("I resolve help context for an empty prompt")
|
||||
def step_resolve_help_context_empty(context):
|
||||
context.resolved_help_context = resolve_help_context("")
|
||||
|
||||
|
||||
@when('I resolve help context for prompt text "{text}"')
|
||||
def step_resolve_help_context(context, text):
|
||||
context.resolved_help_context = resolve_help_context(text)
|
||||
|
||||
|
||||
@then('the resolved help context should be "{expected}"')
|
||||
def step_verify_resolved_help_context(context, expected):
|
||||
assert context.resolved_help_context == expected
|
||||
|
||||
|
||||
@given("a fresh help panel overlay")
|
||||
def step_fresh_help_panel_overlay(context):
|
||||
context.help_panel = HelpPanelOverlay()
|
||||
|
||||
|
||||
@when('I show help for context "{context_name}"')
|
||||
def step_show_help_for_context(context, context_name):
|
||||
context.help_panel.show_context(context_name)
|
||||
|
||||
|
||||
@when('I toggle help for context "{context_name}"')
|
||||
def step_toggle_help_for_context(context, context_name):
|
||||
context.help_panel.toggle(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
|
||||
|
||||
|
||||
@then("the help panel should be visible")
|
||||
def step_help_panel_visible(context):
|
||||
assert context.help_panel.visible is True
|
||||
|
||||
|
||||
@then("the help panel should be hidden")
|
||||
def step_help_panel_hidden(context):
|
||||
assert context.help_panel.visible is False
|
||||
assert context.help_panel._text == ""
|
||||
@@ -64,17 +64,43 @@ Feature: TUI App Coverage
|
||||
When I instantiate the Textual TUI app
|
||||
And I call on_mount on the app
|
||||
Then the persona bar should have content set
|
||||
And the help panel should be hidden on mount
|
||||
And the reference picker should have suggestions initialised
|
||||
And the slash overlay should have commands initialised
|
||||
|
||||
# --- action_help method (lines 123-125) ---
|
||||
|
||||
Scenario: action_help updates conversation with hotkey info
|
||||
Scenario: action_help opens a main-screen help panel
|
||||
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_help on the app
|
||||
Then the conversation widget should contain "Hotkeys"
|
||||
Then the help panel text should contain "Help: Main Screen"
|
||||
And the help panel text should contain "ctrl+q"
|
||||
|
||||
Scenario: action_help reflects slash command context
|
||||
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 set the prompt text to "/persona list"
|
||||
And I call action_help on the app
|
||||
Then the help panel text should contain "Help: Slash Commands"
|
||||
|
||||
Scenario: action_help reflects shell mode context
|
||||
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 set the prompt text to "!ls"
|
||||
And I call action_help on the app
|
||||
Then the help panel text should contain "Help: Shell Mode"
|
||||
|
||||
Scenario: action_help toggles the visible help panel off
|
||||
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_help on the app
|
||||
And I call action_help on the app
|
||||
Then the help panel should be hidden on mount
|
||||
|
||||
# --- action_cycle_preset method (lines 127-129) ---
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
Feature: TUI Help Panel Overlay Coverage
|
||||
Scenarios exercising the context-sensitive help overlay widget.
|
||||
|
||||
Scenario: resolve_help_context detects the active prompt mode
|
||||
When I resolve help context for an empty prompt
|
||||
Then the resolved help context should be "Main Screen"
|
||||
When I resolve help context for prompt text "/persona list"
|
||||
Then the resolved help context should be "Slash Commands"
|
||||
When I resolve help context for prompt text "inspect @README.md"
|
||||
Then the resolved help context should be "Reference Picker"
|
||||
When I resolve help context for prompt text "!ls"
|
||||
Then the resolved help context should be "Shell Mode"
|
||||
|
||||
Scenario: help panel overlay renders context-specific content
|
||||
Given a fresh help panel overlay
|
||||
When I show help for context "Main Screen"
|
||||
Then the standalone help panel text should contain "Help: Main Screen"
|
||||
And the standalone help panel text should contain "ctrl+q"
|
||||
And the standalone help panel text should contain "enter"
|
||||
|
||||
Scenario: help panel overlay toggle hides the current context
|
||||
Given a fresh help panel overlay
|
||||
When I toggle help for context "Slash Commands"
|
||||
Then the help panel should be visible
|
||||
And the standalone help panel text should contain "Help: Slash Commands"
|
||||
When I toggle help for context "Slash Commands"
|
||||
Then the help panel should be hidden
|
||||
@@ -40,3 +40,25 @@ TUI Headless Includes Router Help Payload
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} "help"
|
||||
Should Contain ${result.stdout} /persona
|
||||
|
||||
TUI Help Panel Context Switching
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... from types import SimpleNamespace
|
||||
... from features.steps import tui_app_coverage_steps as steps
|
||||
... context = SimpleNamespace(add_cleanup=lambda fn: None)
|
||||
... steps._install_mock_textual(context)
|
||||
... steps.step_create_mock_deps(context)
|
||||
... steps.step_instantiate_app(context)
|
||||
... steps.step_call_on_mount(context)
|
||||
... steps.step_set_prompt_text(context, "/persona list")
|
||||
... steps.step_call_action_help(context)
|
||||
... steps.step_help_panel_contains(context, "Help: Slash Commands")
|
||||
... steps.step_set_prompt_text(context, "!ls")
|
||||
... steps.step_call_action_help(context)
|
||||
... steps.step_help_panel_contains(context, "Help: Shell Mode")
|
||||
... steps._restore_modules(context)
|
||||
... steps._cleanup_tmpdir(context)
|
||||
... print("tui-help-panel-ok")
|
||||
${result}= Run Process ${PYTHON} -c ${script} shell=False stderr=STDOUT
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} tui-help-panel-ok
|
||||
|
||||
@@ -11,6 +11,10 @@ from cleveragents.tui.input.modes import InputMode, InputModeRouter
|
||||
from cleveragents.tui.input.reference_parser import suggestions
|
||||
from cleveragents.tui.persona.state import PersonaState
|
||||
from cleveragents.tui.slash_catalog import slash_command_names
|
||||
from cleveragents.tui.widgets.help_panel_overlay import (
|
||||
HelpPanelOverlay,
|
||||
resolve_help_context,
|
||||
)
|
||||
from cleveragents.tui.widgets.persona_bar import PersonaBar
|
||||
from cleveragents.tui.widgets.prompt import PromptInput
|
||||
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
||||
@@ -104,6 +108,7 @@ if _TEXTUAL_AVAILABLE:
|
||||
yield _Header(show_clock=True)
|
||||
with _Vertical(id="main-column"):
|
||||
yield _Static("CleverAgents TUI", id="conversation")
|
||||
yield HelpPanelOverlay(id="help-panel")
|
||||
yield ReferencePickerOverlay(id="reference-picker")
|
||||
yield SlashCommandOverlay(id="slash-overlay")
|
||||
yield PromptInput(
|
||||
@@ -114,14 +119,18 @@ if _TEXTUAL_AVAILABLE:
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._refresh_persona_bar()
|
||||
help_panel = self.query_one("#help-panel", HelpPanelOverlay)
|
||||
help_panel.hide()
|
||||
ref_picker = self.query_one("#reference-picker", ReferencePickerOverlay)
|
||||
ref_picker.set_suggestions("", [])
|
||||
slash = self.query_one("#slash-overlay", SlashCommandOverlay)
|
||||
slash.set_commands("", slash_command_names())
|
||||
|
||||
def action_help(self) -> None:
|
||||
conversation = self.query_one("#conversation", _Static)
|
||||
conversation.update("Hotkeys: F1 help, Ctrl+Q quit, Ctrl+T cycle preset")
|
||||
prompt = self.query_one("#prompt", PromptInput)
|
||||
help_panel = self.query_one("#help-panel", HelpPanelOverlay)
|
||||
context_name = resolve_help_context(prompt.value)
|
||||
help_panel.toggle(context_name)
|
||||
|
||||
def action_cycle_preset(self) -> None:
|
||||
self._persona_state.cycle_preset(self._session.session_id)
|
||||
|
||||
@@ -14,6 +14,14 @@ Screen {
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
#help-panel {
|
||||
height: auto;
|
||||
max-height: 12;
|
||||
padding: 0 1;
|
||||
border: round $primary;
|
||||
background: $panel-lighten-1;
|
||||
}
|
||||
|
||||
#reference-picker {
|
||||
height: auto;
|
||||
max-height: 8;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Widget collection for CleverAgents TUI."""
|
||||
|
||||
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
||||
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.slash_command_overlay import SlashCommandOverlay
|
||||
|
||||
__all__ = [
|
||||
"HelpPanelOverlay",
|
||||
"PersonaBar",
|
||||
"PromptInput",
|
||||
"PromptSubmitted",
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Context-sensitive help panel 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
|
||||
|
||||
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()
|
||||
|
||||
_GLOBAL_ITEMS = (
|
||||
("ctrl+q", "Quit TUI immediately"),
|
||||
("F1", "Toggle help panel"),
|
||||
("escape", "Close current overlay / return to prompt"),
|
||||
)
|
||||
|
||||
_CONTEXT_ITEMS: dict[str, tuple[tuple[str, str], ...]] = {
|
||||
"Main Screen": (
|
||||
("enter", "Submit prompt"),
|
||||
("tab", "Cycle to next persona"),
|
||||
("ctrl+tab", "Cycle to next argument preset"),
|
||||
("@", "Open Reference Picker overlay"),
|
||||
("/", "Open Slash Command overlay"),
|
||||
("! / $", "Activate shell mode"),
|
||||
),
|
||||
"Slash Commands": (
|
||||
("/", "Show slash command candidates"),
|
||||
("enter", "Submit slash command"),
|
||||
("escape", "Dismiss overlay and return to prompt"),
|
||||
),
|
||||
"Reference Picker": (
|
||||
("@", "Search reference candidates"),
|
||||
("enter", "Submit prompt with references"),
|
||||
("escape", "Dismiss overlay and return to prompt"),
|
||||
),
|
||||
"Shell Mode": (
|
||||
("enter", "Run shell command"),
|
||||
("tab", "Tab-complete file or directory path"),
|
||||
("backspace", "Exit shell mode at position 0"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def resolve_help_context(prompt_text: str) -> str:
|
||||
"""Resolve the help context from the current prompt text."""
|
||||
|
||||
stripped = prompt_text.lstrip()
|
||||
if stripped.startswith("/"):
|
||||
return "Slash Commands"
|
||||
if stripped.startswith(("!", "$")):
|
||||
return "Shell Mode"
|
||||
if "@" in prompt_text:
|
||||
return "Reference Picker"
|
||||
return "Main Screen"
|
||||
|
||||
|
||||
def render_help_panel(context_name: str) -> str:
|
||||
"""Render help content for the requested context."""
|
||||
|
||||
items = _CONTEXT_ITEMS.get(context_name, _CONTEXT_ITEMS["Main Screen"])
|
||||
lines = [f"Help: {context_name}", "", "Global"]
|
||||
for key, description in _GLOBAL_ITEMS:
|
||||
lines.append(f" {key:<10} {description}")
|
||||
lines.append("")
|
||||
lines.append(context_name)
|
||||
for key, description in items:
|
||||
lines.append(f" {key:<10} {description}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class HelpPanelOverlay(_StaticBase):
|
||||
"""Simple static overlay that toggles contextual help text."""
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self._context_name = ""
|
||||
self._visible = False
|
||||
self._text = ""
|
||||
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
"""Return whether help is currently displayed."""
|
||||
|
||||
return self._visible
|
||||
|
||||
@property
|
||||
def context_name(self) -> str:
|
||||
"""Return the currently rendered help context name."""
|
||||
|
||||
return self._context_name
|
||||
|
||||
def show_context(self, context_name: str) -> None:
|
||||
"""Show help content for a specific context."""
|
||||
|
||||
self._context_name = context_name
|
||||
self._visible = True
|
||||
self._text = render_help_panel(context_name)
|
||||
self.update(self._text)
|
||||
|
||||
def hide(self) -> None:
|
||||
"""Hide the help panel and clear its text."""
|
||||
|
||||
self._context_name = ""
|
||||
self._visible = False
|
||||
self._text = ""
|
||||
self.update("")
|
||||
|
||||
def toggle(self, context_name: str) -> bool:
|
||||
"""Toggle the panel for the given context.
|
||||
|
||||
Returns ``True`` when the panel ends visible and ``False`` when hidden.
|
||||
"""
|
||||
|
||||
if self._visible and self._context_name == context_name:
|
||||
self.hide()
|
||||
return False
|
||||
self.show_context(context_name)
|
||||
return True
|
||||
Reference in New Issue
Block a user