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
18 changes: 13 additions & 5 deletions Sources/ReviewUI/Sidebar/Accounts/AccountContextMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ struct AccountContextMenuView: View {
store.auth
}

private var usagePresentation: AccountUsageSummaryPresentation {
AccountUsageSummaryPresentation(account: account)
}

private func requestDestructiveAccountAction() {
if auth.selectedAccount == account {
store.requestSignOutActiveAccount(requiresConfirmation: store.hasRunningReviewRuns)
Expand All @@ -23,13 +27,17 @@ struct AccountContextMenuView: View {
store.requestSwitchAccountFromUserAction(account)
}
.disabled(store.switchActionIsDisabled(for: account))

Button("Refresh", systemImage: "arrow.clockwise") {
refreshRateLimits()

if usagePresentation.showsRateLimitControls {
Button("Refresh", systemImage: "arrow.clockwise") {
refreshRateLimits()
}
}
}
Section{
AccountRateLimitsSectionView(account:account)
if usagePresentation.showsRateLimitControls {
Section{
AccountRateLimitsSectionView(account:account)
}
}
Section{
Button("Sign Out", systemImage: "rectangle.portrait.and.arrow.right", role:.destructive) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ struct ReviewMonitorAccountRowView: View {
account: account
)
} label: {
AccountRateLimitGaugesView(
AccountUsageSummaryView(
account: account
)
.textScale(.secondary)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
import SwiftUI
import CodexReviewKit

struct AccountRateLimitGaugesView: View {
enum AccountUsageSummaryPresentation: Equatable {
case loading
case gauges
case provider(title: String, systemImage: String)

@MainActor
init(account: CodexReviewAccount?) {
guard let account else {
self = .loading
return
}
guard account.capabilities.supportsRateLimitRefresh == false else {
self = account.rateLimits.isEmpty ? .loading : .gauges
return
}

self = switch account.kind {
case .chatGPT:
.provider(title: "Using ChatGPT", systemImage: "person.crop.circle.fill")
case .apiKey:
.provider(title: "Using API Key", systemImage: "key.fill")
case .amazonBedrock:
.provider(title: "Using Amazon Bedrock", systemImage: "cloud.fill")
}
}

var showsRateLimitControls: Bool {
switch self {
case .loading, .gauges:
true
case .provider:
false
}
}
}

struct AccountUsageSummaryView: View {
var account: CodexReviewAccount?

private static let placeholderRateLimits: [CodexReviewAccount.RateLimitWindow] = [
Expand All @@ -23,12 +59,10 @@ struct AccountRateLimitGaugesView: View {
rateLimits.isEmpty ? Self.placeholderRateLimits : rateLimits
}

private var showsRedactedRateLimits: Bool {
rateLimits.isEmpty
}

var body: some View {
Group {
let presentation = AccountUsageSummaryPresentation(account: account)
switch presentation {
case .loading, .gauges:
VStack(spacing:0) {
ForEach(displayedRateLimits) { window in
RateLimitWindowGaugeView(window: window)
Expand All @@ -37,8 +71,16 @@ struct AccountRateLimitGaugesView: View {
}
}
}
.redacted(reason: showsRedactedRateLimits ? .placeholder : [])
.animation(.easeInOut, value: showsRedactedRateLimits)
.redacted(reason: presentation == .loading ? .placeholder : [])
.animation(.easeInOut, value: presentation == .loading)
case .provider(let title, let systemImage):
Label {
Text(title)
.lineLimit(1)
} icon: {
Image(systemName: systemImage)
}
.foregroundStyle(.secondary)
}
}
}
Expand Down Expand Up @@ -95,11 +137,23 @@ extension CodexReviewAccount.RateLimitWindow {

#if DEBUG
#Preview("Account Rate Limit Gauges") {
AccountRateLimitGaugesView(account: makeAccountRateLimitGaugesPreviewAccount())
AccountUsageSummaryView(account: makeAccountRateLimitGaugesPreviewAccount())
.padding()
.frame(width: 320)
}

#Preview("API Key Usage") {
AccountUsageSummaryView(
account: CodexReviewAccount(
accountKey: "api-key",
email: "API Key",
kind: .apiKey
)
)
.padding()
.frame(width: 320)
}

@MainActor
private func makeAccountRateLimitGaugesPreviewAccount() -> CodexReviewAccount {
let account = CodexReviewAccount(email: "review@example.com", planType: "pro")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,15 @@ struct StatusView: View {

var body: some View {
let currentAccount = store.auth.selectedAccount
let usagePresentation = AccountUsageSummaryPresentation(account: currentAccount)
VStack{
Menu {
Section(currentAccount?.reviewMonitorIdentityName ?? "") {
AccountRateLimitsSectionView(account: currentAccount)
if let currentAccount,
usagePresentation.showsRateLimitControls
{
Section(currentAccount.reviewMonitorIdentityName) {
AccountRateLimitsSectionView(account: currentAccount)
}
}
if let showSettings {
Section{
Expand All @@ -196,7 +201,7 @@ struct StatusView: View {
}
}
} label: {
AccountRateLimitGaugesView(account: currentAccount)
AccountUsageSummaryView(account: currentAccount)
.transition(.blurReplace)
.animation(.default, value: currentAccount)
.frame(maxWidth: .infinity, alignment: .leading)
Expand Down
55 changes: 55 additions & 0 deletions Tests/ReviewUITests/ReviewMonitorAddAccountActionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,61 @@ struct ReviewMonitorAddAccountActionTests {
#expect(bedrockAccount.reviewMonitorIdentityName == "Amazon Bedrock")
}

@Test func usageSummaryDistinguishesLoadingGaugesAndProviderModes() {
let loadingAccount = CodexReviewAccount(
accountKey: "chatgpt@example.com",
email: "chatgpt@example.com",
kind: .chatGPT
)
let gaugesAccount = CodexReviewAccount(
accountKey: "gauges@example.com",
email: "gauges@example.com",
kind: .chatGPT
)
gaugesAccount.updateRateLimits([
(windowDurationMinutes: 300, usedPercent: 25, resetsAt: nil),
])
let apiKeyAccount = CodexReviewAccount(
accountKey: "api-key",
email: "API Key",
kind: .apiKey
)
let bedrockAccount = CodexReviewAccount(
accountKey: "bedrock",
email: "Amazon Bedrock",
kind: .amazonBedrock
)
let capabilityDrivenAccount = CodexReviewAccount(
accountKey: "chatgpt-without-limits",
email: "chatgpt-without-limits@example.com",
kind: .chatGPT,
capabilities: .noCodexRateLimits
)

#expect(AccountUsageSummaryPresentation(account: nil) == .loading)
#expect(AccountUsageSummaryPresentation(account: loadingAccount) == .loading)
#expect(AccountUsageSummaryPresentation(account: gaugesAccount) == .gauges)
#expect(
AccountUsageSummaryPresentation(account: apiKeyAccount)
== .provider(title: "Using API Key", systemImage: "key.fill")
)
#expect(
AccountUsageSummaryPresentation(account: bedrockAccount)
== .provider(title: "Using Amazon Bedrock", systemImage: "cloud.fill")
)
#expect(
AccountUsageSummaryPresentation(account: capabilityDrivenAccount)
== .provider(title: "Using ChatGPT", systemImage: "person.crop.circle.fill")
)
#expect(AccountUsageSummaryPresentation.loading.showsRateLimitControls)
#expect(AccountUsageSummaryPresentation.gauges.showsRateLimitControls)
#expect(
AccountUsageSummaryPresentation
.provider(title: "Using API Key", systemImage: "key.fill")
.showsRateLimitControls == false
)
}

@Test func operationFailureUsesTheAccountActionAlertFlow() async throws {
let store = CodexReviewStore.makePreviewStore()

Expand Down
25 changes: 25 additions & 0 deletions Tests/ReviewUITests/ReviewUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,31 @@ struct ReviewUITests {
#expect(accountsViewController.hasTemporaryContextMenuForTesting == false)
}

@Test func apiKeyAccountContextMenuOmitsRateLimitActions() throws {
let apiKeyAccount = CodexReviewAccount(
accountKey: "api-key",
email: "API Key",
kind: .apiKey
)
let store = CodexReviewStore.makePreviewStore()
store.loadForTesting(
serverState: .running,
account: apiKeyAccount,
persistedAccounts: [apiKeyAccount]
)
let displayedAPIKeyAccount = try #require(store.auth.persistedAccounts.first)

let menu = NSHostingMenu(
rootView: AccountContextMenuView(
store: store,
account: displayedAPIKeyAccount
)
)
let presentedTitles = menu.items.map(\.title).filter { $0.isEmpty == false }

#expect(presentedTitles == ["API Key", "Switch", "Sign Out"])
}

@Test func accountOutlineRowsRejectUserSelection() async throws {
let activeAccount = CodexReviewAccount(email: "active@example.com", planType: "pro")
let otherAccount = CodexReviewAccount(email: "other@example.com", planType: "plus")
Expand Down