Skip to content

Commit aff7e91

Browse files
committed
Merge remote-tracking branch 'origin/main' into pr/635
# Conflicts: # CHANGELOG.md
2 parents bb8837b + 5cf2ea4 commit aff7e91

162 files changed

Lines changed: 10956 additions & 983 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ jobs:
4848
--skip CLIOpenAIDashboardCacheTests \
4949
--skip CodexAccountScopedRefreshDashboardCleanupTests \
5050
--skip CodexAccountScopedRefreshTests \
51+
--skip CodexBaselineCharacterizationTests \
5152
--skip CodexDashboardWorkedExampleParityTests \
53+
--skip CodexUsageFetcherFallbackTests \
5254
--skip CodexWebDashboardStrategyAuthorityTests \
5355
--skip OpenAIDashboardNavigationDelegateTests \
5456
--skip StatusMenuTokenAccountSwitcherTests \
5557
--skip SubprocessRunnerTests
5658
swift test --no-parallel \
57-
--filter 'ClaudeOAuthCredentialsStoreSecurityCLITests|CLIOpenAIDashboardCacheTests|CodexAccountScopedRefreshDashboardCleanupTests|CodexAccountScopedRefreshTests|CodexDashboardWorkedExampleParityTests|CodexWebDashboardStrategyAuthorityTests|OpenAIDashboardNavigationDelegateTests|SubprocessRunnerTests'
59+
--filter 'ClaudeOAuthCredentialsStoreSecurityCLITests|CLIOpenAIDashboardCacheTests|CodexAccountScopedRefreshDashboardCleanupTests|CodexAccountScopedRefreshTests|CodexBaselineCharacterizationTests|CodexDashboardWorkedExampleParityTests|CodexUsageFetcherFallbackTests|CodexWebDashboardStrategyAuthorityTests|OpenAIDashboardNavigationDelegateTests|SubprocessRunnerTests'
5860
5961
build-linux-cli:
6062
timeout-minutes: 20

.github/workflows/release-cli.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,54 @@ jobs:
196196
path: |
197197
${{ steps.pkg.outputs.out_dir }}/${{ steps.pkg.outputs.asset }}
198198
${{ steps.pkg.outputs.out_dir }}/${{ steps.pkg.outputs.asset }}.sha256
199+
200+
update-homebrew-tap:
201+
runs-on: ubuntu-latest
202+
needs: build-cli
203+
if: github.event_name == 'release' || inputs.tag != ''
204+
steps:
205+
- name: Resolve release tag
206+
id: release
207+
shell: bash
208+
run: |
209+
set -euo pipefail
210+
tag="${{ inputs.tag || github.ref_name }}"
211+
if [[ -z "$tag" ]]; then
212+
echo "Missing release tag." >&2
213+
exit 1
214+
fi
215+
echo "tag=$tag" >> "$GITHUB_OUTPUT"
216+
echo "request_id=codexbar-${tag}-${GITHUB_RUN_ID}" >> "$GITHUB_OUTPUT"
217+
218+
- name: Dispatch tap update
219+
env:
220+
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
221+
shell: bash
222+
run: |
223+
set -euo pipefail
224+
test -n "$GH_TOKEN"
225+
gh workflow run update-formula.yml \
226+
--repo steipete/homebrew-tap \
227+
-f formula=codexbar \
228+
-f tag="${{ steps.release.outputs.tag }}" \
229+
-f repository=steipete/CodexBar \
230+
-f artifact_template='CodexBarCLI-{tag}-{target}.tar.gz' \
231+
-f target_aliases='darwin_arm64=macos-arm64,darwin_amd64=macos-x86_64,linux_arm64=linux-aarch64,linux_amd64=linux-x86_64' \
232+
-f request_id="${{ steps.release.outputs.request_id }}"
233+
234+
- name: Wait for tap update
235+
env:
236+
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
237+
shell: bash
238+
run: |
239+
set -euo pipefail
240+
for _ in {1..20}; do
241+
run_id="$(gh run list --repo steipete/homebrew-tap --workflow update-formula.yml --json databaseId,displayTitle --jq '.[] | select(.displayTitle | contains("${{ steps.release.outputs.request_id }}")) | .databaseId' | head -n1)"
242+
if [[ -n "$run_id" ]]; then
243+
gh run watch "$run_id" --repo steipete/homebrew-tap --exit-status
244+
exit 0
245+
fi
246+
sleep 5
247+
done
248+
echo "Timed out waiting for tap workflow to appear." >&2
249+
exit 1

AGENTS.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
## Testing Guidelines
2020
- Add/extend XCTest cases under `Tests/CodexBarTests/*Tests.swift` (`FeatureNameTests` with `test_caseDescription` methods).
21-
- Always run `swift test` (or `./Scripts/compile_and_run.sh`) before handoff; add fixtures for new parsing/formatting scenarios.
22-
- After any code change, run `pnpm check` and fix all reported format/lint issues before handoff.
21+
- Always run `swift test` before handoff; add focused filters for parser/provider fixes when possible.
22+
- After any code change, run `make check` and fix all reported format/lint issues before handoff.
23+
- Prefer CLI/focused tests over app-bundle live tests when behavior can be verified without relaunching CodexBar.
2324
- macOS CI is brittle around headless AppKit status/menu tests. Prefer covering menu behavior through stable state/model seams (`MenuDescriptor`, `ProvidersPane`, `CodexAccountsSectionState`, etc.) instead of constructing live `NSStatusBar`/`NSMenu` flows unless the AppKit wiring itself is the thing under test.
2425

2526
## Commit & PR Guidelines
@@ -28,11 +29,10 @@
2829

2930
## Agent Notes
3031
- Use the provided scripts and package manager (SwiftPM); avoid adding dependencies or tooling without confirmation.
31-
- Validate behavior against the freshly built bundle; restart via the pkill+open command above to avoid running stale binaries.
32+
- Validate UI/runtime behavior against the freshly built bundle; restart via the pkill+open command above to avoid running stale binaries.
3233
- To guarantee the right bundle is running after a rebuild, use: `pkill -x CodexBar || pkill -f CodexBar.app || true; cd /Users/steipete/Projects/codexbar && open -n /Users/steipete/Projects/codexbar/CodexBar.app`.
33-
- After any code change that affects the app, always rebuild with `Scripts/package_app.sh` and restart the app using the command above before validating behavior.
34-
- If you edited code, run `scripts/compile_and_run.sh` before handoff; it kills old instances, builds, tests, packages, relaunches, and verifies the app stays running.
35-
- Per user request: after every edit (code or docs), rebuild and restart using `./Scripts/compile_and_run.sh` so the running app reflects the latest changes.
34+
- For CLI-testable provider/parser/settings behavior, use CLI/focused tests instead of `Scripts/package_app.sh` or `./Scripts/compile_and_run.sh`.
35+
- Run `./Scripts/compile_and_run.sh` only when UI/runtime behavior needs bundle-level validation; it builds, tests, packages, relaunches, and verifies the app stays running.
3636
- Release script: keep it in the foreground; do not background it—wait until it finishes.
3737
- Release keys: find in `~/.profile` if missing (Sparkle + App Store Connect).
3838
- Swift concurrency: treat sibling `async let` tasks as a review red flag when one child is required and another is optional/best-effort. Prefer sequential awaits or a drained `withThrowingTaskGroup` that surfaces required failures and explicitly contains optional failures; crash stacks mentioning `swift_task_dealloc` or `asyncLet_finish_after_task_completion` should trigger an audit of nearby `async let` usage.

CHANGELOG.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,30 @@
44

55
### Providers & Usage
66
- Antigravity: add OAuth-backed remote usage fetching so quotas can refresh even when the IDE is closed (#635). Thanks @abnormal749!
7-
7+
- MiniMax: add multi-service quota cards for text, speech, image, video, and music coding-plan usage (#605). Thanks @XWind18!
8+
- Cost history: add an additive models.dev pricing metadata parser/cache pipeline for future provider-scoped cost lookups (#863). Thanks @iam-brain!
9+
- Notifications: add opt-in quota warning notifications, warning markers, and provider-level thresholds for session and weekly quota windows (#852). Thanks @Alekstodo!
10+
- Venice: add API-key balance provider support with DIEM/USD balance display and token-account CLI wiring (#865). Thanks @clawSean!
11+
- Factory/Droid: add token-rate-limit billing windows, Core fallback buckets, and extra usage balance display (#878). Thanks @dantemoon1!
12+
- Usage pace: compute pace for any explicit reset window instead of a provider allowlist (#875). Thanks @ViperThanks!
13+
- Crof: add API-key provider support with request quota and credit balance tracking (#872). Thanks @baanish!
14+
- Command Code: add browser-cookie provider support for monthly USD billing credits (#857). Thanks @sixhobbits!
15+
- StepFun: add username/password or Oasis-Token provider support for Step Plan rate-limit tracking (#815). Thanks @tevenfeng!
16+
- OpenRouter, Mistral, and Kimi K2: show balance/spend metrics in menu bar text when quota percentage is not useful (#853). Thanks @willytop8!
17+
- Usage pace: show session-level pace indicators for Codex and Claude 5-hour windows (#355). Thanks @johnlarkin1!
818
### Fixes
19+
- Startup: avoid blocking menu-bar creation on synchronous defaults migration/default seeding when macOS preferences services stall.
20+
- Cost history: keep manual refreshes on the incremental scanner cache and drain per-line JSON parse allocations so large Codex/Claude histories do not trigger full local log rescans and CPU/memory spikes.
21+
- Cost history: preserve cached models.dev pricing when an upstream catalog only changes a pinned snapshot suffix for the same model family (#883). Thanks @iam-brain!
22+
- Codex: restrict OAuth auto fallback to missing/invalid auth so transient API/decode errors do not spawn `codex app-server` and burn tokens (#876, fixes #874). Thanks @ViperThanks!
23+
- Codex: show official Pro 5x/Pro 20x plan labels instead of Pro Lite/Pro in menu and CLI output (#882). Thanks @xiaoqianWX!
24+
- Accessibility: add VoiceOver labels for status icons, menu rows, provider switcher buttons, and usage charts (#860, fixes #859). Thanks @WadydX!
25+
- Menu bar: keep status items visible on launch by avoiding macOS autosaved hidden menu-extra state from v0.24 (#861).
26+
- Menu: route provider switcher tab clicks through the parent view's mouse tracking so a sub-provider tab still responds after switching back from the Overview tab (#867). Thanks @Karl-Dai!
27+
- Locale: keep relative timestamps in hardcoded-English UI labels consistently English on non-English macOS systems (#868, fixes #866). Thanks @Karl-Dai!
28+
- Droid: fall back to token/allowance math when the Factory API reports a zero ratio despite non-zero usage (#864). Thanks @proxynico!
29+
- OpenRouter: keep the menu bar rendering the usage meter instead of falling back to the provider logo when no key limit is configured (#854). Thanks @willytop8!
30+
- DeepSeek: show balance as plain text instead of a misleading quota-style progress bar (#856). Thanks @jb381!
931
- Menu: keep the status menu open when manually refreshing usage from the menu (#845). Thanks @OlimjonovOtabek!
1032
- Menu bar: remove stale split provider status items instead of hiding them, avoiding leftover second-icon slots on macOS 26.4.
1133
- Augment: report the real 1-minute keepalive check/min-refresh intervals in startup logs and docs (#434). Thanks @guglielmofonda!

Makefile

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
SHELL := /bin/bash
2+
3+
.PHONY: build check docs-list format lint release restart start start-debug start-release stop test test-live test-tty
4+
5+
start:
6+
./Scripts/compile_and_run.sh
7+
8+
start-debug:
9+
./Scripts/compile_and_run.sh
10+
11+
start-release:
12+
./Scripts/package_app.sh release
13+
pkill -x CodexBar || pkill -f CodexBar.app || true
14+
cd /Users/steipete/Projects/codexbar && open -n /Users/steipete/Projects/codexbar/CodexBar.app
15+
16+
restart: start
17+
18+
stop:
19+
pkill -x CodexBar || pkill -f CodexBar.app || true
20+
21+
check lint:
22+
./Scripts/lint.sh lint
23+
24+
format:
25+
./Scripts/lint.sh format
26+
27+
docs-list:
28+
node Scripts/docs-list.mjs
29+
30+
build:
31+
swift build
32+
33+
test:
34+
swift test
35+
36+
test-tty:
37+
swift test --filter TTYIntegrationTests
38+
39+
test-live:
40+
LIVE_TEST=1 swift test --filter LiveAccountTests
41+
42+
release:
43+
./Scripts/package_app.sh release

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# CodexBar 🎚️ - May your tokens never run out.
22

3-
Tiny macOS 14+ menu bar app that keeps AI coding-provider limits visible and shows when each window resets. CodexBar supports Codex, Claude, Cursor, Gemini, Copilot, z.ai, Kiro, Vertex AI, Augment, OpenRouter, Codebuff, and many newer coding providers. One status item per provider (or Merge Icons mode with a provider switcher); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.
3+
Tiny macOS 14+ menu bar app that keeps AI coding-provider limits visible and shows when each window resets. CodexBar supports Codex, Claude, Cursor, Gemini, Copilot, z.ai, Kiro, Vertex AI, Augment, OpenRouter, Codebuff, Command Code, and many newer coding providers. One status item per provider (or Merge Icons mode with a provider switcher); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.
44

55
<img src="codexbar.png" alt="CodexBar menu screenshot" width="520" />
66

@@ -60,7 +60,11 @@ Or download release tarballs from GitHub Releases:
6060
- [Abacus AI](docs/abacus.md) — Browser cookie auth for ChatLLM/RouteLLM compute credit tracking.
6161
- Mistral — Browser cookies for monthly spend tracking.
6262
- [DeepSeek](docs/deepseek.md) — API key for credit balance tracking (paid vs. granted breakdown).
63+
- [Venice](docs/venice.md) — API key for DIEM or USD balance tracking.
6364
- [Codebuff](docs/codebuff.md) — API token (or `~/.config/manicode/credentials.json`) for credit balance + weekly rate limit.
65+
- [Crof](docs/crof.md) — API key for dollar credit balance and request quota tracking.
66+
- [Command Code](docs/command-code.md) — Browser cookies for monthly USD credits from Command Code billing.
67+
- [StepFun](docs/stepfun.md) — Username + password login for Step Plan rate limits (5‑hour + weekly windows) and subscription plan name.
6468
- Open to new providers: [provider authoring guide](docs/provider.md).
6569

6670
## Icon & Screenshot
@@ -139,8 +143,8 @@ Dev loop:
139143
```bash
140144
./Scripts/compile_and_run.sh
141145
./Scripts/compile_and_run.sh --test # also run swift test before packaging/relaunching
142-
pnpm check # SwiftFormat + SwiftLint
143-
pnpm docs:list # list docs with frontmatter summaries
146+
make check # SwiftFormat + SwiftLint
147+
make docs-list # list docs with frontmatter summaries
144148
```
145149

146150
CLI install:

Scripts/generate-llms.mjs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env node
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7+
const docsDir = path.join(repoRoot, "docs");
8+
const cname = fs.readFileSync(path.join(docsDir, "CNAME"), "utf8").trim();
9+
const origin = "https://" + cname;
10+
const productName = "CodexBar";
11+
const productDescription = "CodexBar shows OpenAI Codex and Claude Code usage limits in the macOS menu bar.";
12+
const source = "https://github.com/steipete/CodexBar";
13+
14+
const pages = allHtml(docsDir)
15+
.map((file) => {
16+
const rel = path.relative(docsDir, file).replaceAll(path.sep, "/");
17+
if (rel === "404.html" || rel === "social.html") return null;
18+
const html = fs.readFileSync(file, "utf8");
19+
return {
20+
rel,
21+
title: textContent(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]) || titleize(path.basename(rel, ".html")),
22+
description: attr(html.match(/<meta\s+name=["']description["']\s+content=["']([^"']*)["'][^>]*>/i)?.[1] || ""),
23+
};
24+
})
25+
.filter(Boolean)
26+
.sort((a, b) => (a.rel === "index.html" ? -1 : b.rel === "index.html" ? 1 : a.rel.localeCompare(b.rel)));
27+
28+
const lines = [
29+
"# " + productName,
30+
"",
31+
productDescription,
32+
"",
33+
"Canonical documentation:",
34+
...pages.map((page) => "- " + page.title + ": " + pageUrl(page.rel) + (page.description ? " - " + page.description : "")),
35+
"",
36+
"Source: " + source,
37+
"",
38+
"Guidance for agents:",
39+
"- Prefer the canonical documentation URLs above over README excerpts or package metadata.",
40+
"- Fetch only the pages needed for the current task; this is an index, not a full-site corpus.",
41+
"",
42+
];
43+
44+
fs.writeFileSync(path.join(docsDir, "llms.txt"), lines.join("\n"), "utf8");
45+
console.log("wrote " + path.relative(repoRoot, path.join(docsDir, "llms.txt")));
46+
47+
function allHtml(dir) {
48+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
49+
const full = path.join(dir, entry.name);
50+
if (entry.name === "node_modules" || entry.name.startsWith(".")) return [];
51+
if (entry.isDirectory()) return allHtml(full);
52+
return entry.name.endsWith(".html") ? [full] : [];
53+
});
54+
}
55+
56+
function pageUrl(rel) {
57+
return rel === "index.html" ? origin + "/" : origin + "/" + rel;
58+
}
59+
60+
function textContent(value) {
61+
return attr(value || "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
62+
}
63+
64+
function attr(value) {
65+
return String(value || "")
66+
.replace(/&mdash;/g, "-")
67+
.replace(/&amp;/g, "&")
68+
.replace(/&nbsp;/g, " ")
69+
.replace(/&#39;/g, "'")
70+
.replace(/&quot;/g, '"')
71+
.trim();
72+
}
73+
74+
function titleize(input) {
75+
return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase());
76+
}

Sources/CodexBar/AppNotifications.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ final class AppNotifications {
1919
_ = self.ensureAuthorizationTask()
2020
}
2121

22-
func post(idPrefix: String, title: String, body: String, badge: NSNumber? = nil) {
22+
func post(
23+
idPrefix: String,
24+
title: String,
25+
body: String,
26+
badge: NSNumber? = nil,
27+
soundEnabled: Bool = true)
28+
{
2329
guard !Self.isRunningUnderTests else { return }
2430
let center = self.centerProvider()
2531
let logger = self.logger
@@ -34,7 +40,7 @@ final class AppNotifications {
3440
let content = UNMutableNotificationContent()
3541
content.title = title
3642
content.body = body
37-
content.sound = .default
43+
content.sound = soundEnabled ? .default : nil
3844
content.badge = badge
3945

4046
let request = UNNotificationRequest(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import AppKit
2+
import SwiftUI
3+
4+
struct ClickToCopyOverlay: NSViewRepresentable {
5+
let copyText: String
6+
7+
func makeNSView(context: Context) -> ClickToCopyView {
8+
ClickToCopyView(copyText: self.copyText)
9+
}
10+
11+
func updateNSView(_ nsView: ClickToCopyView, context: Context) {
12+
nsView.copyText = self.copyText
13+
}
14+
}
15+
16+
final class ClickToCopyView: NSView {
17+
var copyText: String
18+
19+
init(copyText: String) {
20+
self.copyText = copyText
21+
super.init(frame: .zero)
22+
self.wantsLayer = false
23+
}
24+
25+
@available(*, unavailable)
26+
required init?(coder: NSCoder) {
27+
fatalError("init(coder:) has not been implemented")
28+
}
29+
30+
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
31+
true
32+
}
33+
34+
override func mouseDown(with event: NSEvent) {
35+
_ = event
36+
let pb = NSPasteboard.general
37+
pb.clearContents()
38+
pb.setString(self.copyText, forType: .string)
39+
}
40+
}

Sources/CodexBar/CostHistoryChartMenuView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ struct CostHistoryChartMenuView: View {
5252
Text("No cost history data.")
5353
.font(.footnote)
5454
.foregroundStyle(.secondary)
55+
.accessibilityLabel("No cost history data available.")
5556
} else {
5657
Chart {
5758
ForEach(model.points) { point in
@@ -81,6 +82,8 @@ struct CostHistoryChartMenuView: View {
8182
}
8283
.chartLegend(.hidden)
8384
.frame(height: 130)
85+
.accessibilityLabel("Cost history chart")
86+
.accessibilityValue(model.points.isEmpty ? "No data" : "\(model.points.count) days of cost data")
8487
.chartOverlay { proxy in
8588
GeometryReader { geo in
8689
ZStack(alignment: .topLeading) {

0 commit comments

Comments
 (0)