JohnXu22786
statusline
Real-time terminal statusline for agent harnesses: model, context usage, sub-agents, rate limits and session time in one line (zero dependencies).
- Stars
- 0
- Language
- Python
- Created
- Aug 16, 2026
- Updated
- Aug 17, 2026
Introduction
statusline
A real-time terminal status line for plugin-based agent harnesses. Model name, context usage bar, sub-agent count, rate-limit state, session duration, current time and other key information are compressed into a single continuously refreshed line; it also emits structured JSON for scripts, monitoring panels, or other plugins to consume directly.
- Zero third-party dependencies: pure Python standard library (>= 3.8), no external packages;
- Zero network requests: the render path performs no network I/O; data comes only from state fed in by the host;
- Three output forms: terminal line (with ANSI coloring) / single JSON / JSONL stream;
- Three refresh strategies:
once(render once and exit),watch(poll a state file),stream(event-driven); - Configurable: display items and order (layout), progress-bar width and style, thresholds, theme, separator — layered overrides via config file / environment variables / command line;
- Lenient yet rigorous: unknown fields in snapshots are always ignored (forward compatible), wrong types in known fields fail fast, and no missing field can crash rendering;
- Self-contained: the root
manifest.jsonself-describes the plugin contract, so any plugin-based harness can load it — see "Integrating with plugin-based harnesses".
deepseek-chat · ██████▊ 42% · 2/5 · 200 req left · 01:17:06 · 05:17:06
Installing in DSH
The plugin is published for DeepSeek Harness (dsh). Install it into a profile with:
dsh plugin --profile demo add github:JohnXu22786/statusline
Remove it with:
dsh plugin --profile demo remove statusline
Directory layout
statusline/
├── manifest.json # Plugin contract (self-describing manifest; harness reads this to load)
├── pyproject.toml # Optional: packaging metadata for pip install
├── README.md
├── LICENSE
├── .gitignore # Only excludes build artifacts (__pycache__ etc.)
├── statusline/ # Implementation package (pure standard library)
│ ├── __init__.py # Version number
│ ├── __main__.py # python -m statusline entry point
│ ├── cli.py # Argument parsing, three run modes, exit codes
│ ├── config.py # Three-layer config merge and validation
│ ├── snapshot.py # State snapshot contract parsing and normalization
│ ├── metrics.py # Pure functions: progress bar / threshold levels / duration formatting
│ ├── theme.py # Color themes (role -> ANSI code)
│ ├── segments.py # Display item registry (one function per segment)
│ ├── emit.py # Line assembly and output (terminal / JSON / JSONL)
│ └── errors.py # Unified exception hierarchy
├── tests/ # Standard-library unittest, 147 test cases (+ node:test bridge cases)
└── examples/
├── state.example.json # Example snapshot (data fed to the plugin)
├── config.example.json # Example config
└── harness.example.py # Full loading-flow demo (see "Integrating with plugin-based harnesses")
Installation
No installation needed to run; the repo directory works as-is:
# Option 1: run directly (no install)
python -m statusline once --state-file examples/state.example.json
# Option 2: pip install (optional, provides the statusline command)
pip install -e .
statusline once --state-file examples/state.example.json
Quick start
# Render once: feed a snapshot file
python -m statusline once --state-file examples/state.example.json
# Render once: feed a snapshot from a pipe, output structured JSON
Get-Content examples/state.example.json | python -m statusline once --emit jsonl
# Continuous refresh: host periodically writes snapshots to state.json, plugin polls and re-renders
# (on first run state.json does not exist yet; the plugin prints one stderr hint and keeps waiting — expected)
python -m statusline watch --state-file state.json
# Event-driven: host pushes snapshots line by line (recommended integration, see below)
Get-Content snapshots.jsonl | python -m statusline stream --emit jsonl
Command line
statusline [global options] {once|watch|stream}
| Option | Description |
|---|---|
--config FILE | Config file path; when omitted, auto-reads statusline.json in the current directory (if present) |
--emit {terminal,json,jsonl} | Output format. json and jsonl serialize identically, one frame per line |
--theme {default,mono,vivid} | Color theme |
--no-color | Disable ANSI colors |
--state-file FILE | State snapshot file (single JSON object, or JSONL taking the last line) |
--layout A,B,C | Display items and order, comma-separated |
--interval SECONDS | Refresh interval in watch mode |
--width N | Context bar width (0..200; 0 hides the bar, leaving percent or numbers) |
--version | Print version |
Three modes:
once(default): render once and exit. State source falls back in order--state-file→ stdin pipe → empty snapshot (hint only);watch: periodically re-render the same line. When the state file is unavailable (missing / parse failure) it keeps an empty snapshot and prints one stderr hint, then recovers automatically once the file appears. Requires--state-fileorSTATUSLINE_STATE_FILE;stream: read JSONL snapshots line by line from stdin, render one line each. Exits with an error immediately when stdin is a terminal (prevents accidental hangs); unparseable lines are skipped with a hint, without interruption.
Exit codes: 0 normal (including intentional Ctrl-C exit); 1 unexpected error; 2 usage error; 3 config or state-data error.
TTY behavior: watch / stream refresh the current line in place on a terminal (carriage return + clear line + colored line); under a pipe / redirection each frame is a standalone line; once always prints a full line.
State snapshot (input contract)
The single data surface between host and plugin. A snapshot is a JSON object shaped as follows (protocol name statusline/snapshot-v1, all fields optional):
{
"model": "deepseek-chat",
"session_id": "sess-20260816-001",
"started_at": "2026-08-16T04:00:00+08:00",
"context": {"used": 27500, "limit": 65536},
"agents": {"active": 2, "total": 5},
"limits": {"rate_limited": false, "retry_after": null, "remaining": 200}
}
| Field | Type | Description |
|---|---|---|
model | string | Current model name; the model segment hides when missing |
session_id | string | Session identifier; passed through into JSON output verbatim |
started_at | number / ISO-8601 string | Session start time, Unix seconds or ISO string |
context.used | int ≥ 0 | Tokens used; alternatively give context.input + context.output and let the plugin sum them |
context.limit | int > 0 | Context window limit; when missing only the used token count is shown |
agents.active | int ≥ 0 | Number of running sub-agents |
agents.total | int ≥ 0 | Cumulative spawned sub-agents |
limits.rate_limited | bool | Whether rate-limited |
limits.retry_after | number ≥ 0 | Suggested retry seconds while rate-limited |
limits.remaining | int ≥ 0 | Remaining request quota |
Parsing rules: unknown fields are always ignored (forward compatible); wrong types for known fields, negative numbers, non-finite values (NaN/Infinity), and invalid timestamps raise SnapshotError immediately (exit code 3) — dirty data never reaches rendering. Integer values accept integral floats like 100.0; context.limit = 0 counts as missing; an explicit null counts as an unset field. Any missing field simply hides the corresponding display segment, never crashes.
Configuration
Three-layer override: defaults < config file < environment variables < command line. The config file is JSON; a full example lives in examples/config.example.json:
{
"theme": "default",
"color": true,
"separator": " · ",
"layout": ["model", "context", "agents", "limits", "elapsed", "clock"],
"context": {"width": 16, "bar": "blocks", "percent": true},
"thresholds": {"warn": 0.75, "critical": 0.9},
"clock": {"format": "%H:%M:%S"},
"emit": "terminal",
"refresh": {"interval": 1.0}
}
| Key | Default | Description |
|---|---|---|
theme | default | default / mono / vivid |
color | true | Whether to colorize; false is equivalent to forcing mono |
separator | " · " | Separator text between display segments |
layout | all six on | Display items and order: model / context / agents / limits / elapsed / clock |
context.width | 16 | Progress bar cells (0..200) |
context.bar | blocks | blocks (eight-level block chars) or ascii ([##--]) |
context.percent | true | Whether to show the percentage after the bar |
thresholds.warn | 0.75 | Warn color at or above this fill ratio |
thresholds.critical | 0.9 | Critical color at or above this fill ratio |
clock.format | "%H:%M:%S" | strftime format of the clock segment |
emit | terminal | Output format (same as --emit) |
refresh.interval | 1.0 | watch refresh interval in seconds (> 0) |
Environment variables (prefix STATUSLINE_):
| Variable | Maps to |
|---|---|
STATUSLINE_THEME / STATUSLINE_COLOR / STATUSLINE_EMIT / STATUSLINE_SEPARATOR | same-named scalar keys |
STATUSLINE_LAYOUT | layout, comma-separated |
STATUSLINE_STATE_FILE | state file path |
STATUSLINE_WIDTH / STATUSLINE_BAR / STATUSLINE_PERCENT | context.* |
STATUSLINE_WARN_AT / STATUSLINE_CRITICAL_AT | thresholds.* |
STATUSLINE_CLOCK_FORMAT | clock.format |
STATUSLINE_INTERVAL | refresh.interval |
Validation is strict: unknown config keys, unknown display items, warn >= critical, out-of-range widths, non-positive intervals and the like all raise ConfigError (exit code 3) with a readable reason, catching typos at startup.
Output
Terminal line
Display segments are joined by separator in layout order; segments without data hide automatically. The context segment renders as "eight-level block progress bar + percentage", colored by thresholds (ok green / warn yellow / critical red); other segments follow theme role colors.
JSON / JSONL (structured data surface)
For other tools, generated from the same source as the terminal line — the two are always consistent. One frame:
{
"ts": 1786828603.973,
"text": "deepseek-chat · ██████▊ 42% · 2/5 · 200 req left · 01:17:06 · 05:17:06",
"model": "deepseek-chat",
"session_id": "sess-20260816-001",
"elapsed_seconds": 4626.0,
"context": {"used": 27500, "limit": 65536, "fraction": 0.4196},
"agents": {"active": 2, "total": 5},
"limits": {"rate_limited": false, "retry_after": null, "remaining": 200}
}
text is a plain-text line (no ANSI) that can go straight into a tmux status block, dzen, conky, or logs; missing data fields are null. once outputs a single JSON object; under watch / stream each frame is one line (NDJSON) for line-oriented consumption.
Color themes
| Role | default | vivid | mono |
|---|---|---|---|
| model (model name) | cyan 36 | bright cyan 96 | none |
| ok / warn / critical (context bar) | 32 / 33 / 31 | 92 / 93 / 91 | none |
| agents (sub-agent count) | magenta 35 | bright magenta 95 | none |
| limit_ok / limit_bad (rate limit) | dim 2 / bright red 1;31 | bright black 90 / bright red 1;91 | none |
| clock / elapsed (time) | dim 2 | bright black 90 | none |
Integrating with plugin-based harnesses
The manifest.json at the plugin root is a self-describing contract; a harness that loads it gets everything it needs:
| Field | Description |
|---|---|
id / name / version | Plugin identity; id is used for deduplicated registration in the host |
type | renderer: a rendering plugin that only consumes state and never modifies host behavior |
entry.command | Launch command; the {dir} placeholder is replaced with the plugin directory's absolute path |
entry.cwd | Subprocess working directory (also supports {dir}); the plugin auto-loads statusline.json from that directory |
entry.mode | Suggested default run mode: stream (long-running, event-driven) |
events | Host domain events the plugin subscribes to (session / sub-agent / quota) |
input | Input contract: JSONL on stdin, schema statusline/snapshot-v1 |
output | Output contract: line-by-line stdout; terminal-line passes through for display, JSON forms are for aggregation |
lifecycle | Supported run modes: oneshot / watch / stream |
config | Config file conventions and environment variable prefix |
Loading flow (harness-side convention):
- Scan / install plugin directories, read
manifest.json; - Validate
iduniqueness andentry.commandexecutability; replace{dir}with the plugin directory's absolute path; - Launch the subprocess per
entry.mode(stdin piped, stdout piped, stderr logged):stream(recommended): a long-running process. The host translates subscribed events into snapshot JSON and writes them line by line to stdin; the plugin renders once per line; the process exits naturally at EOF;oneshot: spawn a process per event, pass the snapshot via a--state-filetemp file or stdin, read the rendered result and reap it; suitable for very low event frequency;watch: the plugin polls a state file maintained by the host; the host only needs to write the file periodically — suitable when the host would rather not manage subprocess lifecycles;
- Consume stdout:
terminal-lineforms pass through for display;jsonlforms can be aggregated, filtered, and forwarded to other components; stderr content is treated as logs.
Suggested event → snapshot mapping (adapter-layer implementation; event names are the subscription declarations in manifest.events, payloads parsed per host conventions):
| Host event | Snapshot fields |
|---|---|
session/event (session start / end) | started_at, context, agents, limits |
telemetry/usage (usage report) | context.used (or input+output), limits.remaining |
agent/request, agent/finished (sub-agent spawn / finish) | agents.active, agents.total |
limits/changed (rate limit / quota change) | limits.rate_limited, limits.retry_after, limits.remaining |
Fields update incrementally per event — the plugin only renders what the snapshot provides; missing means the segment hides.
Full demo: examples/harness.example.py implements a minimal harness in pure standard library, walking the whole flow "read manifest → resolve entry → launch stream subprocess → translate simulated events into snapshots and feed them → consume rendered lines":
python examples/harness.example.py
dsh bundle: the repo ships an actual Node bridge (index.js + dsh.bundle + cordis.patch.yml) for DeepSeek Harness (dsh). After dsh plugin --profile demo add github:JohnXu22786/statusline, the statusline_render tool appears in the model's toolset: it takes a snapshot-v1 object (all fields optional) and renders the same line the CLI would, returning a structured JSON frame (with a plain text field) or a plain line with emit=terminal. Non-zero Python exits and invalid snapshots surface as readable tool-level errors instead of crashing the host.
To integrate on dsh, mount this directory as a plugin directory (or publish it per host conventions), then write a ~ten-line bridge plugin: in apply(ctx) launch python -m statusline stream and assemble the session / sub-agent / quota event payloads into snapshots written to stdin per the table above; if the host has no status-line component of its own, once mode can be hooked onto any event hook instead.
Development and testing
python -m unittest discover -s tests -v
Self-check points in the implementation: progress bar rounds to 1/8 blocks, threshold boundaries (warn includes left, excludes right), duration-format boundaries (59s / 59:59 / 23:59:59 / day switch past 24h), three-layer config override, snapshot lenient/strict boundaries, TTY vs non-TTY output behavior, Windows encoding safety (entry points normalize to UTF-8).
FAQ
- Garbled Chinese or block characters on Windows? The plugin entry points normalize stdio to UTF-8; if your terminal still shows mojibake, run
chcp 65001in the session and reopen the terminal. watchkeeps hinting the state file is unavailable? The hint appears once and is expected; it recovers automatically once the file appears. Confirm the host actually writes that path.streamexits with an error immediately? It means stdin is a terminal, not a pipe — this mode requires feeding likeGet-Content state.json | python -m statusline stream.- Want a narrower status line?
--width 0hides the bar; combine with--layout model,context,clockto drop unneeded segments. Note--width 0alone leaves the percentage showing — setcontext.percenttofalse(via config file orSTATUSLINE_PERCENT=0) to get numbers-onlyused / limit. - How do other tools consume the state?
--emit jsonloutputs one JSON line per frame; any script can parse line by line. - What if the snapshot has no
context.limit? The context segment degrades to "used token count" (e.g.27,500 tok), no bar, no percentage.
License
MIT — see LICENSE.