DSH HUB
首页插件商店插件包社区排行榜资源发布指南
插件源码
返回插件目录

JohnXu22786 /

statusline

已验证

Real-time terminal statusline for agent harnesses: model, context usage, sub-agents, rate limits and session time in one line (zero dependencies).

★ 0 Stars0 Forks0 IssuesN/A 社区评分0 已确认安装
查看 GitHub
README来源: main@7e56737f

简体中文

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.json self-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-file or STATUSLINE_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):

  1. Scan / install plugin directories, read manifest.json;
  2. Validate id uniqueness and entry.command executability; replace {dir} with the plugin directory's absolute path;
  3. 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-file temp 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;
  4. Consume stdout: terminal-line forms pass through for display; jsonl forms 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 65001 in the session and reopen the terminal.
  • watch keeps 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.
  • stream exits with an error immediately? It means stdin is a terminal, not a pipe — this mode requires feeding like Get-Content state.json | python -m statusline stream.
  • Want a narrower status line? --width 0 hides the bar; combine with --layout model,context,clock to drop unneeded segments. Note --width 0 alone leaves the percentage showing — set context.percent to false (via config file or STATUSLINE_PERCENT=0) to get numbers-only used / limit.
  • How do other tools consume the state? --emit jsonl outputs 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.

—/ 5

暂无评分

已验证 DSH bundle

Commit 7e56737fc961

社区评论

还没有评论,来写第一条。

DSH HUB

社区维护的 DSH 插件索引。不是 GitHub 或 DeepSeek AI 的官方产品。

社区资源API关于