* 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:
5
.changeset/jolly-jays-march.md
Normal file
5
.changeset/jolly-jays-march.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 2174
|
||||
---
|
||||
**Opt-in absolute token count on the statusline context meter** — new `statusline.show_context_tokens` config (default `false`). When enabled, the meter shows the absolute context total after the percentage, e.g. "████░░░░░░ 46% (156k)", summing input, cache-creation, cache-read, and output tokens from the hook payload (a broader basis than the meter's percentage, which is derived from `used_percentage` and excludes output tokens — the two figures can diverge slightly). Default meter output is unchanged. (#2161)
|
||||
@@ -452,6 +452,7 @@ If `.planning/` is in `.gitignore`, `commit_docs` is automatically `false` regar
|
||||
| `hooks.workflow_guard` | boolean | `false` | Warn when file edits happen outside GSD workflow context (advises using `/gsd-quick` or `/gsd-fast`) |
|
||||
| `statusline.show_last_command` | boolean | `false` | Append `last: /<cmd>` suffix to the statusline showing the most recently invoked slash command. Opt-in; reads the active session transcript to extract the latest `<command-name>` tag (closes #2538) |
|
||||
| `statusline.context_position` | string | `"end"` | Position of the context-window meter. `"end"` (default) renders at line tail; `"front"` renders immediately after the model name so the meter stays visible in narrow terminals. Closes #2937 |
|
||||
| `statusline.show_context_tokens` | boolean | `false` | Append the absolute token count (e.g. `(156k)`) after the context meter's percentage. Sums input, cache-creation, cache-read, and output tokens from the hook payload — a broader basis than the meter's percentage (which excludes output tokens), so the two figures can diverge slightly. Opt-in; the meter is unchanged when the flag is absent |
|
||||
|
||||
The prompt injection guard hook (`gsd-prompt-guard.js`) is always active and cannot be disabled — it's a security feature, not a workflow toggle.
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"workflow.context_coverage_gate",
|
||||
"statusline.show_last_command",
|
||||
"statusline.context_position",
|
||||
"statusline.show_context_tokens",
|
||||
"workflow.max_discuss_passes",
|
||||
"features.thinking_partner",
|
||||
"context",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -763,6 +763,13 @@ function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string |
|
||||
const VALID_CONTEXT_POSITIONS = ['front', 'end'];
|
||||
if (kp === 'statusline.context_position') assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position');
|
||||
|
||||
// statusline.show_context_tokens — boolean only
|
||||
if (kp === 'statusline.show_context_tokens') {
|
||||
if (typeof parsedValue !== 'boolean') {
|
||||
error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallow scope + profile enum validation (#3424)
|
||||
const VALID_FALLOW_SCOPES = ['phase', 'repo'];
|
||||
if (kp === 'code_quality.fallow.scope') assertEnumValue(parsedValue, val, VALID_FALLOW_SCOPES, 'code_quality.fallow.scope');
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
tests/fixtures/golden-install-parity/pi.json
vendored
4
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user