Files
steipete--codexbar/Sources/CodexBar/CodexAccountMenuPresentation.swift
T
2026-07-13 12:22:33 +08:00

181 lines
6.2 KiB
Swift

import CodexBarCore
import Foundation
enum CodexAccountHealth: Equatable {
case ok
case needsReauth
case workspaceDeactivated
case missingAuth
case unavailable
var label: String? {
switch self {
case .ok:
nil
case .needsReauth:
"Needs re-auth"
case .workspaceDeactivated:
"Workspace deactivated"
case .missingAuth:
"Missing auth"
case .unavailable:
"Unavailable"
}
}
static func status(for account: CodexVisibleAccount, error: String?) -> CodexAccountHealth {
if let error {
return self.status(forError: error)
}
if account.authenticationHealthLabel != nil {
return .missingAuth
}
return .ok
}
static func status(forError error: String) -> CodexAccountHealth {
let normalized = error.lowercased()
if normalized.contains("deactivated") {
return .workspaceDeactivated
}
if normalized.contains("expired") ||
normalized.contains("revoked") ||
normalized.contains("unauthorized") ||
normalized.contains("401")
{
return .needsReauth
}
if normalized.contains("missing"), normalized.contains("auth") {
return .missingAuth
}
return .unavailable
}
}
enum CodexAccountPresentationOrdering {
static func orderedAccounts(
_ accounts: [CodexVisibleAccount],
snapshots: [CodexAccountUsageSnapshot],
activeVisibleAccountID: String?)
-> [CodexVisibleAccount]
{
guard accounts.count > 1 else { return accounts }
let snapshotByID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) })
let rankedAccounts = accounts.enumerated().map { index, account in
RankedAccount(
account: account,
rank: Rank(
account: account,
snapshot: snapshotByID[account.id],
activeVisibleAccountID: activeVisibleAccountID,
originalIndex: index))
}
let grouped = Dictionary(grouping: rankedAccounts, by: { Self.workspaceSortKey(for: $0.account) })
return grouped.values.sorted { lhs, rhs in
(lhs.map(\.rank).min() ?? .last) < (rhs.map(\.rank).min() ?? .last)
}.flatMap { group in
group.sorted { lhs, rhs in lhs.rank < rhs.rank }.map(\.account)
}
}
private struct RankedAccount {
let account: CodexVisibleAccount
let rank: Rank
}
private struct Rank: Comparable {
static let last = Rank(bucket: Int.max, availabilityScore: -.greatestFiniteMagnitude, originalIndex: Int.max)
let bucket: Int
let availabilityScore: Double
let displaySort: String
let originalIndex: Int
private init(bucket: Int, availabilityScore: Double, originalIndex: Int) {
self.bucket = bucket
self.availabilityScore = availabilityScore
self.displaySort = ""
self.originalIndex = originalIndex
}
init(
account: CodexVisibleAccount,
snapshot: CodexAccountUsageSnapshot?,
activeVisibleAccountID: String?,
originalIndex: Int)
{
self.originalIndex = originalIndex
self.displaySort = account.menuDisplayName.lowercased()
if account.id == activeVisibleAccountID {
self.bucket = 0
} else {
let health = CodexAccountHealth.status(for: account, error: snapshot?.error)
if health != .ok {
self.bucket = health == .missingAuth ? 4 : 3
} else if let availability = Self.availability(snapshot?.snapshot), availability <= 0 {
self.bucket = 2
} else {
self.bucket = 1
}
}
self.availabilityScore = Self.availability(snapshot?.snapshot) ?? -1
}
static func < (lhs: Rank, rhs: Rank) -> Bool {
if lhs.bucket != rhs.bucket { return lhs.bucket < rhs.bucket }
if lhs.availabilityScore != rhs.availabilityScore {
return lhs.availabilityScore > rhs.availabilityScore
}
if lhs.displaySort != rhs.displaySort { return lhs.displaySort < rhs.displaySort }
return lhs.originalIndex < rhs.originalIndex
}
private static func availability(_ snapshot: UsageSnapshot?) -> Double? {
guard let snapshot else { return nil }
let session = snapshot.primary?.remainingPercent
let weekly = snapshot.secondary?.remainingPercent
return switch (session, weekly) {
case let (.some(session), .some(weekly)):
min(session, weekly)
case let (.some(session), .none):
session
case let (.none, .some(weekly)):
weekly
case (.none, .none):
nil
}
}
}
private static func workspaceSortKey(for account: CodexVisibleAccount) -> String {
if let workspaceAccountID = account.workspaceAccountID, !workspaceAccountID.isEmpty {
return workspaceAccountID.lowercased()
}
return account.menuWorkspaceLabel?.lowercased() ?? "personal"
}
}
struct CodexAccountWorkspaceSection: Equatable {
let title: String
let accounts: [CodexVisibleAccount]
}
extension [CodexVisibleAccount] {
func codexWorkspaceSections() -> [CodexAccountWorkspaceSection] {
guard !self.isEmpty else { return [] }
var sections: [CodexAccountWorkspaceSection] = []
for account in self {
let title = account.menuWorkspaceLabel ?? "Personal"
if let index = sections.firstIndex(where: { $0.title == title }) {
var accounts = sections[index].accounts
accounts.append(account)
sections[index] = CodexAccountWorkspaceSection(title: title, accounts: accounts)
} else {
sections.append(CodexAccountWorkspaceSection(title: title, accounts: [account]))
}
}
return sections
}
}