fix(tui): show command descriptions in SlashCommandOverlay alongside command names
CI / lint (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 48s
CI / quality (pull_request) Successful in 37s
CI / security (pull_request) Successful in 59s
CI / build (pull_request) Successful in 38s
CI / helm (pull_request) Successful in 24s
CI / unit_tests (pull_request) Failing after 7m1s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 16m16s
CI / integration_tests (pull_request) Failing after 23m13s
CI / coverage (pull_request) Successful in 11m18s
CI / status-check (pull_request) Failing after 2s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m54s
CI / lint (pull_request) Successful in 20s
CI / typecheck (pull_request) Successful in 48s
CI / quality (pull_request) Successful in 37s
CI / security (pull_request) Successful in 59s
CI / build (pull_request) Successful in 38s
CI / helm (pull_request) Successful in 24s
CI / unit_tests (pull_request) Failing after 7m1s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 16m16s
CI / integration_tests (pull_request) Failing after 23m13s
CI / coverage (pull_request) Successful in 11m18s
CI / status-check (pull_request) Failing after 2s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m54s
Update SlashCommandOverlay.set_commands() to accept list[SlashCommandSpec] instead of list[str], and render each entry as aligned columns: /command-name Description text Changes: - slash_command_overlay.py: update set_commands() signature to accept list[SlashCommandSpec]; render name + description with aligned padding using _COMMAND_COL_WIDTH = 28 characters for the name column - slash_catalog.py: add slash_command_specs() helper returning all specs - app.py: switch on_mount() to call slash_command_specs() instead of slash_command_names() so full spec objects are passed to the overlay - features/tui_slash_command_overlay_coverage.feature: add scenario verifying description rendering; update existing scenarios to use SlashCommandSpec objects via _make_specs() helper - features/steps/tui_slash_command_overlay_coverage_steps.py: update step implementations to build SlashCommandSpec objects from CSV names - features/tui_slash_overlay_descriptions.feature: new feature file with BDD scenarios covering description rendering and @tdd_expected_fail capture of the pre-fix name-only behaviour - features/steps/tui_slash_overlay_descriptions_steps.py: step defs for the new feature file - robot/tui_smoke.robot: add Slash Command Overlay Renders Descriptions integration test verifying descriptions appear in rendered overlay ISSUES CLOSED: #3437
This commit is contained in:
@@ -5,7 +5,7 @@ These steps target uncovered lines in slash_command_overlay.py:
|
||||
- Line 19: _FallbackStatic.update sets self._text = text
|
||||
- Lines 31-32: set_commands filtering logic (query vs empty query)
|
||||
- Line 34: lines = [f"/{query}"]
|
||||
- Lines 35-36: loop appending filtered commands
|
||||
- Lines 35-36: loop appending filtered commands with descriptions
|
||||
- Lines 37-38: "(no commands)" fallback when nothing matches
|
||||
- Line 39: self.update("\n".join(lines))
|
||||
"""
|
||||
@@ -16,9 +16,21 @@ from unittest.mock import patch
|
||||
from behave import given, then, when
|
||||
|
||||
import cleveragents.tui.widgets.slash_command_overlay as _sco_mod
|
||||
from cleveragents.tui.slash_catalog import SlashCommandSpec
|
||||
from cleveragents.tui.widgets.slash_command_overlay import _load_static_base
|
||||
|
||||
|
||||
def _make_specs(names_csv: str) -> list[SlashCommandSpec]:
|
||||
"""Build a list of SlashCommandSpec from a CSV of command names."""
|
||||
return [
|
||||
SlashCommandSpec(
|
||||
command=c.strip(), group="Test", description=f"Desc for {c.strip()}"
|
||||
)
|
||||
for c in names_csv.split(",")
|
||||
if c.strip()
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Background — force FallbackStatic so tests never need a live Textual app
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -81,16 +93,16 @@ def step_verify_text(context, expected):
|
||||
# ---------------------------------------------------------------------------
|
||||
@when('I call set_commands with query "{query}" and commands "{commands_csv}"')
|
||||
def step_call_set_commands(context, query, commands_csv):
|
||||
"""Call set_commands with a query and a CSV list of commands."""
|
||||
commands = [c.strip() for c in commands_csv.split(",") if c.strip()]
|
||||
context.overlay.set_commands(query, commands)
|
||||
"""Call set_commands with a query and a CSV list of commands (as SlashCommandSpec)."""
|
||||
specs = _make_specs(commands_csv)
|
||||
context.overlay.set_commands(query, specs)
|
||||
|
||||
|
||||
@when('I call set_commands with empty query and commands "{commands_csv}"')
|
||||
def step_call_set_commands_empty_query(context, commands_csv):
|
||||
"""Call set_commands with an empty query and a CSV list of commands."""
|
||||
commands = [c.strip() for c in commands_csv.split(",") if c.strip()]
|
||||
context.overlay.set_commands("", commands)
|
||||
"""Call set_commands with an empty query and a CSV list of commands (as SlashCommandSpec)."""
|
||||
specs = _make_specs(commands_csv)
|
||||
context.overlay.set_commands("", specs)
|
||||
|
||||
|
||||
@then('the overlay text should contain "{substring}"')
|
||||
@@ -128,9 +140,12 @@ def step_text_not_contains(context, substring):
|
||||
'I call set_commands with query "" and {count:d} commands prefixed with "{prefix}"'
|
||||
)
|
||||
def step_call_set_commands_many(context, count, prefix):
|
||||
"""Call set_commands with a large number of commands."""
|
||||
commands = [f"{prefix}{i}" for i in range(count)]
|
||||
context.overlay.set_commands("", commands)
|
||||
"""Call set_commands with a large number of commands (as SlashCommandSpec)."""
|
||||
specs = [
|
||||
SlashCommandSpec(command=f"{prefix}{i}", group="Test", description=f"Desc {i}")
|
||||
for i in range(count)
|
||||
]
|
||||
context.overlay.set_commands("", specs)
|
||||
|
||||
|
||||
@then("the overlay text should have exactly {expected:d} lines")
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Step definitions for tui_slash_overlay_descriptions.feature.
|
||||
|
||||
These steps verify that SlashCommandOverlay.set_commands() renders
|
||||
command descriptions alongside command names per ADR-046.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from behave import then, when
|
||||
|
||||
from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS
|
||||
|
||||
|
||||
@when("I initialise the overlay with real slash command specs")
|
||||
def step_init_with_real_specs(context):
|
||||
"""Call set_commands with the real SLASH_COMMAND_SPECS from the catalog."""
|
||||
context.overlay.set_commands("", list(SLASH_COMMAND_SPECS))
|
||||
|
||||
|
||||
@then("the overlay text should contain description for {command!r}")
|
||||
def step_overlay_contains_description(context, command):
|
||||
"""Verify the overlay text contains the description for the given command."""
|
||||
from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS
|
||||
|
||||
spec = next((s for s in SLASH_COMMAND_SPECS if s.command == command), None)
|
||||
assert spec is not None, f"Command {command!r} not found in SLASH_COMMAND_SPECS"
|
||||
assert spec.description in context.overlay._text, (
|
||||
f"Expected description '{spec.description}' in:\n{context.overlay._text}"
|
||||
)
|
||||
@@ -47,3 +47,11 @@ Feature: TUI Slash Command Overlay Coverage
|
||||
When I force the fallback static base to load
|
||||
Then the fallback class should be usable as a standalone object
|
||||
And calling update on the fallback instance should store the text
|
||||
|
||||
Scenario: set_commands renders descriptions alongside command names
|
||||
Given I have a SlashCommandOverlay instance
|
||||
When I call set_commands with query "" and commands "session:create,session:list"
|
||||
Then the overlay text should contain " /session:create"
|
||||
And the overlay text should contain "Desc for session:create"
|
||||
And the overlay text should contain " /session:list"
|
||||
And the overlay text should contain "Desc for session:list"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
Feature: TUI Slash Command Overlay Shows Descriptions
|
||||
Per ADR-046, the SlashCommandOverlay must display each command with its
|
||||
description alongside the command name in aligned columns.
|
||||
|
||||
Background:
|
||||
Given the slash command overlay module is imported
|
||||
|
||||
Scenario: Overlay renders command name and description in aligned columns
|
||||
Given I have a SlashCommandOverlay instance
|
||||
When I call set_commands with query "" and commands "session:create,settings"
|
||||
Then the overlay text should contain " /session:create"
|
||||
And the overlay text should contain "Desc for session:create"
|
||||
And the overlay text should contain " /settings"
|
||||
And the overlay text should contain "Desc for settings"
|
||||
|
||||
Scenario: Overlay renders descriptions for filtered commands
|
||||
Given I have a SlashCommandOverlay instance
|
||||
When I call set_commands with query "session" and commands "session:create,session:list,plan:use"
|
||||
Then the overlay text should contain " /session:create"
|
||||
And the overlay text should contain "Desc for session:create"
|
||||
And the overlay text should contain " /session:list"
|
||||
And the overlay text should contain "Desc for session:list"
|
||||
And the overlay text should not contain " /plan:use"
|
||||
|
||||
Scenario: Overlay uses real slash_command_specs from catalog
|
||||
Given I have a SlashCommandOverlay instance
|
||||
When I initialise the overlay with real slash command specs
|
||||
Then the overlay text should contain " /session:create"
|
||||
And the overlay text should contain "Create a new session tab"
|
||||
And the overlay text should contain " /settings"
|
||||
And the overlay text should contain "Open settings"
|
||||
|
||||
@tdd_expected_fail
|
||||
Scenario: TDD capture - overlay previously showed names only without descriptions
|
||||
Given I have a SlashCommandOverlay instance
|
||||
When I initialise the overlay with real slash command specs
|
||||
Then the overlay text should not contain "Create a new session tab"
|
||||
@@ -41,6 +41,30 @@ TUI Headless Includes Router Help Payload
|
||||
Should Contain ${result.stdout} "help"
|
||||
Should Contain ${result.stdout} /persona
|
||||
|
||||
Slash Command Overlay Renders Descriptions
|
||||
[Documentation] Verify SlashCommandOverlay.set_commands renders descriptions alongside names
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... import importlib
|
||||
... from unittest.mock import patch
|
||||
... import cleveragents.tui.widgets.slash_command_overlay as _sco_mod
|
||||
... from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS, slash_command_specs
|
||||
... with patch("importlib.import_module", side_effect=ImportError("no textual")):
|
||||
... importlib.reload(_sco_mod)
|
||||
... overlay = _sco_mod.SlashCommandOverlay()
|
||||
... overlay.set_commands("", slash_command_specs())
|
||||
... text = overlay._text
|
||||
... assert " /session:create" in text, f"Missing /session:create in: {text}"
|
||||
... assert "Create a new session tab" in text, f"Missing description in: {text}"
|
||||
... overlay2 = _sco_mod.SlashCommandOverlay()
|
||||
... overlay2.set_commands("settings", slash_command_specs())
|
||||
... text2 = overlay2._text
|
||||
... assert " /settings" in text2, f"Missing /settings in: {text2}"
|
||||
... assert "Open settings" in text2, f"Missing settings description in: {text2}"
|
||||
... print("slash-overlay-descriptions-ok")
|
||||
${result}= Run Process ${PYTHON} -c ${script} shell=False stderr=STDOUT
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} slash-overlay-descriptions-ok
|
||||
|
||||
TUI Help Panel Context Switching
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... from types import SimpleNamespace
|
||||
|
||||
@@ -11,7 +11,7 @@ from cleveragents.tui.first_run import create_default_persona_for_actor, is_firs
|
||||
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.slash_catalog import slash_command_specs
|
||||
from cleveragents.tui.widgets.actor_selection_overlay import ActorSelectionOverlay
|
||||
from cleveragents.tui.widgets.help_panel_overlay import (
|
||||
HelpPanelOverlay,
|
||||
@@ -130,7 +130,7 @@ if _TEXTUAL_AVAILABLE:
|
||||
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())
|
||||
slash.set_commands("", slash_command_specs())
|
||||
actor_overlay = self.query_one("#actor-selection", ActorSelectionOverlay)
|
||||
if first_run:
|
||||
actor_overlay.show()
|
||||
|
||||
@@ -93,6 +93,11 @@ def slash_command_names() -> list[str]:
|
||||
return [spec.command for spec in SLASH_COMMAND_SPECS]
|
||||
|
||||
|
||||
def slash_command_specs() -> list[SlashCommandSpec]:
|
||||
"""Return all slash command specs for the overlay (includes descriptions)."""
|
||||
return list(SLASH_COMMAND_SPECS)
|
||||
|
||||
|
||||
def slash_command_groups() -> set[str]:
|
||||
"""Return unique slash command groups represented in the catalog."""
|
||||
return {spec.group for spec in SLASH_COMMAND_SPECS}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cleveragents.tui.slash_catalog import SlashCommandSpec
|
||||
|
||||
|
||||
def _load_static_base() -> type[Any]:
|
||||
@@ -23,17 +26,23 @@ def _load_static_base() -> type[Any]:
|
||||
|
||||
_StaticBase = _load_static_base()
|
||||
|
||||
_COMMAND_COL_WIDTH = 28 # characters reserved for the command name column
|
||||
|
||||
|
||||
class SlashCommandOverlay(_StaticBase):
|
||||
"""Renderable list of slash command candidates."""
|
||||
|
||||
def set_commands(self, query: str, commands: list[str]) -> None:
|
||||
def set_commands(self, query: str, commands: list[SlashCommandSpec]) -> None:
|
||||
filtered = (
|
||||
[item for item in commands if item.startswith(query)] if query else commands
|
||||
[item for item in commands if item.command.startswith(query)]
|
||||
if query
|
||||
else list(commands)
|
||||
)
|
||||
lines = [f"/{query}"]
|
||||
for command in filtered[:12]:
|
||||
lines.append(f" /{command}")
|
||||
for spec in filtered[:12]:
|
||||
name_col = f" /{spec.command}"
|
||||
padding = max(1, _COMMAND_COL_WIDTH - len(name_col))
|
||||
lines.append(f"{name_col}{' ' * padding}{spec.description}")
|
||||
if len(lines) == 1:
|
||||
lines.append(" (no commands)")
|
||||
self.update("\n".join(lines))
|
||||
|
||||
Reference in New Issue
Block a user