Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions panel/CompactView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions panel/Panel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions panel/PanelNav.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions panel/Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions panel/TranscriptStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down