Skip to content
Merged
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
49 changes: 31 additions & 18 deletions Sources/CodexBar/PreferencesProviderSettingsRows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/PreferencesProvidersPane+Testing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ enum ProvidersPaneTestHarness {
accounts: { [] },
activeIndex: { 0 },
setActiveIndex: { _ in },
addAccount: { _, _ in },
showsOrganizationField: false,
addAccount: { _, _, _ in },
removeAccount: { _ in },
primaryAddActionTitle: nil,
primaryAddAction: nil,
Expand Down
9 changes: 7 additions & 2 deletions Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +491 to +492

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve parse failure when session window is absent

parseUsageResponse now treats any missing/unparseable five_hour window as 0.0 usage, not just the enterprise null case. That means malformed or schema-shifted 200 responses can be accepted as success, and StepExecutor.executeAuto will not fall back to CLI because no error is thrown on the web step. In those cases users can get a false 0% session reading instead of the previous fallback behavior. Consider limiting the default-to-zero path to explicit five_hour: null and still throwing for absent/invalid window structures.

Useful? React with 👍 / 👎.


// Parse seven_day (weekly) usage
var weeklyPercent: Double?
Expand Down Expand Up @@ -521,7 +519,7 @@ public enum ClaudeWebAPIFetcher {
}

return WebUsageData(
sessionPercentUsed: sessionPercent,
sessionPercentUsed: resolvedSessionPercent,
sessionResetsAt: sessionResets,
weeklyPercentUsed: weeklyPercent,
weeklyResetsAt: weeklyResets,
Expand Down
19 changes: 19 additions & 0 deletions Tests/CodexBarTests/ClaudeWebEnterpriseUsageTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
13 changes: 13 additions & 0 deletions Tests/CodexBarTests/ProvidersPaneCoverageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down