Implementation plan · v1Native macOS + WidgetKit

Codex Meter

A quiet Notification Center widget that shows current Codex quota pressure, reset timing, and just enough recent activity to make the numbers meaningful.

StatusReady for implementation
UpdatedAugust 21, 2026
RepositoryNew / empty foundation

Desired outcome

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

Recommended v1

Widget plus lightweight menu-bar host

The host owns the Codex process, refreshes shared data, and offers setup and diagnostics. The widget only renders a cached snapshot.

Freshness target

Event-driven while active; periodic otherwise

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.

Scope and constraints

In v1
  • A native SwiftUI containing app and a WidgetKit extension.
  • Small and medium macOS widget families for Notification Center and desktop placement.
  • Codex ChatGPT rate-limit windows from account/rateLimits/read.
  • Today’s token activity and a seven-day micro-history from account/usage/read.
  • CLI discovery, authentication status, manual refresh, launch-at-login, and diagnostics in the host app.
  • Defensive parsing, atomic shared caching, and observable stale/error states.
Deferred
  • OpenAI developer API spend and billing data; it is a separate product/account surface.
  • Changing limits, redeeming reset credits, or sending workspace-owner notifications.
  • Large widgets, iOS support, configurable accounts, cloud sync, or analytics.
  • Mac App Store distribution. The installed-CLI dependency needs a different sandbox/distribution strategy.
  • Starting Codex from the widget extension itself.
Confirmed platform contracts

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.

Proposed architecture

Keep all process, authentication, and network concerns in the containing app. Give the widget a tiny, stable read model.

Containing app

Low-profile menu-bar utility

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

No shell invocation

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

One long-lived connection

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

Cache-first and honest

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.

Freshness limitation

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.

Minimal widget experience

CodexUpdated now
Short windowUsed %
Resets in relative time
Long windowUsed %
Resets on date or in relative time

Illustrative hierarchy only; bar values are not account data.

Codex usagePlan · freshness
Duration-derived labelUsed %
Reset timing
Duration-derived labelUsed %
Reset timing
TODAYToken count
Past 7 available buckets

Medium adds recent activity without turning the widget into a dashboard.

ElementSmallMediumDisplay rule
Quota metersUp to 2Up to 4Normalize all returned windows. Prefer the canonical bucket, then stable-sort by bucket and duration. Never hard-code “5 hour” or “weekly.”
Window labelCompactFullUse a genuinely user-facing limitName; otherwise format windowDurationMins as a duration.
Reset timingRelativeRelative or dateShow “in 2h” for near resets and a concise local date for longer windows.
Today’s tokensFallback when only one meterAlways when availableRead the matching daily bucket. Omit the stat instead of displaying zero when the service returns null.
7-day activityNoMicro barsUse only available daily buckets; no implied completeness.
FreshnessShort labelTimestamp/stateFresh under 30 minutes; stale thereafter. Errors must not overwrite the last successful snapshot.
Reset creditsNoOptional badgeShow only when availableCount > 0. Redemption remains outside the widget.

Data contract and normalization

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 view
  • rateLimitsByLimitId multi-bucket view
  • primary and optional secondary
  • usedPercent, duration, reset timestamp
  • Optional plan, credits, and reset state
  • Optional summary and daily token buckets
UsageSnapshot {
  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?
}

Normalization rules

  • Prefer rateLimitsByLimitId when present; fall back to rateLimits.
  • Flatten each bucket’s non-null primary and secondary windows.
  • Clamp display percentages to 0–100 while retaining the raw decoded value for diagnostics.
  • Deduplicate the fallback and multi-bucket views by stable bucket/window identity.
  • Preserve unknown bucket IDs and fields without crashing.

Failure-state rules

  • No CLI: setup prompt in the host; unavailable widget state.
  • Not authenticated: direct the user to Codex login; do not request credentials.
  • Partial response: render known values and omit unknown stats.
  • Refresh failure: keep the last good data, persist error metadata only in host diagnostics, and mark stale by age.
  • Protocol mismatch: stop rapid retries and show the installed CLI version.

Phased delivery plan

0

Contract and distribution spike

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.

Deliverable: minimal signed Xcode project, redacted JSON fixtures, documented bundle/App Group IDs, and a recorded go/no-go decision for direct distribution.

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.

1

Deep usage module

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.

Deliverable: independently testable usage source returning one normalized 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.

2

Host lifecycle and shared cache

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.

Deliverable: a menu-bar app that stays fresh across app-server restarts and gives the user a clear remedy for every setup state.

Exit: a successful refresh updates the shared snapshot and asks WidgetKit to reload exactly once; a failed refresh preserves the last good snapshot.

3

Widget UI and timelines

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.

Deliverable: gallery-ready widgets that remain legible in Notification Center and on the desktop at supported appearances and sizes.

Exit: both families render every fixture state without clipping, ambiguity, or a false “0% used.”

4

Hardening and direct release

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.

Deliverable: notarized personal/direct build with a reproducible archive checklist and an explicit supported Codex CLI range.

Exit: the widget remains truthful and recoverable through all defined failure cases, and no credentials or raw account payloads are packaged or logged.

Proposed repository shape

CodexMeter.xcodeproj
CodexMeterApp/
  App/
  MenuBar/
  UsageService/
CodexMeterWidget/
  Provider/
  Views/
Shared/
  Models/
  SnapshotStore/
CodexMeterTests/
  Fixtures/
  Protocol/
  Normalization/
docs/
  codex-meter-implementation-plan.html

Module boundary

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.

Validation and success criteria

Main risks and mitigations

High impactCLI/App Server compatibility changes

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.

High impactMac App Store sandbox mismatch

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.

Medium impactWidget freshness expectations

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.

Medium impactAmbiguous or numerous quota buckets

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.

Medium impactGUI process cannot find Homebrew PATH

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.

Low impactUsage data privacy on a visible desktop

Expose only percentages, timing, plan label, and aggregate token counts. Offer an option to hide token totals; avoid account identity and raw payload logging.

Decision log

Recommended

Working product shape

Codex Meter as a small menu-bar host plus small and medium Notification Center widgets.

Recommended

Distribution

Personal/direct distribution first, with Developer ID signing and notarization when sharing beyond the local machine.

Recommended

Compatibility floor

Target macOS 14 or later for modern WidgetKit presentation and launch-at-login support, while building with the installed Xcode 26.6 toolchain.

Confirm before phase 0

Identity and signing

Choose the final app name, bundle identifier prefix, App Group identifier, and Apple Developer team. These values become part of the shared-container contract.

Confirm before release

Privacy preference

Decide whether today’s token count is visible by default or opt-in. Quota percentages and reset times remain the primary experience.

Validate in phase 0

Supported CLI range

The current machine has Codex CLI 0.147.0. Define the actual minimum only after fixture and handshake tests against the implementation.

Primary references