Product promise
At a glance, answer: How much Codex capacity have I used, and when does it come back?
- Small and medium widgets
- Read-only account access
- No copied OAuth credentials
- Clear stale/error states
A quiet Notification Center widget that shows current Codex quota pressure, reset timing, and just enough recent activity to make the numbers meaningful.
Product promise
At a glance, answer: How much Codex capacity have I used, and when does it come back?
Recommended v1
The host owns the Codex process, refreshes shared data, and offers setup and diagnostics. The widget only renders a cached snapshot.
Freshness target
Apply rate-limit update events immediately while the host runs, refresh the complete snapshot every 15 minutes, and visibly mark data stale after 30 minutes.
account/rateLimits/read.account/usage/read.The Codex App Server uses a JSONL protocol over stdio, requires an initialize/initialized handshake, exposes both requested account reads, and can emit rate-limit updates. WidgetKit renders extensions in a separate process and budgets timeline refreshes, so shared cached data is the reliable seam.
Keep all process, authentication, and network concerns in the containing app. Give the widget a tiny, stable read model.
codex app-serverContaining app
Run as an accessory app without a persistent Dock icon. Its menu shows connection health, last refresh, plan, CLI location, “Refresh now,” launch-at-login, and quit.
Process boundary
Launch a validated executable URL directly with Process and the argument app-server. Read stdout as JSONL and stderr separately. Never interpolate a command string.
Update loop
Initialize once, issue both reads, accept account/rateLimits/updated notifications, and perform a full refresh every 15 minutes. Restart with bounded exponential backoff after unexpected exit.
Widget timeline
Read only from the group container. Ask WidgetKit to reload after host writes, and provide an .after policy as a fallback. Use relative date rendering for reset countdowns without source polling.
If the containing app is quit, no component is authorized to query Codex. The widget keeps the last snapshot and marks it stale. Launch-at-login should be offered during setup, but remain opt-in.
| Element | Small | Medium | Display rule |
|---|---|---|---|
| Quota meters | Up to 2 | Up to 4 | Normalize all returned windows. Prefer the canonical bucket, then stable-sort by bucket and duration. Never hard-code “5 hour” or “weekly.” |
| Window label | Compact | Full | Use a genuinely user-facing limitName; otherwise format windowDurationMins as a duration. |
| Reset timing | Relative | Relative or date | Show “in 2h” for near resets and a concise local date for longer windows. |
| Today’s tokens | Fallback when only one meter | Always when available | Read the matching daily bucket. Omit the stat instead of displaying zero when the service returns null. |
| 7-day activity | No | Micro bars | Use only available daily buckets; no implied completeness. |
| Freshness | Short label | Timestamp/state | Fresh under 30 minutes; stale thereafter. Errors must not overwrite the last successful snapshot. |
| Reset credits | No | Optional badge | Show only when availableCount > 0. Redemption remains outside the widget. |
Decode the evolving transport into optional wire models, then convert it to a deliberately small, versioned widget snapshot. This prevents App Server changes from leaking through the UI.
Wire inputs
rateLimits fallback viewrateLimitsByLimitId multi-bucket viewprimary and optional secondaryusedPercent, duration, reset timestampUsageSnapshot {
schemaVersion: 1
fetchedAt: Date
source: .live | .cached
planType: String?
windows: [QuotaWindow]
todayTokens: Int64?
recentDays: [DailyTokens]
resetCreditsAvailable: Int?
}
QuotaWindow {
stableID: String
bucketID: String
displayName: String
usedPercent: Double
windowDurationMinutes: Int
resetsAt: Date
reachedState: String?
}
rateLimitsByLimitId when present; fall back to rateLimits.Capture fixtures from the installed CLI without committing account values. Confirm the stable handshake and both read methods. Prove that a signed direct-distribution host can launch the selected Codex binary and write to an App Group shared with a widget extension.
Exit: one command-line integration test returns and normalizes limits plus usage on this Mac; the widget can read a test snapshot from the shared container.
Build a concurrency-safe AppServerClient actor behind a small UsageSource protocol. Implement process lifecycle, handshake, request correlation, JSONL framing, stderr diagnostics, timeout handling, retry policy, wire decoding, and snapshot normalization.
UsageSnapshot, with fixture coverage for nulls, multiple buckets, unknown fields, and failures.Exit: no UI code knows JSON-RPC method names or raw response shapes.
Add CLI discovery/configuration, atomic App Group persistence, 15-minute polling, rate-limit notification handling, WidgetCenter reloads, manual refresh, app health states, bounded logging, and optional launch-at-login.
Exit: a successful refresh updates the shared snapshot and asks WidgetKit to reload exactly once; a failed refresh preserves the last good snapshot.
Implement small and medium layouts, placeholders, previews, accessibility labels, light/dark and accented rendering, dynamic type tolerance, reset-relative timestamps, stale states, and a tap-through to the host.
Exit: both families render every fixture state without clipping, ambiguity, or a false “0% used.”
Exercise sleep/wake, reboot, logout, CLI upgrade, network loss, expired auth, process crash, and stale-cache scenarios outside the Xcode debugger. Add signing/notarization, a concise privacy note, installation instructions, and a compatibility floor.
Exit: the widget remains truthful and recoverable through all defined failure cases, and no credentials or raw account payloads are packaged or logged.
CodexMeter.xcodeproj
CodexMeterApp/
App/
MenuBar/
UsageService/
CodexMeterWidget/
Provider/
Views/
Shared/
Models/
SnapshotStore/
CodexMeterTests/
Fixtures/
Protocol/
Normalization/
docs/
codex-meter-implementation-plan.html
UsageService should be the deep module: callers ask for a snapshot and observe state. They do not manage processes, parse JSONL, know authentication modes, or understand App Server compatibility.
Shared contains only the versioned display contract and store needed by both targets. Widget code must not import process-management details.
Pin a tested minimum CLI version, decode additively, keep raw transport behind one module, store a schema version, and show an actionable “update Codex” state when the handshake or methods are unsupported.
Make v1 explicitly personal/direct distribution. Treat Store distribution as a separate architecture track rather than discovering late that the host cannot rely on an arbitrary Homebrew executable.
Use a resident launch-at-login host, immediate reload requests on writes, periodic polling, relative reset rendering, and a visible freshness label. Never promise exact WidgetKit refresh timing.
Normalize every bucket, derive duration labels, use user-facing names only when valid, keep ordering stable, and let the medium widget reveal more than the small widget.
Search a short list of known absolute locations, then let the user select the executable. Persist the resolved URL and validate executability on each launch; do not invoke a login shell.
Expose only percentages, timing, plan label, and aggregate token counts. Offer an option to hide token totals; avoid account identity and raw payload logging.
Codex Meter as a small menu-bar host plus small and medium Notification Center widgets.
Personal/direct distribution first, with Developer ID signing and notarization when sharing beyond the local machine.
Target macOS 14 or later for modern WidgetKit presentation and launch-at-login support, while building with the installed Xcode 26.6 toolchain.
Choose the final app name, bundle identifier prefix, App Group identifier, and Apple Developer team. These values become part of the shared-container contract.
Decide whether today’s token count is visible by default or opt-in. Quota percentages and reset times remain the primary experience.
The current machine has Codex CLI 0.147.0. Define the actual minimum only after fixture and handshake tests against the implementation.