use crate::util::{parse_harness, resolve, short_id};
use anyhow::{Context, Result};
use cv_core::ir::truncate;
pub(crate) fn cmd_events(
id: &str,
harness: Option<String>,
kind: Option<String>,
subagents: bool,
json: bool,
) -> Result<()> {
use cv_core::events;
let want = parse_harness(&harness)?;
let (r, _adapter) = resolve(id, want)?;
if events::needs_ingest(&r) {
events::ingest_ref(&r)?;
}
let rows = events::events_for(r.harness.as_str(), &r.id, kind.as_deref());
if json {
let mut out: Vec<serde_json::Value> = rows.iter().map(cv_core::rows::event_row).collect();
if subagents {
out.extend(subagent_events(&r, kind.as_deref())?.into_iter().flat_map(|(s, evs)| {
let agent_id = s.agent_id().to_string();
let parent = r.id.clone();
let harness = r.harness.as_str().to_string();
evs.into_iter().map(move |e| {
cv_core::rows::event_row(&events::EventRow {
harness: harness.clone(),
session_id: parent.clone(),
msg_idx: e.msg_idx as i64,
ts: e.ts,
kind: e.kind.to_string(),
tool: e.tool,
target: e.target,
detail: e.detail,
parent_id: Some(parent.clone()),
agent_id: Some(agent_id.clone()),
workflow: s.workflow.clone(),
})
})
}));
}
println!("{}", serde_json::to_string_pretty(&out)?);
return Ok(());
}
if rows.is_empty() && !subagents {
match &kind {
Some(k) => println!("no {k:?} events in {} (try without --kind)", short_id(&r.id)),
None => println!("no tool events in {} (a chat-only session?)", short_id(&r.id)),
}
return Ok(());
}
println!(
"{} event(s) in {}:{}\n",
rows.len(),
r.harness.as_str(),
short_id(&r.id)
);
for e in &rows {
print_event_row(
e.msg_idx,
e.ts,
&e.kind,
e.tool.as_deref(),
e.target.as_deref(),
e.detail.as_deref(),
);
}
if subagents {
print_subagent_events(&r, kind.as_deref())?;
}
Ok(())
}
fn subagent_events(
r: &cv_core::SessionRef,
kind: Option<&str>,
) -> Result<Vec<(cv_core::SubagentInfo, Vec<cv_core::events::Event>)>> {
use cv_core::events::EventSink;
use cv_core::ParseOptions;
let subs = cv_core::subagent_tree_of(r);
if subs.is_empty() {
return Ok(Vec::new());
}
let adapter = cv_core::harness::for_harness(r.harness).with_context(|| format!("no adapter for {}", r.harness))?;
let mut out = Vec::new();
for s in subs {
let mut sink = EventSink::new(s.session.cwd.clone());
if adapter.stream(&s.session, &ParseOptions::lazy(), &mut sink).is_err() {
continue;
}
let evs: Vec<_> = sink
.into_events()?
.into_iter()
.filter(|e| kind.is_none_or(|k| e.kind == k))
.collect();
if !evs.is_empty() {
out.push((s, evs));
}
}
Ok(out)
}
fn print_subagent_events(r: &cv_core::SessionRef, kind: Option<&str>) -> Result<()> {
let per_agent = subagent_events(r, kind)?;
let n_agents = cv_core::subagent_tree_of(r).len();
let mut total = 0usize;
for (s, evs) in &per_agent {
total += evs.len();
let wf = s.workflow.as_deref().map(|w| format!(" ⟐{w}")).unwrap_or_default();
println!(
"\n┌─ sub-agent {} {}{} ({} event(s)){}",
short_id(s.agent_id()),
s.agent_type.as_deref().unwrap_or("agent"),
wf,
evs.len(),
s.description
.as_deref()
.map(|d| format!(" — {}", truncate(d, 60)))
.unwrap_or_default(),
);
for e in evs {
print!("│ ");
print_event_row(
e.msg_idx as i64,
e.ts,
e.kind,
e.tool.as_deref(),
e.target.as_deref(),
e.detail.as_deref(),
);
}
}
if total > 0 {
println!("\n{total} event(s) across {n_agents} sub-agent(s)");
}
Ok(())
}
fn print_event_row(
msg_idx: i64,
ts: Option<i64>,
kind: &str,
tool: Option<&str>,
target: Option<&str>,
detail: Option<&str>,
) {
let time = ts
.and_then(|t| crate::util::fmt_local_ts(t, "%m-%d %H:%M"))
.unwrap_or_else(|| "-----------".into());
println!(
"{:>5} {:11} {:9} {:14} {}",
msg_idx,
time,
kind,
tool.unwrap_or("-"),
target.map(|t| truncate(t, 90)).unwrap_or_default(),
);
if let Some(d) = detail {
println!(" ↳ {}", truncate(d, 100));
}
}
pub(crate) fn cmd_touched(path: &str, edits_only: bool, json: bool) -> Result<()> {
let rows = cv_core::events::sessions_touching(path, edits_only);
if json {
let out: Vec<serde_json::Value> = rows.iter().map(cv_core::rows::touched_row).collect();
println!("{}", serde_json::to_string_pretty(&out)?);
return Ok(());
}
if rows.is_empty() {
println!(
"no sessions {} {path:?} — events are ingested by `cv index` (run it first?)",
if edits_only { "edited" } else { "touched" }
);
return Ok(());
}
println!("{} session(s) touched {path:?}:\n", rows.len());
for t in &rows {
let date = t
.last_ts
.and_then(|s| crate::util::fmt_local_ts(s, "%Y-%m-%d"))
.unwrap_or_else(|| "----------".into());
let counts = match (t.edits, t.reads) {
(0, n) => format!("{n} read(s)"),
(n, 0) => format!("{n} edit(s)"),
(e, r) => format!("{e} edit(s), {r} read(s)"),
};
println!(
"{:8} {:8} {:10} {:22} {}",
t.harness,
short_id(&t.session_id),
date,
counts,
t.title.as_deref().map(|s| truncate(s, 56)).unwrap_or_default(),
);
if t.parent_id.is_some() || t.agent_id.is_some() || t.workflow.is_some() {
let wf = t.workflow.as_deref().map(|w| format!(" ⟐{w}")).unwrap_or_default();
println!(
" ↳ sub-agent {} of {}{}",
t.agent_id.as_deref().map(short_id).unwrap_or_else(|| "?".into()),
t.parent_id.as_deref().map(short_id).unwrap_or_else(|| "?".into()),
wf,
);
}
}
Ok(())
}