feat(contrib): native SwiftUI menu bar app for account quota (macOS)#605
feat(contrib): native SwiftUI menu bar app for account quota (macOS)#605plustar35 wants to merge 2 commits into
Conversation
A native MenuBarExtra(.window) app showing per-account 5h/7d quota cards. Unlike the NSMenu-based SwiftBar plugin, the .window panel repaints while open, so a probe triggered on open updates the cards in place without a reopen. Includes the Swift source, a no-Xcode build script (swiftc + hand-rolled bundle, ad-hoc signed), an optional login LaunchAgent, and a README explaining the live-update rationale. Outside the npm files whitelist, so the published package is unchanged. 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. |
📝 WalkthroughLow-risk contrib utility: This PR adds a native macOS menu bar app for displaying account quota status as a pure UI companion to existing TypeScript quota functionality. No tests are required (contrib/ excluded from npm publishing), no data-loss risks (read-only access to local cache files), and no architectural changes to core code. Key implementation notes:
The build script (swiftc-based ad-hoc codesign) and LaunchAgent plist are minimalist and well-documented. The app targets macOS 13+ and requires Swift toolchain and Walkthroughnew macOS SwiftUI menu-bar app displays per-account Codex quota from the local ChangesmacOS Quota Display App
Sequence Diagram(s)N/A — the app's primary flow is cache reload on timer and manual refresh, not a multi-component interaction. UI rendering and state update are straightforward unidirectional bindings. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes key review points:
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
|
|
||
| echo "Compiling…" | ||
| mkdir -p "$HERE/.build" | ||
| swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN" |
There was a problem hiding this comment.
swiftc without an explicit -sdk or -target compiles against whatever the toolchain selects by default. on machines with both Xcode and Command Line Tools pointing at different SDK roots, this can link against an unexpected SDK version or silently pick the wrong target architecture (x86_64 vs arm64). passing $(xcrun --show-sdk-path) and an explicit -target makes the build deterministic across setups.
| swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN" | |
| SDK=$(xcrun --show-sdk-path) | |
| ARCH=$(uname -m) | |
| swiftc -O -parse-as-library -sdk "$SDK" -target "${ARCH}-apple-macos13.0" "$HERE/CodexQuota.swift" -o "$BIN" |
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/macos-app/build.sh
Line: 14
Comment:
`swiftc` without an explicit `-sdk` or `-target` compiles against whatever the toolchain selects by default. on machines with both Xcode and Command Line Tools pointing at different SDK roots, this can link against an unexpected SDK version or silently pick the wrong target architecture (x86_64 vs arm64). passing `$(xcrun --show-sdk-path)` and an explicit `-target` makes the build deterministic across setups.
```suggestion
SDK=$(xcrun --show-sdk-path)
ARCH=$(uname -m)
swiftc -O -parse-as-library -sdk "$SDK" -target "${ARCH}-apple-macos13.0" "$HERE/CodexQuota.swift" -o "$BIN"
```
How can I resolve this? If you propose a fix, please make it concise.- refreshIfStale: use the quota cache file's mtime as the staleness signal instead of the asynchronously-committed lastUpdated property. The old check read lastUpdated before loadCache()'s main-queue commit ran, and when no cache entry carried updatedAt it stayed nil, so every panel open fired a live probe and bypassed the 60s guard. File mtime is read synchronously and always present. - LaunchAgent: document and provide a commented-out EnvironmentVariables block so a custom CODEX_MULTI_AUTH_DIR can be forwarded under autostart (LaunchAgents don't source shell profiles). README updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| @State private var titleTick = Date() | ||
| private let tick = Timer.publish(every: 60, on: .main, in: .common).autoconnect() |
There was a problem hiding this comment.
Dead timer — menu bar title never auto-refreshes while panel is closed
tick is declared and autoconnect()ed, but App.body returns a Scene — you can't attach .onReceive to a Scene, and there's no such call anywhere in this file. titleTick is assigned nowhere in body. As a result, model.loadCache() is never called from this timer; model.accounts (which drives model.titleRemaining) only changes when the panel is opened or a refresh completes. The README says the title "refreshes from cache on a timer," but with the panel closed the percentage is frozen at whatever it was at startup/last panel close — a user glancing at the icon after an hour sees stale data. To fix, drive the periodic reload from inside QuotaModel (e.g., schedule an NSTimer in init) so it isn't tied to a view's onReceive.
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/macos-app/CodexQuota.swift
Line: 233-234
Comment:
**Dead timer — menu bar title never auto-refreshes while panel is closed**
`tick` is declared and `autoconnect()`ed, but `App.body` returns a `Scene` — you can't attach `.onReceive` to a `Scene`, and there's no such call anywhere in this file. `titleTick` is assigned nowhere in `body`. As a result, `model.loadCache()` is never called from this timer; `model.accounts` (which drives `model.titleRemaining`) only changes when the panel is opened or a refresh completes. The README says the title "refreshes from cache on a timer," but with the panel closed the percentage is frozen at whatever it was at startup/last panel close — a user glancing at the icon after an hour sees stale data. To fix, drive the periodic reload from inside `QuotaModel` (e.g., schedule an `NSTimer` in `init`) so it isn't tied to a view's `onReceive`.
How can I resolve this? If you propose a fix, please make it concise.| try? task.run() | ||
| task.waitUntilExit() |
There was a problem hiding this comment.
try? task.run() silently discards any launch error and then falls through to task.waitUntilExit() unconditionally. If run() throws (e.g., sandbox denial, unexpected fs state), waitUntilExit() on an un-started Process hits a preconditionFailure("task not launched") and crashes the app. Guard the wait behind a successful launch.
| try? task.run() | |
| task.waitUntilExit() | |
| guard (try? task.run()) != nil else { | |
| DispatchQueue.main.async { self.updating = false } | |
| return | |
| } | |
| task.waitUntilExit() |
Prompt To Fix With AI
This is a comment left during a code review.
Path: contrib/macos-app/CodexQuota.swift
Line: 98-99
Comment:
`try? task.run()` silently discards any launch error and then falls through to `task.waitUntilExit()` unconditionally. If `run()` throws (e.g., sandbox denial, unexpected fs state), `waitUntilExit()` on an un-started `Process` hits a `preconditionFailure("task not launched")` and crashes the app. Guard the wait behind a successful launch.
```suggestion
guard (try? task.run()) != nil else {
DispatchQueue.main.async { self.updating = false }
return
}
task.waitUntilExit()
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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/macos-app/build.sh`:
- Line 17: Add a safety guard before the rm -rf "$APP" call: validate the APP
variable (set earlier) is non-empty and not a dangerous path ("/", "", ".",
".."), and optionally ensure it is inside the project/build workspace (e.g.,
starts with "$PWD" or a known build dir) before running rm -rf; if the check
fails, print an error and exit non-zero. Use the APP variable name and the rm
-rf invocation to locate where to insert this validation in build.sh.
In `@contrib/macos-app/CodexQuota.swift`:
- Around line 39-41: The loadCache() path must clear published state when
backing files disappear or are empty: when json("openai-codex-accounts.json")
returns nil or an empty structure, set accounts = [:] (or the empty collection
type used) and lastUpdated = nil instead of returning early; when computing
newest from the quota snapshot (the logic around newest and the cache handling
near the existing newest == 0 branch), set lastUpdated = nil whenever newest ==
0; update any cache assignment logic for json("quota-cache.json") to tolerate
missing/empty caches but not preserve prior published state. Add a
unit/integration regression test that loads successfully, then deletes or
corrupts openai-codex-accounts.json (and/or switches CODEX_MULTI_AUTH_DIR) and
asserts accounts is empty and lastUpdated is nil after calling loadCache().
- Around line 41-75: The quota lookup currently only checks the cache map byId
(byAccountId) and misses entries stored under the cache's byEmail key; update
the construction around the for-loop (where byId is read and each entry is
retrieved into entry) to first attempt lookup from byId[id], then if nil attempt
lookup from the cache's byEmail using the normalized email key (use
normalizeEmailKey(email) to match CLI behavior) so migrated/re-keyed accounts
find their quota; keep using the existing window(_:,_) and AccountQuota(...)
flow and add a regression test that loads a quota-cache.json containing only
byEmail entries (with mixed-case emails) to assert quota is found for the
account after normalization.
In `@contrib/macos-app/local.codex.quota.plist`:
- Around line 6-7: The install steps copy the plist without substituting the
home_placeholder token, producing a broken LaunchAgent (placeholder used by
local.codex.quota.plist around line with home_placeholder). Update the
instructions so the plist is written with the placeholder replaced (use the same
one-liner pattern from contrib/macos-app/README.md) — i.e., perform an inline
substitution of "home_placeholder" with "$HOME" (or the actual home path) before
placing the file into ~/Library/LaunchAgents/, then run launchctl load on the
substituted file; ensure the sequence replaces the token prior to calling
launchctl load.
In `@contrib/macos-app/README.md`:
- Line 57: Update the README entry for contrib/macos-app/build.sh to mention
that build.sh accepts an optional install path override via its first argument
(see APP="${1:-$HOME/Applications/CodexQuota.app}" in build.sh), and include an
example showing how to pass a custom path (e.g., using
/Applications/CodexQuota.app for system-wide install) so users know they can
override the default ~/Applications/CodexQuota.app.
- Line 52: The README claim that `codex-multi-auth` just needs to be "on PATH"
is misleading because contrib/macos-app/CodexQuota.swift hardcodes PATH at the
top of the spawned process to
"/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH", so installs in
custom prefixes (e.g., ~/.local/bin) will be ignored; fix by either (A) updating
the README to explicitly state the app uses that fixed PATH and list the exact
directories included (referencing CodexQuota.swift and the hardcoded PATH
string) or (B) change the app to respect the user’s environment PATH by removing
the hardcoded PATH override in CodexQuota.swift (or appending/prepending
additional common prefixes like ~/.local/bin) so subprocesses can find
user-installed `codex-multi-auth`.
🪄 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: 56b37fa3-1fb8-49e9-8fb2-693e65733646
📒 Files selected for processing (5)
contrib/macos-app/.gitignorecontrib/macos-app/CodexQuota.swiftcontrib/macos-app/README.mdcontrib/macos-app/build.shcontrib/macos-app/local.codex.quota.plist
📜 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/macos-app/local.codex.quota.plistcontrib/macos-app/build.shcontrib/macos-app/README.mdcontrib/macos-app/CodexQuota.swift
🪛 SwiftLint (0.63.3)
contrib/macos-app/CodexQuota.swift
[Warning] 30-30: Prefer empty collection over optional collection
(discouraged_optional_collection)
[Warning] 55-55: Prefer empty collection over optional collection
(discouraged_optional_collection)
[Warning] 18-18: Classes should have an explicit deinit method
(required_deinit)
🔇 Additional comments (8)
contrib/macos-app/build.sh (1)
1-16: LGTM!Also applies to: 18-45
contrib/macos-app/local.codex.quota.plist (1)
1-5: LGTM!Also applies to: 8-18, 20-36
contrib/macos-app/.gitignore (1)
1-1: LGTM!contrib/macos-app/README.md (5)
1-23: LGTM!
25-35: LGTM!
37-46: LGTM!
65-76: LGTM!
78-86: LGTM!
| swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN" | ||
|
|
||
| echo "Assembling bundle at $APP" | ||
| rm -rf "$APP" |
There was a problem hiding this comment.
add a safety guard before recursive delete.
line 17 runs rm -rf on a user-provided path from line 9. this can delete unintended directories if the arg is malformed or mis-passed.
proposed fix
APP="${1:-$HOME/Applications/CodexQuota.app}"
BIN="$HERE/.build/CodexQuota"
+if [[ -z "${APP:-}" || "$APP" == "/" || "$APP" == "$HOME" || "$APP" != *.app ]]; then
+ echo "refusing unsafe app target: $APP" >&2
+ exit 1
+fi
+
echo "Compiling…"
mkdir -p "$HERE/.build"
swiftc -O -parse-as-library "$HERE/CodexQuota.swift" -o "$BIN"🤖 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/macos-app/build.sh` at line 17, Add a safety guard before the rm -rf
"$APP" call: validate the APP variable (set earlier) is non-empty and not a
dangerous path ("/", "", ".", ".."), and optionally ensure it is inside the
project/build workspace (e.g., starts with "$PWD" or a known build dir) before
running rm -rf; if the check fails, print an error and exit non-zero. Use the
APP variable name and the rm -rf invocation to locate where to insert this
validation in build.sh.
| func loadCache() { | ||
| guard let store = json("openai-codex-accounts.json") else { return } | ||
| let cache = json("quota-cache.json") ?? [:] |
There was a problem hiding this comment.
clear published state when the backing files disappear or go empty.
line 40 returns without resetting anything, and line 80 never clears lastUpdated when the new snapshot has no timestamps. after logout, storage repair, or a CODEX_MULTI_AUTH_DIR switch, the window can keep showing the previous account cards and stale freshness text even though the source files are gone. clear accounts and lastUpdated on load failure, and nil out lastUpdated when newest == 0. please add a regression test that deletes or corrupts openai-codex-accounts.json after a successful load. the cache here is a derived local artifact, so stale ui state is worse than showing nothing. see lib/quota-cache.ts:9-36.
proposed fix
/// Read the three local files and rebuild `accounts`. No network, no probe.
func loadCache() {
- guard let store = json("openai-codex-accounts.json") else { return }
+ guard let store = json("openai-codex-accounts.json") else {
+ DispatchQueue.main.async {
+ self.accounts = []
+ self.lastUpdated = nil
+ }
+ return
+ }
@@
DispatchQueue.main.async {
self.accounts = result
- if newest > 0 { self.lastUpdated = Date(timeIntervalSince1970: newest / 1000) }
+ self.lastUpdated = newest > 0
+ ? Date(timeIntervalSince1970: newest / 1000)
+ : nil
}
}Also applies to: 78-80
🤖 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/macos-app/CodexQuota.swift` around lines 39 - 41, The loadCache()
path must clear published state when backing files disappear or are empty: when
json("openai-codex-accounts.json") returns nil or an empty structure, set
accounts = [:] (or the empty collection type used) and lastUpdated = nil instead
of returning early; when computing newest from the quota snapshot (the logic
around newest and the cache handling near the existing newest == 0 branch), set
lastUpdated = nil whenever newest == 0; update any cache assignment logic for
json("quota-cache.json") to tolerate missing/empty caches but not preserve prior
published state. Add a unit/integration regression test that loads successfully,
then deletes or corrupts openai-codex-accounts.json (and/or switches
CODEX_MULTI_AUTH_DIR) and asserts accounts is empty and lastUpdated is nil after
calling loadCache().
| let cache = json("quota-cache.json") ?? [:] | ||
| let observ = json("runtime-observability.json") ?? [:] | ||
| let byId = cache["byAccountId"] as? [String: Any] ?? [:] | ||
|
|
||
| let storeAccounts = store["accounts"] as? [[String: Any]] ?? [] | ||
| let order = storeAccounts.compactMap { $0["accountId"] as? String } | ||
|
|
||
| var activeId = observ["lastAccountId"] as? String | ||
| if activeId == nil || !order.contains(activeId!) { | ||
| if let idx = store["activeIndex"] as? Int, idx >= 0, idx < order.count { | ||
| activeId = order[idx] | ||
| } | ||
| } | ||
|
|
||
| func window(_ entry: [String: Any]?, _ key: String) -> QuotaWindow? { | ||
| guard let w = entry?[key] as? [String: Any] else { return nil } | ||
| let used = (w["usedPercent"] as? NSNumber)?.intValue ?? 0 | ||
| let reset = (w["resetAtMs"] as? NSNumber)?.doubleValue | ||
| return QuotaWindow(remaining: 100 - used, resetAtMs: reset) | ||
| } | ||
|
|
||
| var newest: Double = 0 | ||
| var result: [AccountQuota] = [] | ||
| for acc in storeAccounts { | ||
| guard let id = acc["accountId"] as? String else { continue } | ||
| let email = acc["email"] as? String ?? id | ||
| let name = email.split(separator: "@").first.map(String.init) ?? email | ||
| let entry = byId[id] as? [String: Any] | ||
| if let u = (entry?["updatedAt"] as? NSNumber)?.doubleValue { newest = max(newest, u) } | ||
| result.append(AccountQuota( | ||
| id: id, | ||
| name: name, | ||
| isActive: id == activeId, | ||
| fiveHour: window(entry, "primary"), | ||
| sevenDay: window(entry, "secondary") |
There was a problem hiding this comment.
mirror the cache's email fallback when resolving quota entries.
line 68 only checks byAccountId. the persisted quota cache also carries byEmail, and the main cli display path resolves through that fallback before rendering quota state. with the current lookup, migrated or re-keyed accounts can render no data here even though the canonical ui still has quota for the same account. please add a regression test for a cache file that only matches on normalized email. see lib/quota-cache.ts:9-36 and lib/codex-manager/login-menu-data.ts:430-468.
proposed fix
let cache = json("quota-cache.json") ?? [:]
let observ = json("runtime-observability.json") ?? [:]
let byId = cache["byAccountId"] as? [String: Any] ?? [:]
+ let byEmail = cache["byEmail"] as? [String: Any] ?? [:]
+
+ func normalizeEmailKey(_ email: String) -> String {
+ email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ }
@@
guard let id = acc["accountId"] as? String else { continue }
let email = acc["email"] as? String ?? id
let name = email.split(separator: "@").first.map(String.init) ?? email
- let entry = byId[id] as? [String: Any]
+ let entry =
+ (byId[id] as? [String: Any]) ??
+ (byEmail[normalizeEmailKey(email)] as? [String: Any])
if let u = (entry?["updatedAt"] as? NSNumber)?.doubleValue { newest = max(newest, u) }as per coding guidelines, email dedup is case-insensitive via normalizeEmailKey() (trim + lowercase).
🧰 Tools
🪛 SwiftLint (0.63.3)
[Warning] 55-55: Prefer empty collection over optional collection
(discouraged_optional_collection)
🤖 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/macos-app/CodexQuota.swift` around lines 41 - 75, The quota lookup
currently only checks the cache map byId (byAccountId) and misses entries stored
under the cache's byEmail key; update the construction around the for-loop
(where byId is read and each entry is retrieved into entry) to first attempt
lookup from byId[id], then if nil attempt lookup from the cache's byEmail using
the normalized email key (use normalizeEmailKey(email) to match CLI behavior) so
migrated/re-keyed accounts find their quota; keep using the existing
window(_:,_) and AccountQuota(...) flow and add a regression test that loads a
quota-cache.json containing only byEmail entries (with mixed-case emails) to
assert quota is found for the account after normalization.
Source: Coding guidelines
| cp contrib/macos-app/local.codex.quota.plist ~/Library/LaunchAgents/ | ||
| launchctl load ~/Library/LaunchAgents/local.codex.quota.plist |
There was a problem hiding this comment.
fix install instructions to always replace home_placeholder.
current install steps can produce a broken launchagent because line 19 requires placeholder substitution. use the same one-liner pattern documented in contrib/macos-app/README.md.
proposed fix
- Install:
- cp contrib/macos-app/local.codex.quota.plist ~/Library/LaunchAgents/
- launchctl load ~/Library/LaunchAgents/local.codex.quota.plist
+ Install:
+ sed "s|HOME_PLACEHOLDER|$HOME|" contrib/macos-app/local.codex.quota.plist \
+ > ~/Library/LaunchAgents/local.codex.quota.plist
+ launchctl load ~/Library/LaunchAgents/local.codex.quota.plistAlso applies to: 19-19
🤖 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/macos-app/local.codex.quota.plist` around lines 6 - 7, The install
steps copy the plist without substituting the home_placeholder token, producing
a broken LaunchAgent (placeholder used by local.codex.quota.plist around line
with home_placeholder). Update the instructions so the plist is written with the
placeholder replaced (use the same one-liner pattern from
contrib/macos-app/README.md) — i.e., perform an inline substitution of
"home_placeholder" with "$HOME" (or the actual home path) before placing the
file into ~/Library/LaunchAgents/, then run launchctl load on the substituted
file; ensure the sequence replaces the token prior to calling launchctl load.
|
|
||
| - macOS 13 (Ventura) or newer | ||
| - Swift toolchain — Xcode or the Command Line Tools (`xcode-select --install`) | ||
| - `codex-multi-auth` on `PATH` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
path requirement is narrower than stated.
line 52 says codex-multi-auth must be "on PATH", but contrib/macos-app/CodexQuota.swift:95 hardcodes PATH=/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH. if a user installed codex-multi-auth to a custom prefix (e.g., ~/.local/bin or a non-standard npm prefix), the subprocess won't find it even though their interactive shell PATH includes that location.
recommend either:
- document the explicit PATH construction here so users know which install paths are supported, or
- note that the app uses a fixed PATH and may not honor all shell configurations.
🤖 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/macos-app/README.md` at line 52, The README claim that
`codex-multi-auth` just needs to be "on PATH" is misleading because
contrib/macos-app/CodexQuota.swift hardcodes PATH at the top of the spawned
process to "/usr/local/bin:/opt/homebrew/bin:$HOME/.npm-global/bin:$PATH", so
installs in custom prefixes (e.g., ~/.local/bin) will be ignored; fix by either
(A) updating the README to explicitly state the app uses that fixed PATH and
list the exact directories included (referencing CodexQuota.swift and the
hardcoded PATH string) or (B) change the app to respect the user’s environment
PATH by removing the hardcoded PATH override in CodexQuota.swift (or
appending/prepending additional common prefixes like ~/.local/bin) so
subprocesses can find user-installed `codex-multi-auth`.
| ## Build & install | ||
|
|
||
| ```bash | ||
| contrib/macos-app/build.sh # compiles and installs ~/Applications/CodexQuota.app |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
build.sh accepts an optional install path override.
line 57 shows the default ~/Applications/CodexQuota.app, but contrib/macos-app/build.sh:6 accepts an optional first argument to override the install location (APP="${1:-$HOME/Applications/CodexQuota.app}"). consider mentioning this for users who want a different install path:
contrib/macos-app/build.sh /Applications/CodexQuota.app # install system-wide🤖 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/macos-app/README.md` at line 57, Update the README entry for
contrib/macos-app/build.sh to mention that build.sh accepts an optional install
path override via its first argument (see
APP="${1:-$HOME/Applications/CodexQuota.app}" in build.sh), and include an
example showing how to pass a custom path (e.g., using
/Applications/CodexQuota.app for system-wide install) so users know they can
override the default ~/Applications/CodexQuota.app.
What
A native macOS menu bar app (
contrib/macos-app/) showing per-account Codex quota cards, as an alternative to the SwiftBar plugin in #603.Why, on top of the SwiftBar plugin
SwiftBar/xbar render the dropdown as an
NSMenu, and macOS does not repaint anNSMenuwhile it is held open (menu tracking mode). So a probe fired on open can't visibly update the panel — you must close and reopen to see fresh numbers. That's a real wart for a "check my quota" widget.This app uses SwiftUI
MenuBarExtrawith.menuBarExtraStyle(.window), which renders the panel as a window. On open it shows cached values instantly, runs a backgroundcodex-multi-auth check, and updates the cards in place when it returns. Same data source as the plugin — just a UI that can update while you're looking at it.Contents
CodexQuota.swift— the app (~240 lines, no dependencies)build.sh— compiles withswiftcand assembles a minimal ad-hoc-signed.app(no Xcode project required)local.codex.quota.plist— optional login autostart LaunchAgentREADME.md— build/install steps and the NSMenu-vs-window rationaleBehavior
⚡<n>%: active account's 5h remaining (active resolved from runtime observability, falling back to the stored active index)Notes
~/.codex/multi-auth/, honorsCODEX_MULTI_AUTH_DIR); fails soft (⚡?) on missing fieldscontrib/is outside the npmfileswhitelist, so the published package is unchangedComplements rather than replaces #603 — some users prefer dropping a script into an existing SwiftBar setup, others prefer a standalone app that updates live. Happy to land only one if you'd rather not carry both.
🤖 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 native swiftui menu bar app (
contrib/macos-app/) as a standalone alternative to the swiftbar plugin, showing per-account codex quota cards with live in-place updates while the panel is open.CodexQuota.swift(~254 lines, no dependencies) reads the same local cache files as the swiftbar plugin and spawnscodex-multi-auth checkin a background process for live probing;refreshIfStalenow correctly uses cache file mtime (synchronous) rather than the async-committedlastUpdated.build.shcompiles withswiftcand assembles an ad-hoc-signed.appbundle;local.codex.quota.plistprovides an optional login autostart launchagent with aHOME_PLACEHOLDERthat users substitute via the documentedsedone-liner.Confidence Score: 3/5
the panel live-update story works correctly, but the menu bar title is frozen while the panel is closed, and a launch failure in refresh() can crash the app
the 60s timer in CodexQuotaApp is declared but never connected via onReceive — model.loadCache() is never called from it, so the menu bar percentage is stale from the moment the panel is closed. separately, try? task.run() followed by unconditional waitUntilExit() is a crash path if the process launch is denied. both issues affect the primary code file and the core user-facing behaviour described in the pr.
contrib/macos-app/CodexQuota.swift — the timer wiring in CodexQuotaApp and the try?/waitUntilExit pattern in refresh() both need attention before this lands
Important Files Changed
Sequence Diagram
sequenceDiagram participant App as CodexQuotaApp (init) participant Model as QuotaModel participant FS as Local Cache Files participant View as QuotaView (onAppear) participant Proc as /bin/zsh codex-multi-auth check App->>Model: loadCache() Model->>FS: read cache files FS-->>Model: data Model-->>App: DispatchQueue.main.async accounts updated View->>Model: refreshIfStale() Model->>FS: cacheFileMTime() alt "mtime < 60s" Model-->>View: return cache fresh else "mtime >= 60s" Model->>Proc: Process run via DispatchQueue.global Proc-->>Model: waitUntilExit Model->>FS: loadCache FS-->>Model: updated data Model-->>View: DispatchQueue.main.async accounts updated end Note over App: tick timer 60s declared but never subscribed title won't auto-refreshPrompt To Fix All With AI
Reviews (2): Last reviewed commit: "fix(contrib): address Greptile review on..." | Re-trigger Greptile