Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
timeout-minutes: 5
outputs:
macos-tests: ${{ steps.macos-tests.outputs.macos-tests }}
macos-tests-reason: ${{ steps.macos-tests.outputs.macos-tests-reason }}
changed-path-count: ${{ steps.macos-tests.outputs.changed-path-count }}
steps:
- uses: actions/checkout@v6
with:
Expand Down Expand Up @@ -49,6 +51,26 @@ jobs:
sed 's/^/- /' "$changed_paths"
./Scripts/ci_macos_test_gate.sh "$changed_paths"

- name: Summarize macOS test gate
if: ${{ always() }}
shell: bash
env:
MACOS_TESTS: ${{ steps.macos-tests.outputs.macos-tests }}
MACOS_TESTS_REASON: ${{ steps.macos-tests.outputs.macos-tests-reason }}
CHANGED_PATH_COUNT: ${{ steps.macos-tests.outputs.changed-path-count }}
run: |
set -euo pipefail
reason="${MACOS_TESTS_REASON:-<unset>}"
reason="${reason//|/\\|}"
{
printf '### macOS test gate\n\n'
printf '| Field | Value |\n'
printf '| --- | --- |\n'
printf '| macOS Swift tests required | `%s` |\n' "${MACOS_TESTS:-<unset>}"
printf '| Reason | %s |\n' "$reason"
printf '| Changed path entries | `%s` |\n' "${CHANGED_PATH_COUNT:-<unset>}"
} >> "$GITHUB_STEP_SUMMARY"

lint:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand Down Expand Up @@ -105,6 +127,26 @@ jobs:
CODEXBAR_TEST_SHARD_COUNT=${{ matrix.shard-count }} \
./Scripts/test.sh

- name: Summarize macOS shard
if: ${{ always() }}
shell: bash
env:
SHARD_INDEX: ${{ matrix.shard-index }}
SHARD_COUNT: ${{ matrix.shard-count }}
RUNS_LINT_MACOS: ${{ matrix.shard-index == 0 }}
run: |
set -euo pipefail
xcode_version="$(/usr/bin/xcodebuild -version 2>/dev/null | tr '\n' ' ' || true)"
{
printf '### macOS Swift shard\n\n'
printf '| Field | Value |\n'
printf '| --- | --- |\n'
printf '| Shard | `%s / %s` |\n' "$SHARD_INDEX" "$SHARD_COUNT"
printf '| Runner | `%s` |\n' "${RUNNER_NAME:-unknown}"
printf '| Xcode | `%s` |\n' "${xcode_version:-unknown}"
printf '| Runs lint-macos | `%s` |\n' "$RUNS_LINT_MACOS"
} >> "$GITHUB_STEP_SUMMARY"

lint-build-test:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand All @@ -124,6 +166,30 @@ jobs:
"${{ needs.changes.outputs.macos-tests }}" \
"${{ needs.swift-test-macos.result }}"

- name: Summarize aggregate CI gate
if: ${{ always() }}
shell: bash
env:
LINT_RESULT: ${{ needs.lint.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
MACOS_TESTS: ${{ needs.changes.outputs.macos-tests }}
MACOS_TESTS_REASON: ${{ needs.changes.outputs.macos-tests-reason }}
MACOS_RESULT: ${{ needs.swift-test-macos.result }}
run: |
set -euo pipefail
reason="${MACOS_TESTS_REASON:-<unset>}"
reason="${reason//|/\\|}"
{
printf '### Aggregate CI gate\n\n'
printf '| Field | Value |\n'
printf '| --- | --- |\n'
printf '| lint result | `%s` |\n' "$LINT_RESULT"
printf '| changes result | `%s` |\n' "$CHANGES_RESULT"
printf '| macOS Swift tests required | `%s` |\n' "${MACOS_TESTS:-<unset>}"
printf '| macOS gate reason | %s |\n' "$reason"
printf '| swift-test-macos result | `%s` |\n' "$MACOS_RESULT"
} >> "$GITHUB_STEP_SUMMARY"

build-linux-cli:
timeout-minutes: 20
strategy:
Expand Down Expand Up @@ -212,3 +278,22 @@ jobs:
fi
"$BIN" usage --provider codex --web 2>&1 | tee /tmp/codexbarcli-stderr.txt >/dev/null || true
grep -q "macOS" /tmp/codexbarcli-stderr.txt

- name: Summarize Linux CLI build
if: ${{ always() }}
shell: bash
env:
MATRIX_NAME: ${{ matrix.name }}
MATRIX_RUNS_ON: ${{ matrix.runs-on }}
SWIFT_CACHE_HIT: ${{ steps.swift-toolchain-cache.outputs.cache-hit }}
run: |
set -euo pipefail
{
printf '### Linux CLI build\n\n'
printf '| Field | Value |\n'
printf '| --- | --- |\n'
printf '| Matrix | `%s` |\n' "$MATRIX_NAME"
printf '| Runner label | `%s` |\n' "$MATRIX_RUNS_ON"
printf '| Swift version | `%s` |\n' "$SWIFT_VERSION"
printf '| Swift toolchain cache hit | `%s` |\n' "${SWIFT_CACHE_HIT:-false}"
} >> "$GITHUB_STEP_SUMMARY"
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## 0.37.3 — Unreleased

### Fixed
- Memory pressure: finish isolating utility-queue source reads from main-actor state to prevent the remaining callback crash. Thanks @Zihao-Qi!
- Claude: stop installed-version checks from invoking a login shell and triggering unwanted Keychain prompts. Thanks @enieuwy!
- Localization: reject blank translated values and restore the affected Vietnamese provider prompts. Thanks @kiranmagic7!
- Usage totals: keep Today tied to the current local calendar day across cost, Admin API, and Poe surfaces instead of showing the latest historical bucket. Thanks @Zihao-Qi!
- Antigravity: align compact icons and automatic highest-usage selection with grouped Gemini and Claude/GPT 5-hour and weekly lanes while ignoring non-renderable cadences. Thanks @Yuxin-Qiao!

## 0.37.2 — 2026-06-22

### Added
Expand Down
19 changes: 15 additions & 4 deletions Scripts/check-app-locales.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ function formatKeyList(keys, limit = 12) {
return remaining > 0 ? `${shown}, ... +${remaining} more` : shown;
}

function blankKeys(catalog, referenceKeys) {
return referenceKeys.filter((key) => Object.hasOwn(catalog, key) && !catalog[key]?.trim());
}

function swiftInterpolationTokens(value) {
const tokens = [];
for (let index = 0; index < value.length - 1; index += 1) {
Expand Down Expand Up @@ -82,6 +86,10 @@ if (isTest) {
formatKeyList(["alpha", "beta", "gamma", "delta"], 2),
"alpha, beta, ... +2 more",
"truncated key list");
assertEqual(
blankKeys({ alpha: "", beta: " ", gamma: "ok" }, ["alpha", "beta", "gamma", "delta"]),
["alpha", "beta"],
"blank keys");
console.log("app locale checker tests OK");
process.exit(0);
}
Expand All @@ -106,6 +114,7 @@ for (const directory of fs.readdirSync(resources).filter((name) => name.endsWith

checkedCount++;
const catalogKeys = Object.keys(catalog);
const emptyKeys = blankKeys(catalog, englishKeys);

// 1. Missing keys
const missingKeys = englishKeys.filter((key) => !catalogKeys.includes(key));
Expand Down Expand Up @@ -134,15 +143,17 @@ for (const directory of fs.readdirSync(resources).filter((name) => name.endsWith
}
}

if (emptyKeys.length > 0) {
console.error(
`\x1b[31m[${locale}] Error: Blank values for ${emptyKeys.length} keys: ${formatKeyList(emptyKeys)}.\x1b[0m`);
hasErrors = true;
}

// 2. Identical values count
let identicalCount = 0;

for (const key of englishKeys) {
if (!catalog[key]?.trim()) {
if (strictLocales.includes(locale) && catalogKeys.includes(key)) {
console.error(`\x1b[31m[${locale}] Error: Blank value for strict locale key "${key}".\x1b[0m`);
hasErrors = true;
}
continue;
}

Expand Down
8 changes: 8 additions & 0 deletions Scripts/ci_macos_test_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,16 @@ if [[ "$path_count" -eq 0 ]]; then
require_macos_tests '<empty diff>' 'no changed paths were reported'
fi

if [[ "$macos_tests" == true ]]; then
summary_reason="$macos_tests_reason"
else
summary_reason="docs/site-only changes covered by portable checks"
fi

if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
printf 'macos-tests=%s\n' "$macos_tests" >> "$GITHUB_OUTPUT"
printf 'macos-tests-reason=%s\n' "$summary_reason" >> "$GITHUB_OUTPUT"
printf 'changed-path-count=%s\n' "$path_count" >> "$GITHUB_OUTPUT"
fi

if [[ "$macos_tests" == true ]]; then
Expand Down
20 changes: 20 additions & 0 deletions Scripts/test_ci_path_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ assert_gate() {
printf '%s: expected macos-tests=%s, got %s\n' "$name" "$expected" "${actual:-<empty>}" >&2
exit 1
fi

local reason
reason="$(sed -n 's/^macos-tests-reason=//p' "$output_file")"
if [[ -z "$reason" ]]; then
printf '%s: expected macos-tests-reason output\n' "$name" >&2
exit 1
fi

local path_count
path_count="$(sed -n 's/^changed-path-count=//p' "$output_file")"
if ! [[ "$path_count" =~ ^[0-9]+$ ]]; then
printf '%s: expected numeric changed-path-count output, got %s\n' \
"$name" "${path_count:-<empty>}" >&2
exit 1
fi

if [[ "$expected" == false && "$reason" != "docs/site-only changes covered by portable checks" ]]; then
printf '%s: expected docs/site skip reason, got %s\n' "$name" "$reason" >&2
exit 1
fi
}

assert_gate false docs-only $'M\tdocs/providers.md' $'M\tREADME.md'
Expand Down
19 changes: 1 addition & 18 deletions Sources/CodexBar/IconRemainingResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import CodexBarCore
enum IconRemainingResolver {
private static let visibleZeroPercent = 0.0001
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static let antigravityGeminiQuotaBucketIDPrefix = "gemini-"
// Antigravity quota summaries currently expose exact 5-hour session and weekly buckets for the compact icon.
// Antigravity quota summaries expose exact 5-hour session and weekly buckets for the compact icon.
private static let sessionWindowMinutes = 5 * 60
private static let weeklyWindowMinutes = 7 * 24 * 60

Expand Down Expand Up @@ -38,13 +37,6 @@ enum IconRemainingResolver {
} ?? []
guard !quotaSummaryWindows.isEmpty else { return nil }

let geminiWindows = quotaSummaryWindows.filter(Self.isAntigravityGeminiQuotaSummaryWindow)
// The Antigravity menu-bar icon represents Gemini quotas. If any Gemini cadence is present,
// keep missing Gemini lanes empty instead of silently borrowing Claude + GPT quota.
if !geminiWindows.isEmpty {
return self.antigravityQuotaSummaryPair(in: geminiWindows.filter(\.usageKnown))
?? (primary: nil, secondary: nil)
}
return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown))
}

Expand All @@ -58,15 +50,6 @@ enum IconRemainingResolver {
return (primary: session, secondary: weekly)
}

private static func isAntigravityGeminiQuotaSummaryWindow(_ window: NamedRateWindow) -> Bool {
self.antigravityQuotaSummaryBucketID(for: window)?.hasPrefix(self.antigravityGeminiQuotaBucketIDPrefix) == true
}

private static func antigravityQuotaSummaryBucketID(for window: NamedRateWindow) -> String? {
guard window.id.hasPrefix(self.antigravityQuotaSummaryWindowIDPrefix) else { return nil }
return String(window.id.dropFirst(self.antigravityQuotaSummaryWindowIDPrefix.count))
}

/// Returns the highest-usage window for an exact Antigravity compact-icon cadence.
private static func mostConstrainedWindow(in windows: [NamedRateWindow], windowMinutes: Int) -> RateWindow? {
windows
Expand Down
32 changes: 21 additions & 11 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extension UsageMenuCardView.Model {
if input.provider == .poe,
let usage = input.snapshot?.poeUsage
{
return self.poeUsageNotes(usage)
return self.poeUsageNotes(usage, now: input.now)
}

if input.provider == .ollama,
Expand All @@ -89,7 +89,7 @@ extension UsageMenuCardView.Model {
}

static func openAIAPIUsageNotes(_ usage: OpenAIAPIUsageSnapshot) -> [String] {
let today = usage.latestDay
let today = usage.currentDay
let seven = usage.last7Days
let thirty = usage.last30Days
let historyLabel = usage.historyWindowLabel
Expand All @@ -113,8 +113,12 @@ extension UsageMenuCardView.Model {
return notes
}

static func poeUsageNotes(_ usage: PoeUsageHistorySnapshot) -> [String] {
let today = usage.latestDay
static func poeUsageNotes(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> [String]
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let todayUSD = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
Expand Down Expand Up @@ -196,7 +200,7 @@ extension UsageMenuCardView.Model {
let usage = input.snapshot?.poeUsage,
!usage.daily.isEmpty
{
return Self.poeInlineDashboard(usage)
return Self.poeInlineDashboard(usage, now: input.now)
}
if [.codex, .claude, .vertexai, .bedrock].contains(input.provider),
input.tokenCostUsageEnabled,
Expand Down Expand Up @@ -229,8 +233,12 @@ extension UsageMenuCardView.Model {
}
}

static func poeInlineDashboard(_ usage: PoeUsageHistorySnapshot) -> InlineUsageDashboardModel {
let today = usage.latestDay
static func poeInlineDashboard(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> InlineUsageDashboardModel
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let points = usage.daily.suffix(30).map {
Expand Down Expand Up @@ -318,7 +326,9 @@ extension UsageMenuCardView.Model {
value: cost,
accessibilityValue: "\(entry.date): \(Self.costString(cost, currencyCode: snapshot.currencyCode))")
}
let latest = snapshot.daily.max { lhs, rhs in lhs.date < rhs.date }
let latest = CostUsageTokenSnapshot.latestEntry(in: snapshot.daily)
let usesLatestPrimary = provider == .bedrock || provider == .mistral
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
var details: [String] = []
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
Expand All @@ -337,8 +347,8 @@ extension UsageMenuCardView.Model {
valueStyle: Self.costValueStyle(currencyCode: snapshot.currencyCode),
kpis: [
.init(
title: provider == .bedrock || provider == .mistral ? L("Latest") : L("Today"),
value: latest?.costUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—",
title: usesLatestPrimary ? L("Latest") : L("Today"),
value: primaryCostUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "—",
emphasis: true),
.init(
title: historyTitle,
Expand Down Expand Up @@ -378,7 +388,7 @@ extension UsageMenuCardView.Model {
fileprivate static func claudeAdminAPIInlineDashboard(_ usage: ClaudeAdminAPIUsageSnapshot)
-> InlineUsageDashboardModel
{
let today = usage.latestDay
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let points = usage.daily.suffix(30).map {
Expand Down
12 changes: 11 additions & 1 deletion Sources/CodexBar/MemoryPressureMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,24 @@ final class MemoryPressureMonitor {
eventMask: [.warning, .critical],
queue: .global(qos: .utility))
source.setEventHandler(handler: Self.makeEventHandler(
eventReader: { [weak source] in source?.data ?? [] },
source: source,
handle: { [weak self] isWarning, isCritical in
self?.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}))
self.source = source
source.resume()
}

nonisolated static func makeEventHandler(
source: DispatchSourceMemoryPressure,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
self.makeEventHandler(
eventReader: { [weak source] in source?.data ?? [] },
handle: handle)
}

nonisolated static func makeEventHandler(
eventReader: @escaping @Sendable () -> DispatchSource.MemoryPressureEvent,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
Expand Down
Loading
Loading