@xiaosat/pi-speed-sparkline
pi extension: three-line status area with token-speed sparkline, AI output-time timer, skill counts and estimated default context usage
Package details
Install @xiaosat/pi-speed-sparkline from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@xiaosat/pi-speed-sparkline- Package
@xiaosat/pi-speed-sparkline- Version
1.6.0- Published
- Oct 21, 2026
- Downloads
- 1,130/mo · 65/wk
- Author
- xiaosat
- License
- MIT
- Types
- extension
- Size
- 71.6 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./speed-sparkline.ts",
"./agent-timer.ts",
"./skill-status.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-speed-sparkline
English | 简体中文
npm: @xiaosat/pi-speed-sparkline · Author: xiaosat · License: MIT
A pi extension package that renders a three-line status area below the editor: line 1 is the AI output time (⏱/🤖) + 🔧 tool-time breakdown + 💬 conversation age, line 2 is the response-speed sparkline + live speed + statistics, and line 3 shows skill counts + estimated default context usage.
⏱ 1m23s / 🤖 12m05s 🔧 3m40s 💬 45m
📈 ▁▂▅▇▆▃▂▁▅▇ 93.2 tok/s ↑128 ↓~61
📚 Skills 12 (project 3) system ~5.8k tok (2.9%)
Features
Status layout
- Three lines: pi's built-in footer joins every
setStatusentry into a single line and strips newlines, so it cannot render multiple lines. This package usesctx.ui.setWidget()to render a multi-line area below the editor, above the built-in footer. - Line 0: ⏱ output timer / 🤖 output total (
agent-timer.ts) + 🔧 tool-time breakdown + 💬 conversation age + right-edge badges (speed-sparkline.ts). - Line 1: 📈 speed sparkline + live/latest speed + max/median statistics.
- Line 2: loaded skill count, project-level count, and estimated system prompt size in k tokens (1k = 1,000 tokens).
- One renderer: all three extensions register "segments" (
line/order) withstatus-bus.ts, which composes the lines; TUI and RPC use widgets, with a single-linesetStatusfallback for other UI-capable modes.
Speed sparkline (speed-sparkline.ts)
- Speed sparkline: after each completed response, its average token speed (tok/s) is recorded and the sparkline scrolls left, keeping the last 10 samples. The chart is always 10 cells wide — until 10 samples exist, empty cells on the left are padded with
▁(zero height). Speeds are bucketed at10 / 30 / 50 / 80 / 120 / 200tok/s and mapped to the▁▂▃▄▅▆▇character heights (7 levels:▁below 10 tok/s,▇at 200 tok/s and above) so the trend is readable at a glance. - Live streaming speed: while a response streams, the current speed (output tokens ÷ elapsed time) refreshes in real time; the rightmost cell shows a live bar and number.
- No blank between tokens: while a tool runs or the next response is awaited, the last measured speed stays visible.
- Tool-time breakdown: line 0 shows the cumulative tool execution time (
🔧 3m40s) so you can compare "AI streaming" vs "tool (code) execution" at a glance; the segment stays a pure statistic, and the running tool's name appears as a status badge at the right edge of line 0 (⚙ bash). Overlapping tools are counted once; the clock pauses while waiting for your answer. - Persisted tool time: the cumulative tool time is persisted into the session record (
speed-sparkline-tool) and keeps accumulating across/reloadand restarts; a new session resets to zero. - Conversation age: line 0 shows
💬 45m, the time since the conversation was created; it is restored on reload/resume and reset by/new. - Status badges: at the right edge of line 0,
⚙ <tool>shows the tool that is executing (only while it runs),⚡ streamingshows tokens arriving, and⏳ waitingshows the request sent but the first token not yet arrived. Streaming and waiting are mutually exclusive; the tool badge is independent of both. - Statistics: when idle, line 1 also shows the max (
↑) and median (↓~) speed of the last 10 responses. - Token counting fallback: prefers the provider-reported
usage.output; falls back to estimating ≈4 characters per token across text / thinking / toolCall content. - Multi-model aware: each sample records its
provider/model.
AI output timer (agent-timer.ts)
- Counts only model streaming time: each measured window is one assistant message
message_start → message_end(the streaming output window). Tool execution is not counted — tools can take as long as they like. - Restarting after each tool call: after every tool call, the model's next output starts counting from zero (every assistant message is a fresh window).
- Excludes waiting for you: while a window blocks on a user-facing UI prompt (permission confirmations, multiple-choice questions,
ask_user_question, …) the clock pauses and shows⏸; the gaps between windows (tools / your typing) are not counted either. - Current / session total: after
⏱the left number is the current (last when idle) output time;🤖on the right is the sum of every output window in this session. - Refreshes every second: the value ticks live while streaming, and the UI is not re-rendered when nothing changed.
- Survives reload/restart: each settled window is persisted into the session record with
pi.appendEntry(), so/reload, a restart, or resuming the session continues accumulating; a new session (/new) resets to zero. - Interruptions count too: aborted outputs, or quitting/reloading mid-window, still settle and keep the time already spent.
Skills and default context (skill-status.ts)
Example: 📚 Skills 12 (project 3) system ~5.8k tok (2.9%).
- Skills 12: 12 successfully loaded skills, including explicit-command-only skills. Disabled, malformed or duplicate resources that pi did not load are not counted. Uses
pi.getCommands()resource metadata, not a filesystem scan; hiding skill command completions does not change the count. - project 3: 3 of those skills have
sourceInfo.scope === "project", including project packages installed outside the working directory. The other 9 may be user-level or temporary resources. - The automatically advertised skill count and its separate token estimate are not displayed; the skill catalog is already included in the system prompt estimate.
- system ~5.8k tok (2.9%): estimated size of the entire effective system prompt (including the skill section), divided by the selected model's context window. Tokens use decimal k units with one decimal place (e.g. 2,152 tokens →
~2.2k tok); percentages are calculated before rounding. Not the complete API request or conversation usage: excludes tool JSON schemas, history, messages and on-demand skill bodies. The built-in footer remains the source for conversation context usage. - Estimates, not billing:
~meansceil(characters / 4), not a provider tokenizer; results vary with language and model. Missing host APIs show?, not a misleading zero. - Refreshes on startup/reload, before each run/request and on model changes. No extra model requests, skill file reads or timers.
Robustness
- Status rendering failures are silently ignored (e.g. session reloads); speed history lives on a
Symbol.forkey inglobalThis;session_shutdownunregisters segments and clears the status area. - Because pi's extension loader uses
moduleCache: false, extensions in the same package get separate module instances —status-bustherefore keeps its registry on aglobalThissymbol key.
Install
pi install npm:@xiaosat/pi-speed-sparkline
Restart pi and the three-line status area appears below the editor.
Use pi -e npm:@xiaosat/pi-speed-sparkline to try it for a single invocation without installing.
How it works
Multi-line status area (status-bus)
Three extensions are declared in pi.extensions; all publish segments through status-bus.ts:
| Extension | line | order | Content |
|---|---|---|---|
agent-timer |
0 | 0 | ⏱ 1m23s / 🤖 12m05s (adds ⏸ while waiting for you) |
speed-sparkline |
0 | 10 | 🔧 3m40s (cumulative tool time; ticks live while a tool runs) |
speed-sparkline |
0 | 20 | 💬 45m (conversation age since it was created) |
speed-sparkline |
0 | 30 | ⚙ bash / ⚡ streaming / ⏳ waiting (rightmost; each shown only while its state is active) |
speed-sparkline |
1 | 0 | 📈 ▁▂▅▇▆▃▂▁▅▇ 93.2 tok/s ↑128 ↓~61 |
skill-status |
2 | 0 | 📚 Skills 12 (project 3) system ~5.8k tok (2.9%) |
The bus groups segments by line, sorts by order, joins a line with two spaces, and renders via ctx.ui.setWidget(key, lines, { placement: "belowEditor" }); unchanged content skips rendering.
Speed sparkline events
| Event | Behavior |
|---|---|
session_start |
clear history, register segments; restore cumulative tool time (reset on /new); set conversation start from the session header |
before_provider_request |
mark "waiting for first token" |
message_start / message_update |
start the clock, refresh live speed, clear the tool badge |
message_end |
compute average speed, push into history, scroll the sparkline |
tool_execution_start / tool_execution_end |
show the ⚙ <tool> badge at the right edge of line 0 while it runs; accumulate tool time (overlapping tools counted once) |
ui_prompt_start / ui_prompt_end |
pause/resume the tool clock while waiting for your answer |
agent_settled |
settle the tool clock and persist the cumulative tool time to the session record |
agent_end / session_shutdown |
clear in-progress state; unregister segments and clear the status area |
AI output timer events
| Event | Behavior |
|---|---|
session_start |
restore the total from the session record (reset for a new session), register segments, start the 1 s refresh |
message_start |
a new assistant message: count from zero (the window after every tool call restarts here) |
ui_prompt_start / ui_prompt_end |
pause/resume counting while waiting for the user (⏸) |
message_end |
settle the window, add it to the session total, persist it |
agent_settled |
settle an unfinished window (abnormal interruption) and persist |
session_shutdown |
stop the ticker; settle an interrupted window; unregister segments |
Timing semantics (tool time is never counted; each output window starts from zero):
window 1 starts ──► window 1 ends ──► tool execution (not counted) ──► window 2 starts (from zero) ──► ...
Prompts waiting for your answer are excluded whenever they appear, with the clock frozen (⏸).
Status area legend
⏱ 1m23s / 🤖 12m05s 🔧 3m40s 💬 45m ← line 0: output time + total + tool breakdown + conversation age (+ ⚙ tool / ⚡ / ⏳ on the right edge)
📈 ▁▂▅▇▆▃▂▁▅▇ 93.2 tok/s ↑128 ↓~61 ← line 1: sparkline + speed + stats
📚 Skills 12 (project 3) system ~5.8k tok (2.9%) ← line 2: skills + default prompt estimates
~/proj · model · 12% ctx ← pi's built-in footer
| Display | Meaning |
|---|---|
⏱ 1m23s / 🤖 12m05s |
current output window (last one when idle) / the sum of every output window this session (tool time excluded) |
⏱ 1m23s ⏸ / 🤖 12m05s |
waiting for your answer, the clock is paused |
🔧 3m40s |
cumulative tool (code) execution time, compared against ⏱; ticks live while a tool runs |
⚙ bash |
a tool is running (rightmost on line 0); disappears as soon as it finishes |
⚡ streaming |
the reply is streaming tokens (rightmost on line 0); independent of ⚙ <tool> |
⏳ waiting |
waiting for the next first token (rightmost on line 0); mutually exclusive with ⚡ streaming — one response cycles ⏳ waiting → ⚡ streaming → (done) |
💬 45m |
time since this conversation was created |
📈 ▁▂▅▇▆▃▂▁▅▇ |
speed trend of the last 10 responses (height = speed bucket; ▁ padding = no sample yet, always 10 cells) |
93.2 tok/s (line 1) |
live speed while streaming, or the latest average speed when idle / waiting |
… (line 1) |
a tool is running and no speed sample exists yet |
↑128 ↓~61 |
last 10: max speed / median speed |
Duration format: 3.4s (< 10 s) / 42s (< 1 min) / 1m23s (< 1 h) / 2h05m.
Tests
The status area and timing logic ship with a dependency-free self-test (a fake pi/ctx driven through the real event order, asserting pause exclusion, idle exclusion, persistence/restore and rendering). Additional skill-status tests cover project package scope, automatic exposure, token estimates, three-line composition, model changes, reload, RPC and headless modes. Tests live in the source repository and are excluded from the npm package:
node --experimental-strip-types agent-timer.test.ts
node --experimental-strip-types skill-status.test.ts
Dependencies
- Pi packages (
@earendil-works/pi-coding-agent,@earendil-works/pi-ai) are provided by the pi runtime viapeerDependencies— nothing extra to install. - Node.js ≥ 22.19.0
Changelog
1.6.0
- Add a third status line for loaded/project skill counts and estimated system prompt tokens in k units, including the system prompt's percentage of the model context window. Omit the separate
defaultcount and skill-token estimate. - Read pi's loaded resource metadata and effective prompt; do not scan skill files or count full skill bodies as default context.
- Add dependency-free skill-status and three-line integration tests.
- Add a leading
📈icon and space to the speed line; widen the gap between⏱and its timer value.
1.5.1
- Statistic and status separated: the
🔧segment now shows only the cumulative tool time (🔧 3m40s, still ticking live while a tool runs). The running tool's name moved out of it into a status badge at the right edge of line 0 (⚙ bash), shown only while the tool executes and gone as soon as it finishes. - Docs: the status table and legend are updated for the new layout.
1.5.0
- Timer semantics rework: ⏱ now counts only real model streaming time (one assistant message
message_start → message_end) and no longer includes tool execution. - Restart after tool calls: after each tool call the model's next output starts counting from zero.
- 🤖 session total = the sum of every output window in the session.
- Tool-time breakdown: line 0 adds
🔧 3m40s(cumulative tool execution time) next to ⏱; while a tool runs the segment also shows its name (e.g.🔧 bash 1m23s). - Layout shift: 💬 now follows the tool segment (line 0 order:
⏱ / 🤖 → 🔧 → 💬); the right-edge badges are only⚡ streaming/⏳ waiting.
1.4.0
- Conversation age
💬 45mon line 0 (time since the conversation was created; restored on reload/resume, reset on/new). - Streaming badge
⚡ streamingon line 0, shown while the reply is streaming tokens; shown next to🔧 <tool>when both are active. - Waiting badge
⏳ waitingon line 0 while the request is sent but the first token has not arrived; mutually exclusive with⚡ streaming(one response cycles⏳ waiting → ⚡ streaming → done). Line 1 no longer shows a waiting indicator. - Fixed-length sparkline: the chart always renders 10 cells; empty cells are padded with
▁from the very first response. - Tool time tracking: cumulative wall-clock tool execution time is tracked internally (parallel tools counted once, paused while waiting for your answer) and persisted to the session (
speed-sparkline-tool). It is not displayed yet. - Icon change: the session total of the timer is now shown as
🤖(wasΣ). - Shared helper:
formatDurationmoved intostatus-bus.tsand is shared by both extensions.agent-timer.tsstill re-exports it for compatibility. - Docs: README (EN / 中文) updated for the new status-area layout, event table and legend.
License
MIT © xiaosat