Skip to content

Status Display (TUI)

StatusRenderer is a terminal UI that replaces scrolling log output with a single updating status line during agent execution. It is designed for interactive terminal sessions where you want a clean, information-dense view of what the agent is doing without a wall of streaming text.

  • Interactive terminals — running an agent from a shell script, REPL, or CLI tool
  • Long-running tasks — research agents, file-processing pipelines, multi-step workflows where you need elapsed time and cost visible at all times
  • Demos — cleaner than scrolling log output when showing the agent to someone

Use mode: "stream" instead when you need every token visible (server logs, CI pipelines, or piped output).

StatusRenderer activates automatically when process.stdout.isTTY is true and you have not explicitly set mode: "stream". In CI or piped output (agent.run() | tee log.txt) it falls back to plain line-by-line output with no ANSI escape codes.

import { ReactiveAgents } from "reactive-agents";
const agent = await ReactiveAgents.create()
.withName("researcher")
.withProvider("anthropic")
.withReasoning()
.withTools()
.build();
// In an interactive terminal: StatusRenderer starts automatically.
// In CI or piped output: plain log lines, no ANSI.
const result = await agent.run("Summarize the top 5 papers on attention mechanisms");
console.log(result.output);

Pass logging: { mode: "status" } to force the TUI on regardless of TTY, or mode: "stream" to force plain streaming output even in an interactive terminal.

import { ReactiveAgents, defaultReactiveAgentsConfig } from "reactive-agents";
import { createReactiveAgentsRuntime } from "@reactive-agents/runtime";
// Force status mode (TUI) even if stdout is not a TTY
const config = defaultReactiveAgentsConfig("my-agent", {
logging: { mode: "status" },
});
// Force stream mode (plain output) even in an interactive terminal
const configStream = defaultReactiveAgentsConfig("my-agent", {
logging: { mode: "stream" },
});

A single line updates in place at 100 ms intervals:

⠙ Thinking... iter 3 14s 1,234 tok $0.0012 entropy 0.43 ↓ [t: expand]
FieldDescription
SpinnerBraille animation — confirms the agent is alive
ActionCurrent phase: Starting..., Thinking..., Acting..., Calling <tool>...
iter NCurrent reasoning iteration (hidden on iteration 0)
ElapsedWall-clock time since agent.run() was called
N tokCumulative tokens used (hidden until first token metric arrives)
$N.NNNNCumulative cost in USD (hidden until first cost metric arrives)
entropy N.NN ↑↓→Semantic entropy with trend arrow (hidden during tool calls)
[t: expand]Keyboard hint — only shown during the think phase when text is available

Each completed tool call prints a permanent line above the status line:

→ web-search ✓ 1.2s
→ file-write ✓ 0.3s
→ web-search ✗ 0.8s — connection timeout

These lines scroll up as more calls complete. The status line stays pinned at the bottom.

When the agent finishes, the status line is replaced with a final summary:

✓ Done · 18s · 3,412 tok · 4 calls · $0.0021

Or on failure:

✗ Failed · 5s · 800 tok · 1 call · $0.0004

Cost is always shown — including $0.0000 for local models — so the line format is consistent.

These print as permanent scrollback lines immediately above the status:

⚠ High entropy detected
✗ Max iterations exceeded
ℹ Reactive Intelligence — Telemetry enabled

During the think phase, press t or T to expand a 4-line panel showing the tail of the model’s current reasoning stream:

the most relevant paper appears to be "Attention Is All You Need"
(Vaswani et al., 2017), which introduced the transformer architecture.
I should also check for more recent work on sparse attention and
linear attention variants before writing the summary.
[t: collapse thinking]
⠸ Thinking... iter 2 8s 980 tok $0.0008 [t: collapse]

Press t again to collapse it back to the single-line preview. The panel collapses automatically when a tool call starts or a new iteration begins.

KeyAction
t / TToggle think panel open / closed
Ctrl+CExit the process immediately
Featuremode: "status" (TUI)mode: "stream" (plain)
OutputSingle updating lineScrolling log lines
Think textCollapsible panelStreamed tokens to stdout
Tool resultsScrollback linesLog lines
ANSI escape codesYes (TTY only)No
Good forInteractive terminals, demosCI, piped output, server logs
Auto-selected whenstdout.isTTY === truestdout.isTTY === false
import { ReactiveAgents } from "reactive-agents";
const agent = await ReactiveAgents.create()
.withName("research-assistant")
.withProvider("anthropic")
.withReasoning({ maxIterations: 10 })
.withTools()
.build();
// Run in an interactive terminal — StatusRenderer starts automatically.
// Press `t` during execution to expand the think panel.
const result = await agent.run(
"Find the three most-cited papers on retrieval-augmented generation and summarize each in two sentences."
);
if (result.success) {
console.log(result.output);
} else {
console.error("Agent failed:", result.error);
}
await agent.dispose();

Sample terminal output during execution:

→ web-search ✓ 1.4s
→ web-search ✓ 0.9s
→ web-search ✓ 1.1s
⠦ Thinking... iter 4 18s 2,104 tok $0.0019 entropy 0.31 ↓ [t: expand]

After completion:

→ web-search ✓ 1.4s
→ web-search ✓ 0.9s
→ web-search ✓ 1.1s
✓ Done · 23s · 2,891 tok · 3 calls · $0.0026

makeStatusRenderer is exported from @reactive-agents/observability for advanced use cases where you want to drive the renderer manually (custom CLI tools, testing, etc.).

import { makeObservableLogger, makeStatusRenderer } from "@reactive-agents/observability";
import { Effect } from "effect";
const logger = await Effect.runPromise(makeObservableLogger({ live: false }));
const renderer = makeStatusRenderer(logger, process.stdout);
await Effect.runPromise(renderer.start());
// Feed events to the logger — the renderer reacts automatically.
// Push LLM text deltas into the think panel:
renderer.pushThinkChunk("Analyzing the search results...");
// Stop and clear the status line when done:
renderer.stop();

The StatusRenderer interface:

interface StatusRenderer {
/** Subscribe to the logger and start the spinner. */
readonly start: () => Effect.Effect<void, never>;
/** Stop the spinner, clear the status line, and unsubscribe. */
readonly stop: () => void;
/** Append a streaming LLM text chunk to the think panel. */
readonly pushThinkChunk: (text: string) => void;
}