Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions aai_cli/commands/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ def render(d: dict[str, object]) -> object:
shown_with_breakdown = [(item, _line_items_summary(item)) for item in shown]
show_breakdown = any(summary for _, summary in shown_with_breakdown)
table = (
Table("period", "total", "breakdown", header_style="aai.heading")
output.data_table("period", "total", "breakdown")
if show_breakdown
else Table("period", "total", header_style="aai.heading")
else output.data_table("period", "total")
)
hidden_count = len(items) - len(shown)
for item, breakdown in shown_with_breakdown:
Expand Down Expand Up @@ -230,7 +230,7 @@ def body(state: AppState, json_mode: bool) -> None:
data = ams.get_rate_limits(account_id, jwt)

def render(d: dict[str, object]) -> Table:
table = Table("service", "limit", header_style="aai.heading")
table = output.data_table("service", "limit")
for limit in jsonshape.mapping_list(d.get("rate_limits")):
table.add_row(
escape(str(limit.get("service", ""))),
Expand Down
3 changes: 1 addition & 2 deletions aai_cli/commands/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import typer
from rich.console import Group
from rich.markup import escape
from rich.table import Table
from rich.text import Text

from aai_cli import help_panels, jsonshape, output, timeparse
Expand Down Expand Up @@ -141,7 +140,7 @@ def render(data: list[dict[str, object]]) -> object:
)
return Text(message, style="aai.muted")

table = Table("when (UTC)", "event", "resource", "actor", header_style="aai.heading")
table = output.data_table("when (UTC)", "event", "resource", "actor")
for entry in shown:
table.add_row(
escape(_format_time(entry.get("log_time"))),
Expand Down
2 changes: 1 addition & 1 deletion aai_cli/commands/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def body(state: AppState, json_mode: bool) -> None:
)

def render(data: list[dict[str, object]]) -> Table:
table = Table("id", "name", "project", "key", "disabled", header_style="aai.heading")
table = output.data_table("id", "name", "project", "key", "disabled")
for row in data:
table.add_row(
str(row["id"]),
Expand Down
4 changes: 1 addition & 3 deletions aai_cli/commands/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,7 @@ def body(state: AppState, json_mode: bool) -> None:
account_id = config.get_account_id(profile)

def render(_d: dict[str, object]) -> Table:
table = Table.grid(padding=(0, 3))
table.add_column(style="aai.muted")
table.add_column()
table = output.detail_table()
table.add_row("Profile", escape(profile))
table.add_row("Env", escape(env))
table.add_row("API key", escape(masked))
Expand Down
8 changes: 4 additions & 4 deletions aai_cli/commands/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,12 @@ def body(state: AppState, json_mode: bool) -> None:
rows = _session_rows(payload.get("data"))

def render(data: list[dict[str, object]]) -> Table:
table = Table(
table = output.data_table(
"session id",
"status",
"created",
"audio (s)",
"model",
header_style="aai.heading",
)
for s in data:
status_str = str(s["status"])
Expand Down Expand Up @@ -99,10 +98,11 @@ def body(state: AppState, json_mode: bool) -> None:
data = ams.get_streaming(session_id, jwt)

def render(d: dict[str, object]) -> Table:
table = Table(show_header=False)
table = output.detail_table()
for field in _DETAIL_FIELDS:
value = d.get(field)
table.add_row(field, escape("" if value is None else str(value)))
label = field.replace("_", " ")
table.add_row(label, escape("" if value is None else str(value)))
return table

output.emit(data, render, json_mode=json_mode)
Expand Down
2 changes: 1 addition & 1 deletion aai_cli/commands/transcripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def body(state: AppState, json_mode: bool) -> None:
rows = client.list_transcripts(api_key, limit=limit)

def render(data: list[dict[str, object]]) -> Table:
table = Table("id", "status", "created", header_style="aai.heading")
table = output.data_table("id", "status", "created")
for row in data:
status = str(row["status"])
table.add_row(
Expand Down
27 changes: 27 additions & 0 deletions aai_cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, TypeVar

from rich import box
from rich.markup import escape
from rich.table import Table

from aai_cli import theme
from aai_cli.errors import UsageError
Expand Down Expand Up @@ -92,6 +94,31 @@ def heading(text: str) -> str:
return f"[aai.heading]{text}[/aai.heading]"


def data_table(*columns: str) -> Table:
"""A list table with the one consistent, minimal look used CLI-wide.

Headers render in the brand heading style with a single rule beneath them and
no surrounding box — the quiet, scannable style the Vercel/Supabase CLIs use.
Defined once here so every listing command (`transcripts list`, `keys list`,
`sessions list`, `usage`, `limits`, `audit`) shares the same table, rather than
each re-deriving Rich's heavy default box.
"""
return Table(*columns, box=box.SIMPLE_HEAD, header_style="aai.heading", pad_edge=False)


def detail_table() -> Table:
"""A borderless label/value grid for single-record views (`whoami`, `sessions get`).

The label column is muted so the values read as the content and the pair scans
as a definition list, not a boxed table. Centralizes what was two divergent
one-off tables into one look.
"""
table = Table.grid(padding=(0, 3))
table.add_column(style="aai.muted")
table.add_column()
return table


def emit(data: T, human_renderer: Callable[[T], object], *, json_mode: bool) -> None:
if json_mode:
print(json.dumps(data, default=str))
Expand Down
16 changes: 16 additions & 0 deletions tests/test_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ def test_keys_list_flattens_tokens():
assert "sk_abcdef1234" not in result.output # api key is masked


def test_keys_list_renders_table_human(monkeypatch):
_auth()
monkeypatch.setattr("aai_cli.output.resolve_json", lambda *, explicit: explicit)
projects = [
{
"project": {"id": 1, "name": "Default"},
"tokens": [{"id": 10, "name": "ci", "api_key": "sk_abcdef1234", "is_disabled": False}],
}
]
with patch("aai_cli.commands.keys.ams.list_projects", return_value=projects):
result = runner.invoke(app, ["keys", "list"])
assert result.exit_code == 0
assert "ci" in result.output and "Default" in result.output
assert "sk_abcdef1234" not in result.output # masked in the human table too


def test_keys_shape_helpers_filter_invalid_values():
assert keys._project_id({"id": True}) is None
assert keys._project_id({"id": 7}) == 7
Expand Down
12 changes: 12 additions & 0 deletions tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ def test_whoami_reports_authenticated():
assert data["api_key"].startswith("sk_") and "…" in data["api_key"]


def test_whoami_human_render_shows_detail_rows(monkeypatch):
config.set_api_key("default", "sk_1234567890")
monkeypatch.setattr("aai_cli.output.resolve_json", lambda *, explicit: explicit)
with patch("aai_cli.commands.login.client.validate_key", return_value=True):
result = runner.invoke(app, ["whoami"])
assert result.exit_code == 0
# The shared borderless detail grid: labelled rows, no JSON, key masked.
assert "Profile" in result.output and "default" in result.output
assert "reachable" in result.output
assert "…" in result.output and '"profile"' not in result.output


def test_whoami_unauthenticated_runs_login(monkeypatch):
monkeypatch.setattr("aai_cli.context.run_login_flow", _fake_login_result)
with patch("aai_cli.commands.login.client.validate_key", return_value=True) as validate:
Expand Down
19 changes: 19 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,22 @@ def test_print_code_highlights_for_interactive_human(monkeypatch, capsys):
out = capsys.readouterr().out
assert "import" in out
assert "\x1b[" in out # syntax-highlighted -> ANSI present


def test_data_table_is_minimal_and_themed():
from rich import box

table = output.data_table("id", "status")
# One shared, quiet look: a header-rule box (no heavy outer border) and the
# brand heading style — so every listing command renders identically.
assert table.box is box.SIMPLE_HEAD
assert table.header_style == "aai.heading"
assert [str(col.header) for col in table.columns] == ["id", "status"]


def test_detail_table_is_borderless_label_value_grid():
table = output.detail_table()
# A grid (no box) with a muted label column, shared by whoami / sessions get.
assert table.box is None
assert len(table.columns) == 2
assert table.columns[0].style == "aai.muted"
2 changes: 2 additions & 0 deletions tests/test_sessions_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def test_sessions_get_renders_detail(monkeypatch):
result = runner.invoke(app, ["sessions", "get", "s_1"])
assert result.exit_code == 0
assert "s_1" in result.output and "universal" in result.output
# Field labels are humanized (underscores -> spaces) for the detail view.
assert "speech model" in result.output and "speech_model" not in result.output


def test_sessions_without_session_runs_login(monkeypatch):
Expand Down
Loading