Skip to content

feat: Claude usage widget in left sidebar#749

Open
adamcongdon wants to merge 2 commits intoRunMaestro:mainfrom
adamcongdon:feature/claude-usage-widget
Open

feat: Claude usage widget in left sidebar#749
adamcongdon wants to merge 2 commits intoRunMaestro:mainfrom
adamcongdon:feature/claude-usage-widget

Conversation

@adamcongdon
Copy link
Copy Markdown

@adamcongdon adamcongdon commented Apr 7, 2026

Summary

Adds a compact Claude usage widget at the bottom of the left sidebar (above the New Agent button), showing current 5-hour session and 7-day usage limits as color-coded progress bars.

How it works

  • Data source: Reads from ~/.claude/MEMORY/STATE/usage-cache.json (the cache file kept fresh by Claude Code's session hooks), with a fallback to calling the Anthropic OAuth usage API directly using credentials from ~/.claude/.credentials.json
  • Polling: Every 5 minutes, matching the cache TTL — zero additional API overhead for users whose Claude session hooks are active
  • Visibility: Only shown when the left sidebar is expanded; silently hidden if usage data isn't available (non-Claude users, missing credentials, etc.)

Visual behavior

  • Two labeled progress bars: 5-Hour Session and 7-Day Limit
  • Color coding: green (<50%) → amber (50–80%) → red (>80%)
  • Shows countdown to reset (e.g., "resets in 4h 22m")
  • Small ↻ refresh button for on-demand update

Scope

Currently Claude-specific (reads Anthropic OAuth data). Other agent providers could be added later if equivalent usage APIs become available.

Files changed

File Change
src/main/ipc/handlers/system.ts New usage:getClaudeUsage IPC handler
src/main/preload/system.ts createUsageApi() factory + TypeScript interfaces
src/main/preload/index.ts Wire usage into contextBridge
src/renderer/global.d.ts window.maestro.usage type declaration
src/renderer/components/SessionList/ClaudeUsageWidget.tsx New widget component
src/renderer/components/SessionList/SessionList.tsx Mount widget above SidebarActions

Summary by CodeRabbit

New Features

  • Claude Usage Widget: Added a usage monitoring widget to the left sidebar (expanded mode) displaying real-time Claude API metrics across multiple rate limit windows (5-hour, 7-day, and 7-day Sonnet) with color-coded utilization bars and reset timers.
  • Auto-refreshes every 5 minutes with a manual refresh button.

Shows 5-hour session and 7-day usage limits as color-coded progress
bars at the bottom of the left sidebar (above the New Agent button),
visible only when the sidebar is expanded.

Data source: reads from ~/.claude/MEMORY/STATE/usage-cache.json
(kept fresh by PAI hooks) with fallback to Anthropic OAuth API via
~/.claude/.credentials.json. Polls every 5 minutes matching the
cache TTL — zero overhead when PAI is active.

Progress bars are color-coded green/amber/red and show time until
each period resets.

Only applies to Claude Code agents (uses Anthropic OAuth API).
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

This PR introduces a Claude API usage monitoring feature, adding an IPC handler that retrieves usage metrics from either a local cache or the Anthropic OAuth API, exposing the data through Electron's preload layer, and rendering a visual usage widget in the application sidebar.

Changes

Cohort / File(s) Summary
IPC Handler & Preload Infrastructure
src/main/ipc/handlers/system.ts, src/main/preload/system.ts, src/main/preload/index.ts
New IPC handler usage:getClaudeUsage retrieves usage data from cache or Anthropic OAuth endpoint. Preload layer exports createUsageApi() factory and TypeScript interfaces (ClaudeUsagePeriod, ClaudeUsageData, ClaudeUsageResult) to model the response structure.
Type Declarations
src/renderer/global.d.ts
Extended global MaestroAPI interface with usage namespace exposing getClaudeUsage() method returning usage metrics for multiple rate-limit windows.
UI Component & Integration
src/renderer/components/SessionList/ClaudeUsageWidget.tsx, src/renderer/components/SessionList/SessionList.tsx
New React component renders utilization bars for five_hour and seven_day usage windows, polling data every 5 minutes with manual refresh capability. Component is conditionally rendered in expanded sidebar mode.

Sequence Diagram

sequenceDiagram
    participant Renderer as Renderer Process
    participant Preload as Preload Layer
    participant IPC as IPC Handler
    participant Cache as Local Cache<br/>~/.claude/MEMORY/
    participant Creds as Credentials<br/>~/.claude/
    participant API as Anthropic API
    
    Renderer->>Preload: window.maestro.usage.getClaudeUsage()
    Preload->>IPC: ipcRenderer.invoke('usage:getClaudeUsage')
    IPC->>Cache: Read usage-cache.json
    alt Cache Hit
        Cache-->>IPC: Cache data
        IPC-->>Preload: {success: true, data}
    else Cache Miss
        IPC->>Creds: Read .credentials.json
        alt Token Available
            Creds-->>IPC: OAuth token
            IPC->>API: GET /api/oauth/usage<br/>(with token)
            API-->>IPC: Usage metrics
            IPC-->>Preload: {success: true, data}
        else No Token
            IPC-->>Preload: {success: false, error}
        end
    end
    Preload-->>Renderer: Promise<ClaudeUsageResult>
    Renderer->>Renderer: Update state & render widget
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Bounces with joy
A Claude usage tracker hops and bounds,
Five hours, seven days—through API rounds!
From cache to fetch, the data flows,
Through IPC pipes where preload shows,
With colors bright the widget gleams,
Usage tracked in your app's dreams! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and directly summarizes the main change: adding a Claude usage widget to the left sidebar, which matches the core objective and all file modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 7, 2026

Greptile Summary

Adds a compact Claude usage widget to the left sidebar showing 5-hour and 7-day usage limits as color-coded progress bars, polling every 5 minutes from a local cache file with fallback to the Anthropic OAuth usage API.

  • The utilization field from the API is used directly as a 0–100 percentage in the progress bar width and color thresholds, but the Anthropic OAuth API may return a fractional 0–1 value. If so, all bars would silently render as invisible slivers and percentages would always display as 0%. The expected scale should be verified and documented (e.g. via a comment or a * 100 guard).

Confidence Score: 4/5

Mostly safe to merge, but the utilization scale assumption needs confirmation before it can be trusted to display correct data.

One P1 finding: if utilization from the API is 0–1 (fractional) rather than 0–100 as the code assumes, progress bars and percentage labels will silently be wrong for all users. The feature degrades gracefully (returns null on error) but would show misleading data rather than hiding. Resolving or documenting the expected scale brings this to a clear 5.

ClaudeUsageWidget.tsx (utilization scale) and system.ts IPC handler (dynamic os import).

Important Files Changed

Filename Overview
src/main/ipc/handlers/system.ts Adds usage:getClaudeUsage IPC handler; reads cache file then falls back to OAuth API. Uses an unnecessary dynamic import('os') inside the handler.
src/main/preload/system.ts Adds ClaudeUsagePeriod, ClaudeUsageData, ClaudeUsageResult interfaces and createUsageApi() factory; clean and correct.
src/main/preload/index.ts Wires createUsageApi() into contextBridge and re-exports types; straightforward.
src/renderer/components/SessionList/ClaudeUsageWidget.tsx New widget component with 5-min polling and color-coded bars; assumes utilization is on a 0–100 scale which needs verification against the API spec.
src/renderer/components/SessionList/SessionList.tsx Mounts ClaudeUsageWidget when sidebar is expanded; minimal, correct change.
src/renderer/global.d.ts Adds window.maestro.usage type declaration matching the preload API shape.

Sequence Diagram

sequenceDiagram
    participant W as ClaudeUsageWidget
    participant P as Preload (usage API)
    participant H as IPC Handler
    participant C as ~/.claude/MEMORY/STATE/usage-cache.json
    participant API as api.anthropic.com/api/oauth/usage
    participant K as ~/.claude/.credentials.json

    W->>P: getClaudeUsage()
    P->>H: ipcRenderer.invoke('usage:getClaudeUsage')
    H->>C: readFileSync(cachePath)
    alt Cache hit
        C-->>H: raw JSON
        H-->>P: { success: true, data }
    else Cache miss
        C-->>H: Error
        H->>K: readFileSync(credsPath)
        K-->>H: { claudeAiOauth: { accessToken } }
        H->>API: GET /api/oauth/usage (Bearer token)
        alt API success
            API-->>H: usage JSON
            H-->>P: { success: true, data }
        else API error
            API-->>H: non-200 / network error
            H-->>P: { success: false, error }
        end
    end
    P-->>W: ClaudeUsageResult
    alt success && data
        W->>W: setUsage(data) → render bars
    else failure
        W->>W: setError(true) → return null
    end
Loading

Reviews (1): Last reviewed commit: "feat: add Claude usage widget to left si..." | Re-trigger Greptile


// Read Claude usage data from PAI cache file, falling back to Anthropic OAuth API
ipcMain.handle('usage:getClaudeUsage', async () => {
const os = await import('os');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Dynamic import('os') should be a static import

os is a Node.js built-in that's always available. The dynamic import adds unnecessary async overhead on every IPC call. Since path is already statically imported at the top of this file, os should follow the same pattern.

Suggested change
const os = await import('os');
const cachePath = path.join(require('os').homedir(), '.claude', 'MEMORY', 'STATE', 'usage-cache.json');

Or better, add import * as os from 'os'; at the top of the file alongside the existing path import and remove the dynamic import entirely.

Comment on lines +51 to +52
function UsageBar({ label, sublabel, utilization, theme }: UsageBarProps) {
const pct = Math.min(100, Math.max(0, utilization));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Verify utilization is on a 0–100 scale, not 0–1

The code clamps utilization to [0, 100] and applies thresholds of 50 and 80, treating the value as a percentage integer/float. If the Anthropic OAuth API actually returns a fraction (0–1, which is a common REST convention for utilization ratios), then pct would always be between 0 and 1 — progress bars would render as invisible slivers (~0–1% wide) and the displayed percentage would always show 0% or 1%. The widget would silently appear broken.

A comment or assertion documenting the expected range (e.g. // API returns 0–100) would protect against future confusion, and a multiplication guard would make the intent explicit.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
src/main/ipc/handlers/system.ts (1)

696-698: Avoid any type for caught errors.

Use unknown instead of any for the error type, then safely access the message property:

Proposed fix
-	} catch (err: any) {
-		return { success: false, error: err?.message ?? 'Unknown error' };
+	} catch (err: unknown) {
+		const message = err instanceof Error ? err.message : 'Unknown error';
+		return { success: false, error: message };
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/system.ts` around lines 696 - 698, Replace the unsafe
catch clause using "err: any" with a catch using "err: unknown" and safely
extract the message via a type-guard (e.g., if (err instanceof Error) use
err.message, else fallback to String(err) or 'Unknown error') before returning {
success: false, error: ... }; update the catch block that currently reads "catch
(err: any) { return { success: false, error: err?.message ?? 'Unknown error' }
}" to perform the type check and produce a well-typed error string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/ipc/handlers/system.ts`:
- Around line 671-677: Replace the synchronous fsSync.readFileSync call with the
async fs.promises.readFile (or fs.readFile with await) inside the handler that
currently uses try { const raw = fsSync.readFileSync(cachePath, 'utf-8'); ... }
so it no longer blocks the Electron main process; update the enclosing function
(the IPC handler in system.ts) to be async if needed, await the read, JSON.parse
the result as before, and preserve the same { success: true, data } return shape
while keeping the existing catch fallback behavior.

---

Nitpick comments:
In `@src/main/ipc/handlers/system.ts`:
- Around line 696-698: Replace the unsafe catch clause using "err: any" with a
catch using "err: unknown" and safely extract the message via a type-guard
(e.g., if (err instanceof Error) use err.message, else fallback to String(err)
or 'Unknown error') before returning { success: false, error: ... }; update the
catch block that currently reads "catch (err: any) { return { success: false,
error: err?.message ?? 'Unknown error' } }" to perform the type check and
produce a well-typed error string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c5b7a74b-2288-4f6e-a5ec-2888ece54dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 74ea8a8 and c8091f7.

📒 Files selected for processing (6)
  • src/main/ipc/handlers/system.ts
  • src/main/preload/index.ts
  • src/main/preload/system.ts
  • src/renderer/components/SessionList/ClaudeUsageWidget.tsx
  • src/renderer/components/SessionList/SessionList.tsx
  • src/renderer/global.d.ts

Comment on lines +671 to +677
try {
const raw = fsSync.readFileSync(cachePath, 'utf-8');
const data = JSON.parse(raw);
return { success: true, data };
} catch {
// Cache not available — fall back to calling the Anthropic OAuth API directly
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Synchronous file reads block the Electron main process event loop.

fsSync.readFileSync is synchronous and blocks the main process event loop while reading. Per the established pattern in other handlers (e.g., symphony.ts), use the async fs.promises.readFile instead:

Proposed fix using async reads
+import { promises as fs } from 'fs';
+
 // Read Claude usage data from PAI cache file, falling back to Anthropic OAuth API
 ipcMain.handle('usage:getClaudeUsage', async () => {
 	const os = await import('os');
 	const cachePath = path.join(os.homedir(), '.claude', 'MEMORY', 'STATE', 'usage-cache.json');

 	// Try reading from the PAI cache file first (already kept fresh by PAI hooks)
 	try {
-		const raw = fsSync.readFileSync(cachePath, 'utf-8');
+		const raw = await fs.readFile(cachePath, 'utf-8');
 		const data = JSON.parse(raw);
 		return { success: true, data };
 	} catch {
 		// Cache not available — fall back to calling the Anthropic OAuth API directly
 	}

 	// Fallback: read credentials and call the API
 	try {
 		const credsPath = path.join(os.homedir(), '.claude', '.credentials.json');
-		const credsRaw = fsSync.readFileSync(credsPath, 'utf-8');
+		const credsRaw = await fs.readFile(credsPath, 'utf-8');
 		const creds = JSON.parse(credsRaw);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/system.ts` around lines 671 - 677, Replace the
synchronous fsSync.readFileSync call with the async fs.promises.readFile (or
fs.readFile with await) inside the handler that currently uses try { const raw =
fsSync.readFileSync(cachePath, 'utf-8'); ... } so it no longer blocks the
Electron main process; update the enclosing function (the IPC handler in
system.ts) to be async if needed, await the read, JSON.parse the result as
before, and preserve the same { success: true, data } return shape while keeping
the existing catch fallback behavior.

@pedramamini
Copy link
Copy Markdown
Collaborator

@adamcongdon thank you for the contribution! Do you have any screenshots you can attach? I'm very curious to see it. At the moment, I'm quite busy trying to get the latest RC out, but we'll switch focus to this later in the week. In the meanwhile, I'd love to get a sneak peek!

- Add static 'os' import; remove dynamic import('os') inside handler
- Switch readFileSync to async fs.promises.readFile (non-blocking)
- Change err: any to err: unknown with instanceof Error type guard
- Break long JSX line in ClaudeUsageWidget to satisfy printWidth:100
- Add usage:getClaudeUsage to system.test.ts expected channel list

Fixes lint-and-format and test CI failures.
@adamcongdon
Copy link
Copy Markdown
Author

@pedramamini yeah, I should have put that in. My apologies!

Simple and clean. Works great for my dev build.

image

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.

2 participants