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:
- Command Logic opens an
OutputSessionand creates typed element handles. - OutputSession coordinates handles, tracks their lifecycle, and emits events to the active materialisation strategy.
- ElementHandles are the producer-facing API — thread-safe, format-agnostic write interfaces.
- MaterializationStrategy decides when and how to render content.
- 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)→PanelHandlesession.table(columns=[...])→TableHandlesession.status(message)→StatusHandlesession.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 metadatastrategy— 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:
rich→color→plain(based on TTY, ANSI, cursor support)color→plain(when no ANSI support)table→plain(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.