Skip to content

Statusline — Provider-Aware Route Display

Phase 4 implementation of the approved Phase 3 design (Statusline/Design/01–06). Supersedes the model segment of the previous ~/.claude/statusline.js (per 01-display-requirements.md).

What it does

Renders a single statusline line whose primary segment is the resolved provider/model of the last assistant response, e.g. opencode/big-pickle, degrading gracefully:

Condition Renders
Resolved route known, provider in mapping provider/model
Resolved route known, provider unknown model
No assistant message yet requested label (auto/cheap)
Route older than 5 minutes appends [stale]
Long route (> 32 chars) truncated with , provider prefix kept

The line is pipe-grouped (|) and context-aware:

🤖 <route> | <bar + %> | 🪟 <used/window> | ⇡in⇣out | 🌿 <branch>/🌳 <worktree-name> <state>
  • Bar: a 10-segment ▓░ gauge; filled blocks and the % are health-colored by usage (green <50%, yellow <80%, red ≥80% — same thresholds as before), empty blocks a dark grey.
  • Fill meter: tokens-in-window over the window size (78K/200K), derived from the same used_percentage as the bar so bar, %, and meter always agree.
  • Token deltas: ⇡in⇣out from the last API call (current_usage), unchanged.
  • Branch sits last; 🌿 <branch> in the main working tree, 🌳 <worktree-name> inside a linked worktree (the worktree labels itself, not its branch), muted green on feature branches and bright red on a protected default branch (main/master plus each repo's remote HEAD default). Omitted when a clean default branch is checked out in the main tree🌿 main ✅ is noise; it still renders for any noteworthy state (dirty, in-progress, detached, or a worktree). A working-tree state emoji follows: clean · ✏️ modified · 📦 staged · 🆕 untracked · 🔄 mixed · ⚔️ conflict · 🧩 rebasing · 🤝 merging · 🍒 cherry-picking · ↩️ reverting · 📍 detached HEAD. Omitted when not in a git repo.

Layout

Statusline/
  statusline.js          CLI entry point (small; wires the modules)
  lib/
    config.js            All implementation constants (tail window, staleness, timeouts)
    parse-stdin.js       Payload JSON parsing
    transcript.js        Bounded transcript tail read + last-assistant-model finder
    mapping.js           Read-only mapping loader + provider/context-window resolvers (never writes)
    staleness.js         Staleness check
    route.js             Route composition + truncation
    format.js            Git branch + context usage formatting (preserved baseline)
    timeout.js           Promise.race hard-timeout helper
  test/
    statusline.test.js   Contract tests (all cases + invariants + V2 pipeline tests) via node:test
    fixtures/            Transcript / mapping fixtures
Scripts/
  update-model-provider-mapping.js   Regenerate mapping: provider + contextWindows (out-of-band)
  deploy-statusline.js              Install to ~/.claude/statusline/ with a thin wrapper

Requirements

  • Node.js 18+ (developed and tested on v24)
  • Zero npm dependencies (the tests need nothing beyond Node)
  • OmniRoute — required for the provider display: the statusline reads a mapping file that only OmniRoute's call logs can generate. Without it, the route degrades to model-only (e.g. claude-3-5-haiku); the tests still pass.

Running the tests

node --test Statusline/test/statusline.test.js

Install / deploy

node Scripts/deploy-statusline.js

This backs up ~/.claude/statusline.js once, installs statusline.js + lib/ into ~/.claude/statusline/, and writes a thin wrapper at ~/.claude/statusline.js. The existing settings.json command (node ~/.claude/statusline.js) is unchanged.

Regenerating the model→provider mapping

The provider display depends on this mapping, and only OmniRoute's call logs can generate it — the provider prefix is an OmniRoute-dependent feature. Without the mapping, the route renders model-only (e.g. claude-3-5-haiku).

The statusline reads ~/.claude/statusline-model-provider.json at runtime and never writes it (invariant: read-only runtime dependency). Regeneration is an explicit, out-of-band maintenance action:

node Scripts/update-model-provider-mapping.js

The script queries OmniRoute read-only (Docker volume mounted :ro), filters call_logs to successful auto-routed requests, writes the most-frequent provider per model, and — since V2 — LEFT JOINs model_capabilities on model_id to also write the resolved model's real context window under a sibling contextWindows block. Requires Docker and the omniroute-data volume.

V2: real context window

The used/window fill meter (e.g. 78K/200K) uses the resolved model's real context window from model_capabilities.limit_context (populated by the models.dev Model Database sync) as its denominator, not Claude Code's payload guess. The numerator is derived from used_percentage, so the bar and meter stay consistent. Fallback chain:

Condition Window used
Model has a contextWindows entry that entry (real window, e.g. 262K for a 262,144 model)
No contextWindows entry (unmapped or stale mapping) payload context_window_size
No window available at all fill meter omitted (bar + % may still render)

Regenerate the mapping when a model's window looks like the payload guess — the same maintenance action as provider refresh, and both are written in one regeneration.

Environment variables

Variable Purpose
STATUSLINE_MAPPING_PATH Override the mapping file path (used by tests)
OMNIROUTE_DATA_VOLUME Override the Docker data volume (maintenance script)

Design & security

  • Deterministic: identical inputs → identical output (no randomness, no timestamps beyond the staleness decision).
  • Failure invariant: every failure path degrades toward the requested label; no error, exception, undefined, or null in output.
  • Never renders tokens, prompts, request bodies, or sensitive content — only the route/context segments.
  • No network: all reads are local files. The route read path uses plain file reads only (no classifier-gated commands).
  • Model strings are display data: rendered verbatim, never parsed or executed (05-security-privacy.md).

Operational notes

  • Deployment layout: ~/.claude/statusline.js is a thin wrapper; the implementation lives in ~/.claude/statusline/ (statusline.js + lib/). The wrapper keeps the settings.json command (node ~/.claude/statusline.js) stable across re-deploys.
  • Runtime data: the statusline reads the session transcript (a local JSONL written by Claude Code) and the mapping file (~/.claude/statusline-model-provider.json). It never calls OmniRoute, never writes files, and never makes network requests. Stopping OmniRoute therefore does not break the statusline; it only stops new assistant messages from arriving (and the mapping from being regenerated), so the route ages and [stale] appears. The provider prefix comes from the local mapping file, not from OmniRoute live.
  • Sensitive content: the statusline renders only route/context segments. Tokens, prompts, and request bodies are never displayed (05-security-privacy.md).
  • Model label timing: the displayed model is the last assistant message's model, re-read per statusline event (debounced ~300 ms). Assistant entries stream into the transcript at generation start, so the model updates as soon as a turn begins — and the [stale] marker clears at that point, which is correct (staleness keys to the last assistant message's timestamp, per ADR-0009).

Troubleshooting

Symptom Likely cause Fix
Shows the requested label (auto/cheap) instead of a route No assistant message in the transcript yet, or transcript path missing/stale Send a message so the transcript has an assistant entry; confirm the session's transcript_path exists
Shows model only (no provider/) No mapping file (no OmniRoute yet), or the model is not in the empirical mapping Generate/regenerate the mapping with OmniRoute (below) so the model gains a provider entry; until then model-only is correct
Shows the payload context window instead of the model's real window The model has no contextWindows entry (unmapped or stale mapping) Regenerate the mapping (below); the payload value is the correct fallback until then
Route shows truncation Route > 32 chars Expected; the model portion is truncated while the provider prefix is kept
[stale] never appears Turn still active (fresh transcript) or threshold not reached Wait 5 minutes of idle; the marker is a soft signal, not an error
Empty line Unparseable stdin or total failure The statusline degrades to "" only on total failure; Claude Code then shows its own default
Changed the tail window / threshold and nothing changed Constants in lib/config.js; re-deploy required Edit lib/config.js, re-run node Scripts/deploy-statusline.js

Maintenance plan

  • Refresh the model→provider + contextWindows mapping when new models appear as model-only, or when a window looks like the payload guess, or after OmniRoute provider/config changes: node Scripts/update-model-provider-mapping.js (out-of-band; Docker and the omniroute-data volume required). The mapping is a snapshot; there is no automatic refresh. Both blocks are written by the same regeneration.
  • Re-deploy after edits to Statusline/: node Scripts/deploy-statusline.js (backs up the current wrapper once; the settings command path is unchanged).
  • Run the contract suite after any code change: node --test Statusline/test/statusline.test.js (68 tests).
  • Tuning constants: staleness threshold and route length bound live in Statusline/lib/config.js; changing them needs a re-deploy but no ADR (ADR-0009/0010).
  • Versioning: V1.0 is repository history (the internal milestone); v2.0.0 is the first public release (2026-08-05). The V1→V2 work (real context window from model_capabilities, provider-hue styling, worktree/branch labels) is merged on main. See ../CHANGELOG.md.