Statusline

SkillAI & models

Guides 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.

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:

  1. Read CONTEXT.md at 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.
  2. Catalogue PUA glyphs on touched lines. Scan the package (glyphs can appear in any module, though most are hoisted into constants.py):
    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
    
    Any hit on a line you plan to Edit triggers the PUA refactor rule below.
  3. Baseline tests: make test (or uv run pytest -q). Note pass count. On Android (Termux) uv run/make test is unavailable — activate the prebuilt venv and run pytest directly:
    . ~/.uvenv/bin/activate
    pytest -n 4 test/
    
  4. Baseline demo: make demo (or make statusline/test, both run uv 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 into demo/, honours COLUMNS=). 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 emoji 0x1F300–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 on BorderRenderer; Renderer has 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_pill paints a pill segment flush to the right edge.
  • A Pill passed to border_top / border_separator_dim paints itself across [pill.start, pill.end] using border_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-populated LayoutSpec. Don't push rendering side effects into it; only build RowSpecs.
  • New row types need: a new kind string, a branch in render_layout, and a BorderRenderer method (if it draws a border) or a Renderer section helper (if it's content).
  • Conditional rows: append to a local rows: list[RowSpec] and assign spec.rows = rows at the end. See build_wide for the canonical pattern with optional plugins_line, task_row, and openspec_bars.
  • When a row drops out (e.g., no plugins), the surrounding ups/downs need to be re-threaded — build_wide carries a next_ups/pending_ups local and a sep_kind helper for this. Don't try to "fix it up" inside render_layout.
  • Pill threading: when the pill is active, the row immediately under the top border uses pill_flush=True and an empty bg_lead; the surrounding top_border / separator_dim receive the same Pill object. When the pill is inactive, you fall back to bg_lead/bg_trail and elbow ups/downs.

Post-edit checklist

  1. 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.
  2. 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.
  3. Tests — any behaviour change needs a test added or updated. Tests resolve the package via pythonpath = ["claude"] and import modules as yas.<module> / yas.info.<module> / yas.render.<module>; conftest.py exposes a strip_ansi fixture (from test/helper.py) and a tmp_home fixture that patches CLAUDE_DIR across yas.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 a SessionView directly — construct one with a known SessionInfo and Config rather than calling the builders with raw reader data.
  4. 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