Skip to content

Commit 58ba4fa

Browse files
committed
Add optional Codex profile discovery
1 parent b731b4c commit 58ba4fa

23 files changed

Lines changed: 1492 additions & 39 deletions

Sources/CodexBar/PreferencesCodexAccountsSection.swift

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ struct CodexAccountsSectionNotice: Equatable {
2121
let tone: Tone
2222
}
2323

24+
struct CodexDiscoveredProfileState: Identifiable, Equatable {
25+
let id: String
26+
let title: String
27+
let subtitle: String?
28+
let detail: String?
29+
let isDisplayed: Bool
30+
let isLive: Bool
31+
}
32+
2433
struct CodexAccountsSectionState: Equatable {
2534
let visibleAccounts: [CodexVisibleAccount]
2635
let activeVisibleAccountID: String?
@@ -29,6 +38,8 @@ struct CodexAccountsSectionState: Equatable {
2938
let authenticatingManagedAccountID: UUID?
3039
let isAuthenticatingLiveAccount: Bool
3140
let notice: CodexAccountsSectionNotice?
41+
let localProfiles: [CodexDiscoveredProfileState]
42+
let hasUnavailableSelectedProfile: Bool
3243

3344
var showsActivePicker: Bool {
3445
self.visibleAccounts.count > 1
@@ -84,6 +95,17 @@ struct CodexAccountsSectionState: Equatable {
8495
}
8596
return "Re-auth"
8697
}
98+
99+
var showsLocalProfiles: Bool {
100+
!self.localProfiles.isEmpty || self.hasUnavailableSelectedProfile
101+
}
102+
103+
var localProfilesNotice: CodexAccountsSectionNotice? {
104+
guard self.hasUnavailableSelectedProfile else { return nil }
105+
return CodexAccountsSectionNotice(
106+
text: "The selected local Codex profile is unavailable. Pick another profile or reload profiles.",
107+
tone: .warning)
108+
}
87109
}
88110

89111
@MainActor
@@ -93,6 +115,9 @@ struct CodexAccountsSectionView: View {
93115
let reauthenticateAccount: (CodexVisibleAccount) -> Void
94116
let removeAccount: (CodexVisibleAccount) -> Void
95117
let addAccount: () -> Void
118+
let selectLocalProfile: (String) -> Void
119+
let reloadLocalProfiles: () -> Void
120+
let openLocalProfilesFolder: () -> Void
96121

97122
var body: some View {
98123
ProviderSettingsSection(title: "Accounts") {
@@ -167,6 +192,56 @@ struct CodexAccountsSectionView: View {
167192
.buttonStyle(.bordered)
168193
.controlSize(.small)
169194
.disabled(self.state.canAddAccount == false)
195+
196+
if self.state.showsLocalProfiles {
197+
Divider()
198+
199+
VStack(alignment: .leading, spacing: 10) {
200+
Text("Local Profiles (Advanced)")
201+
.font(.subheadline.weight(.semibold))
202+
203+
Text(
204+
"Reuse existing local Codex profiles/auth files. Selecting one switches CodexBar back to the local live-system account.")
205+
.font(.footnote)
206+
.foregroundStyle(.secondary)
207+
208+
if self.state.localProfiles.isEmpty {
209+
Text("No saved local Codex profiles found in ~/.codex/profiles.")
210+
.font(.footnote)
211+
.foregroundStyle(.secondary)
212+
} else {
213+
VStack(alignment: .leading, spacing: 10) {
214+
ForEach(self.state.localProfiles) { profile in
215+
CodexLocalProfileRowView(
216+
profile: profile,
217+
onSelect: { self.selectLocalProfile(profile.id) })
218+
}
219+
}
220+
}
221+
222+
if let notice = self.state.localProfilesNotice {
223+
Text(notice.text)
224+
.font(.footnote)
225+
.foregroundStyle(notice.tone == .warning ? .red : .secondary)
226+
.fixedSize(horizontal: false, vertical: true)
227+
}
228+
229+
HStack(spacing: 8) {
230+
Button("Reload profiles") {
231+
self.reloadLocalProfiles()
232+
}
233+
.buttonStyle(.bordered)
234+
.controlSize(.small)
235+
236+
Button("Open profiles folder") {
237+
self.openLocalProfilesFolder()
238+
}
239+
.buttonStyle(.bordered)
240+
.controlSize(.small)
241+
}
242+
}
243+
.disabled(self.state.isAuthenticatingManagedAccount || self.state.isAuthenticatingLiveAccount)
244+
}
170245
}
171246
}
172247

@@ -223,3 +298,60 @@ private struct CodexAccountsSectionRowView: View {
223298
}
224299
}
225300
}
301+
302+
private struct CodexLocalProfileRowView: View {
303+
let profile: CodexDiscoveredProfileState
304+
let onSelect: () -> Void
305+
306+
var body: some View {
307+
HStack(alignment: .center, spacing: 12) {
308+
VStack(alignment: .leading, spacing: 3) {
309+
HStack(alignment: .firstTextBaseline, spacing: 6) {
310+
Text(self.profile.title)
311+
.font(.subheadline.weight(.semibold))
312+
if self.profile.isDisplayed {
313+
CodexLocalProfileBadgeView(title: "Displayed", tone: .emphasized)
314+
}
315+
if self.profile.isLive {
316+
CodexLocalProfileBadgeView(title: "Live", tone: .subtle)
317+
}
318+
}
319+
if let subtitle = self.profile.subtitle, !subtitle.isEmpty {
320+
Text(subtitle)
321+
.font(.footnote)
322+
.foregroundStyle(.secondary)
323+
}
324+
if let detail = self.profile.detail, !detail.isEmpty {
325+
Text(detail)
326+
.font(.caption)
327+
.foregroundStyle(.secondary)
328+
}
329+
}
330+
331+
Spacer(minLength: 8)
332+
333+
Button(self.profile.isDisplayed ? "Displayed" : "Display") {
334+
self.onSelect()
335+
}
336+
.buttonStyle(.bordered)
337+
.controlSize(.small)
338+
.disabled(self.profile.isDisplayed)
339+
}
340+
}
341+
}
342+
343+
private struct CodexLocalProfileBadgeView: View {
344+
enum Tone {
345+
case emphasized
346+
case subtle
347+
}
348+
349+
let title: String
350+
let tone: Tone
351+
352+
var body: some View {
353+
Text(self.title)
354+
.font(.caption.weight(.semibold))
355+
.foregroundStyle(self.tone == .emphasized ? Color.accentColor : .secondary)
356+
}
357+
}

Sources/CodexBar/PreferencesProvidersPane+Testing.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ extension ProvidersPane {
4242
func _test_reauthenticateCodexAccount(_ account: CodexVisibleAccount) async {
4343
await self.reauthenticateCodexAccount(account)
4444
}
45+
46+
func _test_selectCodexLocalProfile(path: String) async {
47+
await self.selectCodexLocalProfile(path: path)
48+
}
49+
50+
func _test_reloadCodexLocalProfiles() async {
51+
await self.reloadCodexLocalProfiles()
52+
}
4553
}
4654

4755
@MainActor

Sources/CodexBar/PreferencesProvidersPane.swift

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ struct ProvidersPane: View {
9191
Task { @MainActor in
9292
await self.addManagedCodexAccount()
9393
}
94+
},
95+
selectLocalProfile: { profilePath in
96+
Task { @MainActor in
97+
await self.selectCodexLocalProfile(path: profilePath)
98+
}
99+
},
100+
reloadLocalProfiles: {
101+
Task { @MainActor in
102+
await self.reloadCodexLocalProfiles()
103+
}
104+
},
105+
openLocalProfilesFolder: {
106+
self.openCodexProfilesFolder()
94107
})
95108
}
96109
})
@@ -181,6 +194,39 @@ struct ProvidersPane: View {
181194
func codexAccountsSectionState(for provider: UsageProvider) -> CodexAccountsSectionState? {
182195
guard provider == .codex else { return nil }
183196
let projection = self.settings.codexVisibleAccountProjection
197+
let profiles = self.settings.codexProfiles()
198+
let defaultAuthPath = CodexOAuthCredentialsStore.authFilePath().standardizedFileURL.path
199+
let selectedProfilePath: String? = {
200+
if let rawSelectedPath = self.settings.selectedCodexProfilePath?.trimmingCharacters(in: .whitespacesAndNewlines),
201+
!rawSelectedPath.isEmpty
202+
{
203+
let standardized = URL(fileURLWithPath: rawSelectedPath).standardizedFileURL.path
204+
if standardized == defaultAuthPath {
205+
return profiles.first(where: \.isActiveInCodex)?.fileURL.standardizedFileURL.path
206+
}
207+
if profiles.contains(where: { $0.fileURL.standardizedFileURL.path == standardized }) {
208+
return standardized
209+
}
210+
return nil
211+
}
212+
return self.settings.selectedCodexProfile()?.fileURL.standardizedFileURL.path
213+
}()
214+
let localProfiles = profiles.map { profile in
215+
let cleanedPlan = profile.plan.flatMap { plan in
216+
let cleaned = UsageFormatter.cleanPlanName(plan)
217+
return cleaned.isEmpty ? plan : cleaned
218+
}
219+
let title = profile.fileURL.standardizedFileURL.path == defaultAuthPath && profile.alias == "Live"
220+
? "Live (unsaved)"
221+
: profile.alias
222+
return CodexDiscoveredProfileState(
223+
id: profile.fileURL.standardizedFileURL.path,
224+
title: title,
225+
subtitle: PersonalInfoRedactor.redactEmail(profile.accountEmail, isEnabled: self.settings.hidePersonalInfo),
226+
detail: cleanedPlan,
227+
isDisplayed: selectedProfilePath == profile.fileURL.standardizedFileURL.path,
228+
isLive: profile.isActiveInCodex)
229+
}
184230
let degradedNotice: CodexAccountsSectionNotice? = if projection.hasUnreadableAddedAccountStore {
185231
CodexAccountsSectionNotice(
186232
text: "Managed account storage is unreadable. Live account access is still available, "
@@ -197,7 +243,9 @@ struct ProvidersPane: View {
197243
isAuthenticatingManagedAccount: self.managedCodexAccountCoordinator.isAuthenticatingManagedAccount,
198244
authenticatingManagedAccountID: self.managedCodexAccountCoordinator.authenticatingManagedAccountID,
199245
isAuthenticatingLiveAccount: self.isAuthenticatingLiveCodexAccount,
200-
notice: self.codexAccountsNotice ?? degradedNotice)
246+
notice: self.codexAccountsNotice ?? degradedNotice,
247+
localProfiles: localProfiles,
248+
hasUnavailableSelectedProfile: self.settings.hasUnavailableSelectedCodexProfile)
201249
}
202250

203251
func selectCodexVisibleAccount(id: String) async {
@@ -263,6 +311,27 @@ struct ProvidersPane: View {
263311
}
264312
}
265313

314+
func selectCodexLocalProfile(path: String) async {
315+
self.codexAccountsNotice = nil
316+
self.settings.codexActiveSource = .liveSystem
317+
self.settings.selectCodexProfile(path: path)
318+
await self.refreshCodexProvider()
319+
}
320+
321+
func reloadCodexLocalProfiles() async {
322+
self.codexAccountsNotice = nil
323+
self.settings.reloadCodexProfiles()
324+
await self.refreshCodexProvider()
325+
}
326+
327+
func openCodexProfilesFolder() {
328+
let profilesURL = CodexOAuthCredentialsStore.authFilePath()
329+
.deletingLastPathComponent()
330+
.appendingPathComponent("profiles", isDirectory: true)
331+
let fallbackURL = profilesURL.deletingLastPathComponent()
332+
NSWorkspace.shared.open(FileManager.default.fileExists(atPath: profilesURL.path) ? profilesURL : fallbackURL)
333+
}
334+
266335
func requestManagedCodexAccountRemoval(_ account: CodexVisibleAccount) {
267336
guard let accountID = account.storedAccountID else { return }
268337
self.activeConfirmation = ProviderSettingsConfirmationState(

Sources/CodexBar/ProviderRegistry.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ struct ProviderRegistry {
108108
env[key] = value
109109
}
110110
}
111+
if provider == .codex {
112+
let profileOverride = settings.codexEnvironmentOverrides(tokenOverride: tokenOverride)
113+
for (key, value) in profileOverride {
114+
env[key] = value
115+
}
116+
}
111117
// Managed Codex routing only scopes remote account fetches such as identity, plan,
112118
// quotas, and dashboard data, and only when the active source is a managed account.
113119
// Token-cost/session history is intentionally not routed through the managed home

0 commit comments

Comments
 (0)