diff --git a/panel/CompactView.swift b/panel/CompactView.swift index b249db6..ab1827a 100644 --- a/panel/CompactView.swift +++ b/panel/CompactView.swift @@ -310,8 +310,16 @@ struct CompactView: View { } private func transcriptStats(for s: Session) -> TranscriptStats? { - guard let id = s.claudeSessionID else { return nil } - return nav.claudeSessionStats[id] + if let id = s.claudeSessionID, let stats = nav.claudeSessionStats[id] { + return stats + } + // Non-sidecar agents (Codex) have no claudeSessionID on the Session; + // resolve via the PID→transcript cache so the pill shows their context + // too, surviving navigation and event pruning. + if let ref = nav.transcriptRefByPID[s.pid] { + return nav.claudeSessionStats[ref.sessionID] + } + return nil } private func displayName(_ s: Session) -> String { diff --git a/panel/Panel.swift b/panel/Panel.swift index 2c4c39e..5f5f29e 100644 --- a/panel/Panel.swift +++ b/panel/Panel.swift @@ -1359,6 +1359,23 @@ final class PanelController: NSObject, NSApplicationDelegate, PanelKeyDelegate, } } } + + // Non-sidecar agents (Codex): re-read from the transcript path learned + // from their hook events, keyed by PID. Keeps stats live and repopulates + // them after EventStore pruning / panel navigation, mirroring the + // Claude path above (which is driven by the per-pid sidecar). + for session in sessions.sessions where session.status == .active { + guard session.agent != "claude", + let ref = nav.transcriptRefByPID[session.pid] + else { continue } + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let stats = TranscriptReader.read(path: ref.path) else { return } + DispatchQueue.main.async { + self?.nav.claudeSessionStats[ref.sessionID] = stats + self?.evaluateContextThreshold(sessionID: ref.sessionID, stats: stats) + } + } + } } // Claude Code stores transcripts at: @@ -1373,6 +1390,12 @@ final class PanelController: NSObject, NSApplicationDelegate, PanelKeyDelegate, guard let sessionID = event.claudeSessionID, let path = event.transcriptPath, !path.isEmpty else { return } + // Cache the (session id, path) by agent PID so non-sidecar agents + // (Codex) resolve + re-read stats without depending on this event + // still being in the event list later. + if let pid = event.agentPID, pid > 0 { + nav.transcriptRefByPID[pid] = TranscriptRef(sessionID: sessionID, path: path) + } DispatchQueue.global(qos: .utility).async { [weak self] in guard let stats = TranscriptReader.read(path: path) else { return } DispatchQueue.main.async { diff --git a/panel/PanelNav.swift b/panel/PanelNav.swift index 9cd12d4..cc857a1 100644 --- a/panel/PanelNav.swift +++ b/panel/PanelNav.swift @@ -154,6 +154,11 @@ final class PanelNav: ObservableObject { // Sessions.swift renders entries from this map alongside matched // sessions in the Sessions tab. @Published var claudeSessionStats: [String: TranscriptStats] = [:] + // Agents without a per-pid session sidecar (Codex) learn their transcript + // (session id + path) from the first hook event; we cache it by agent PID + // so the Sessions/Compact views can resolve stats by PID instead of + // scanning the prunable event list, and so the poll refresh can re-read it. + @Published var transcriptRefByPID: [Int: TranscriptRef] = [:] // Transient feedback for the "Check for updates…" action row. // Set by PanelController around UpdateChecker.check(); cleared // back to .idle a few seconds after a terminal result so the diff --git a/panel/Sessions.swift b/panel/Sessions.swift index 1473470..44272a1 100644 --- a/panel/Sessions.swift +++ b/panel/Sessions.swift @@ -115,6 +115,13 @@ struct SessionsView: View { let stats = nav.claudeSessionStats[id] { return stats } + // Non-sidecar agents (Codex): resolve via the PID→transcript cache. + // This persists across navigation and EventStore pruning, so the row + // doesn't vanish once the source event ages out of the event list. + if let ref = nav.transcriptRefByPID[session.pid], + let stats = nav.claudeSessionStats[ref.sessionID] { + return stats + } // Fallback for sessions whose sidecar isn't readable (e.g. pre-2.1 // Claude Code): infer from the most recent matching event that // carried a claudeSessionID. events array is newest-first. diff --git a/panel/TranscriptStats.swift b/panel/TranscriptStats.swift index 8a3e923..6f7ef6b 100644 --- a/panel/TranscriptStats.swift +++ b/panel/TranscriptStats.swift @@ -12,6 +12,18 @@ struct TranscriptStats: Equatable { let model: String? } +// Stable per-PID handle to an agent's transcript, learned from a hook event. +// Claude exposes a per-pid session sidecar so its sessions resolve stats +// directly; agents without one (Codex) have no such anchor, so we cache the +// (session id, transcript path) the first hook event carries, keyed by the +// agent PID. The Sessions/Compact views resolve stats through this — which +// survives EventStore pruning and panel navigation — and the poll refresh +// re-reads `path` to keep stats live. +struct TranscriptRef: Equatable { + let sessionID: String + let path: String +} + enum TranscriptReader { // Dispatch by transcript path. Codex rollout files live under