Skip to content

Commit 7515103

Browse files
authored
feat(ccusage): support context_window field from Claude Code statusline hook (#749)
* feat(ccusage): support context_window field from Claude Code statusline hook Claude Code now provides context_window data in its statusline hook JSON, containing total_input_tokens, total_output_tokens, and context_window_size. This change: - Adds context_window field to statuslineHookJsonSchema in _types.ts - Updates statusline command to prefer context_window data when available - Falls back to existing transcript-based calculation when not provided - Refactors context info formatting into reusable helper function - Uses Result type pattern for consistent functional error handling The context_window data from Claude Code is more accurate than parsing the transcript file, as it reflects the actual token counts used by the API. * docs: add context_window reference to statusline guide Add note about Claude Code's context_window data being used for accurate token counts, with link to official documentation. * chore(ccusage): fix typo colour to color
1 parent 0bd2914 commit 7515103

File tree

7 files changed

+55
-17
lines changed

7 files changed

+55
-17
lines changed

apps/ccusage/src/_types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ export const statuslineHookJsonSchema = v.object({
173173
total_lines_added: v.optional(v.number()),
174174
total_lines_removed: v.optional(v.number()),
175175
})),
176+
context_window: v.optional(v.object({
177+
total_input_tokens: v.number(),
178+
total_output_tokens: v.optional(v.number()),
179+
context_window_size: v.number(),
180+
})),
176181
});
177182

178183
/**

apps/ccusage/src/commands/statusline.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -430,28 +430,40 @@ export const statuslineCommand = define({
430430
Result.unwrap({ blockInfo: 'No active block', burnRateInfo: '' }),
431431
);
432432

433-
// Calculate context tokens from transcript with model-specific limits
434-
const contextInfo = await Result.pipe(
435-
Result.try({
436-
try: calculateContextTokens(hookData.transcript_path, hookData.model.id, mergedOptions.offline),
437-
catch: error => error,
438-
}),
433+
// Helper function to format context info with color coding
434+
const formatContextInfo = (inputTokens: number, contextLimit: number): string => {
435+
const percentage = Math.round((inputTokens / contextLimit) * 100);
436+
const color = percentage < ctx.values.contextLowThreshold
437+
? pc.green
438+
: percentage < ctx.values.contextMediumThreshold
439+
? pc.yellow
440+
: pc.red;
441+
const coloredPercentage = color(`${percentage}%`);
442+
const tokenDisplay = inputTokens.toLocaleString();
443+
return `${tokenDisplay} (${coloredPercentage})`;
444+
};
445+
446+
// Get context tokens from Claude Code hook data, or fall back to calculating from transcript
447+
const contextDataResult = hookData.context_window != null
448+
// Prefer context_window data from Claude Code hook if available
449+
? Result.succeed({
450+
inputTokens: hookData.context_window.total_input_tokens,
451+
contextLimit: hookData.context_window.context_window_size,
452+
})
453+
// Fall back to calculating context tokens from transcript
454+
: await Result.try({
455+
try: async () => calculateContextTokens(hookData.transcript_path, hookData.model.id, mergedOptions.offline),
456+
catch: error => error,
457+
})();
458+
459+
const contextInfo = Result.pipe(
460+
contextDataResult,
439461
Result.inspectError(error => logger.debug(`Failed to calculate context tokens: ${error instanceof Error ? error.message : String(error)}`)),
440462
Result.map((contextResult) => {
441463
if (contextResult == null) {
442464
return undefined;
443465
}
444-
// Format context percentage with color coding using option thresholds
445-
const color = contextResult.percentage < ctx.values.contextLowThreshold
446-
? pc.green
447-
: contextResult.percentage < ctx.values.contextMediumThreshold
448-
? pc.yellow
449-
: pc.red;
450-
const coloredPercentage = color(`${contextResult.percentage}%`);
451-
452-
// Format token count with thousand separators
453-
const tokenDisplay = contextResult.inputTokens.toLocaleString();
454-
return `${tokenDisplay} (${coloredPercentage})`;
466+
return formatContextInfo(contextResult.inputTokens, contextResult.contextLimit);
455467
}),
456468
Result.unwrap(undefined),
457469
);

apps/ccusage/test/statusline-test-opus4.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@
2121
"total_lines_added": 0,
2222
"total_lines_removed": 0
2323
},
24+
"context_window": {
25+
"total_input_tokens": 85000,
26+
"total_output_tokens": 5000,
27+
"context_window_size": 200000
28+
},
2429
"exceeds_200k_tokens": false
2530
}

apps/ccusage/test/statusline-test-sonnet4.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@
2121
"total_lines_added": 0,
2222
"total_lines_removed": 0
2323
},
24+
"context_window": {
25+
"total_input_tokens": 35000,
26+
"total_output_tokens": 2500,
27+
"context_window_size": 200000
28+
},
2429
"exceeds_200k_tokens": false
2530
}

apps/ccusage/test/statusline-test-sonnet41.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@
2121
"total_lines_added": 0,
2222
"total_lines_removed": 0
2323
},
24+
"context_window": {
25+
"total_input_tokens": 52000,
26+
"total_output_tokens": 3800,
27+
"context_window_size": 200000
28+
},
2429
"exceeds_200k_tokens": false
2530
}

apps/ccusage/test/statusline-test.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@
2121
"total_lines_added": 0,
2222
"total_lines_removed": 0
2323
},
24+
"context_window": {
25+
"total_input_tokens": 42500,
26+
"total_output_tokens": 3200,
27+
"context_window_size": 200000
28+
},
2429
"exceeds_200k_tokens": false
2530
}

docs/guide/statusline.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ When using `--cost-source both`, the session cost shows both Claude Code and ccu
105105
- Green text: Low usage (< 50% by default)
106106
- Yellow text: Medium usage (50-80% by default)
107107
- Red text: High usage (> 80% by default)
108+
- Uses Claude Code's [`context_window` data](https://code.claude.com/docs/en/statusline) when available for accurate token counts
108109

109110
When no active block exists:
110111

0 commit comments

Comments
 (0)