From 9b9fa009d120a2c3d1e00d5cd3baf7c1cdc9b72a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 00:13:33 +0000 Subject: [PATCH] fix(mac): render context bar reliably --- .../Sources/Clawdis/ContextUsageBar.swift | 20 +++--- apps/macos/Sources/Clawdis/SessionData.swift | 70 +++++++++++++------ 2 files changed, 59 insertions(+), 31 deletions(-) diff --git a/apps/macos/Sources/Clawdis/ContextUsageBar.swift b/apps/macos/Sources/Clawdis/ContextUsageBar.swift index 9c08dfa25..755511dce 100644 --- a/apps/macos/Sources/Clawdis/ContextUsageBar.swift +++ b/apps/macos/Sources/Clawdis/ContextUsageBar.swift @@ -24,21 +24,19 @@ struct ContextUsageBar: View { } var body: some View { - GeometryReader { proxy in - let fillWidth = proxy.size.width * self.clampedFractionUsed - ZStack(alignment: .leading) { - Capsule() - .fill(Color.secondary.opacity(0.25)) - Capsule() - .fill(self.tint) - .frame(width: max(1, fillWidth)) - } + // Prefer the native progress indicator in menus; `GeometryReader` can get wonky + // inside `MenuBarExtra`-backed menus (often receiving zero width). + ZStack { + Capsule() + .fill(Color.secondary.opacity(0.25)) + ProgressView(value: self.clampedFractionUsed, total: 1) + .progressViewStyle(.linear) + .tint(self.tint) + .clipShape(Capsule()) } - .frame(maxWidth: .infinity) .frame(height: self.height) .accessibilityLabel("Context usage") .accessibilityValue(self.accessibilityValue) - .drawingGroup() } private var accessibilityValue: String { diff --git a/apps/macos/Sources/Clawdis/SessionData.swift b/apps/macos/Sources/Clawdis/SessionData.swift index a58954459..ea23abc0e 100644 --- a/apps/macos/Sources/Clawdis/SessionData.swift +++ b/apps/macos/Sources/Clawdis/SessionData.swift @@ -321,26 +321,7 @@ enum SessionLoader { return nil } - guard let text = try? String(contentsOf: logURL, encoding: .utf8) 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 } + guard let lastUsage = self.readLastUsageFromJsonl(logURL) else { return nil } let input = self.number(from: lastUsage["input"]) ?? 0 let output = self.number(from: lastUsage["output"]) ?? 0 @@ -354,6 +335,55 @@ enum SessionLoader { 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? { switch raw { case let v as Int: v