Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a4423fc
Add Qwen and Doubao coding plan providers
LeoLin990405 Mar 9, 2026
258871f
Add CI workflow to build CodexBar app artifact
LeoLin990405 Mar 9, 2026
3832f3f
Fix exhaustive switch cases for qwen and doubao
LeoLin990405 Mar 9, 2026
94260ad
Fix remaining exhaustive switches for qwen/doubao
LeoLin990405 Mar 9, 2026
35cf6fa
Fix Qwen model name and improve build workflow
LeoLin990405 Mar 9, 2026
3511424
Add Zenmux and AigoCode providers, fix Doubao API endpoint
LeoLin990405 Mar 9, 2026
a1fe56b
Fix provider API endpoints and improve usage display
LeoLin990405 Mar 9, 2026
c6ba841
Improve provider status display and error handling
LeoLin990405 Mar 9, 2026
c9ac8a6
Fix Gemini OAuth: resolve multi-level symlink chain
LeoLin990405 Mar 9, 2026
7e8970c
Add Trae provider and redesign Zenmux/AigoCode icons
LeoLin990405 Mar 9, 2026
14c9bdb
Add account info display for all custom providers
LeoLin990405 Mar 9, 2026
0dc3f77
Fix Zai build: pass apiKey at fetchUsage level, not parseUsageSnapshot
LeoLin990405 Mar 9, 2026
0a7a1cd
Fix Kimi account info: show masked API key and plan type (API/Web)
LeoLin990405 Mar 9, 2026
b5a613a
Add MiniMax account info: show masked API key when using API token
LeoLin990405 Mar 9, 2026
72301ac
Fix case-insensitive lookup for reset header in QwenUsageFetcher
LeoLin990405 Mar 9, 2026
ebc3536
fix: increase Doubao API timeout from 15s to 30s
LeoLin990405 Mar 11, 2026
55e05c6
feat: add local usage tracking for Doubao and Qwen (weekly/monthly)
LeoLin990405 Mar 11, 2026
ba83be7
fix: always enumerate Claude directory tree and remove redundant TTL …
LeoLin990405 Mar 12, 2026
b505366
fix: persist refresh interval to UserDefaults (#506)
LeoLin990405 Mar 12, 2026
ff503aa
fix: correct Claude/Gemini icon swap in Antigravity tab (#486)
LeoLin990405 Mar 12, 2026
c262c32
feat: add AigoCode web dashboard scraping for usage monitoring
LeoLin990405 Mar 13, 2026
8473420
fix: resolve build errors in AigoCode web strategy and Claude scanner
LeoLin990405 Mar 13, 2026
8962d1e
feat(aigocode): import browser cookies for web dashboard scraping
LeoLin990405 Mar 13, 2026
7e890fc
fix(aigocode): extract Supabase session from Chrome localStorage
LeoLin990405 Mar 13, 2026
37e1ae0
fix(aigocode): use ChromiumLocalStorageReader for proper LevelDB parsing
LeoLin990405 Mar 13, 2026
aae3668
feat(trae): add web dashboard usage monitoring via browser cookies
LeoLin990405 Mar 13, 2026
95b5ef4
fix(trae): resolve access level mismatch in TraeUsageSnapshot
LeoLin990405 Mar 13, 2026
57a43df
fix(trae): update API models to match ByteDance Volc Engine format
LeoLin990405 Mar 13, 2026
74b07a1
ci: re-trigger CI after main sync
LeoLin990405 Mar 13, 2026
616bd40
debug(trae): add cookie header debug logging for CheckLogin
LeoLin990405 Mar 13, 2026
bccd031
fix: address review feedback for Qwen & Doubao providers
LeoLin990405 Mar 13, 2026
79ae37c
fix: add accumulated usage parameter to toUsageSnapshot
LeoLin990405 Mar 13, 2026
341d14e
test: add regression tests for multi-level symlink oauth2.js resolution
LeoLin990405 Mar 13, 2026
315cb27
ci: add swift test step to build workflow
LeoLin990405 Mar 13, 2026
5206f54
fix(trae): use global API endpoint and real usage data
LeoLin990405 Mar 13, 2026
f26376f
fix(test): update provider order test for new providers
LeoLin990405 Mar 13, 2026
c0f92f6
feat(kimi): add localStorage token extraction
LeoLin990405 Mar 13, 2026
32d69e6
fix(doubao): increase API timeout to 30s
LeoLin990405 Mar 13, 2026
a0c6c99
Add StepFun (阶跃星辰) provider for balance monitoring
LeoLin990405 Mar 23, 2026
b465aa5
Fix exhaustive switch cases and add StepFun UI integration
LeoLin990405 Mar 23, 2026
c67a643
Add .stepfun case to TokenAccountCLI exhaustive switch
LeoLin990405 Mar 23, 2026
51d53ab
Register StepFun in ProviderDescriptorRegistry bootstrap map
LeoLin990405 Mar 23, 2026
2e53aaf
Add .stepfun to SettingsStoreTests provider order expectation
LeoLin990405 Mar 23, 2026
b6488de
Enhance StepFun fetcher with Step Plan + rate limit support
LeoLin990405 Mar 23, 2026
770a89f
Add StepFun browser cookie integration for Step Plan monitoring
LeoLin990405 Mar 23, 2026
664c212
Fix StepFun cookie auth: pass Oasis-Webid to prevent embezzled error
LeoLin990405 Mar 23, 2026
96a9aad
Fix StepFun balance display: show 0% used when balance is positive
LeoLin990405 Mar 23, 2026
709c3f7
Use WKWebView dashboard scraping for StepFun plan data (like AigoCode)
LeoLin990405 Mar 23, 2026
65b121d
Fix MainActor isolation for StepFunDashboardFetcher
LeoLin990405 Mar 23, 2026
ace7ab8
Fix MainActor bridge: use withCheckedThrowingContinuation for WKWebVi…
LeoLin990405 Mar 23, 2026
3333f62
feat(mimo): add MiMo provider (port from upstream PR #651)
LeoLin990405 Apr 14, 2026
d3b408c
fix: Linux cross-platform compat + lint autofix
LeoLin990405 Apr 14, 2026
bbc3d85
chore(lint): suppress pre-existing swiftlint violations surfaced by CI
LeoLin990405 Apr 14, 2026
9275123
fix(lint): use block disable for line_length in raw-string test fixture
LeoLin990405 Apr 14, 2026
93a3707
fix(test): ProviderDetailView is not generic on this branch
LeoLin990405 Apr 14, 2026
feeafac
fix(test): use epsilon comparison for Double usedPercent (5.05 vs 5.0…
LeoLin990405 Apr 14, 2026
35eb0e6
test(mimo): disable 4 tests that depend on unported PR 651 registry c…
LeoLin990405 Apr 14, 2026
8dda6ce
feat(mimo): add mimo to balance-provider allowlist (UI)
LeoLin990405 Apr 14, 2026
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
58 changes: 58 additions & 0 deletions .github/workflows/build-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Build CodexBar App

on:
workflow_dispatch:
push:
branches: ["feat/qwen-doubao-providers"]

jobs:
build-macos-app:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6

- name: Select Xcode
run: |
set -euo pipefail
for candidate in /Applications/Xcode_26.1.1.app /Applications/Xcode_26.1.app /Applications/Xcode.app; do
if [[ -d "$candidate" ]]; then
sudo xcode-select -s "${candidate}/Contents/Developer"
echo "DEVELOPER_DIR=${candidate}/Contents/Developer" >> "$GITHUB_ENV"
break
fi
done
xcodebuild -version
swift --version

- name: Resolve dependencies
run: swift package resolve

- name: Build release
run: swift build -c release 2>&1

- name: Run tests
run: swift test --no-parallel

- name: Fix rpath and package
run: |
set -euo pipefail
BIN_DIR="$(swift build -c release --show-bin-path)"
echo "Binary directory: $BIN_DIR"

# Add @executable_path/../Frameworks rpath so Sparkle.framework loads from .app bundle
install_name_tool -add_rpath @executable_path/../Frameworks "$BIN_DIR/CodexBar" || true
install_name_tool -add_rpath @executable_path/../Frameworks "$BIN_DIR/CodexBarCLI" || true

# Create a zip of the built products
cd "$BIN_DIR"
zip -r "$GITHUB_WORKSPACE/CodexBar-custom-build.zip" \
CodexBar CodexBarCLI CodexBarClaudeWatchdog CodexBarClaudeWebProbe \
CodexBar_CodexBar.bundle KeyboardShortcuts_KeyboardShortcuts.bundle \
Sparkle.framework

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: CodexBar-custom-build
path: CodexBar-custom-build.zip
retention-days: 30
9 changes: 8 additions & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,14 @@ struct MenuDescriptor {
entries.append(.text("Activity: \(detail)", .secondary))
}
} else if let loginMethodText, !loginMethodText.isEmpty {
entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText))", .secondary))
let formatted = AccountFormatter.plan(loginMethodText)
// Balance-style providers (openrouter, mimo) already emit "Balance: $X.XX";
// don't double-prefix with "Plan: " in that case.
if provider == .openrouter || provider == .mimo, formatted.hasPrefix("Balance:") {
entries.append(.text(formatted, .secondary))
} else {
entries.append(.text("Plan: \(formatted)", .secondary))
}
}

if metadata.usesAccountFallback {
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/PreferencesProviderDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct ProviderDetailView: View {
else {
return nil
}
guard provider == .openrouter else {
guard provider == .openrouter || provider == .mimo else {
return (label: "Plan", value: rawPlan)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct AigoCodeProviderImplementation: ProviderImplementation {
let id: UsageProvider = .aigocode

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

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "aigocode-api-token",
title: "API key",
subtitle: "Optional when using web dashboard mode. "
+ "Stored in ~/.codexbar/config.json.",
kind: .secure,
placeholder: "sk-...",
binding: context.stringBinding(\.aigocodeAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "aigocode-open-dashboard",
title: "Open AigoCode Dashboard",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://www.aigocode.com/dashboard/console") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/AigoCode/AigoCodeSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var aigocodeAPIToken: String {
get { self.configSnapshot.providerConfig(for: .aigocode)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .aigocode) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .aigocode, field: "apiKey", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct DoubaoProviderImplementation: ProviderImplementation {
let id: UsageProvider = .doubao

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

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "doubao-api-token",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. Get your API key from the Volcengine "
+ "Ark console.",
kind: .secure,
placeholder: "ark-...",
binding: context.stringBinding(\.doubaoAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "doubao-open-dashboard",
title: "Open Volcengine Ark Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.volcengine.com/ark/") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Doubao/DoubaoSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var doubaoAPIToken: String {
get { self.configSnapshot.providerConfig(for: .doubao)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .doubao) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .doubao, field: "apiKey", value: newValue)
}
}
}
102 changes: 102 additions & 0 deletions Sources/CodexBar/Providers/MiMo/MiMoProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct MiMoProviderImplementation: ProviderImplementation {
let id: UsageProvider = .mimo
let supportsLoginFlow: Bool = true

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

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

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.mimo(context.settings.miMoSettingsSnapshot(tokenOverride: context.tokenOverride))
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.miMoCookieSource.rawValue },
set: { raw in
context.settings.miMoCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: false,
keychainDisabled: context.settings.debugDisableKeychainAccess)
let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.miMoCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports Chrome browser cookies from Xiaomi MiMo.",
manual: "Paste a Cookie header from platform.xiaomimimo.com.",
off: "Xiaomi MiMo cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "mimo-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports Chrome browser cookies from Xiaomi MiMo.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: nil,
onChange: nil,
trailingText: {
guard let entry = CookieHeaderCache.load(provider: .mimo) else { return nil }
let when = entry.storedAt.relativeDescription()
return "Cached: \(entry.sourceLabel) • \(when)"
}),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "mimo-cookie",
title: "",
subtitle: "",
kind: .secure,
placeholder: "Cookie: ...",
binding: context.stringBinding(\.miMoCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "mimo-open-balance",
title: "Open MiMo Balance",
style: .link,
isVisible: nil,
perform: {
guard let url = URL(string: "https://platform.xiaomimimo.com/#/console/balance") else {
return
}
NSWorkspace.shared.open(url)
}),
],
isVisible: { context.settings.miMoCookieSource == .manual },
onActivate: { context.settings.ensureMiMoCookieLoaded() }),
]
}

@MainActor
func runLoginFlow(context _: ProviderLoginContext) async -> Bool {
let loginURL = "https://platform.xiaomimimo.com/api/v1/genLoginUrl?currentPath=%2F%23%2Fconsole%2Fbalance"
guard let url = URL(string: loginURL) else {
return false
}
NSWorkspace.shared.open(url)
return false
}
}
35 changes: 35 additions & 0 deletions Sources/CodexBar/Providers/MiMo/MiMoSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var miMoCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .mimo)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .mimo) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .mimo, field: "cookieHeader", value: newValue)
}
}

var miMoCookieSource: ProviderCookieSource {
get { self.resolvedCookieSource(provider: .mimo, fallback: .auto) }
set {
self.updateProviderConfig(provider: .mimo) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(provider: .mimo, field: "cookieSource", value: newValue.rawValue)
}
}

func ensureMiMoCookieLoaded() {}
}

extension SettingsStore {
func miMoSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot.MiMoProviderSettings {
_ = tokenOverride
return ProviderSettingsSnapshot.MiMoProviderSettings(
cookieSource: self.miMoCookieSource,
manualCookieHeader: self.miMoCookieHeader)
}
}
42 changes: 42 additions & 0 deletions Sources/CodexBar/Providers/Qwen/QwenProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct QwenProviderImplementation: ProviderImplementation {
let id: UsageProvider = .qwen

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

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "qwen-api-token",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. Get your API key from the Alibaba Cloud "
+ "Bailian console (DashScope).",
kind: .secure,
placeholder: "sk-...",
binding: context.stringBinding(\.qwenAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "qwen-open-dashboard",
title: "Open Bailian Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://bailian.console.aliyun.com/") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Qwen/QwenSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var qwenAPIToken: String {
get { self.configSnapshot.providerConfig(for: .qwen)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .qwen) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .qwen, field: "apiKey", value: newValue)
}
}
}
Loading
Loading