fix(mac): render context bar as image

This commit is contained in:
Peter Steinberger
2025-12-13 00:19:29 +00:00
parent 9b9fa009d1
commit 3bb33bdeed
3 changed files with 63 additions and 15 deletions

View File

@@ -1,8 +1,10 @@
import AppKit
import SwiftUI import SwiftUI
struct ContextUsageBar: View { struct ContextUsageBar: View {
let usedTokens: Int let usedTokens: Int
let contextTokens: Int let contextTokens: Int
var width: CGFloat = 220
var height: CGFloat = 6 var height: CGFloat = 6
private var clampedFractionUsed: Double { private var clampedFractionUsed: Double {
@@ -24,17 +26,16 @@ struct ContextUsageBar: View {
} }
var body: some View { var body: some View {
// Prefer the native progress indicator in menus; `GeometryReader` can get wonky // SwiftUI menus (MenuBarExtraStyle.menu) drop certain view types (including ProgressView/Canvas).
// inside `MenuBarExtra`-backed menus (often receiving zero width). // Render the bar as an image to reliably display inside the menu.
ZStack { Image(nsImage: Self.renderBar(
Capsule() width: self.width,
.fill(Color.secondary.opacity(0.25)) height: self.height,
ProgressView(value: self.clampedFractionUsed, total: 1) fractionUsed: self.clampedFractionUsed,
.progressViewStyle(.linear) percentUsed: self.percentUsed))
.tint(self.tint) .resizable()
.clipShape(Capsule()) .interpolation(.none)
} .frame(width: self.width, height: self.height)
.frame(height: self.height)
.accessibilityLabel("Context usage") .accessibilityLabel("Context usage")
.accessibilityValue(self.accessibilityValue) .accessibilityValue(self.accessibilityValue)
} }
@@ -44,4 +45,49 @@ struct ContextUsageBar: View {
let pct = Int(round(self.clampedFractionUsed * 100)) let pct = Int(round(self.clampedFractionUsed * 100))
return "\(pct) percent used" return "\(pct) percent used"
} }
private static func renderBar(
width: CGFloat,
height: CGFloat,
fractionUsed: Double,
percentUsed: Int?) -> NSImage
{
let clamped = min(1, max(0, fractionUsed))
let size = NSSize(width: max(1, width), height: max(1, height))
let image = NSImage(size: size)
image.isTemplate = false
image.lockFocus()
defer { image.unlockFocus() }
let rect = NSRect(origin: .zero, size: size)
let radius = rect.height / 2
let background = NSColor.white.withAlphaComponent(0.12)
let stroke = NSColor.white.withAlphaComponent(0.18)
let fill: NSColor = {
guard let pct = percentUsed else { return NSColor.secondaryLabelColor }
if pct >= 95 { return .systemRed }
if pct >= 80 { return .systemOrange }
if pct >= 60 { return .systemYellow }
return .systemGreen
}()
let track = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
background.setFill()
track.fill()
stroke.setStroke()
track.lineWidth = 0.75
track.stroke()
let fillWidth = max(1, floor(rect.width * clamped))
let fillRect = NSRect(x: rect.minX, y: rect.minY, width: fillWidth, height: rect.height)
let clip = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
clip.addClip()
fill.setFill()
NSBezierPath(rect: fillRect).fill()
return image
}
} }

View File

@@ -265,8 +265,8 @@ struct MenuContent: View {
} }
ContextUsageBar( ContextUsageBar(
usedTokens: row.tokens.total, usedTokens: row.tokens.total,
contextTokens: row.tokens.contextTokens) contextTokens: row.tokens.contextTokens,
.frame(width: 220) width: 220)
} }
.padding(.vertical, 2) .padding(.vertical, 2)
} else { } else {

View File

@@ -151,8 +151,10 @@ struct SessionsSettings: View {
.font(.caption.monospacedDigit()) .font(.caption.monospacedDigit())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
ContextUsageBar(usedTokens: row.tokens.total, contextTokens: row.tokens.contextTokens) ContextUsageBar(
.frame(maxWidth: .infinity) usedTokens: row.tokens.total,
contextTokens: row.tokens.contextTokens,
width: 260)
} }
HStack(spacing: 10) { HStack(spacing: 10) {