From d430d671a0cb1f2e873a5c860ed33bca77cc9dd8 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Fri, 3 Apr 2026 00:23:58 +0000 Subject: [PATCH] fix(cli): add missing Type field, Config, Capabilities, and Tools panels to actor add rich output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extended `_print_actor()` to render the full spec-required output for `agents actor add`: the Actor Added panel now includes a Type field, and three additional panels (Config, Capabilities, Tools) plus a success status line are rendered when `show_add_panels=True`. - Add `Type:` field to the Actor Added panel (from `config_blob["type"]`) - Implement Config panel: Path, Hash, Options count, Nodes count, Edges count - Implement Capabilities panel: bulleted list (rendered only when non-empty) - Implement Tools panel: Rich table with Tool, Read-Only, Safe columns (rendered only when non-empty; string tool entries default to yes/yes) - Add `✓ OK Actor added` success status line after all panels - Pass `config_path` and `show_add_panels=True` from `add()` command - Add Behave BDD scenarios covering each new panel and the success line - Add Robot Framework integration test verifying the full rich output ISSUES CLOSED: #1499 --- features/actor_add_rich_output.feature | 49 ++++ features/steps/actor_add_rich_output_steps.py | 274 ++++++++++++++++++ robot/actor_add_rich_output.robot | 49 ++++ robot/helper_actor_add_rich_output.py | 196 +++++++++++++ src/cleveragents/cli/commands/actor.py | 77 ++++- 5 files changed, 637 insertions(+), 8 deletions(-) create mode 100644 features/actor_add_rich_output.feature create mode 100644 features/steps/actor_add_rich_output_steps.py create mode 100644 robot/actor_add_rich_output.robot create mode 100644 robot/helper_actor_add_rich_output.py diff --git a/features/actor_add_rich_output.feature b/features/actor_add_rich_output.feature new file mode 100644 index 00000000..39f4c5e3 --- /dev/null +++ b/features/actor_add_rich_output.feature @@ -0,0 +1,49 @@ +Feature: actor add rich output panels + As a user of the CleverAgents CLI + I want `agents actor add` to display all spec-required panels + So that I can see the full details of a newly added actor + + Scenario: Actor Added panel includes Type field + Given an actor add CLI runner with a typed config + When I run actor add with the typed config + Then the actor added panel should contain the Type field + + Scenario: Config panel is rendered after actor add + Given an actor add CLI runner with a typed config + When I run actor add with the typed config + Then the output should contain a Config panel + + Scenario: Config panel shows Path, Hash, Options, Nodes, Edges + Given an actor add CLI runner with a typed config + When I run actor add with the typed config + Then the Config panel should show Path Hash Options Nodes and Edges + + Scenario: Capabilities panel is rendered when capabilities are present + Given an actor add CLI runner with capabilities in config + When I run actor add with the capabilities config + Then the output should contain a Capabilities panel with the capability list + + Scenario: Tools panel is rendered when tools are present + Given an actor add CLI runner with tools in config + When I run actor add with the tools config + Then the output should contain a Tools panel with tool rows + + Scenario: Success status line is printed after all panels + Given an actor add CLI runner with a typed config + When I run actor add with the typed config + Then the output should end with the actor added success line + + Scenario: No Capabilities panel when capabilities list is empty + Given an actor add CLI runner with a typed config + When I run actor add with the typed config + Then the output should not contain a Capabilities panel + + Scenario: No Tools panel when tools list is empty + Given an actor add CLI runner with a typed config + When I run actor add with the typed config + Then the output should not contain a Tools panel + + Scenario: Non-rich format skips all extra panels + Given an actor add CLI runner with a typed config + When I run actor add with the typed config in json format + Then the output should be valid JSON without panels diff --git a/features/steps/actor_add_rich_output_steps.py b/features/steps/actor_add_rich_output_steps.py new file mode 100644 index 00000000..0cebb76e --- /dev/null +++ b/features/steps/actor_add_rich_output_steps.py @@ -0,0 +1,274 @@ +"""Step definitions for actor add rich output panels feature.""" + +from __future__ import annotations + +import json +import tempfile +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, patch + +from behave import given, then, when +from typer.testing import CliRunner + +from cleveragents.cli.commands.actor import app as actor_app +from cleveragents.domain.models.core.actor import Actor + + +def _make_actor( + *, + name: str = "local/rich-actor", + provider: str = "openai", + model: str = "gpt-4", + config: dict[str, Any] | None = None, + graph_descriptor: dict[str, Any] | None = None, + unsafe: bool = False, + is_default: bool = False, + is_built_in: bool = False, +) -> Actor: + blob = config or {} + return Actor( + id=1, + name=name, + provider=provider, + model=model, + config_blob=blob, + config_hash=Actor.compute_hash(blob), + graph_descriptor=graph_descriptor, + unsafe=unsafe, + is_built_in=is_built_in, + is_default=is_default, + ) + + +def _register_cleanup(context: Any, path: Path) -> None: + context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True)) + + +@given("an actor add CLI runner with a typed config") +def step_impl(context: Any) -> None: + context.runner = CliRunner() + context.actor_config_data = { + "name": "local/rich-actor", + "provider": "openai", + "model": "gpt-4", + "type": "graph", + } + with tempfile.NamedTemporaryFile( + delete=False, suffix=".json", mode="w", encoding="utf-8" + ) as handle: + json.dump(context.actor_config_data, handle) + handle.flush() + context.actor_config_path = Path(handle.name) + _register_cleanup(context, context.actor_config_path) + + graph_descriptor = { + "nodes": [{"id": "n1"}, {"id": "n2"}, {"id": "n3"}], + "edges": [{"from": "n1", "to": "n2"}, {"from": "n2", "to": "n3"}], + } + context.mock_actor = _make_actor( + config=context.actor_config_data, + graph_descriptor=graph_descriptor, + ) + + +@given("an actor add CLI runner with capabilities in config") +def step_impl(context: Any) -> None: + context.runner = CliRunner() + context.actor_config_data = { + "name": "local/cap-actor", + "provider": "openai", + "model": "gpt-4", + "type": "llm", + "capabilities": ["code review", "diff summarization", "lint guidance"], + } + with tempfile.NamedTemporaryFile( + delete=False, suffix=".json", mode="w", encoding="utf-8" + ) as handle: + json.dump(context.actor_config_data, handle) + handle.flush() + context.actor_config_path = Path(handle.name) + _register_cleanup(context, context.actor_config_path) + context.mock_actor = _make_actor(config=context.actor_config_data) + + +@given("an actor add CLI runner with tools in config") +def step_impl(context: Any) -> None: + context.runner = CliRunner() + context.actor_config_data = { + "name": "local/tool-actor", + "provider": "openai", + "model": "gpt-4", + "type": "llm", + "tools": [ + {"name": "read_file", "read_only": True, "safe": True}, + {"name": "write_file", "read_only": False, "safe": False}, + ], + } + with tempfile.NamedTemporaryFile( + delete=False, suffix=".json", mode="w", encoding="utf-8" + ) as handle: + json.dump(context.actor_config_data, handle) + handle.flush() + context.actor_config_path = Path(handle.name) + _register_cleanup(context, context.actor_config_path) + context.mock_actor = _make_actor(config=context.actor_config_data) + + +@when("I run actor add with the typed config") +def step_impl(context: Any) -> None: + with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: + registry = MagicMock() + registry.upsert_actor.return_value = context.mock_actor + mock_svc.return_value = (MagicMock(), registry) + context.result = context.runner.invoke( + actor_app, + ["add", "--config", str(context.actor_config_path)], + ) + + +@when("I run actor add with the capabilities config") +def step_impl(context: Any) -> None: + with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: + registry = MagicMock() + registry.upsert_actor.return_value = context.mock_actor + mock_svc.return_value = (MagicMock(), registry) + context.result = context.runner.invoke( + actor_app, + ["add", "--config", str(context.actor_config_path)], + ) + + +@when("I run actor add with the tools config") +def step_impl(context: Any) -> None: + with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: + registry = MagicMock() + registry.upsert_actor.return_value = context.mock_actor + mock_svc.return_value = (MagicMock(), registry) + context.result = context.runner.invoke( + actor_app, + ["add", "--config", str(context.actor_config_path)], + ) + + +@when("I run actor add with the typed config in json format") +def step_impl(context: Any) -> None: + with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: + registry = MagicMock() + registry.upsert_actor.return_value = context.mock_actor + mock_svc.return_value = (MagicMock(), registry) + context.result = context.runner.invoke( + actor_app, + ["add", "--config", str(context.actor_config_path), "--format", "json"], + ) + + +@then("the actor added panel should contain the Type field") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + assert "Type:" in context.result.output, ( + f"Expected 'Type:' in output:\n{context.result.output}" + ) + assert "graph" in context.result.output, ( + f"Expected actor type value 'graph' in output:\n{context.result.output}" + ) + + +@then("the output should contain a Config panel") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + assert "Config" in context.result.output, ( + f"Expected 'Config' panel in output:\n{context.result.output}" + ) + + +@then("the Config panel should show Path Hash Options Nodes and Edges") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + output = context.result.output + for field in ("Path:", "Hash:", "Options:", "Nodes:", "Edges:"): + assert field in output, f"Expected '{field}' in Config panel output:\n{output}" + + +@then("the output should contain a Capabilities panel with the capability list") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + output = context.result.output + assert "Capabilities" in output, ( + f"Expected 'Capabilities' panel in output:\n{output}" + ) + for cap in context.actor_config_data.get("capabilities", []): + assert cap in output, f"Expected capability '{cap}' in output:\n{output}" + + +@then("the output should contain a Tools panel with tool rows") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + output = context.result.output + assert "Tools" in output, f"Expected 'Tools' panel in output:\n{output}" + for tool_entry in context.actor_config_data.get("tools", []): + if isinstance(tool_entry, dict): + tool_name = tool_entry.get("name", "") + else: + tool_name = str(tool_entry) + assert tool_name in output, f"Expected tool '{tool_name}' in output:\n{output}" + + +@then("the output should end with the actor added success line") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + assert "✓ OK" in context.result.output, ( + f"Expected '✓ OK' in output:\n{context.result.output}" + ) + assert "Actor added" in context.result.output, ( + f"Expected 'Actor added' in output:\n{context.result.output}" + ) + + +@then("the output should not contain a Capabilities panel") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + assert "Capabilities" not in context.result.output, ( + f"Did not expect 'Capabilities' panel in output:\n{context.result.output}" + ) + + +@then("the output should not contain a Tools panel") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + assert "Tools" not in context.result.output, ( + f"Did not expect 'Tools' panel in output:\n{context.result.output}" + ) + + +@then("the output should be valid JSON without panels") +def step_impl(context: Any) -> None: + assert context.result.exit_code == 0, ( + f"exit_code={context.result.exit_code}, output={context.result.output}" + ) + output = context.result.output.strip() + data = json.loads(output) + assert isinstance(data, dict), f"Expected JSON object, got: {type(data)}" + assert "name" in data, f"Expected 'name' field in JSON output: {data}" + assert "Config" not in output, ( + f"Did not expect 'Config' panel in JSON output:\n{output}" + ) + assert "✓ OK" not in output, ( + f"Did not expect success line in JSON output:\n{output}" + ) diff --git a/robot/actor_add_rich_output.robot b/robot/actor_add_rich_output.robot new file mode 100644 index 00000000..18fa3a67 --- /dev/null +++ b/robot/actor_add_rich_output.robot @@ -0,0 +1,49 @@ +*** Settings *** +Documentation Integration tests for actor add rich output panels +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} ${CURDIR}/helper_actor_add_rich_output.py + +*** Test Cases *** +Actor Add Rich Output Contains Type Field + [Documentation] Verify that ``actor add`` rich output includes the Type field + ${result}= Run Process ${PYTHON} ${HELPER} add-type-field cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} actor-add-type-field-ok + +Actor Add Rich Output Contains Config Panel + [Documentation] Verify that ``actor add`` rich output includes the Config panel + ${result}= Run Process ${PYTHON} ${HELPER} add-config-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} actor-add-config-panel-ok + +Actor Add Rich Output Contains Capabilities Panel + [Documentation] Verify that ``actor add`` rich output includes the Capabilities panel + ${result}= Run Process ${PYTHON} ${HELPER} add-capabilities-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} actor-add-capabilities-panel-ok + +Actor Add Rich Output Contains Tools Panel + [Documentation] Verify that ``actor add`` rich output includes the Tools panel + ${result}= Run Process ${PYTHON} ${HELPER} add-tools-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} actor-add-tools-panel-ok + +Actor Add Rich Output Contains Success Line + [Documentation] Verify that ``actor add`` rich output ends with the success status line + ${result}= Run Process ${PYTHON} ${HELPER} add-success-line cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} actor-add-success-line-ok diff --git a/robot/helper_actor_add_rich_output.py b/robot/helper_actor_add_rich_output.py new file mode 100644 index 00000000..7578c5ac --- /dev/null +++ b/robot/helper_actor_add_rich_output.py @@ -0,0 +1,196 @@ +"""Helper script for actor add rich output panels Robot test.""" + +from __future__ import annotations + +import json +import sys +import tempfile +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock, patch + +from typer.testing import CliRunner + +from cleveragents.cli.commands.actor import app as actor_app +from cleveragents.domain.models.core.actor import Actor + + +def _make_actor( + *, + name: str = "local/robot-add-actor", + provider: str = "openai", + model: str = "gpt-4", + config: dict[str, Any] | None = None, + graph_descriptor: dict[str, Any] | None = None, + unsafe: bool = False, + is_default: bool = False, + is_built_in: bool = False, +) -> Actor: + blob = config or {} + return Actor( + id=1, + name=name, + provider=provider, + model=model, + config_blob=blob, + config_hash=Actor.compute_hash(blob), + graph_descriptor=graph_descriptor, + unsafe=unsafe, + is_built_in=is_built_in, + is_default=is_default, + ) + + +def _run_actor_add( + config_data: dict[str, Any], + actor: Actor, +) -> str: + """Invoke actor add with a temp config file and return the output.""" + runner = CliRunner() + with tempfile.NamedTemporaryFile( + delete=False, suffix=".json", mode="w", encoding="utf-8" + ) as handle: + json.dump(config_data, handle) + handle.flush() + config_path = Path(handle.name) + + try: + with patch("cleveragents.cli.commands.actor._get_services") as mock_svc: + registry = MagicMock() + registry.upsert_actor.return_value = actor + mock_svc.return_value = (MagicMock(), registry) + result = runner.invoke( + actor_app, + ["add", "--config", str(config_path)], + ) + finally: + config_path.unlink(missing_ok=True) + + assert result.exit_code == 0, ( + f"actor add failed (exit_code={result.exit_code}):\n{result.output}" + ) + return result.output + + +def test_add_type_field() -> None: + """Verify actor add rich output includes the Type field.""" + config_data: dict[str, Any] = { + "name": "local/robot-add-actor", + "provider": "openai", + "model": "gpt-4", + "type": "graph", + } + actor = _make_actor(config=config_data) + output = _run_actor_add(config_data, actor) + + assert "Type:" in output, f"Expected 'Type:' in output:\n{output}" + assert "graph" in output, f"Expected type value 'graph' in output:\n{output}" + print("actor-add-type-field-ok") + + +def test_add_config_panel() -> None: + """Verify actor add rich output includes the Config panel with all fields.""" + config_data: dict[str, Any] = { + "name": "local/robot-add-actor", + "provider": "openai", + "model": "gpt-4", + "type": "graph", + "options": {"temperature": 0.5, "max_tokens": 256}, + } + graph_descriptor = { + "nodes": [{"id": "n1"}, {"id": "n2"}], + "edges": [{"from": "n1", "to": "n2"}], + } + actor = _make_actor(config=config_data, graph_descriptor=graph_descriptor) + output = _run_actor_add(config_data, actor) + + for field in ("Config", "Path:", "Hash:", "Options:", "Nodes:", "Edges:"): + assert field in output, f"Expected '{field}' in output:\n{output}" + + # Verify counts + assert "2" in output, f"Expected node count '2' in output:\n{output}" + assert "1" in output, f"Expected edge count '1' in output:\n{output}" + print("actor-add-config-panel-ok") + + +def test_add_capabilities_panel() -> None: + """Verify actor add rich output includes the Capabilities panel.""" + config_data: dict[str, Any] = { + "name": "local/robot-add-actor", + "provider": "openai", + "model": "gpt-4", + "type": "llm", + "capabilities": ["code review", "diff summarization", "lint guidance"], + } + actor = _make_actor(config=config_data) + output = _run_actor_add(config_data, actor) + + assert "Capabilities" in output, ( + f"Expected 'Capabilities' panel in output:\n{output}" + ) + for cap in config_data["capabilities"]: + assert cap in output, f"Expected capability '{cap}' in output:\n{output}" + print("actor-add-capabilities-panel-ok") + + +def test_add_tools_panel() -> None: + """Verify actor add rich output includes the Tools panel.""" + config_data: dict[str, Any] = { + "name": "local/robot-add-actor", + "provider": "openai", + "model": "gpt-4", + "type": "llm", + "tools": [ + {"name": "read_file", "read_only": True, "safe": True}, + {"name": "search_files", "read_only": True, "safe": True}, + {"name": "git_diff", "read_only": True, "safe": True}, + ], + } + actor = _make_actor(config=config_data) + output = _run_actor_add(config_data, actor) + + assert "Tools" in output, f"Expected 'Tools' panel in output:\n{output}" + assert "Tool" in output, f"Expected 'Tool' column header in output:\n{output}" + assert "Read-Only" in output, f"Expected 'Read-Only' column in output:\n{output}" + assert "Safe" in output, f"Expected 'Safe' column in output:\n{output}" + for tool in config_data["tools"]: + assert tool["name"] in output, ( + f"Expected tool '{tool['name']}' in output:\n{output}" + ) + print("actor-add-tools-panel-ok") + + +def test_add_success_line() -> None: + """Verify actor add rich output ends with the success status line.""" + config_data: dict[str, Any] = { + "name": "local/robot-add-actor", + "provider": "openai", + "model": "gpt-4", + "type": "graph", + } + actor = _make_actor(config=config_data) + output = _run_actor_add(config_data, actor) + + assert "✓ OK" in output, f"Expected '✓ OK' in output:\n{output}" + assert "Actor added" in output, f"Expected 'Actor added' in output:\n{output}" + print("actor-add-success-line-ok") + + +def main() -> None: + command = sys.argv[1] if len(sys.argv) > 1 else "add-type-field" + dispatch = { + "add-type-field": test_add_type_field, + "add-config-panel": test_add_config_panel, + "add-capabilities-panel": test_add_capabilities_panel, + "add-tools-panel": test_add_tools_panel, + "add-success-line": test_add_success_line, + } + fn = dispatch.get(command) + if fn is None: + print(f"Unknown command: {command}", file=sys.stderr) + sys.exit(1) + fn() + + +if __name__ == "__main__": + main() diff --git a/src/cleveragents/cli/commands/actor.py b/src/cleveragents/cli/commands/actor.py index c9557bf5..8f415664 100644 --- a/src/cleveragents/cli/commands/actor.py +++ b/src/cleveragents/cli/commands/actor.py @@ -340,25 +340,81 @@ def _print_actor( actor: Actor, title: str = "Actor", fmt: str = OutputFormat.RICH.value, + config_path: Path | None = None, + show_add_panels: bool = False, ) -> None: - """Print actor details in the requested format.""" + """Print actor details in the requested format. + + When ``show_add_panels`` is True (used by the ``add`` command), renders + the full spec-required output: Actor Added panel (with Type field), Config + panel, Capabilities panel, Tools panel, and a success status line. + """ if fmt != OutputFormat.RICH.value: data = _actor_spec_dict(actor) console.print(format_output(data, fmt)) return + blob = actor.config_blob or {} + actor_type = str(blob.get("type", "")) + details = ( f"[bold]Name:[/bold] {actor.name}\n" f"[bold]Provider:[/bold] {actor.provider}\n" f"[bold]Model:[/bold] {actor.model}\n" - f"[bold]Unsafe:[/bold] {'yes' if actor.unsafe else 'no'}\n" f"[bold]Default:[/bold] {'yes' if actor.is_default else 'no'}\n" - f"[bold]Built-in:[/bold] {'yes' if actor.is_built_in else 'no'}\n" - f"[bold]Config Hash:[/bold] {actor.config_hash}\n" - f"[bold]Updated:[/bold] {actor.updated_at}" + f"[bold]Unsafe:[/bold] {'yes' if actor.unsafe else 'no'}\n" + f"[bold]Type:[/bold] {actor_type}" ) console.print(Panel(details, title=title, expand=False)) + if not show_add_panels: + return + + # ── Config panel ────────────────────────────────────────────────────────── + path_str = str(config_path) if config_path is not None else "" + options_count = len(blob.get("options", {}) or {}) + + graph_descriptor = actor.graph_descriptor or {} + nodes_count = len(graph_descriptor.get("nodes", []) or []) + edges_count = len(graph_descriptor.get("edges", []) or []) + + config_details = ( + f"[bold]Path:[/bold] {path_str}\n" + f"[bold]Hash:[/bold] {actor.config_hash}\n" + f"[bold]Options:[/bold] {options_count}\n" + f"[bold]Nodes:[/bold] {nodes_count}\n" + f"[bold]Edges:[/bold] {edges_count}" + ) + console.print(Panel(config_details, title="Config", expand=False)) + + # ── Capabilities panel ──────────────────────────────────────────────────── + capabilities: list[str] = list(blob.get("capabilities", []) or []) + if capabilities: + cap_lines = "\n".join(f"- {cap}" for cap in capabilities) + console.print(Panel(cap_lines, title="Capabilities", expand=False)) + + # ── Tools panel ─────────────────────────────────────────────────────────── + raw_tools: list[Any] = list(blob.get("tools", []) or []) + if raw_tools: + tools_table = Table(show_header=True, header_style="bold", box=None) + tools_table.add_column("Tool") + tools_table.add_column("Read-Only", justify="center") + tools_table.add_column("Safe", justify="center") + for tool_entry in raw_tools: + if isinstance(tool_entry, dict): + tool_name = str(tool_entry.get("name", "")) + read_only = "yes" if tool_entry.get("read_only", False) else "no" + safe = "yes" if tool_entry.get("safe", False) else "no" + else: + tool_name = str(tool_entry) + read_only = "yes" + safe = "yes" + tools_table.add_row(tool_name, read_only, safe) + console.print(Panel(tools_table, title="Tools", expand=False)) + + # ── Success status line ─────────────────────────────────────────────────── + console.print("[green bold]✓ OK[/green bold] Actor added") + def _print_role_warnings(config_blob: dict[str, Any]) -> None: """Print non-fatal role compatibility warnings for actor configs.""" @@ -462,7 +518,13 @@ def add( set_default=set_default, ) title = "Actor updated" if update_existing else "Actor added" - _print_actor(actor, title=title, fmt=fmt) + _print_actor( + actor, + title=title, + fmt=fmt, + config_path=config, + show_add_panels=(not update_existing), + ) except (ValidationError, BusinessRuleViolation) as exc: console.print(f"[red]Error:[/red] {exc}") raise typer.Abort() from exc @@ -639,8 +701,7 @@ def remove(name: Annotated[str, typer.Argument(help="Actor name to remove")]) -> # Display Cleanup panel cleanup_info = ( - "[blue]Config:[/blue] kept on disk\n" - "[blue]Contexts:[/blue] 0 orphaned" + "[blue]Config:[/blue] kept on disk\n[blue]Contexts:[/blue] 0 orphaned" ) cleanup_panel = Panel(cleanup_info, title="Cleanup", border_style="dim") console.print(cleanup_panel)