diff --git a/.changeset/proud-cranes-click.md b/.changeset/proud-cranes-click.md new file mode 100644 index 000000000..81133bff2 --- /dev/null +++ b/.changeset/proud-cranes-click.md @@ -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) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 59ef61380..5578e0bfd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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) | diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index ffe7679fb..7b6a3b007 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -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); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index 53518e4ef..fa4959393 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 5a95390ad..b17bce74d 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index b73f7ae5a..298fe2aa6 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 74156c173..06633d0f2 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index 60f20de40..9c9ae39a5 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index ca4301a01..c3820f81b 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 21ea43a53..4b638fcb9 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 2b49ba506..256b45f9a 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index bcceabcc6..5045a1765 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -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", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 00c538545..36ddf63e9 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -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", diff --git a/tests/gsd-statusline.test.cjs b/tests/gsd-statusline.test.cjs index f0ddf07d1..04ef43773 100644 --- a/tests/gsd-statusline.test.cjs +++ b/tests/gsd-statusline.test.cjs @@ -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) // ────────────────────────────────────────────────────────────────────────