pi-jefferson
A starship-style statusline for pi — instruments, a real layout, and per-instrument formatting.
Package details
Install pi-jefferson from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-jefferson- Package
pi-jefferson- Version
0.8.0- Published
- Nov 8, 2026
- Downloads
- 2,731/mo · 2,731/wk
- Author
- joelleibow
- License
- MIT
- Types
- extension
- Size
- 62.9 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions/jefferson.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-jefferson
Pre-1.0. A statusline footer of configured rows for pi. Install with
pi install npm:pi-jefferson.
A starship-style statusline for pi: instruments you place in a row's cells, a real layout, and per-instrument formatting.
The name is a nod to the lineage — Jefferson Airplane became Jefferson Starship became Starship.
Status
The statusline is a footer of one line per configured row, each spanning the full width. By default
a cell takes the width its content needs and its neighbours sit against it. blank cells and cells
with a $bar fill whatever the row leaves, and a row option restores evenly
divided cells. The configuration table lists the available instruments, their
variables and default formats. With no configuration you get the default single row of
placeholder cells.
Install
pi install npm:pi-jefferson
Then /jefferson reports the footer this session installed and how many rows it renders.
Configuration
Create pi-jefferson.json in pi's agent directory, or .pi/pi-jefferson.json in a trusted
project. The project document replaces the agent document whole; the two are not merged.
Each cell names its instrument; instruments configures each instrument once by name for every
cell that uses it:
{
"rows": [
{
"cells": [
{ "instrument": "model" },
{ "instrument": "context", "align": "right" }
]
}
],
"instruments": {
"model": {
"format": "🧠: $model from $provider",
"style": { "fg": "accent", "bold": true }
},
"context": {
"format": "$bar $percent"
}
}
}
A row sizes its cells by its distribution, either "content" or "equal". Without it a row is
"content":
{ "distribution": "equal", "cells": [{ "instrument": "model" }, { "instrument": "path" }] }
With "content", a cell takes the columns its text needs, label included, so neighbours sit against
it. Cells that fill (blank, and any cell whose format holds $bar) share the columns the row leaves,
the earliest taking any odd one. When no cell fills, the last left-aligned cell with text takes them,
else the first cell with text, else the first cell. A cell with no text, because its data is unknown,
takes no columns. Two neighbours that both have text get one column between them, and that column
gives way before any text does when the row is too narrow. No column goes between them when a filling
cell sits there, or when the row's spare columns do. A cell with no spare columns has nothing for
align to pad, so align changes nothing in it.
With "equal", every cell gets the same share of the width, and the earliest cells take one extra
column each when the width does not divide. A value that is neither rejects the document, like a bad
align: the default row renders and the warning names the value. distribution belongs on a row; beside
a rows list it rejects the document.
Upgrading: earlier releases divided every row evenly. Set "distribution": "equal" on a row to keep
that.
format is plain text with the instrument's already-formatted $variables, not Markdown. $ and
| are its two special characters, escaped \$ and \|. Control characters become spaces, then
whitespace at the front and back of a format is removed; whitespace inside it is text. Omit format
to use its default:
| Instrument | Variables | Default format |
|---|---|---|
blank |
None | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor |
model |
$provider, $model |
$provider: $model |
thinking |
$level |
$level|capitalize |
path |
$path |
$path |
context |
$tokens, $window, $percent, $bar |
$percent/$window |
tokens |
$input, $output, $cacheRead, $cacheWrite, $totalTokens |
$totalTokens |
cost |
$cost |
$cost |
Every instrument also accepts label, a string placed in front of its format, whether the format
is configured or the default. It is literal text: $ means nothing in it, and $label is not a
variable (a format that names it rejects the document). Each control character in it becomes one
space, then whitespace at its front and back is removed, and it takes the instrument's style. A
label that is not a string rejects the document.
"thinking": { "label": "🤔" }
That renders 🤔 High. A label followed by a format always gets one space between them, so you never
write it yourself. The space takes the label's style, counts toward the cell's width, and comes out
of what $bar has left. A label that is empty after the trim, label: "" included, shows no label
and no space. A format that is empty or only whitespace leaves the label alone, with no space after it.
Only thinking has a default label, Thinking:, so its default cell reads Thinking: High. A
configured label replaces the default, and label: "" removes it. When an instrument's data is
unknown the cell renders nothing, label included. With known data and "format": "", the cell is the
label alone.
Upgrading: the default label also applies to a thinking format you had already configured. If that
format has no label, the cell now starts with Thinking: and one space. Set label: "" to get the
previous text back. A space you wrote at the front or back of a label or a format is now removed, and a
label's one space comes from the gap.
A |name after a variable, or after the text in front of it, modifies that text. Modifiers chain,
each applying in the order the format writes them. On plain text a modifier covers the run of
non-whitespace characters directly before it, so a multi-word phrase takes a modifier on each word:
🧠 $model via|dim $provider|capitalize
Here via renders dim and the provider's value is capitalized.
| String modifier | Effect |
|---|---|
capitalize |
Upper-cases the first character and leaves the rest alone: high becomes High |
upper |
Upper-cases the whole value: high becomes HIGH |
lower |
Lower-cases the whole value: HIGH becomes high |
A | begins a modifier only when a name follows it directly. A name is a run of letters, digits
and underscores, so 10|20 and $model|_x start a modifier and an unknown one rejects the
document. Followed by anything else (a space, the end of the format, $, | or other
punctuation) the pipe is the character itself, so $model | $provider renders
claude-opus-4 | anthropic. Where a literal pipe has to sit beside a name, write \|.
That escape changed meaning in this release. A format written \| used to render \|, because a
backslash before anything but $ was a literal backslash; it now renders |.
$bar is available on context: it takes the width the rest of the format leaves in the cell,
and shrinks to nothing when none is left. A cell with a $bar fills the row's spare columns, sharing
them with any blank cell; drop the $bar from its format and the cell takes only its text's width.
Use at most one per format, and no modifier after it. Its filled portion changes to the theme's
warning color above 70% and error above 90%; without an estimate from the host, it draws an
unfilled, dim track.
style uses pi's ThemeStyle: fg accepts foreground theme color tokens, bg background
tokens, or either slot can use a color pi's parser accepts. The six boolean attributes are
bold, dim, italic, underline, inverse and strikethrough, and each is also a modifier
inside a format. A modifier only adds: $model|bold renders bold even where the instrument's
style sets bold: false, and nothing in a format switches an attribute off. fg and bg are
not modifiers; colors belong to style.
thinking's $level is the one segment style does not fully control. It takes the foreground
pi gives the thinking rules on the input box for the current level: the theme's thinkingOff
through thinkingMax token, read from the theme current at render time. That replaces a configured
fg on $level only. A configured bg and the boolean attributes stay, and the label and any other
text keep the instrument's fg.
If the extension cannot read a document's style or format, it rejects the whole document, renders
the default row, and warns with a message naming the offender. The offenders include a modifier it
does not implement, a modifier on $bar, and a modifier with nothing before it.
Why another statusline
pi-powerline-footer is excellent and is the reason this exists. It renders its row as a
joined string with a fixed set of presets, which means segment spacing is three columns
by construction, a segment cannot carry another segment's value, and the left / right
/ secondary groups are one flat list rather than rows or columns.
This package is for the case where you want a layout — rows, alignment, per-gap spacing, a stated truncation priority — and per-instrument formatting, with the pi session's own data behind each instrument.
Development
Trunk-based: main is the only long-lived branch, and every merge to it is a release.
pnpm install
pnpm typecheck
pnpm test
Tests live in tests/, never beside a source file. The pi.extensions manifest in package.json
names the one entry, extensions/jefferson.ts, and pi loads only what the manifest lists, so
the files under extensions/jefferson/ are never entries. A test file there would still ship in the
package, because files includes extensions.
@earendil-works/pi-coding-agent and @earendil-works/pi-tui are peerDependencies, not
dependencies, because pi supplies them at runtime. A physical copy bypasses pi's module
mapping and duplicates classes and registries; pi reports a warning when it sees a
manifest configured that way.
lodash-es is the package's one runtime dependencies entry. Its upperFirst, toUpper and
toLower perform every string mutation a modifier makes. pi installs a package's dependencies when
it installs the package, so it arrives with this one rather than shipping in the tarball; why this
build and not the CommonJS one is
ADR 0004.
A workstation cannot publish
publishConfig.provenance is true, and provenance is signed from a CI OIDC provider.
A workstation has neither that provider nor a trusted-publishing credential, so the
identical pnpm publish fails before it reaches the registry. Publishing is CI's job,
and that is enforced by the mechanism rather than by a note in a file.
Releasing
Merging to main releases. There is no separate release step and nothing to remember to
do.
The bump comes from a label on the pull request: release:major, release:minor, or
release:patch. No label means a patch. It defaults low on purpose — an unlabelled change
should never ship a major version by accident.
The base comes from the git tags, not from package.json. Main accepts no direct
pushes, so the workflow cannot commit an updated version back to it, and package.json
would drift from what actually shipped. Every release writes a v… tag, and those tags
are the record. Specifically, the base is the highest release tag reachable from the
commit before the one being released, which makes a re-run compute the same version it
did the first time.
Authentication is npm trusted publishing over OIDC. The workflow holds no token and the
package accepts publishes from this repository's release.yml, so there is nothing to
rotate and nothing to leak. Provenance is attached automatically.
License
MIT