121 lines
4.1 KiB
Swift
121 lines
4.1 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
/// Context usage card shown at the top of the menubar menu.
|
|
struct ContextMenuCardView: View {
|
|
private let rows: [SessionRow]
|
|
private let statusText: String?
|
|
private let isLoading: Bool
|
|
private let paddingTop: CGFloat = 8
|
|
private let paddingBottom: CGFloat = 8
|
|
private let paddingTrailing: CGFloat = 18
|
|
private let paddingLeading: CGFloat = 28
|
|
private let barHeight: CGFloat = 3
|
|
|
|
init(
|
|
rows: [SessionRow],
|
|
statusText: String? = nil,
|
|
isLoading: Bool = false)
|
|
{
|
|
self.rows = rows
|
|
self.statusText = statusText
|
|
self.isLoading = isLoading
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(alignment: .firstTextBaseline) {
|
|
Text("Context")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
Spacer(minLength: 10)
|
|
Text(self.subtitle)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
if let statusText {
|
|
Text(statusText)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
} else if self.rows.isEmpty, !self.isLoading {
|
|
Text("No active sessions")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if self.rows.isEmpty, self.isLoading {
|
|
ForEach(0..<2, id: \.self) { _ in
|
|
self.placeholderRow
|
|
}
|
|
} else {
|
|
ForEach(self.rows) { row in
|
|
self.sessionRow(row)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.top, self.paddingTop)
|
|
.padding(.bottom, self.paddingBottom)
|
|
.padding(.leading, self.paddingLeading)
|
|
.padding(.trailing, self.paddingTrailing)
|
|
.frame(minWidth: 300, maxWidth: .infinity, alignment: .leading)
|
|
.transaction { txn in txn.animation = nil }
|
|
}
|
|
|
|
private var subtitle: String {
|
|
let count = self.rows.count
|
|
if count == 1 { return "1 session · 24h" }
|
|
return "\(count) sessions · 24h"
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func sessionRow(_ row: SessionRow) -> some View {
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
ContextUsageBar(
|
|
usedTokens: row.tokens.total,
|
|
contextTokens: row.tokens.contextTokens,
|
|
height: self.barHeight)
|
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
Text(row.key)
|
|
.font(.caption.weight(row.key == "main" ? .semibold : .regular))
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.layoutPriority(1)
|
|
Spacer(minLength: 8)
|
|
Text(row.tokens.contextSummaryShort)
|
|
.font(.caption.monospacedDigit())
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
.layoutPriority(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var placeholderRow: some View {
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
ContextUsageBar(
|
|
usedTokens: 0,
|
|
contextTokens: 200_000,
|
|
height: self.barHeight)
|
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
Text("main")
|
|
.font(.caption.weight(.semibold))
|
|
.lineLimit(1)
|
|
.layoutPriority(1)
|
|
Spacer(minLength: 8)
|
|
Text("000k/000k")
|
|
.font(.caption.monospacedDigit())
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
.layoutPriority(2)
|
|
}
|
|
.redacted(reason: .placeholder)
|
|
}
|
|
}
|
|
}
|