Files
cleveragents-core/docs/reference/output_rendering.md
T

5.2 KiB

Output Rendering Framework

The output rendering framework decouples command output data from its visual presentation. Every CLI command produces structured output through a common abstraction layer, and the active format determines how that output is rendered to the terminal or piped to external consumers.

Architecture

All CLI output flows through a five-stage reactive pipeline:

  1. Command Logic opens an OutputSession and creates typed element handles.
  2. OutputSession coordinates handles, tracks their lifecycle, and emits events to the active materialisation strategy.
  3. ElementHandles are the producer-facing API — thread-safe, format-agnostic write interfaces.
  4. MaterializationStrategy decides when and how to render content.
  5. Terminal / Pipe receives the final byte stream.

Quick Start

from cleveragents.cli.output import OutputSession

with OutputSession(format="plain") as session:
    panel = session.panel("Project Details")
    panel.set_entry("Name", "local/api-service")
    panel.set_entry("Status", "active")
    panel.close()

    table = session.table(columns=["Resource", "Type"])
    table.add_row({"Resource": "api-repo", "Type": "git-checkout"})
    table.close()

OutputSession

The OutputSession is the central coordinator. It is used as a context manager and provides factory methods for each element type:

  • session.panel(title)PanelHandle
  • session.table(columns=[...])TableHandle
  • session.status(message)StatusHandle
  • session.progress(label)ProgressHandle

The session can be created with:

  • format — the desired output format (rich, color, table, plain, json, yaml)
  • command — the command name for metadata
  • strategy — an explicit materialisation strategy (overrides format auto-selection)

Element Handles

PanelHandle

Key-value pair output with a title.

panel = session.panel("Details")
panel.set_entry("Name", "my-project")
panel.set_entries({"A": "1", "B": "2"})
panel.remove_entry("A")
panel.close()

TableHandle

Tabular data with typed columns.

table = session.table(columns=["Name", "Status", "Count"])
table.add_row({"Name": "svc-1", "Status": "up", "Count": "42"})
table.add_rows([{"Name": "svc-2", "Status": "down", "Count": "0"}])
table.set_summary({"total": "42"})
table.set_sort_key("Name")
table.close()

StatusHandle

Single-line status messages with level indicators.

status = session.status("Processing...", level="info")
status.set_message("Done")
status.set_level("ok")
status.set_detail("All 42 items processed")
status.close()

ProgressHandle

Progress bars and spinners with throttled updates (max 10 FPS in rich mode).

prog = session.progress("Uploading", total=100)
prog.set_progress(50, 100)
prog.tick()
prog.increment(10)
prog.set_label("Finalising")
prog.set_indeterminate()
prog.set_step_status("validate", "done")
prog.close()

Materialisation Strategies

Format Strategy Behaviour
rich RichMaterializer In-place terminal updates via Rich library
color ColorMaterializer ANSI codes, scrolling output (no cursor moves)
table TableMaterializer ASCII box-drawing for tables
plain PlainMaterializer ASCII-only, no ANSI, no Unicode box characters
json JsonMaterializer Accumulate-then-serialize as JSON
yaml YamlMaterializer Accumulate-then-dump as YAML

Automatic Fallback

When terminal capabilities are insufficient, the framework automatically falls back:

  • richcolorplain (based on TTY, ANSI, cursor support)
  • colorplain (when no ANSI support)
  • tableplain (when not a TTY)
  • json / yaml — always available (no terminal needed)

Use --format <value> to explicitly set a format and skip fallback.

Error Envelope

JSON and YAML materializers support a unified error envelope:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found",
    "details": {"id": "abc-123"}
  }
}

Producer Integration Guide

Writing Format-Agnostic Commands

Commands should create handles and write data without knowing which format is active:

def my_command(session: OutputSession, items: list[Item]) -> None:
    panel = session.panel("Summary")
    panel.set_entry("Total", str(len(items)))
    panel.close()

    table = session.table(columns=["Name", "Status"])
    for item in items:
        table.add_row({"Name": item.name, "Status": item.status})
    table.close()

Thread Safety

Multiple handles can be written concurrently from different threads. The session ensures elements are rendered in declaration order even when handles close out of order.

Backward Compatibility

The existing format_output() function in cleveragents.cli.formatting continues to work unchanged. A new format_output_session() function is also available that routes through the OutputSession framework.