feat: Claude usage widget in left sidebar#749
feat: Claude usage widget in left sidebar#749adamcongdon wants to merge 2 commits intoRunMaestro:mainfrom
Conversation
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).
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryAdds 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.
Confidence Score: 4/5Mostly safe to merge, but the utilization scale assumption needs confirmation before it can be trusted to display correct data. One P1 finding: if
Important Files Changed
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "feat: add Claude usage widget to left si..." | Re-trigger Greptile |
src/main/ipc/handlers/system.ts
Outdated
|
|
||
| // Read Claude usage data from PAI cache file, falling back to Anthropic OAuth API | ||
| ipcMain.handle('usage:getClaudeUsage', async () => { | ||
| const os = await import('os'); |
There was a problem hiding this comment.
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.
| 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.
| function UsageBar({ label, sublabel, utilization, theme }: UsageBarProps) { | ||
| const pct = Math.min(100, Math.max(0, utilization)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/ipc/handlers/system.ts (1)
696-698: Avoidanytype for caught errors.Use
unknowninstead ofanyfor 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
📒 Files selected for processing (6)
src/main/ipc/handlers/system.tssrc/main/preload/index.tssrc/main/preload/system.tssrc/renderer/components/SessionList/ClaudeUsageWidget.tsxsrc/renderer/components/SessionList/SessionList.tsxsrc/renderer/global.d.ts
| 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 | ||
| } |
There was a problem hiding this comment.
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.
|
@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.
|
@pedramamini yeah, I should have put that in. My apologies! Simple and clean. Works great for my dev build.
|

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
~/.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.jsonVisual behavior
Scope
Currently Claude-specific (reads Anthropic OAuth data). Other agent providers could be added later if equivalent usage APIs become available.
Files changed
src/main/ipc/handlers/system.tsusage:getClaudeUsageIPC handlersrc/main/preload/system.tscreateUsageApi()factory + TypeScript interfacessrc/main/preload/index.tsusageinto contextBridgesrc/renderer/global.d.tswindow.maestro.usagetype declarationsrc/renderer/components/SessionList/ClaudeUsageWidget.tsxsrc/renderer/components/SessionList/SessionList.tsxSummary by CodeRabbit
New Features