Statusline
SkillAI & modelsGuides your agent to safely edit the Claude Code statusline display code without breaking its layered rendering system.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Statusline skill
About this skill
Edit the Claude Code statusline renderer safely. Use when touching claude/yas/**/*.py (the yas package), claude/statusline_command.py (the entry shim), claude/mon.py, or related tests under test/. Covers the layered renderer (GradientEngine / BorderRenderer / Renderer), the SessionView gather seam (
What this skill tells your AI
The instructions your AI receives, as published by tmck-code/yet-another-statusline in .claude/skills/tmck-code-statusline/SKILL.md and read by ahel’s review.
The statusline renderer is a single-pass terminal painter with hand-tuned column math. Most bugs here are silent — wrong by one column, invisible icon, dropped byte through an Edit round-trip. This skill exists to make those bugs loud.
Architecture map
claude/statusline_command.py is a 4-line shim into the yas package under
claude/yas/ (app.py/layout.py/renderer.py/… at top level, info/ for
data sources, render/ for pure painting/maths). Full per-module map, entry
points, and the "where to make a change" table live in
ARCHITECTURE.md — read it before adding a module, a data
source, or a new row/border/gradient kind; skip it for a same-module tweak.
Pre-edit checklist
Run all four before editing:
- Read
CONTEXT.mdat repo root. The terms Billed Input, Cache Read, Output, Day Total, Context Window Size, Compaction-Risk Zone, Five-Hour Limit, Seven-Day Limit are canonical — don't rename or alias them in code without a paired update. - Catalogue PUA glyphs on touched lines. Scan the package (glyphs can appear in any module, though most are hoisted into
constants.py):
Any hit on a line you plan to Edit triggers the PUA refactor rule below.python3 -c " import sys for path in sys.argv[1:]: for ln, line in enumerate(open(path), 1): for c in line: cp = ord(c) if 0xE000 <= cp <= 0xF8FF or 0xF0000 <= cp <= 0xFFFFD: print(f'{path}:{ln} U+{cp:05X} {c!r}') " claude/yas/*.py claude/yas/info/*.py claude/yas/render/*.py - Baseline tests:
make test(oruv run pytest -q). Note pass count. On Android (Termux)uv run/make testis unavailable — activate the prebuilt venv and run pytest directly:. ~/.uvenv/bin/activate pytest -n 4 test/ - Baseline demo:
make demo(ormake statusline/test, both runuv run python ops/demo.py). It animates 60 frames in place via cursor escapes; eyeball the final frame and the elbow alignment as it crosses layout thresholds (narrow → medium → wide on$COLUMNS). For static snapshot images,make demo/img(writes scenario PNGs intodemo/, honoursCOLUMNS=). For a single piped frame when you need stdout, render one directly:COLUMNS=160 uv run python claude/statusline_command.py < ops/session-info-example.json(no transcript-derived rows; enough for border math). For a precise, diff-able baseline instead of eyeballing colour, capture the snapshots as ANSI-stripped text via the yas-demo-text skill:make demo/img && .claude/skills/yas-demo-text/scripts/demo-text.sh && cp -r demo/text /tmp/yas-base.
PUA refactor rule (mandatory before editing)
Nerd Font icons in this repo live in the Unicode Private Use Area (U+E000–U+F8FF and U+F0000–U+FFFFD). Literal PUA glyphs in source are invisible in many editors, render as □ in others, and get dropped through chat/agent round-trips — which makes Edit.old_string matching fail with a stale-looking "string to replace not found" error.
If a line you need to Edit contains a raw PUA glyph, hoist the glyph to a named constant in constants.py first, then Edit. No exceptions.
Convention (matches the existing block in constants.py):
# Nerd Font Private Use Area glyphs. Encoded as escapes so Edit, diff, and
# chat round-trips never lose the bytes. Render only in a Nerd-Font-capable
# terminal.
ICON_COST = '\uefc8' # nf-md currency-usd (cost row)
ICON_TOK_RATE = '\U000f18a7' # nf-md gauge (t/m rate label)
GLYPH_MODEL = '\U000f08b9' # nf-md monitor-dashboard (model row)
GLYPH_THINKING = '\U000f1a53' # nf-md brain (thinking indicator)
Import the constant where needed (from yas.constants import GLYPH_MODEL) and reference it in f-strings: f'{model_clr}{GLYPH_MODEL} {model_name}...'. Note that Renderer.ICON_PATH holds a colour code, not a glyph — don't reuse that namespace for glyphs. New glyph constants go in constants.py alongside ICON_COST/GLYPH_MODEL.
Runtime cost is zero — '\uefc8' (in source) and the literal glyph compile to the identical str object; CPython interns and the .pyc cache eliminates parse cost after first load.
Fallback when refactor isn't feasible mid-task
If the line has a PUA glyph and you genuinely can't refactor first (e.g., user is mid-edit and asked for one surgical change), use a Bash heredoc with python3 that reads, str.replaces, and writes. Python preserves the bytes exactly:
python3 << 'PY'
path = 'claude/yas/renderer.py'
with open(path) as f:
s = f.read()
old = "...exact old text with raw glyph copied through Read...\n"
new = "...replacement...\n"
assert old in s, 'old not found'
with open(path, 'w') as f:
f.write(s.replace(old, new, 1))
PY
This works because Read preserves the bytes when it loads them into your context, even when subsequent Edit calls can't transmit them through old_string.
Rendering invariants (silent-bug cheat-sheet)
These are the things pytest won't catch — get them wrong and the box draws crooked.
Width math
- Never use
len()for column math. Use_visible_width(render/text.py) — it strips ANSI escapes via_ANSI_RE(constants.py) and counts wide chars (BMP emoji0x1F300–0x1FAFF) as 2. - Nerd Font PUA chars count as width 1. Correct in a Nerd-Font terminal; would be wrong elsewhere, but elsewhere isn't supported.
Column indexing on borders (render/borders.py)
border_top(width, session_id='', downs=..., fill=..., pill=...),border_separator(width, ups=...),border_separator_dim(width, downs=..., ups=..., pill=..., pill_edge=...),border_bottom(width, ups=...)take 1-indexed visual positions of the inline│they should attach an elbow to. Live onBorderRenderer;Rendererhas matching delegators.border_line(content, width, fill=..., bg_lead='', bg_trail='', pill_flush=False, right_pill='')wraps content as│ <content>...│. Content starts at visual column 2, which is col-form 3 (1-indexed).right_pillpaints a pill segment flush to the right edge.- A
Pillpassed toborder_top/border_separator_dimpaints itself across[pill.start, pill.end]usingborder_char(col, edge)instead of the default top/separator glyph.pill_edge='top'is used when the pill sits below the separator.
vsep convention
The vertical divider inside a content row is the 5-char string ' │ ' (two spaces, pipe, two spaces). The │ sits at vsep-index 2.
vsep = f' {self.BORDER}│{self.R} ' # visible width 5; │ at offset 2
Section helpers that participate in dividers return (line, div_offset)
When a section contributes a │ that should grow elbows on the surrounding borders, the helper returns (line, div_offset) where div_offset is the 0-indexed visible position of the │ inside line. Examples: model_section_compact, model_right_section, tokens_cost (which returns (lines, vsep_cols, …)).
Caller (a build_* function) converts to a border col and threads it into RowSpec.downs / RowSpec.ups:
# Standalone row:
model_div_col = 3 + model_div_offset
# Inside a combined row whose own divider sits at top_div_col:
model_div_col = top_div_col + 3 + model_div_offset
rows = [
RowSpec('top_border', downs=(top_div_col, model_div_col)),
RowSpec('content', content=combined_line, bg_trail=bg_trail),
RowSpec('separator_dim', ups=(top_div_col, model_div_col)),
...
]
Every ┬ in a top border must line up with a │ in the row beneath it and a ┴ in the separator below — ups/downs are how you make that happen.
Gradient
grad_at(i, width, fill=...) returns the ANSI for column i of the rainbow border. Don't reorder the parts list when extending border helpers — the gradient is positional.
Layout-spec rules (layout.py)
- A
build_*function returns a fully-populatedLayoutSpec. Don't push rendering side effects into it; only buildRowSpecs. - New row types need: a new
kindstring, a branch inrender_layout, and aBorderRenderermethod (if it draws a border) or aRenderersection helper (if it's content). - Conditional rows: append to a local
rows: list[RowSpec]and assignspec.rows = rowsat the end. Seebuild_widefor the canonical pattern with optionalplugins_line,task_row, andopenspec_bars. - When a row drops out (e.g., no plugins), the surrounding
ups/downsneed to be re-threaded —build_widecarries anext_ups/pending_upslocal and asep_kindhelper for this. Don't try to "fix it up" insiderender_layout. - Pill threading: when the pill is active, the row immediately under the top border uses
pill_flush=Trueand an emptybg_lead; the surroundingtop_border/separator_dimreceive the samePillobject. When the pill is inactive, you fall back tobg_lead/bg_trailand elbowups/downs.
Post-edit checklist
make test(uv run pytest -q; on Android (Termux) use. ~/.uvenv/bin/activate && pytest -n 4 test/) — must be green. The pass count should match the baseline plus any tests you added.make demo— eyeball the animation:- Every
┬in a top border lines up with a│in the row beneath it and a┴in the separator below. - Pill colours flow continuously across the top, sides, and bottom of the model row.
- Resize the terminal narrower/wider during the run to verify the narrow ↔ medium ↔ wide thresholds.
- For an exact comparison, re-strip and diff against the baseline from the pre-edit step:
make demo/img && .claude/skills/yas-demo-text/scripts/demo-text.sh && diff -ru /tmp/yas-base demo/text. Every moved cell shows up as a line diff — this catches off-by-one column bugs the eye misses across the 60-frame animation.
- Every
- Tests — any behaviour change needs a test added or updated. Tests resolve the package via
pythonpath = ["claude"]and import modules asyas.<module>/yas.info.<module>/yas.render.<module>;conftest.pyexposes astrip_ansifixture (fromtest/helper.py) and atmp_homefixture that patchesCLAUDE_DIRacrossyas.app/yas.config/yas.constants/yas.session/yas.info.subagents/yas.tokens. Width-sensitive assertions go through_visible_width. Put new tests in the file that matches the layer touched:test_gradient_math.py,test_borders.py,test_model_section.py,test_context_line.py,test_openspec_bar.py,test_tokens_cost.py,test_config.py,test_layout_seam.py,test_subagent_rows.py/test_subagent_metrics.py/test_cohort_visibility.py(subagents),test_info.py(denominator math,_fmt_elapsed, laziness), etc. Layout tests inject aSessionViewdirectly — construct one with a knownSessionInfoandConfigrather than calling the builders with raw reader data. CONTEXT.md— if any displayed term changed (label, glyph meaning, what a number represents), update the glossary in the same change.
Multi-session observer (claude/mon.py/claude/mon/) module map is in
ARCHITECTURE.md. Launch it with make mon/run.
Sibling skills
python-style applies as usual when touching .py files. This skill adds the statusline-specific rules on top. After a render change, use yas-demo-text to turn make demo/img snapshots into ANSI-stripped plain text for a before/after diff (see the demo steps above) — the reliable way to confirm column math instead of eyeballing the coloured animation.
Signals
- GitHub stars
- 242
- Forks
- 24
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
tmck-code-statusline- Source
- github.com/tmck-code/yet-another-statusline