Release notes

FEATURE

Grok CLI gets its own usage status line

Grok CLI supports the same command-style status line mechanism as Claude Code, so usage_statusline_grok.py joins the existing install / remove / self-heal flow behind the switch already shared with Claude Code and Antigravity. Grok's stdin payload carries no rate-limit summary — its documentation says so explicitly — so the script tails ~/.grok/logs/unified.jsonl for creditUsagePercent and currentPeriod.end; when neither can be read the segment is omitted rather than printed as placeholder characters. Only the weekly axis is shown, since Grok has no 5-hour window and reports no cache hit ratio, and neither is estimated.

FEATURE

The status line's second row shows cache hit rate and a countdown

Claude Code 2.1.251 added a prompt_cache object to the status line hook's stdin JSON, and usage was already storing the whole payload in usage-status.json without reading it. The second row now shows the prompt cache hit rate and a countdown to expiry. A new color_by_pct_inverted() mirrors the thresholds, so a 91% hit rate reads as good instead of borrowing the quota palette's red, and progress_bar() takes an optional color_func, leaving existing callers unchanged. The segment is skipped entirely when caching_observed is not True or the ratio is unavailable, so older Claude Code versions silently omit it; an expired expires_at drops only the countdown and keeps the bar. Narrow terminals degrade in two further steps: the countdown goes first, then the whole segment.

FEATURE

The status line lights up the numbers and dims everything else

Empty progress cells, the context window total, and the | separators were pure white and brighter than the figures themselves; they are now grey 240 / dim, leaving usage as the only lit element. The second row is secondary information, so a cache hit rate of 80% or better takes that row's muted purple and only turns orange below 80% and red below 50% — good news does not need to glow. The same batch reworded the cache countdown. The previous wording was a literal rendering of warm/cold that read as nothing in particular; it now reuses the existing remaining_prefix, giving (59min left) in English alongside the two segments before it. cache_cold_suffix, used in exactly one place, was removed, so five languages no longer need a separate word for it.

BUGFIX

A third-party status line with a similar name is no longer taken over

Hook ownership was decided by the substring test "usage-statusline" in cmd, so someone else's usage-statusline-pro.py was treated as a usage installation: taken over without a backup, and its configuration deleted on uninstall. The comparison now matches the complete filename, derived from HOOK_TARGET.name and its siblings instead of a hardcoded string, requiring a path separator, quote, whitespace, or string boundary on each side. It deliberately does not compare full paths: the command string shifts with interpreter location, quoting, and Windows backslashes, and a path comparison would stop usage recognizing itself.

BUGFIX

The status line's file lock times out instead of hanging

Every message you sent left the status line stalled, because another process was holding the same lock. _exclusive_lock()'s POSIX branch used a plainly blocking flock(LOCK_EX), while the Windows branch already had a 10-second _LOCK_TIMEOUT_S guard. The POSIX branch now polls with LOCK_EX | LOCK_NB and, on timeout, follows the msvcrt path: the atomic write proceeds without the lock.

FEATURE

Installed status line copies actually update now

needs_update() compares __version__ in ~/.claude/usage-statusline.py against HOOK_VERSION and skips the copy when they match, so rendering changes that shipped without a version bump never reached an installed copy, silently and with no error. Both constants were bumped, and a drift test was added: the Token Saver hook had that guard already; the status line was missing it.

FEATURE

The report adds a first-pass rate, compared across models

The report's usage habits section gained a card measuring what share of your turns this period ran without an interruption and without a blocked tool call, split by model — a quality axis rather than another quota count. Both signals are structural, read from ~/.claude/projects: interruptedMessageId on user lines, and permission-denial strings inside tool_result content; nothing is inferred semantically. The denominator is user turns rather than assistant messages, because a single exchange emits a dozen assistant messages and would compress every model above 99.5%. Multiple interruptions inside one turn count once. Angle-bracketed internal markers such as <synthetic> are excluded. Interrupted assistant messages are frequently aborted before reaching disk, so unresolvable signals go to an unattributed bucket and are not shown rather than being assigned to a model. The insight line is written only when the spread between the highest and lowest model exceeds 15 points.

FEATURE

The report draws a day-by-day curve and prints properly

The report's header already named the period's peak day, but you had no chart to check it against: daily_trend held 30 days of data that no chart used. A daily area line now sits above the weekly rollup, marking the highest day and labelling only the period's first and last dates. The stylesheet also gained a @media print block: sections avoid internal page breaks, interactive controls are hidden, shadows and offsets are removed, and colors invert to dark-on-white so a PDF stays readable when the reader is in dark mode.

FEATURE

The report's tables, charts, and section rhythm were filled in

The five heaviest sessions were the report's only all-text table; the tokens column now carries a thin bar scaled against the largest of the five, colored by the same provider palette the model table uses. The active-hours chart, previously hour labels alone, now marks its peak value in muted type in the lower right, with no gridlines or Y axis added. The ten sections shared one identical treatment; insights and the heatmap now carry heavier shadows while the recent-work and tools sections are lighter with --faint borders — shadows and borders only, since the persona section holds two cards and a background change would make their height difference conspicuous.

FEATURE

Project and model rows in the report carry share bars

Comparing ten stacked rows previously meant reading the numbers. Each name now has a 4px solid proportion bar beneath it — solid rather than gradient, so the color is not misread as carrying meaning. The donut's "Other" slice, which used to draw the palette's seventh color and read as nearly the same green as the first, is now a neutral #8b8577, selected inside _donut_svg by a synthetic flag rather than guessed from its index.

BUGFIX

A week still in progress is no longer compared against a complete one

The weekly trend measured the current partial week against the previous full one, which on Monday 2026-08-31 produced "W36 ↘ −97%" and the conclusion that this week was 97% lighter. The final week is now marked in progress when its Sunday falls after date_to (new trend_week_in_progress key, added in all five languages) and shows no percentage, and _trend_summary() compares the last two completed weeks instead.

BUGFIX

The Antigravity model name stops mixing two languages, and the menu bar title tightens

Antigravity's display_name already ends in an English effort suffix such as (High), and usage appended a localized one after it, naming the same level twice in two languages. The English suffix is now stripped and only the localized label is kept; when display_name carries no effort field, the stripped suffix becomes the source. The menu bar title tightened up too: the interpunct separator was removed, since the provider icons already separate the figures, and icon spacing was reduced to two spaces. Separately, prompt cache health was documented in all five READMEs under real-time visibility, noting it needs Claude Code 2.1.251 or newer, and the website's description of the AI Tool Update Digest was removed, a report section that no longer exists.

FEATURE

Uninstalling restores Grok's original status line

unsetup() handled Claude Code and Codex but skipped Grok, so after --unsetup the user's ~/.grok/config.toml still pointed at a usage script with no supported way back to their own [ui.status_line]. Uninstalling now restores Grok's original status line. The old test exercised only the private _unsetup_grok(), which is why this slipped through; the new one enters through the public unsetup().

FEATURE

Weekly burn trend bars are now CSS gradients

The donut, heatmap, and active-hours chart on the same page were all drawn graphics, while the weekly trend drew its bars with █ characters, looking pasted in from another document. Those bars are now CSS gradients. The four-column layout and terminal styling are unchanged; the rendering moved into a new ui/report_charts.py.

BUGFIX

The heatmap stops skipping months, and the donut agrees with the table

A heatmap column's month was taken from its Monday, so a week spanning a month boundary was credited to the old month it barely touched, and a label suppressed by the spacing rule was lost permanently — real output dropped September entirely. The month is now read from the column's Thursday and compared against the last month actually labelled, so a suppressed month reappears at the next column that satisfies the spacing. Separately, the project donut divided by the sum of by_project[:10] while the table's percentage used total tokens, so one project showed 42.5% in the chart and 36.1% in the table, and the center figure read as a total it was not. The denominator is now summary.total_tokens, with everything not shown (including projects outside the top ten) combined into an "Other" slice reusing the existing report_chart_other string.

BUGFIX

Interruptions and denied tools on messages with no model name are no longer dropped

A message carrying message.id but no message.model was indexed as an empty string, while the later check tested only model is None. Those events were therefore neither attributed to a model nor counted as unattributed, undercounting interruptions in the first-pass rate described on the previous card. That is now fixed.

FEATURE

A report with no usage renders a clean empty state

At zero usage the narrative sentence interpolated zeros and placeholders — "---- -- -- was the peak (0/day), main LLM 未知" — above seven sections each saying only "nothing here yet", which is what a new user saw on first open. When both total_tokens and messages are zero the narrative is now replaced by report_empty_state_hint, a key added in all five languages, and only the header, stat cards, and sponsor section render. Output with data is byte-identical; the untouched full_zh_tw and full_en snapshots are the proof.

Original changelog

Original changelogGrok CLI gets its own usage status line
**Grok CLI now gets its own `usage` status line.** Grok CLI supports the same command-style status line mechanism as Claude Code, so `usage_statusline_grok.py` joins the existing install / remove / self-heal flow behind the switch already shared with Claude Code and Antigravity. Grok's stdin payload carries no rate-limit summary (its documentation says so explicitly), so the script tails `~/.grok/logs/unified.jsonl` for `creditUsagePercent` and `currentPeriod.end`; when neither can be read the segment is omitted rather than printed as placeholder characters. Only the weekly axis is shown — Grok has no 5-hour window and reports no cache hit ratio, and neither is estimated.
Original changelogThe status line's second row shows cache hit rate and a countdown
**The status line's second row now shows prompt cache hit rate and a countdown to expiry.** Claude Code 2.1.251 added a `prompt_cache` object to the status line hook's stdin JSON; `usage` was already storing the whole payload in `usage-status.json` without reading it. A new `color_by_pct_inverted()` mirrors the thresholds, so a 91% hit rate reads as good instead of borrowing the quota palette's red, and `progress_bar()` takes an optional `color_func` leaving existing callers unchanged. The segment is skipped entirely when `caching_observed` is not `True` or the ratio is unavailable, so older Claude Code versions silently omit it; an expired `expires_at` drops only the countdown and keeps the bar. Narrow terminals degrade in two further steps: the countdown goes first, then the whole segment.
Original changelogThe status line lights up the numbers and dims everything else
**The status line dims what isn't the number.** Empty progress cells, the context window total, and the `|` separators were pure white and brighter than the figures themselves; they are now grey 240 / dim, leaving usage as the only lit element. The second row is secondary information, so a cache hit rate of 80% or better takes that row's muted purple and only turns orange below 80% and red below 50% — good news does not need to glow.
**The cache countdown now says "left", matching the 5-hour and 7-day segments.** The previous wording was a literal rendering of warm/cold that read as nothing in particular; it now reuses the existing `remaining_prefix`, giving `(59min left)` in English alongside the two segments before it. `cache_cold_suffix`, used in exactly one place, was removed, so five languages no longer need a separate word for it.
Original changelogA third-party status line with a similar name is no longer taken over
**A third-party status line with a similar name is no longer mistaken for our own.** Hook ownership was decided by the substring test `"usage-statusline" in cmd`, so someone else's `usage-statusline-pro.py` was treated as a `usage` installation: taken over without a backup, and its configuration deleted on uninstall. The comparison now matches the complete filename, derived from `HOOK_TARGET.name` and its siblings instead of a hardcoded string, requiring a path separator, quote, whitespace, or string boundary on each side. It deliberately does not compare full paths — the command string shifts with interpreter location, quoting, and Windows backslashes, and a path comparison would stop `usage` recognizing itself.
Original changelogThe status line's file lock times out instead of hanging
**The status line's file lock now times out instead of hanging a message.** `_exclusive_lock()`'s POSIX branch used a plainly blocking `flock(LOCK_EX)` while the Windows branch already had a 10-second `_LOCK_TIMEOUT_S` guard, so a process holding the lock stalled the status line on every message. The POSIX branch now polls with `LOCK_EX | LOCK_NB` and, on timeout, follows the `msvcrt` path: the atomic write proceeds without the lock.
Original changelogInstalled status line copies actually update now
**Installed status line copies now actually update.** `needs_update()` compares `__version__` in `~/.claude/usage-statusline.py` against `HOOK_VERSION` and skips the copy when they match, so rendering changes that shipped without a version bump never reached an installed copy, silently and with no error. Both constants were bumped, and a drift test was added — the Token Saver hook had that guard already; the status line was missing it.
Original changelogThe report adds a first-pass rate, compared across models
**The HTML report's usage habits section now has a first-pass rate, compared across models.** The card measures what share of your turns this period ran without an interruption and without a blocked tool call, split by model — a quality axis rather than another quota count. Both signals are structural, read from `~/.claude/projects`: `interruptedMessageId` on user lines, and permission-denial strings inside `tool_result` content; nothing is inferred semantically. The denominator is user turns rather than assistant messages, because a single exchange emits a dozen assistant messages and would compress every model above 99.5%. Multiple interruptions inside one turn count once. Angle-bracketed internal markers such as `<synthetic>` are excluded. Interrupted assistant messages are frequently aborted before reaching disk, so unresolvable signals go to an unattributed bucket and are not shown rather than being assigned to a model. The insight line is written only when the spread between the highest and lowest model exceeds 15 points.
Original changelogThe report draws a day-by-day curve and prints properly
**The report's trend section now draws a day-by-day curve for the period, and prints properly.** `daily_trend` already held 30 days of data that no chart used, leaving the peak day named in the header with nothing to check it against. A daily area line now sits above the weekly rollup, marking the highest day and labelling only the period's first and last dates. The stylesheet also gained a `@media print` block: sections avoid internal page breaks, interactive controls are hidden, shadows and offsets are removed, and colors invert to dark-on-white so a PDF stays readable when the reader is in dark mode.
Original changelogThe report's tables, charts, and section rhythm were filled in
**The report's heaviest-sessions table, active-hours chart, and section rhythm were filled in.** The five heaviest sessions were the report's only all-text table; the tokens column now carries a thin bar scaled against the largest of the five, colored by the same provider palette the model table uses. The active-hours chart, previously hour labels alone, now marks its peak value in muted type in the lower right, with no gridlines or Y axis added. The ten sections shared one identical treatment; insights and the heatmap now carry heavier shadows while the recent-work and tools sections are lighter with `--faint` borders — shadows and borders only, since the persona section holds two cards and a background change would make their height difference conspicuous.
Original changelogProject and model rows in the report carry share bars
**Project and model rows in the report now carry share bars.** Comparing ten stacked rows previously meant reading the numbers; each name now has a 4px solid proportion bar beneath it — solid rather than gradient, so the color is not misread as carrying meaning. The donut's "Other" slice, which used to draw the palette's seventh color and read as nearly the same green as the first, is now a neutral `#8b8577`, selected inside `_donut_svg` by a synthetic flag rather than guessed from its index.
Original changelogA week still in progress is no longer compared against a complete one
**A week still in progress is no longer compared against a complete one.** The weekly trend measured the current partial week against the previous full one, which on Monday 2026-08-31 produced "W36 ↘ −97%" and the conclusion that this week was 97% lighter. The final week is now marked in progress when its Sunday falls after `date_to` (new `trend_week_in_progress` key, added in all five languages) and shows no percentage, and `_trend_summary()` compares the last two completed weeks instead.
Original changelogThe Antigravity model name stops mixing two languages, and the menu bar title tightens
**The Antigravity model name no longer mixes two languages.** Antigravity's `display_name` already ends in an English effort suffix such as `(High)`, and `usage` appended a localized one after it, naming the same level twice in two languages. The English suffix is now stripped and only the localized label is kept; when `display_name` carries no effort field, the stripped suffix becomes the source.
**The menu bar title tightened up.** The interpunct separator was removed, since the provider icons already separate the figures, and icon spacing was reduced to two spaces.
Documented prompt cache health in all five READMEs under real-time visibility, noting it needs Claude Code 2.1.251 or newer, and removed the website's description of the AI Tool Update Digest, a report section that no longer exists.
Original changelogUninstalling restores Grok's original status line
**Uninstalling now restores Grok's original status line.** `unsetup()` handled Claude Code and Codex but skipped Grok, so after `--unsetup` the user's `~/.grok/config.toml` still pointed at a `usage` script with no supported way back to their own `[ui.status_line]`. The old test exercised only the private `_unsetup_grok()`, which is why this slipped through; the new one enters through the public `unsetup()`.
Original changelogWeekly burn trend bars are now CSS gradients
**Weekly burn trend bars are now CSS gradients.** The weekly trend drew its bars with `█` characters while the donut, heatmap, and active-hours chart on the same page were graphical. The four-column layout and terminal styling are unchanged; the rendering moved into a new `ui/report_charts.py`.
Original changelogThe heatmap stops skipping months, and the donut agrees with the table
**The heatmap's month labels no longer skip or mislabel a month.** A column's month was taken from its Monday, so a week spanning a month boundary was credited to the old month it barely touched, and a label suppressed by the spacing rule was lost permanently — real output dropped September entirely. The month is now read from the column's Thursday and compared against the last month actually labelled, so a suppressed month reappears at the next column that satisfies the spacing.
**The project donut's percentages now agree with the table beneath it.** The donut divided by the sum of `by_project[:10]` while the table's percentage used total tokens, so one project showed 42.5% in the chart and 36.1% in the table, and the center figure read as a total it was not. The denominator is now `summary.total_tokens`, with everything not shown — including projects outside the top ten — combined into an "Other" slice reusing the existing `report_chart_other` string.
Original changelogInterruptions and denied tools on messages with no model name are no longer dropped
**Interruptions and denied tools on messages with no model name are no longer dropped.** A message carrying `message.id` but no `message.model` was indexed as an empty string, while the later check tested only `model is None` — so those events were neither attributed to a model nor counted as unattributed, undercounting interruptions in the first-pass rate.
Original changelogA report with no usage renders a clean empty state
**A report with no usage now renders a clean empty state.** At zero usage the narrative sentence interpolated zeros and placeholders — "---- -- -- was the peak (0/day), main LLM 未知" — above seven sections each saying only "nothing here yet", which is what a new user saw on first open. When both `total_tokens` and `messages` are zero the narrative is replaced by `report_empty_state_hint` (added in all five languages) and only the header, stat cards, and sponsor section render. Output with data is byte-identical; the untouched `full_zh_tw` and `full_en` snapshots are the proof.