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
32 changes: 32 additions & 0 deletions Sources/ContextPanelCore/AccountConfigurationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,38 @@ public struct LocalProviderAccountConfiguration: Codable, Equatable, Identifiabl
}
}

public extension LocalProviderAccountConfiguration {
var providerReportAccountIDs: [String] {
switch connectorKind {
case .codexRateLimits, .geminiCodeAssist:
guard let authPath else { return [] }
return Self.localAccountIDs(provider: provider, path: authPath)
case .claudeLocalStatus:
guard let authPath = effectiveAuthPath else { return [] }
return Self.localAccountIDs(provider: provider, path: authPath)
case .claudeOAuthUsage:
return [ConnectorRedactor.localAccountID(provider: provider, stableID: id)]
}
}

func matchesProviderReport(_ report: StoredProviderReport) -> Bool {
guard report.provider == provider else { return false }
if let configuredAccountID = report.configuredAccountID {
return configuredAccountID == id
}
return report.accountID == id || providerReportAccountIDs.contains(report.accountID)
}

private static func localAccountIDs(provider: Provider, path: String) -> [String] {
var ids = [ConnectorRedactor.localAccountID(provider: provider, path: path)]
let expandedPath = NSString(string: path).expandingTildeInPath
if expandedPath != path {
ids.append(ConnectorRedactor.localAccountID(provider: provider, path: expandedPath))
}
return ids
}
}

public struct AccountConfigurationDocument: Codable, Equatable, Sendable {
public let schemaVersion: Int
public var updatedAt: Date
Expand Down
19 changes: 2 additions & 17 deletions Sources/ContextPanelPreview/ContextPanelPreviewApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -663,16 +663,7 @@ final class SettingsPaneModel: ObservableObject {
guard let storedSnapshot else {
return SettingsAccountRefreshSummary(text: "No refresh yet", status: .unknown)
}
let reports = storedSnapshot.reports.filter { report in
switch account.connectorKind {
case .codexRateLimits:
report.provider == .openAI
case .geminiCodeAssist:
report.provider == .google
case .claudeLocalStatus, .claudeOAuthUsage:
report.provider == .anthropic
}
}
let reports = storedSnapshot.reports.filter { account.matchesProviderReport($0) }
guard !reports.isEmpty else {
return SettingsAccountRefreshSummary(text: "No refresh report yet", status: .unknown)
}
Expand Down Expand Up @@ -2328,13 +2319,7 @@ final class ContextPanelAppModel: ObservableObject {
}

func reportNeedsAttention(_ account: LocalProviderAccountConfiguration) -> Bool {
providerReportsNeedingAttention.contains { report in
guard report.provider == account.provider else { return false }
if let configuredAccountID = report.configuredAccountID {
return configuredAccountID == account.id
}
return true
}
providerReportsNeedingAttention.contains { account.matchesProviderReport($0) }
}

var lastSuccessfulProviderRefreshText: String? {
Expand Down
145 changes: 145 additions & 0 deletions Tests/ContextPanelCoreTests/AccountConfigurationStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,151 @@ import Testing
#expect(result.document.accounts.contains { $0.connectorKind == .claudeOAuthUsage && $0.effectiveAuthPath == nil })
}

@Test func localProviderAccountConfigurationMatchesReportsByConfiguredAccountID() throws {
let account = LocalProviderAccountConfiguration(
id: "openai-code-default",
provider: .openAI,
connectorKind: .codexRateLimits,
displayName: "Every Code",
authPath: "/tmp/code-auth.json"
)
let report = StoredProviderReport(
provider: .openAI,
accountID: ConnectorRedactor.localAccountID(provider: .openAI, stableID: "chatgpt:user-a"),
configuredAccountID: "openai-code-default",
accountName: "Every Code",
generatedAt: Date(timeIntervalSince1970: 0),
status: .failure,
errorMessage: nil
)

#expect(account.matchesProviderReport(report))
}

@Test func localProviderAccountConfigurationMatchesLocalConnectorReportsByResolvedPath() throws {
let authPath = "/tmp/gemini-oauth.json"
let account = LocalProviderAccountConfiguration(
id: "gemini-default",
provider: .google,
connectorKind: .geminiCodeAssist,
displayName: "Gemini",
authPath: authPath
)
let report = StoredProviderReport(
provider: .google,
accountID: ConnectorRedactor.localAccountID(provider: .google, path: authPath),
accountName: "Gemini",
generatedAt: Date(timeIntervalSince1970: 0),
status: .stale,
errorMessage: nil
)

#expect(account.matchesProviderReport(report))
}

@Test func localProviderAccountConfigurationMatchesExpandedLocalConnectorPaths() throws {
let expandedPath = "\(ContextPanelLocations.realUserHomeDirectory().path)/.gemini/oauth_creds.json"
let account = LocalProviderAccountConfiguration(
id: "gemini-default",
provider: .google,
connectorKind: .geminiCodeAssist,
displayName: "Gemini",
authPath: "~/.gemini/oauth_creds.json"
)
let report = StoredProviderReport(
provider: .google,
accountID: ConnectorRedactor.localAccountID(provider: .google, path: expandedPath),
accountName: "Gemini",
generatedAt: Date(timeIntervalSince1970: 0),
status: .failure,
errorMessage: nil
)

#expect(account.matchesProviderReport(report))
}

@Test func localProviderAccountConfigurationDoesNotMatchProviderWideReportFromAnotherAccount() throws {
let account = LocalProviderAccountConfiguration(
id: "gemini-default",
provider: .google,
connectorKind: .geminiCodeAssist,
displayName: "Gemini",
authPath: "/tmp/gemini-a.json"
)
let report = StoredProviderReport(
provider: .google,
accountID: ConnectorRedactor.localAccountID(provider: .google, path: "/tmp/gemini-b.json"),
accountName: "Other Gemini",
generatedAt: Date(timeIntervalSince1970: 0),
status: .failure,
errorMessage: nil
)

#expect(!account.matchesProviderReport(report))
}

@Test func localProviderAccountConfigurationUsesConfiguredAccountIDAsAuthoritative() throws {
let account = LocalProviderAccountConfiguration(
id: "openai-code-default",
provider: .openAI,
connectorKind: .codexRateLimits,
displayName: "Every Code",
authPath: "/tmp/code-auth.json"
)
let report = StoredProviderReport(
provider: .openAI,
accountID: "openai-code-default",
configuredAccountID: "openai-codex-default",
accountName: "Codex",
generatedAt: Date(timeIntervalSince1970: 0),
status: .failure,
errorMessage: nil
)

#expect(!account.matchesProviderReport(report))
}

@Test func localProviderAccountConfigurationMatchesClaudeLocalStatusReportsByEffectivePath() throws {
let account = LocalProviderAccountConfiguration(
id: "claude-local-default",
provider: .anthropic,
connectorKind: .claudeLocalStatus,
displayName: "Claude"
)
let report = StoredProviderReport(
provider: .anthropic,
accountID: ConnectorRedactor.localAccountID(
provider: .anthropic,
path: ContextPanelLocations.claudeStatuslineCacheURL().path
),
accountName: "Claude",
generatedAt: Date(timeIntervalSince1970: 0),
status: .stale,
errorMessage: nil
)

#expect(account.matchesProviderReport(report))
}

@Test func localProviderAccountConfigurationMatchesClaudeOAuthReportsByRedactedStableID() throws {
let account = LocalProviderAccountConfiguration(
id: "claude-oauth-default",
provider: .anthropic,
connectorKind: .claudeOAuthUsage,
displayName: "Claude"
)
let report = StoredProviderReport(
provider: .anthropic,
accountID: ConnectorRedactor.localAccountID(provider: .anthropic, stableID: "claude-oauth-default"),
accountName: "Claude",
generatedAt: Date(timeIntervalSince1970: 0),
status: .failure,
errorMessage: nil
)

#expect(account.matchesProviderReport(report))
}

@Test func accountConfigurationStorePreservesCustomAccountsWithoutAddingDefaults() throws {
let url = try temporaryDirectory().appending(path: "accounts.json")
let store = AccountConfigurationStore(configurationURL: url)
Expand Down