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, Droid (Factory), Copilot, z.ai, Kiro, Qwen Code, 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 @@ -38,6 +38,7 @@ Linux support via Omarchy: community Waybar module and TUI, driven by the `codex
- [Antigravity](docs/antigravity.md) — Local language server probe (experimental); no external auth.
- [Droid](docs/factory.md) — Browser cookies + WorkOS token flows for Factory usage + billing.
- [Copilot](docs/copilot.md) — GitHub device flow + Copilot internal usage API.
- [Qwen Code](docs/qwencode.md) — Local Qwen Code session logs; daily request usage.
- [z.ai](docs/zai.md) — API token (Keychain) for quota + MCP windows.
- [Kimi](docs/kimi.md) — Auth token (JWT from `kimi-auth` cookie) for weekly quota + 5‑hour rate limit.
- [Kimi K2](docs/kimi-k2.md) — API key for credit-based usage totals.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct QwenCodeProviderImplementation: ProviderImplementation {
let id: UsageProvider = .qwencode

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "local" }
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.qwenCodeDailyRequestLimit
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.qwencode(context.settings.qwencodeSettingsSnapshot())
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "qwencode-daily-request-limit",
title: "Daily request limit",
subtitle: "Used to compute the daily usage percentage.",
kind: .plain,
placeholder: "2000",
binding: context.stringBinding(\.qwenCodeDailyRequestLimit),
actions: [],
isVisible: nil,
onActivate: nil),
]
}
}
15 changes: 15 additions & 0 deletions Sources/CodexBar/Providers/QwenCode/QwenCodeSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import CodexBarCore
import Foundation

extension SettingsStore {
func qwencodeSettingsSnapshot() -> ProviderSettingsSnapshot.QwenCodeProviderSettings {
ProviderSettingsSnapshot.QwenCodeProviderSettings(
dailyRequestLimit: self.qwencodeRequestLimitValue())
}

private func qwencodeRequestLimitValue() -> Int? {
let raw = self.qwenCodeDailyRequestLimit.trimmingCharacters(in: .whitespacesAndNewlines)
guard !raw.isEmpty else { return nil }
return Int(raw)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ enum ProviderImplementationRegistry {
case .minimax: MiniMaxProviderImplementation()
case .kimi: KimiProviderImplementation()
case .kiro: KiroProviderImplementation()
case .qwencode: QwenCodeProviderImplementation()
case .vertexai: VertexAIProviderImplementation()
case .augment: AugmentProviderImplementation()
case .jetbrains: JetBrainsProviderImplementation()
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-qwencode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ extension SettingsStore {
}
}

var qwenCodeDailyRequestLimit: String {
get { self.defaultsState.qwenCodeDailyRequestLimit }
set {
self.defaultsState.qwenCodeDailyRequestLimit = newValue
self.userDefaults.set(newValue, forKey: "qwenCodeDailyRequestLimit")
}
}

var mergeIcons: Bool {
get { self.defaultsState.mergeIcons }
set {
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ extension SettingsStore {
_ = self.augmentCookieHeader
_ = self.ampCookieHeader
_ = self.copilotAPIToken
_ = self.qwenCodeDailyRequestLimit
_ = self.tokenAccountsByProvider
_ = self.debugLoadingPattern
_ = self.selectedMenuProvider
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ extension SettingsStore {
let openAIWebAccessEnabled = openAIWebAccessDefault ?? true
if openAIWebAccessDefault == nil { userDefaults.set(true, forKey: "openAIWebAccessEnabled") }
let jetbrainsIDEBasePath = userDefaults.string(forKey: "jetbrainsIDEBasePath") ?? ""
let qwenCodeDailyRequestLimit = userDefaults.string(forKey: "qwenCodeDailyRequestLimit") ?? "2000"
let mergeIcons = userDefaults.object(forKey: "mergeIcons") as? Bool ?? true
let switcherShowsIcons = userDefaults.object(forKey: "switcherShowsIcons") as? Bool ?? true
let selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider")
Expand Down Expand Up @@ -237,6 +238,7 @@ extension SettingsStore {
showOptionalCreditsAndExtraUsage: showOptionalCreditsAndExtraUsage,
openAIWebAccessEnabled: openAIWebAccessEnabled,
jetbrainsIDEBasePath: jetbrainsIDEBasePath,
qwenCodeDailyRequestLimit: qwenCodeDailyRequestLimit,
mergeIcons: mergeIcons,
switcherShowsIcons: switcherShowsIcons,
selectedMenuProviderRaw: selectedMenuProviderRaw,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct SettingsDefaultsState: Sendable {
var showOptionalCreditsAndExtraUsage: Bool
var openAIWebAccessEnabled: Bool
var jetbrainsIDEBasePath: String
var qwenCodeDailyRequestLimit: String
var mergeIcons: Bool
var switcherShowsIcons: Bool
var selectedMenuProviderRaw: String?
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,10 @@ extension UsageStore {
let text = "Kiro debug log not yet implemented"
await MainActor.run { self.probeLogs[.kiro] = text }
return text
case .qwencode:
let text = "Qwen Code debug log not yet implemented"
await MainActor.run { self.probeLogs[.qwencode] = text }
return text
case .augment:
let text = await self.debugAugmentLog()
await MainActor.run { self.probeLogs[.augment] = text }
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:
case .gemini, .antigravity, .copilot, .kiro, .qwencode, .vertexai, .kimik2, .synthetic:
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 @@ -65,6 +65,7 @@ public enum ProviderDescriptorRegistry {
.minimax: MiniMaxProviderDescriptor.descriptor,
.kimi: KimiProviderDescriptor.descriptor,
.kiro: KiroProviderDescriptor.descriptor,
.qwencode: QwenCodeProviderDescriptor.descriptor,
.vertexai: VertexAIProviderDescriptor.descriptor,
.augment: AugmentProviderDescriptor.descriptor,
.jetbrains: JetBrainsProviderDescriptor.descriptor,
Expand Down
17 changes: 17 additions & 0 deletions Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public struct ProviderSettingsSnapshot: Sendable {
kimi: KimiProviderSettings? = nil,
augment: AugmentProviderSettings? = nil,
amp: AmpProviderSettings? = nil,
qwencode: QwenCodeProviderSettings? = nil,
jetbrains: JetBrainsProviderSettings? = nil) -> ProviderSettingsSnapshot
{
ProviderSettingsSnapshot(
Expand All @@ -31,6 +32,7 @@ public struct ProviderSettingsSnapshot: Sendable {
kimi: kimi,
augment: augment,
amp: amp,
qwencode: qwencode,
jetbrains: jetbrains)
}

Expand Down Expand Up @@ -157,6 +159,14 @@ public struct ProviderSettingsSnapshot: Sendable {
}
}

public struct QwenCodeProviderSettings: Sendable {
public let dailyRequestLimit: Int?

public init(dailyRequestLimit: Int?) {
self.dailyRequestLimit = dailyRequestLimit
}
}

public struct AmpProviderSettings: Sendable {
public let cookieSource: ProviderCookieSource
public let manualCookieHeader: String?
Expand All @@ -180,6 +190,7 @@ public struct ProviderSettingsSnapshot: Sendable {
public let kimi: KimiProviderSettings?
public let augment: AugmentProviderSettings?
public let amp: AmpProviderSettings?
public let qwencode: QwenCodeProviderSettings?
public let jetbrains: JetBrainsProviderSettings?

public var jetbrainsIDEBasePath: String? {
Expand All @@ -200,6 +211,7 @@ public struct ProviderSettingsSnapshot: Sendable {
kimi: KimiProviderSettings?,
augment: AugmentProviderSettings?,
amp: AmpProviderSettings?,
qwencode: QwenCodeProviderSettings?,
jetbrains: JetBrainsProviderSettings? = nil)
{
self.debugMenuEnabled = debugMenuEnabled
Expand All @@ -215,6 +227,7 @@ public struct ProviderSettingsSnapshot: Sendable {
self.kimi = kimi
self.augment = augment
self.amp = amp
self.qwencode = qwencode
self.jetbrains = jetbrains
}
}
Expand All @@ -231,6 +244,7 @@ public enum ProviderSettingsSnapshotContribution: Sendable {
case kimi(ProviderSettingsSnapshot.KimiProviderSettings)
case augment(ProviderSettingsSnapshot.AugmentProviderSettings)
case amp(ProviderSettingsSnapshot.AmpProviderSettings)
case qwencode(ProviderSettingsSnapshot.QwenCodeProviderSettings)
case jetbrains(ProviderSettingsSnapshot.JetBrainsProviderSettings)
}

Expand All @@ -248,6 +262,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable {
public var kimi: ProviderSettingsSnapshot.KimiProviderSettings?
public var augment: ProviderSettingsSnapshot.AugmentProviderSettings?
public var amp: ProviderSettingsSnapshot.AmpProviderSettings?
public var qwencode: ProviderSettingsSnapshot.QwenCodeProviderSettings?
public var jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings?

public init(debugMenuEnabled: Bool = false, debugKeepCLISessionsAlive: Bool = false) {
Expand All @@ -268,6 +283,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable {
case let .kimi(value): self.kimi = value
case let .augment(value): self.augment = value
case let .amp(value): self.amp = value
case let .qwencode(value): self.qwencode = value
case let .jetbrains(value): self.jetbrains = value
}
}
Expand All @@ -287,6 +303,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable {
kimi: self.kimi,
augment: self.augment,
amp: self.amp,
qwencode: self.qwencode,
jetbrains: self.jetbrains)
}
}
13 changes: 13 additions & 0 deletions Sources/CodexBarCore/Providers/ProviderVersionDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ public enum ProviderVersionDetector {
return nil
}

public static func qwenVersion() -> String? {
guard let path = TTYCommandRunner.which("qwen") else { return nil }
let candidates = [
["--version"],
["-v"],
["version"],
]
for args in candidates {
if let version = Self.run(path: path, args: args) { return version }
}
return nil
}

private static func run(path: String, args: [String]) -> String? {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: path)
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 @@ -15,6 +15,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case minimax
case kimi
case kiro
case qwencode
case vertexai
case augment
case jetbrains
Expand All @@ -39,6 +40,7 @@ public enum IconStyle: Sendable, CaseIterable {
case kimi
case kimik2
case kiro
case qwencode
case vertexai
case augment
case jetbrains
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import CodexBarMacroSupport
import Foundation

@ProviderDescriptorRegistration
@ProviderDescriptorDefinition
public enum QwenCodeProviderDescriptor {
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .qwencode,
metadata: ProviderMetadata(
id: .qwencode,
displayName: "Qwen Code",
sessionLabel: "Requests",
weeklyLabel: "Tokens",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Qwen Code usage",
cliName: "qwen",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: "https://chat.qwen.ai",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .qwencode,
iconResourceName: "ProviderIcon-qwencode",
color: ProviderColor(red: 32 / 255, green: 140 / 255, blue: 220 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Qwen Code cost summary is not supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .cli],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [QwenCodeLocalFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "qwen",
aliases: ["qwen-code"],
versionDetector: { _ in ProviderVersionDetector.qwenVersion() }))
}
}

struct QwenCodeLocalFetchStrategy: ProviderFetchStrategy {
let id: String = "qwencode.local"
let kind: ProviderFetchKind = .localProbe

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
let baseDirectory = QwenCodeUsageProbe.resolveBaseDirectory(env: context.env)
return QwenCodeUsageProbe.projectsDirectoryExists(baseDirectory: baseDirectory)
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
let baseDirectory = QwenCodeUsageProbe.resolveBaseDirectory(env: context.env)
let requestLimit = Self.resolveRequestLimit(settings: context.settings, env: context.env)
let probe = QwenCodeUsageProbe(requestLimit: requestLimit, baseDirectory: baseDirectory)
let snapshot = try probe.fetch()
return self.makeResult(
usage: snapshot.toUsageSnapshot(requestLimit: requestLimit),
sourceLabel: "local")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}

private static func resolveRequestLimit(settings: ProviderSettingsSnapshot?, env: [String: String]) -> Int {
if let override = env["CODEXBAR_QWENCODE_DAILY_REQUEST_LIMIT"],
let limit = Int(override.trimmingCharacters(in: .whitespacesAndNewlines)),
limit > 0
{
return limit
}

if let limit = settings?.qwencode?.dailyRequestLimit, limit > 0 {
return limit
}

return QwenCodeUsageProbe.defaultDailyRequestLimit
}
}
Loading