664 lines
27 KiB
Swift
664 lines
27 KiB
Swift
import AppKit
|
|
import CodexBarCore
|
|
import QuartzCore
|
|
import SwiftUI
|
|
|
|
enum HostedSubviewContentFingerprint: Equatable {
|
|
case text(String)
|
|
case costHistory(CostHistoryChartMenuView.RenderFingerprint)
|
|
}
|
|
|
|
struct HostedSubviewRenderSignature: Equatable {
|
|
let chartID: String
|
|
let providerRawValue: String?
|
|
let widthBitPattern: UInt64
|
|
let content: HostedSubviewContentFingerprint
|
|
}
|
|
|
|
final class HostedSubviewRenderSignatureBox: NSObject {
|
|
let signature: HostedSubviewRenderSignature
|
|
|
|
init(_ signature: HostedSubviewRenderSignature) {
|
|
self.signature = signature
|
|
}
|
|
}
|
|
|
|
extension StatusItemController {
|
|
private struct HostedSubviewIdentity {
|
|
let chartID: String
|
|
let provider: UsageProvider?
|
|
let providerRawValue: String?
|
|
}
|
|
|
|
func refreshHostedSubviewHeights(in menu: NSMenu) {
|
|
let width = self.renderedMenuWidth(for: menu)
|
|
|
|
for item in menu.items {
|
|
guard let view = item.view else { continue }
|
|
let height = self.hostedSubviewFittingHeight(for: view, width: width)
|
|
view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: height))
|
|
}
|
|
}
|
|
|
|
/// Measures the natural height of a hosted submenu view at the given width using the live
|
|
/// view that will actually be displayed. Hosted chart items used to spin up a second,
|
|
/// throwaway `NSHostingController` purely to size the chart even though every build path
|
|
/// immediately re-measures the live view via `fittingSize`; that extra SwiftUI hierarchy was
|
|
/// pure overhead on a popup-menu hot path, so callers now size the displayed view directly.
|
|
func hostedSubviewFittingHeight(for view: NSView, width: CGFloat) -> CGFloat {
|
|
view.frame = NSRect(origin: .zero, size: NSSize(width: width, height: 1))
|
|
view.layoutSubtreeIfNeeded()
|
|
return view.fittingSize.height
|
|
}
|
|
|
|
func isHostedSubviewMenu(_ menu: NSMenu) -> Bool {
|
|
let ids: Set = [
|
|
Self.usageBreakdownChartID,
|
|
Self.creditsHistoryChartID,
|
|
Self.costHistoryChartID,
|
|
Self.usageHistoryChartID,
|
|
Self.storageBreakdownID,
|
|
Self.statusComponentsID,
|
|
Self.zaiHourlyUsageChartID,
|
|
]
|
|
return menu.items.contains { item in
|
|
guard let id = item.representedObject as? String else { return false }
|
|
return ids.contains(id)
|
|
}
|
|
}
|
|
|
|
func makeHostedSubviewPlaceholderMenu(
|
|
chartID: String,
|
|
provider: UsageProvider? = nil,
|
|
width: CGFloat? = nil) -> NSMenu
|
|
{
|
|
let submenu = NSMenu()
|
|
submenu.autoenablesItems = false
|
|
if let width {
|
|
submenu.minimumWidth = width
|
|
}
|
|
submenu.delegate = self
|
|
let chartItem = NSMenuItem()
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = chartID
|
|
chartItem.toolTip = provider?.rawValue
|
|
submenu.addItem(chartItem)
|
|
return submenu
|
|
}
|
|
|
|
@discardableResult
|
|
func hydrateHostedSubviewMenuIfNeeded(_ menu: NSMenu, width requestedWidth: CGFloat? = nil) -> Bool {
|
|
guard let placeholder = menu.items.first,
|
|
menu.items.count == 1,
|
|
placeholder.view == nil,
|
|
let chartID = placeholder.representedObject as? String
|
|
else {
|
|
return false
|
|
}
|
|
|
|
let width = requestedWidth ?? self.renderedMenuWidth(for: menu.supermenu ?? menu)
|
|
let identity = HostedSubviewIdentity(
|
|
chartID: chartID,
|
|
provider: placeholder.toolTip.flatMap(UsageProvider.init(rawValue:)),
|
|
providerRawValue: placeholder.toolTip)
|
|
menu.removeAllItems()
|
|
|
|
let t0 = CACurrentMediaTime()
|
|
MainThreadActivityBreadcrumb.push("hydrateChart:\(chartID)")
|
|
defer { MainThreadActivityBreadcrumb.pop() }
|
|
let didHydrate: Bool = switch chartID {
|
|
case Self.usageBreakdownChartID:
|
|
self.appendUsageBreakdownChartItem(to: menu, width: width)
|
|
case Self.creditsHistoryChartID:
|
|
self.appendCreditsHistoryChartItem(to: menu, width: width)
|
|
case Self.costHistoryChartID:
|
|
if let providerRawValue = placeholder.toolTip,
|
|
let provider = UsageProvider(rawValue: providerRawValue)
|
|
{
|
|
self.appendCostHistoryChartItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.usageHistoryChartID:
|
|
if let providerRawValue = placeholder.toolTip,
|
|
let provider = UsageProvider(rawValue: providerRawValue)
|
|
{
|
|
self.appendUsageHistoryChartItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.storageBreakdownID:
|
|
if let providerRawValue = placeholder.toolTip,
|
|
let provider = UsageProvider(rawValue: providerRawValue)
|
|
{
|
|
self.appendStorageBreakdownItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.statusComponentsID:
|
|
if let providerRawValue = self.hostedSubviewProviderRawValue(for: placeholder),
|
|
let provider = UsageProvider(rawValue: providerRawValue)
|
|
{
|
|
self.appendStatusComponentsItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.zaiHourlyUsageChartID:
|
|
if let providerRawValue = placeholder.toolTip,
|
|
let provider = UsageProvider(rawValue: providerRawValue)
|
|
{
|
|
self.appendZaiHourlyUsageChartItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
default:
|
|
false
|
|
}
|
|
self.logChartRenderDurationIfSlow("hydrateHostedSubview:\(chartID)", startedAt: t0)
|
|
|
|
if !didHydrate {
|
|
self.appendHostedSubviewUnavailableItem(
|
|
to: menu,
|
|
chartID: chartID,
|
|
providerRawValue: placeholder.toolTip)
|
|
}
|
|
self.recordHostedSubviewRenderSignature(for: menu, identity: identity, width: width)
|
|
return true
|
|
}
|
|
|
|
func refreshHostedSubviewMenu(_ menu: NSMenu) {
|
|
let width = self.renderedMenuWidth(for: menu)
|
|
guard let identity = self.hostedSubviewIdentity(for: menu) else {
|
|
self.refreshHostedSubviewHeights(in: menu)
|
|
return
|
|
}
|
|
let signature = self.hostedSubviewRenderSignature(identity: identity, width: width)
|
|
if self.hostedSubviewRenderSignatures.object(forKey: menu)?.signature == signature {
|
|
if identity.chartID == Self.zaiHourlyUsageChartID {
|
|
self.refreshHostedSubviewHeights(in: menu)
|
|
}
|
|
return
|
|
}
|
|
|
|
menu.removeAllItems()
|
|
let t0 = CACurrentMediaTime()
|
|
MainThreadActivityBreadcrumb.push("refreshChart:\(identity.chartID)")
|
|
defer { MainThreadActivityBreadcrumb.pop() }
|
|
let didHydrate: Bool = switch identity.chartID {
|
|
case Self.usageBreakdownChartID:
|
|
self.appendUsageBreakdownChartItem(to: menu, width: width)
|
|
case Self.creditsHistoryChartID:
|
|
self.appendCreditsHistoryChartItem(to: menu, width: width)
|
|
case Self.costHistoryChartID:
|
|
if let provider = identity.provider {
|
|
self.appendCostHistoryChartItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.usageHistoryChartID:
|
|
if let provider = identity.provider {
|
|
self.appendUsageHistoryChartItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.storageBreakdownID:
|
|
if let provider = identity.provider {
|
|
self.appendStorageBreakdownItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.statusComponentsID:
|
|
if let provider = identity.provider {
|
|
self.appendStatusComponentsItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
case Self.zaiHourlyUsageChartID:
|
|
if let provider = identity.provider {
|
|
self.appendZaiHourlyUsageChartItem(to: menu, provider: provider, width: width)
|
|
} else {
|
|
false
|
|
}
|
|
default:
|
|
false
|
|
}
|
|
self.logChartRenderDurationIfSlow("refreshHostedSubview:\(identity.chartID)", startedAt: t0)
|
|
|
|
if !didHydrate {
|
|
self.appendHostedSubviewUnavailableItem(
|
|
to: menu,
|
|
chartID: identity.chartID,
|
|
providerRawValue: identity.provider?.rawValue ?? identity.providerRawValue)
|
|
}
|
|
self.hostedSubviewRenderSignatures.setObject(
|
|
HostedSubviewRenderSignatureBox(signature),
|
|
forKey: menu)
|
|
}
|
|
|
|
private func hostedSubviewIdentity(for menu: NSMenu)
|
|
-> HostedSubviewIdentity? {
|
|
for item in menu.items {
|
|
guard let chartID = item.representedObject as? String else { continue }
|
|
let providerRawValue = self.hostedSubviewProviderRawValue(for: item)
|
|
return HostedSubviewIdentity(
|
|
chartID: chartID,
|
|
provider: providerRawValue.flatMap(UsageProvider.init(rawValue:)),
|
|
providerRawValue: providerRawValue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func hostedSubviewProviderRawValue(for item: NSMenuItem) -> String? {
|
|
if let providerRawValue = item.toolTip {
|
|
return providerRawValue
|
|
}
|
|
guard item.representedObject as? String == Self.statusComponentsID else { return nil }
|
|
return item.identifier?.rawValue
|
|
}
|
|
|
|
private func recordHostedSubviewRenderSignature(
|
|
for menu: NSMenu,
|
|
identity: HostedSubviewIdentity,
|
|
width: CGFloat)
|
|
{
|
|
let signature = self.hostedSubviewRenderSignature(identity: identity, width: width)
|
|
self.hostedSubviewRenderSignatures.setObject(
|
|
HostedSubviewRenderSignatureBox(signature),
|
|
forKey: menu)
|
|
}
|
|
|
|
private func hostedSubviewRenderSignature(
|
|
identity: HostedSubviewIdentity,
|
|
width: CGFloat) -> HostedSubviewRenderSignature
|
|
{
|
|
let contentSignature: HostedSubviewContentFingerprint = switch identity.chartID {
|
|
case Self.usageBreakdownChartID:
|
|
.text(Self.dashboardBreakdownReadinessSignature(
|
|
OpenAIDashboardDailyBreakdown.removingSkillUsageServices(
|
|
from: self.store.openAIDashboard?.usageBreakdown ?? [])))
|
|
case Self.creditsHistoryChartID:
|
|
.text(Self.dashboardBreakdownReadinessSignature(self.store.openAIDashboard?.dailyBreakdown ?? []))
|
|
case Self.costHistoryChartID:
|
|
if let provider = identity.provider {
|
|
self.costHistoryRenderFingerprint(for: provider)
|
|
} else {
|
|
.text("missing-provider")
|
|
}
|
|
case Self.usageHistoryChartID:
|
|
.text(identity.provider.map(self.usageHistoryRenderSignature(for:)) ?? "missing-provider")
|
|
case Self.storageBreakdownID:
|
|
.text(identity.provider.map(self.storageBreakdownRenderSignature(for:)) ?? "missing-provider")
|
|
case Self.statusComponentsID:
|
|
.text(identity.provider.map(self.statusComponentsRenderSignature(for:)) ?? "missing-provider")
|
|
case Self.zaiHourlyUsageChartID:
|
|
.text(identity.provider.map(self.zaiHourlyUsageRenderSignature(for:)) ?? "missing-provider")
|
|
default:
|
|
.text("unknown")
|
|
}
|
|
return HostedSubviewRenderSignature(
|
|
chartID: identity.chartID,
|
|
providerRawValue: identity.providerRawValue,
|
|
widthBitPattern: Double(width).bitPattern,
|
|
content: contentSignature)
|
|
}
|
|
|
|
private func costHistoryRenderFingerprint(for provider: UsageProvider) -> HostedSubviewContentFingerprint {
|
|
guard let snapshot = self.tokenSnapshotForCostHistorySubmenu(provider: provider) else {
|
|
return .text("none")
|
|
}
|
|
return .costHistory(CostHistoryChartMenuView.renderFingerprint(from: snapshot, provider: provider))
|
|
}
|
|
|
|
private func usageHistoryRenderSignature(for provider: UsageProvider) -> String {
|
|
let snapshot = self.store.snapshot(for: provider)
|
|
let selection = self.store.planUtilizationHistorySelection(for: provider)
|
|
return [
|
|
"\(self.store.planUtilizationHistoryRevision)",
|
|
"\(Int(Date().timeIntervalSince1970 / 60))",
|
|
selection.accountKey ?? "unscoped",
|
|
snapshot?.primary == nil ? "0" : "1",
|
|
snapshot?.secondary == nil ? "0" : "1",
|
|
snapshot?.tertiary == nil ? "0" : "1",
|
|
].joined(separator: "|")
|
|
}
|
|
|
|
func statusComponentsRenderSignature(for provider: UsageProvider) -> String {
|
|
let components = self.store.statusComponents(for: provider)
|
|
guard !components.isEmpty else { return "none" }
|
|
func signature(_ component: ProviderStatusComponent) -> String {
|
|
let childSig = component.children.map(signature).joined(separator: ",")
|
|
return "\(component.id)=\(component.indicator.rawValue)[\(childSig)]"
|
|
}
|
|
return components.map(signature).joined(separator: ";")
|
|
}
|
|
|
|
private func storageBreakdownRenderSignature(for provider: UsageProvider) -> String {
|
|
guard let footprint = self.store.storageFootprint(for: provider) else { return "none" }
|
|
let components = footprint.components
|
|
.map { "\($0.path)=\($0.totalBytes)" }
|
|
.joined(separator: ";")
|
|
return [
|
|
"\(footprint.totalBytes)",
|
|
footprint.paths.joined(separator: ";"),
|
|
footprint.missingPaths.joined(separator: ";"),
|
|
footprint.unreadablePaths.joined(separator: ";"),
|
|
components,
|
|
String(Double(self.storageBreakdownMenuMaxHeight()).bitPattern, radix: 16),
|
|
].joined(separator: "|")
|
|
}
|
|
|
|
private func zaiHourlyUsageRenderSignature(for provider: UsageProvider) -> String {
|
|
guard let modelUsage = self.store.snapshot(for: provider)?.zaiUsage?.modelUsage else { return "none" }
|
|
return Self.zaiHourlyUsageRenderSignature(modelUsage: modelUsage, now: Date())
|
|
}
|
|
|
|
static func zaiHourlyUsageRenderSignature(modelUsage: ZaiModelUsageData, now: Date) -> String {
|
|
let models = modelUsage.modelDataList
|
|
.map { model in
|
|
let usage = model.tokensUsage
|
|
.map { $0.map(String.init) ?? "nil" }
|
|
.joined(separator: ",")
|
|
return "\(model.modelName ?? "")=\(usage)"
|
|
}
|
|
.joined(separator: ";")
|
|
let ranges: [ZaiHourlyRange] = [.today(referenceDate: now), .last24h]
|
|
let visibleBars = ranges
|
|
.map { range in
|
|
ZaiHourlyBars.from(modelData: modelUsage, range: range, now: now)
|
|
.map { bar in
|
|
let segments = bar.segments
|
|
.map { "\($0.model)=\($0.tokens)" }
|
|
.joined(separator: ",")
|
|
return "\(bar.label):\(segments)"
|
|
}
|
|
.joined(separator: ";")
|
|
}
|
|
return [
|
|
modelUsage.xTime.joined(separator: ","),
|
|
models,
|
|
visibleBars.joined(separator: "|"),
|
|
].joined(separator: "|")
|
|
}
|
|
|
|
private func appendHostedSubviewUnavailableItem(
|
|
to menu: NSMenu,
|
|
chartID: String,
|
|
providerRawValue: String?)
|
|
{
|
|
let unavailableItem = NSMenuItem(title: L("No data available"), action: nil, keyEquivalent: "")
|
|
unavailableItem.isEnabled = false
|
|
unavailableItem.representedObject = chartID
|
|
unavailableItem.toolTip = providerRawValue
|
|
menu.addItem(unavailableItem)
|
|
}
|
|
|
|
@discardableResult
|
|
func appendUsageBreakdownChartItem(to submenu: NSMenu, width: CGFloat) -> Bool {
|
|
let breakdown = OpenAIDashboardDailyBreakdown.removingSkillUsageServices(
|
|
from: self.store.openAIDashboard?.usageBreakdown ?? [])
|
|
guard !breakdown.isEmpty else { return false }
|
|
|
|
if !self.menuCardRenderingEnabledForController {
|
|
let chartItem = NSMenuItem()
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = Self.usageBreakdownChartID
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
let chartView = UsageBreakdownChartMenuView(breakdown: breakdown, width: width)
|
|
let hosting = MenuHostingView(rootView: chartView)
|
|
hosting.frame = NSRect(
|
|
origin: .zero,
|
|
size: NSSize(width: width, height: self.hostedSubviewFittingHeight(for: hosting, width: width)))
|
|
|
|
let chartItem = NSMenuItem()
|
|
chartItem.view = hosting
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = Self.usageBreakdownChartID
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
@discardableResult
|
|
func appendCreditsHistoryChartItem(to submenu: NSMenu, width: CGFloat) -> Bool {
|
|
let breakdown = self.store.openAIDashboard?.dailyBreakdown ?? []
|
|
guard !breakdown.isEmpty else { return false }
|
|
|
|
if !self.menuCardRenderingEnabledForController {
|
|
let chartItem = NSMenuItem()
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = Self.creditsHistoryChartID
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
let chartView = CreditsHistoryChartMenuView(breakdown: breakdown, width: width)
|
|
let hosting = MenuHostingView(rootView: chartView)
|
|
hosting.frame = NSRect(
|
|
origin: .zero,
|
|
size: NSSize(width: width, height: self.hostedSubviewFittingHeight(for: hosting, width: width)))
|
|
|
|
let chartItem = NSMenuItem()
|
|
chartItem.view = hosting
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = Self.creditsHistoryChartID
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
@discardableResult
|
|
func appendCostHistoryChartItem(
|
|
to submenu: NSMenu,
|
|
provider: UsageProvider,
|
|
width: CGFloat) -> Bool
|
|
{
|
|
guard let tokenSnapshot = self.tokenSnapshotForCostHistorySubmenu(provider: provider) else { return false }
|
|
guard !tokenSnapshot.daily.isEmpty else { return false }
|
|
|
|
if !self.menuCardRenderingEnabledForController {
|
|
let chartItem = NSMenuItem()
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = Self.costHistoryChartID
|
|
chartItem.toolTip = provider.rawValue
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
let chartView = CostHistoryChartMenuView(
|
|
provider: provider,
|
|
daily: tokenSnapshot.daily,
|
|
totalCostUSD: tokenSnapshot.last30DaysCostUSD,
|
|
currencyCode: tokenSnapshot.currencyCode,
|
|
historyDays: tokenSnapshot.historyDays,
|
|
windowLabel: tokenSnapshot.historyLabel,
|
|
projects: provider == .codex ? tokenSnapshot.projects : [],
|
|
width: width)
|
|
let hosting = MenuHostingView(rootView: chartView)
|
|
hosting.applyMeasuredHeight(
|
|
width: width,
|
|
height: self.hostedSubviewFittingHeight(for: hosting, width: width))
|
|
|
|
let chartItem = NSMenuItem()
|
|
chartItem.view = hosting
|
|
chartItem.isEnabled = true
|
|
chartItem.representedObject = Self.costHistoryChartID
|
|
chartItem.toolTip = provider.rawValue
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
@discardableResult
|
|
func appendStorageBreakdownItem(
|
|
to submenu: NSMenu,
|
|
provider: UsageProvider,
|
|
width: CGFloat)
|
|
-> Bool
|
|
{
|
|
guard let footprint = self.store.storageFootprint(for: provider),
|
|
!footprint.components.isEmpty
|
|
else { return false }
|
|
|
|
if !self.menuCardRenderingEnabledForController {
|
|
let item = NSMenuItem()
|
|
item.isEnabled = true
|
|
item.representedObject = Self.storageBreakdownID
|
|
item.toolTip = provider.rawValue
|
|
submenu.addItem(item)
|
|
return true
|
|
}
|
|
|
|
let maxHeight = self.storageBreakdownMenuMaxHeight()
|
|
final class HostingRelay {
|
|
weak var hosting: MenuHostingView<StorageBreakdownMenuView>?
|
|
var collapsedHeight: CGFloat = 1
|
|
}
|
|
let relay = HostingRelay()
|
|
let view = StorageBreakdownMenuView(
|
|
footprint: footprint,
|
|
width: width,
|
|
maxHeight: maxHeight,
|
|
onExpansionHeightChange: { additionalHeight in
|
|
relay.hosting?.applyMeasuredHeight(
|
|
width: width,
|
|
height: min(maxHeight, relay.collapsedHeight + additionalHeight))
|
|
})
|
|
let hosting = MenuHostingView(rootView: view)
|
|
relay.hosting = hosting
|
|
relay.collapsedHeight = self.hostedSubviewFittingHeight(for: hosting, width: width)
|
|
hosting.applyMeasuredHeight(width: width, height: relay.collapsedHeight)
|
|
|
|
let item = NSMenuItem()
|
|
item.view = hosting
|
|
item.isEnabled = true
|
|
item.representedObject = Self.storageBreakdownID
|
|
item.toolTip = provider.rawValue
|
|
submenu.addItem(item)
|
|
return true
|
|
}
|
|
|
|
@discardableResult
|
|
func appendStatusComponentsItem(
|
|
to submenu: NSMenu,
|
|
provider: UsageProvider,
|
|
width: CGFloat) -> Bool
|
|
{
|
|
// The list of component rows is shown only once the provider's status has been fetched.
|
|
// Before the first fetch lands the submenu still renders (just the website link below), so
|
|
// every provider with a status feed gets the native submenu rather than a bare link; it
|
|
// re-hydrates with the live component list once data arrives (see makeStatusComponentsSubmenu).
|
|
let components = self.store.statusComponents(for: provider)
|
|
if !components.isEmpty {
|
|
if self.menuCardRenderingEnabledForController {
|
|
final class HostingRelay {
|
|
weak var hosting: MenuHostingView<StatusComponentsMenuView>?
|
|
}
|
|
let relay = HostingRelay()
|
|
let listView = StatusComponentsMenuView(
|
|
components: components,
|
|
width: width,
|
|
onToggle: {
|
|
// Re-measure the live content after SwiftUI applies the expand/collapse so the
|
|
// row grows/shrinks to fit exactly (no leftover blank space).
|
|
DispatchQueue.main.async {
|
|
guard let hosting = relay.hosting else { return }
|
|
hosting.applyMeasuredHeight(
|
|
width: width,
|
|
height: hosting.measuredFittingHeight(width: width))
|
|
}
|
|
})
|
|
let hosting = MenuHostingView(rootView: listView)
|
|
relay.hosting = hosting
|
|
hosting.applyMeasuredHeight(width: width, height: hosting.measuredFittingHeight(width: width))
|
|
|
|
let listItem = NSMenuItem()
|
|
listItem.view = hosting
|
|
listItem.isEnabled = false
|
|
listItem.representedObject = Self.statusComponentsID
|
|
listItem.toolTip = provider.rawValue
|
|
submenu.addItem(listItem)
|
|
} else {
|
|
let placeholder = NSMenuItem()
|
|
placeholder.isEnabled = false
|
|
placeholder.representedObject = Self.statusComponentsID
|
|
placeholder.toolTip = provider.rawValue
|
|
submenu.addItem(placeholder)
|
|
}
|
|
|
|
submenu.addItem(.separator())
|
|
}
|
|
|
|
let linkItem = NSMenuItem(
|
|
title: L("Open Status Page"),
|
|
action: #selector(self.openStatusPageFromMenuItem(_:)),
|
|
keyEquivalent: "")
|
|
linkItem.target = self
|
|
// Tag the link with the chart identity so the menu is still recognized as a status
|
|
// submenu (and re-hydrates) when the component list hasn't loaded yet and the link is the
|
|
// only row. The identifier also scopes the action to this submenu's provider so a later
|
|
// menu selection change cannot open another provider's status page.
|
|
linkItem.representedObject = Self.statusComponentsID
|
|
linkItem.identifier = NSUserInterfaceItemIdentifier(provider.rawValue)
|
|
if let image = NSImage(systemSymbolName: "arrow.up.right.square", accessibilityDescription: nil) {
|
|
image.isTemplate = true
|
|
image.size = NSSize(width: 16, height: 16)
|
|
linkItem.image = image
|
|
}
|
|
submenu.addItem(linkItem)
|
|
return true
|
|
}
|
|
|
|
private func storageBreakdownMenuMaxHeight() -> CGFloat {
|
|
let visibleHeight = NSScreen.main?.visibleFrame.height ?? 900
|
|
return min(620, max(360, floor(visibleHeight * 0.72)))
|
|
}
|
|
|
|
@discardableResult
|
|
func appendZaiHourlyUsageChartItem(
|
|
to submenu: NSMenu,
|
|
provider: UsageProvider,
|
|
width: CGFloat) -> Bool
|
|
{
|
|
guard provider == .zai,
|
|
let snapshot = self.store.snapshot(for: provider),
|
|
let modelUsage = snapshot.zaiUsage?.modelUsage
|
|
else { return false }
|
|
|
|
if !self.menuCardRenderingEnabledForController {
|
|
let chartItem = NSMenuItem()
|
|
chartItem.isEnabled = false
|
|
chartItem.representedObject = Self.zaiHourlyUsageChartID
|
|
chartItem.toolTip = provider.rawValue
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
|
|
let chartView = ZaiHourlyUsageChartMenuView(modelUsage: modelUsage, width: width)
|
|
let hosting = MenuHostingView(rootView: chartView)
|
|
hosting.frame = NSRect(
|
|
origin: .zero,
|
|
size: NSSize(width: width, height: self.hostedSubviewFittingHeight(for: hosting, width: width)))
|
|
|
|
let chartItem = NSMenuItem()
|
|
chartItem.view = hosting
|
|
chartItem.isEnabled = false
|
|
chartItem.representedObject = Self.zaiHourlyUsageChartID
|
|
chartItem.toolTip = provider.rawValue
|
|
submenu.addItem(chartItem)
|
|
return true
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
extension StatusItemController {
|
|
func _hostedSubviewRenderSignatureForTesting(menu: NSMenu, width: CGFloat) -> HostedSubviewRenderSignature? {
|
|
guard let identity = self.hostedSubviewIdentity(for: menu) else { return nil }
|
|
return self.hostedSubviewRenderSignature(identity: identity, width: width)
|
|
}
|
|
|
|
func _storedHostedSubviewRenderSignatureForTesting(menu: NSMenu) -> HostedSubviewRenderSignature? {
|
|
self.hostedSubviewRenderSignatures.object(forKey: menu)?.signature
|
|
}
|
|
}
|
|
#endif
|