Skip to content
7 changes: 7 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,13 @@ extension UsageMenuCardView.Model {
return notes
}

if input.provider == .mimo, input.snapshot != nil {
return [
"Balance updates in near-real time (up to 5 min lag)",
"Daily billing data finalizes at 07:00 UTC",
]
}

guard input.provider == .openrouter,
let openRouter = input.snapshot?.openRouterUsage
else {
Expand Down
13 changes: 12 additions & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,18 @@ struct MenuDescriptor {
entries.append(.text("Activity: \(detail)", .secondary))
}
} else if let loginMethodText, !loginMethodText.isEmpty {
entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText))", .secondary))
if provider == .openrouter || provider == .mimo {
let balanceValue = loginMethodText
.replacingOccurrences(
of: #"(?i)^\s*balance:\s*"#,
with: "",
options: [.regularExpression])
.trimmingCharacters(in: .whitespacesAndNewlines)
let value = balanceValue.isEmpty ? loginMethodText : balanceValue
entries.append(.text("Balance: \(AccountFormatter.plan(value))", .secondary))
} else {
entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText))", .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
97 changes: 97 additions & 0 deletions Sources/CodexBar/Providers/MiMo/MiMoProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

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

@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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum ProviderImplementationRegistry {
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
case .mimo: MiMoProviderImplementation()
}
}

Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-mimo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ extension UsageStore {
.kimi: "Kimi debug log not yet implemented",
.kimik2: "Kimi K2 debug log not yet implemented",
.jetbrains: "JetBrains AI debug log not yet implemented",
.mimo: "Xiaomi MiMo debug log not yet implemented",
]
let buildText = {
switch provider {
Expand Down Expand Up @@ -1231,7 +1232,7 @@ extension UsageStore {
let source = resolution?.source.rawValue ?? "none"
return "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains:
.kimik2, .jetbrains, .mimo:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
13 changes: 11 additions & 2 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ struct TokenAccountCLIContext {
kimi: ProviderSettingsSnapshot.KimiProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .mimo:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
return self.makeSnapshot(
mimo: ProviderSettingsSnapshot.MiMoProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .zai:
return self.makeSnapshot(
zai: ProviderSettingsSnapshot.ZaiProviderSettings(apiRegion: self.resolveZaiRegion(config)))
Expand Down Expand Up @@ -196,7 +203,8 @@ struct TokenAccountCLIContext {
augment: ProviderSettingsSnapshot.AugmentProviderSettings? = nil,
amp: ProviderSettingsSnapshot.AmpProviderSettings? = nil,
ollama: ProviderSettingsSnapshot.OllamaProviderSettings? = nil,
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil) -> ProviderSettingsSnapshot
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil,
mimo: ProviderSettingsSnapshot.MiMoProviderSettings? = nil) -> ProviderSettingsSnapshot
{
ProviderSettingsSnapshot.make(
codex: codex,
Expand All @@ -212,7 +220,8 @@ struct TokenAccountCLIContext {
augment: augment,
amp: amp,
ollama: ollama,
jetbrains: jetbrains)
jetbrains: jetbrains,
mimo: mimo)
}

func environment(
Expand Down
131 changes: 131 additions & 0 deletions Sources/CodexBarCore/Providers/MiMo/MiMoCookieImporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Foundation

enum MiMoCookieHeader {
static let requiredCookieNames: Set<String> = [
"api-platform_serviceToken",
"userId",
]
static let knownCookieNames: Set<String> = requiredCookieNames.union([
"api-platform_ph",
"api-platform_slh",
])

static func normalizedHeader(from raw: String?) -> String? {
guard let normalized = CookieHeaderNormalizer.normalize(raw) else { return nil }
let pairs = CookieHeaderNormalizer.pairs(from: normalized)
guard !pairs.isEmpty else { return nil }

var byName: [String: String] = [:]
for pair in pairs {
let name = pair.name.trimmingCharacters(in: .whitespacesAndNewlines)
let value = pair.value.trimmingCharacters(in: .whitespacesAndNewlines)
guard self.knownCookieNames.contains(name), !value.isEmpty else { continue }
byName[name] = value
}

guard self.requiredCookieNames.isSubset(of: Set(byName.keys)) else { return nil }
return byName.keys.sorted().compactMap { name in
guard let value = byName[name] else { return nil }
return "\(name)=\(value)"
}.joined(separator: "; ")
}

static func header(from cookies: [HTTPCookie]) -> String? {
var byName: [String: HTTPCookie] = [:]

for cookie in cookies {
guard self.knownCookieNames.contains(cookie.name) else { continue }
guard !cookie.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { continue }
if let expiry = cookie.expiresDate, expiry < Date() { continue }

if let existing = byName[cookie.name] {
let existingExpiry = existing.expiresDate ?? .distantPast
let candidateExpiry = cookie.expiresDate ?? .distantPast
if candidateExpiry >= existingExpiry {
byName[cookie.name] = cookie
}
} else {
byName[cookie.name] = cookie
}
}

guard self.requiredCookieNames.isSubset(of: Set(byName.keys)) else { return nil }
return byName.keys.sorted().compactMap { name in
guard let cookie = byName[name] else { return nil }
return "\(cookie.name)=\(cookie.value)"
}.joined(separator: "; ")
}
}

#if os(macOS)
import SweetCookieKit

private let miMoCookieImportOrder: BrowserCookieImportOrder =
ProviderDefaults.metadata[.mimo]?.browserCookieOrder ?? Browser.defaultImportOrder

public enum MiMoCookieImporter {
private static let cookieClient = BrowserCookieClient()
private static let cookieDomains = [
"platform.xiaomimimo.com",
"xiaomimimo.com",
]

public struct SessionInfo: Sendable {
public let cookieHeader: String
public let sourceLabel: String

public init(cookieHeader: String, sourceLabel: String) {
self.cookieHeader = cookieHeader
self.sourceLabel = sourceLabel
}
}

nonisolated(unsafe) static var importSessionsOverrideForTesting:
((BrowserDetection, ((String) -> Void)?) throws -> [SessionInfo])?

public static func importSessions(
browserDetection: BrowserDetection,
logger: ((String) -> Void)? = nil) throws -> [SessionInfo]
{
if let override = self.importSessionsOverrideForTesting {
return try override(browserDetection, logger)
}

let log: (String) -> Void = { msg in logger?("[mimo-cookie] \(msg)") }
var sessions: [SessionInfo] = []
let installed = miMoCookieImportOrder.cookieImportCandidates(using: browserDetection)
let labels = installed.map(\.displayName).joined(separator: ", ")
log("Cookie import candidates: \(labels)")

for browserSource in installed {
do {
let query = BrowserCookieQuery(domains: self.cookieDomains)
let sources = try Self.cookieClient.records(
matching: query,
in: browserSource,
logger: log)

for source in sources where !source.records.isEmpty {
let cookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
guard let cookieHeader = MiMoCookieHeader.header(from: cookies) else {
continue
}
sessions.append(SessionInfo(cookieHeader: cookieHeader, sourceLabel: source.label))
}
} catch {
BrowserCookieAccessGate.recordIfNeeded(error)
log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")
}
}

return sessions
}

public static func hasSession(
browserDetection: BrowserDetection,
logger: ((String) -> Void)? = nil) -> Bool
{
(try? self.importSessions(browserDetection: browserDetection, logger: logger).isEmpty == false) ?? false
}
}
#endif
Loading