@@ -65,8 +65,7 @@ class ContextMonitor {
6565 try ? fh. close ( )
6666
6767 if let headStr = String ( data: headData, encoding: . utf8) {
68- let lines = headStr. components ( separatedBy: " \n " )
69- for line in lines where !line. isEmpty {
68+ for line in headStr. split ( separator: " \n " ) {
7069 guard let ld = line. data ( using: . utf8) ,
7170 let json = try ? JSONSerialization . jsonObject ( with: ld) as? [ String : Any ] else { continue }
7271
@@ -85,7 +84,7 @@ class ContextMonitor {
8584 }
8685 }
8786
88- firstMessage = firstMessage. components ( separatedBy : " \n " ) . first ?? " "
87+ firstMessage = firstMessage. split ( separator : " \n " ) . first. map ( String . init ) ?? " "
8988 firstMessage = firstMessage. trimmingCharacters ( in: . whitespacesAndNewlines)
9089
9190 results. append ( SessionInfo (
@@ -114,7 +113,7 @@ class ContextMonitor {
114113 let iso8601 = ISO8601DateFormatter ( )
115114 iso8601. formatOptions = [ . withInternetDateTime, . withFractionalSeconds]
116115
117- for line in content. components ( separatedBy : " \n " ) where !line . isEmpty {
116+ for line in content. split ( separator : " \n " ) {
118117 guard let lineData = line. data ( using: . utf8) ,
119118 let json = try ? JSONSerialization . jsonObject ( with: lineData) as? [ String : Any ] ,
120119 let type = json [ " type " ] as? String , type == " user " ,
@@ -169,7 +168,7 @@ class ContextMonitor {
169168 var currentTurnIndex = - 1
170169 var seenPromptIds = Set < String > ( )
171170
172- for line in content. components ( separatedBy : " \n " ) where !line . isEmpty {
171+ for line in content. split ( separator : " \n " ) {
173172 guard let lineData = line. data ( using: . utf8) ,
174173 let json = try ? JSONSerialization . jsonObject ( with: lineData) as? [ String : Any ] ,
175174 let type = json [ " type " ] as? String else { continue }
@@ -203,7 +202,7 @@ class ContextMonitor {
203202 let filename = ( fp as NSString ) . lastPathComponent
204203 desc = " \( name) \( filename) "
205204 } else if let cmd = input [ " command " ] as? String {
206- let brief = cmd. components ( separatedBy : " \n " ) . first ?? cmd
205+ let brief = cmd. split ( separator : " \n " ) . first. map ( String . init ) ?? cmd
207206 desc = " \( name) : \( String ( brief. prefix ( 50 ) ) ) "
208207 } else if let pattern = input [ " pattern " ] as? String {
209208 desc = " \( name) \( pattern) "
@@ -226,7 +225,7 @@ class ContextMonitor {
226225 guard let data = try ? Data ( contentsOf: URL ( fileURLWithPath: jsonlPath) ) ,
227226 let content = String ( data: data, encoding: . utf8) else { return nil }
228227
229- let lines = content. components ( separatedBy : " \n " )
228+ let lines = content. split ( separator : " \n " , omittingEmptySubsequences : false )
230229 var seenPromptIds = Set < String > ( )
231230 var uniqueTurnCount = - 1 // will be incremented to 0 on first user turn
232231 var cutoffLineIndex = lines. count
@@ -276,66 +275,103 @@ class ContextMonitor {
276275 }
277276 }
278277
278+ /// Per-session cache so we don't flicker the context bar to nil when a tail
279+ /// read misses the usage entry (e.g. large tool-result block at end of file).
280+ /// Access only via `cachedUsage(_:)` and `setCachedUsage(_:for:)`.
281+ private var usageCache : [ String : ContextUsage ] = [ : ]
282+ private let cacheLock = NSLock ( )
283+
284+ private func cachedUsage( _ sessionId: String ) -> ContextUsage ? {
285+ cacheLock. lock ( )
286+ defer { cacheLock. unlock ( ) }
287+ return usageCache [ sessionId]
288+ }
289+
290+ private func setCachedUsage( _ usage: ContextUsage , for sessionId: String ) {
291+ cacheLock. lock ( )
292+ defer { cacheLock. unlock ( ) }
293+ usageCache [ sessionId] = usage
294+ }
295+
279296 /// Get context usage for a session by reading its JSONL file.
280297 /// Only reads the tail of the file to find the most recent usage entry.
298+ /// Falls back to a cached value when the tail doesn't contain a usage entry.
281299 func getUsage( sessionId: String , projectPath: String ) -> ContextUsage ? {
282300 let encoded = projectPath. claudeProjectDirName
283301 let jsonlPath = NSHomeDirectory ( ) + " /.claude/projects/ \( encoded) / \( sessionId) .jsonl "
284302
303+ if let usage = getUsageFromFile ( at: jsonlPath) {
304+ setCachedUsage ( usage, for: sessionId)
305+ DiagnosticLog . shared. log ( " context " ,
306+ " getUsage: \( sessionId) \( usage. contextUsed) / \( usage. contextLimit) ( \( Int ( usage. percentage) ) %) model= \( usage. model) " )
307+ return usage
308+ }
309+
310+ // No usage found — return cached value if available
311+ let cached = cachedUsage ( sessionId)
312+ DiagnosticLog . shared. log ( " context " ,
313+ " getUsage: \( sessionId) no usage found, cached= \( cached != nil ) " )
314+ return cached
315+ }
316+
317+ /// Parse context usage from a JSONL file at the given path.
318+ /// Uses a progressive tail read (256KB then 1MB) to handle large files
319+ /// where tool results push usage entries far from the end.
320+ func getUsageFromFile( at jsonlPath: String ) -> ContextUsage ? {
285321 guard let fh = FileHandle ( forReadingAtPath: jsonlPath) else { return nil }
286322 defer { try ? fh. close ( ) }
287323
288324 let fileSize = fh. seekToEndOfFile ( )
289325 guard fileSize > 0 else { return nil }
290326
291- // --- Find last usage: read only the tail of the file ---
292- // Usage entries appear near the end. Read the last 64KB (enough for
293- // several assistant response entries).
294- let tailSize : UInt64 = 64 * 1024
295- let tailOffset = fileSize > tailSize ? fileSize - tailSize : 0
296- fh. seek ( toFileOffset: tailOffset)
297- let tailData = fh. readData ( ofLength: Int ( fileSize - tailOffset) )
298- guard let tailContent = String ( data: tailData, encoding: . utf8) else { return nil }
299-
300- var lastInput = 0
301- var lastCacheRead = 0
302- var model = " "
303-
304- // Split tail into lines and scan in reverse for the last usage entry
305- let lines = tailContent. components ( separatedBy: " \n " )
327+ // --- Progressive tail read ---
328+ // Start with 256KB; if that misses, try 1MB. Large tool results
329+ // (file reads, grep output) can easily exceed 64KB.
330+ let tailSizes : [ UInt64 ] = [ 256 * 1024 , 1024 * 1024 ]
331+
332+ for tailSize in tailSizes {
333+ let tailOffset = fileSize > tailSize ? fileSize - tailSize : 0
334+ fh. seek ( toFileOffset: tailOffset)
335+ let tailData = fh. readData ( ofLength: Int ( fileSize - tailOffset) )
336+ guard let tailContent = String ( data: tailData, encoding: . utf8) else { continue }
337+
338+ if let usage = parseUsage ( from: tailContent) {
339+ return usage
340+ }
341+ }
342+
343+ return nil
344+ }
345+
346+ /// Parse the last usage entry from JSONL content, scanning lines in reverse.
347+ func parseUsage( from content: String ) -> ContextUsage ? {
348+ let lines = content. split ( separator: " \n " )
306349 for line in lines. reversed ( ) {
307- guard !line. isEmpty else { continue }
308350 guard let lineData = line. data ( using: . utf8) ,
309351 let json = try ? JSONSerialization . jsonObject ( with: lineData) as? [ String : Any ] else { continue }
310352
311353 if let msg = json [ " message " ] as? [ String : Any ] , let usage = msg [ " usage " ] as? [ String : Any ] {
312- lastInput = usage [ " input_tokens " ] as? Int ?? 0
313- lastCacheRead = usage [ " cache_read_input_tokens " ] as? Int ?? 0
314- if lastInput + lastCacheRead == 0 { continue }
315- model = msg [ " model " ] as? String ?? model
316- break
354+ let input = usage [ " input_tokens " ] as? Int ?? 0
355+ let cacheRead = usage [ " cache_read_input_tokens " ] as? Int ?? 0
356+ if input + cacheRead == 0 { continue }
357+ let model = msg [ " model " ] as? String ?? " "
358+ let limit = contextLimits [ model] ?? defaultLimit
359+ return ContextUsage ( model: model, inputTokens: input,
360+ cacheReadTokens: cacheRead, contextLimit: limit)
317361 }
318362
319363 if let msg = json [ " message " ] as? [ String : Any ] ,
320364 let inner = msg [ " message " ] as? [ String : Any ] ,
321365 let usage = inner [ " usage " ] as? [ String : Any ] {
322- lastInput = usage [ " input_tokens " ] as? Int ?? 0
323- lastCacheRead = usage [ " cache_read_input_tokens " ] as? Int ?? 0
324- if lastInput + lastCacheRead == 0 { continue }
325- model = inner [ " model " ] as? String ?? model
326- break
366+ let input = usage [ " input_tokens " ] as? Int ?? 0
367+ let cacheRead = usage [ " cache_read_input_tokens " ] as? Int ?? 0
368+ if input + cacheRead == 0 { continue }
369+ let model = inner [ " model " ] as? String ?? " "
370+ let limit = contextLimits [ model] ?? defaultLimit
371+ return ContextUsage ( model: model, inputTokens: input,
372+ cacheReadTokens: cacheRead, contextLimit: limit)
327373 }
328374 }
329-
330- guard !model. isEmpty || lastInput > 0 || lastCacheRead > 0 else { return nil }
331-
332- let limit = contextLimits [ model] ?? defaultLimit
333-
334- return ContextUsage (
335- model: model,
336- inputTokens: lastInput,
337- cacheReadTokens: lastCacheRead,
338- contextLimit: limit,
339- )
375+ return nil
340376 }
341377}
0 commit comments