--- name: tmck-code-statusline description: 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 (yas/info/__init__.py), the LayoutSpec/RowSpec layout pipeline, record_tick/TickRecord, Nerd Font PUA glyph hazards, border/elbow column math, and the demo-based visual check. --- # Statusline 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`](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`): ```bash 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: ```bash . ~/.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`): ```python # 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.replace`s, and writes. Python preserves the bytes exactly: ```bash 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 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. ```python 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`: ```python # 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 `RowSpec`s. - 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.` / `yas.info.` / `yas.render.`; `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`](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.