use agent_top_core::Harness;
use agent_top_core::harness::{self, SessionSummary, SpanRetention};
use agent_top_core::model::ToolSpan;
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum Format {
Chrome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Source {
pub path: PathBuf,
pub harness: Harness,
}
pub fn resolve(what: &str) -> Result<Source> {
let as_path = Path::new(what);
if as_path.is_file() {
let harness = harness::detect(as_path)
.with_context(|| format!("{what}: not a transcript agent-top knows how to read (Claude Code or Codex JSONL)"))?;
return Ok(Source { path: as_path.to_path_buf(), harness });
}
if what.is_empty() {
bail!("a session id or transcript path is required");
}
let candidates = candidates(what, &harness::claude::recent_transcripts(UNIX_EPOCH), &harness::codex::recent_rollouts(UNIX_EPOCH));
match candidates.len() {
1 => Ok(candidates.into_iter().next().unwrap()),
0 => bail!("no session id starts with {what:?}, and it is not a file"),
_ => {
let mut msg = format!("{what:?} matches {} sessions; give more of the id:", candidates.len());
for c in &candidates {
msg.push_str(&format!("\n {:<7} {}", c.harness.label(), c.path.display()));
}
bail!(msg)
}
}
}
fn candidates(prefix: &str, claude: &[PathBuf], codex: &[PathBuf]) -> Vec<Source> {
let mut out = Vec::new();
for p in claude {
if stem(p).starts_with(prefix) {
out.push(Source { path: p.clone(), harness: Harness::Claude });
}
}
for p in codex {
if codex_id(&stem(p)).starts_with(prefix) {
out.push(Source { path: p.clone(), harness: Harness::Codex });
}
}
out.sort_by(|a, b| a.path.cmp(&b.path));
out
}
fn stem(p: &Path) -> String {
p.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default()
}
fn codex_id(stem: &str) -> &str {
const TS_LEN: usize = "2026-05-14T21-37-50-".len();
stem.strip_prefix("rollout-").and_then(|s| s.get(TS_LEN..)).unwrap_or(stem)
}
pub fn read(src: &Source) -> Result<SessionSummary> {
let mut tracker = harness::open_transcript(&src.path, src.harness, SpanRetention::All);
tracker.refresh_all().with_context(|| format!("reading {}", src.path.display()))?;
Ok(tracker.summary().clone())
}
pub fn render(src: &Source, summary: &SessionSummary, format: Format) -> Value {
match format {
Format::Chrome => chrome(src, summary),
}
}
fn chrome(src: &Source, s: &SessionSummary) -> Value {
let pid = pid_for(s.session_id.as_deref().unwrap_or(&stem(&src.path)));
let label = match &s.cwd {
Some(cwd) => format!("{} {}", src.harness.label(), cwd.file_name().map(|n| n.to_string_lossy()).unwrap_or_default()),
None => src.harness.label().to_string(),
};
let mut events = vec![meta("process_name", pid, 0, &label), meta("thread_name", pid, MAIN_TID, "agent")];
if s.spans.iter().any(|sp| sp.sidechain) {
events.push(meta("thread_name", pid, SUBAGENT_TID, "subagents"));
}
events.extend(s.spans.iter().map(|sp| span_event(sp, pid)));
let open = s.spans.iter().filter(|sp| sp.is_open()).count();
json!({
"traceEvents": events,
"displayTimeUnit": "ms",
"otherData": {
"generator": "agent-top",
"harness": src.harness.label(),
"harness_version": s.harness_version,
"session_id": s.session_id,
"model": s.model,
"cwd": s.cwd.as_ref().map(|p| p.to_string_lossy().into_owned()),
"transcript": src.path.to_string_lossy(),
"tool_calls": s.tool_calls,
"spans": s.spans.len(),
"open_spans": open,
},
})
}
const MAIN_TID: u64 = 1;
const SUBAGENT_TID: u64 = 2;
fn meta(name: &str, pid: u64, tid: u64, value: &str) -> Value {
json!({"name": name, "ph": "M", "pid": pid, "tid": tid, "args": {"name": value}})
}
fn span_event(sp: &ToolSpan, pid: u64) -> Value {
let tid = if sp.sidechain { SUBAGENT_TID } else { MAIN_TID };
let args = json!({"call_id": sp.id, "error": sp.error, "sidechain": sp.sidechain});
match sp.duration_ms {
Some(ms) => json!({
"name": sp.name, "cat": "tool", "ph": "X",
"ts": micros(sp.started_at), "dur": ms * 1000,
"pid": pid, "tid": tid, "args": args,
}),
None => json!({
"name": sp.name, "cat": "tool", "ph": "B",
"ts": micros(sp.started_at),
"pid": pid, "tid": tid, "args": args,
}),
}
}
fn micros(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH).map(|d| d.as_micros() as u64).unwrap_or(0)
}
fn pid_for(session_id: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in session_id.bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0100_0000_01b3);
}
((h ^ (h >> 32)) & 0x7fff_ffff).max(1)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn pids_are_deterministic_positive_and_distinct() {
assert_eq!(pid_for("abc"), pid_for("abc"));
assert_ne!(pid_for("abc"), pid_for("abd"));
assert!(pid_for("") >= 1);
assert!(pid_for("00000000-1111-2222-3333-444444444444") <= 0x7fff_ffff);
}
#[test]
fn matches_ids_by_prefix_in_both_layouts() {
let claude = vec![PathBuf::from("/c/p/00000000-1111-2222-3333-444444444444.jsonl"), PathBuf::from("/c/p/agent-0000aaaa.jsonl")];
let codex = vec![
PathBuf::from("/x/2026/05/14/rollout-2026-05-14T21-37-50-01000000-0000-7000-0000-000000000000.jsonl"),
PathBuf::from("/x/2026/05/15/rollout-2026-05-15T09-00-00-0f000000-0000-7000-0000-000000000000.jsonl"),
];
let one = candidates("0100", &claude, &codex);
assert_eq!(one.len(), 1);
assert_eq!(one[0].harness, Harness::Codex);
let one = candidates("00000000-1111", &claude, &codex);
assert_eq!(one.len(), 1);
assert_eq!(one[0].harness, Harness::Claude);
assert_eq!(candidates("0", &claude, &codex).len(), 3);
assert!(candidates("2026-05", &claude, &codex).is_empty());
assert!(candidates("zzz", &claude, &codex).is_empty());
}
#[test]
fn open_spans_become_begin_events_on_their_own_track() {
let at = UNIX_EPOCH + Duration::from_millis(1_700_000_000_123);
let closed =
ToolSpan { id: "a".into(), name: "Bash".into(), started_at: at, duration_ms: Some(2_500), sidechain: false, error: true };
let open = ToolSpan { id: "b".into(), name: "Grep".into(), started_at: at, duration_ms: None, sidechain: true, error: false };
let x = span_event(&closed, 7);
assert_eq!(x["ph"], "X");
assert_eq!(x["ts"], 1_700_000_000_123_000u64);
assert_eq!(x["dur"], 2_500_000u64);
assert_eq!(x["tid"], MAIN_TID);
assert_eq!(x["args"]["error"], true);
let b = span_event(&open, 7);
assert_eq!(b["ph"], "B");
assert!(b.get("dur").is_none());
assert_eq!(b["tid"], SUBAGENT_TID);
}
}