fix(macos): group usage by selected model
This commit is contained in:
@@ -272,6 +272,14 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var cursor = cursor
|
var cursor = cursor
|
||||||
|
|
||||||
|
if cursor > 0, !menu.items[cursor - 1].isSeparatorItem {
|
||||||
|
let separator = NSMenuItem.separator()
|
||||||
|
separator.tag = self.tag
|
||||||
|
menu.insertItem(separator, at: cursor)
|
||||||
|
cursor += 1
|
||||||
|
}
|
||||||
|
|
||||||
let headerItem = NSMenuItem()
|
let headerItem = NSMenuItem()
|
||||||
headerItem.tag = self.tag
|
headerItem.tag = self.tag
|
||||||
headerItem.isEnabled = false
|
headerItem.isEnabled = false
|
||||||
@@ -292,6 +300,28 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
|||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let selectedProvider = self.selectedUsageProviderId,
|
||||||
|
let primary = rows.first(where: { $0.providerId.lowercased() == selectedProvider }),
|
||||||
|
rows.count > 1
|
||||||
|
{
|
||||||
|
let others = rows.filter { $0.providerId.lowercased() != selectedProvider }
|
||||||
|
|
||||||
|
let item = NSMenuItem()
|
||||||
|
item.tag = self.tag
|
||||||
|
item.isEnabled = true
|
||||||
|
if !others.isEmpty {
|
||||||
|
item.submenu = self.buildUsageOverflowMenu(rows: others, width: width)
|
||||||
|
}
|
||||||
|
item.view = self.makeHostedView(
|
||||||
|
rootView: AnyView(UsageMenuLabelView(row: primary, width: width, showsChevron: !others.isEmpty)),
|
||||||
|
width: width,
|
||||||
|
highlighted: true)
|
||||||
|
menu.insertItem(item, at: cursor)
|
||||||
|
cursor += 1
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let item = NSMenuItem()
|
let item = NSMenuItem()
|
||||||
item.tag = self.tag
|
item.tag = self.tag
|
||||||
@@ -307,11 +337,34 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
|||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var selectedUsageProviderId: String? {
|
||||||
|
guard let model = self.cachedSnapshot?.defaults.model.nonEmpty else { return nil }
|
||||||
|
let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard let slash = trimmed.firstIndex(of: "/") else { return nil }
|
||||||
|
let provider = trimmed[..<slash].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
return provider.nonEmpty
|
||||||
|
}
|
||||||
|
|
||||||
private var usageRows: [UsageRow] {
|
private var usageRows: [UsageRow] {
|
||||||
guard let summary = self.cachedUsageSummary else { return [] }
|
guard let summary = self.cachedUsageSummary else { return [] }
|
||||||
return summary.primaryRows()
|
return summary.primaryRows()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func buildUsageOverflowMenu(rows: [UsageRow], width: CGFloat) -> NSMenu {
|
||||||
|
let menu = NSMenu()
|
||||||
|
for row in rows {
|
||||||
|
let item = NSMenuItem()
|
||||||
|
item.tag = self.tag
|
||||||
|
item.isEnabled = false
|
||||||
|
item.view = self.makeHostedView(
|
||||||
|
rootView: AnyView(UsageMenuLabelView(row: row, width: width)),
|
||||||
|
width: width,
|
||||||
|
highlighted: false)
|
||||||
|
menu.addItem(item)
|
||||||
|
}
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
private var isControlChannelConnected: Bool {
|
private var isControlChannelConnected: Bool {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if let override = self.testControlChannelConnected { return override }
|
if let override = self.testControlChannelConnected { return override }
|
||||||
@@ -938,6 +991,12 @@ extension MenuSessionsInjector {
|
|||||||
self.cacheUpdatedAt = Date()
|
self.cacheUpdatedAt = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setTestingUsageSummary(_ summary: GatewayUsageSummary?, errorText: String? = nil) {
|
||||||
|
self.cachedUsageSummary = summary
|
||||||
|
self.cachedUsageErrorText = errorText
|
||||||
|
self.usageCacheUpdatedAt = Date()
|
||||||
|
}
|
||||||
|
|
||||||
func injectForTesting(into menu: NSMenu) {
|
func injectForTesting(into menu: NSMenu) {
|
||||||
self.inject(into: menu)
|
self.inject(into: menu)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct GatewayUsageSummary: Codable {
|
|||||||
|
|
||||||
struct UsageRow: Identifiable {
|
struct UsageRow: Identifiable {
|
||||||
let id: String
|
let id: String
|
||||||
|
let providerId: String
|
||||||
let displayName: String
|
let displayName: String
|
||||||
let plan: String?
|
let plan: String?
|
||||||
let windowLabel: String?
|
let windowLabel: String?
|
||||||
@@ -73,6 +74,7 @@ extension GatewayUsageSummary {
|
|||||||
if let error = provider.error, provider.windows.isEmpty {
|
if let error = provider.error, provider.windows.isEmpty {
|
||||||
return UsageRow(
|
return UsageRow(
|
||||||
id: provider.provider,
|
id: provider.provider,
|
||||||
|
providerId: provider.provider,
|
||||||
displayName: provider.displayName,
|
displayName: provider.displayName,
|
||||||
plan: provider.plan,
|
plan: provider.plan,
|
||||||
windowLabel: nil,
|
windowLabel: nil,
|
||||||
@@ -87,6 +89,7 @@ extension GatewayUsageSummary {
|
|||||||
|
|
||||||
return UsageRow(
|
return UsageRow(
|
||||||
id: "\(provider.provider)-\(window.label)",
|
id: "\(provider.provider)-\(window.label)",
|
||||||
|
providerId: provider.provider,
|
||||||
displayName: provider.displayName,
|
displayName: provider.displayName,
|
||||||
plan: provider.plan,
|
plan: provider.plan,
|
||||||
windowLabel: window.label,
|
windowLabel: window.label,
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import SwiftUI
|
|||||||
struct UsageMenuLabelView: View {
|
struct UsageMenuLabelView: View {
|
||||||
let row: UsageRow
|
let row: UsageRow
|
||||||
let width: CGFloat
|
let width: CGFloat
|
||||||
|
var showsChevron: Bool = false
|
||||||
|
@Environment(\.menuItemHighlighted) private var isHighlighted
|
||||||
private let paddingLeading: CGFloat = 22
|
private let paddingLeading: CGFloat = 22
|
||||||
private let paddingTrailing: CGFloat = 14
|
private let paddingTrailing: CGFloat = 14
|
||||||
private let barHeight: CGFloat = 6
|
private let barHeight: CGFloat = 6
|
||||||
|
|
||||||
private var primaryTextColor: Color { .primary }
|
private var primaryTextColor: Color {
|
||||||
private var secondaryTextColor: Color { .secondary }
|
self.isHighlighted ? Color(nsColor: .selectedMenuItemTextColor) : .primary
|
||||||
|
}
|
||||||
|
|
||||||
|
private var secondaryTextColor: Color {
|
||||||
|
self.isHighlighted ? Color(nsColor: .selectedMenuItemTextColor).opacity(0.85) : .secondary
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
@@ -36,6 +43,13 @@ struct UsageMenuLabelView: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.fixedSize(horizontal: true, vertical: false)
|
.fixedSize(horizontal: true, vertical: false)
|
||||||
.layoutPriority(2)
|
.layoutPriority(2)
|
||||||
|
|
||||||
|
if self.showsChevron {
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.foregroundStyle(self.secondaryTextColor)
|
||||||
|
.padding(.leading, 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct MenuSessionsInjectorTests {
|
|||||||
let injector = MenuSessionsInjector()
|
let injector = MenuSessionsInjector()
|
||||||
injector.setTestingControlChannelConnected(true)
|
injector.setTestingControlChannelConnected(true)
|
||||||
|
|
||||||
let defaults = SessionDefaults(model: "claude-opus-4-5", contextTokens: 200_000)
|
let defaults = SessionDefaults(model: "anthropic/claude-opus-4-5", contextTokens: 200_000)
|
||||||
let rows = [
|
let rows = [
|
||||||
SessionRow(
|
SessionRow(
|
||||||
id: "main",
|
id: "main",
|
||||||
@@ -66,6 +66,24 @@ struct MenuSessionsInjectorTests {
|
|||||||
rows: rows)
|
rows: rows)
|
||||||
injector.setTestingSnapshot(snapshot, errorText: nil)
|
injector.setTestingSnapshot(snapshot, errorText: nil)
|
||||||
|
|
||||||
|
let usage = GatewayUsageSummary(
|
||||||
|
updatedAt: Date().timeIntervalSince1970 * 1000,
|
||||||
|
providers: [
|
||||||
|
GatewayUsageProvider(
|
||||||
|
provider: "anthropic",
|
||||||
|
displayName: "Claude",
|
||||||
|
windows: [GatewayUsageWindow(label: "5h", usedPercent: 12, resetAt: nil)],
|
||||||
|
plan: "Pro",
|
||||||
|
error: nil),
|
||||||
|
GatewayUsageProvider(
|
||||||
|
provider: "openai-codex",
|
||||||
|
displayName: "Codex",
|
||||||
|
windows: [GatewayUsageWindow(label: "day", usedPercent: 3, resetAt: nil)],
|
||||||
|
plan: nil,
|
||||||
|
error: nil),
|
||||||
|
])
|
||||||
|
injector.setTestingUsageSummary(usage, errorText: nil)
|
||||||
|
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
||||||
menu.addItem(.separator())
|
menu.addItem(.separator())
|
||||||
@@ -73,5 +91,6 @@ struct MenuSessionsInjectorTests {
|
|||||||
|
|
||||||
injector.injectForTesting(into: menu)
|
injector.injectForTesting(into: menu)
|
||||||
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
||||||
|
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user