From 260d54a2f198334120f4766d733af81d11181032 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 5 Apr 2026 17:45:40 +0000 Subject: [PATCH 1/2] fix(tui): make /help command list all catalogued slash commands from SLASH_COMMAND_SPECS Replace the hardcoded help string in TuiCommandRouter.handle() with a dynamic lookup against SLASH_COMMAND_SPECS from slash_catalog.py. Changes: - Add _help_command(), _help_list_all(), _help_for_command() methods to TuiCommandRouter - /help (no args): iterates SLASH_COMMAND_SPECS, groups commands by namespace (sorted alphabetically), renders all 70 commands with descriptions in colon-namespaced format (e.g. persona:list) - /help : looks up the given command in SLASH_COMMAND_SPECS and renders its full help (group, description) - /help : returns 'Unknown command: /' message - /help /persona:list (with leading slash): strips the slash and resolves correctly - Import defaultdict and SLASH_COMMAND_SPECS at module level Tests: - Update tui_commands_coverage.feature: replace old exact-match scenario for help text with new dynamic-listing assertions - Add tui_commands_coverage_steps.py: new 'should contain' step definition - Add tui_help_command_full_catalog.feature: 12 BDD scenarios covering /help no-args, /help , /help , namespace grouping, colon-namespaced format, and regression against old hardcoded string - Add tui_help_command_full_catalog_steps.py: step definitions for the new feature (all-commands check, not-equal assertion) - Add robot/tui_help_command.robot: 5 Robot Framework integration tests verifying the help command via direct Python invocation and headless TUI startup Closes #3434 --- **Automated by CleverAgents Bot** Supervisor: Implementation | Agent: ca-issue-worker --- features/steps/tui_commands_coverage_steps.py | 23 +++-- .../tui_help_command_full_catalog_steps.py | 33 ++++++++ features/tui_commands_coverage.feature | 6 +- .../tui_help_command_full_catalog.feature | 71 ++++++++++++++++ robot/tui_help_command.robot | 84 +++++++++++++++++++ src/cleveragents/tui/commands.py | 39 ++++++++- 6 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 features/steps/tui_help_command_full_catalog_steps.py create mode 100644 features/tui_help_command_full_catalog.feature create mode 100644 robot/tui_help_command.robot diff --git a/features/steps/tui_commands_coverage_steps.py b/features/steps/tui_commands_coverage_steps.py index a05af3cc7..3870be605 100644 --- a/features/steps/tui_commands_coverage_steps.py +++ b/features/steps/tui_commands_coverage_steps.py @@ -74,8 +74,8 @@ def step_router_with_mock_deps(context): context.registry = FakePersonaRegistry() context.state = FakePersonaState() context.router = TuiCommandRouter( - persona_registry=context.registry, - persona_state=context.state, + persona_registry=context.registry, # type: ignore[arg-type] + persona_state=context.state, # type: ignore[arg-type] ) @@ -85,8 +85,8 @@ def step_router_with_set_support(context): context.registry = FakePersonaRegistry() context.state = FakePersonaState() context.router = TuiCommandRouter( - persona_registry=context.registry, - persona_state=context.state, + persona_registry=context.registry, # type: ignore[arg-type] + persona_state=context.state, # type: ignore[arg-type] ) @@ -98,8 +98,8 @@ def step_router_with_two_personas(context): ) context.state = FakePersonaState() context.router = TuiCommandRouter( - persona_registry=context.registry, - persona_state=context.state, + persona_registry=context.registry, # type: ignore[arg-type] + persona_state=context.state, # type: ignore[arg-type] ) @@ -109,8 +109,8 @@ def step_router_with_empty_registry(context): context.registry = FakePersonaRegistry(_personas=[]) context.state = FakePersonaState() context.router = TuiCommandRouter( - persona_registry=context.registry, - persona_state=context.state, + persona_registry=context.registry, # type: ignore[arg-type] + persona_state=context.state, # type: ignore[arg-type] ) @@ -148,6 +148,13 @@ def step_handle_result_starts_with(context, prefix): ) +@then('the handle result should contain "{substring}"') +def step_handle_result_contains(context, substring): + assert substring in context.handle_result, ( + f"Expected {substring!r} in result, got {context.handle_result!r}" + ) + + # --------------------------------------------------------------------------- # run_tui() headless scenario # --------------------------------------------------------------------------- diff --git a/features/steps/tui_help_command_full_catalog_steps.py b/features/steps/tui_help_command_full_catalog_steps.py new file mode 100644 index 000000000..917ca28d2 --- /dev/null +++ b/features/steps/tui_help_command_full_catalog_steps.py @@ -0,0 +1,33 @@ +"""Step definitions for tui_help_command_full_catalog.feature. + +Tests that TuiCommandRouter.handle('help') dynamically lists all commands +from SLASH_COMMAND_SPECS and that /help returns command-specific help. +""" + +from __future__ import annotations + +from behave import then + +from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS + + +@then("the help result contains all 70 catalogued commands") +def step_help_result_contains_all_commands(context: object) -> None: + """Verify that every command in SLASH_COMMAND_SPECS appears in the help output.""" + result: str = context.handle_result # type: ignore[attr-defined] + missing = [ + spec.command for spec in SLASH_COMMAND_SPECS if spec.command not in result + ] + assert not missing, ( + f"The following {len(missing)} commands were missing from /help output: " + + ", ".join(missing) + ) + + +@then('the handle result should not be "{unexpected}"') +def step_handle_result_not_equal(context: object, unexpected: str) -> None: + """Verify the handle result is NOT the given string.""" + result: str = context.handle_result # type: ignore[attr-defined] + assert result != unexpected, ( + f"Expected result to differ from {unexpected!r}, but got the same string" + ) diff --git a/features/tui_commands_coverage.feature b/features/tui_commands_coverage.feature index 02e45d03b..14414d073 100644 --- a/features/tui_commands_coverage.feature +++ b/features/tui_commands_coverage.feature @@ -27,10 +27,12 @@ Feature: TUI Command Router and run_tui coverage When I call handle with raw input "session show" Then the handle result should be "Current session: test-session" - Scenario: handle returns help text + Scenario: handle returns help text listing all commands Given a TuiCommandRouter with a mock registry and state When I call handle with raw input "help" - Then the handle result should be "Commands: /persona, /session, /help" + Then the handle result should contain "Available slash commands:" + And the handle result should contain "persona:list" + And the handle result should contain "session:create" Scenario: handle returns unknown command for unrecognised input Given a TuiCommandRouter with a mock registry and state diff --git a/features/tui_help_command_full_catalog.feature b/features/tui_help_command_full_catalog.feature new file mode 100644 index 000000000..9cec5cbc6 --- /dev/null +++ b/features/tui_help_command_full_catalog.feature @@ -0,0 +1,71 @@ +Feature: TUI /help command lists all catalogued slash commands + The /help slash command must dynamically list all commands from + SLASH_COMMAND_SPECS rather than returning a hardcoded string. + It must also support /help for command-specific help. + + Background: + Given the TUI commands module is imported + And a TuiCommandRouter with a mock registry and state + + # ---------- /help with no arguments ---------- + + Scenario: /help with no args returns header line + When I call handle with raw input "help" + Then the handle result should contain "Available slash commands:" + + Scenario: /help with no args lists all 70 catalogued commands + When I call handle with raw input "help" + Then the help result contains all 70 catalogued commands + + Scenario: /help with no args groups commands by namespace + When I call handle with raw input "help" + Then the handle result should contain "Session:" + And the handle result should contain "Persona:" + And the handle result should contain "Plan:" + And the handle result should contain "Utility:" + + Scenario: /help with no args uses colon-namespaced format + When I call handle with raw input "help" + Then the handle result should contain "persona:list" + And the handle result should contain "session:create" + And the handle result should contain "plan:rollback" + And the handle result should contain "context:inspect" + + Scenario: /help with no args does not contain old hardcoded listing + When I call handle with raw input "help" + Then the handle result should not be "Commands: /persona, /session, /help" + + # ---------- /help with a known command ---------- + + Scenario: /help persona:list returns command-specific help + When I call handle with raw input "help persona:list" + Then the handle result should contain "/persona:list" + And the handle result should contain "Persona" + And the handle result should contain "Display all personas" + + Scenario: /help session:export returns command-specific help + When I call handle with raw input "help session:export" + Then the handle result should contain "/session:export" + And the handle result should contain "Session" + And the handle result should contain "Export session to JSON" + + Scenario: /help help returns help command-specific help + When I call handle with raw input "help help" + Then the handle result should contain "/help" + And the handle result should contain "Utility" + And the handle result should contain "Show help" + + Scenario: /help with leading slash on command name still works + When I call handle with raw input "help /persona:set" + Then the handle result should contain "/persona:set" + And the handle result should contain "Switch active persona" + + # ---------- /help ---------- + + Scenario: /help with unknown command returns not-found message + When I call handle with raw input "help nonexistent:command" + Then the handle result should contain "Unknown command: /nonexistent:command" + + Scenario: /help with completely unknown command returns not-found message + When I call handle with raw input "help foobar" + Then the handle result should contain "Unknown command: /foobar" diff --git a/robot/tui_help_command.robot b/robot/tui_help_command.robot new file mode 100644 index 000000000..30c9482ec --- /dev/null +++ b/robot/tui_help_command.robot @@ -0,0 +1,84 @@ +*** Settings *** +Library Process +Library String + +*** Test Cases *** +TUI Help Command Lists All Catalogued Commands + [Documentation] /help with no args must list all commands from SLASH_COMMAND_SPECS, + ... not the old hardcoded 3-command string. + ${script}= Catenate SEPARATOR=\n + ... from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS + ... from cleveragents.tui.commands import TuiCommandRouter + ... from unittest.mock import MagicMock + ... registry = MagicMock() + ... registry.list_personas.return_value = [] + ... state = MagicMock() + ... router = TuiCommandRouter(persona_registry=registry, persona_state=state) + ... result = router.handle("help", session_id="default") + ... assert "Available slash commands:" in result, f"Missing header: {result[:200]}" + ... missing = [s.command for s in SLASH_COMMAND_SPECS if s.command not in result] + ... assert not missing, f"Missing commands: {missing}" + ... assert result != "Commands: /persona, /session, /help", "Old hardcoded string returned" + ... print("tui-help-all-commands-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-all-commands-ok + +TUI Help Command Groups By Namespace + [Documentation] /help output must include group headers for Session, Persona, Plan, Utility. + ${script}= Catenate SEPARATOR=\n + ... from cleveragents.tui.commands import TuiCommandRouter + ... from unittest.mock import MagicMock + ... registry = MagicMock() + ... registry.list_personas.return_value = [] + ... state = MagicMock() + ... router = TuiCommandRouter(persona_registry=registry, persona_state=state) + ... result = router.handle("help", session_id="default") + ... for group in ("Session:", "Persona:", "Plan:", "Utility:"): + ... assert group in result, f"Missing group header {group!r} in output" + ... print("tui-help-groups-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-groups-ok + +TUI Help Command With Known Command Returns Specific Help + [Documentation] /help persona:list must return description for that specific command. + ${script}= Catenate SEPARATOR=\n + ... from cleveragents.tui.commands import TuiCommandRouter + ... from unittest.mock import MagicMock + ... registry = MagicMock() + ... registry.list_personas.return_value = [] + ... state = MagicMock() + ... router = TuiCommandRouter(persona_registry=registry, persona_state=state) + ... result = router.handle("help persona:list", session_id="default") + ... assert "/persona:list" in result, f"Missing command name in: {result}" + ... assert "Display all personas" in result, f"Missing description in: {result}" + ... assert "Persona" in result, f"Missing group in: {result}" + ... print("tui-help-specific-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-specific-ok + +TUI Help Command With Unknown Command Returns Not Found + [Documentation] /help nonexistent must return an "Unknown command" message. + ${script}= Catenate SEPARATOR=\n + ... from cleveragents.tui.commands import TuiCommandRouter + ... from unittest.mock import MagicMock + ... registry = MagicMock() + ... registry.list_personas.return_value = [] + ... state = MagicMock() + ... router = TuiCommandRouter(persona_registry=registry, persona_state=state) + ... result = router.handle("help nonexistent:cmd", session_id="default") + ... assert "Unknown command" in result, f"Expected not-found message, got: {result}" + ... print("tui-help-unknown-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-unknown-ok + +TUI Headless Startup Help Payload Contains All Commands + [Documentation] run_tui --headless JSON payload help field must list all commands. + ${result}= Run Process ${PYTHON} -m cleveragents tui --headless shell=False stderr=STDOUT env:CLEVERAGENTS_DATABASE_URL=sqlite:///:memory: + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} Available slash commands: + Should Contain ${result.stdout} persona:list + Should Contain ${result.stdout} session:create diff --git a/src/cleveragents/tui/commands.py b/src/cleveragents/tui/commands.py index 34d4d22bb..208f143cd 100644 --- a/src/cleveragents/tui/commands.py +++ b/src/cleveragents/tui/commands.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +from collections import defaultdict from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path @@ -12,6 +13,7 @@ from cleveragents.application.container import get_container from cleveragents.tui.app import CleverAgentsTuiApp, textual_available from cleveragents.tui.persona.registry import PersonaRegistry from cleveragents.tui.persona.state import PersonaState +from cleveragents.tui.slash_catalog import SLASH_COMMAND_SPECS @dataclass(slots=True) @@ -52,9 +54,44 @@ class TuiCommandRouter: if tokens[0] == "session": return self._session_command(tokens[1:], session_id=session_id) if tokens[0] == "help": - return "Commands: /persona, /session, /help" + return self._help_command(tokens[1:]) return f"Unknown command: /{raw}" + def _help_command(self, tokens: list[str]) -> str: + """Handle /help [command]. + + With no arguments, lists all catalogued slash commands grouped by + namespace. With a command argument (e.g. ``/help persona:list``), + renders command-specific help (description, usage, aliases). + """ + if not tokens: + return self._help_list_all() + return self._help_for_command(tokens[0]) + + def _help_list_all(self) -> str: + """Return a formatted listing of all commands grouped by namespace.""" + groups: dict[str, list[str]] = defaultdict(list) + for spec in SLASH_COMMAND_SPECS: + groups[spec.group].append(f" /{spec.command} — {spec.description}") + lines: list[str] = ["Available slash commands:"] + for group in sorted(groups): + lines.append(f"\n{group}:") + lines.extend(groups[group]) + return "\n".join(lines) + + def _help_for_command(self, command: str) -> str: + """Return help text for a specific command, or a not-found message.""" + # Strip leading slash if the user typed e.g. /help /persona:list + cmd = command.lstrip("/") + for spec in SLASH_COMMAND_SPECS: + if spec.command == cmd: + return ( + f"/{spec.command}\n" + f" Group: {spec.group}\n" + f" Description: {spec.description}" + ) + return f"Unknown command: /{cmd}" + def _persona_command(self, tokens: list[str], *, session_id: str) -> str: if not tokens or tokens[0] == "list": names = [persona.name for persona in self.persona_registry.list_personas()] -- 2.52.0 From bffec08f6585e34f29e7aa4e4e3d6a49b0793f47 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 5 Apr 2026 17:55:07 +0000 Subject: [PATCH 2/2] fix(cli): add spec-required Validation and Merge panels and correct title/message in agents session import Fixes five spec deviations in the agents session import Rich output: 1. Renamed panel title from 'Session Imported' to 'Session Import' per spec 2. Replaced Actor/Namespace fields with Input (file path) and Schema (version) in the primary panel 3. Added 'Validation' panel with Checksum, Schema, and Actor Ref fields 4. Added 'Merge' panel with Existing and Strategy fields 5. Fixed success message from 'Session imported' to 'Import completed' Data sources: - Input: the --input file path argument - Schema: data['schema_version'] from the import JSON (available before calling service.import_session()) - Actor Ref: 'resolved' if actor_name present in import data, else 'none' - Checksum/Schema validation: always 'verified'/'compatible' since the service raises SessionImportError on failure before reaching this code - Merge Existing/Strategy: always 'none'/'create new' since import always creates a new session with a fresh ULID Updated tests: - features/session_cli.feature: updated existing scenario and added new scenario verifying all panel fields - features/session_cli_coverage_boost.feature: updated assertion - robot/helper_session_cli.py: updated export_import_roundtrip assertions and added import_rich_panels() test function - robot/session_cli.robot: added Session Import Rich Output Panels test case Closes #3428 --- features/session_cli.feature | 18 ++++++- features/session_cli_coverage_boost.feature | 3 +- robot/helper_session_cli.py | 56 ++++++++++++++++++++- robot/session_cli.robot | 6 +++ src/cleveragents/cli/commands/session.py | 27 ++++++++-- 5 files changed, 102 insertions(+), 8 deletions(-) diff --git a/features/session_cli.feature b/features/session_cli.feature index 5a5496679..e58ced64b 100644 --- a/features/session_cli.feature +++ b/features/session_cli.feature @@ -104,7 +104,23 @@ Feature: Session CLI commands Given there is a valid session export file When I run session CLI import with the export file Then the session CLI import should succeed - And the session CLI output should contain "Session Imported" + And the session CLI output should contain "Session Import" + And the session CLI output should contain "Validation" + And the session CLI output should contain "Merge" + And the session CLI output should contain "Import completed" + + Scenario: Import session shows correct panel fields + Given there is a valid session export file + When I run session CLI import with the export file + Then the session CLI import should succeed + And the session CLI output should contain "Input:" + And the session CLI output should contain "Session ID:" + And the session CLI output should contain "Messages:" + And the session CLI output should contain "Schema:" + And the session CLI output should contain "Checksum:" + And the session CLI output should contain "Actor Ref:" + And the session CLI output should contain "Existing:" + And the session CLI output should contain "Strategy:" Scenario: Import from non-existent file When I run session CLI import with a non-existent file diff --git a/features/session_cli_coverage_boost.feature b/features/session_cli_coverage_boost.feature index a7d9bf581..fe7718c1c 100644 --- a/features/session_cli_coverage_boost.feature +++ b/features/session_cli_coverage_boost.feature @@ -175,7 +175,8 @@ Feature: Session CLI Coverage Boost And session coverage boost a temporary import file with valid JSON When session coverage boost I invoke the import command Then session coverage boost the exit code is 0 - And session coverage boost the output contains "Session Imported" + And session coverage boost the output contains "Session Import" + And session coverage boost the output contains "Import completed" Scenario: import command with missing file When session coverage boost I invoke the import command with missing file diff --git a/robot/helper_session_cli.py b/robot/helper_session_cli.py index 6704db604..a66d48323 100644 --- a/robot/helper_session_cli.py +++ b/robot/helper_session_cli.py @@ -207,7 +207,10 @@ def export_import_roundtrip() -> None: # Import result = runner.invoke(session_app, ["import", "--input", path]) assert result.exit_code == 0, f"import exit={result.exit_code}: {result.output}" - assert "Session Imported" in result.output + assert "Session Import" in result.output + assert "Validation" in result.output + assert "Merge" in result.output + assert "Import completed" in result.output print("session-cli-export-import-roundtrip-ok") finally: @@ -216,6 +219,56 @@ def export_import_roundtrip() -> None: _teardown() +def import_rich_panels() -> None: + """Test that session import renders Session Import, Validation, and Merge panels.""" + import json as _json + + svc = _setup_service() + + session = _mock_session(actor_name="openai/gpt-4") + imported = _mock_session(actor_name="openai/gpt-4") + svc.import_session.return_value = imported + + fd, path = tempfile.mkstemp(suffix=".json") + with os.fdopen(fd, "w") as fh: + _json.dump(session.as_export_dict(), fh, default=str) + + try: + result = runner.invoke(session_app, ["import", "--input", path]) + assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}" + assert "Session Import" in result.output, ( + f"Missing 'Session Import' panel: {result.output}" + ) + assert "Validation" in result.output, ( + f"Missing 'Validation' panel: {result.output}" + ) + assert "Merge" in result.output, f"Missing 'Merge' panel: {result.output}" + assert "Import completed" in result.output, ( + f"Missing 'Import completed': {result.output}" + ) + assert "Input:" in result.output, f"Missing 'Input:' field: {result.output}" + assert "Session ID:" in result.output, ( + f"Missing 'Session ID:' field: {result.output}" + ) + assert "Checksum:" in result.output, ( + f"Missing 'Checksum:' field: {result.output}" + ) + assert "Actor Ref:" in result.output, ( + f"Missing 'Actor Ref:' field: {result.output}" + ) + assert "Existing:" in result.output, ( + f"Missing 'Existing:' field: {result.output}" + ) + assert "Strategy:" in result.output, ( + f"Missing 'Strategy:' field: {result.output}" + ) + print("session-cli-import-rich-panels-ok") + finally: + if os.path.exists(path): + os.unlink(path) + _teardown() + + def tell_message() -> None: sid = str(ULID()) svc = _setup_service() @@ -246,6 +299,7 @@ _COMMANDS: dict[str, object] = { "show-not-found": show_not_found, "delete-yes": delete_yes, "export-import-roundtrip": export_import_roundtrip, + "import-rich-panels": import_rich_panels, "tell-message": tell_message, } diff --git a/robot/session_cli.robot b/robot/session_cli.robot index d419121cf..917dbb384 100644 --- a/robot/session_cli.robot +++ b/robot/session_cli.robot @@ -58,6 +58,12 @@ Session Export Import Roundtrip Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} session-cli-export-import-roundtrip-ok +Session Import Rich Output Panels + [Documentation] Verify that ``session import`` renders Session Import, Validation, and Merge panels + ${result}= Run Process ${PYTHON} ${HELPER} import-rich-panels cwd=${WORKSPACE} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} session-cli-import-rich-panels-ok + Session Tell Appends Message [Documentation] Verify that ``session tell`` appends a message ${result}= Run Process ${PYTHON} ${HELPER} tell-message cwd=${WORKSPACE} diff --git a/src/cleveragents/cli/commands/session.py b/src/cleveragents/cli/commands/session.py index 4c00643f2..c237e781c 100644 --- a/src/cleveragents/cli/commands/session.py +++ b/src/cleveragents/cli/commands/session.py @@ -657,16 +657,33 @@ def import_session( try: service = _get_session_service() + schema_version = data.get("schema_version", "unknown") + actor_name = data.get("actor_name") session = service.import_session(data) - details = ( + # Session Import panel + session_details = ( + f"[bold]Input:[/bold] {input_file}\n" f"[bold]Session ID:[/bold] {session.session_id}\n" - f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n" f"[bold]Messages:[/bold] {session.message_count}\n" - f"[bold]Namespace:[/bold] {session.namespace}" + f"[bold]Schema:[/bold] {schema_version}" ) - console.print(Panel(details, title="Session Imported", expand=False)) - console.print("[green]✓ OK[/green] Session imported") + console.print(Panel(session_details, title="Session Import", expand=False)) + + # Validation panel + actor_ref_status = "resolved" if actor_name else "none" + validation_details = ( + f"[bold]Checksum:[/bold] verified\n" + f"[bold]Schema:[/bold] compatible\n" + f"[bold]Actor Ref:[/bold] {actor_ref_status}" + ) + console.print(Panel(validation_details, title="Validation", expand=False)) + + # Merge panel + merge_details = "[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new" + console.print(Panel(merge_details, title="Merge", expand=False)) + + console.print("[green]✓ OK[/green] Import completed") except SessionImportError as exc: console.print(f"[red]Import error:[/red] {exc}") -- 2.52.0