# pi-signal-footer

[简体中文](README.md) | English

A readable status footer for [Pi Coding Agent](https://github.com/earendil-works/pi): model, tokens, cache, cost, context bar, streaming rate, and MCP/LSP status.

Examples below use a 160-column terminal. Colors follow the host theme, and narrower terminals reflow the layout.

## Appearance

**classic** (default)

```text
C:/Users/dev/agent-demo · fix-context-bar  │  opencode-go › ◎ deepseek-v4-flash-0731 │ ✦ max │ ⎇ main                      ⎔ 12% [━━──────────────────] 36k/300k
↓ 220 ↑ 5.4k │ ↻ 51k (96.92%) ✎ 1.4k │ $0.087 │ ◷ 2m · 1 turn · 45 tok/s                                                                           ⇄ MCP tools 2
```

**vivid** (`/footer theme`)

```text
📁 C:/Users/dev/agent-demo · fix-context-bar  │  opencode-go › 🐳 deepseek-v4-flash-0731 │ 🧠 max │ 🔀 main               📊 12% [━━──────────────────] 36k/300k
📥 220 📤 5.4k │ 🔄 51k (96.92%) 📝 1.4k │ 💰 0.087 │ ⏳ 2m · 💬 1 turn · 🚀 45 tok/s                                                             🔌 MCP tools 2
```

LSP appears on demand and is hidden without an active or failed status.

Requires Pi Coding Agent >=0.84.4. Older hosts keep the native footer.

Every line is truncated to the terminal width before it is returned.

If a render throws, the footer falls back to the previous frame re-truncated to the current width.

The legend is five lines, 77 columns at its widest, so it never wraps in an 80-column terminal.

## Install

```sh
pi install npm:pi-signal-footer
```

| Use | Command |
| --- | --- |
| Pin a version | `pi install npm:pi-signal-footer@<version>` |
| Update extensions | `pi update --extensions` |
| Install from a tag | `pi install git:github.com/chengzhi-c/pi-signal-footer@v<version>` |

## Configure

Settings live in `pi-signal-footer.json` under Pi's agent directory (usually `~/.pi/agent/`). A missing or invalid file falls back to these defaults.

```json
{
  "enabled": true,
  "locale": "auto",
  "theme": "classic",
  "showProject": true,
  "showSessionName": true,
  "showDuration": true,
  "showTurns": true,
  "showSpeed": true,
  "showBranch": true,
  "showCacheRatio": true
}
```

| Key | Default | Effect |
| --- | --- | --- |
| `enabled` | `true` | Enable this footer |
| `locale` | `"auto"` | UI language: `auto` / `zh` / `en` |
| `theme` | `"classic"` | Appearance: `classic` / `vivid` |
| `showProject` | `true` | Project path |
| `showSessionName` | `true` | Session name |
| `showDuration` | `true` | `◷` work time |
| `showTurns` | `true` | Turns |
| `showSpeed` | `true` | `≈` live rate |
| `showBranch` | `true` | Git branch |
| `showCacheRatio` | `true` | `↻` cache hit ratio |

Only the commands that change a setting rewrite this file.

`classic` is white readouts on a gray skeleton with amber cost (first example above). `vivid` uses emoji glyphs and pastel syntax colors (second example above). Both keep the same layout: gray is reserved for chrome, and alarm colors stay on context thresholds and failures (cost excepted).

## Commands

`/signal-footer` and the short alias `/footer` work identically.

| Command | Effect |
| --- | --- |
| `/footer` | Show the metric legend (same as `/footer legend`) |
| `/footer legend` | Show the metric legend |
| `/footer hide` | Hide the legend (this session) |
| `/footer help` | Show every command |
| `/footer off` | Restore the native footer |
| `/footer on` | Enable this footer |
| `/footer status` | Show the current settings |
| `/footer locale` | Set the UI language: `auto` / `zh` / `en` |
| `/footer theme` | Switch theme: `classic` / `vivid`; omit to toggle |
| `/footer <item>` | Show or hide one item; omit `on` / `off` to toggle |

Items: `path`, `session`, `time`, `turns`, `speed`, `branch`, `cache`.

`off` and `on` persist across sessions. Turning off clears transient stream/rate/compaction state and hides the legend.

The legend is session UI, independent of `enabled`. `/footer off`, `/footer hide` and session end clear it.

## Readings

- `≈` marks an **approximation**, not a lower bound. The live rate and the in-flight `≈+N` suffix on ↑ include visible text, thinking and tool-call arguments.
- Fixed densities: prose/thinking ≈4 chars/token, tool-call JSON ≈2, CJK/Hangul ≈1. They can over- or underestimate; hidden reasoning is not observable.
- Changed positive provider output calibrates the live count, then only new visible content is estimated. A valid positive output at end becomes `+N` until that same message lands.
- A message replaced by another extension, or an entry array shortened by a fork, ends the reading. Missing, invalid or zero usage does not make a non-empty estimate exact.
- The live rate uses token and time deltas over the same observed span. One sample has no rate, and empty updates do not start the first-output clock.
- It updates only on chunks, not renders. The continuous window excludes long pauses; a pause holds the last measurement, and the short resume period may reuse it.
- Sparse chunks without a mature window use a request-wide observed average. At end, output ÷ first-observed-output-to-end time is frozen; request start is the fallback if no output was observed.
- This is observed throughput, not a provider speed benchmark.
- Token, cache, cost and turn totals cover **all entries in the current session file**. Same-file `/tree` navigation keeps spent tokens from abandoned paths; `/fork` creates a new file with the selected path, not a total across every fork.
- Turns = user messages, including steering.
- `◷` is estimated agent work time. While enabled, `agent_start` and `session_before_compact` append a small work-start custom entry, outside LLM context, to exclude preceding idle time.
- The work marker keeps live, landed and reloaded readings consistent. Human gaps and gaps before model/thinking/name/label changes are excluded; each counted gap caps at 15 minutes.
- Old unmarked history still uses entry gaps and cannot reconstruct every idle boundary. An `aborted` response rewinds entry-time minus message-start-time, at most its counted gap; `error` does not.
- Failed or cancelled compaction without a result leaves no added completed work.
- `↻` is accumulated cache-read tokens. The parentheses are the **latest landed assistant request with usable input dimensions**, not the current streaming request.
- The ratio is `cacheRead / (input + cacheRead + cacheWrite)`, to two decimals. All-zero or unreported input dimensions carry over the last known rate (stale, never a faked 0.00%); usable input but no read reports 0.00%.
- Context comes from the host's current-branch `getContextUsage()`, which may combine reported usage and new-message estimates. Unknown values stay unknown.
- A reported window of 0 or NaN falls back to the model's `contextWindow`; `?` shows only when neither is available.
- Cost sums recorded `usage.cost.total`, not a verified invoice. Colors follow the displayed rounded context percentage: ≥50% warn, ≥75% error.

## MCP / LSP status

This footer reads public extension statuses and MCP tool inventories, not private connection state. Known LSP failures come first, then explicitly failed or partially connected MCP servers, then normal/unknown statuses in stable key order; unknown text keeps its original colors.

Zero MCP connections can mean idle lazy connections, not failure. The LSP server list is capped at three names plus `+N`, so the status block does not stretch and no half-cut name sits at the edge.

Status examples: explicit MCP attention states color the ratio and `✗N` red; LSP failures are also red.

| State | classic | vivid |
| --- | --- | --- |
| MCP needs attention | `⇄ MCP 1/2 ✗1 · tools 2` | `🔌 MCP 1/2 ✗1 · tools 2` |
| Tool inventory only | `⇄ MCP tools 2` | `🔌 MCP tools 2` |
| Active LSP | `⇄ MCP tools 2 · LSP typescript` | `🔌 MCP tools 2 · 🛠️ LSP typescript` |
| Failed LSP (shown first) | `LSP ✗ typescript · ⇄ MCP tools 2` | `🛠️ LSP ✗ typescript · 🔌 MCP tools 2` |

- **LSP**: published active or failed statuses only, in full (`LSP Active: …` / `LSP Failed: …`) or compact (`LSP ✓` / `LSP ✗`) form. Failures take priority; `LSP Inactive` or an unpublished status is hidden.
- Absence alone does not establish server health. If a published active/failed status is still missing, widen the terminal to rule out clipping.
- **MCP health**: a published connected/enabled status takes priority. The native contract `MCP native C/E failed F` renders as `MCP C/E`, with a red `✗F` when servers need attention (failed, disconnected, awaiting authentication).
- Healthy connections take `text` in classic and `success` in vivid; partial connections warn; zero without an explicit failure is dimmed.
- Native health can include `· tools N` for discovered `mcp__` tools. Compact ratio statuses are also supported; recognized `MCP 0/0` is hidden and suppresses inventory fallback.
- A bare `N/M connected` is read as a ratio only when its status key names MCP; a connection pool under another key stays an ordinary status.
- Pi does not publish a connected/enabled status, so built-in MCP shows only the tool count. The ratio appears when an extension publishes a supported status.
- **MCP inventory**: without a recognized health status, `MCP tools N` shows public `getAllTools()` entries whose names start with `mcp__`. Tools join as their servers connect, so the count grows over the first seconds of a session.
- Labels stay muted; the number takes the input hue (`text` in classic, cyan in vivid). Inventories are not connection health counts.
- While this footer is active, public in-memory inventories are checked once per second. A tick that finds a different status than the last one any path read requests a redraw; disposing the footer stops the check.
- No servers or network are polled. Missing or unavailable APIs do not fabricate a zero.

Native health requires the host to publish a supported status. Server configuration and tool discovery alone do not establish health.

This package only reads status; it does not patch the host or create connections. Inventory can update without a health publisher when the extension is loaded and the host provides compatible public inventory APIs, but this is not a guarantee for every future host version.

A config-only server may have no indicator before discovery. Use the host's `/mcp` command for server details, and widen the terminal to rule out clipping.

## Development checks

| Command | Effect |
| --- | --- |
| `npm run check` | Tests and strict type checking |
| `npm run bench:footer` | Microbenchmark including the host's shallow `getEntries()` copy |
| `npm run bench:estimate` | Measures controlled streams |
| `npm run bench:reality -- <session-directory>` | Read-only recorded-entry replay; formula/render consistency checks |
| `npm run pack:check` | Checks package contents |

These local microbenchmarks are not end-to-end Desktop performance measurements. Without a directory, `bench:reality` scans nothing; estimate/output statistics are descriptive, not independent tokenizer accuracy or ground truth.

## License

[MIT](LICENSE)
