feat: Usage page redesign with CLI fallback + API billing support - #44
Merged
Conversation
Lumo's usage page previously failed completely whenever Anthropic's
OAuth usage API returned 403 (edge-layer rejection, likely TLS
fingerprinting of rustls). The existing CLI fallback was broken: it
used a non-existent --output json flag and had no PTY, but Claude Code
CLI is a React/Ink TUI that requires a terminal to function.
This ports ClaudeBar's dual-probe architecture to Rust:
- interactive_runner.rs: generic PTY runner via portable-pty. Spawns a
child process in a 50x160 PTY, writes input, auto-responds to known
prompts (trust dialog, "Press Enter to continue", etc.), and exits
on child exit, hard timeout, or idle timeout after meaningful data.
- terminal_renderer.rs: thin wrapper around vt100::Parser that replays
the captured bytes through a headless terminal emulator and returns
the rendered screen contents as plain text.
- claude_cli_probe.rs: Claude-specific probe. Runs `claude /usage
--allowed-tools ""` with CLAUDE_CODE_OAUTH_TOKEN stripped, inside a
stable working directory (~/Library/Application Support/Lumo/Probe
on macOS, ~/.lumo/probe on Linux). Pre-writes the trust entry to
~/.claude.json to avoid the workspace trust dialog stalling the run.
Parses the rendered text with ClaudeBar's regex patterns
(([0-9]{1,3})\s*%\s*(used|left) plus section-label windows) into
SubscriptionUsageResponse.
subscription_usage_service.rs now delegates fetch_via_cli() to
ClaudeCliProbe, removing the broken naive parser helpers. The existing
API-primary/CLI-fallback flow is preserved.
Adds three new dependencies: portable-pty 0.9, vt100 0.16, regex 1.
All new modules have unit tests for prompt detection and usage parsing
(19 total tests passing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous API-primary/CLI-fallback design always hit
load_credentials() first, which triggered a macOS Keychain
authorization prompt on every release build launch — unreasonable UX
when the CLI path can fetch the same data without any Keychain access.
Matches ClaudeBar's default behaviour: CLI is the primary probe, API
is the fallback that only runs if the CLI can't spawn (binary missing,
PTY failure, parse error, etc.).
To keep the subscription tier badge working without reading credentials
from Keychain, the CLI probe now also parses the account tier from the
rendered /usage header line ("Opus 4.5 · Claude Max" → "MAX", etc.)
and returns it alongside the usage buckets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Redesign the Usage page around circular ECharts gauges and add a real
CLI parser that correctly handles untouched sub-buckets and
pay-as-you-go API accounts.
Frontend
- Replace the horizontal progress-bar cards with ECharts `type: gauge`
circular gauges (progress fill + inverted color bands so the "safe"
zone is where a full ring points). Gauges show percent REMAINING
instead of used, matching ClaudeBar's fuel-gauge metaphor.
- Card header now sits top-left for scan-ability; reset text is left-
aligned below the gauge. Gauges are capped at 200px square and flow
in a 1-col → 2-col responsive grid with no section headers — the
card labels already disambiguate each bucket.
- Untouched buckets (e.g. Sonnet on a Max account where it hasn't been
touched yet) still render the full gauge at 100% remaining, but the
reset slot is swapped for "You haven't used Sonnet yet" when the
backend reports `utilization == null`.
- New `ApiBillingNotice` empty state for pay-as-you-go / API billing
accounts so they no longer see the misleading "Claude Code login
required" message. Copy is provider-neutral ("Check your provider's
dashboard") to cover third-party gateways.
- Extract `UsageStatusTheme`, `UsageStatus`, and `UseUsageBucketCardResult`
into `usage-category-card/types.ts`; move `buildUsageStatusTheme` and
`prefersReducedMotion` into `libs.ts`. `use-service.ts` is now just
the hook body.
Backend
- `ExtraUsage` gains an optional `resets_at` field so CLI-parsed extra
usage reset lines can flow through to the UI.
- `ClaudeCliProbe` now returns `CliProbeResult.usage: Option<...>`.
If the CLI header reports "API Usage Billing", we short-circuit with
`usage: None` so the frontend gets a clean signal instead of the
probe bailing and falling through to the OAuth API path.
- Per-bucket reset extraction (no more shared weekly reset): each
bucket extracts its own reset inside the section window bounded by
the "Extra usage" section so inline "... Resets May 1" fragments
can't bleed backwards into Sonnet / Opus.
- New tri-state `SectionPercent::{NotFound, Untouched, Used}` so the
parser can distinguish "section absent" from "section present but
the user hasn't touched this model yet" (CLI renders `0% used` with
no block characters). Untouched buckets carry `utilization: None`
all the way to the frontend.
Tests
- 12 `claude_cli_probe` unit tests including `real_cli_output_produces
_expected_buckets` which pins the exact rendered CLI output from a
Max account where Opus is missing and Sonnet is untouched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete rewrite of the Usage page to work reliably across all account types:
portable-pty+vt100to runclaude /usageinside a PTY and parse the output. No Keychain access on the happy path —claudeitself reads the stored credentials.Backend changes
interactive_runner.rs— generic PTY runner with auto-response prompts, idle timeout, andhas_meaningful_contentdetection (ported from ClaudeBar'sInteractiveRunner.swift).terminal_renderer.rs— thin wrapper aroundvt100::Parserfor ANSI → plain text rendering.claude_cli_probe.rs— Claude-specific parser with tri-stateSectionPercent::{NotFound, Untouched, Used(f64)}. Per-bucket reset extraction bounded by theExtra usagesection so inline"... Resets May 1"fragments can't bleed backwards. Detects API billing via the subscription badge and returnsusage: Noneso the frontend gets a clean signal.ExtraUsage.resets_atadded to the typeshared struct so CLI-parsed extra usage reset lines flow to the UI.subscription_usage_service.rs— CLI-first, OAuth API as fallback; propagatescli.usage: Option<...>directly.Frontend changes
usage-category-card/— gauge card using EChartstype: gaugewith progress fill + color bands + centered percent.types.ts/libs.ts/use-service.ts/index.tsxsplit per project convention.extra-usage-card/— matching gauge for the pay-as-you-go overage section.api-billing-notice/— new empty state for API billing accounts (provider-neutral copy so third-party gateways are covered).use-service.ts— new"api_billing"FetchStatus branch.index.tsx— single responsive grid (sm:grid-cols-2), section headers dropped in favor of per-card labels, container widened tomax-w-3xl.Testing
claude_cli_probeunit tests, includingreal_cli_output_produces_expected_bucketswhich pins the exact CLI rendering captured from a real Max account (Opus absent, Sonnet at0% usedno-bar → untouched).cargo clippy --workspace -- -D warningspasses.pnpm build(Next.js static export) passes.Test plan
pnpm tauri:dev— Usage page loads with gauges on a Max account🤖 Generated with Claude Code