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
This commit is contained in:
Neil
2026-07-03 00:16:19 -07:00
committed by GitHub
parent 0ab4d8925c
commit f1bcb77392
2 changed files with 53 additions and 2 deletions
+43
View File
@@ -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)
+10 -2
View File
@@ -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