-
-
Notifications
You must be signed in to change notification settings - Fork 317
feat: add context token display to statusline command #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add context window usage tracking to the statusline hook output by: - Reading transcript file provided by Claude Code statusline hook - Extracting current input token count from transcript usage data - Calculating percentage of 200K context window used - Displaying tokens with thousand separators and color-coded percentage - Green: <50% (normal usage) - Yellow: 50-80% (moderate usage) - Red: >80% (high usage, approaching limit) Format: 🧠 25,000 (13%) appears at end of statusline Addresses user request for context window monitoring to help optimize prompt length and prevent unexpected context management issues. Test files included for validation of different usage levels. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Caution Review failedThe pull request is closed. WalkthroughAdds constants and types for context-window tracking, implements transcript-based context token calculation and thresholds in the data loader (with Zod schemas), integrates a non-fatal, color-coded context usage segment into the statusline command, updates tests and fixtures, and documents the new statusline segment and environment configuration. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as statusline command
participant Loader as data-loader.calculateContextTokens
participant FS as File System
User->>CLI: Run statusline
CLI->>Loader: calculateContextTokens(transcript_path)
Loader->>FS: Read transcript .jsonl (tail → head)
FS-->>Loader: Lines
Loader->>Loader: Parse lines with transcriptMessageSchema, find latest assistant usage, sum input + cache tokens
Loader-->>CLI: { inputTokens, percentage, contextLimit } or null
CLI->>CLI: Format token display and color via getContextUsageThresholds()
CLI-->>User: Render status line with context segment (🧠 tokens (pct%))
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Out-of-scope changes
Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of Changes
Hello @ItsLucas, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
I've implemented a new feature for the statusline command to display the current context window usage for Claude Code. This enhancement addresses a user request to help monitor prompt length and prevent unexpected context management issues by providing real-time feedback on token consumption and context window utilization. The change involves reading transcript files, calculating token usage, and presenting this information clearly within the existing status line, complete with color-coded indicators for usage levels.
Highlights
- Transcript File Reading: I've added the capability to read the transcript file generated by Claude Code to extract the current input token count.
- Context Window Usage Calculation: The system now calculates the percentage of the 200K context window that is currently being utilized based on the extracted token count.
- Enhanced Statusline Display: The statusline command will now display the total input tokens with thousand separators and the context usage percentage, color-coded for quick visual assessment (green for normal, yellow for moderate, red for high usage).
- Updated Statusline Format: The new format for the statusline output now includes the context token information, appearing as
| 🧠 tokens (percentage)at the end.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a valuable feature for monitoring context window usage in the status line. The implementation correctly reads the transcript, calculates token usage, and displays it with helpful color-coding. My review includes a few suggestions to improve maintainability and clarity by refactoring hardcoded values and clarifying the token calculation logic. Overall, this is a great addition.
|
@ItsLucas good but can you move logic to data-loader.ts and import it from statusline.ts |
…ontext display - Move context token calculation logic to data-loader.ts for better organization - Add CONTEXT_LIMIT and CONTEXT_USAGE_THRESHOLDS constants to _consts.ts - Rename totalInputTokens to maxInputTokens for clarity - Refactor color-coding logic to use constants and ternary operators - Improve JSONL parsing based on Python reference implementation - Use Result.try pattern for better error handling - Include all token types (input + cache_creation + cache_read + output) for accuracy 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src/_consts.ts (1)
143-157: Good centralization of context constants; minor readability nitConstants look correct and are used in statusline/data-loader. Consider numeric separators for readability and consistency with codebase style.
-export const CONTEXT_LIMIT = 200000; +export const CONTEXT_LIMIT = 200_000;Optionally, align threshold semantics with _live-rendering.ts to avoid divergence in color thresholds across UIs. If they should differ by design, add a short doc comment noting that.
src/_types.ts (1)
162-178: Types OK; consider validating shape to reduce unsafe JSON handlingThe types are fine and match usage. To cut down on “unsafe” JSON access in data-loader, consider adding a minimal Zod schema for transcript lines and using safeParse. That would also quell several ts/no-unsafe-* lints in parsing.
src/data-loader.ts (1)
1339-1407: Optional: Add in-source tests for calculateContextTokensAdd basic tests (missing file => null, valid line with/without cache tokens) to prevent regressions and document behavior.
// Place near other tests in this file if (import.meta.vitest != null) { describe('calculateContextTokens', () => { it('returns null when transcript cannot be read', async () => { const result = await calculateContextTokens('/nonexistent/path.jsonl'); expect(result).toBeNull(); }); it('parses latest assistant line and excludes output tokens', async () => { await using fixture = await createFixture({ 'transcript.jsonl': [ JSON.stringify({ type: 'user', message: {} }), JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000, output_tokens: 999 } } }), JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 2000, cache_creation_input_tokens: 100, cache_read_input_tokens: 50 } } }), ].join('\n'), }); const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); expect(res).not.toBeNull(); // Should pick the last assistant line and exclude output tokens expect(res?.inputTokens).toBe(2000 + 100 + 50); expect(res?.percentage).toBeGreaterThan(0); }); }); }src/commands/statusline.ts (1)
165-185: Optional simplification of color selectionMinor readability: choose a color function once, then apply it.
-const coloredPercentage = contextData.percentage < CONTEXT_USAGE_THRESHOLDS.LOW - ? pc.green(`${contextData.percentage}%`) - : contextData.percentage < CONTEXT_USAGE_THRESHOLDS.MEDIUM - ? pc.yellow(`${contextData.percentage}%`) - : pc.red(`${contextData.percentage}%`); +const p = contextData.percentage; +const color = p < CONTEXT_USAGE_THRESHOLDS.LOW ? pc.green : p < CONTEXT_USAGE_THRESHOLDS.MEDIUM ? pc.yellow : pc.red; +const coloredPercentage = color(`${p}%`);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/_consts.ts(1 hunks)src/_types.ts(1 hunks)src/commands/statusline.ts(2 hunks)src/data-loader.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{js,jsx,ts,tsx}: Lint code using ESLint MCP server (available via Claude Code tools)
Format code with ESLint (writes changes) usingbun run format
No console.log allowed except where explicitly disabled with eslint-disable
Do not use console.log. Use logger.ts instead.
Files:
src/_consts.tssrc/_types.tssrc/data-loader.tssrc/commands/statusline.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Type check with TypeScript using
bun typecheck
Files:
src/_consts.tssrc/_types.tssrc/data-loader.tssrc/commands/statusline.ts
**/*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use.tsextensions for local file imports (e.g.,import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.isFailure()for checking errors (more readable than!Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators
For async operations: create wrapper function withResult.try()then call it
Keep traditional try-catch only for: file I/O with complex error handling, legacy code that's hard to refactor
Always useResult.isFailure()andResult.isSuccess()type guards for better code clarity
Variables: start with lowercase (camelCase) - e.g.,usageDataSchema,modelBreakdownSchema
Types: start with uppercase (PascalCase) - e.g.,UsageData,ModelBreakdown
Constants: can use UPPER_SNAKE_CASE - e.g.,DEFAULT_CLAUDE_CODE_PATH
Only export constants, functions, and types that are actually used by other modules
Internal/private constants that are only used within the same file should NOT be exported
Always check if a constant is used elsewhere before making itexport constvs justconst
All test files must use current Claude 4 models, not outdated Claude 3 models
Test coverage should include both Sonnet and Opus models for comprehensive validation
Model names in tests must exactly match LiteLLM's pricing database entries
When adding new model tests, verify the model exists in LiteLLM before implementation
Tests depend on real pricing data from LiteLLM - failures may indicate model availability issues
Dynamic imports usingawait import()should only be used within test blocks to avoid tree-shaking issues
Mock data is created usingfs-fixturewithcreateFixture()for Claude data directory simulation
In-source testing pattern: Tests are written...
Files:
src/_consts.tssrc/_types.tssrc/data-loader.tssrc/commands/statusline.ts
**/_*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
Internal files: use underscore prefix - e.g.,
_types.ts,_utils.ts,_consts.ts
Files:
src/_consts.tssrc/_types.ts
🧠 Learnings (1)
📚 Learning: 2025-08-10T20:39:26.428Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T20:39:26.428Z
Learning: Applies to **/*.ts : Constants: can use UPPER_SNAKE_CASE - e.g., `DEFAULT_CLAUDE_CODE_PATH`
Applied to files:
src/data-loader.ts
🧬 Code Graph Analysis (3)
src/_consts.ts (2)
src/_live-rendering.ts (1)
renderLiveDisplay(104-448)src/commands/blocks.ts (1)
limit(278-290)
src/data-loader.ts (3)
src/logger.ts (1)
logger(19-19)src/_types.ts (1)
TranscriptMessage(173-178)src/_consts.ts (1)
CONTEXT_LIMIT(147-147)
src/commands/statusline.ts (4)
src/data-loader.ts (1)
calculateContextTokens(1345-1406)src/_consts.ts (1)
CONTEXT_USAGE_THRESHOLDS(152-156)src/logger.ts (1)
logger(19-19)src/_utils.ts (1)
formatCurrency(302-304)
🪛 ESLint
src/data-loader.ts
[error] 1352-1352: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1352-1352: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1355-1355: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1355-1355: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 1367-1370: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1367-1367: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1367-1367: Unsafe member access .try on an error typed value.
(ts/no-unsafe-member-access)
[error] 1371-1371: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1371-1371: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1372-1372: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 1372-1372: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1372-1372: Unsafe member access .isFailure on an error typed value.
(ts/no-unsafe-member-access)
[error] 1376-1376: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1376-1376: Unsafe member access .value on an error typed value.
(ts/no-unsafe-member-access)
[error] 1379-1379: Unsafe member access .type on an error typed value.
(ts/no-unsafe-member-access)
[error] 1380-1380: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1381-1381: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1382-1382: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1383-1383: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1384-1384: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1385-1385: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1386-1386: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1386-1386: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1387-1391: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1388-1388: Unsafe member access .input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1389-1389: Unsafe member access .cache_creation_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1390-1390: Unsafe member access .cache_read_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1391-1391: Unsafe member access .output_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1396-1396: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1404-1404: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1404-1404: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
src/commands/statusline.ts
[error] 168-168: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 168-168: Unsafe member access .transcript_path on an error typed value.
(ts/no-unsafe-member-access)
[error] 171-175: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 172-172: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 172-172: Unsafe member access .green on an error typed value.
(ts/no-unsafe-member-access)
[error] 174-174: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 174-174: Unsafe member access .yellow on an error typed value.
(ts/no-unsafe-member-access)
[error] 175-175: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 175-175: Unsafe member access .red on an error typed value.
(ts/no-unsafe-member-access)
[error] 183-183: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 183-183: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 187-187: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 187-187: Unsafe member access .model on an error typed value.
(ts/no-unsafe-member-access)
🔇 Additional comments (1)
src/commands/statusline.ts (1)
190-193: Status line format: consistent and clearLGTM. The added context segment is appended at the end and uses the thresholds provided. No functional issues here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/commands/statusline.ts (1)
165-185: Simplify color selection and fix ESLint catch typing
- Reduce duplication by selecting a color function once, then applying it.
- Type the catch variable as unknown to satisfy ts/no-unsafe rules.
- try { - const contextData = await calculateContextTokens(hookData.transcript_path); + try { + const contextData = await calculateContextTokens(hookData.transcript_path); if (contextData != null) { - // Format context percentage with color coding using constants - const coloredPercentage = contextData.percentage < CONTEXT_USAGE_THRESHOLDS.LOW - ? pc.green(`${contextData.percentage}%`) - : contextData.percentage < CONTEXT_USAGE_THRESHOLDS.MEDIUM - ? pc.yellow(`${contextData.percentage}%`) - : pc.red(`${contextData.percentage}%`); + // Format context percentage with color coding using constants + const percentageText = `${contextData.percentage}%`; + const colorFn = contextData.percentage < CONTEXT_USAGE_THRESHOLDS.LOW + ? pc.green + : contextData.percentage < CONTEXT_USAGE_THRESHOLDS.MEDIUM + ? pc.yellow + : pc.red; + const coloredPercentage = colorFn(percentageText); // Format token count with thousand separators const tokenDisplay = contextData.maxInputTokens.toLocaleString(); contextInfo = ` | 🧠 ${tokenDisplay} (${coloredPercentage})`; } - } - catch (error) { - logger.debug('Failed to calculate context tokens:', error); + } + catch (error: unknown) { + logger.debug('Failed to calculate context tokens:', error); }
🧹 Nitpick comments (1)
src/_consts.ts (1)
147-147: Use numeric separator for readability200000 reads better as 200_000.
-export const CONTEXT_LIMIT = 200000; +export const CONTEXT_LIMIT = 200_000;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/_consts.ts(1 hunks)src/_types.ts(1 hunks)src/commands/statusline.ts(2 hunks)src/data-loader.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{js,jsx,ts,tsx}: Lint code using ESLint MCP server (available via Claude Code tools)
Format code with ESLint (writes changes) usingbun run format
No console.log allowed except where explicitly disabled with eslint-disable
Do not use console.log. Use logger.ts instead.
Files:
src/_consts.tssrc/_types.tssrc/data-loader.tssrc/commands/statusline.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Type check with TypeScript using
bun typecheck
Files:
src/_consts.tssrc/_types.tssrc/data-loader.tssrc/commands/statusline.ts
**/*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use.tsextensions for local file imports (e.g.,import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.isFailure()for checking errors (more readable than!Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators
For async operations: create wrapper function withResult.try()then call it
Keep traditional try-catch only for: file I/O with complex error handling, legacy code that's hard to refactor
Always useResult.isFailure()andResult.isSuccess()type guards for better code clarity
Variables: start with lowercase (camelCase) - e.g.,usageDataSchema,modelBreakdownSchema
Types: start with uppercase (PascalCase) - e.g.,UsageData,ModelBreakdown
Constants: can use UPPER_SNAKE_CASE - e.g.,DEFAULT_CLAUDE_CODE_PATH
Only export constants, functions, and types that are actually used by other modules
Internal/private constants that are only used within the same file should NOT be exported
Always check if a constant is used elsewhere before making itexport constvs justconst
All test files must use current Claude 4 models, not outdated Claude 3 models
Test coverage should include both Sonnet and Opus models for comprehensive validation
Model names in tests must exactly match LiteLLM's pricing database entries
When adding new model tests, verify the model exists in LiteLLM before implementation
Tests depend on real pricing data from LiteLLM - failures may indicate model availability issues
Dynamic imports usingawait import()should only be used within test blocks to avoid tree-shaking issues
Mock data is created usingfs-fixturewithcreateFixture()for Claude data directory simulation
In-source testing pattern: Tests are written...
Files:
src/_consts.tssrc/_types.tssrc/data-loader.tssrc/commands/statusline.ts
**/_*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
Internal files: use underscore prefix - e.g.,
_types.ts,_utils.ts,_consts.ts
Files:
src/_consts.tssrc/_types.ts
🧠 Learnings (1)
📚 Learning: 2025-08-10T20:39:26.428Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T20:39:26.428Z
Learning: Applies to **/*.ts : Constants: can use UPPER_SNAKE_CASE - e.g., `DEFAULT_CLAUDE_CODE_PATH`
Applied to files:
src/data-loader.ts
🧬 Code Graph Analysis (3)
src/_consts.ts (2)
docs/update-api-index.ts (1)
updateConstsPage(49-74)src/_live-rendering.ts (1)
renderLiveDisplay(104-448)
src/data-loader.ts (3)
src/logger.ts (1)
logger(19-19)src/_types.ts (1)
TranscriptMessage(173-178)src/_consts.ts (1)
CONTEXT_LIMIT(147-147)
src/commands/statusline.ts (3)
src/data-loader.ts (1)
calculateContextTokens(1345-1406)src/_consts.ts (1)
CONTEXT_USAGE_THRESHOLDS(152-156)src/_utils.ts (1)
formatCurrency(302-304)
🪛 ESLint
src/data-loader.ts
[error] 1352-1352: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1352-1352: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1355-1355: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1355-1355: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 1367-1370: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1367-1367: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1367-1367: Unsafe member access .try on an error typed value.
(ts/no-unsafe-member-access)
[error] 1371-1371: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1371-1371: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1372-1372: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 1372-1372: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1372-1372: Unsafe member access .isFailure on an error typed value.
(ts/no-unsafe-member-access)
[error] 1376-1376: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1376-1376: Unsafe member access .value on an error typed value.
(ts/no-unsafe-member-access)
[error] 1379-1379: Unsafe member access .type on an error typed value.
(ts/no-unsafe-member-access)
[error] 1380-1380: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1381-1381: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1382-1382: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1383-1383: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1384-1384: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1385-1385: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1386-1386: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1386-1386: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1387-1391: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1388-1388: Unsafe member access .input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1389-1389: Unsafe member access .cache_creation_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1390-1390: Unsafe member access .cache_read_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1391-1391: Unsafe member access .output_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1396-1396: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1404-1404: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1404-1404: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
src/commands/statusline.ts
[error] 168-168: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 168-168: Unsafe member access .transcript_path on an error typed value.
(ts/no-unsafe-member-access)
[error] 171-175: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 172-172: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 172-172: Unsafe member access .green on an error typed value.
(ts/no-unsafe-member-access)
[error] 174-174: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 174-174: Unsafe member access .yellow on an error typed value.
(ts/no-unsafe-member-access)
[error] 175-175: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 175-175: Unsafe member access .red on an error typed value.
(ts/no-unsafe-member-access)
[error] 183-183: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 183-183: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 187-187: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 187-187: Unsafe member access .model on an error typed value.
(ts/no-unsafe-member-access)
🔇 Additional comments (5)
src/_types.ts (1)
162-179: Types look good and align with data-loader usageOptional fields match transcript variability and keep parsing resilient. No blockers.
src/_consts.ts (1)
152-156: Threshold constants are clear and documentedColor threshold constants and comments are clear and match the PR objectives.
src/commands/statusline.ts (2)
5-5: Good: thresholds are imported from shared constantsCentralizing thresholds in _consts.ts improves consistency and maintainability.
11-11: Good: data-loader owns context parsing logicImporting calculateContextTokens keeps statusline lean and follows the requested organization.
src/data-loader.ts (1)
1345-1406: Add in-source tests for calculateContextTokensRecommend adding minimal tests to lock behavior and prevent regressions (covers: valid usage, missing fields treated as 0, invalid JSON lines, and excluding output tokens from context).
Add near existing tests:
if (import.meta.vitest != null) { describe('calculateContextTokens', () => { it('calculates input-side context tokens and percentage from latest assistant line', async () => { await using fixture = await createFixture({ 'transcript.jsonl': [ JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000, output_tokens: 500 } } }), JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 2000, cache_creation_input_tokens: 300, cache_read_input_tokens: 700, output_tokens: 800 } } }), ].join('\n'), }); const path = fixture.getPath('transcript.jsonl'); const res = await calculateContextTokens(path); expect(res).not.toBeNull(); // Excludes output tokens: 2000 + 300 + 700 = 3000 expect(res?.maxInputTokens).toBe(3000); expect(res?.percentage).toBe(Math.round((3000 / CONTEXT_LIMIT) * 100)); }); it('treats missing fields as zero and skips invalid JSON', async () => { await using fixture = await createFixture({ 'transcript.jsonl': [ 'invalid json', JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1500 } } }), ].join('\n'), }); const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); expect(res?.maxInputTokens).toBe(1500); }); it('returns null when no assistant usage found', async () => { await using fixture = await createFixture({ 'transcript.jsonl': JSON.stringify({ type: 'user', message: { content: 'hi' } }), }); const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); expect(res).toBeNull(); }); }); }⛔ Skipped due to learnings
Learnt from: CR PR: ryoppippi/ccusage#0 File: CLAUDE.md:0-0 Timestamp: 2025-08-10T20:39:26.428Z Learning: Applies to **/*.ts : In-source testing pattern: Tests are written directly in the same files as the source code, not in separate test files. Use `if (import.meta.vitest != null)` blocks.
… type safety - Fix context calculation to exclude output_tokens from percentage calculation - Rename maxInputTokens to inputTokens for accuracy - Make cache fields optional using nullish coalescing - Clamp percentage to 0-100 range - Improve JSON parsing with proper try-catch and type safety - Add numeric separators to CONTEXT_LIMIT constant for readability - Simplify color selection logic in statusline - Add comprehensive in-source tests for calculateContextTokens 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/data-loader.ts (1)
1345-1398: Solid implementation; switch JSON parsing to Result.try and drop non-null assertionLogic aligns with PR goals: scans from end, excludes output_tokens, defaults missing cache fields to 0, and clamps percentage. To match codebase guidelines and avoid ts/no-unsafe patterns:
- Parse with Result.try() and check Result.isFailure(...) instead of try/catch inside the loop.
- Remove the non-null assertion on usage.input_tokens and use ?? 0.
- for (const line of lines) { + for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine === '') { continue; } - - try { - const obj = JSON.parse(trimmedLine) as TranscriptMessage; - - // Check if this line contains the required token usage fields - if (obj.type === 'assistant' - && obj.message != null - && obj.message.usage != null - && obj.message.usage.input_tokens != null) { - const usage = obj.message.usage; - const inputTokens - = usage.input_tokens! - + (usage.cache_creation_input_tokens ?? 0) - + (usage.cache_read_input_tokens ?? 0); - - const percentage = Math.min(100, Math.max(0, Math.round((inputTokens / CONTEXT_LIMIT) * 100))); - - return { - inputTokens, - percentage, - contextLimit: CONTEXT_LIMIT, - }; - } - } - catch { - continue; // Skip malformed JSON lines - } + const parse = Result.try<TranscriptMessage>(() => JSON.parse(trimmedLine) as TranscriptMessage)(); + if (Result.isFailure(parse)) { + continue; // Skip malformed JSON lines + } + const obj = parse.value; + if (obj.type === 'assistant' && obj.message?.usage?.input_tokens != null) { + const usage = obj.message.usage; + const inputTokens = + (usage.input_tokens ?? 0) + + (usage.cache_creation_input_tokens ?? 0) + + (usage.cache_read_input_tokens ?? 0); + const percentage = Math.min(100, Math.max(0, Math.round((inputTokens / CONTEXT_LIMIT) * 100))); + return { inputTokens, percentage, contextLimit: CONTEXT_LIMIT }; + } }src/commands/statusline.ts (1)
165-183: Harden logging to satisfy ts/no-unsafe- and avoid passing unknown to logger*Format the caught error into a string instead of passing the raw unknown to logger.debug. This matches prior guidance and clears lints.
try { const contextData = await calculateContextTokens(hookData.transcript_path); if (contextData != null) { // Format context percentage with color coding using constants const p = contextData.percentage; const color = p < CONTEXT_USAGE_THRESHOLDS.LOW ? pc.green : p < CONTEXT_USAGE_THRESHOLDS.MEDIUM ? pc.yellow : pc.red; const coloredPercentage = color(`${p}%`); // Format token count with thousand separators const tokenDisplay = contextData.inputTokens.toLocaleString(); contextInfo = ` | 🧠 ${tokenDisplay} (${coloredPercentage})`; } } catch (error) { - logger.debug('Failed to calculate context tokens:', error); + logger.debug( + `Failed to calculate context tokens: ${error instanceof Error ? error.message : String(error)}`, + ); }
🧹 Nitpick comments (2)
src/data-loader.ts (1)
1401-1446: Tests cover key cases; import fs-fixture dynamically inside test blockCoverage is great: unreadable path, latest assistant line, optional cache fields, and clamping. To follow the repo’s “dynamic imports only within test blocks” guidance and avoid bundling test-only deps at runtime, import fs-fixture inside the vitest block.
if (import.meta.vitest != null) { + const { createFixture } = await import('fs-fixture'); describe('calculateContextTokens', () => { it('returns null when transcript cannot be read', async () => { const result = await calculateContextTokens('/nonexistent/path.jsonl'); expect(result).toBeNull(); });Note: Other test sections in this file also use createFixture; consider applying the same pattern there in a follow-up. I can help batch-edit if desired.
src/commands/statusline.ts (1)
171-174: Minor: simplify color selection for readabilityUse a single colorFn ternary and avoid the temporary variable color.
- const p = contextData.percentage; - const color = p < CONTEXT_USAGE_THRESHOLDS.LOW ? pc.green : p < CONTEXT_USAGE_THRESHOLDS.MEDIUM ? pc.yellow : pc.red; - const coloredPercentage = color(`${p}%`); + const p = contextData.percentage; + const colorFn = p < CONTEXT_USAGE_THRESHOLDS.LOW + ? pc.green + : p < CONTEXT_USAGE_THRESHOLDS.MEDIUM + ? pc.yellow + : pc.red; + const coloredPercentage = colorFn(`${p}%`);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/_consts.ts(1 hunks)src/commands/statusline.ts(2 hunks)src/data-loader.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/_consts.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{js,jsx,ts,tsx}: Lint code using ESLint MCP server (available via Claude Code tools)
Format code with ESLint (writes changes) usingbun run format
No console.log allowed except where explicitly disabled with eslint-disable
Do not use console.log. Use logger.ts instead.
Files:
src/data-loader.tssrc/commands/statusline.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Type check with TypeScript using
bun typecheck
Files:
src/data-loader.tssrc/commands/statusline.ts
**/*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use.tsextensions for local file imports (e.g.,import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.isFailure()for checking errors (more readable than!Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators
For async operations: create wrapper function withResult.try()then call it
Keep traditional try-catch only for: file I/O with complex error handling, legacy code that's hard to refactor
Always useResult.isFailure()andResult.isSuccess()type guards for better code clarity
Variables: start with lowercase (camelCase) - e.g.,usageDataSchema,modelBreakdownSchema
Types: start with uppercase (PascalCase) - e.g.,UsageData,ModelBreakdown
Constants: can use UPPER_SNAKE_CASE - e.g.,DEFAULT_CLAUDE_CODE_PATH
Only export constants, functions, and types that are actually used by other modules
Internal/private constants that are only used within the same file should NOT be exported
Always check if a constant is used elsewhere before making itexport constvs justconst
All test files must use current Claude 4 models, not outdated Claude 3 models
Test coverage should include both Sonnet and Opus models for comprehensive validation
Model names in tests must exactly match LiteLLM's pricing database entries
When adding new model tests, verify the model exists in LiteLLM before implementation
Tests depend on real pricing data from LiteLLM - failures may indicate model availability issues
Dynamic imports usingawait import()should only be used within test blocks to avoid tree-shaking issues
Mock data is created usingfs-fixturewithcreateFixture()for Claude data directory simulation
In-source testing pattern: Tests are written...
Files:
src/data-loader.tssrc/commands/statusline.ts
🧠 Learnings (1)
📚 Learning: 2025-08-10T20:39:26.428Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T20:39:26.428Z
Learning: Applies to **/*.ts : Constants: can use UPPER_SNAKE_CASE - e.g., `DEFAULT_CLAUDE_CODE_PATH`
Applied to files:
src/data-loader.ts
🪛 ESLint
src/data-loader.ts
[error] 1352-1352: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1352-1352: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1355-1355: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1355-1355: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 1396-1396: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1396-1396: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 1409-1415: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1409-1409: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1416-1416: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 1416-1416: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1416-1416: Unsafe member access .getPath on an error typed value.
(ts/no-unsafe-member-access)
[error] 1424-1428: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1424-1424: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1429-1429: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 1429-1429: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1429-1429: Unsafe member access .getPath on an error typed value.
(ts/no-unsafe-member-access)
[error] 1436-1440: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1436-1436: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1441-1441: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 1441-1441: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1441-1441: Unsafe member access .getPath on an error typed value.
(ts/no-unsafe-member-access)
src/commands/statusline.ts
[error] 168-168: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 168-168: Unsafe member access .transcript_path on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 172-172: Unsafe member access .green on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe member access .yellow on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe member access .red on an error typed value.
(ts/no-unsafe-member-access)
[error] 173-173: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 173-173: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 181-181: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 181-181: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 185-185: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 185-185: Unsafe member access .model on an error typed value.
(ts/no-unsafe-member-access)
🔇 Additional comments (5)
src/data-loader.ts (2)
20-23: Type-only import is correct and minimalImporting TranscriptMessage as a type keeps runtime lean. LGTM.
36-36: Good: centralizes context limit via constantUsing CONTEXT_LIMIT avoids magic numbers and respects config conventions.
src/commands/statusline.ts (3)
5-5: Good: uses shared thresholds for color-codingPulling CONTEXT_USAGE_THRESHOLDS from consts avoids magic numbers and centralizes behavior.
11-11: Correct: consumes loader API for transcript-based context dataImporting calculateContextTokens from data-loader.ts keeps statusline thin and aligned with the requested organization.
188-191: Statusline composition looks goodFormat matches spec and appends context segment safely when available.
- Fix error logging to avoid passing unknown to logger.debug - Move fs-fixture import inside test block to follow dynamic import guidelines - Apply code style improvements for better maintainability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
ryoppippi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi added some comments!
@ItsLucas
PTAL
commit: |
ryoppippi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not approved, sorry please take a look
- Move calculateContextTokens test to end of file - Add Zod schemas for transcript usage validation (transcriptUsageSchema, transcriptMessageSchema) - Make CONTEXT_USAGE_THRESHOLDS configurable via environment variables: - CCUSAGE_CONTEXT_LOW_THRESHOLD (default: 50) - CCUSAGE_CONTEXT_MEDIUM_THRESHOLD (default: 80) - Update statusline documentation with new environment variables and context usage display 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/_consts.ts (1)
143-148: Make CONTEXT_LIMIT overridable via env var for flexibility across modelsHardcoding 200k is fine for Claude 4 default, but allowing an env override helps when using models with different context windows and addresses prior feedback about env-driven config.
Apply this diff to introduce an overridable env var name:
/** * Default Claude 4 context window limit (200,000 tokens) * Used for calculating context usage percentages in statusline */ -export const CONTEXT_LIMIT = 200_000; +export const CONTEXT_LIMIT = 200_000; +/** + * Environment variable name for overriding context window limit (tokens) + */ +export const CONTEXT_LIMIT_ENV = 'CCUSAGE_CONTEXT_LIMIT';Follow-up: wire this into calculateContextTokens in data-loader.ts so it uses the override when present (see my comment there).
🧹 Nitpick comments (4)
src/commands/statusline.ts (1)
164-183: Minor readability: rename p to percentageUsing an explicit variable name avoids ambiguity and improves maintainability.
Apply this diff:
- // Format context percentage with color coding using configurable thresholds - const p = contextData.percentage; + // Format context percentage with color coding using configurable thresholds + const percentage = contextData.percentage; const thresholds = getContextUsageThresholds(); - const color = p < thresholds.LOW ? pc.green : p < thresholds.MEDIUM ? pc.yellow : pc.red; - const coloredPercentage = color(`${p}%`); + const color = percentage < thresholds.LOW + ? pc.green + : percentage < thresholds.MEDIUM + ? pc.yellow + : pc.red; + const coloredPercentage = color(`${percentage}%`);src/data-loader.ts (3)
199-218: Limit schema visibility; these are internaltranscriptUsageSchema and transcriptMessageSchema are only used within this module; exporting leaks internal details.
Apply this diff:
-export const transcriptUsageSchema = z.object({ +const transcriptUsageSchema = z.object({ input_tokens: z.number().optional(), cache_creation_input_tokens: z.number().optional(), cache_read_input_tokens: z.number().optional(), output_tokens: z.number().optional(), }); -export const transcriptMessageSchema = z.object({ +const transcriptMessageSchema = z.object({ type: z.string().optional(), message: z.object({ usage: transcriptUsageSchema.optional(), }).optional(), });
1374-1439: Optional: prefer Result.try for JSON.parse per project conventionsThe codebase leans toward Result.try for throwable operations. Not a blocker, but aligning here reduces try/catch noise.
If you want to align now, I can provide a Result.try-based parse loop.
4677-4721: Add tests for env overrides (thresholds and limit)We already cover parsing and clamping; add focused tests to lock in env behavior and prevent regressions.
You can append tests like these within the import.meta.vitest block:
describe('getContextUsageThresholds', () => { afterEach(() => { vi.unstubAllEnvs(); }); it('clamps invalid env values and enforces LOW < MEDIUM', () => { vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '150'); // >100 → clamp vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '10'); // <= LOW → bump const t = getContextUsageThresholds(); expect(t.LOW).toBe(100); expect(t.MEDIUM).toBe(100); // LOW==100 → MEDIUM stays 100 }); it('parses valid integer envs', () => { vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '40'); vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '75'); const t = getContextUsageThresholds(); expect(t).toEqual({ LOW: 40, MEDIUM: 75 }); }); }); describe('calculateContextTokens with CONTEXT_LIMIT override', async () => { afterEach(() => { vi.unstubAllEnvs(); }); it('uses CCUSAGE_CONTEXT_LIMIT when valid', async () => { vi.stubEnv('CCUSAGE_CONTEXT_LIMIT', '100000'); // 100k limit await using fixture = await createFixture({ 'transcript.jsonl': JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 50_000 } } }), }); const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); expect(res?.contextLimit).toBe(100000); expect(res?.percentage).toBe(50); }); it('ignores invalid CONTEXT_LIMIT overrides', async () => { vi.stubEnv('CCUSAGE_CONTEXT_LIMIT', 'not-a-number'); await using fixture = await createFixture({ 'transcript.jsonl': JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 100_000 } } }), }); const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); expect(res?.contextLimit).toBe(CONTEXT_LIMIT); expect(res?.percentage).toBe(50); }); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
docs/guide/statusline.md(3 hunks)src/_consts.ts(1 hunks)src/commands/statusline.ts(2 hunks)src/data-loader.ts(5 hunks)
✅ Files skipped from review due to trivial changes (1)
- docs/guide/statusline.md
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{js,jsx,ts,tsx}: Lint code using ESLint MCP server (available via Claude Code tools)
Format code with ESLint (writes changes) usingbun run format
No console.log allowed except where explicitly disabled with eslint-disable
Do not use console.log. Use logger.ts instead.
Files:
src/_consts.tssrc/data-loader.tssrc/commands/statusline.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Type check with TypeScript using
bun typecheck
Files:
src/_consts.tssrc/data-loader.tssrc/commands/statusline.ts
**/*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use.tsextensions for local file imports (e.g.,import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.isFailure()for checking errors (more readable than!Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators
For async operations: create wrapper function withResult.try()then call it
Keep traditional try-catch only for: file I/O with complex error handling, legacy code that's hard to refactor
Always useResult.isFailure()andResult.isSuccess()type guards for better code clarity
Variables: start with lowercase (camelCase) - e.g.,usageDataSchema,modelBreakdownSchema
Types: start with uppercase (PascalCase) - e.g.,UsageData,ModelBreakdown
Constants: can use UPPER_SNAKE_CASE - e.g.,DEFAULT_CLAUDE_CODE_PATH
Only export constants, functions, and types that are actually used by other modules
Internal/private constants that are only used within the same file should NOT be exported
Always check if a constant is used elsewhere before making itexport constvs justconst
All test files must use current Claude 4 models, not outdated Claude 3 models
Test coverage should include both Sonnet and Opus models for comprehensive validation
Model names in tests must exactly match LiteLLM's pricing database entries
When adding new model tests, verify the model exists in LiteLLM before implementation
Tests depend on real pricing data from LiteLLM - failures may indicate model availability issues
Dynamic imports usingawait import()should only be used within test blocks to avoid tree-shaking issues
Mock data is created usingfs-fixturewithcreateFixture()for Claude data directory simulation
In-source testing pattern: Tests are written...
Files:
src/_consts.tssrc/data-loader.tssrc/commands/statusline.ts
**/_*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
Internal files: use underscore prefix - e.g.,
_types.ts,_utils.ts,_consts.ts
Files:
src/_consts.ts
🧠 Learnings (1)
📚 Learning: 2025-08-10T20:39:26.465Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T20:39:26.465Z
Learning: Applies to **/*.ts : Constants: can use UPPER_SNAKE_CASE - e.g., `DEFAULT_CLAUDE_CODE_PATH`
Applied to files:
src/_consts.tssrc/data-loader.ts
🧬 Code Graph Analysis (2)
src/data-loader.ts (2)
src/_consts.ts (4)
CONTEXT_LOW_THRESHOLD_ENV(161-161)CONTEXT_MEDIUM_THRESHOLD_ENV(166-166)DEFAULT_CONTEXT_USAGE_THRESHOLDS(152-156)CONTEXT_LIMIT(147-147)src/logger.ts (1)
logger(19-19)
src/commands/statusline.ts (3)
src/data-loader.ts (2)
calculateContextTokens(1380-1438)getContextUsageThresholds(143-152)src/logger.ts (1)
logger(19-19)src/_utils.ts (1)
formatCurrency(302-304)
🪛 ESLint
src/data-loader.ts
[error] 145-145: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 145-145: Unsafe member access .env on an error typed value.
(ts/no-unsafe-member-access)
[error] 146-146: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 146-146: Unsafe member access .env on an error typed value.
(ts/no-unsafe-member-access)
[error] 149-149: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 150-150: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 202-207: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 202-202: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 202-202: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 203-203: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 203-203: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 203-203: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 203-203: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 203-203: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 204-204: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 204-204: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 204-204: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 204-204: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 204-204: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 205-205: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 205-205: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 205-205: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 205-205: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 205-205: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 206-206: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 206-206: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 206-206: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 206-206: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 206-206: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 212-217: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 212-212: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 212-212: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 213-213: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 213-213: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 213-213: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 213-213: Unsafe member access .string on an error typed value.
(ts/no-unsafe-member-access)
[error] 213-213: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 214-216: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 214-216: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 214-214: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 214-214: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 215-215: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 215-215: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 215-215: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 216-216: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 1387-1387: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1387-1387: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1390-1390: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1390-1390: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 1404-1404: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1404-1404: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1404-1404: Unsafe member access .safeParse on an error typed value.
(ts/no-unsafe-member-access)
[error] 1405-1405: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 1405-1405: Unsafe member access .success on an error typed value.
(ts/no-unsafe-member-access)
[error] 1408-1408: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1408-1408: Unsafe member access .data on an error typed value.
(ts/no-unsafe-member-access)
[error] 1411-1411: Unsafe member access .type on an error typed value.
(ts/no-unsafe-member-access)
[error] 1412-1412: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1413-1413: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1414-1414: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1415-1415: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1415-1415: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1416-1419: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1417-1417: Unsafe member access .input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1418-1418: Unsafe member access .cache_creation_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1419-1419: Unsafe member access .cache_read_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1424-1424: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1436-1436: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1436-1436: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 4683-4683: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4685-4691: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4685-4685: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4692-4692: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4692-4692: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4692-4692: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
[error] 4700-4704: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4700-4700: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4705-4705: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4705-4705: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4705-4705: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
[error] 4712-4716: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4712-4712: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4717-4717: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4717-4717: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4717-4717: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
src/commands/statusline.ts
[error] 167-167: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 167-167: Unsafe member access .transcript_path on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 172-172: Unsafe member access .green on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe member access .yellow on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe member access .red on an error typed value.
(ts/no-unsafe-member-access)
[error] 173-173: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 173-173: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 181-181: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 181-181: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 185-185: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 185-185: Unsafe member access .model on an error typed value.
(ts/no-unsafe-member-access)
🔇 Additional comments (2)
src/commands/statusline.ts (2)
10-10: LGTM: imports align with modularization requestImporting calculateContextTokens and getContextUsageThresholds from data-loader keeps statusline slim and follows the requested re-organization.
188-191: LGTM: concise, color-coded context segment appended to status lineMatches the desired format “🧠 25,000 (13%)” and keeps non-fatal behavior on parsing failures.
…ty features - Add comprehensive input validation for environment variables - Clamp values to 0-100 range preventing out-of-bounds percentages - Enforce LOW < MEDIUM ordering constraint with fallback to defaults - Handle non-numeric values gracefully by falling back to safe defaults - Prevent NaN propagation that caused incorrect UI coloring - Add 11 comprehensive tests covering edge cases and invalid configurations - Update documentation with validation features and invalid config examples This ensures the statusline context usage display will always show meaningful color coding regardless of environment variable configuration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/data-loader.ts (3)
35-35: Import CONTEXT_LIMIT_ENV constant for environment override supportMissing CONTEXT_LIMIT_ENV from the imports. This constant is referenced in the PR objectives and past review comments but not imported.
Apply this diff:
-import { CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, CONTEXT_LIMIT, CONTEXT_LOW_THRESHOLD_ENV, CONTEXT_MEDIUM_THRESHOLD_ENV, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_CONTEXT_USAGE_THRESHOLDS, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR } from './_consts.ts'; +import { CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, CONTEXT_LIMIT, CONTEXT_LIMIT_ENV, CONTEXT_LOW_THRESHOLD_ENV, CONTEXT_MEDIUM_THRESHOLD_ENV, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_CONTEXT_USAGE_THRESHOLDS, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR } from './_consts.ts';
144-178: Fix environment variable access and harden threshold validationThe static analysis correctly identifies unsafe access to
process.envwithout proper typing. Also implement proper validation with ordering constraints.Apply this diff:
export function getContextUsageThresholds(): { readonly LOW: number; readonly MEDIUM: number } { - // Parse and validate environment variables - const lowThresholdStr = process.env[CONTEXT_LOW_THRESHOLD_ENV]; - const mediumThresholdStr = process.env[CONTEXT_MEDIUM_THRESHOLD_ENV]; - - // Parse with validation - use defaults for invalid values - let lowThreshold: number = DEFAULT_CONTEXT_USAGE_THRESHOLDS.LOW; - let mediumThreshold: number = DEFAULT_CONTEXT_USAGE_THRESHOLDS.MEDIUM; - - if (lowThresholdStr != null) { - const parsed = Number.parseInt(lowThresholdStr, 10); - if (!Number.isNaN(parsed)) { - lowThreshold = Math.max(0, Math.min(100, parsed)); // Clamp to 0-100 - } - } - - if (mediumThresholdStr != null) { - const parsed = Number.parseInt(mediumThresholdStr, 10); - if (!Number.isNaN(parsed)) { - mediumThreshold = Math.max(0, Math.min(100, parsed)); // Clamp to 0-100 - } - } - - // Enforce ordering: LOW must be less than MEDIUM - // If ordering is violated, reset both to defaults - if (lowThreshold >= mediumThreshold) { - lowThreshold = DEFAULT_CONTEXT_USAGE_THRESHOLDS.LOW; - mediumThreshold = DEFAULT_CONTEXT_USAGE_THRESHOLDS.MEDIUM; - } - - return { - LOW: lowThreshold, - MEDIUM: mediumThreshold, - } as const; + // Parse with fallbacks and proper typing + const parsePct = (val: string | undefined, fallback: number): number => { + const n = val != null ? Number.parseInt(val, 10) : Number.NaN; + return Number.isFinite(n) ? n : fallback; + }; + let low = parsePct(process.env[CONTEXT_LOW_THRESHOLD_ENV], DEFAULT_CONTEXT_USAGE_THRESHOLDS.LOW); + let medium = parsePct(process.env[CONTEXT_MEDIUM_THRESHOLD_ENV], DEFAULT_CONTEXT_USAGE_THRESHOLDS.MEDIUM); + // Clamp to [0, 100] + low = Math.max(0, Math.min(100, low)); + medium = Math.max(0, Math.min(100, medium)); + // Ensure LOW < MEDIUM (keep at least 1% gap when possible) + if (medium <= low) { + medium = low === 100 ? 100 : Math.min(100, low + 1); + } + return { LOW: low, MEDIUM: medium } as const; }
1400-1464: Fix context calculation, add environment override, and resolve unsafe type accessMultiple critical issues need to be addressed:
- Context calculation should exclude output tokens (as specified in PR objectives)
- Support CONTEXT_LIMIT_ENV override
- Fix all unsafe type access from static analysis
Apply this diff:
export async function calculateContextTokens(transcriptPath: string): Promise<{ - inputTokens: number; + inputTokens: number; percentage: number; contextLimit: number; } | null> { + // Resolve context limit (env override when valid) + const envLimitRaw = process.env[CONTEXT_LIMIT_ENV]; + const envLimit = envLimitRaw != null ? Number.parseInt(envLimitRaw, 10) : Number.NaN; + const contextLimit = Number.isFinite(envLimit) && envLimit > 0 ? envLimit : CONTEXT_LIMIT; + let content: string; try { content = await readFile(transcriptPath, 'utf-8'); } catch (error: unknown) { logger.debug(`Failed to read transcript file: ${String(error)}`); return null; } const lines = content.split('\n').reverse(); // Iterate from last line to first line for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine === '') { continue; } try { const parsed = JSON.parse(trimmedLine) as unknown; const result = transcriptMessageSchema.safeParse(parsed); if (!result.success) { continue; // Skip malformed JSON lines } const obj = result.data; // Check if this line contains the required token usage fields if (obj.type === 'assistant' && obj.message != null && obj.message.usage != null && obj.message.usage.input_tokens != null) { const usage = obj.message.usage; + // Context window usage: input + cache tokens (exclude output tokens) const inputTokens = usage.input_tokens! + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0); - const percentage = Math.min(100, Math.max(0, Math.round((inputTokens / CONTEXT_LIMIT) * 100))); + const percentage = Math.min(100, Math.max(0, Math.round((inputTokens / contextLimit) * 100))); return { inputTokens, percentage, - contextLimit: CONTEXT_LIMIT, + contextLimit, }; } } catch { continue; // Skip malformed JSON lines } } // No valid usage information found logger.debug('No usage information found in transcript'); return null; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
docs/guide/statusline.md(3 hunks)src/data-loader.ts(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/guide/statusline.md
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.{js,jsx,ts,tsx}: Lint code using ESLint MCP server (available via Claude Code tools)
Format code with ESLint (writes changes) usingbun run format
No console.log allowed except where explicitly disabled with eslint-disable
Do not use console.log. Use logger.ts instead.
Files:
src/data-loader.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (CLAUDE.md)
Type check with TypeScript using
bun typecheck
Files:
src/data-loader.ts
**/*.ts
📄 CodeRabbit Inference Engine (CLAUDE.md)
**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use.tsextensions for local file imports (e.g.,import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
UseResult.try()for wrapping operations that may throw (JSON parsing, etc.)
UseResult.isFailure()for checking errors (more readable than!Result.isSuccess())
Use early return pattern (if (Result.isFailure(result)) continue;) instead of ternary operators
For async operations: create wrapper function withResult.try()then call it
Keep traditional try-catch only for: file I/O with complex error handling, legacy code that's hard to refactor
Always useResult.isFailure()andResult.isSuccess()type guards for better code clarity
Variables: start with lowercase (camelCase) - e.g.,usageDataSchema,modelBreakdownSchema
Types: start with uppercase (PascalCase) - e.g.,UsageData,ModelBreakdown
Constants: can use UPPER_SNAKE_CASE - e.g.,DEFAULT_CLAUDE_CODE_PATH
Only export constants, functions, and types that are actually used by other modules
Internal/private constants that are only used within the same file should NOT be exported
Always check if a constant is used elsewhere before making itexport constvs justconst
All test files must use current Claude 4 models, not outdated Claude 3 models
Test coverage should include both Sonnet and Opus models for comprehensive validation
Model names in tests must exactly match LiteLLM's pricing database entries
When adding new model tests, verify the model exists in LiteLLM before implementation
Tests depend on real pricing data from LiteLLM - failures may indicate model availability issues
Dynamic imports usingawait import()should only be used within test blocks to avoid tree-shaking issues
Mock data is created usingfs-fixturewithcreateFixture()for Claude data directory simulation
In-source testing pattern: Tests are written...
Files:
src/data-loader.ts
🧠 Learnings (1)
📚 Learning: 2025-08-10T20:39:26.465Z
Learnt from: CR
PR: ryoppippi/ccusage#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-10T20:39:26.465Z
Learning: Applies to **/*.ts : Constants: can use UPPER_SNAKE_CASE - e.g., `DEFAULT_CLAUDE_CODE_PATH`
Applied to files:
src/data-loader.ts
🧬 Code Graph Analysis (1)
src/data-loader.ts (2)
src/_consts.ts (4)
CONTEXT_LOW_THRESHOLD_ENV(161-161)CONTEXT_MEDIUM_THRESHOLD_ENV(166-166)DEFAULT_CONTEXT_USAGE_THRESHOLDS(152-156)CONTEXT_LIMIT(147-147)src/logger.ts (1)
logger(19-19)
🪛 ESLint
src/data-loader.ts
[error] 146-146: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 146-146: Unsafe member access .env on an error typed value.
(ts/no-unsafe-member-access)
[error] 147-147: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 147-147: Unsafe member access .env on an error typed value.
(ts/no-unsafe-member-access)
[error] 154-154: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 161-161: Unsafe argument of type error typed assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 228-233: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 228-228: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 228-228: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 229-229: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 229-229: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 229-229: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 229-229: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 229-229: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 230-230: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 230-230: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 230-230: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 230-230: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 230-230: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 231-231: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 231-231: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 231-231: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 231-231: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 231-231: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 232-232: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 232-232: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 232-232: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 232-232: Unsafe member access .number on an error typed value.
(ts/no-unsafe-member-access)
[error] 232-232: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 238-243: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 238-238: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 238-238: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 239-239: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 239-239: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 239-239: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 239-239: Unsafe member access .string on an error typed value.
(ts/no-unsafe-member-access)
[error] 239-239: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 240-242: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 240-242: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 240-240: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 240-240: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 241-241: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 241-241: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 241-241: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 242-242: Unsafe member access .optional on an error typed value.
(ts/no-unsafe-member-access)
[error] 1413-1413: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1413-1413: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1416-1416: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1416-1416: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 1430-1430: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1430-1430: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1430-1430: Unsafe member access .safeParse on an error typed value.
(ts/no-unsafe-member-access)
[error] 1431-1431: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 1431-1431: Unsafe member access .success on an error typed value.
(ts/no-unsafe-member-access)
[error] 1434-1434: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1434-1434: Unsafe member access .data on an error typed value.
(ts/no-unsafe-member-access)
[error] 1437-1437: Unsafe member access .type on an error typed value.
(ts/no-unsafe-member-access)
[error] 1438-1438: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1439-1439: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1440-1440: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1441-1441: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1441-1441: Unsafe member access .message on an error typed value.
(ts/no-unsafe-member-access)
[error] 1442-1445: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1443-1443: Unsafe member access .input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1444-1444: Unsafe member access .cache_creation_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1445-1445: Unsafe member access .cache_read_input_tokens on an error typed value.
(ts/no-unsafe-member-access)
[error] 1450-1450: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 1462-1462: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1462-1462: Unsafe member access .debug on an error typed value.
(ts/no-unsafe-member-access)
[error] 4709-4709: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4711-4717: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4711-4711: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4718-4718: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4718-4718: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4718-4718: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
[error] 4726-4730: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4726-4726: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4731-4731: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4731-4731: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4731-4731: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
[error] 4738-4742: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4738-4738: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4743-4743: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4743-4743: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4743-4743: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
🔇 Additional comments (1)
src/data-loader.ts (1)
225-243: Fix unsafe Zod schema constructionThe static analysis correctly identifies unsafe access to the
zmodule. Import Zod properly and ensure type safety.Apply this diff:
/** * Zod schema for transcript usage data from Claude messages */ -export const transcriptUsageSchema = z.object({ - input_tokens: z.number().optional(), - cache_creation_input_tokens: z.number().optional(), - cache_read_input_tokens: z.number().optional(), - output_tokens: z.number().optional(), -}); +export const transcriptUsageSchema = z.object({ + input_tokens: z.number().optional(), + cache_creation_input_tokens: z.number().optional(), + cache_read_input_tokens: z.number().optional(), + output_tokens: z.number().optional(), +}); /** * Zod schema for transcript message data */ -export const transcriptMessageSchema = z.object({ - type: z.string().optional(), - message: z.object({ - usage: transcriptUsageSchema.optional(), - }).optional(), -}); +export const transcriptMessageSchema = z.object({ + type: z.string().optional(), + message: z.object({ + usage: transcriptUsageSchema.optional(), + }).optional(), +});Likely an incorrect or invalid review comment.
| // Test for calculateContextTokens | ||
| describe('calculateContextTokens', async () => { | ||
| it('returns null when transcript cannot be read', async () => { | ||
| const result = await calculateContextTokens('/nonexistent/path.jsonl'); | ||
| expect(result).toBeNull(); | ||
| }); | ||
| const { createFixture } = await import('fs-fixture'); | ||
| it('parses latest assistant line and excludes output tokens', async () => { | ||
| await using fixture = await createFixture({ | ||
| 'transcript.jsonl': [ | ||
| JSON.stringify({ type: 'user', message: {} }), | ||
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000, output_tokens: 999 } } }), | ||
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 2000, cache_creation_input_tokens: 100, cache_read_input_tokens: 50 } } }), | ||
| ].join('\n'), | ||
| }); | ||
| const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | ||
| expect(res).not.toBeNull(); | ||
| // Should pick the last assistant line and exclude output tokens | ||
| expect(res?.inputTokens).toBe(2000 + 100 + 50); | ||
| expect(res?.percentage).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it('handles missing cache fields gracefully', async () => { | ||
| await using fixture = await createFixture({ | ||
| 'transcript.jsonl': [ | ||
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000 } } }), | ||
| ].join('\n'), | ||
| }); | ||
| const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | ||
| expect(res).not.toBeNull(); | ||
| expect(res?.inputTokens).toBe(1000); | ||
| expect(res?.percentage).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it('clamps percentage to 0-100 range', async () => { | ||
| await using fixture = await createFixture({ | ||
| 'transcript.jsonl': [ | ||
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 300_000 } } }), | ||
| ].join('\n'), | ||
| }); | ||
| const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | ||
| expect(res).not.toBeNull(); | ||
| expect(res?.percentage).toBe(100); // Should be clamped to 100 | ||
| }); | ||
| }); | ||
|
|
||
| describe('getContextUsageThresholds', () => { | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
|
|
||
| it('returns default values when no environment variables are set', () => { | ||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(50); | ||
| expect(thresholds.MEDIUM).toBe(80); | ||
| }); | ||
|
|
||
| it('parses valid environment variables correctly', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '30'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '70'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(30); | ||
| expect(thresholds.MEDIUM).toBe(70); | ||
| }); | ||
|
|
||
| it('clamps values to 0-100 range', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '-10'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '150'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(0); | ||
| expect(thresholds.MEDIUM).toBe(100); | ||
| }); | ||
|
|
||
| it('handles non-numeric values by falling back to defaults', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', 'invalid'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', 'not-a-number'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(50); | ||
| expect(thresholds.MEDIUM).toBe(80); | ||
| }); | ||
|
|
||
| it('handles empty string values by falling back to defaults', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', ''); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', ''); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(50); | ||
| expect(thresholds.MEDIUM).toBe(80); | ||
| }); | ||
|
|
||
| it('handles partial invalid values correctly', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '25'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', 'invalid'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(25); | ||
| expect(thresholds.MEDIUM).toBe(80); // Falls back to default | ||
| }); | ||
|
|
||
| it('enforces ordering constraint: resets to defaults when LOW >= MEDIUM', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '85'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '60'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| // Should reset both to defaults due to ordering violation | ||
| expect(thresholds.LOW).toBe(50); | ||
| expect(thresholds.MEDIUM).toBe(80); | ||
| }); | ||
|
|
||
| it('enforces ordering constraint: resets to defaults when LOW equals MEDIUM', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '70'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '70'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| // Should reset both to defaults due to ordering violation | ||
| expect(thresholds.LOW).toBe(50); | ||
| expect(thresholds.MEDIUM).toBe(80); | ||
| }); | ||
|
|
||
| it('handles edge case: LOW=0, MEDIUM=100', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '0'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '100'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(0); | ||
| expect(thresholds.MEDIUM).toBe(100); | ||
| }); | ||
|
|
||
| it('handles decimal values by truncating to integer', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '25.7'); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '75.9'); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(25); // parseInt truncates | ||
| expect(thresholds.MEDIUM).toBe(75); // parseInt truncates | ||
| }); | ||
|
|
||
| it('handles values with extra whitespace', () => { | ||
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', ' 30 '); | ||
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', ' 70 '); | ||
|
|
||
| const thresholds = getContextUsageThresholds(); | ||
| expect(thresholds.LOW).toBe(30); | ||
| expect(thresholds.MEDIUM).toBe(70); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix unsafe assignments in test code
The static analysis identifies multiple unsafe assignments in the test blocks. While test code is generally more permissive, following TypeScript best practices is important.
Apply proper typing to the test imports and fixture usage:
- const { createFixture } = await import('fs-fixture');
+ const { createFixture } = await import('fs-fixture') as { createFixture: typeof import('fs-fixture').createFixture };For the fixture.getPath() calls, ensure proper typing by using type assertion where needed:
- const res = await calculateContextTokens(fixture.getPath('transcript.jsonl'));
+ const res = await calculateContextTokens(fixture.getPath('transcript.jsonl') as string);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Test for calculateContextTokens | |
| describe('calculateContextTokens', async () => { | |
| it('returns null when transcript cannot be read', async () => { | |
| const result = await calculateContextTokens('/nonexistent/path.jsonl'); | |
| expect(result).toBeNull(); | |
| }); | |
| const { createFixture } = await import('fs-fixture'); | |
| it('parses latest assistant line and excludes output tokens', async () => { | |
| await using fixture = await createFixture({ | |
| 'transcript.jsonl': [ | |
| JSON.stringify({ type: 'user', message: {} }), | |
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000, output_tokens: 999 } } }), | |
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 2000, cache_creation_input_tokens: 100, cache_read_input_tokens: 50 } } }), | |
| ].join('\n'), | |
| }); | |
| const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | |
| expect(res).not.toBeNull(); | |
| // Should pick the last assistant line and exclude output tokens | |
| expect(res?.inputTokens).toBe(2000 + 100 + 50); | |
| expect(res?.percentage).toBeGreaterThan(0); | |
| }); | |
| it('handles missing cache fields gracefully', async () => { | |
| await using fixture = await createFixture({ | |
| 'transcript.jsonl': [ | |
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000 } } }), | |
| ].join('\n'), | |
| }); | |
| const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | |
| expect(res).not.toBeNull(); | |
| expect(res?.inputTokens).toBe(1000); | |
| expect(res?.percentage).toBeGreaterThan(0); | |
| }); | |
| it('clamps percentage to 0-100 range', async () => { | |
| await using fixture = await createFixture({ | |
| 'transcript.jsonl': [ | |
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 300_000 } } }), | |
| ].join('\n'), | |
| }); | |
| const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | |
| expect(res).not.toBeNull(); | |
| expect(res?.percentage).toBe(100); // Should be clamped to 100 | |
| }); | |
| }); | |
| describe('getContextUsageThresholds', () => { | |
| afterEach(() => { | |
| vi.unstubAllEnvs(); | |
| }); | |
| it('returns default values when no environment variables are set', () => { | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(50); | |
| expect(thresholds.MEDIUM).toBe(80); | |
| }); | |
| it('parses valid environment variables correctly', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '30'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '70'); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(30); | |
| expect(thresholds.MEDIUM).toBe(70); | |
| }); | |
| it('clamps values to 0-100 range', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '-10'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '150'); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(0); | |
| expect(thresholds.MEDIUM).toBe(100); | |
| }); | |
| it('handles non-numeric values by falling back to defaults', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', 'invalid'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', 'not-a-number'); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(50); | |
| expect(thresholds.MEDIUM).toBe(80); | |
| }); | |
| it('handles empty string values by falling back to defaults', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', ''); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', ''); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(50); | |
| expect(thresholds.MEDIUM).toBe(80); | |
| }); | |
| it('handles partial invalid values correctly', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '25'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', 'invalid'); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(25); | |
| expect(thresholds.MEDIUM).toBe(80); // Falls back to default | |
| }); | |
| it('enforces ordering constraint: resets to defaults when LOW >= MEDIUM', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '85'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '60'); | |
| const thresholds = getContextUsageThresholds(); | |
| // Should reset both to defaults due to ordering violation | |
| expect(thresholds.LOW).toBe(50); | |
| expect(thresholds.MEDIUM).toBe(80); | |
| }); | |
| it('enforces ordering constraint: resets to defaults when LOW equals MEDIUM', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '70'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '70'); | |
| const thresholds = getContextUsageThresholds(); | |
| // Should reset both to defaults due to ordering violation | |
| expect(thresholds.LOW).toBe(50); | |
| expect(thresholds.MEDIUM).toBe(80); | |
| }); | |
| it('handles edge case: LOW=0, MEDIUM=100', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '0'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '100'); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(0); | |
| expect(thresholds.MEDIUM).toBe(100); | |
| }); | |
| it('handles decimal values by truncating to integer', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', '25.7'); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', '75.9'); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(25); // parseInt truncates | |
| expect(thresholds.MEDIUM).toBe(75); // parseInt truncates | |
| }); | |
| it('handles values with extra whitespace', () => { | |
| vi.stubEnv('CCUSAGE_CONTEXT_LOW_THRESHOLD', ' 30 '); | |
| vi.stubEnv('CCUSAGE_CONTEXT_MEDIUM_THRESHOLD', ' 70 '); | |
| const thresholds = getContextUsageThresholds(); | |
| expect(thresholds.LOW).toBe(30); | |
| expect(thresholds.MEDIUM).toBe(70); | |
| }); | |
| }); | |
| // Test for calculateContextTokens | |
| describe('calculateContextTokens', async () => { | |
| it('returns null when transcript cannot be read', async () => { | |
| const result = await calculateContextTokens('/nonexistent/path.jsonl'); | |
| expect(result).toBeNull(); | |
| }); | |
| - const { createFixture } = await import('fs-fixture'); | |
| + const { createFixture } = (await import('fs-fixture')) as { createFixture: typeof import('fs-fixture').createFixture }; | |
| it('parses latest assistant line and excludes output tokens', async () => { | |
| await using fixture = await createFixture({ | |
| 'transcript.jsonl': [ | |
| JSON.stringify({ type: 'user', message: {} }), | |
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 1000, output_tokens: 999 } } }), | |
| JSON.stringify({ type: 'assistant', message: { usage: { input_tokens: 2000, cache_creation_input_tokens: 100, cache_read_input_tokens: 50 } } }), | |
| ].join('\n'), | |
| }); | |
| - const res = await calculateContextTokens(fixture.getPath('transcript.jsonl')); | |
| + const res = await calculateContextTokens(fixture.getPath('transcript.jsonl') as string); | |
| expect(res).not.toBeNull(); | |
| // Should pick the last assistant line and exclude output tokens | |
| expect(res?.inputTokens).toBe(2000 + 100 + 50); | |
| expect(res?.percentage).toBeGreaterThan(0); | |
| }); | |
| // …the rest of your tests remain unchanged |
🧰 Tools
🪛 ESLint
[error] 4709-4709: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4711-4717: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4711-4711: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4718-4718: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4718-4718: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4718-4718: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
[error] 4726-4730: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4726-4726: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4731-4731: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4731-4731: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4731-4731: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
[error] 4738-4742: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 4738-4738: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4743-4743: Unsafe argument of type any assigned to a parameter of type string.
(ts/no-unsafe-argument)
[error] 4743-4743: Unsafe call of a(n) any typed value.
(ts/no-unsafe-call)
[error] 4743-4743: Unsafe member access .getPath on an any value.
(ts/no-unsafe-member-access)
🤖 Prompt for AI Agents
src/data-loader.ts lines 4703-4851: tests use untyped imports and a "using
fixture = await createFixture(...)" assignment that triggers unsafe-assignment
warnings; fix by importing or declaring proper types for createFixture (e.g.
import the CreateFixture type from 'fs-fixture' or declare its signature) and
annotate the fixture variable with the appropriate awaited/return type (e.g.
Awaited<ReturnType<typeof createFixture>> or the CreateFixtureInstance type),
and where necessary assert the return of fixture.getPath(...) as string to
satisfy the type checker; update the test file imports and variable declarations
accordingly so all fixture values have explicit types instead of implicit
any/unknown.
- Create test-transcript.jsonl with correct message structure for context token calculation - Update statusline-test.json to reference the new transcript file with relative path - Ensures statusline can properly display context usage (🧠) in tests
ryoppippi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
* feat: add context token display to statusline command Add context window usage tracking to the statusline hook output by: - Reading transcript file provided by Claude Code statusline hook - Extracting current input token count from transcript usage data - Calculating percentage of 200K context window used - Displaying tokens with thousand separators and color-coded percentage - Green: <50% (normal usage) - Yellow: 50-80% (moderate usage) - Red: >80% (high usage, approaching limit) Format: 🧠 25,000 (13%) appears at end of statusline Addresses user request for context window monitoring to help optimize prompt length and prevent unexpected context management issues. Test files included for validation of different usage levels. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: address PR #480 reviewer comments for statusline context display - Move context token calculation logic to data-loader.ts for better organization - Add CONTEXT_LIMIT and CONTEXT_USAGE_THRESHOLDS constants to _consts.ts - Rename totalInputTokens to maxInputTokens for clarity - Refactor color-coding logic to use constants and ternary operators - Improve JSONL parsing based on Python reference implementation - Use Result.try pattern for better error handling - Include all token types (input + cache_creation + cache_read + output) for accuracy 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: correct context calculation to exclude output tokens and improve type safety - Fix context calculation to exclude output_tokens from percentage calculation - Rename maxInputTokens to inputTokens for accuracy - Make cache fields optional using nullish coalescing - Clamp percentage to 0-100 range - Improve JSON parsing with proper try-catch and type safety - Add numeric separators to CONTEXT_LIMIT constant for readability - Simplify color selection logic in statusline - Add comprehensive in-source tests for calculateContextTokens 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: apply PR reviewer feedback for context display - Fix error logging to avoid passing unknown to logger.debug - Move fs-fixture import inside test block to follow dynamic import guidelines - Apply code style improvements for better maintainability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: add configurable context usage thresholds for statusline - Move calculateContextTokens test to end of file - Add Zod schemas for transcript usage validation (transcriptUsageSchema, transcriptMessageSchema) - Make CONTEXT_USAGE_THRESHOLDS configurable via environment variables: - CCUSAGE_CONTEXT_LOW_THRESHOLD (default: 50) - CCUSAGE_CONTEXT_MEDIUM_THRESHOLD (default: 80) - Update statusline documentation with new environment variables and context usage display 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: harden context usage threshold parsing with validation and safety features - Add comprehensive input validation for environment variables - Clamp values to 0-100 range preventing out-of-bounds percentages - Enforce LOW < MEDIUM ordering constraint with fallback to defaults - Handle non-numeric values gracefully by falling back to safe defaults - Prevent NaN propagation that caused incorrect UI coloring - Add 11 comprehensive tests covering edge cases and invalid configurations - Update documentation with validation features and invalid config examples This ensures the statusline context usage display will always show meaningful color coding regardless of environment variable configuration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * test: add proper transcript file for statusline context display testing - Create test-transcript.jsonl with correct message structure for context token calculation - Update statusline-test.json to reference the new transcript file with relative path - Ensures statusline can properly display context usage (🧠) in tests --------- Co-authored-by: Claude <[email protected]> Co-authored-by: ryoppippi <[email protected]>

Add context window usage tracking to the statusline hook output by:
Format: 🧠 25,000 (13%) appears at end of statusline
Addresses user request for context window monitoring to help optimize prompt length and prevent unexpected context management issues.
Fixes #454
🤖 Generated with Claude Code
Summary by CodeRabbit