import Foundation public struct CodexVisibleAccount: Equatable, Identifiable, Sendable { public let id: String public let email: String public let workspaceLabel: String? public let workspaceAccountID: String? public let authFingerprint: String? public let storedAccountID: UUID? public let selectionSource: CodexActiveSource public let isActive: Bool public let isLive: Bool public let canReauthenticate: Bool public let canRemove: Bool public init( id: String, email: String, workspaceLabel: String? = nil, workspaceAccountID: String? = nil, authFingerprint: String? = nil, storedAccountID: UUID?, selectionSource: CodexActiveSource, isActive: Bool, isLive: Bool, canReauthenticate: Bool, canRemove: Bool) { self.id = id self.email = email self.workspaceLabel = Self.normalizeWorkspaceLabel(workspaceLabel) self.workspaceAccountID = workspaceAccountID self.authFingerprint = CodexAuthFingerprint.normalize(authFingerprint) self.storedAccountID = storedAccountID self.selectionSource = selectionSource self.isActive = isActive self.isLive = isLive self.canReauthenticate = canReauthenticate self.canRemove = canRemove } public var displayName: String { guard let workspaceLabel else { return self.email } return "\(self.email) — \(workspaceLabel)" } public var menuDisplayName: String { guard let menuWorkspaceLabel else { return self.email } return "\(self.email) — \(menuWorkspaceLabel)" } public var menuWorkspaceLabel: String? { guard let workspaceLabel, workspaceLabel.compare("Personal", options: [.caseInsensitive]) != .orderedSame else { return nil } return workspaceLabel } public var authenticationHealthLabel: String? { guard !self.isLive, self.storedAccountID != nil, self.authFingerprint == nil else { return nil } return "Missing auth" } private static func normalizeWorkspaceLabel(_ workspaceLabel: String?) -> String? { guard let trimmed = workspaceLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { return nil } return trimmed } } public struct CodexVisibleAccountProjection: Equatable, Sendable { public let visibleAccounts: [CodexVisibleAccount] public let activeVisibleAccountID: String? public let liveVisibleAccountID: String? public let hasUnreadableAddedAccountStore: Bool public init( visibleAccounts: [CodexVisibleAccount], activeVisibleAccountID: String?, liveVisibleAccountID: String?, hasUnreadableAddedAccountStore: Bool) { self.visibleAccounts = visibleAccounts self.activeVisibleAccountID = activeVisibleAccountID self.liveVisibleAccountID = liveVisibleAccountID self.hasUnreadableAddedAccountStore = hasUnreadableAddedAccountStore } public func source(forVisibleAccountID id: String) -> CodexActiveSource? { self.visibleAccounts.first { $0.id == id }?.selectionSource } } extension DefaultCodexAccountReconciler { public func loadVisibleAccounts() -> CodexVisibleAccountProjection { CodexVisibleAccountProjection.make(from: self.loadSnapshot()) } } extension CodexVisibleAccountProjection { public static func make(from snapshot: CodexAccountReconciliationSnapshot) -> CodexVisibleAccountProjection { let resolvedActiveSource = CodexActiveSourceResolver.resolve(from: snapshot).resolvedSource var drafts: [VisibleAccountDraft] = [] for storedAccount in snapshot.storedAccounts { let normalizedEmail = snapshot.runtimeEmail(for: storedAccount) let runtimeIdentity = snapshot.runtimeIdentity(for: storedAccount) let runtimeWorkspaceAccountID: String? = switch runtimeIdentity { case let .providerAccount(id): ManagedCodexAccount.normalizeWorkspaceAccountID(id) case .emailOnly, .unresolved: nil } drafts.append(VisibleAccountDraft( email: normalizedEmail, workspaceLabel: Self.normalizeWorkspaceLabel(storedAccount.workspaceLabel), workspaceAccountID: storedAccount.workspaceAccountID ?? runtimeWorkspaceAccountID, authFingerprint: storedAccount.authFingerprint, storedAccountID: storedAccount.id, selectionSource: .managedAccount(id: storedAccount.id), isLive: false, canReauthenticate: true, canRemove: true, identity: runtimeIdentity)) } if let liveSystemAccount = snapshot.liveSystemAccount { let normalizedEmail = Self.normalizeVisibleEmail(liveSystemAccount.email) let liveIdentity = snapshot.runtimeIdentity(for: liveSystemAccount) if let exactStoredAccountID = snapshot.matchingStoredAccountForLiveSystemAccount?.id, let exactIndex = drafts.firstIndex(where: { $0.storedAccountID == exactStoredAccountID }) { let existingDraft = drafts[exactIndex] let liveWorkspaceLabel = Self.normalizeWorkspaceLabel(liveSystemAccount.workspaceLabel) drafts[exactIndex] = VisibleAccountDraft( email: existingDraft.email, workspaceLabel: liveWorkspaceLabel ?? existingDraft.workspaceLabel, workspaceAccountID: liveSystemAccount.workspaceAccountID ?? existingDraft.workspaceAccountID, authFingerprint: liveSystemAccount.authFingerprint ?? existingDraft.authFingerprint, storedAccountID: existingDraft.storedAccountID, selectionSource: .liveSystem, isLive: true, canReauthenticate: existingDraft.canReauthenticate, canRemove: existingDraft.canRemove, identity: liveIdentity) } else if let existingIndex = drafts.firstIndex(where: { draft in CodexIdentityMatcher.matches( draft.identity, lhsEmail: draft.email, liveIdentity, rhsEmail: normalizedEmail) }) { let existingDraft = drafts[existingIndex] let liveWorkspaceLabel = Self.normalizeWorkspaceLabel(liveSystemAccount.workspaceLabel) drafts[existingIndex] = VisibleAccountDraft( email: existingDraft.email, workspaceLabel: liveWorkspaceLabel ?? existingDraft.workspaceLabel, workspaceAccountID: liveSystemAccount.workspaceAccountID ?? existingDraft.workspaceAccountID, authFingerprint: liveSystemAccount.authFingerprint ?? existingDraft.authFingerprint, storedAccountID: existingDraft.storedAccountID, selectionSource: .liveSystem, isLive: true, canReauthenticate: existingDraft.canReauthenticate, canRemove: existingDraft.canRemove, identity: liveIdentity) } else { drafts.append(VisibleAccountDraft( email: normalizedEmail, workspaceLabel: Self.normalizeWorkspaceLabel(liveSystemAccount.workspaceLabel), workspaceAccountID: liveSystemAccount.workspaceAccountID, authFingerprint: liveSystemAccount.authFingerprint, storedAccountID: nil, selectionSource: .liveSystem, isLive: true, canReauthenticate: true, canRemove: false, identity: liveIdentity)) } } let livePath = snapshot.liveSystemAccount.flatMap { CodexHomeScope.normalizedHomePath($0.codexHomePath) } let managedPaths = Set(snapshot.storedAccounts.compactMap { CodexHomeScope.normalizedHomePath($0.managedHomePath) }) for profileAccount in snapshot.profileHomeAccounts { let profilePath = CodexHomeScope.normalizedHomePath(profileAccount.codexHomePath) guard let profilePath, profilePath != livePath, !managedPaths.contains(profilePath) else { continue } drafts.append(VisibleAccountDraft( email: Self.normalizeVisibleEmail(profileAccount.email), workspaceLabel: Self.normalizeWorkspaceLabel(profileAccount.workspaceLabel), workspaceAccountID: profileAccount.workspaceAccountID, authFingerprint: profileAccount.authFingerprint, storedAccountID: nil, selectionSource: .profileHome(path: profilePath), isLive: false, canReauthenticate: false, canRemove: false, identity: snapshot.runtimeIdentity(for: profileAccount))) } let groupedByEmail = Dictionary(grouping: drafts.indices, by: { drafts[$0].email }) let visibleAccounts = drafts.map { draft in let id = Self.visibleAccountID(for: draft, emailGroupSize: groupedByEmail[draft.email]?.count ?? 0) let isActive = switch resolvedActiveSource { case .liveSystem: draft.selectionSource == .liveSystem case let .managedAccount(id): draft.selectionSource == .managedAccount(id: id) case let .profileHome(path): draft.selectionSource == .profileHome(path: path) } return CodexVisibleAccount( id: id, email: draft.email, workspaceLabel: draft.workspaceLabel, workspaceAccountID: draft.workspaceAccountID, authFingerprint: draft.authFingerprint, storedAccountID: draft.storedAccountID, selectionSource: draft.selectionSource, isActive: isActive, isLive: draft.isLive, canReauthenticate: draft.canReauthenticate, canRemove: draft.canRemove) }.sorted { lhs, rhs in if lhs.email != rhs.email { return lhs.email < rhs.email } if lhs.isLive != rhs.isLive { return lhs.isLive && !rhs.isLive } if lhs.displayName != rhs.displayName { return lhs.displayName < rhs.displayName } return lhs.id < rhs.id } return CodexVisibleAccountProjection( visibleAccounts: visibleAccounts, activeVisibleAccountID: visibleAccounts.first { $0.isActive }?.id, liveVisibleAccountID: visibleAccounts.first { $0.isLive }?.id, hasUnreadableAddedAccountStore: snapshot.hasUnreadableAddedAccountStore) } private static func normalizeVisibleEmail(_ email: String) -> String { email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } private static func normalizeWorkspaceLabel(_ workspaceLabel: String?) -> String? { guard let trimmed = workspaceLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { return nil } return trimmed } private static func visibleAccountID(for draft: VisibleAccountDraft, emailGroupSize: Int) -> String { guard emailGroupSize > 1 else { return draft.email } switch draft.selectionSource { case .liveSystem: return "live:\(CodexIdentityMatcher.selectionKey(for: draft.identity, fallbackEmail: draft.email))" case let .managedAccount(id): return "managed:\(id.uuidString.lowercased())" case let .profileHome(path): return "profile:\(path)" } } } private struct VisibleAccountDraft { let email: String let workspaceLabel: String? let workspaceAccountID: String? let authFingerprint: String? let storedAccountID: UUID? let selectionSource: CodexActiveSource let isLive: Bool let canReauthenticate: Bool let canRemove: Bool let identity: CodexIdentity }