Skip to content

Conversation

@ItsLucas
Copy link
Contributor

@ItsLucas ItsLucas commented Aug 11, 2025

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.

Fixes #454

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Status line now shows Context Usage (🧠 token count + colored percentage); thresholds configurable via environment.
  • Robustness
    • If context data can’t be determined or errors occur, status line generation continues unaffected.
  • Documentation
    • Guide updated with Context Usage explanation, configuration examples, validation rules, and color mappings.
  • Tests
    • Added tests for transcript parsing, context calculation, threshold parsing, clamping, and edge cases.

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]>
@coderabbitai
Copy link

coderabbitai bot commented Aug 11, 2025

Note

Other AI code review bot(s) detected

CodeRabbit 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 failed

The pull request is closed.

Walkthrough

Adds 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

Cohort / File(s) Summary
Constants for context usage
src/_consts.ts
Adds CONTEXT_LIMIT = 200_000, DEFAULT_CONTEXT_USAGE_THRESHOLDS = { LOW: 50, MEDIUM: 80 } as const, and env-name constants CCUSAGE_CONTEXT_LOW_THRESHOLD / CCUSAGE_CONTEXT_MEDIUM_THRESHOLD.
Types for transcript parsing
src/_types.ts
Adds TranscriptUsage and TranscriptMessage types to model transcript JSONL usage fields and assistant message structure.
Data loading: context calculation & schemas
src/data-loader.ts
Adds Zod schemas transcriptUsageSchema and transcriptMessageSchema, getContextUsageThresholds() to read and validate env thresholds, and calculateContextTokens(transcriptPath) which reads transcript JSONL (tail→head), finds latest assistant usage, sums input + cache tokens, computes/clamps percentage vs CONTEXT_LIMIT, and returns `{ inputTokens, percentage, contextLimit }
Statusline integration
src/commands/statusline.ts
Imports calculateContextTokens and getContextUsageThresholds, calls calculateContextTokens(...) inside try/catch, formats token count with thousands separators and color-coded percentage, and appends a context segment (🧠 ...) to the statusline output.
Tests & fixtures
test/test-transcript.jsonl, test/statusline-test.json
Adds assistant usage lines to test-transcript.jsonl and updates statusline test fixture to reference the repo-relative transcript path.
Docs: statusline guide
docs/guide/statusline.md
Documents the new Context Usage segment, env vars CCUSAGE_CONTEXT_LOW_THRESHOLD / CCUSAGE_CONTEXT_MEDIUM_THRESHOLD, defaults, clamping/validation rules, and examples.

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%))
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Display current context token count in ccusage statusline (#454)

Out-of-scope changes

Code Change Explanation
Duplicate calculateContextTokens implementation (src/data-loader.ts) Diff shows two inserted implementations of calculateContextTokens in src/data-loader.ts (merge artifact). This duplication is not part of the feature objective and should be deduplicated.

Possibly related PRs

Poem

I nibble bytes beneath the moon, 🐇
Counting tokens in a quiet tune.
🧠 Numbers glow, green → yellow → red,
Lines parsed, percentages said.
Hop, hop—statusline sings: context fed.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bbe97a and 3cdaac9.

📒 Files selected for processing (3)
  • src/data-loader.ts (5 hunks)
  • test/statusline-test.json (1 hunks)
  • test/test-transcript.jsonl (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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

  1. 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.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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.

@ryoppippi
Copy link
Owner

@ItsLucas good but can you move logic to data-loader.ts and import it from statusline.ts

@ItsLucas
Copy link
Contributor Author

image Seems the line is a little longer when the "approach limit" warning appears

…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]>
Copy link

@coderabbitai coderabbitai bot left a 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 nit

Constants 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 handling

The 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 calculateContextTokens

Add 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 selection

Minor 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

📥 Commits

Reviewing files that changed from the base of the PR and between bb47158 and fb96122.

📒 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) using bun 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.ts
  • src/_types.ts
  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Type check with TypeScript using bun typecheck

Files:

  • src/_consts.ts
  • src/_types.ts
  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.ts

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.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 with Result.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 use Result.isFailure() and Result.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 it export const vs just const
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 using await import() should only be used within test blocks to avoid tree-shaking issues
Mock data is created using fs-fixture with createFixture() for Claude data directory simulation
In-source testing pattern: Tests are written...

Files:

  • src/_consts.ts
  • src/_types.ts
  • src/data-loader.ts
  • src/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
  • src/_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 clear

LGTM. The added context segment is appended at the end and uses the thresholds provided. No functional issues here.

Copy link

@coderabbitai coderabbitai bot left a 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 readability

200000 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

📥 Commits

Reviewing files that changed from the base of the PR and between bb47158 and fb96122.

📒 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) using bun 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.ts
  • src/_types.ts
  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Type check with TypeScript using bun typecheck

Files:

  • src/_consts.ts
  • src/_types.ts
  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.ts

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.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 with Result.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 use Result.isFailure() and Result.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 it export const vs just const
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 using await import() should only be used within test blocks to avoid tree-shaking issues
Mock data is created using fs-fixture with createFixture() for Claude data directory simulation
In-source testing pattern: Tests are written...

Files:

  • src/_consts.ts
  • src/_types.ts
  • src/data-loader.ts
  • src/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
  • src/_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 usage

Optional fields match transcript variability and keep parsing resilient. No blockers.

src/_consts.ts (1)

152-156: Threshold constants are clear and documented

Color threshold constants and comments are clear and match the PR objectives.

src/commands/statusline.ts (2)

5-5: Good: thresholds are imported from shared constants

Centralizing thresholds in _consts.ts improves consistency and maintainability.


11-11: Good: data-loader owns context parsing logic

Importing calculateContextTokens keeps statusline lean and follows the requested organization.

src/data-loader.ts (1)

1345-1406: Add in-source tests for calculateContextTokens

Recommend 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]>
Copy link

@coderabbitai coderabbitai bot left a 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 assertion

Logic 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 block

Coverage 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 readability

Use 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb96122 and 2b05c01.

📒 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) using bun 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
  • src/commands/statusline.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Type check with TypeScript using bun typecheck

Files:

  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.ts

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.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 with Result.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 use Result.isFailure() and Result.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 it export const vs just const
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 using await import() should only be used within test blocks to avoid tree-shaking issues
Mock data is created using fs-fixture with createFixture() for Claude data directory simulation
In-source testing pattern: Tests are written...

Files:

  • src/data-loader.ts
  • src/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 minimal

Importing TranscriptMessage as a type keeps runtime lean. LGTM.


36-36: Good: centralizes context limit via constant

Using CONTEXT_LIMIT avoids magic numbers and respects config conventions.

src/commands/statusline.ts (3)

5-5: Good: uses shared thresholds for color-coding

Pulling CONTEXT_USAGE_THRESHOLDS from consts avoids magic numbers and centralizes behavior.


11-11: Correct: consumes loader API for transcript-based context data

Importing calculateContextTokens from data-loader.ts keeps statusline thin and aligned with the requested organization.


188-191: Statusline composition looks good

Format 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]>
Copy link
Owner

@ryoppippi ryoppippi left a 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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Aug 12, 2025

Open in StackBlitz

npm i https://pkg.pr.new/ryoppippi/ccusage@480

commit: 3cdaac9

Copy link
Owner

@ryoppippi ryoppippi left a 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]>
Copy link

@coderabbitai coderabbitai bot left a 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 models

Hardcoding 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 percentage

Using 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 internal

transcriptUsageSchema 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 conventions

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2b05c01 and 509f361.

📒 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) using bun 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.ts
  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Type check with TypeScript using bun typecheck

Files:

  • src/_consts.ts
  • src/data-loader.ts
  • src/commands/statusline.ts
**/*.ts

📄 CodeRabbit Inference Engine (CLAUDE.md)

**/*.ts: File paths always use Node.js path utilities for cross-platform compatibility
Use .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.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 with Result.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 use Result.isFailure() and Result.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 it export const vs just const
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 using await import() should only be used within test blocks to avoid tree-shaking issues
Mock data is created using fs-fixture with createFixture() for Claude data directory simulation
In-source testing pattern: Tests are written...

Files:

  • src/_consts.ts
  • src/data-loader.ts
  • src/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.ts
  • src/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 request

Importing 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 line

Matches 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]>
Copy link

@coderabbitai coderabbitai bot left a 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 support

Missing 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 validation

The static analysis correctly identifies unsafe access to process.env without 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 access

Multiple critical issues need to be addressed:

  1. Context calculation should exclude output tokens (as specified in PR objectives)
  2. Support CONTEXT_LIMIT_ENV override
  3. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 509f361 and 2bbe97a.

📒 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) using bun 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 .ts extensions for local file imports (e.g., import { foo } from './utils.ts')
Prefer @praha/byethrow Result type over traditional try-catch for functional error handling
Use Result.try() for wrapping operations that may throw (JSON parsing, etc.)
Use Result.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 with Result.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 use Result.isFailure() and Result.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 it export const vs just const
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 using await import() should only be used within test blocks to avoid tree-shaking issues
Mock data is created using fs-fixture with createFixture() 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 construction

The static analysis correctly identifies unsafe access to the z module. 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.

Comment on lines +4703 to +4851
// 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);
});
});
Copy link

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.

Suggested change
// 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.

@ItsLucas ItsLucas requested a review from ryoppippi August 13, 2025 03:33
- 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
Copy link
Owner

@ryoppippi ryoppippi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ryoppippi ryoppippi merged commit d873374 into ryoppippi:main Aug 13, 2025
8 checks passed
ryoppippi added a commit that referenced this pull request Aug 13, 2025
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Display current context token count in ccusage statusline

2 participants