Full-source re-verification of pi/statusline.md against dist/cli.js plus a live screenshot matrix. False and corrected: claude's top rule is NOT the context meter (meter is box/band only; rule elsewhere is a session-accent filler); 'Tern-only' composer-bar claim (real knob: composer.tokenRate); segmentOptions gated to preset custom (they merge under every preset - only the arrays are gated); mode segment yolo (states are Plan/Prewalk/Goal/ Vibe/Loop). Expanded: 7-preset defaults table, per-shape statusAttachment/ bottomBar model incl. plugin shape registry, contextLine per-value behavior, 27-segment catalog with triggers and options, segmentOptions schema, full statusLine.*/composer.* knob registry. Changelog entry added. Refs #5
12 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||
|---|---|---|---|---|---|---|---|---|
| reference | OMP Status Line Customization | How the omp status line actually works — presets vs custom arrays, composer-shape segment routing, the context meter (box/band only), the 27-segment catalog, and the fleet's tuned layout. |
|
2026-10-18 |
OMP Status Line Customization
Ground truth from the installed bundle (@oh-my-pi/pi-coding-agent/dist/cli.js),
verified live on blankpad 2026-10-18 and re-audited line-by-line against the
minified source the same day (see Audit log). Status knobs live
in agent/config.yml under statusLine:; the two composer knobs
(composer.shape, composer.tokenRate) under composer:. The file is
file-watched — edits repaint a running session without restart (verify
visually: niri screenshot, see niri capture recipe).
/settings (Appearance tab) previews these live with sample data.
Preset vs custom — the trap
statusLine.presetships whole configurations. **leftSegments/rightSegmentsare ignored unlesspreset: custom** (`s ? user ?? preset- preset
in the resolver). **segmentOptions` is NOT gated** — user options merge over the preset's own options under every preset.
| preset | leftSegments | rightSegments | separator |
|---|---|---|---|
default |
pi vim model mode collab stream path git pr context_pct cost | session_name | powerline-thin |
minimal |
vim path git | session_name mode context_pct | slash |
compact |
vim model mode git pr | session_name cost context_pct | powerline-thin |
full |
pi vim hostname model mode path git pr subagents | session_name cache_hit token_in token_out token_rate cache_read cost context_pct time_spent time | powerline |
nerd |
pi vim hostname model mode path git pr session subagents | session_name token_in token_out cache_read cache_write token_rate cost context_pct context_total time_spent time | powerline |
ascii |
vim model mode path git pr | session_name token_total cost context_pct | ascii |
custom |
your statusLine.leftSegments |
your statusLine.rightSegments |
powerline-thin |
The built-in default for statusLine.leftSegments/rightSegments themselves
is the ascii-era pair (vim model mode path git pr / session_name token_total cost context_pct) — a config carrying those arrays with a named
preset is running the preset, not the arrays. The fleet runs preset: custom.
Composer shape routes the segments
composer.shape decides where the two arrays render. Eight shapes ship in
the bundle; the plugin API can register more (zG() = built-ins + a live
registration map). Internally each shape carries statusAttachment,
bottomBar, bottomBarGap:
| shape | attach | bottom bar | leftSegments | rightSegments |
|---|---|---|---|---|
band (fleet, default) |
top-band | none | merged single row above editor, left | same row, right (session_name skipped — title rides the working row) |
box |
top-border | none | box top border | box top border |
claude |
top-rule-chip | left | bottom strip, left | top rule chip, right-aligned |
rule |
top-rule-chip | left + gap | like claude, gap above strip | like claude |
pi |
none | full | single bottom bar | single bottom bar |
borderless |
none | full | single bottom bar | single bottom bar |
field / rail |
none | full + gap (filled surface) | single bottom bar | single bottom bar |
The chip/strip layouts paint their rules as session-accent fillers, not
meters — the context meter exists only in box and band
(see next section). The top-rule chip renders right segments only, the bottom
strip left segments only.
The working row is composer chrome, not segment-addressable: session title +
shimmer, plus the tok/s readout when composer.tokenRate: true (default
false). The readout is estimated from streamed deltas, corrected by billed
output counts; the bundle also mentions a native-composer-bar placement in
filled styles.
The context meter
statusLine.contextLine (enum off / percentage / annotated /
embedded, default embedded) tunes the rule between the segment groups.
The knob description says "box composer only" — that is wrong: the meter
renders in box and band; in every other shape the rule is a constant
session-accent filler regardless of this setting (live-verified claude with
off vs embedded: identical, and not context-proportional).
In box/band:
off→ plain accent rule (single color, full width).embedded→ the inline labeled meter:60% ——— × 200K. RequirescontextWindow > 0, acontext_pct/context_totalsegment present in either array, and at least one non-context segment. The context segment(s) are hoisted out of the arrays — they never render as text in this mode.percentage/annotated→ unlabeled proportional meter painted into the rule (live-verified in band: colored fill ∝ usage at the rule's right end);context_pctstays a normal text segment.
Fill length ∝ min(100, contextPercent); >100% paints the raw percent and
shortens the meter. Meter/segment color thresholds scale with window size —
warning at ≥ min(50 %, 150K tokens), purple ≥ min(70 %, 270K), error ≥
min(90 %, 500K); below that the theme's statusLineContext color.
Segment universe (27)
pi, status, model, mode, path, git, pr, subagents,
token_in, token_out, token_total, token_rate, cost, context_pct,
context_total, time_spent, time, session, hostname, cache_read,
cache_write, cache_hit, session_name, usage, collab, stream, vim.
Zero/inactive segments render nothing — dead weight costs no width.
| segment | renders | trigger / notes |
|---|---|---|
pi |
brand glyph + turn-elapsed | while streaming; focused-agent ghost id (warning color) when a subagent is focused |
status |
hook/extension statuses, dot-joined | extension pushes via ctx.ui.setStatus() (ordered by key); suppressed in footer when statusLine.showHookStatus: false |
model |
display name + thinking level |
suffix hidden by segmentOptions.model.showThinkingLevel: false; statusLine.compactThinkingLevel: true (default) swaps in a compact glyph; fast-mode, advisor/tariff badges attach here |
mode |
Plan / Prewalk / Goal / Vibe / Loop … |
only in those modes (paused variants add a pause glyph); there is no yolo/auto-approval display in this segment |
path |
cwd or project/worktree label |
see Path segment |
git |
branch + dirty badges | +N staged, *N unstaged, ?N untracked (segmentOptions.git.showStaged/showUnstaged/showUntracked, default true); showBranch: false hides branch text and keeps badges (git icon carries them); color flips on dirty |
pr |
#<number> hyperlink |
only once the session has an associated PR |
subagents |
count | hidden at 0; when subagents are actively running the composer paints its own live badge and this segment self-suppresses |
token_in / token_out |
cumulative session input/output tokens | hidden at 0; cache_read / cache_write likewise cumulative |
token_total |
input + output + cache_write + orchestration_in + orchestration_out |
hidden at 0 |
token_rate |
N.N tok/s |
hidden when not streaming (the working-row readout is a separate, composer.tokenRate-gated thing) |
cost |
session cost | includes subagent-tree + advisor cost; ↑/↓ peak/off-peak arrow for time-priced providers |
context_pct |
NN.N%/200K (or window alone when percent is unknown) |
hoisted into the meter in embedded box/band; auto-compact speculation icon rides along |
context_total |
absolute context tokens | also hoisted by the embedded meter |
time_spent |
active session time | hidden under 1 s; this is active time, not wall clock |
time |
wall clock | segmentOptions.time.format: "12h"/"24h", showSeconds |
session |
first 8 chars of session id (new before resume exists) |
resume-by-id handle |
session_name |
session title, accent | in band it renders on the working row instead |
hostname |
short hostname | accent-tinted with session color |
usage |
rate-limit windows | 5h / daily / 7d / monthly bars + tier badge, when the provider reports them |
collab |
collab:N / collab guest:N |
only in collab sessions |
stream |
● LIVE <viewers> / ● REC |
only while streaming/recording |
vim |
mode glyph/word | only while vim keybindings are active; selected-line count appended ( 3L); display icon vs text comes from the vim runtime, not config |
Path segment and stripWorkPrefix
segmentOptions.path.stripWorkPrefix (default true — the code tests
!== false) gates two behaviors in the path renderer:
- Worktree sessions:
truenever shows the disk path — it renders the logicalprojectName/worktreeNamelabel (bareprojectNamewhen the current branch is named after the worktree) with the worktree glyph.falseopts out and shows the real cwd with a folder glyph, plus a↳ relativeRepoRootsuffix inside a repo. Base-directory-only is not renderable in either mode. - All sessions:
trueuses the pretty display path (scratch sessions get the scratch-folder icon);falseuses the raw absolute cwd.
abbreviate (default true: home → ~; false keeps the full path —
also affects tool labels/errors per upstream docs) and maxLength (clip,
default 40) apply afterwards in both modes. The fleet runs true — logical
label, worktree glyph (verified live 2026-10-18; an earlier trial of false
for the full cwd was rejected by eye).
Status knobs (registry-verified)
statusLine.preset · .leftSegments · .rightSegments · .segmentOptions ·
.separator (powerline powerline-thin slash pipe block none
ascii; default powerline-thin) · .contextLine · .sessionAccent
(default true; hashes the session name into the editor border and the
accent-tinted segments/meter color) · .transparent (terminal background,
no status bg fill) · .showHookStatus · .compactThinkingLevel (default
true). Plus symbolPreset at top level (unicode nerd ascii — segment
glyphs follow it).
Fleet configuration (2026-10-18)
- Shape
band: one merged status row. Left group:session_name→working row (skipped),vim,mode,hostname,path,git(no branch text — worktree branches are autogenerated names),pr. Right group (array order = paint order, so the last entry hugs the screen edge):model,cost,context_pct,time_spent—context_pcthoists out of the run to paint the labeled inline meter (60% ——— × 200K) between the groups undercontextLine: embedded. - Deliberately cut:
token_rate(working-row readout covers it),time(panel clock),cache_*(noise),context_total(implied by the meter),subagents(live badge supersedes it),session,token_in/token_out(cumulative totals are in/sessionanyway). models.yml: frank-archon's displaynameslimmed to"frank-archon"(endpoint suffix dropped); frank-dragoon/frank-zealot keep their endpoint suffixes.name:is free-form display,id:is the wiring — renaming ids is the cutover procedure, see Pi Models.
Audit log
2026-10-18 full-source re-audit corrected the first pass of this page:
claude shape's top rule is the context meter— false. The meter code path runs only forbox/band; claude/rule/pi/borderless/ field/rail rules are constant session-accent fillers. (Earlier reading mistook the accent rule for a meter.)working-row composer-bar variant is "Tern-only"— false; the tok/s readout is thecomposer.tokenRateoption (default false), title placement is composer chrome.— false; options merge under every preset, only the segment arrays are gated.segmentOptionsgated topreset: custom— false; states are Plan/Prewalk/Goal/Vibe/Loop.modecovers yolocontextLine"(box composer only)" — the bundle description is wrong; band honors it too (verified live:offplain rule,percentagerule meter,embeddedinline meter, screenshots + code).