feat(#2161): opt-in absolute token count on the statusline context meter (#2174)

* feat(#2161): opt-in absolute token count on the statusline context meter

New statusline.show_context_tokens config (default false). When enabled,
the context meter shows the absolute token total after the percentage,
e.g. "████░░░░░░ 46% (156k)" — summing input, cache-creation, cache-read,
and output tokens from context_window.current_usage (matching /context).

Default output is byte-for-byte unchanged when the flag is absent or
false. The .planning config is now read once per render and shared with
the last-command/position block instead of being re-read.

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* docs(#2161): changeset fragment for PR #2174

* fix(#2161): review fixes — k-to-M threshold, boundary tests, changeset format

- formatTokens promotes to the M branch when k-rounding reaches 1000
  (999,500-999,999 rendered "1000k" instead of "1.0M")
- boundary tests at 999499/999500/999999/1000000/1000001
- Number() guards on the four usage fields (silent string-concat gap)
- changeset body ends with the (#2161) citation per house convention

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* fix(#2161): round-2 review fixes — config-set coverage, precision claim, exports style

- config-set accept/reject tests for statusline.show_context_tokens
  (mirrors the post-planning-gaps precedent the issue scope names)
- changeset + docs no longer claim parity with /context: the suffix sums
  four fields while the meter %% derives from used_percentage (three), so
  the figures can diverge slightly
- module.exports one entry per line

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

* test: regenerate golden-install-parity fixtures for the statusline hook change

Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Cody Anderson
2026-07-13 11:25:47 -06:00
committed by GitHub
parent 880fbd963a
commit ad7111e50b
24 changed files with 260 additions and 33 deletions

View File

@@ -286,6 +286,36 @@ function formatGsdState(s) {
return parts.join(' · ');
}
// --- Context token count (opt-in) ---------------------------------------------
/**
* Format a token count compactly: 156342 → '156k', 1234567 → '1.2M'.
*/
function formatTokens(tokens) {
// Promote to the M branch when k-rounding would reach 1000 (999,500-999,999
// must render "1.0M", never "1000k").
if (tokens >= 1000000 || Math.round(tokens / 1000) >= 1000) {
return (tokens / 1000000).toFixed(1) + 'M';
}
if (tokens >= 1000) return Math.round(tokens / 1000) + 'k';
return String(tokens);
}
/**
* Pure function: build the token-count suffix for the context meter from the
* hook input's context_window.current_usage block. Sums input, cache-creation,
* cache-read, and output tokens (the same total Claude Code's /context shows).
* Returns ' (156k)' or '' when usage is absent/empty.
*/
function contextTokenSuffix(currentUsage) {
if (!currentUsage || typeof currentUsage !== 'object') return '';
const total = (Number(currentUsage.input_tokens) || 0) +
(Number(currentUsage.cache_creation_input_tokens) || 0) +
(Number(currentUsage.cache_read_input_tokens) || 0) +
(Number(currentUsage.output_tokens) || 0);
return total > 0 ? ` (${formatTokens(total)})` : '';
}
// --- stdin ------------------------------------------------------------------
function runStatusline() {
@@ -304,6 +334,11 @@ function runStatusline() {
const session = data.session_id || '';
const remaining = data.context_window?.remaining_percentage;
// Read .planning config once — used by the context meter (token suffix)
// and the last-command/position block below. Fail-soft to {}.
let cfg = {};
try { cfg = readGsdConfig(dir); } catch (e) {}
// Context window display (shows USED percentage scaled to usable context)
// Claude Code reserves a buffer for autocompact. By default this is ~16.5%
// of the total window, but users can override it via CLAUDE_CODE_AUTO_COMPACT_WINDOW
@@ -349,15 +384,21 @@ function runStatusline() {
const filled = Math.floor(used / 10);
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
// Opt-in absolute token count after the percentage (statusline.show_context_tokens)
let tokenSuffix = '';
if (getConfigValue(cfg, 'statusline.show_context_tokens') === true) {
tokenSuffix = contextTokenSuffix(data.context_window?.current_usage);
}
// Color based on usable context thresholds
if (used < 50) {
ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
ctx = ` \x1b[32m${bar} ${used}%${tokenSuffix}\x1b[0m`;
} else if (used < 65) {
ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
ctx = ` \x1b[33m${bar} ${used}%${tokenSuffix}\x1b[0m`;
} else if (used < 80) {
ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
ctx = ` \x1b[38;5;208m${bar} ${used}%${tokenSuffix}\x1b[0m`;
} else {
ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
ctx = ` \x1b[5;31m💀 ${bar} ${used}%${tokenSuffix}\x1b[0m`;
}
}
@@ -422,7 +463,6 @@ function runStatusline() {
let lastCmdSuffix = '';
let position = 'end';
try {
const cfg = readGsdConfig(dir);
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
const transcriptPath = data.transcript_path;
const lastCmd = readLastSlashCommand(transcriptPath);
@@ -531,6 +571,8 @@ module.exports = {
composeStatusline,
isInstalledAheadOfLatest,
evaluateUpdateCache,
formatTokens,
contextTokenSuffix,
};
/**