diff --git a/apps/macos/Sources/Clawdis/ContextUsageBar.swift b/apps/macos/Sources/Clawdis/ContextUsageBar.swift index adb946c872..9c08dfa250 100644 --- a/apps/macos/Sources/Clawdis/ContextUsageBar.swift +++ b/apps/macos/Sources/Clawdis/ContextUsageBar.swift @@ -31,9 +31,10 @@ struct ContextUsageBar: View { .fill(Color.secondary.opacity(0.25)) Capsule() .fill(self.tint) - .frame(width: fillWidth) + .frame(width: max(1, fillWidth)) } } + .frame(maxWidth: .infinity) .frame(height: self.height) .accessibilityLabel("Context usage") .accessibilityValue(self.accessibilityValue) @@ -46,4 +47,3 @@ struct ContextUsageBar: View { return "\(pct) percent used" } } - diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index a90b37b4d2..3f4b9f4627 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -266,6 +266,7 @@ struct MenuContent: View { ContextUsageBar( usedTokens: row.tokens.total, contextTokens: row.tokens.contextTokens) + .frame(width: 220) } .padding(.vertical, 2) } else { diff --git a/apps/macos/Sources/Clawdis/SessionData.swift b/apps/macos/Sources/Clawdis/SessionData.swift index b2db8dbcd6..a58954459a 100644 --- a/apps/macos/Sources/Clawdis/SessionData.swift +++ b/apps/macos/Sources/Clawdis/SessionData.swift @@ -270,11 +270,15 @@ enum SessionLoader { throw SessionLoadError.decodeFailed(error.localizedDescription) } + let storeDir = URL(fileURLWithPath: path).deletingLastPathComponent() + return decoded.map { key, entry in let updated = entry.updatedAt.map { Date(timeIntervalSince1970: $0 / 1000) } let input = entry.inputTokens ?? 0 let output = entry.outputTokens ?? 0 - let total = entry.totalTokens ?? input + output + let fallbackTotal = entry.totalTokens ?? input + output + let promptTokens = entry.sessionId.flatMap { self.promptTokensFromSessionLog(sessionId: $0, storeDir: storeDir) } + let total = max(fallbackTotal, promptTokens ?? 0) let context = entry.contextTokens ?? defaults.contextTokens let model = entry.model ?? defaults.model @@ -299,6 +303,67 @@ enum SessionLoader { }.value } + private static func promptTokensFromSessionLog(sessionId: String, storeDir: URL) -> Int? { + let trimmed = sessionId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + + let candidates: [URL] = [ + storeDir.appendingPathComponent("\(trimmed).jsonl"), + FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".pi/agent/sessions") + .appendingPathComponent("\(trimmed).jsonl"), + FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".tau/agent/sessions/clawdis") + .appendingPathComponent("\(trimmed).jsonl"), + ] + + guard let logURL = candidates.first(where: { FileManager.default.fileExists(atPath: $0.path) }) else { + 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 } + + let input = self.number(from: lastUsage["input"]) ?? 0 + let output = self.number(from: lastUsage["output"]) ?? 0 + let cacheRead = self.number(from: lastUsage["cacheRead"] ?? lastUsage["cache_read"]) ?? 0 + let cacheWrite = self.number(from: lastUsage["cacheWrite"] ?? lastUsage["cache_write"]) ?? 0 + let totalTokens = self.number(from: lastUsage["totalTokens"] ?? lastUsage["total_tokens"] ?? lastUsage["total"]) + + let prompt = input + cacheRead + cacheWrite + if prompt > 0 { return prompt } + if let totalTokens, totalTokens > output { return totalTokens - output } + return nil + } + + private static func number(from raw: Any?) -> Int? { + switch raw { + case let v as Int: v + case let v as Double: Int(v) + case let v as NSNumber: v.intValue + case let v as String: Int(v) + default: nil + } + } + private static func standardize(_ path: String) -> String { (path as NSString).expandingTildeInPath.replacingOccurrences(of: "//", with: "/") } diff --git a/apps/macos/Sources/Clawdis/SessionsSettings.swift b/apps/macos/Sources/Clawdis/SessionsSettings.swift index 0188470313..5c44bc107f 100644 --- a/apps/macos/Sources/Clawdis/SessionsSettings.swift +++ b/apps/macos/Sources/Clawdis/SessionsSettings.swift @@ -152,6 +152,7 @@ struct SessionsSettings: View { .foregroundStyle(.secondary) } ContextUsageBar(usedTokens: row.tokens.total, contextTokens: row.tokens.contextTokens) + .frame(maxWidth: .infinity) } HStack(spacing: 10) {