fix(mac): render context bar reliably
This commit is contained in:
@@ -24,21 +24,19 @@ struct ContextUsageBar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
// Prefer the native progress indicator in menus; `GeometryReader` can get wonky
|
||||||
let fillWidth = proxy.size.width * self.clampedFractionUsed
|
// inside `MenuBarExtra`-backed menus (often receiving zero width).
|
||||||
ZStack(alignment: .leading) {
|
ZStack {
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(Color.secondary.opacity(0.25))
|
.fill(Color.secondary.opacity(0.25))
|
||||||
Capsule()
|
ProgressView(value: self.clampedFractionUsed, total: 1)
|
||||||
.fill(self.tint)
|
.progressViewStyle(.linear)
|
||||||
.frame(width: max(1, fillWidth))
|
.tint(self.tint)
|
||||||
}
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.frame(height: self.height)
|
.frame(height: self.height)
|
||||||
.accessibilityLabel("Context usage")
|
.accessibilityLabel("Context usage")
|
||||||
.accessibilityValue(self.accessibilityValue)
|
.accessibilityValue(self.accessibilityValue)
|
||||||
.drawingGroup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var accessibilityValue: String {
|
private var accessibilityValue: String {
|
||||||
|
|||||||
@@ -321,26 +321,7 @@ enum SessionLoader {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let text = try? String(contentsOf: logURL, encoding: .utf8) else { return nil }
|
guard let lastUsage = self.readLastUsageFromJsonl(logURL) else { return nil }
|
||||||
var lastUsage: [String: Any]?
|
|
||||||
|
|
||||||
for line in text.split(whereSeparator: \.isNewline) {
|
|
||||||
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if trimmedLine.isEmpty { continue }
|
|
||||||
guard let data = trimmedLine.data(using: .utf8) else { continue }
|
|
||||||
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue }
|
|
||||||
|
|
||||||
if let message = obj["message"] as? [String: Any], let usage = message["usage"] as? [String: Any] {
|
|
||||||
lastUsage = usage
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if let usage = obj["usage"] as? [String: Any] {
|
|
||||||
lastUsage = usage
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let lastUsage else { return nil }
|
|
||||||
|
|
||||||
let input = self.number(from: lastUsage["input"]) ?? 0
|
let input = self.number(from: lastUsage["input"]) ?? 0
|
||||||
let output = self.number(from: lastUsage["output"]) ?? 0
|
let output = self.number(from: lastUsage["output"]) ?? 0
|
||||||
@@ -354,6 +335,55 @@ enum SessionLoader {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func readLastUsageFromJsonl(_ url: URL) -> [String: Any]? {
|
||||||
|
// Logs can contain huge toolResult payloads (base64 images). Avoid parsing the whole file:
|
||||||
|
// read a tail window and scan backwards for the last JSON line that contains a usage blob.
|
||||||
|
let handle: FileHandle
|
||||||
|
do {
|
||||||
|
handle = try FileHandle(forReadingFrom: url)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer { try? handle.close() }
|
||||||
|
|
||||||
|
let fileSize: UInt64
|
||||||
|
do {
|
||||||
|
fileSize = try handle.seekToEnd()
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let window: UInt64 = 512 * 1024
|
||||||
|
let start = fileSize > window ? fileSize - window : 0
|
||||||
|
do {
|
||||||
|
try handle.seek(toOffset: start)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = (try? handle.readToEnd()) ?? Data()
|
||||||
|
guard let text = String(data: data, encoding: .utf8) else { return nil }
|
||||||
|
|
||||||
|
let lines = text.split(whereSeparator: \.isNewline)
|
||||||
|
for line in lines.reversed() {
|
||||||
|
let trimmedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmedLine.isEmpty { continue }
|
||||||
|
// Cheap prefilter before JSON parsing.
|
||||||
|
if !trimmedLine.contains("\"usage\"") { continue }
|
||||||
|
guard let lineData = trimmedLine.data(using: .utf8) else { continue }
|
||||||
|
guard let obj = try? JSONSerialization.jsonObject(with: lineData) as? [String: Any] else { continue }
|
||||||
|
|
||||||
|
if let message = obj["message"] as? [String: Any], let usage = message["usage"] as? [String: Any] {
|
||||||
|
return usage
|
||||||
|
}
|
||||||
|
if let usage = obj["usage"] as? [String: Any] {
|
||||||
|
return usage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private static func number(from raw: Any?) -> Int? {
|
private static func number(from raw: Any?) -> Int? {
|
||||||
switch raw {
|
switch raw {
|
||||||
case let v as Int: v
|
case let v as Int: v
|
||||||
|
|||||||
Reference in New Issue
Block a user