* feat(#2160): collapse verbose '(1M context)' model suffix to compact (1M) badge Claude Code appends " (1M context)" to the model display name in long-context sessions, eating 12 characters of statusline width. Collapse it to " (1M)" — the signal stays, the width doesn't. Any other display name passes through unchanged. Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg * docs(#2160): changeset fragment for PR #2173 * fix(#2160): review fixes — ctx variant, boundary test, changeset format - broaden the suffix match with a context|ctx alternation (approval-condition variant the regex missed) - pin non-context parentheticals ((beta), (deprecated)) as untouched - changeset body ends with the (#2160) citation per house convention 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/proud-cranes-click.md
Normal file
5
.changeset/proud-cranes-click.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 2173
|
||||
---
|
||||
**Long-context model names render compactly in the statusline** — the verbose " (1M context)" suffix Claude Code appends to the model display name now collapses to a compact " (1M)" badge (tolerant of future window sizes and the abbreviated "ctx" variant: "(500K context)" → "(500K)", "(1M ctx)" → "(1M)"). Lossless — the long-context signal stays, the 12 characters of width don't. (#2160)
|
||||
@@ -286,7 +286,7 @@ Runtime hooks that integrate with the host AI agent:
|
||||
|
||||
| Hook | Event | Purpose |
|
||||
|------|-------|---------|
|
||||
| `gsd-statusline.js` | `statusLine` | Displays model, task, directory, and context usage bar |
|
||||
| `gsd-statusline.js` | `statusLine` | Displays model (long-context suffixes like `(1M context)` collapse to a compact `(1M)` badge), task, directory, and context usage bar |
|
||||
| `gsd-context-monitor.js` | `PostToolUse` / `AfterTool` | Injects agent-facing context warnings at 35%/25% remaining |
|
||||
| `gsd-check-update.js` | `SessionStart` | Foreground trigger for the background update check |
|
||||
| `gsd-ensure-canonical-path.js` | `SessionStart` | For Claude Code plugin installs, symlinks `~/.claude/gsd-core/{bin,contexts,references,templates,workflows}` to the plugin's bundled tree so `@~/.claude/gsd-core/...` includes resolve; runs first in `SessionStart`, no-op in classic installs, self-heals after `claude plugin update` (#997) |
|
||||
|
||||
@@ -319,6 +319,22 @@ function contextTokenSuffix(currentUsage) {
|
||||
return total > 0 ? ` (${formatTokens(total)})` : '';
|
||||
}
|
||||
|
||||
// --- Model name --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Collapse the verbose " (… context)" model-name suffix Claude Code sends for
|
||||
* long-context sessions (e.g. "Sonnet 4.5 (1M context)") to a compact badge
|
||||
* (" (1M)"). The signal is preserved; the width isn't. Tolerant by design
|
||||
* (issue #2160 approval condition): any trailing parenthesized token ending
|
||||
* in "context" is collapsed — a future "(500K context)" becomes "(500K)"
|
||||
* rather than silently no-opping. The token's own casing is preserved.
|
||||
* Any other display name passes through unchanged.
|
||||
*/
|
||||
function compactModelName(name) {
|
||||
if (typeof name !== 'string') return name;
|
||||
return name.replace(/\s*\(([^)]+?)\s+(?:context|ctx)\)$/i, ' ($1)');
|
||||
}
|
||||
|
||||
// --- Git segment (opt-in) ------------------------------------------------------
|
||||
//
|
||||
// Opt-in via `statusline.show_git: true` in .planning/config.json. Renders the
|
||||
@@ -409,7 +425,7 @@ function runStatusline() {
|
||||
clearTimeout(stdinTimeout);
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
const model = data.model?.display_name || 'Claude';
|
||||
const model = compactModelName(data.model?.display_name || 'Claude');
|
||||
const dir = data.workspace?.current_dir || process.cwd();
|
||||
const session = data.session_id || '';
|
||||
const remaining = data.context_window?.remaining_percentage;
|
||||
@@ -659,6 +675,7 @@ module.exports = {
|
||||
evaluateUpdateCache,
|
||||
formatTokens,
|
||||
contextTokenSuffix,
|
||||
compactModelName,
|
||||
readGitStatus, parseGitStatus, buildGitSegment,
|
||||
};
|
||||
|
||||
@@ -667,7 +684,7 @@ module.exports = {
|
||||
* testing without feeding stdin. Returns the rendered string.
|
||||
*/
|
||||
function renderStatusline(data) {
|
||||
const model = data.model?.display_name || 'Claude';
|
||||
const model = compactModelName(data.model?.display_name || 'Claude');
|
||||
const dir = data.workspace?.current_dir || process.cwd();
|
||||
const dirname = path.basename(dir);
|
||||
|
||||
|
||||
@@ -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": "25996df685a0dac9",
|
||||
"hooks/gsd-statusline.js": "e284419f4ce60383",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "2fab68f4fd190331",
|
||||
"hooks/gsd-statusline.js": "05144fab95be6e45",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "4ad7c2f59577c5bc",
|
||||
"hooks/gsd-statusline.js": "5d2c06224db20d23",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "4ad7c2f59577c5bc",
|
||||
"hooks/gsd-statusline.js": "5d2c06224db20d23",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "29cdce15038d1ab1",
|
||||
"hooks/gsd-statusline.js": "49fe17737e4565f9",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "884347ccd6549f29",
|
||||
"hooks/gsd-statusline.js": "815e107016e994a1",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "be35341758d50fa5",
|
||||
".kimi/hooks/gsd-statusline.js": "e0a50e21a1e9aeb6",
|
||||
".kimi/hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
".kimi/hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
".kimi/hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "6fcb59ad86d2d0ea",
|
||||
"hooks/gsd-statusline.js": "f93d64704cba14fd",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
2
tests/fixtures/golden-install-parity/pi.json
vendored
2
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -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": "daa1a98fde95ccaf",
|
||||
"hooks/gsd-statusline.js": "239c1b15d1ff4ef9",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -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": "2541196849ec5ffc",
|
||||
"hooks/gsd-statusline.js": "e9cfc9ddfffabe4d",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
|
||||
@@ -1365,6 +1365,60 @@ test('config-set statusline.show_context_tokens yes → rejected', () => {
|
||||
}
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Compact 1M model badge
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const statusline = require('../hooks/gsd-statusline.js');
|
||||
const { compactModelName } = statusline;
|
||||
|
||||
describe('compactModelName', () => {
|
||||
test('collapses "(1M context)" to "(1M)"', () => {
|
||||
assert.equal(compactModelName('Sonnet 4.5 (1M context)'), 'Sonnet 4.5 (1M)');
|
||||
});
|
||||
test('is case-insensitive on "context" and preserves the token case', () => {
|
||||
assert.equal(compactModelName('Opus 4.6 (1m CONTEXT)'), 'Opus 4.6 (1m)');
|
||||
});
|
||||
test('tolerates future window sizes (#2160 approval condition)', () => {
|
||||
assert.equal(compactModelName('Sonnet 5 (500K context)'), 'Sonnet 5 (500K)');
|
||||
assert.equal(compactModelName('Opus 5 (2M context)'), 'Opus 5 (2M)');
|
||||
});
|
||||
test('handles the abbreviated "ctx" variant (#2160 approval condition)', () => {
|
||||
assert.equal(compactModelName('Sonnet 4.5 (1M ctx)'), 'Sonnet 4.5 (1M)');
|
||||
assert.equal(compactModelName('Opus 5 (2M CTX)'), 'Opus 5 (2M)');
|
||||
});
|
||||
test('leaves non-context parentheticals untouched', () => {
|
||||
assert.equal(compactModelName('Sonnet 5 (beta)'), 'Sonnet 5 (beta)');
|
||||
assert.equal(compactModelName('Opus 4 (deprecated)'), 'Opus 4 (deprecated)');
|
||||
});
|
||||
test('leaves ordinary names unchanged', () => {
|
||||
assert.equal(compactModelName('Claude'), 'Claude');
|
||||
assert.equal(compactModelName('Sonnet 4.5'), 'Sonnet 4.5');
|
||||
});
|
||||
test('only matches the suffix position', () => {
|
||||
assert.equal(
|
||||
compactModelName('(1M context) Special'), '(1M context) Special');
|
||||
});
|
||||
test('passes non-strings through', () => {
|
||||
assert.equal(compactModelName(undefined), undefined);
|
||||
assert.equal(compactModelName(null), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderStatusline uses the compact badge', () => {
|
||||
test('rendered output carries (1M), not (1M context)', () => {
|
||||
const out = statusline.renderStatusline({
|
||||
model: { display_name: 'Sonnet 4.5 (1M context)' },
|
||||
workspace: { current_dir: require('node:os').tmpdir() },
|
||||
});
|
||||
assert.ok(out.includes('Sonnet 4.5 (1M)'), `expected compact badge; got: ${out}`);
|
||||
assert.ok(!out.includes('(1M context)'), `expected no verbose suffix; got: ${out}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Git segment (statusline.show_git)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user