pi-display-runtimes
Durable timing, token, and cost accounting for pi: compact transcript rows after every model request, tool call, compaction, branch summary, and multi-step agent run; a live below-editor activity widget while pi is busy; and /timings for a per-operation i
Package details
Install pi-display-runtimes from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-display-runtimes- Package
pi-display-runtimes- Version
1.0.0- Published
- Sep 13, 2026
- Downloads
- 187/mo · 187/wk
- Author
- prawnbear
- License
- MIT
- Types
- extension
- Size
- 89.2 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-display-runtimes
A pi extension that tells you where the time, tokens, and money went — for every operation, durably, in every session.
When a coding-agent run finishes, pi tells you what the model said. It does not tell you that the request took 8.4 seconds, that 44k of the input was prompt-cache reads, that the tool batch ran 14 seconds of wall time inside a two-minute total, or that you waited 20 minutes before sending the next prompt. This extension records exactly that, and keeps the records in the session so they survive restarts.
It adds three things:
- Timing rows in the transcript. One compact, dim row after each model
request, tool call, compaction, branch summary, and multi-step agent run:
duration, tokens by billing category, and cost. Expand any row with
app.tools.expand(Ctrl+O by default) for a full detail view — stream phases, exact token counts, exact cost, status. - A live activity widget below the editor. While pi is busy, a one-line widget shows what is happening right now — the current model request, compaction, or up to three parallel tools, each with its running elapsed time — refreshed every 250 ms. It disappears when pi settles and never touches the footer.
/timings. A picker over every record on the active branch; pick one (or run/timings last, or/timings <number>) to open its detail view.
No configuration. Nothing to enable. Start pi and it works.
Quick start
Drop the package in
~/.pi/agent/extensions/pi-display-runtimes/(pi auto-discoversextensions/*/index.ts) — or install it from your package sources withpi install.Send any prompt. Watch the rows appear under each assistant reply:
⏱ model 8.40s · ↑3.21k ↓812 R44.10k · $0.0237With tool use, the same request yields tool rows and a total:
⏱ model 2.10s · ↑5.00k ↓120 R38.90k · $0.0031 bash node --test extensions/pi-display-runt... · 1.24s read checkpointing/README.md · 0.01s ⏱ total 24.31s · 3 model calls · 2 tools · ↑12.4k ↓3.1k R120k $0.0891Press
Ctrl+Oto expand a row into its detail view.Run
/timingsto browse every record on the branch, or/timings lastto jump straight to the newest.
How to read a timing row
⏱ model 8.40s · ↑3.21k ↓812 R44.10k · $0.0237
⏱marks a model-side row (model, total, compaction, branch summary, user response). Tool rows are deliberately unmarked and indented, so they are never mistaken for the model request around them.8.40s— wall-clock duration, two decimals, unit escalates automatically (s → m → h → d).↑/↓— input / output tokens for that request.R/W— prompt-cache reads / writes, shown when present.$— cost, as calculated by pi from provider usage and model pricing metadata (4 decimals; sub-$0.0001costs render as<$0.0001).- A trailing word (
error,aborted,interrupted) appears whenever the operation did not end cleanly.
Tool rows show the tool name and a preview of its primary argument (path,
command, or pattern — collapsed to one line, capped at 50 characters, and
only for tools whose argument key is known, so nothing sensitive leaks
from unknown tools). A tool that makes its own nested model call and reports
usage gets that usage on its row: nested LLM ↑100 ↓10 $0.0020.
A user-response row is written for each prompt you send while pi was idle:
⏱ replied after 3.27m
What gets measured
| Row | Kind | When it is written |
|---|---|---|
⏱ model … |
one assistant turn | after each model request finalizes |
tool … |
one tool execution | after each tool's result message |
⏱ total … |
whole agent request | when a multi-step run settles |
⏱ compaction … |
context compaction | after each compaction |
⏱ branch summary … |
branch summary | after each /tree navigation that summarizes |
⏱ replied after … |
user think-time | when you submit after pi was idle |
For each model request, the detail view additionally breaks the stream into phases, measured from the events pi receives:
- First stream event — request start to the first event (the "time to first token" the provider actually exposes);
- Thinking stream — total time covered by
thinking_*events; - Text stream — time covered by
text_*events; - Tool-call stream — time covered by
toolcall_*events.
A multi-step request (several model calls and tool batches) records each
child operation and adds a total row, so no single tool is blamed for time
that belonged to the whole run. The total's usage is the sum of every model
call, every nested model call reported by tools, and compaction usage, with
statuses merged by severity (interrupted > aborted > error >
success).
What the numbers mean — and don't
- Tokens are per-request, not per-phase. Providers report usage once per request, so phases have separate durations but share one token total.
- A user message has no token estimate. The next request's input also contains system instructions, tools, and prior context; assigning it backward would be fiction, so the extension doesn't.
- Durations come from a monotonic clock (
performance.now), so NTP adjustments and clock skew cannot distort them; wall-clock timestamps are stored separately for display and resume. - Agent totals never double-count. Totals sum model, nested-tool, and compaction usage only; tool wall time lives on tool rows.
Persistence, resume, and the wait anchor
Every completed record is appended to the session as a custom
operation-timing entry. These are TUI metadata: they render in the
transcript, are fully branch-aware (/timings shows only the active
branch), and never enter model context.
On resume, the renderer reconstructs the rows from the session file, and
/timings works immediately.
User-wait tracking survives restarts too. When a run settles, the exact
settlement time is recorded (a visible total row for multi-step runs, or a
hidden operation-timing-wait-anchor entry for simple ones). Your next
interactive prompt is measured from that moment — even if you sent it hours
later, after closing pi. Anchors render as nothing.
Two caveats: an --no-session run cannot preserve records across processes,
and an operation cut short by a hard kill is lost if it never finalized —
already-written records stay durable. Graceful shutdowns (quit, /resume,
/fork, Ctrl+C) finalize any in-flight operation as interrupted first.
Boundaries
The extension measures what pi exposes, nothing more:
- startup time before extensions load is invisible;
- provider-internal stages that emit no stream events are not measurable — the phases show when pi received events;
- work inside uninstrumented third-party extensions is only visible if it reports usage through a tool result;
- pi also renders its own native elapsed line for Bash; during the originating process you may see both. Pi's line belongs to the immediate Bash renderer; this extension's row is the consistent, persisted, expandable, branch-aware account.
Validation
From the package root:
npm test # unit tests for the pure helpers (20 tests)
npm run smoke # full lifecycle simulation with a fake clock:
# multi-step runs, parallel tools, errors, interrupted
# shutdown, restart reconstruction, /timings, widget
npm run check # strict type check against the installed pi types
Both test scripts run in plain Node — no TypeScript toolchain and no local
node_modules needed. They load the extension through jiti, the same
transform pi itself uses, aliasing the @earendil-works/* peer packages
to the installed pi distribution, so they exercise the code exactly as pi
runs it. The installed pi package is found via PI_CODING_AGENT_DIR, the
directory tree above the scripts, or the global npm root.
Files
| File | Purpose |
|---|---|
index.ts |
the extension: event wiring, record lifecycle, live widget, /timings |
helpers.ts |
pure formatting, usage math, record schema, resume reconstruction — unit-testable without pi |
scripts/test.mjs |
unit tests for helpers.ts |
scripts/smoke.mjs |
lifecycle simulation over the real index.ts with a stubbed pi API |
scripts/lib/load.mjs |
jiti-based module loader shared by both scripts |