mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-11-07 07:08:25 +00:00
* feat(quota): Command Code quota and ClinePass from the Cline sign-in Command Code: reads the CLI's ~/.commandcode/auth.json key and the billing credits and subscription endpoints. 5-hour and weekly windows as used of cap, remaining credits as a note, plan from planId. Live in the Mac dock from the local CLI, in codeburn quota and the Windows dock. ClinePass: without a saved or env key, falls back to the Cline CLI's providers.json session (accessToken, then apiKey, then auth.apiKey, with Cline's path overrides). Read-only; an expired session stops before the network and says to run cline. The Mac catalog adds the local CLI source so Automatic connects without a key. * fix(quota): keep retrying after an expired Cline sign-in Running cline refreshes the session file CodeBurn only reads, so an expired or rejected Cline session is now a retryable failure that keeps the last quota and shows the guidance, on the Mac and in the CLI. A rejected API key and a missing credential stay terminal. * test(quota): build expected Cline paths with path.join for Windows
501 lines
20 KiB
Swift
501 lines
20 KiB
Swift
import Foundation
|
|
|
|
/// Stable Capacity Dock identity backed by the audited provider catalog.
|
|
///
|
|
/// This is a value type rather than a 69-case enum so the provider inventory has
|
|
/// one source of truth. Existing CodeBurn quota adapters retain their
|
|
/// `ProviderFilter` bridge; the remaining providers are fetched through the
|
|
/// CodeBurn-owned provider adapter at runtime.
|
|
struct CapacityDockProvider: RawRepresentable, CaseIterable, Identifiable, Hashable, Sendable {
|
|
let rawValue: String
|
|
|
|
init?(rawValue: String) {
|
|
guard ProviderConnectionCatalog.providers.contains(where: { $0.id == rawValue }) else {
|
|
return nil
|
|
}
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
private init(known rawValue: String) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
var id: String { rawValue }
|
|
|
|
static var allCases: [CapacityDockProvider] {
|
|
ProviderConnectionCatalog.providers.map { CapacityDockProvider(known: $0.id) }
|
|
}
|
|
|
|
static let codex = CapacityDockProvider(known: "codex")
|
|
static let claude = CapacityDockProvider(known: "claude")
|
|
static let gemini = CapacityDockProvider(known: "gemini")
|
|
static let copilot = CapacityDockProvider(known: "copilot")
|
|
static let kimiCode = CapacityDockProvider(known: "kimi")
|
|
static let antigravity = CapacityDockProvider(known: "antigravity")
|
|
|
|
var catalogEntry: ProviderConnectionCatalogEntry {
|
|
ProviderConnectionCatalog.providers.first { $0.id == rawValue }!
|
|
}
|
|
|
|
var legacyFilter: ProviderFilter? {
|
|
switch rawValue {
|
|
case Self.claude.rawValue: .claude
|
|
case Self.codex.rawValue: .codex
|
|
case Self.gemini.rawValue: .gemini
|
|
case Self.copilot.rawValue: .copilot
|
|
case Self.kimiCode.rawValue: .kimiCode
|
|
case Self.antigravity.rawValue: .antigravity
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
var displayName: String { catalogEntry.displayName }
|
|
|
|
/// The CLI's provider id for this dock provider, used to find its row in the
|
|
/// payload's `providerDetails`. Catalog ids and CLI ids agree everywhere the
|
|
/// dock has no legacy bridge; where it has one the bridge is the authority,
|
|
/// because the dock's `kimi` entry is Kimi Code, which the CLI calls
|
|
/// `kimicode` (its `kimi` is a different provider).
|
|
var payloadProviderID: String { legacyFilter?.cliArg ?? rawValue }
|
|
|
|
/// Every payload provider this one tile stands for. A tile is an account the
|
|
/// user recognises; the CLI splits some accounts across more than one provider
|
|
/// row, and a tile that reads only the first row reports zero while the account
|
|
/// was busy. Cursor is the case in hand: one subscription, but sessions land
|
|
/// under `cursor` for the editor and `cursor-agent` for the agent.
|
|
var payloadProviderIDs: [String] {
|
|
switch payloadProviderID {
|
|
// One subscription, two rows: the editor and the agent.
|
|
case "cursor": ["cursor", "cursor-agent"]
|
|
// The tile is the Factory account, which the CLI records under its
|
|
// product name. Without this the Droid tile reads zero while Droid works.
|
|
case "factory": ["droid"]
|
|
// The ClinePass plan pays for both Cline surfaces, and the CLI records
|
|
// the extension and the command line tool as separate rows.
|
|
case "clinepass": ["cline", "cline-cli"]
|
|
case "commandcode": ["command-code"]
|
|
default: [payloadProviderID]
|
|
}
|
|
}
|
|
|
|
var iconName: String {
|
|
switch rawValue {
|
|
case Self.codex.rawValue, "openai", "azureopenai": "codex"
|
|
case Self.gemini.rawValue: "gemini"
|
|
case Self.copilot.rawValue: "copilot"
|
|
case "alibabatokenplan": "alibaba"
|
|
case "moonshot": "kimi"
|
|
default: rawValue
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CapacityDockEdge: String, CaseIterable, Sendable {
|
|
case left
|
|
case right
|
|
case top
|
|
case bottom
|
|
|
|
var isVertical: Bool { self == .left || self == .right }
|
|
|
|
var opposite: CapacityDockEdge {
|
|
switch self {
|
|
case .left: .right
|
|
case .right: .left
|
|
case .top: .bottom
|
|
case .bottom: .top
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CapacityDockTheme: String, CaseIterable, Sendable {
|
|
case graphite
|
|
case liquidGlass
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .graphite: L("Graphite")
|
|
case .liquidGlass: L("Liquid Glass")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CapacityDockGaugeShape: String, CaseIterable, Sendable {
|
|
case circle
|
|
case squircle
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .circle: L("Circle")
|
|
case .squircle: L("Squircle")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CapacityDockProviderSelection {
|
|
static func isDockEligible(
|
|
_ provider: CapacityDockProvider,
|
|
isConnected: Bool,
|
|
hasSavedCredential: Bool
|
|
) -> Bool {
|
|
isConnected || (
|
|
provider.catalogEntry.hasLiveCodeBurnQuotaAdapter && hasSavedCredential
|
|
)
|
|
}
|
|
|
|
static func eligibleProviders(
|
|
from providers: [CapacityDockProvider] = CapacityDockPreferences.supportedProviders,
|
|
isEligible: (CapacityDockProvider) -> Bool
|
|
) -> [CapacityDockProvider] {
|
|
providers.filter(isEligible)
|
|
}
|
|
|
|
static func manageableProviders(
|
|
from providers: [CapacityDockProvider] = CapacityDockPreferences.supportedProviders,
|
|
selected: [CapacityDockProvider],
|
|
isConnected: (CapacityDockProvider) -> Bool
|
|
) -> [CapacityDockProvider] {
|
|
let selectedSet = Set(selected)
|
|
return providers.filter { selectedSet.contains($0) || isConnected($0) }
|
|
}
|
|
|
|
static func canDeselect(
|
|
_ provider: CapacityDockProvider,
|
|
selected: [CapacityDockProvider],
|
|
isConnected: (CapacityDockProvider) -> Bool
|
|
) -> Bool {
|
|
guard selected.contains(provider) else { return true }
|
|
let connectedSelection = selected.filter(isConnected)
|
|
return !isConnected(provider) || connectedSelection.count > 1
|
|
}
|
|
}
|
|
|
|
enum CapacityDockPreferences {
|
|
static let enabledKey = "CodeBurnCapacityDockEnabled"
|
|
static let selectedProvidersKey = "CodeBurnCapacityDockProviders"
|
|
static let preferredProviderKey = "CodeBurnCapacityDockPreferredProvider"
|
|
static let dockEdgeKey = "CodeBurnCapacityDockEdge"
|
|
static let attachmentEdgeKey = "CodeBurnCapacityDockAttachmentEdge"
|
|
static let legacyDockedKey = "CodeBurnCapacityDockDocked"
|
|
static let normalizedHorizontalOffsetKey = "CodeBurnCapacityDockHorizontalOffset"
|
|
static let normalizedVerticalOffsetKey = "CodeBurnCapacityDockVerticalOffset"
|
|
static let scaleKey = "CodeBurnCapacityDockScale"
|
|
static let themeKey = "CodeBurnCapacityDockTheme"
|
|
static let gaugeShapeKey = "CodeBurnCapacityDockGaugeShape"
|
|
static let glanceWindowsKey = "CodeBurnCapacityDockGlanceWindows"
|
|
static let manualSelectionKey = "CodeBurnCapacityDockManualSelection"
|
|
|
|
static let defaultProvider: CapacityDockProvider = .codex
|
|
static let supportedProviders = CapacityDockProvider.allCases
|
|
static let maxAutoProviders = 5
|
|
static let defaultScale = 0.6
|
|
static let scaleRange = 0.6 ... 1.2
|
|
|
|
struct Snapshot: Equatable, Sendable {
|
|
let isEnabled: Bool
|
|
let selectedProviders: [CapacityDockProvider]
|
|
let preferredProvider: CapacityDockProvider
|
|
let dockedEdge: CapacityDockEdge?
|
|
let attachmentEdge: CapacityDockEdge
|
|
let normalizedHorizontalOffset: Double?
|
|
let normalizedVerticalOffset: Double?
|
|
let scale: Double
|
|
let theme: CapacityDockTheme
|
|
let gaugeShape: CapacityDockGaugeShape
|
|
/// Which quota horizon each provider's gauge reports, keyed by provider
|
|
/// id. Sparse: an absent provider uses the billing horizon, which is
|
|
/// what the dock has always shown.
|
|
let glanceWindows: [String: CapacityDockGlanceWindowKind]
|
|
|
|
func glanceWindow(for provider: CapacityDockProvider) -> CapacityDockGlanceWindowKind {
|
|
glanceWindows[provider.rawValue] ?? .billing
|
|
}
|
|
}
|
|
|
|
static func load(defaults: UserDefaults = .standard) -> Snapshot {
|
|
let selected = normalizedProviders(
|
|
rawIdentifiers: defaults.stringArray(forKey: selectedProvidersKey)
|
|
)
|
|
let storedPreferred = defaults.string(forKey: preferredProviderKey)
|
|
.flatMap(CapacityDockProvider.init(rawValue:))
|
|
let preferred = normalizedPreferred(storedPreferred, selected: selected)
|
|
|
|
let horizontalOffset: Double?
|
|
if defaults.object(forKey: normalizedHorizontalOffsetKey) != nil {
|
|
let stored = defaults.double(forKey: normalizedHorizontalOffsetKey)
|
|
horizontalOffset = stored.isFinite ? clamp(stored) : nil
|
|
} else {
|
|
horizontalOffset = nil
|
|
}
|
|
|
|
let verticalOffset: Double?
|
|
if defaults.object(forKey: normalizedVerticalOffsetKey) != nil {
|
|
let stored = defaults.double(forKey: normalizedVerticalOffsetKey)
|
|
verticalOffset = stored.isFinite ? clamp(stored) : nil
|
|
} else {
|
|
verticalOffset = nil
|
|
}
|
|
|
|
let dockedEdge: CapacityDockEdge?
|
|
if let rawEdge = defaults.string(forKey: dockEdgeKey),
|
|
let storedEdge = CapacityDockEdge(rawValue: rawEdge) {
|
|
dockedEdge = storedEdge
|
|
} else if defaults.object(forKey: legacyDockedKey) != nil,
|
|
!defaults.bool(forKey: legacyDockedKey) {
|
|
dockedEdge = nil
|
|
} else {
|
|
dockedEdge = .right
|
|
}
|
|
let attachmentEdge = defaults.string(forKey: attachmentEdgeKey)
|
|
.flatMap(CapacityDockEdge.init(rawValue:))
|
|
?? dockedEdge
|
|
?? .right
|
|
|
|
return Snapshot(
|
|
isEnabled: defaults.bool(forKey: enabledKey),
|
|
selectedProviders: selected,
|
|
preferredProvider: preferred,
|
|
dockedEdge: dockedEdge,
|
|
attachmentEdge: attachmentEdge,
|
|
normalizedHorizontalOffset: horizontalOffset,
|
|
normalizedVerticalOffset: verticalOffset,
|
|
scale: normalizedScale(
|
|
defaults.object(forKey: scaleKey) == nil
|
|
? defaultScale
|
|
: defaults.double(forKey: scaleKey)
|
|
),
|
|
theme: defaults.string(forKey: themeKey)
|
|
.flatMap(CapacityDockTheme.init(rawValue:)) ?? .graphite,
|
|
gaugeShape: defaults.string(forKey: gaugeShapeKey)
|
|
.flatMap(CapacityDockGaugeShape.init(rawValue:)) ?? .squircle,
|
|
glanceWindows: normalizedGlanceWindows(
|
|
defaults.dictionary(forKey: glanceWindowsKey)
|
|
)
|
|
)
|
|
}
|
|
|
|
static func setEnabled(_ enabled: Bool, defaults: UserDefaults = .standard) {
|
|
defaults.set(enabled, forKey: enabledKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setSelectedProviders(
|
|
_ providers: [CapacityDockProvider],
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
let identifiers = providers.map(\.rawValue)
|
|
let normalized = normalizedProviders(rawIdentifiers: identifiers)
|
|
let currentPreferred = defaults.string(forKey: preferredProviderKey)
|
|
.flatMap(CapacityDockProvider.init(rawValue:))
|
|
let preferred = normalizedPreferred(currentPreferred, selected: normalized)
|
|
defaults.set(normalized.map(\.rawValue), forKey: selectedProvidersKey)
|
|
defaults.set(preferred.rawValue, forKey: preferredProviderKey)
|
|
// The user has taken over the dock provider set, so stop auto-seeding
|
|
// from connected subscriptions and respect their choice from now on.
|
|
defaults.set(true, forKey: manualSelectionKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
/// Drops one provider from the persisted dock selection without latching
|
|
/// manual-selection mode. Disconnect needs this: a credential-less
|
|
/// adapter (Cursor) left in the selection would silently reconnect on the
|
|
/// next scheduled refresh.
|
|
static func removeProvider(
|
|
_ provider: CapacityDockProvider,
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
let stored = defaults.stringArray(forKey: selectedProvidersKey) ?? []
|
|
let remaining = stored.filter { $0 != provider.rawValue }
|
|
guard remaining.count != stored.count else { return }
|
|
defaults.set(remaining, forKey: selectedProvidersKey)
|
|
let preferred = normalizedPreferred(
|
|
defaults.string(forKey: preferredProviderKey).flatMap(CapacityDockProvider.init(rawValue:)),
|
|
selected: normalizedProviders(rawIdentifiers: remaining)
|
|
)
|
|
defaults.set(preferred.rawValue, forKey: preferredProviderKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
/// Until the user manually edits the dock set, mirror the connected
|
|
/// subscriptions (capped) so a fresh install shows what's actually active.
|
|
/// No-ops once `manualSelectionKey` latches or the set already matches.
|
|
static func autoSeedFromConnected(
|
|
_ connected: [CapacityDockProvider],
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
guard !defaults.bool(forKey: manualSelectionKey) else { return }
|
|
// ZCode yields to Z.ai at seed time: both adapters read the same z.ai
|
|
// plan endpoint, so a machine signed in through the Pi CLI and the
|
|
// ZCode app would otherwise seed two rings reporting the same numbers.
|
|
// The deliberately configured Z.ai credential wins; pinning ZCode in
|
|
// Settings always shows it regardless.
|
|
var seeded = connected
|
|
if seeded.contains(where: { $0.id == "zai" }) {
|
|
seeded.removeAll { $0.id == "zcode" }
|
|
}
|
|
let desired = supportedProviders
|
|
.filter(seeded.contains)
|
|
.prefix(maxAutoProviders)
|
|
let desiredIDs = desired.map(\.rawValue)
|
|
guard !desiredIDs.isEmpty else { return } // nothing active yet — wait
|
|
guard defaults.stringArray(forKey: selectedProvidersKey) != desiredIDs else { return }
|
|
defaults.set(desiredIDs, forKey: selectedProvidersKey)
|
|
let preferred = normalizedPreferred(
|
|
defaults.string(forKey: preferredProviderKey).flatMap(CapacityDockProvider.init(rawValue:)),
|
|
selected: Array(desired)
|
|
)
|
|
defaults.set(preferred.rawValue, forKey: preferredProviderKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setPreferredProvider(
|
|
_ provider: CapacityDockProvider,
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
let selected = load(defaults: defaults).selectedProviders
|
|
let normalized = normalizedPreferred(provider, selected: selected)
|
|
defaults.set(normalized.rawValue, forKey: preferredProviderKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setNormalizedVerticalOffset(
|
|
_ offset: Double?,
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
if let offset, offset.isFinite {
|
|
defaults.set(clamp(offset), forKey: normalizedVerticalOffsetKey)
|
|
} else {
|
|
defaults.removeObject(forKey: normalizedVerticalOffsetKey)
|
|
}
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setPlacement(
|
|
dockedEdge: CapacityDockEdge?,
|
|
attachmentEdge: CapacityDockEdge? = nil,
|
|
normalizedHorizontalOffset: Double?,
|
|
normalizedVerticalOffset: Double?,
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
if let dockedEdge {
|
|
defaults.set(dockedEdge.rawValue, forKey: dockEdgeKey)
|
|
defaults.removeObject(forKey: legacyDockedKey)
|
|
} else {
|
|
defaults.removeObject(forKey: dockEdgeKey)
|
|
// Absence means a fresh install, whose product default is the
|
|
// right edge. Retain an explicit false marker so a deliberately
|
|
// detached rail survives relaunch and screen changes.
|
|
defaults.set(false, forKey: legacyDockedKey)
|
|
}
|
|
let retainedAttachmentEdge = attachmentEdge
|
|
?? dockedEdge
|
|
?? defaults.string(forKey: attachmentEdgeKey).flatMap(CapacityDockEdge.init(rawValue:))
|
|
?? .right
|
|
defaults.set(retainedAttachmentEdge.rawValue, forKey: attachmentEdgeKey)
|
|
if let normalizedHorizontalOffset,
|
|
normalizedHorizontalOffset.isFinite {
|
|
defaults.set(clamp(normalizedHorizontalOffset), forKey: normalizedHorizontalOffsetKey)
|
|
} else {
|
|
defaults.removeObject(forKey: normalizedHorizontalOffsetKey)
|
|
}
|
|
if let normalizedVerticalOffset, normalizedVerticalOffset.isFinite {
|
|
defaults.set(clamp(normalizedVerticalOffset), forKey: normalizedVerticalOffsetKey)
|
|
} else {
|
|
defaults.removeObject(forKey: normalizedVerticalOffsetKey)
|
|
}
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setScale(_ scale: Double, defaults: UserDefaults = .standard) {
|
|
defaults.set(normalizedScale(scale), forKey: scaleKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setTheme(_ theme: CapacityDockTheme, defaults: UserDefaults = .standard) {
|
|
defaults.set(theme.rawValue, forKey: themeKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
static func setGaugeShape(
|
|
_ gaugeShape: CapacityDockGaugeShape,
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
defaults.set(gaugeShape.rawValue, forKey: gaugeShapeKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
/// Records which horizon one provider's gauge reports. Stored per provider
|
|
/// rather than as one dock-wide switch because the providers are
|
|
/// independent questions: a user can watch Claude's 5-hour window while
|
|
/// keeping Codex on its weekly one.
|
|
static func setGlanceWindow(
|
|
_ kind: CapacityDockGlanceWindowKind,
|
|
for provider: CapacityDockProvider,
|
|
defaults: UserDefaults = .standard
|
|
) {
|
|
var stored = storedGlanceWindows(defaults: defaults)
|
|
guard stored[provider.rawValue] != kind.rawValue else { return }
|
|
stored[provider.rawValue] = kind.rawValue
|
|
defaults.set(stored, forKey: glanceWindowsKey)
|
|
notifyChanged()
|
|
}
|
|
|
|
private static func storedGlanceWindows(defaults: UserDefaults) -> [String: String] {
|
|
var stored: [String: String] = [:]
|
|
for (identifier, value) in defaults.dictionary(forKey: glanceWindowsKey) ?? [:] {
|
|
guard let value = value as? String else { continue }
|
|
stored[identifier] = value
|
|
}
|
|
return stored
|
|
}
|
|
|
|
/// Drops entries a newer or older build cannot make sense of — an unknown
|
|
/// provider id, a horizon this build does not have — so a foreign value in
|
|
/// the domain cannot decide what the gauge shows.
|
|
private static func normalizedGlanceWindows(
|
|
_ stored: [String: Any]?
|
|
) -> [String: CapacityDockGlanceWindowKind] {
|
|
guard let stored else { return [:] }
|
|
var normalized: [String: CapacityDockGlanceWindowKind] = [:]
|
|
for (identifier, value) in stored {
|
|
guard CapacityDockProvider(rawValue: identifier) != nil,
|
|
let rawValue = value as? String,
|
|
let kind = CapacityDockGlanceWindowKind(rawValue: rawValue) else { continue }
|
|
normalized[identifier] = kind
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
private static func normalizedProviders(rawIdentifiers: [String]?) -> [CapacityDockProvider] {
|
|
let identifiers = Set(rawIdentifiers ?? [defaultProvider.rawValue])
|
|
let normalized = supportedProviders.filter { identifiers.contains($0.rawValue) }
|
|
return normalized.isEmpty ? [defaultProvider] : normalized
|
|
}
|
|
|
|
private static func normalizedPreferred(
|
|
_ preferred: CapacityDockProvider?,
|
|
selected: [CapacityDockProvider]
|
|
) -> CapacityDockProvider {
|
|
if let preferred, selected.contains(preferred) { return preferred }
|
|
if selected.contains(defaultProvider) { return defaultProvider }
|
|
return selected.first ?? defaultProvider
|
|
}
|
|
|
|
private static func clamp(_ value: Double) -> Double {
|
|
min(max(value, 0), 1)
|
|
}
|
|
|
|
private static func normalizedScale(_ value: Double) -> Double {
|
|
guard value.isFinite else { return defaultScale }
|
|
return min(max(value, scaleRange.lowerBound), scaleRange.upperBound)
|
|
}
|
|
|
|
private static func notifyChanged() {
|
|
NotificationCenter.default.post(name: .capacityDockPreferencesDidChange, object: nil)
|
|
}
|
|
}
|
|
|
|
extension Notification.Name {
|
|
static let capacityDockPreferencesDidChange = Notification.Name("com.codeburn.capacityDockPreferencesDidChange")
|
|
static let capacityDockOpenProviderSettings = Notification.Name("com.codeburn.capacityDockOpenProviderSettings")
|
|
}
|