feat(contrib): SwiftBar menu bar quota plugin for macOS#603
Conversation
Card-style menu bar widget showing per-account 5h/7d quota windows, active-account marker from runtime observability, reset countdowns, and an explicit live-refresh action. Reads the local quota cache only (zero quota cost); not part of the published npm package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughadds a new swiftbar menu bar plugin for codex multi-auth quota display. a bash entrypoint wraps embedded python that reads local cache and account store json files, renders per-account quota cards with active/idle indicators and remaining percentages, and outputs swiftbar-formatted menu sections. supports both passive cache reads and live probes via ChangesSwiftBar Codex Quota Plugin
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes notes for review
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
您好,您的邮件已经收到,我会尽快回复,谢谢。祝好——中山大学管理学院蔡佳新
|
| age_min = int((time.time() - newest / 1000) / 60) | ||
| age = "just now" if age_min < 1 else (f"{age_min}m ago" if age_min < 60 else f"{age_min//60}h ago") | ||
| print(f"Cache updated {age} | size=11 color=gray") | ||
| print(f"Live refresh (one probe per account) | bash={SELF} param1=livecheck terminal=false refresh=true") |
There was a problem hiding this comment.
Unquoted
SELF path in SwiftBar action
bash={SELF} is written without quotes; if the plugin is installed in a path with spaces (e.g. a home dir like /Users/John Doe/) SwiftBar's parameter parser splits on whitespace and treats everything after the first space as a new param, silently dropping the livecheck dispatch and breaking the live-refresh action. SwiftBar supports bash="{SELF}" (or percent-encoding), which handles spaces cleanly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/swiftbar/codex-quota.5m.sh
Line: 133
Comment:
**Unquoted `SELF` path in SwiftBar action**
`bash={SELF}` is written without quotes; if the plugin is installed in a path with spaces (e.g. a home dir like `/Users/John Doe/`) SwiftBar's parameter parser splits on whitespace and treats everything after the first space as a new param, silently dropping the `livecheck` dispatch and breaking the live-refresh action. SwiftBar supports `bash="{SELF}"` (or percent-encoding), which handles spaces cleanly.
How can I resolve this? If you propose a fix, please make it concise.| cache = load(CACHE) | ||
| store = load(STORE) | ||
| if not cache or not store: | ||
| print("⚡?") | ||
| print("---") | ||
| print("Cannot read quota cache or account store | color=red") | ||
| print(f"Expected under: {DATA_DIR} | size=11 color=gray") | ||
| sys.exit(0) | ||
|
|
||
| emails, order = {}, [] | ||
| for acc in store.get("accounts", []): | ||
| aid = acc.get("accountId", "") | ||
| emails[aid] = acc.get("email", aid[-6:] if aid else "?") | ||
| order.append(aid) | ||
|
|
||
| by_id = cache.get("byAccountId", {}) | ||
|
|
||
| observ = load(OBSERV) or {} |
There was a problem hiding this comment.
Concurrent reads across three separate files
quota-cache.json, openai-codex-accounts.json, and runtime-observability.json are opened sequentially with no advisory lock. the rotation proxy can write any of these between reads, so order (from the accounts store) and by_id (from the quota cache) can be momentarily inconsistent — a freshly-added account appears in order but has no entry in by_id, producing a "?" quota row. the load() except guard handles full parse failures, but not this cross-file ordering gap. for a 5 m plugin this is cosmetic, but flagging explicitly since the codebase has active concurrent writers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/swiftbar/codex-quota.5m.sh
Line: 73-90
Comment:
**Concurrent reads across three separate files**
`quota-cache.json`, `openai-codex-accounts.json`, and `runtime-observability.json` are opened sequentially with no advisory lock. the rotation proxy can write any of these between reads, so `order` (from the accounts store) and `by_id` (from the quota cache) can be momentarily inconsistent — a freshly-added account appears in `order` but has no entry in `by_id`, producing a `"?"` quota row. the `load()` except guard handles full parse failures, but not this cross-file ordering gap. for a 5 m plugin this is cosmetic, but flagging explicitly since the codebase has active concurrent writers.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@contrib/swiftbar/codex-quota.5m.sh`:
- Around line 64-67: The remaining quota value can be outside 0..100 causing
malformed bars and noisy titles; clamp it to that range before formatting or
rendering. Update the bar(remaining, slots=10) usage (or inside bar itself) to
sanitize remaining via remaining = max(0, min(100, remaining)) and ensure the
same clamped value is used when building the title/display strings (the code
that formats the title around the quota and the call sites that render the bar),
so both the bar width and title always use a value constrained to 0–100.
- Around line 1-2: The script codex-quota.5m.sh currently lacks a runtime guard
for non-macOS systems; add an early check right after the shebang (#!/bin/bash)
that verifies the OS (e.g., [[ "$(uname -s)" == "Darwin" ]] or OSTYPE contains
darwin) and if not darwin print a short soft/friendly message and exit 0 so the
plugin fails softly on Linux/Windows; ensure the check is placed before any
SwiftBar logic runs so accidental execution outside macOS exits cleanly.
- Around line 22-135: Add fixture-driven regression tests that cover
active-account fallback, quota_color thresholds (<30 -> ORANGE, <10 -> RED),
title formatting (active account wrapped in brackets), and missing/bad window
handling; refactor the rendering logic into a testable function (e.g., expose
load + the loop that builds titles/blocks as a function that returns (titles,
blocks, newest) or export a render_rows helper used by the main script), then
write tests that load sample CACHE/STORE/OBSERV fixtures and assert either the
generated textual lines (golden) or the structured rows/blocks (preferred) for
each fixture case: active-id missing, primary/secondary missing, usedPercent
edge values (9, 29, 30), and ensure fmt_reset, bar, and quota_color produce the
expected strings/colors.
- Around line 14-17: The live refresh block currently swallows failures by
redirecting codex-multi-auth check output to /dev/null and unconditionally
exiting 0; change the if [ "$1" = "livecheck" ] block so it does not discard
output and it propagates the command's exit status (i.e., run codex-multi-auth
check without >/dev/null 2>&1 and exit with its exit code—use its return value
or short-circuit like running codex-multi-auth check and then exit $?),
referencing the codex-multi-auth check invocation to ensure refresh failures are
detectable.
In `@contrib/swiftbar/README.md`:
- Around line 6-9: The README uses fenced code blocks without language tags
(tripping markdownlint MD040) for the menu-bar title block and the account
display block; update both fenced code blocks (the one containing "⚡88·[90] ←
menu bar title..." and the one starting with
"╭──────────────────────────────────╮" / "│ ● account-a ACTIVE │")
to include a language like text (i.e., replace ``` with ```text) so markdownlint
passes; ensure you update the other similar fenced block between lines 13–24 the
same way.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: abe7733f-d2d4-4d0c-b94f-3e1e804c54c0
📒 Files selected for processing (2)
contrib/swiftbar/README.mdcontrib/swiftbar/codex-quota.5m.sh
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (1)
**
⚙️ CodeRabbit configuration file
**: # PROJECT KNOWLEDGE BASEGenerated: 2026-04-25
Commit: a87e005
Validated: 2026-06-10 against commit 98d9819 (repo audit; claims re-checked against the tree, content not regenerated)
Branch: main
Package version: 2.3.0-beta.3OVERVIEW
codex-multi-authis a Codex CLI-first OAuth account manager and optional forwarding wrapper for the official Codex CLI. The installedcodex-multi-authentrypoint handles account-management commands locally,codex-multi-auth-codexforwards official Codex commands through this package's wrapper when explicitly used, and runtime rotation can route live Responses traffic through a localhost account-rotation proxy by default. The plugin-host entrypoint remains exported for compatibility, but the primary product surface is the account manager, optional wrapper, storage, runtime proxy, and repair tooling.STRUCTURE
./ ├── scripts/ │ ├── codex.js # codex-multi-auth-codex wrapper, official CLI forwarder, shadow CODEX_HOME/runtime proxy setup │ ├── codex-multi-auth.js # standalone package CLI entrypoint │ ├── codex-routing.js # auth command and compatibility alias routing │ ├── codex-bin-resolver.js # official Codex binary discovery │ ├── codex-app-router.js # persistent localhost router for packaged Codex app bind │ └── codex-app-launcher.js # reversible user-level app launcher routing helper ├── index.ts # optional plugin-host runtime entry ├── lib/ # core runtime logic (see lib/AGENTS.md) │ ├── auth/ # OAuth flow, PKCE, callback server │ ├── runtime/ # Codex CLI/app integration helpers, app bind, live sync, runtime observability │ ├── request/ # request transform, SSE, failover, backoff │ ├── storage/ # path resolution, migrations, backups, restore, import/export │ ├── codex-cli/ # Codex CLI state sync and writer helpers │ ├── codex-manager/ # command modules and...
Files:
contrib/swiftbar/README.mdcontrib/swiftbar/codex-quota.5m.sh
🪛 markdownlint-cli2 (0.22.1)
contrib/swiftbar/README.md
[warning] 6-6: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 13-13: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
| /usr/bin/python3 - "$SELF" <<'PYEOF' | ||
| import json, os, sys, time | ||
|
|
||
| SELF = sys.argv[1] | ||
| HOME = os.path.expanduser("~") | ||
| DATA_DIR = os.environ.get("CODEX_MULTI_AUTH_DIR") or os.path.join(HOME, ".codex", "multi-auth") | ||
| CACHE = os.path.join(DATA_DIR, "quota-cache.json") | ||
| STORE = os.path.join(DATA_DIR, "openai-codex-accounts.json") | ||
| OBSERV = os.path.join(DATA_DIR, "runtime-observability.json") | ||
|
|
||
| GREEN = "#34C759" | ||
| GRAY = "#9A9A9E" | ||
| RED = "#FF3B30" | ||
| ORANGE = "#FF9F0A" | ||
| # Card interior must stay within Menlo-native glyphs (ASCII, box drawing, | ||
| # block elements, arrows, geometric circles). CJK or emoji fall back to other | ||
| # fonts with non-integer widths and break the right border alignment. | ||
| W = 32 | ||
|
|
||
| def load(path): | ||
| try: | ||
| with open(path) as f: | ||
| return json.load(f) | ||
| except Exception: | ||
| return None | ||
|
|
||
| def pad(s, width): | ||
| return s + " " * max(0, width - len(s)) | ||
|
|
||
| def fmt_reset(ms): | ||
| left = int(ms / 1000 - time.time()) | ||
| if left <= 0: | ||
| return "now" | ||
| d, r = divmod(left, 86400) | ||
| h, r = divmod(r, 3600) | ||
| m = r // 60 | ||
| if d > 0: | ||
| return f"{d}d {h}h" | ||
| if h > 0: | ||
| return f"{h}h {m}m" | ||
| return f"{m}m" | ||
|
|
||
| def bar(remaining, slots=10): | ||
| filled = round(remaining / 100 * slots) | ||
| return "█" * filled + "░" * (slots - filled) | ||
|
|
||
| def quota_color(rem): | ||
| if rem < 10: return RED | ||
| if rem < 30: return ORANGE | ||
| return None | ||
|
|
||
| cache = load(CACHE) | ||
| store = load(STORE) | ||
| if not cache or not store: | ||
| print("⚡?") | ||
| print("---") | ||
| print("Cannot read quota cache or account store | color=red") | ||
| print(f"Expected under: {DATA_DIR} | size=11 color=gray") | ||
| sys.exit(0) | ||
|
|
||
| emails, order = {}, [] | ||
| for acc in store.get("accounts", []): | ||
| aid = acc.get("accountId", "") | ||
| emails[aid] = acc.get("email", aid[-6:] if aid else "?") | ||
| order.append(aid) | ||
|
|
||
| by_id = cache.get("byAccountId", {}) | ||
|
|
||
| observ = load(OBSERV) or {} | ||
| active_id = observ.get("lastAccountId") | ||
| if active_id not in order: | ||
| idx = store.get("activeIndex") | ||
| active_id = order[idx] if isinstance(idx, int) and 0 <= idx < len(order) else None | ||
|
|
||
| titles, blocks = [], [] | ||
| newest = 0 | ||
| for aid in order: | ||
| email = emails.get(aid, "?") | ||
| short = email.split("@")[0] | ||
| is_active = (aid == active_id) | ||
| frame = GREEN if is_active else GRAY | ||
| dot = "●" if is_active else "○" | ||
| tag = "ACTIVE" if is_active else "IDLE" | ||
| rows = [(pad(f"{dot} {short}", W - len(tag)) + tag, None)] | ||
| q = by_id.get(aid) | ||
| if not q: | ||
| titles.append("?") | ||
| rows.append((pad("no quota data", W), None)) | ||
| else: | ||
| newest = max(newest, q.get("updatedAt", 0)) | ||
| for key, label in (("primary", "5h"), ("secondary", "7d")): | ||
| win = q.get(key, {}) | ||
| rem = 100 - win.get("usedPercent", 0) | ||
| reset = fmt_reset(win["resetAtMs"]) if win.get("resetAtMs") else "-" | ||
| rows.append((pad(f"{label} {bar(rem)} {rem:>3}% → {reset}", W), quota_color(rem))) | ||
| if key == "primary": | ||
| titles.append(f"[{rem}]" if is_active else str(rem)) | ||
| blocks.append((frame, rows)) | ||
|
|
||
| print("⚡" + "·".join(titles)) | ||
| print("---") | ||
| style = "font=Menlo size=12 trim=false emojize=false" | ||
| for frame, rows in blocks: | ||
| print(f"╭{'─' * (W + 2)}╮ | color={frame} {style}") | ||
| for text, override in rows: | ||
| print(f"│ {text} │ | color={override or frame} {style}") | ||
| print(f"╰{'─' * (W + 2)}╯ | color={frame} {style}") | ||
| if newest: | ||
| age_min = int((time.time() - newest / 1000) / 60) | ||
| age = "just now" if age_min < 1 else (f"{age_min}m ago" if age_min < 60 else f"{age_min//60}h ago") | ||
| print(f"Cache updated {age} | size=11 color=gray") | ||
| print(f"Live refresh (one probe per account) | bash={SELF} param1=livecheck terminal=false refresh=true") | ||
| print("Reload from cache | refresh=true") | ||
| PYEOF |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift
add regression coverage for quota parsing/render formatting.
this feature ships non-trivial transform/render logic with internal-cache compatibility assumptions, but this pr has no tests for: active-account fallback, color thresholds (<30, <10), title formatting, and bad/missing windows. please add fixture-driven regression tests (golden output or structured row assertions) so future cache/schema changes do not silently break the widget.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contrib/swiftbar/codex-quota.5m.sh` around lines 22 - 135, Add fixture-driven
regression tests that cover active-account fallback, quota_color thresholds (<30
-> ORANGE, <10 -> RED), title formatting (active account wrapped in brackets),
and missing/bad window handling; refactor the rendering logic into a testable
function (e.g., expose load + the loop that builds titles/blocks as a function
that returns (titles, blocks, newest) or export a render_rows helper used by the
main script), then write tests that load sample CACHE/STORE/OBSERV fixtures and
assert either the generated textual lines (golden) or the structured rows/blocks
(preferred) for each fixture case: active-id missing, primary/secondary missing,
usedPercent edge values (9, 29, 30), and ensure fmt_reset, bar, and quota_color
produce the expected strings/colors.
- guard against non-macOS execution with a soft exit - clamp quota percentages to 0-100 before rendering bars/title - propagate codex-multi-auth check exit code from the livecheck action instead of swallowing it - tag README code fences with a language (markdownlint MD040) - correct the cache-refresh comment: the quota cache is written by quota-bearing commands, not passively by proxied traffic Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the review. Pushed fixes for the actionable items:
On the suggestion to refactor the renderer into testable functions with golden/structured fixtures: I'd prefer to skip that here. This is a self-contained, fail-soft |
What
Adds an optional SwiftBar plugin under
contrib/swiftbar/that renders per-account quota cards in the macOS menu bar:lastAccountId), falling back to the stored active indexcodex-multi-auth check(one probe per account)livecheckarg for the menu action); honorsCODEX_MULTI_AUTH_DIRWhy
With runtime rotation, "how much quota does each account have left, and which one is serving right now" becomes the question users ask most. A glanceable menu bar view answers it without shelling out to
check(and without spending probes).Notes
contrib/is outside the npmfileswhitelist, so the published package is unchanged⚡?) if the internal cache format changes🤖 Generated with Claude Code
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
adds a self-contained swiftbar plugin under
contrib/swiftbar/that renders per-account codex quota cards in the macOS menu bar, reading only the local cache (zero quota cost) with an optional live-refresh menu action.codex-quota.5m.shembeds a python3 heredoc that parsesquota-cache.json,openai-codex-accounts.json, andruntime-observability.jsonto render 5h/7d progress bars with color-coded thresholds and reset countdowns; active account is resolved fromlastAccountIdwith fallback toactiveIndexlivecheckdispatch re-invokes the script with a hardcoded PATH that misses nvm/volta installs, causing silent failures for users who installed via nvm; the header row'spad()also has no truncation guard for long email local-parts, which can blow pastW=32and break border alignmentConfidence Score: 5/5
safe to merge — contrib-only change, outside npm files whitelist, no core lib or proxy code touched
the plugin is fully isolated in contrib/, cache reads are read-only, clamp_pct correctly bounds floats to ints, and fail-soft behavior is consistent throughout; the two issues found are cosmetic display concerns that don’t affect the rotation proxy or auth flow
contrib/swiftbar/codex-quota.5m.sh — header truncation and livecheck PATH coverage worth a second look before publicising the install guide widely
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A([SwiftBar invokes script]) --> B{arg == livecheck?} B -- yes --> C[export hardcoded PATH] C --> D[codex-multi-auth check] D --> E([exit]) B -- no --> F{uname == Darwin?} F -- no --> G([show error, exit]) F -- yes --> H[invoke python3 heredoc] H --> I[load quota-cache.json] I --> J{files readable?} J -- no --> K([print error]) J -- yes --> L[resolve active_id] L --> M[build quota rows per account] M --> N[print title bar] N --> O[print card blocks] O --> P[print Live refresh action]Prompt To Fix All With AI
Reviews (2): Last reviewed commit: "fix(contrib): address review feedback on..." | Re-trigger Greptile