@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

Packages

Package details

extension

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 setStatus entry into a single line and strips newlines, so it cannot render multiple lines. This package uses ctx.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) with status-bus.ts, which composes the lines; TUI and RPC use widgets, with a single-line setStatus fallback 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 at 10 / 30 / 50 / 80 / 120 / 200 tok/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 /reload and 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), ⚡ streaming shows tokens arriving, and ⏳ waiting shows 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: ~ means ceil(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.for key in globalThis; session_shutdown unregisters segments and clears the status area.
  • Because pi's extension loader uses moduleCache: false, extensions in the same package get separate module instances — status-bus therefore keeps its registry on a globalThis symbol 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 via peerDependencies — 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 default count 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 💬 45m on line 0 (time since the conversation was created; restored on reload/resume, reset on /new).
  • Streaming badge ⚡ streaming on line 0, shown while the reply is streaming tokens; shown next to 🔧 <tool> when both are active.
  • Waiting badge ⏳ waiting on 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: formatDuration moved into status-bus.ts and is shared by both extensions. agent-timer.ts still re-exports it for compatibility.
  • Docs: README (EN / 中文) updated for the new status-area layout, event table and legend.

License

MIT © xiaosat