Skip to content

Commit cd201b9

Browse files
enieuwysteipete
andauthored
Add Antigravity agy CLI fallback (#1313)
* Add Antigravity CLI usage source * Fix Antigravity session record ownership * Scope ambient Antigravity probes to selected account In auto mode the local desktop and agy CLI probes report whichever account is signed into the local session, which can differ from a selected token account. Validate ambient snapshots against the selected account's identity and fall through to the account-scoped OAuth fetch on mismatch. Explicit cli/oauth source modes stay authoritative. * Scope desktop-local probe to IDE-only, keep serve agy session warm, add serve last-good cache Fixes three flakiness sources in the Antigravity pipeline and codexbar serve: 1. Desktop-local probe attached to any Antigravity process, including the agy CLI language server owned by AntigravityCLIHTTPSFetchStrategy. A stale/initializing agy accepts the connection but fails GetUserStatus, burning the probe timeout before the CLI strategy's readiness loop runs. Add AntigravityStatusProbe.ProcessScope and scope the local strategy to .ideOnly; it now only handles the running-desktop case and otherwise fails over to the CLI strategy. isRunning() keeps .ideAndCLI for status. 2. codexbar serve reset the warm agy session after every refresh because shouldResetSessionAfterFetch keyed only on runtime == .cli, which serve shares with one-shot CLI usage. Every 60s poll cold-started agy and raced its readiness deadline, timing out. Add ProviderFetchContext.persistsCLISessions (set true by serve via UsageCommandContext.persistCLISessions); shouldResetSessionAfterFetch now keeps the warm session for long-lived hosts and resets only for one-shot CLI. The 180s session idle window covers the 60s refresh. 3. serve discarded the last good payload on transient failures. CLIServeResponseCache now records the last good response per key and serves it for failed refreshes, bounded by serveStaleTTL (ten refresh intervals, five-minute floor; disabled when --refresh-interval 0). Tests: probe ideOnly scope (2), serve last-good fallback + TTL bounds (2), session reset honors persistsCLISessions (extended). * fix: harden Antigravity CLI fallback * fix: harden Antigravity helper process isolation * fix: prevent hidden Antigravity sign-in flows * fix: merge serve stale usage per account * Mark unknown Antigravity model usage * fix: render unknown Antigravity usage safely * fix: preserve Antigravity reset causes * fix: isolate CLI usage fallback by account * ci: refresh exact-head checks * fix: harden Antigravity fallback integration * fix: gate Antigravity CLI fallback on Linux * fix: preserve Antigravity plan debug session * fix: clean up CLI helpers on termination * refactor: split TTY shutdown helpers --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 1bae26b commit cd201b9

41 files changed

Lines changed: 6127 additions & 179 deletions

Some content is hidden

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

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Menu bar: reserve quota-bar space consistently across Overview and provider switcher segments so selection no longer changes segment height (#1445). Thanks @Zihao-Qi!
1414
- Cost usage: accept normal models.dev catalog churn while retaining prior model prices as fallbacks, so newly priced models appear without requiring a manual cache reset (#1438). Thanks @tom-rigelblu!
1515
- Menu bar: anchor merged provider dropdowns to the status item's trailing edge without marking preserved in-flight refresh content fresh, preventing horizontal drift while keeping deferred updates visible (#1288). Thanks @Yuxin-Qiao!
16+
- Antigravity: fall back to the CLI usage server when the desktop app is closed, keep helper sessions owned and bounded without hidden sign-in flows, and show model rows with missing usage as unavailable instead of exhausted (#1313). Thanks @enieuwy!
1617
- Cost usage: replace repeated Foundation metadata/root checks with one portable file-stat pass so expired Codex history refreshes stay responsive on very large session archives (#1392). Thanks @TheAngryPit and @ProspectOre!
1718
- Cursor: show the Safari Full Disk Access recovery hint before the long browser login list so permission guidance remains visible when menu errors truncate (#1419, fixes #1417). Thanks @hhh2210!
1819
- Cursor: present legacy request-based plans as one Requests quota with the raw used/limit count instead of unrelated token-based Auto/API bars (#1420, fixes #1418). Thanks @hhh2210!

Sources/CodexBar/MenuCardView+ModelHelpers.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,18 @@ extension UsageMenuCardView.Model {
211211
provider: input.provider,
212212
window: namedWindow.window,
213213
input: input)
214+
let usageKnown = namedWindow.usageKnown
215+
let resetText = Self.resetText(
216+
for: namedWindow.window,
217+
style: input.resetTimeDisplayStyle,
218+
now: input.now)
219+
let statusText: String? = if usageKnown {
220+
nil
221+
} else if let resetText {
222+
"\(L("Unavailable")) - \(resetText)"
223+
} else {
224+
L("Unavailable")
225+
}
214226
return Metric(
215227
id: namedWindow.id,
216228
title: namedWindow.title,
@@ -219,14 +231,12 @@ extension UsageMenuCardView.Model {
219231
? namedWindow.window.usedPercent
220232
: namedWindow.window.remainingPercent),
221233
percentStyle: percentStyle,
222-
resetText: Self.resetText(
223-
for: namedWindow.window,
224-
style: input.resetTimeDisplayStyle,
225-
now: input.now),
234+
statusText: statusText,
235+
resetText: usageKnown ? resetText : nil,
226236
detailText: nil,
227-
detailLeftText: paceDetail?.leftLabel,
228-
detailRightText: paceDetail?.rightLabel,
229-
pacePercent: paceDetail?.pacePercent,
237+
detailLeftText: usageKnown ? paceDetail?.leftLabel : nil,
238+
detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
239+
pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
230240
paceOnTop: paceDetail?.paceOnTop ?? true)
231241
}
232242
}

Sources/CodexBar/PreferencesProviderDetailView.swift

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import CodexBarCore
22
import SwiftUI
33

4+
enum ProviderMetricInlinePresentation: Equatable {
5+
case progress
6+
case status(String)
7+
}
8+
49
@MainActor
510
struct ProviderDetailView<SupplementaryContent: View>: View {
611
let provider: UsageProvider
@@ -63,6 +68,15 @@ struct ProviderDetailView<SupplementaryContent: View>: View {
6368
L(UsageMenuCardView.popupMetricTitle(provider: provider, metric: metric))
6469
}
6570

71+
static func metricInlinePresentation(
72+
_ metric: UsageMenuCardView.Model.Metric) -> ProviderMetricInlinePresentation
73+
{
74+
if let statusText = metric.statusText {
75+
return .status(statusText)
76+
}
77+
return .progress
78+
}
79+
6680
static func planRow(provider: UsageProvider, planText: String?) -> (label: String, value: String)? {
6781
guard let rawPlan = planText?.trimmingCharacters(in: .whitespacesAndNewlines),
6882
!rawPlan.isEmpty
@@ -455,50 +469,57 @@ private struct ProviderMetricInlineRow: View {
455469
.frame(width: self.labelWidth, alignment: .leading)
456470

457471
VStack(alignment: .leading, spacing: 4) {
458-
UsageProgressBar(
459-
percent: self.metric.percent,
460-
tint: self.progressColor,
461-
accessibilityLabel: self.metric.percentStyle.accessibilityLabel,
462-
pacePercent: self.metric.pacePercent,
463-
paceOnTop: self.metric.paceOnTop,
464-
warningMarkerPercents: self.metric.warningMarkerPercents)
465-
.frame(minWidth: ProviderSettingsMetrics.metricBarWidth, maxWidth: .infinity)
466-
467-
HStack(alignment: .firstTextBaseline, spacing: 8) {
468-
Text(self.metric.percentLabel)
472+
switch ProviderDetailView<EmptyView>.metricInlinePresentation(self.metric) {
473+
case let .status(statusText):
474+
Text(statusText)
469475
.font(.footnote)
470476
.foregroundStyle(.secondary)
471-
.monospacedDigit()
472-
Spacer(minLength: 8)
473-
if let resetText = self.metric.resetText, !resetText.isEmpty {
474-
Text(resetText)
475-
.font(.footnote)
476-
.foregroundStyle(.secondary)
477-
}
478-
}
477+
case .progress:
478+
UsageProgressBar(
479+
percent: self.metric.percent,
480+
tint: self.progressColor,
481+
accessibilityLabel: self.metric.percentStyle.accessibilityLabel,
482+
pacePercent: self.metric.pacePercent,
483+
paceOnTop: self.metric.paceOnTop,
484+
warningMarkerPercents: self.metric.warningMarkerPercents)
485+
.frame(minWidth: ProviderSettingsMetrics.metricBarWidth, maxWidth: .infinity)
479486

480-
let hasLeftDetail = self.metric.detailLeftText?.isEmpty == false
481-
let hasRightDetail = self.metric.detailRightText?.isEmpty == false
482-
if hasLeftDetail || hasRightDetail {
483487
HStack(alignment: .firstTextBaseline, spacing: 8) {
484-
if let leftDetail = self.metric.detailLeftText, !leftDetail.isEmpty {
485-
Text(leftDetail)
486-
.font(.footnote)
487-
.foregroundStyle(.secondary)
488-
}
488+
Text(self.metric.percentLabel)
489+
.font(.footnote)
490+
.foregroundStyle(.secondary)
491+
.monospacedDigit()
489492
Spacer(minLength: 8)
490-
if let rightDetail = self.metric.detailRightText, !rightDetail.isEmpty {
491-
Text(rightDetail)
493+
if let resetText = self.metric.resetText, !resetText.isEmpty {
494+
Text(resetText)
492495
.font(.footnote)
493496
.foregroundStyle(.secondary)
494497
}
495498
}
496-
}
497499

498-
if let detail = self.detailText, !detail.isEmpty {
499-
Text(detail)
500-
.font(.footnote)
501-
.foregroundStyle(.tertiary)
500+
let hasLeftDetail = self.metric.detailLeftText?.isEmpty == false
501+
let hasRightDetail = self.metric.detailRightText?.isEmpty == false
502+
if hasLeftDetail || hasRightDetail {
503+
HStack(alignment: .firstTextBaseline, spacing: 8) {
504+
if let leftDetail = self.metric.detailLeftText, !leftDetail.isEmpty {
505+
Text(leftDetail)
506+
.font(.footnote)
507+
.foregroundStyle(.secondary)
508+
}
509+
Spacer(minLength: 8)
510+
if let rightDetail = self.metric.detailRightText, !rightDetail.isEmpty {
511+
Text(rightDetail)
512+
.font(.footnote)
513+
.foregroundStyle(.secondary)
514+
}
515+
}
516+
}
517+
518+
if let detail = self.detailText, !detail.isEmpty {
519+
Text(detail)
520+
.font(.footnote)
521+
.foregroundStyle(.tertiary)
522+
}
502523
}
503524
}
504525
.frame(maxWidth: .infinity, alignment: .leading)

Sources/CodexBar/ProviderRegistry.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,21 @@ struct ProviderRegistry {
8181
}
8282
}
8383
},
84-
costUsageHistoryDays: settings.costUsageHistoryDays)
84+
costUsageHistoryDays: settings.costUsageHistoryDays,
85+
persistsCLISessions: true,
86+
persistentCLISessionIdleWindow: Self.persistentCLISessionIdleWindow(
87+
refreshInterval: settings.refreshFrequency.seconds))
8588
})
8689
specs[provider] = spec
8790
}
8891

8992
return specs
9093
}
9194

95+
static func persistentCLISessionIdleWindow(refreshInterval: TimeInterval?) -> TimeInterval {
96+
max(180, (refreshInterval ?? 120) + 60)
97+
}
98+
9299
@MainActor
93100
static func makeSettingsSnapshot(
94101
settings: SettingsStore,

Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ struct AntigravityProviderImplementation: ProviderImplementation {
4242
ProviderSettingsPickerDescriptor(
4343
id: "antigravity-usage-source",
4444
title: "Usage source",
45-
subtitle: "Auto uses the local IDE API first, then Google OAuth when the IDE is closed.",
45+
subtitle: "Auto uses the desktop local API first, then agy CLI, then Google OAuth.",
4646
binding: usageBinding,
4747
options: usageOptions,
4848
isVisible: nil,

Sources/CodexBar/UsageStore+HighestUsage.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ extension UsageStore {
99
var highest: (provider: UsageProvider, usedPercent: Double)?
1010
for provider in self.enabledProviders() {
1111
guard let snapshot = self.snapshots[provider] else { continue }
12-
let window = self.menuBarMetricWindowForHighestUsage(provider: provider, snapshot: snapshot)
13-
let percent = window?.usedPercent ?? 0
12+
guard let window = self.menuBarMetricWindowForHighestUsage(provider: provider, snapshot: snapshot) else {
13+
continue
14+
}
15+
let percent = window.usedPercent
1416
guard !self.shouldExcludeFromHighestUsage(
1517
provider: provider,
1618
snapshot: snapshot,
@@ -49,18 +51,7 @@ extension UsageStore {
4951
// In automatic mode Copilot can have one depleted lane while another still has quota.
5052
return primary.usedPercent >= 100 && secondary.usedPercent >= 100
5153
}
52-
if provider == .cursor,
53-
effectivePreference == .automatic
54-
{
55-
let percents = [
56-
snapshot.primary?.usedPercent,
57-
snapshot.secondary?.usedPercent,
58-
snapshot.tertiary?.usedPercent,
59-
].compactMap(\.self)
60-
guard !percents.isEmpty else { return true }
61-
return percents.allSatisfy { $0 >= 100 }
62-
}
63-
if provider == .antigravity,
54+
if provider == .cursor || provider == .antigravity,
6455
effectivePreference == .automatic
6556
{
6657
let percents = [
@@ -71,6 +62,7 @@ extension UsageStore {
7162
guard !percents.isEmpty else { return true }
7263
return percents.allSatisfy { $0 >= 100 }
7364
}
65+
7466
return true
7567
}
7668
}

Sources/CodexBar/UsageStore+TokenAccounts.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,10 @@ extension UsageStore {
680680
self.settings.stepfunToken = token
681681
}
682682
},
683-
costUsageHistoryDays: self.settings.costUsageHistoryDays)
683+
costUsageHistoryDays: self.settings.costUsageHistoryDays,
684+
persistsCLISessions: true,
685+
persistentCLISessionIdleWindow: ProviderRegistry.persistentCLISessionIdleWindow(
686+
refreshInterval: self.settings.refreshFrequency.seconds))
684687
}
685688

686689
func sourceMode(for provider: UsageProvider) -> ProviderSourceMode {

Sources/CodexBarCLI/CLIEntry.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ enum CodexBarCLI {
3333
Self.bootstrapLogging(path: invocation.path, values: invocation.parsedValues)
3434
switch invocation.path {
3535
case ["usage"]:
36+
let signalMonitor = CLITerminationSignalMonitor { signalNumber in
37+
CLITerminationSignalMonitor.terminateActiveHelpersAndReraise(signalNumber)
38+
}
39+
defer { signalMonitor.cancel() }
3640
await self.runUsage(invocation.parsedValues)
3741
case ["cost"]:
3842
await self.runCost(invocation.parsedValues)
@@ -53,6 +57,10 @@ enum CodexBarCLI {
5357
case ["cache", "clear"]:
5458
self.runCacheClear(invocation.parsedValues)
5559
case ["diagnose"]:
60+
let signalMonitor = CLITerminationSignalMonitor { signalNumber in
61+
CLITerminationSignalMonitor.terminateActiveHelpersAndReraise(signalNumber)
62+
}
63+
defer { signalMonitor.cancel() }
5664
await self.runDiagnose(invocation.parsedValues)
5765
default:
5866
Self.exit(

Sources/CodexBarCLI/CLIErrorReporting.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ extension CodexBarCLI {
4949
static func makeProviderErrorPayload(
5050
provider: UsageProvider,
5151
account: String?,
52+
cacheAccountKey: String? = nil,
5253
source: String,
5354
status: ProviderStatusPayload?,
5455
error: Error,
@@ -57,6 +58,7 @@ extension CodexBarCLI {
5758
ProviderPayload(
5859
provider: provider,
5960
account: account,
61+
cacheAccountKey: cacheAccountKey,
6062
version: nil,
6163
source: source,
6264
status: status,

0 commit comments

Comments
 (0)