Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
132 changes: 132 additions & 0 deletions Sources/CodexBar/PreferencesCodexAccountsSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ struct CodexAccountsSectionNotice: Equatable {
let tone: Tone
}

struct CodexDiscoveredProfileState: Identifiable, Equatable {
let id: String
let title: String
let subtitle: String?
let detail: String?
let isDisplayed: Bool
let isLive: Bool
}

struct CodexAccountsSectionState: Equatable {
let visibleAccounts: [CodexVisibleAccount]
let activeVisibleAccountID: String?
Expand All @@ -29,6 +38,8 @@ struct CodexAccountsSectionState: Equatable {
let authenticatingManagedAccountID: UUID?
let isAuthenticatingLiveAccount: Bool
let notice: CodexAccountsSectionNotice?
let localProfiles: [CodexDiscoveredProfileState]
let hasUnavailableSelectedProfile: Bool

var showsActivePicker: Bool {
self.visibleAccounts.count > 1
Expand Down Expand Up @@ -84,6 +95,17 @@ struct CodexAccountsSectionState: Equatable {
}
return "Re-auth"
}

var showsLocalProfiles: Bool {
!self.localProfiles.isEmpty || self.hasUnavailableSelectedProfile
}

var localProfilesNotice: CodexAccountsSectionNotice? {
guard self.hasUnavailableSelectedProfile else { return nil }
return CodexAccountsSectionNotice(
text: "The selected local Codex profile is unavailable. Pick another profile or reload profiles.",
tone: .warning)
}
}

@MainActor
Expand All @@ -93,6 +115,9 @@ struct CodexAccountsSectionView: View {
let reauthenticateAccount: (CodexVisibleAccount) -> Void
let removeAccount: (CodexVisibleAccount) -> Void
let addAccount: () -> Void
let selectLocalProfile: (String) -> Void
let reloadLocalProfiles: () -> Void
let openLocalProfilesFolder: () -> Void

var body: some View {
ProviderSettingsSection(title: "Accounts") {
Expand Down Expand Up @@ -167,6 +192,56 @@ struct CodexAccountsSectionView: View {
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.state.canAddAccount == false)

if self.state.showsLocalProfiles {
Divider()

VStack(alignment: .leading, spacing: 10) {
Text("Local Profiles (Advanced)")
.font(.subheadline.weight(.semibold))

Text(
"Reuse existing local Codex profiles/auth files. Selecting one switches CodexBar back to the local live-system account.")
.font(.footnote)
.foregroundStyle(.secondary)

if self.state.localProfiles.isEmpty {
Text("No saved local Codex profiles found in ~/.codex/profiles.")
.font(.footnote)
.foregroundStyle(.secondary)
} else {
VStack(alignment: .leading, spacing: 10) {
ForEach(self.state.localProfiles) { profile in
CodexLocalProfileRowView(
profile: profile,
onSelect: { self.selectLocalProfile(profile.id) })
}
}
}

if let notice = self.state.localProfilesNotice {
Text(notice.text)
.font(.footnote)
.foregroundStyle(notice.tone == .warning ? .red : .secondary)
.fixedSize(horizontal: false, vertical: true)
}

HStack(spacing: 8) {
Button("Reload profiles") {
self.reloadLocalProfiles()
}
.buttonStyle(.bordered)
.controlSize(.small)

Button("Open profiles folder") {
self.openLocalProfilesFolder()
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}
.disabled(self.state.isAuthenticatingManagedAccount || self.state.isAuthenticatingLiveAccount)
}
}
}

Expand Down Expand Up @@ -223,3 +298,60 @@ private struct CodexAccountsSectionRowView: View {
}
}
}

private struct CodexLocalProfileRowView: View {
let profile: CodexDiscoveredProfileState
let onSelect: () -> Void

var body: some View {
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(self.profile.title)
.font(.subheadline.weight(.semibold))
if self.profile.isDisplayed {
CodexLocalProfileBadgeView(title: "Displayed", tone: .emphasized)
}
if self.profile.isLive {
CodexLocalProfileBadgeView(title: "Live", tone: .subtle)
}
}
if let subtitle = self.profile.subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
if let detail = self.profile.detail, !detail.isEmpty {
Text(detail)
.font(.caption)
.foregroundStyle(.secondary)
}
}

Spacer(minLength: 8)

Button(self.profile.isDisplayed ? "Displayed" : "Display") {
self.onSelect()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.profile.isDisplayed)
}
}
}

private struct CodexLocalProfileBadgeView: View {
enum Tone {
case emphasized
case subtle
}

let title: String
let tone: Tone

var body: some View {
Text(self.title)
.font(.caption.weight(.semibold))
.foregroundStyle(self.tone == .emphasized ? Color.accentColor : .secondary)
}
}
8 changes: 8 additions & 0 deletions Sources/CodexBar/PreferencesProvidersPane+Testing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ extension ProvidersPane {
func _test_reauthenticateCodexAccount(_ account: CodexVisibleAccount) async {
await self.reauthenticateCodexAccount(account)
}

func _test_selectCodexLocalProfile(path: String) async {
await self.selectCodexLocalProfile(path: path)
}

func _test_reloadCodexLocalProfiles() async {
await self.reloadCodexLocalProfiles()
}
}

@MainActor
Expand Down
71 changes: 70 additions & 1 deletion Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ struct ProvidersPane: View {
Task { @MainActor in
await self.addManagedCodexAccount()
}
},
selectLocalProfile: { profilePath in
Task { @MainActor in
await self.selectCodexLocalProfile(path: profilePath)
}
},
reloadLocalProfiles: {
Task { @MainActor in
await self.reloadCodexLocalProfiles()
}
},
openLocalProfilesFolder: {
self.openCodexProfilesFolder()
})
}
})
Expand Down Expand Up @@ -181,6 +194,39 @@ struct ProvidersPane: View {
func codexAccountsSectionState(for provider: UsageProvider) -> CodexAccountsSectionState? {
guard provider == .codex else { return nil }
let projection = self.settings.codexVisibleAccountProjection
let profiles = self.settings.codexProfiles()
let defaultAuthPath = CodexOAuthCredentialsStore.authFilePath().standardizedFileURL.path
let selectedProfilePath: String? = {
if let rawSelectedPath = self.settings.selectedCodexProfilePath?.trimmingCharacters(in: .whitespacesAndNewlines),
!rawSelectedPath.isEmpty
{
let standardized = URL(fileURLWithPath: rawSelectedPath).standardizedFileURL.path
if standardized == defaultAuthPath {
return profiles.first(where: \.isActiveInCodex)?.fileURL.standardizedFileURL.path
}
if profiles.contains(where: { $0.fileURL.standardizedFileURL.path == standardized }) {
return standardized
}
return nil
}
return self.settings.selectedCodexProfile()?.fileURL.standardizedFileURL.path
}()
let localProfiles = profiles.map { profile in
let cleanedPlan = profile.plan.flatMap { plan in
let cleaned = UsageFormatter.cleanPlanName(plan)
return cleaned.isEmpty ? plan : cleaned
}
let title = profile.fileURL.standardizedFileURL.path == defaultAuthPath && profile.alias == "Live"
? "Live (unsaved)"
: profile.alias
return CodexDiscoveredProfileState(
id: profile.fileURL.standardizedFileURL.path,
title: title,
subtitle: PersonalInfoRedactor.redactEmail(profile.accountEmail, isEnabled: self.settings.hidePersonalInfo),
detail: cleanedPlan,
isDisplayed: selectedProfilePath == profile.fileURL.standardizedFileURL.path,
isLive: profile.isActiveInCodex)
}
let degradedNotice: CodexAccountsSectionNotice? = if projection.hasUnreadableAddedAccountStore {
CodexAccountsSectionNotice(
text: "Managed account storage is unreadable. Live account access is still available, "
Expand All @@ -197,7 +243,9 @@ struct ProvidersPane: View {
isAuthenticatingManagedAccount: self.managedCodexAccountCoordinator.isAuthenticatingManagedAccount,
authenticatingManagedAccountID: self.managedCodexAccountCoordinator.authenticatingManagedAccountID,
isAuthenticatingLiveAccount: self.isAuthenticatingLiveCodexAccount,
notice: self.codexAccountsNotice ?? degradedNotice)
notice: self.codexAccountsNotice ?? degradedNotice,
localProfiles: localProfiles,
hasUnavailableSelectedProfile: self.settings.hasUnavailableSelectedCodexProfile)
}

func selectCodexVisibleAccount(id: String) async {
Expand Down Expand Up @@ -263,6 +311,27 @@ struct ProvidersPane: View {
}
}

func selectCodexLocalProfile(path: String) async {
self.codexAccountsNotice = nil
self.settings.codexActiveSource = .liveSystem
self.settings.selectCodexProfile(path: path)
await self.refreshCodexProvider()
}

func reloadCodexLocalProfiles() async {
self.codexAccountsNotice = nil
self.settings.reloadCodexProfiles()
await self.refreshCodexProvider()
}

func openCodexProfilesFolder() {
let profilesURL = CodexOAuthCredentialsStore.authFilePath()
.deletingLastPathComponent()
.appendingPathComponent("profiles", isDirectory: true)
let fallbackURL = profilesURL.deletingLastPathComponent()
NSWorkspace.shared.open(FileManager.default.fileExists(atPath: profilesURL.path) ? profilesURL : fallbackURL)
}

func requestManagedCodexAccountRemoval(_ account: CodexVisibleAccount) {
guard let accountID = account.storedAccountID else { return }
self.activeConfirmation = ProviderSettingsConfirmationState(
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/ProviderRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ struct ProviderRegistry {
env[key] = value
}
}
if provider == .codex {
let profileOverride = settings.codexEnvironmentOverrides(tokenOverride: tokenOverride)
for (key, value) in profileOverride {
env[key] = value
}
}
// Managed Codex routing only scopes remote account fetches such as identity, plan,
// quotas, and dashboard data, and only when the active source is a managed account.
// Token-cost/session history is intentionally not routed through the managed home
Expand Down
Loading