@underactive/pi-topping-statusline
oh-my-pi's powerline statusline as a pi extension with some customizations
Package details
Install @underactive/pi-topping-statusline from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@underactive/pi-topping-statusline- Package
@underactive/pi-topping-statusline- Version
0.4.0- Published
- Nov 3, 2026
- Downloads
- 936/mo · 100/wk
- Author
- underactive
- License
- MIT
- Types
- extension
- Size
- 359.1 KB
- Dependencies
- 1 dependency · 3 peers
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/underactive/pi-topping-statusline/main/media/poster.png",
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-topping-statusline
OMP's powerline statusline, ported as a Pi extension with modifications.

The bar renders into the editor's
top border: π > model · thinking level > path > git > PR (with the default
powerline-thin separator), with the session name (hue hashed from the name) right-aligned
over a border-colored fill. The box's bottom border carries configurable left/right
segment groups — by default a scroll hint on the left, and pi's footer stats plus a context
usage graph on the right.
Install
pi install npm:@underactive/pi-topping-statusline
Then /reload (or restart pi). Requirements: a Nerd Font and a
truecolor terminal for the default look — the Symbols setting in
/topping-statusline-settings (unicode or ascii) drops the font requirement.
The bar's color depth follows pi's terminal detection, so PI_TRUE_COLOR and the
terminal.trueColor setting pick 24-bit or 256-color output for it too.
Configuration — /topping-statusline-settings
/topping-statusline-settings opens a settings TUI with a live preview of the box's top and
bottom bars. Requires TUI mode. Settings persist to ~/.pi/agent/pi-topping-statusline/settings.json
and apply live.
╔═[ Pi Topping Statusline: Settings ]════════════════════════════╗
╟─ Preview ──────────────────────────────────────────────────────╢
║ ║
║ ╭── π > ⬢ Sonnet · ◉ max > pi/pi-topping > main ── session ──╮ ║
║ │ │ ║
║ ╰── ↑ 3 more ── ↑ 12.4K ↓ 3.1K R 148K W 12K 92.3% $0.42 42%──╯ ║
║ ║
╟─ Global ───────────────────────────────────────────────────────╢
║ ❯ [■] Transparent Segments ON ║
║ Separator ‹ powerline-thin › ║
║ Symbols ‹ nerdfont › ║
║ Border style ‹ rounded › ║
║ [■] Rainbow border on max thinking ON ║
║ [■] Animate rainbow border ON ║
║ Model routers switchyard › ║
║ [■] NVIDIA-green border when using model routers ON ║
║ [■] Animate NVIDIA-green border ON ║
║ [ ] Embed status spinners OFF ║
║ [■] Embed compaction progress ON ║
║ ║
╟─ Top Left Segment Group ───────────────────────────────────────╢
║ [■] Pi symbol ON ║
║ [■] Model ON ║
║ [■] Routed model ON ║
║ [ ] Provider OFF ║
║ [■] Thinking level ON ║
║ [■] Path ON ║
║ [■] Git ON ║
║ [■] PR ON ║
║ ║
╟─ Top Right Segment Group ──────────────────────────────────────╢
║ [ ] Token rate OFF ║
║ [■] Session name ON ║
║ … Bottom Right / Bottom Left groups — 10 more rows ║
║ ║
╟─ Feeds ────────────────────────────────────────────────────────╢
║ 1. type my-extension/savings ║
║ 1. field savedUsd ║
║ 1. prefix CS ║
║ 1. format ‹ currency › ║
║ 1. remove this feed ║
║ + add feed ║
║ ║
║ ↑↓ move ←→ cycle ␣ toggle ⏎ apply/edit esc cancel ║
╚════════════════════════════════════════════════════════[ 1/37 ]╝
| Section | Settings |
|---|---|
| Global | Transparent Segments · Separator (powerline powerline-thin slash pipe ascii) · Symbols (nerdfont unicode ascii — stored in settings.json as nerd/unicode/ascii) · Border style (rounded heavy double single) · Rainbow border on max thinking · Animate rainbow border · Model routers (switchyard plus your own provider ids) · NVIDIA-green border when using model routers · Animate NVIDIA-green border · Embed status spinners · Embed compaction progress |
| Top Left Segment Group | Pi symbol · Model · Routed model · Provider · Thinking level · Path · Git · PR |
| Top Right Segment Group | Token rate · Session name |
| Bottom Right Segment Group | Feeds · Token rate · Pi stats · Cache hit bar · CH bar segments (3–10) · Context bar · Context stats |
| Bottom Left Segment Group | Scroll hint · Feeds · Token rate |
| Feeds | One subscription per row: type · field · prefix · format, plus add/remove |
| Defaults | Transparent on · Separator powerline-thin · Symbols nerdfont · Border style rounded · Rainbow border on · Animate rainbow border on · Model routers switchyard · Routed model on · NVIDIA-green border when using model routers on · Animate NVIDIA-green border on · Embed status spinners off · Embed compaction progress on · CH bar segments 4 |
Cache hit bar renders a logarithmic graph and its CH percentage as a separate bottom-right segment. CH bar segments sets its length, from 3 to 10 (default 4). Only a 0% hit rate leaves it empty; any hit fills the first cell, the second fills at 50% and the last at 99.2%, and the cells between are spaced evenly on a log scale of the miss rate, so every length keeps its resolution near 100%. At five segments the cells fill at above 0%, 50%, 87.5%, 96.9% and 99.2%, each quartering the remaining miss rate. The filled cells share one color picked from the hit rate at any length: red below 50%, orange below 87.5%, yellow below 96.9%, then a subdued green. When the Cache hit bar is enabled, the repeated CH field is removed from Pi stats; the configured separator sits between Pi stats, the cache-hit segment, and the context graph.
With Rainbow border on max thinking on (the default), cycling the thinking level to max
replaces the border's fixed theme color with a rainbow: a full hue cycle distributed around the
box perimeter. Animate rainbow border is also on by default, so the hues flow around the
border (~14s per rotation), like Apple Intelligence's screen border. Turn animation off to keep
the rainbow at a fixed color phase without its repaint timer; the settings preview uses a stable
phase too. Any other thinking level keeps the normal theme border color. Disable animation over
slow SSH links or in terminals with expensive redraws while retaining the rainbow border.
When the active model's provider is one of the Model routers and NVIDIA-green border when
using model routers is on (the default), the border uses an NVIDIA-green gradient from #84c51a to
#0b3d20. Green takes precedence over the max-thinking rainbow and ignores thinking level; turn
this setting off to restore the normal theme or rainbow border. Animate NVIDIA-green border (on
by default) sweeps the gradient around the box on the same ~14s cycle as the max-thinking rainbow,
independently of Animate rainbow border; turn it off to keep the gradient at a fixed color phase
without its repaint timer.
Model routers lists the providers that count — services that route requests to other providers'
models. switchyard is built in and can't be edited or removed. To add one, turn on Provider in
the Top Left Segment Group to see the id pi reports for the active model, then press ⏎ on
Model routers to open its popup. + add router adds an empty row above it; move up to that row,
press ⏎, type a provider id, and press ⏎ again. Edit an existing N. provider row the same way, or
press ⏎ on remove this router to delete it. Ids can't contain spaces or commas, can't repeat
(ignoring case), and can't be switchyard; Done (⇥ to reach the buttons) refuses an invalid list
and shows why. Done returns to the settings menu, and the list is saved when that menu is applied;
Cancel or Esc discards the popup's edits. User-defined ids are stored as "modelRouters": ["openrouter"], with switchyard implied; invalid or repeated entries in a hand-edited file are
dropped on load.
Routed model (on by default) appends the model a router actually served after the selected model,
for example sy-small → anthropic/claude-sonnet-4.5. It appears only when Model is enabled and
the selected provider is listed in Model routers. It reflects the latest assistant response and
is restored from the latest response when resuming a session. A response that reports no model or
selecting a different model clears the suffix. It hides while an embedded status spinner is visible,
since pi-topping's loader names that response model there. If the API or host does not report a
response model, only the selected model is shown.
Embed status spinners (off by default) moves pi's status indicators out of their own row and into the top-left group, right after the Pi symbol and its chevron. On pi 0.86 and later this covers the working, retry, compaction, and branch-summary spinners; pi 0.85 embeds only the working indicator. The configured model, provider, and thinking details remain visible after the status; path, git, and PR step aside while a status is visible, and the Pi symbol never moves. An appearing status slides out from behind the Pi symbol's chevron over 300ms, pushing the model details right; one that replaces a status still on screen swaps in place without sliding. A cleared status is held briefly so handoffs between spinners do not flash the left segments. Only the working status cross-fades over 750ms; stable model details remain solid, and the message-style spinners cut after the hold. While the bar hosts the working status, it notifies pi-topping. Pi-topping's loader slides its response model out the same way; elsewhere, that model appears at once.
A status too long for the bar is truncated. The working spinner keeps pi's bare cut, while the message-style spinners use an ellipsis. Focusing the embed row in the settings preview shows the longest compaction sample. Requires pi 0.85 or later. Toggling the setting reinstalls the editor through pi, so it applies immediately, even mid-response. Only the statusline's own editor opts in: when another extension owns the editor slot and is wrapped, pi keeps its standalone status row.
Embed compaction progress (on by default) tracks pi's built-in summarizer for /compact,
threshold, overflow, and extension-triggered compactions that use it. Pi runs
session_before_compact handlers in sequence, so if an earlier-loaded extension computes the
summary in its own handler, the bar appears only after that handler returns. In the bottom-right
group, a green progress bar takes the place of the context graph, followed
by the tokens being compacted against the context window, 86K/131K (66%). The bar starts just
below that usage and drains as the summary progresses. In place of pi's stats, 47% summarized · 16.3s shows the share summarized so far and the time elapsed. The bar and its label follow the
Context bar and Context stats toggles. The summarized share is a time-based estimate: pi does not
expose the summarizer's progress, so it starts at 10%, eases up to 86%, and reaches 100% only
when pi completes. At completion, the empty bar and 100% summarized hold for a moment. Then pi's stats and the
context graph return, and the graph shows the compacted context as a ~ estimate until the next
response. If the compaction fails or is cancelled, the usual segments return at once on pi 0.84.3
and later; earlier hosts keep the bar until the next prompt. The bottom bar does not change when
the setting is off, when Pi stats, Context bar, and Context
stats are all off, or when the terminal is too narrow for the stand-ins. In every case pi's own
compaction indicator still shows, in its own row or, with Embed status spinners on, in the top
bar. While the setting is on, focusing its row in the settings preview shows the bottom bar
mid-compaction.
Segments
Ported with pi data: pi, model (model · provider · thinking level), path
(worktree/scratch-dir aware; always strips ~/Projects and /work prefixes — not exposed in
the settings TUI), git (branch + *unstaged +staged ?untracked, HEAD
fs-watch), pr (via gh, hidden if missing), session_name, token_rate (live tok/s —
accent while active, held 1.5s, faded 0.5s, then a dim --- tok/s placeholder; estimate
pipelined from pi-topping's word-count EMA; available in the top-right, bottom-left, and
bottom-right groups), pi_stats, context_graph (bar + stats), scroll_hint, plus feeds
(documented below), with compaction_info and compaction_graph standing in for pi_stats and
context_graph while a compaction is hosted. On pi 0.86 and later, pi_stats
includes cache-warming refreshes folded
into pi's own footer totals, so cache R/W and cost can increase while idle; this segment matches
the host footer by construction.
Between a compaction and the next response, pi reports context usage as unknown. Rather than
vanish for that stretch, context_graph shows pi's own estimate of the compacted context, the
summary plus the kept messages sized by pi's estimator, marked with ~ (~9.2%/131K). pi's
measured figure replaces it after the next response.
When the terminal narrows: right segments drop first, then an embedded status is truncated (the model details after it drop only if the status would otherwise vanish), then the path shrinks (to ~8 cells), then left segments drop end-first — the path is always the last to go.
Feeds
feeds surfaces values other extensions publish as custom session entries, so the bar can
carry figures this extension knows nothing about. There is one subscription list, shared by
the Bottom Right and Bottom Left Feeds toggles — enabling both shows the same values twice.
Each subscription is four fields, all configurable in the settings TUI:
| Field | Meaning | Example |
|---|---|---|
| type | the publisher's customType |
my-extension/savings |
| field | property of the entry's data to read |
savedUsd |
| prefix | literal label drawn before the value | CS |
| format | currency, number, or text |
currency |
In settings.json the first column is persisted as customType; the settings TUI labels it
type. That example renders CS$1.44. The prefix is glued directly to the value, so write
it as TOK when you want TOK 42; leading and trailing spaces survive, while escape
sequences and control characters are stripped.
The list ships empty. The currency rules come from the savings feed that
pi-topping-compact (formerly pi-prompt-cache, a private extension) used to publish: figures
under half a cent are hidden, matching that extension's own footer cutoff. That floor hides the negative values published
early in a session, before the cache-write premium is amortized by reads. Above the floor,
currency shows two decimals below $100 and whole dollars above.
A feed contributes nothing when its publisher is absent, has published nothing this run, or
reports a value its format suppresses; the segment disappears entirely once every feed is
quiet, so it costs nothing to leave switched on. Feed values are shown in dim text; a value
that remains unchanged for five minutes fades into the statusline background over 500ms and
then hides. It reappears in dim text only when that configured field's value changes.
Publishers typically reset per process run,
so entries predating the current session start (--resume, /reload, /new, fork) are
ignored rather than shown as a stale total. During steady-state rendering, the session is
re-read at most once every two seconds, because pi copies the whole entry list on each read
while the bar re-renders on every keystroke; changing subscriptions or starting a new
session forces an immediate scan.
Publishing a feed
Any extension can expose a value to this bar with one call and no dependency on this package — the contract is a pi custom session entry, not an API of ours:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
let published = 0;
pi.on("turn_end", () => {
const savedUsd = currentSavings();
// Publish only when the figure actually moves: consumers read the newest
// entry, never the history.
if (Math.abs(savedUsd - published) < 0.01) return;
published = savedUsd;
pi.appendEntry("my-extension/savings", { savedUsd });
});
}
A user then subscribes with type my-extension/savings, field savedUsd, and whatever
prefix and format they like.
Use appendEntry. It writes a type: "custom" entry, which is what the bar looks for.
It checks the entry kind before the customType, because custom message entries carry a
customType field too — so a value pushed through sendMessage is never read.
Make data an object. The subscription names a field, and the bar reads data[field].
A bare number or string published as the whole payload cannot be addressed. Several fields
in one entry are fine; each can be subscribed to separately, and one entry can feed several
segments.
Match the value to the format. currency and number need a finite number and ignore
anything else, so a pre-formatted "$1.44" string only works under text. Publish the raw
number and let the user pick the presentation.
Publish at turn boundaries, not per token. A high emission rate buys no extra freshness, and every append writes to the session file.
Re-publish after a session start if your figure carries over. As noted above, entries
written before the consumer's session_start are ignored, so after --resume the segment
stays empty until you publish again. Per-run counters get this for free; a lifetime counter
should emit once on the first turn of each run to become visible again.
Nothing appears in the transcript. Custom entries are excluded from LLM context, and pi
renders nothing for a customType with no registered entry renderer — a feed costs a line
in the session file and no screen space.
Name the type after your extension (your-extension/metric) to avoid collisions, and
document the field names and units — those are the strings your users type into the
settings TUI.
Caveats
- Other editor-replacing extensions are wrapped rather than raced — their editor renders inside this box — but ones that draw their own bar or chrome will clash visually.
- The palette is OMP's dark theme; light terminal themes will look off.
- pi-topping-compact is no longer needed: the bar tracks compactions from pi's own events. If it is still installed, its above-editor widget shows alongside the bar, so uninstall it.
Development
Requires Node >= 22.19 (the test runner uses --import with TypeScript resolution).
ln -s "$(pwd)" ~/.pi/agent/extensions/pi-topping-statusline
npm install && npm run typecheck && npm test
Attribution
The statusline design and most of the rendering code come from oh-my-pi (MIT, © 2025 Mario Zechner, © 2025–2026 Can Bölük), itself a fork of Mario Zechner's pi. This port is MIT as well.
