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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CodexBar 🎚️ - May your tokens never run out.

Tiny macOS 14+ menu bar app that keeps your Codex, Claude, Cursor, Gemini, Antigravity, Droid (Factory), Copilot, z.ai, Kiro, Vertex AI, Augment, Amp, and JetBrains AI limits visible (session + weekly where available) and shows when each window resets. One status item per provider (or Merge Icons mode); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.
Tiny macOS 14+ menu bar app that keeps your Codex, Claude, Cursor, Gemini, Antigravity, Windsurf, Droid (Factory), Copilot, z.ai, Kiro, Vertex AI, Augment, Amp, and JetBrains AI limits visible (session + weekly where available) and shows when each window resets. One status item per provider (or Merge Icons mode); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.

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

Expand Down Expand Up @@ -36,6 +36,7 @@ Linux support via Omarchy: community Waybar module and TUI, driven by the `codex
- [Cursor](docs/cursor.md) — Browser session cookies for plan + usage + billing resets.
- [Gemini](docs/gemini.md) — OAuth-backed quota API using Gemini CLI credentials (no browser cookies).
- [Antigravity](docs/antigravity.md) — Local language server probe (experimental); no external auth.
- [Windsurf](docs/windsurf.md) — Local Windsurf plan cache (state.vscdb); messages/actions/credits.
- [Droid](docs/factory.md) — Browser cookies + WorkOS token flows for Factory usage + billing.
- [Copilot](docs/copilot.md) — GitHub device flow + Copilot internal usage API.
- [z.ai](docs/zai.md) — API token (Keychain) for quota + MCP windows.
Expand Down
68 changes: 28 additions & 40 deletions Sources/CodexBar/IconRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -662,51 +662,39 @@ enum IconRenderer {
remaining: topValue,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addGeminiTwist: style == .gemini || style == .antigravity || style == .windsurf,
addAntigravityTwist: style == .antigravity || style == .windsurf,
addFactoryTwist: style == .factory,
addWarpTwist: style == .warp,
blink: blink)
drawBar(rectPx: bottomRectPx, remaining: bottomValue)
} else if !hasWeekly || warpNoBonus {
if style == .warp {
// Warp: no bonus or bonus exhausted -> top=monthly credits, bottom=dimmed track
} else if !hasWeekly {
// Weekly missing (e.g. Claude enterprise): keep normal layout but
// dim the bottom track to indicate N/A.
if topValue == nil, let ratio = creditsRatio {
// Credits-only: show credits prominently (e.g. credits loaded before usage).
drawBar(
rectPx: creditsRectPx,
remaining: ratio,
alpha: creditsAlpha,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addFactoryTwist: style == .factory,
blink: blink)
drawBar(rectPx: creditsBottomRectPx, remaining: nil, alpha: 0.45)
} else {
drawBar(
rectPx: topRectPx,
remaining: topValue,
addWarpTwist: true,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addFactoryTwist: style == .factory,
blink: blink)
drawBar(rectPx: bottomRectPx, remaining: nil, alpha: 0.45)
} else {
// Weekly missing (e.g. Claude enterprise): keep normal layout but
// dim the bottom track to indicate N/A.
if topValue == nil, let ratio = creditsRatio {
// Credits-only: show credits prominently (e.g. credits loaded before usage).
drawBar(
rectPx: creditsRectPx,
remaining: ratio,
alpha: creditsAlpha,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addFactoryTwist: style == .factory,
addWarpTwist: style == .warp,
blink: blink)
drawBar(rectPx: creditsBottomRectPx, remaining: nil, alpha: 0.45)
} else {
drawBar(
rectPx: topRectPx,
remaining: topValue,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addFactoryTwist: style == .factory,
addWarpTwist: style == .warp,
blink: blink)
drawBar(rectPx: bottomRectPx, remaining: nil, alpha: 0.45)
}
}
} else {
// Weekly exhausted/missing: show credits on top (thicker), weekly (likely 0) on bottom.
Expand All @@ -717,8 +705,8 @@ enum IconRenderer {
alpha: creditsAlpha,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addGeminiTwist: style == .gemini || style == .antigravity || style == .windsurf,
addAntigravityTwist: style == .antigravity || style == .windsurf,
addFactoryTwist: style == .factory,
addWarpTwist: style == .warp,
blink: blink)
Expand All @@ -729,8 +717,8 @@ enum IconRenderer {
remaining: topValue,
addNotches: style == .claude,
addFace: style == .codex,
addGeminiTwist: style == .gemini || style == .antigravity,
addAntigravityTwist: style == .antigravity,
addGeminiTwist: style == .gemini || style == .antigravity || style == .windsurf,
addAntigravityTwist: style == .antigravity || style == .windsurf,
addFactoryTwist: style == .factory,
addWarpTwist: style == .warp,
blink: blink)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum ProviderImplementationRegistry {
case .factory: FactoryProviderImplementation()
case .gemini: GeminiProviderImplementation()
case .antigravity: AntigravityProviderImplementation()
case .windsurf: WindsurfProviderImplementation()
case .copilot: CopilotProviderImplementation()
case .zai: ZaiProviderImplementation()
case .minimax: MiniMaxProviderImplementation()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct WindsurfProviderImplementation: ProviderImplementation {
let id: UsageProvider = .windsurf
}
3 changes: 3 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-windsurf.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 38 additions & 42 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,8 @@ extension UsageStore {
let keepCLISessionsAlive = self.settings.debugKeepCLISessionsAlive
let cursorCookieSource = self.settings.cursorCookieSource
let cursorCookieHeader = self.settings.cursorCookieHeader
let ampCookieSource = self.settings.ampCookieSource
let ampCookieHeader = self.settings.ampCookieHeader
return await Task.detached(priority: .utility) { () -> String in
switch provider {
case .codex:
Expand Down Expand Up @@ -1179,32 +1181,12 @@ extension UsageStore {
let text = "SYNTHETIC_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
await MainActor.run { self.probeLogs[.synthetic] = text }
return text
case .gemini:
let text = "Gemini debug log not yet implemented"
await MainActor.run { self.probeLogs[.gemini] = text }
return text
case .antigravity:
let text = "Antigravity debug log not yet implemented"
await MainActor.run { self.probeLogs[.antigravity] = text }
return text
case .cursor:
let text = await self.debugCursorLog(
cursorCookieSource: cursorCookieSource,
cursorCookieHeader: cursorCookieHeader)
await MainActor.run { self.probeLogs[.cursor] = text }
return text
case .opencode:
let text = "OpenCode debug log not yet implemented"
await MainActor.run { self.probeLogs[.opencode] = text }
return text
case .factory:
let text = "Droid debug log not yet implemented"
await MainActor.run { self.probeLogs[.factory] = text }
return text
case .copilot:
let text = "Copilot debug log not yet implemented"
await MainActor.run { self.probeLogs[.copilot] = text }
return text
case .minimax:
let tokenResolution = ProviderTokenResolver.minimaxTokenResolution()
let cookieResolution = ProviderTokenResolver.minimaxCookieResolution()
Expand All @@ -1215,47 +1197,61 @@ extension UsageStore {
"source=\(cookieSource)"
await MainActor.run { self.probeLogs[.minimax] = text }
return text
case .vertexai:
let text = "Vertex AI debug log not yet implemented"
await MainActor.run { self.probeLogs[.vertexai] = text }
return text
case .kiro:
let text = "Kiro debug log not yet implemented"
await MainActor.run { self.probeLogs[.kiro] = text }
return text
case .augment:
let text = await self.debugAugmentLog()
await MainActor.run { self.probeLogs[.augment] = text }
return text
case .kimi:
let text = "Kimi debug log not yet implemented"
await MainActor.run { self.probeLogs[.kimi] = text }
return text
case .kimik2:
let text = "Kimi K2 debug log not yet implemented"
await MainActor.run { self.probeLogs[.kimik2] = text }
return text
case .amp:
let text = await self.debugAmpLog(
ampCookieSource: self.settings.ampCookieSource,
ampCookieHeader: self.settings.ampCookieHeader)
ampCookieSource: ampCookieSource,
ampCookieHeader: ampCookieHeader)
await MainActor.run { self.probeLogs[.amp] = text }
return text
case .jetbrains:
let text = "JetBrains AI debug log not yet implemented"
await MainActor.run { self.probeLogs[.jetbrains] = text }
return text
case .warp:
let resolution = ProviderTokenResolver.warpResolution()
let hasAny = resolution != nil
let source = resolution?.source.rawValue ?? "none"
let text = "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
await MainActor.run { self.probeLogs[.warp] = text }
return text
case .gemini, .antigravity, .windsurf, .opencode, .factory, .copilot, .vertexai, .kiro, .kimi, .kimik2,
.jetbrains:
let text = Self.notImplementedDebugLogText(for: provider)
await MainActor.run { self.probeLogs[provider] = text }
return text
}
}.value
}

private nonisolated static func notImplementedDebugLogText(for provider: UsageProvider) -> String {
switch provider {
case .gemini:
"Gemini debug log not yet implemented"
case .antigravity:
"Antigravity debug log not yet implemented"
case .windsurf:
"Windsurf debug log not yet implemented"
case .opencode:
"OpenCode debug log not yet implemented"
case .factory:
"Droid debug log not yet implemented"
case .copilot:
"Copilot debug log not yet implemented"
case .vertexai:
"Vertex AI debug log not yet implemented"
case .kiro:
"Kiro debug log not yet implemented"
case .kimi:
"Kimi debug log not yet implemented"
case .kimik2:
"Kimi K2 debug log not yet implemented"
case .jetbrains:
"JetBrains AI debug log not yet implemented"
case .codex, .claude, .cursor, .zai, .minimax, .augment, .amp, .synthetic, .warp:
"Debug log not yet implemented"
}
}

private func debugClaudeLog(
claudeWebExtrasEnabled: Bool,
claudeUsageDataSource: ClaudeUsageDataSource,
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ struct TokenAccountCLIContext {
return self.makeSnapshot(
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(
ideBasePath: nil))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .warp:
case .gemini, .antigravity, .windsurf, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .warp:
return nil
}
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/ProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public enum ProviderDescriptorRegistry {
.factory: FactoryProviderDescriptor.descriptor,
.gemini: GeminiProviderDescriptor.descriptor,
.antigravity: AntigravityProviderDescriptor.descriptor,
.windsurf: WindsurfProviderDescriptor.descriptor,
.copilot: CopilotProviderDescriptor.descriptor,
.zai: ZaiProviderDescriptor.descriptor,
.minimax: MiniMaxProviderDescriptor.descriptor,
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCore/Providers/Providers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case factory
case gemini
case antigravity
case windsurf
case copilot
case zai
case minimax
Expand All @@ -33,6 +34,7 @@ public enum IconStyle: Sendable, CaseIterable {
case minimax
case gemini
case antigravity
case windsurf
case cursor
case opencode
case factory
Expand Down
Loading