A Rust TUI that shows TTFT, current/average output rate, tool runtime, and stall state for live LLM coding-agent sessions on one screen.
- Current scope
- Install and run
- Reading the metrics
- Per-tool wiring
- CLI commands
- Architecture
- Privacy
- Known limitations
Phase 1 and Phase 2 cover eight tools plus Grok Build, all on one normalized event model.
| Phase | Tool | Input surface | Auto-discovery |
|---|---|---|---|
| 1 | Pi | RPC/session JSONL | ~/.pi/agent/sessions/**/*.jsonl |
| 1 | Factory Droid | stream JSON/JSON-RPC, hook | process |
| 1 | Gemini CLI | hook, telemetry JSON | process |
| 1 | Claude Code | hook, transcript JSONL | ~/.claude/projects/**/*.jsonl |
| 2 | Codex CLI | rollout JSONL | ~/.codex/sessions/**/rollout-*.jsonl |
| 2 | OpenCode | server/SSE/run JSON | process |
| 2 | Qwen Code | hook, telemetry JSON, daemon event | process |
| 2 | Kiro CLI | hook, ACP JSON-RPC | process |
| Extension | Grok Build | hook, streaming JSON, ACP, updates JSONL | ~/.grok/sessions/**/updates.jsonl |
Automatic process discovery estimates session presence, PID, and project path. TTFT and output rate are computed only when structured events or a session file are attached.
Install the latest release into ~/.local/bin (no Rust toolchain, no sudo):
curl -fsSL https://raw.githubusercontent.com/bengHak/llmeter/master/scripts/install.sh | shPin a version:
curl -fsSL https://raw.githubusercontent.com/bengHak/llmeter/master/scripts/install.sh | LLMETER_VERSION=0.1.3 shDefaults and overrides:
- Install path:
~/.local/bin(override withINSTALL_DIR) - Ensure
~/.local/binis on yourPATHif the installer prints a hint - Manual downloads: GitHub Releases
Supported platforms:
- macOS arm64 (
aarch64-apple-darwin) - Linux x86_64 (
x86_64-unknown-linux-gnu) - Linux arm64 (
aarch64-unknown-linux-gnu)
Not supported in this installer: Windows; macOS Intel (x86_64). Binaries are not notarized—on macOS you may need to clear quarantine (xattr -d com.apple.quarantine ~/.local/bin/llmeter) if Gatekeeper blocks the binary.
Review before piping (optional):
curl -fsSL https://raw.githubusercontent.com/bengHak/llmeter/master/scripts/install.sh -o install.sh
less install.sh
sh install.shThe installer verifies the downloaded tarball against the release SHA256SUMS before installing.
Requires Rust 1.85 or later.
cargo build --release
./target/release/llmeterAfter install, run llmeter with no extra setup: it discovers supported CLI processes and known session stores. On first pass it bootstraps recent history; after that it only processes newly appended bytes per file. llmeter setup <tool> is optional—it adds precise hook, RPC, and OTLP events rather than being required for discovery.
Print a one-shot JSON snapshot of running processes and known session files:
llmeter --once --jsonAttach explicit JSONL sources:
llmeter --source codex:$HOME/.codex/sessions/2026/07/20/rollout-demo.jsonl
llmeter --source pi:/tmp/pi-session.jsonl \
--source qwen:/tmp/qwen-telemetry.jsonlReplay a historical file:
llmeter replay /tmp/pi-session.jsonl --tool pi --json
llmeter replay examples/normalized-session.jsonl --jsonTTFT: time from request submit to first output eventNOW: output throughput over the last 2 secondsAVG: average throughput from first to last output of the current or latest turnTOOL: cumulative tool-call runtimeSTALL: time spent producing output with no new output for at least the default 2 seconds
Each metric shows its own confidence grade:
| Mark | Grade | Meaning |
|---|---|---|
● |
Exact | Directly instrumented timestamps or token counts |
◐ |
Derived | Computed from exact values |
~ |
Estimated | Based on character volume, process signals, or indirect events |
- |
Unknown | No usable data |
Rates are token-only (tok/s). Events without token counts still advance TTFT/stall timing, but do not invent a character throughput.
See which connection methods work in your environment:
llmeter adapters
llmeter setup pi
llmeter setup claude
llmeter setup codex
llmeter setup grok-buildPi session JSONL is auto-discovered. You can also attach a file explicitly:
llmeter --source pi:/path/to/session.jsonlNormalize structured output into the journal. ingest reads stdin line by line and flushes each event batch immediately, so a running llmeter in another terminal updates before the stream ends:
droid exec -o stream-json <args> \
| llmeter ingest --tool droid --input -Attach JSONL that a hook or telemetry exporter appends:
llmeter --source gemini:/tmp/gemini-events.jsonl
llmeter --source qwen:/tmp/qwen-events.jsonlOr normalize hook-command stdin into the local journal:
llmeter hook --tool gemini
llmeter hook --tool qwenTranscript JSONL is discovered under the known default root. Lifecycle hooks can use this command sink:
llmeter hook --tool claudeRollout JSONL is auto-discovered under the default session root:
llmeter --source codex:/path/to/rollout.jsonlSave server events, SSE, or run --format json output as JSONL and attach it:
llmeter --source opencode:/tmp/opencode-events.jsonlAttach hook or ACP wire JSONL:
llmeter --source kiro:/tmp/kiro-events.jsonl
llmeter hook --tool kiroDiscovers running grok, xai-grok-pager, and xai-grok-shell processes and auto-reads interactive session files at ~/.grok/sessions/**/updates.jsonl. Headless and ACP can be wired via wrapper:
# Print hook setup and connection commands
llmeter setup grok-build
# Headless streaming JSON
llmeter wrap --tool grok-build -- \
grok --no-auto-update -p <prompt> --output-format streaming-json
# ACP stdio
llmeter wrap --tool grok-build -- \
grok --no-auto-update agent stdio
# Load saved updates into the normalized journal
llmeter ingest --tool grok-build \
--file ~/.grok/sessions/<project>/<session>/updates.jsonl
llmeter jsonGrok prompts, responses, thinking content, and raw tool I/O are not stored in the journal—only character/byte deltas and token usage remain.
llmeter [watch]
llmeter --once --json
llmeter replay <FILE> [--tool <TOOL>] [--json]
llmeter ingest --tool <TOOL> --input <FILE|-> [--output <JOURNAL>]
llmeter hook --tool <TOOL> [--output <JOURNAL>]
llmeter doctor [--json]
llmeter setup <TOOL>
llmeter adapters [--json]
Main global options:
--source <TOOL:PATH> repeatable
--journal <PATH> normalized journal path
--no-auto-discover disable process and default session-root discovery
--refresh-ms <MS> TUI refresh interval, default 250ms
--process-scan-ms <MS> process/session rescan interval, default 2000ms
--stall-threshold-ms <MS> stall threshold, default 2000ms
TUI keys:
j/k or ↑/↓ select session
p pause/resume
r refresh now
q or Esc quit
process discovery ─┐
native JSONL tail ─┼─> tool adapter ─> TelemetryEvent ─> SessionAggregator
hook journal ──────┘ │
├─> TUI
└─> JSON snapshot
The single Rust crate is split by responsibility:
src/model.rs: normalized event and session snapshot modelssrc/adapters/: parsers for nine toolssrc/discovery.rs: process and native session discoverysrc/aggregate/: TTFT / TPS / stall computationsrc/live.rs: stateful source index, incremental tail, process correlationsrc/runtime.rs: ingest, SSE, wrapper, and replay compatibilitysrc/tui.rs: Ratatui dashboard
Tool parsers never compute metrics themselves. Every parser emits only TelemetryEvents; one aggregator owns timing and state.
The default journal stores only:
- session and turn identifiers
- event kind and timestamps
- token, character, and byte deltas
- model, project, and PID metadata
- tool name and call ID
- whether an error occurred (provider error text is stripped)
Prompts, response bodies, tool arguments, API keys, and raw error messages are not written to the normalized journal. On Unix, journal directories and files are created with 0700 and 0600 permissions, and existing journal permissions are tightened to 0600 on append. Security of original CLI transcript or rollout files follows each tool’s own settings.
- There are no live smoke tests against real user environments with external CLI binaries installed. Parsers are verified with fixtures and replay tests based on official event surfaces.
- There is no network server yet that subscribes directly to OpenCode SSE, Qwen daemon SSE, or Gemini OTLP. Today events are tailed as recorded JSONL or streamed as structured stdout via
ingest. - Process and native session rows merge only when they share the same PID or a clear 1:1 relationship. Ambiguous multi-candidate cases stay as separate rows to avoid wrong merges.
- If internal session file schemas change (Codex, Claude, etc.), the corresponding parser fixtures and mappings must be updated.
- Dynamic plugin ABI, remote hosts, Kubernetes, and a web dashboard are out of Phase 1–2 scope.