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

@@ -41,7 +41,7 @@
"gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c",
@@ -328,7 +328,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "eefea61f9b0e464c",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "8ae31be7a006204b",
"hooks/gsd-statusline.js": "9e64af477e774de6",
"hooks/gsd-update-banner.js": "55143a25f978f301",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -112,7 +112,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -399,7 +399,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "c8800819f7443a15",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "3be32d2012c77fc1",
"hooks/gsd-statusline.js": "3e633cd082663b99",
"hooks/gsd-update-banner.js": "55143a25f978f301",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -111,7 +111,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -398,7 +398,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "7c315416ffc99a9a",
"hooks/gsd-statusline.js": "920558a55eb86587",
"hooks/gsd-update-banner.js": "b457746cb76c1957",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -40,7 +40,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -327,7 +327,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "7c315416ffc99a9a",
"hooks/gsd-statusline.js": "920558a55eb86587",
"hooks/gsd-update-banner.js": "b457746cb76c1957",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -44,7 +44,7 @@
"gsd-core/bin/gsd-tools.cjs": "a84914f7cab74332",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -112,7 +112,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -399,7 +399,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "7f7a7615b303369a",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "ef8dcb6d64fd4493",
"hooks/gsd-statusline.js": "6ab2905537f885ba",
"hooks/gsd-update-banner.js": "55143a25f978f301",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -147,7 +147,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -42,7 +42,7 @@
"gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf",

View File

@@ -112,7 +112,7 @@
"gsd-core/bin/gsd-tools.cjs": "db16ded31dd6eecf",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -41,7 +41,7 @@
"gsd-core/bin/gsd-tools.cjs": "b3f927ee4b4c4711",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -328,7 +328,7 @@
"hooks/gsd-read-guard.js": "1f58b020a91f032b",
"hooks/gsd-read-injection-scanner.js": "f358eca3fa1eab24",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "861808560e60b233",
"hooks/gsd-statusline.js": "34d2ae303558b917",
"hooks/gsd-update-banner.js": "b457746cb76c1957",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -112,7 +112,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -18,7 +18,7 @@
".kimi/hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
".kimi/hooks/gsd-read-injection-scanner.js": "c519598b9257aafa",
".kimi/hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
".kimi/hooks/gsd-statusline.js": "2736b0885aa97bbf",
".kimi/hooks/gsd-statusline.js": "ae0c0e7a934c5deb",
".kimi/hooks/gsd-update-banner.js": "55143a25f978f301",
".kimi/hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
".kimi/hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
@@ -105,7 +105,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -112,7 +112,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -399,7 +399,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "f72060dfe035f706",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "9c132b5985800462",
"hooks/gsd-statusline.js": "3dd40389fa786dd6",
"hooks/gsd-update-banner.js": "55143a25f978f301",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -8,7 +8,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -295,7 +295,7 @@
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "f454242c010804cf",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "5539e1ae859b987e",
"hooks/gsd-statusline.js": "41b0080cf0f3e7e9",
"hooks/gsd-update-banner.js": "55143a25f978f301",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -41,7 +41,7 @@
"gsd-core/bin/gsd-tools.cjs": "6454021dec4d9563",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
@@ -328,7 +328,7 @@
"hooks/gsd-read-guard.js": "2c8d417d12b51040",
"hooks/gsd-read-injection-scanner.js": "396574bd25e99ff9",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "739140996a3c0d49",
"hooks/gsd-statusline.js": "5686cf31c721b2b1",
"hooks/gsd-update-banner.js": "b457746cb76c1957",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",

View File

@@ -41,7 +41,7 @@
"gsd-core/bin/gsd-tools.cjs": "c28c9076058c5e73",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -41,7 +41,7 @@
"gsd-core/bin/gsd-tools.cjs": "568e7c6bdd415c1a",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -112,7 +112,7 @@
"gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/config-schema.manifest.json": "b67705431d738ae7",
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",

View File

@@ -1190,5 +1190,176 @@ test('config-set rejects invalid statusline.context_position', () => {
cleanup(tmpDir);
}
});
// Same write-path enforcement for the boolean statusline.show_context_tokens
// key (#2161) — mirrors the workflow.post_planning_gaps precedent the issue's
// scope names (tests/post-planning-gaps-2493.test.cjs).
test('config-set statusline.show_context_tokens true → persisted as boolean', () => {
const tmpDir = createTempProject();
try {
const r = runGsdTools(['config-set', 'statusline.show_context_tokens', 'true'], tmpDir);
assert.ok(r.success, r.error);
const config = JSON.parse(
fs.readFileSync(path.join(tmpDir, '.planning', 'config.json'), 'utf-8'));
assert.strictEqual(config.statusline.show_context_tokens, true);
} finally {
cleanup(tmpDir);
}
});
test('config-set statusline.show_context_tokens yes → rejected', () => {
const tmpDir = createTempProject();
try {
const r = runGsdTools(['config-set', 'statusline.show_context_tokens', 'yes'], tmpDir);
assert.equal(r.success, false, 'non-boolean value must be rejected');
assert.match(r.error || r.output, /boolean|true|false/i);
} finally {
cleanup(tmpDir);
}
});
});
}
// ────────────────────────────────────────────────────────────────────────
// Context meter token count (statusline.show_context_tokens)
// ────────────────────────────────────────────────────────────────────────
{
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const { cleanup } = require('./helpers.cjs');
const { formatTokens, contextTokenSuffix } = require('../hooks/gsd-statusline.js');
const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config-schema.cjs');
const hookPath = path.join(__dirname, '..', 'hooks', 'gsd-statusline.js');
describe('config schema: statusline.show_context_tokens', () => {
test('registers statusline.show_context_tokens', () => {
assert.ok(
VALID_CONFIG_KEYS.has('statusline.show_context_tokens'),
'statusline.show_context_tokens must be in VALID_CONFIG_KEYS',
);
});
});
describe('formatTokens', () => {
test('passes small counts through', () => {
assert.equal(formatTokens(0), '0');
assert.equal(formatTokens(999), '999');
});
test('rounds thousands to k', () => {
assert.equal(formatTokens(1000), '1k');
assert.equal(formatTokens(156342), '156k');
assert.equal(formatTokens(156700), '157k');
});
test('formats millions with one decimal', () => {
assert.equal(formatTokens(1000000), '1.0M');
assert.equal(formatTokens(1234567), '1.2M');
});
test('k-to-M threshold boundary: limit-1 / limit / limit+1', () => {
// 999,999 k-rounds to 1000 — must promote to the M branch, never "1000k"
assert.equal(formatTokens(999999), '1.0M');
assert.equal(formatTokens(1000000), '1.0M');
assert.equal(formatTokens(1000001), '1.0M');
// 999,499 is the last value that still k-rounds below 1000
assert.equal(formatTokens(999499), '999k');
assert.equal(formatTokens(999500), '1.0M');
});
});
describe('contextTokenSuffix', () => {
test('returns empty string for absent/malformed usage', () => {
assert.equal(contextTokenSuffix(null), '');
assert.equal(contextTokenSuffix(undefined), '');
assert.equal(contextTokenSuffix('nope'), '');
assert.equal(contextTokenSuffix({}), '');
});
test('sums all four token dimensions', () => {
const suffix = contextTokenSuffix({
input_tokens: 1000,
cache_creation_input_tokens: 2000,
cache_read_input_tokens: 150000,
output_tokens: 3000,
});
assert.equal(suffix, ' (156k)');
});
test('tolerates missing dimensions', () => {
assert.equal(contextTokenSuffix({ input_tokens: 500 }), ' (500)');
});
});
describe('statusline output token suffix (e2e)', () => {
function makeProject(flag) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ctx-tokens-'));
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
if (flag !== undefined) {
fs.writeFileSync(
path.join(dir, '.planning', 'config.json'),
JSON.stringify({ statusline: { show_context_tokens: flag } }),
);
}
return dir;
}
function runHook(dir) {
const payload = JSON.stringify({
model: { display_name: 'Claude' },
workspace: { current_dir: dir },
session_id: `test-tokens-${Date.now()}-${Math.random().toString(36).slice(2)}`,
context_window: {
remaining_percentage: 70,
total_tokens: 200000,
current_usage: {
input_tokens: 1000,
cache_read_input_tokens: 150000,
output_tokens: 5000,
},
},
});
let stdout = '';
try {
stdout = execFileSync(process.execPath, [hookPath], {
input: payload, encoding: 'utf8', timeout: 4000,
});
} catch (e) {
stdout = e.stdout || '';
}
// eslint-disable-next-line no-control-regex -- stripping ANSI SGR sequences from captured CLI output
return stdout.replace(/\x1b\[[0-9;]*m/g, '');
}
test('flag=true appends the token count after the percentage', () => {
const dir = makeProject(true);
try {
const out = runHook(dir);
assert.match(out, /% \(156k\)/, `expected "(156k)" after the meter %; got: ${out}`);
} finally {
cleanup(dir);
}
});
test('default (flag absent) meter is unchanged — no token count', () => {
const dir = makeProject(undefined);
try {
const out = runHook(dir);
assert.doesNotMatch(out, /\(\d+(?:\.\d+)?[kM]?\)/, `expected no token suffix; got: ${out}`);
} finally {
cleanup(dir);
}
});
test('flag=false meter is unchanged — no token count', () => {
const dir = makeProject(false);
try {
const out = runHook(dir);
assert.doesNotMatch(out, /\(\d+(?:\.\d+)?[kM]?\)/, `expected no token suffix; got: ${out}`);
} finally {
cleanup(dir);
}
});
});
}