From f1bcb7739286a5006124d68c85f0fbe5c8de91d0 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:16:19 -0700 Subject: [PATCH] fix: parse Claude Fable current-week usage Claude Code 2.1.199 renders the Fable plan bucket as Current week (Fable), while Orca only parsed a standalone Fable heading. Accept weekly-style Fable headings as the distinct Fable weekly meter while keeping broader Fable-weekly copy as a section boundary to avoid false positives. Validation: - pnpm exec vitest run --config config/vitest.config.ts src/main/rate-limits/claude-pty.test.ts src/main/rate-limits/claude-fetcher.test.ts src/main/rate-limits/service.test.ts src/renderer/src/components/status-bar/tooltip.test.ts src/renderer/src/components/status-bar/inline-usage-bars.test.tsx - pnpm run typecheck - pnpm exec oxlint src/main/rate-limits/claude-pty.ts src/main/rate-limits/claude-pty.test.ts src/main/rate-limits/claude-fetcher.ts src/main/rate-limits/claude-fetcher.test.ts src/main/rate-limits/service.ts src/main/rate-limits/service.test.ts src/renderer/src/components/status-bar/tooltip.test.ts src/renderer/src/components/status-bar/inline-usage-bars.test.tsx - git diff --check HEAD~1..HEAD - live local Claude fetcher returned fableWeekly: 62 --- src/main/rate-limits/claude-pty.test.ts | 43 +++++++++++++++++++++++++ src/main/rate-limits/claude-pty.ts | 12 +++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/main/rate-limits/claude-pty.test.ts b/src/main/rate-limits/claude-pty.test.ts index a613194868e..acea13231c7 100644 --- a/src/main/rate-limits/claude-pty.test.ts +++ b/src/main/rate-limits/claude-pty.test.ts @@ -258,6 +258,49 @@ describe('fetchViaPty', () => { }) }) + it('parses Claude current-week Fable usage as a distinct weekly window', async () => { + const term = makeMockTerm() + spawnMock.mockReturnValue(term) + + const resultPromise = fetchViaPty() + + await vi.advanceTimersByTimeAsync(2_000) + term.emitData(` + Plan usage limits + + Current session + 8% used + Resets 3:39am + + Current week (all models) + 33% used + Resets Jul 3 at 12:59pm + + Current week (Fable) + 62% used + Resets Jul 3 at 12:59pm + `) + await vi.advanceTimersByTimeAsync(2_000) + + await expect(resultPromise).resolves.toMatchObject({ + provider: 'claude', + status: 'ok', + session: { + usedPercent: 8, + resetDescription: '3:39am' + }, + weekly: { + usedPercent: 33, + resetDescription: 'Jul 3 at 12:59pm' + }, + fableWeekly: { + usedPercent: 62, + resetDescription: 'Jul 3 at 12:59pm' + }, + error: null + }) + }) + it('does not let an incomplete Fable section consume later usage sections', async () => { const term = makeMockTerm() spawnMock.mockReturnValue(term) diff --git a/src/main/rate-limits/claude-pty.ts b/src/main/rate-limits/claude-pty.ts index f0363eba3f2..20919a391ca 100644 --- a/src/main/rate-limits/claude-pty.ts +++ b/src/main/rate-limits/claude-pty.ts @@ -22,6 +22,8 @@ const SESSION_RE = /current\s*session/i const WEEKLY_RE = /(?:current\s*week|weekly\s*(?:limits?|usage|rate\s*limits?)|7\s*[- ]?\s*day)/i const FABLE_WORD_RE = /\bfable\b/i const FABLE_LABEL_RE = /^\s*fable\s*$/i +const FABLE_WEEKLY_LABEL_RE = + /(?:current\s*week|weekly\s*(?:limits?|usage|rate\s*limits?)|7\s*[- ]?\s*day)\s*(?:\([^)]*\bfable\b[^)]*\)|[-:]?\s*\bfable\b)/i const PERCENT_RE = /(\d{1,3})(?:\.\d+)?\s*%\s*(used|consumed|left|remaining|available)/i const RESET_LINE_RE = /resets?\s+(?:at\s+|in\s+)?(.+)/i const ESC = String.fromCharCode(27) @@ -45,6 +47,12 @@ function matchesFableBoundary(line: string): boolean { return FABLE_LABEL_RE.test(line) || (FABLE_WORD_RE.test(line) && WEEKLY_RE.test(line)) } +function matchesFableUsageLabel(line: string): boolean { + // Why: broad Fable-weekly copy should stop nearby scans, but only a real + // Fable usage heading should produce the distinct Fable meter. + return FABLE_LABEL_RE.test(line) || FABLE_WEEKLY_LABEL_RE.test(line) +} + function isSectionLabel(line: string): boolean { return SESSION_RE.test(line) || matchesWeeklyLabel(line) || matchesFableBoundary(line) } @@ -104,7 +112,7 @@ function parsePtyUsage(output: string): { const sessionPct = extractPercentAfterLabel(lines, (line) => SESSION_RE.test(line)) const weeklyPct = extractPercentAfterLabel(lines, matchesWeeklyLabel) - const fableWeeklyPct = extractPercentAfterLabel(lines, (line) => FABLE_LABEL_RE.test(line)) + const fableWeeklyPct = extractPercentAfterLabel(lines, matchesFableUsageLabel) const session: RateLimitWindow | null = sessionPct !== null @@ -132,7 +140,7 @@ function parsePtyUsage(output: string): { usedPercent: Math.min(100, Math.max(0, fableWeeklyPct)), windowMinutes: 10080, resetsAt: null, - resetDescription: extractResetAfterLabel(lines, (line) => FABLE_LABEL_RE.test(line)) + resetDescription: extractResetAfterLabel(lines, matchesFableUsageLabel) } : null