diff --git a/Sources/CodexBar/PreferencesProviderSettingsRows.swift b/Sources/CodexBar/PreferencesProviderSettingsRows.swift index e063773dfb..1623384540 100644 --- a/Sources/CodexBar/PreferencesProviderSettingsRows.swift +++ b/Sources/CodexBar/PreferencesProviderSettingsRows.swift @@ -246,6 +246,7 @@ struct ProviderSettingsTokenAccountsRowView: View { let descriptor: ProviderSettingsTokenAccountsDescriptor @State private var newLabel: String = "" @State private var newToken: String = "" + @State private var newOrgID: String = "" var body: some View { VStack(alignment: .leading, spacing: 10) { @@ -317,25 +318,37 @@ struct ProviderSettingsTokenAccountsRowView: View { } if self.descriptor.primaryAddAction == nil { - HStack(spacing: 8) { - TextField("Label", text: self.$newLabel) - .textFieldStyle(.roundedBorder) - .font(.footnote) - SecureField(self.descriptor.placeholder, text: self.$newToken) - .textFieldStyle(.roundedBorder) - .font(.footnote) - Button("Add") { - let label = self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines) - let token = self.newToken.trimmingCharacters(in: .whitespacesAndNewlines) - guard !label.isEmpty, !token.isEmpty else { return } - self.descriptor.addAccount(label, token) - self.newLabel = "" - self.newToken = "" + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 8) { + TextField("Label", text: self.$newLabel) + .textFieldStyle(.roundedBorder) + .font(.footnote) + SecureField(self.descriptor.placeholder, text: self.$newToken) + .textFieldStyle(.roundedBorder) + .font(.footnote) + Button("Add") { + let label = self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines) + let token = self.newToken.trimmingCharacters(in: .whitespacesAndNewlines) + guard !label.isEmpty, !token.isEmpty else { return } + let orgID = self.descriptor.showsOrganizationField + ? self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines) + : "" + self.descriptor.addAccount(label, token, orgID.isEmpty ? nil : orgID) + self.newLabel = "" + self.newToken = "" + self.newOrgID = "" + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || + self.newToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + if self.descriptor.showsOrganizationField { + TextField("Org ID (optional)", text: self.$newOrgID) + .textFieldStyle(.roundedBorder) + .font(.footnote) + .help("Optional organization ID for accounts linked to multiple Anthropic organizations.") } - .buttonStyle(.bordered) - .controlSize(.small) - .disabled(self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || - self.newToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } } diff --git a/Sources/CodexBar/PreferencesProvidersPane+Testing.swift b/Sources/CodexBar/PreferencesProvidersPane+Testing.swift index cf106b036d..585690c089 100644 --- a/Sources/CodexBar/PreferencesProvidersPane+Testing.swift +++ b/Sources/CodexBar/PreferencesProvidersPane+Testing.swift @@ -247,7 +247,8 @@ enum ProvidersPaneTestHarness { accounts: { [] }, activeIndex: { 0 }, setActiveIndex: { _ in }, - addAccount: { _, _ in }, + showsOrganizationField: false, + addAccount: { _, _, _ in }, removeAccount: { _ in }, primaryAddActionTitle: nil, primaryAddAction: nil, diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 2908cd94f0..f4e383001f 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -384,8 +384,13 @@ struct ProvidersPane: View { } } }, - addAccount: { label, token in - self.settings.addTokenAccount(provider: provider, label: label, token: token) + showsOrganizationField: provider == .claude, + addAccount: { label, token, organizationID in + self.settings.addTokenAccount( + provider: provider, + label: label, + token: token, + organizationID: organizationID) Task { @MainActor in await ProviderInteractionContext.$current.withValue(.userInitiated) { await self.store.refreshProvider(provider, allowDisabled: true) diff --git a/Sources/CodexBar/Providers/Shared/ProviderSettingsDescriptors.swift b/Sources/CodexBar/Providers/Shared/ProviderSettingsDescriptors.swift index 9b419ce431..449ff4b923 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderSettingsDescriptors.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderSettingsDescriptors.swift @@ -134,7 +134,8 @@ struct ProviderSettingsTokenAccountsDescriptor: Identifiable { let accounts: () -> [ProviderTokenAccount] let activeIndex: () -> Int let setActiveIndex: (Int) -> Void - let addAccount: (_ label: String, _ token: String) -> Void + let showsOrganizationField: Bool + let addAccount: (_ label: String, _ token: String, _ organizationID: String?) -> Void let removeAccount: (_ accountID: UUID) -> Void let primaryAddActionTitle: String? let primaryAddAction: (() async -> Void)? diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift index 41d65d8542..d6a74c7e9a 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift @@ -488,10 +488,8 @@ public enum ClaudeWebAPIFetcher { sessionResets = self.parseISO8601Date(resetsAt) } } - guard let sessionPercent else { - // If we can't parse session utilization, treat this as a failure so callers can fall back to the CLI. - throw FetchError.invalidResponse - } + // Enterprise/credit-based accounts return null for five_hour; treat as 0% rather than an error. + let resolvedSessionPercent = sessionPercent ?? 0.0 // Parse seven_day (weekly) usage var weeklyPercent: Double? @@ -521,7 +519,7 @@ public enum ClaudeWebAPIFetcher { } return WebUsageData( - sessionPercentUsed: sessionPercent, + sessionPercentUsed: resolvedSessionPercent, sessionResetsAt: sessionResets, weeklyPercentUsed: weeklyPercent, weeklyResetsAt: weeklyResets, diff --git a/Tests/CodexBarTests/ClaudeWebEnterpriseUsageTests.swift b/Tests/CodexBarTests/ClaudeWebEnterpriseUsageTests.swift new file mode 100644 index 0000000000..ee81a881b7 --- /dev/null +++ b/Tests/CodexBarTests/ClaudeWebEnterpriseUsageTests.swift @@ -0,0 +1,19 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct ClaudeWebEnterpriseUsageTests { + @Test + func `parses usage response when session window is null`() throws { + let json = """ + { + "five_hour": null, + "seven_day": { "utilization": 42, "resets_at": "2025-12-29T23:00:00.000Z" } + } + """ + let data = Data(json.utf8) + let parsed = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(data) + #expect(parsed.sessionPercentUsed == 0) + #expect(parsed.weeklyPercentUsed == 42) + } +} diff --git a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift index d4ef4ddb1f..87874a882a 100644 --- a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift +++ b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift @@ -14,6 +14,19 @@ struct ProvidersPaneCoverageTests { ProvidersPaneTestHarness.exercise(settings: settings, store: store) } + @Test + func `claude token account descriptor shows organization field`() throws { + let settings = Self.makeSettingsStore(suite: "ProvidersPaneCoverageTests-claude-org-field") + let store = Self.makeUsageStore(settings: settings) + let pane = ProvidersPane(settings: settings, store: store) + + let claudeDescriptor = try #require(pane._test_tokenAccountDescriptor(for: .claude)) + #expect(claudeDescriptor.showsOrganizationField) + + let copilotDescriptor = try #require(pane._test_tokenAccountDescriptor(for: .copilot)) + #expect(!copilotDescriptor.showsOrganizationField) + } + @Test func `open router menu bar metric picker shows only automatic and primary`() { let settings = Self.makeSettingsStore(suite: "ProvidersPaneCoverageTests-openrouter-picker")