Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"metadata": {
"description": "Session optimizer, split into three independently installable plugins: context-guard (Stop-hook context budget + memory-writer checkpoint subagent + subagent spend tracker), refine-gate (UserPromptSubmit prompt-binding gate + /refine skill), and statusline (multi-line status bar + install skill).",
"version": "2.1.0"
"version": "2.1.1"
},
"plugins": [
{
Expand Down Expand Up @@ -58,7 +58,7 @@
"name": "statusline",
"source": "./plugins/statusline",
"description": "Multi-line status bar: a discrete heat-track context bar tied to per-model checkpoint thresholds (shared with context-guard), one deduplicated cost ledger that prices each session from its own transcript plus its subagents, 5h/7d rate-limit gauges with burn-rate pacing (the projection of usage at reset, not just the percentage), per-session telemetry (tok/s, compactions, prompt-cache countdown), and terminal-width fitting that drops the lowest-priority segments instead of letting the host truncate. Words, not emoji. Ships an install skill.",
"version": "2.1.0",
"version": "2.1.1",
"author": {
"name": "Clement Deust",
"email": "admin@ai-architect.tools"
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.1] - 2026-07-26

Statusline only. No change to context-guard or refine-gate.

### Fixed

- **The line-width budget is now the host's actual reserve, not a 15% haircut.**
`FIT_RATIO=85` held every line to 85% of the terminal, a figure inherited from
an unrelated plugin and never verified here. The reserve Claude Code takes is
additive, and it is now read from the host itself (2.1.220): 4 columns of
container padding (`<Box paddingLeft={2} paddingRight={2}>`) plus
`statusLine.padding` on both sides. On a 200-column terminal a line may now use
196 columns instead of 170; narrow terminals reserve less than before.
- **Preset selection no longer contradicts the budget.** The verbosity preset is
chosen on the fitted budget rather than the raw width. Previously `l` was
selected from 90 columns up but could not render untrimmed below 104, so
between those widths it was selected and then trimmed on every refresh.
- **`$COLUMNS` is consulted before the controlling tty.** The host sets it to the
width it renders into, which is the authority; the tty probe stays as the
fallback for hand-run invocations. Under the host the tty probe answers nothing
at all — there is no controlling terminal in the hook environment.

### Notes

- The claim that an over-wide first line drops the second status line is false
for 2.1.220: each line is rendered `<Text wrap="truncate">` independently.

### Internal

- `test_perf_heat_rgb_20run_avg` was flaky (~4 runs in 10) and asserted nothing:
it compared one sample of `heat_rgb` against the mean of twenty more of the
same call, so the delta was noise around zero, and per-sample `date +%s%N`
forks vary by ~19 ms on the measurement host — four times the 5 ms budget
being asserted. It now times a block of 2000 calls against an equally-sized
no-op control, amortizing the fork: `heat_rgb` measures 0.13 ms above the
control against the same 5 ms budget.

## [2.1.0] - 2026-07-26

Statusline only. No change to context-guard or refine-gate.
Expand Down
2 changes: 1 addition & 1 deletion plugins/statusline/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "statusline",
"description": "Multi-line Claude Code statusline with a discrete heat-track context bar tied to per-model checkpoint thresholds, one deduplicated cost ledger covering subagent spend, per-session telemetry (tok/s, compactions, prompt-cache countdown), rate-limit gauges with burn-rate pacing, and terminal-width fitting. Words, not emoji: every value is named. Ships an install skill that copies the bundled assets into ~/.claude and wires settings.json.",
"version": "2.1.0",
"version": "2.1.1",
"author": {
"name": "Clement Deust",
"email": "admin@ai-architect.tools"
Expand Down
23 changes: 15 additions & 8 deletions plugins/statusline/README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,21 @@ Chaque ligne est ajustée au terminal. Le preset de verbosité est le réglage
grossier (combien de **lignes**), `fit_line` le réglage fin : il retire les
segments de plus faible priorité en fin de ligne jusqu'à ce qu'elle rentre. Les
lignes sont construites du plus important au moins important, donc **c'est la
queue qui part**. Les lignes sont tenues à 85 % de la largeur et non 100 % : une
ligne qui atteint la marge droite est tronquée par l'hôte et peut coûter une
rangée au bloc entier.

La largeur est sondée depuis le tty de contrôle, puis `$COLUMNS`, puis
`tput cols` (uniquement quand stdout est un terminal — sur un tube il renvoie le
80 aveugle de terminfo). Quand rien ne répond, le repli est volontairement large.
Surchargeable par `$STATUSLINE_COLS`.
queue qui part**.

Le budget est la largeur du terminal moins ce que l'hôte garde pour lui : 4
colonnes de marge du conteneur, plus `statusLine.padding` compté deux fois
(l'hôte l'applique des deux côtés). Ces deux chiffres sont lus dans le rendu de
Claude Code 2.1.220 lui-même, qui enveloppe le bloc dans
`<Box paddingLeft={2} paddingRight={2}>` et chaque ligne dans
`<Text wrap="truncate">` — une ligne trop large est donc tronquée **seule** et ne
coûte jamais une rangée au bloc.

La largeur est sondée depuis `$COLUMNS` d'abord — l'hôte y met la largeur dans
laquelle il rend — puis le tty de contrôle, puis `tput cols` (uniquement quand
stdout est un terminal ; sur un tube il renvoie le 80 aveugle de terminfo). Quand
rien ne répond, le repli est volontairement large. Surchargeable par
`$STATUSLINE_COLS`.

## Seuils partagés avec context-guard

Expand Down
21 changes: 13 additions & 8 deletions plugins/statusline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,19 @@ wildly on a single burst.
Every line is fitted to the terminal. The verbosity preset is the coarse
adjustment (how many **lines**), and `fit_line` is the fine one: it drops each
line's lowest-priority trailing segments until the line fits. Lines are built
most-important-first, so **the tail is what goes**. Lines are held to 85% of the
width rather than 100%: a line that reaches the right margin is truncated by the
host and can cost the block a row.

Width is probed from the controlling tty, then `$COLUMNS`, then `tput cols`
(only when stdout is a terminal — on a pipe it returns terminfo's blind 80).
When nothing can answer, the fallback is deliberately wide. Override with
`$STATUSLINE_COLS`.
most-important-first, so **the tail is what goes**.

The budget is the terminal width minus what the host keeps for itself: 4 columns
of container padding, plus `statusLine.padding` twice over (the host applies it
on both sides). Both figures are read from Claude Code 2.1.220's own renderer,
which wraps the block in `<Box paddingLeft={2} paddingRight={2}>` and each line
in `<Text wrap="truncate">` — so an over-wide line is truncated **on its own**
and never costs the block another row.

Width is probed from `$COLUMNS` first — the host sets it to the width it is
rendering into — then the controlling tty, then `tput cols` (only when stdout is
a terminal; on a pipe it returns terminfo's blind 80). When nothing can answer,
the fallback is deliberately wide. Override with `$STATUSLINE_COLS`.

## Shared thresholds with context-guard

Expand Down
8 changes: 6 additions & 2 deletions plugins/statusline/assets/statusline-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,12 @@ read_transcript_telemetry "$transcript_path" "$now_epoch" # -> txt_*
read_subagent_totals "$session_id" # -> sub_count sub_tokens

COLS=$(probe_cols)
FIT_W=$(( COLS * FIT_RATIO / 100 ))
resolve_preset "$COLS" # -> SIZE RANK CTX_W BW
FIT_W=$(fit_budget "$COLS" "$(read_statusline_padding)")
# The preset is chosen on the BUDGET, not the raw width: what decides whether a
# preset fits is the room its lines actually get, which is the width less the
# host's chrome (fit.sh). Choosing on the raw width would select a preset the
# budget then trims on every refresh.
resolve_preset "$FIT_W" # -> SIZE RANK CTX_W BW

# --- Emit (%b interprets ANSI escapes; data is in args, not the format) ---
# One concern per line; empty lines are skipped so the block stays compact.
Expand Down
35 changes: 35 additions & 0 deletions plugins/statusline/assets/statusline-lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
# drift.
CTXGUARD_CONFIG="${HOME}/.claude/ctxguard-thresholds.json"
BUDGET_CONFIG="${HOME}/.claude/statusline-budget.json"
# The host's own settings file. Read for exactly one field — statusLine.padding
# — because that padding is applied by the host AROUND this script's output and
# therefore comes out of the width available to it (see fit.sh, fit_budget).
SETTINGS_CONFIG="${HOME}/.claude/settings.json"

# resolve_ctx_thresholds — context-window thresholds for the model named $1.
# pre: $1 the model's display name (any case).
Expand Down Expand Up @@ -67,6 +71,37 @@ read_cache_ttl_min() {
case "$b_ttl" in ''|*[!0-9]*) printf '5' ;; *) printf '%s' "$b_ttl" ;; esac
}

# read_statusline_padding — the host's configured statusLine.padding.
# post: prints a non-negative integer, always. 0 when the setting is absent,
# unreadable, negative or non-numeric — which is also the host's own
# default, so an unreadable settings file degrades to the host default
# rather than to a guess.
# source: code.claude.com/docs/en/statusline — "The optional `padding` field
# adds extra horizontal spacing (in characters) to the status line
# content. Defaults to `0`."
# Only the user-level file is read. A project or local settings file can also
# carry statusLine, and the host merges them; this reader does not, so a
# project-level padding is not seen. The cost of that miss is bounded: the
# budget is off by twice the difference, and fit_line trims a segment early or
# late by that much. Merging the full settings precedence would put the host's
# resolution rules in this renderer, which is a larger contract than the width
# budget needs.
# The type test is done in jq, not on the printed text: the setting is a JSON
# number for the host, and a JSON string "2" is NOT one — the host would hand it
# to a layout engine that expects a number. Filtering on the shell side alone
# would accept "2" and charge two columns the host never indents.
read_statusline_padding() {
local p=""
[ -r "$SETTINGS_CONFIG" ] && {
p=$(jq -r '
.statusLine.padding as $p
| if ($p | type) == "number" and $p >= 0 and ($p | floor) == $p
then ($p | tostring) else "0" end
' "$SETTINGS_CONFIG" 2>/dev/null) || p=""
}
case "$p" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$p" ;; esac
}

# read_configured_size — the verbosity preset the config asks for.
# post: prints one of xs|s|m|l|xl, or nothing when the config is absent or does
# not name a valid preset. It is a PREFERENCE: layout.sh caps it by width.
Expand Down
67 changes: 40 additions & 27 deletions plugins/statusline/assets/statusline-lib/fit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,44 @@
# the terminal cut mid-word, the renderer measures each line and drops its
# lowest-priority segments itself.

# Share of the terminal width a line may occupy. Lines are fitted to a fraction
# of the width rather than to the width itself, because the host keeps part of
# the row for its own chrome and cuts whatever crosses into it.
# Columns the host keeps for itself around this script's output, EXCLUDING the
# user-configurable statusLine.padding (fit_budget adds that separately).
#
# NOT A MEASURED CONSTANT — read this before trusting it. 85 is inherited from
# the pictet-tech claude-statusline plugin (assets/statusline.sh:98-108), which
# is no longer installed here and can no longer be consulted. Its claim — that a
# line approaching the full width is truncated with "…" AND costs the block its
# second row — has never been reproduced against the host by this repo. The
# value is a working default carried forward, not evidence.
# source: Claude Code 2.1.220, /Users/…/.local/share/claude/versions/2.1.220,
# read 2026-07-26. The status line is rendered by a component whose container is
# <Box width={columns} flexDirection={…} flexWrap="wrap" alignItems="flex-start"
# paddingLeft={2} paddingRight={compact ? 1 : 2} columnGap={1}>
# so the row is the full terminal width less 2 columns on the left and 2 on the
# right — 4. The compact branch reserves 1 on the right, i.e. 3 total; the wider
# of the two is used, because a budget that is one column pessimistic trims one
# segment early, while one column optimistic is cut by the host.
#
# What IS established, source: code.claude.com/docs/en/statusline, read
# 2026-07-26 against Claude Code 2.1.220 — the reserve the host takes is
# ADDITIVE, not proportional. `statusLine.padding` is "extra horizontal spacing
# (in characters)", defaults to 0, and is "in addition to the interface's
# built-in spacing", whose column count the docs do not publish. A percentage
# therefore models the constraint in the wrong shape: it over-reserves on wide
# terminals and under-reserves on narrow ones.
#
# Known cost of keeping the ratio, so the next reader is not surprised by it:
# tests/statusline/measure_widths.sh puts preset l's widest line at 89 columns
# (measured 2026-07-26), so at 85% l needs 104 columns to render untrimmed —
# while SIZE_L_MIN_COLS in layout.sh selects l from 90 up. Between 90 and 104
# columns l is selected and then trimmed on every refresh. Replacing this with a
# measured additive reserve closes that gap and needs one observation against a
# live host; it is deliberately not done here.
FIT_RATIO=85
# The same read settles what happens on overflow, replacing an inherited claim
# this repo could not reproduce: each emitted line is rendered as
# <Text dimColor wrap="truncate">
# individually (single-line path, and the per-line map for multi-line output),
# so an over-wide line is truncated ON ITS OWN and does NOT cost the block any
# other row. Line count is independent of line width.
FIT_CHROME_COLS=4

# fit_budget — columns one rendered line may occupy.
# pre: $1 the terminal width in columns, $2 the configured statusLine.padding.
# Both are validated here rather than trusted: $1 comes from probe_cols,
# whose last rung is a fallback, and $2 from a user-edited settings file.
# post: prints a positive integer, always. Pure: no I/O, no global mutation.
# The host applies padding as Ink's paddingX, which indents BOTH sides, so the
# configured value costs twice its own size. A terminal too narrow to hold even
# one column after the reserve yields 1 rather than 0 or a negative: fit_line
# treats a non-positive budget as "no budget" and returns the line untrimmed,
# which is precisely the overflow this exists to prevent.
fit_budget() {
local cols="$1" pad="${2:-0}" w
case "$cols" in ''|*[!0-9]*) printf '1'; return ;; esac
case "$pad" in ''|*[!0-9]*) pad=0 ;; esac
w=$(( cols - FIT_CHROME_COLS - 2 * pad ))
[ "$w" -lt 1 ] && w=1
printf '%s' "$w"
}

# vislen — terminal columns a rendered segment will occupy.
# pre: $1 is a segment as built by this renderer: literal "\033[<params>m" SGR
Expand All @@ -58,8 +69,10 @@ FIT_RATIO=85
# - The block/box glyphs used here (│ █ ░ …) are East-Asian-Ambiguous width.
# They are counted as 1, which matches Terminal.app, iTerm2 and Warp in
# their default configuration. A terminal explicitly configured to render
# ambiguous characters double-width will be under-measured; the FIT_RATIO
# headroom absorbs the common case and STATUSLINE_COLS overrides it.
# ambiguous characters double-width will be under-measured, and the budget
# from fit_budget is exact rather than generous, so such a line is cut by
# the host's own truncate. STATUSLINE_COLS is the override for that case:
# declaring a narrower width buys back the difference.
# - Under a non-UTF-8 locale bash counts bytes, so multi-byte characters
# over-measure. That can only make fit_line trim earlier, never overflow.
vislen() {
Expand Down
Loading
Loading