use super::*;
pub(crate) fn render_text(
summaries: &[SessionSummary],
dropped: usize,
scope_top: usize,
scope_sub: usize,
) {
if summaries.is_empty() {
println!("no sessions found");
return;
}
crate::text::emit_scope_banner(scope_top, scope_sub);
for (i, s) in summaries.iter().enumerate() {
if i > 0 {
println!();
}
if s.is_subagent {
println!(
"SUBAGENT {} · parent SESSION {}",
s.session_id, s.parent_session_id
);
} else {
println!("SESSION {}", s.session_id);
}
let cwd = s.cwd.as_deref().unwrap_or("<unknown cwd>");
let mut meta = String::new();
if let Some(b) = &s.git_branch {
match &s.git_branch_first {
Some(f) if f != b => meta.push_str(&format!("branch {f}->{b}")),
_ => meta.push_str(&format!("branch {b}")),
}
}
if let Some(v) = &s.version {
if !meta.is_empty() {
meta.push_str(", ");
}
match &s.version_first {
Some(f) if f != v => meta.push_str(&format!("CC {f}->{v}")),
_ => meta.push_str(&format!("CC {v}")),
}
}
if meta.is_empty() {
println!(" cwd {cwd}");
} else {
println!(" cwd {cwd} ({meta})");
}
if let Some(bu) = &s.clone_boundary_uuid {
match &s.clone_of {
Some(origin) => {
println!(" clone forked from SESSION {origin} at compaction boundary {bu}")
}
None => println!(
" clone forked at compaction boundary {bu} (origin not in this \
project dir)"
),
}
}
if s.minted_by_clear {
println!(" cleared {}", cleared_line(s));
}
if let Some(child) = &s.continued_in {
println!(
" handoff continued in {} (a background handoff: Claude Code names the \
child itself, so this one is no inference)",
first8(child)
);
}
if !s.session_kind.is_empty() {
println!(" lane {}", lane_line(s));
}
print_preview("first ◂", s.first_user.as_ref());
print_preview("last ◂ ", s.last_user.as_ref());
print_preview("last ▸ ", s.last_agent.as_ref());
if !s.pending_elicitations.is_empty() {
println!(" pending with elicitation sidecar");
for p in &s.pending_elicitations {
println!(
" ⏳ {}",
crate::text::truncate_excerpt(p, EXCERPT_MAX)
);
}
}
if s.skipped_lines > 0 {
println!(
" note {} (among the head/tail lines read — full census: csift stats)",
crate::text::malformed_note(s.skipped_lines)
);
}
}
if dropped > 0 {
println!();
println!(
"… (+{dropped} more session(s) not shown — the most recently active are listed; \
narrow with a target or --since, or raise --max-count)"
);
}
}
pub(crate) fn cleared_line(s: &SessionSummary) -> String {
let when = |d: i64| {
format!(
"the checkpoint the clear wrote closes {d} ms {} this file opens",
if s.cleared_from_after {
"after"
} else {
"before"
}
)
};
match (&s.cleared_from, s.cleared_from_distance_ms) {
(Some(origin), Some(d)) => format!("from {} (an inference: {})", first8(origin), when(d)),
_ if !s.cleared_from_candidates.is_empty() => format!(
"minted by /clear; {} siblings tie at {} ms ({}) - joined to neither",
s.cleared_from_candidates.len(),
s.cleared_from_distance_ms.unwrap_or_default(),
s.cleared_from_candidates
.iter()
.map(|c| first8(c))
.collect::<Vec<_>>()
.join(", ")
),
_ => format!(
"minted by /clear; no cost-ledger checkpoint within {} ms in this project dir",
crate::session::CLEAR_JOIN_WINDOW_MS
),
}
}
pub(crate) fn lane_line(s: &SessionSummary) -> String {
let kinds = s.session_kind.join(", ");
match (s.session_kind_first_line, s.session_kind_last_line) {
(Some(a), Some(b)) => format!("{kinds} on L{a}..L{b}"),
_ => format!("{kinds} (seen in the head/tail windows; span needs --lineage)"),
}
}
fn first8(id: &str) -> &str {
id.get(..8).unwrap_or(id)
}
pub(crate) fn print_preview(label: &str, preview: Option<&MessagePreview>) {
match preview {
Some(p) => {
println!(
" {label} {}",
format_timestamp(p.timestamp_utc.as_deref())
);
println!(" {}", p.excerpt);
}
None => {
println!(" {label} —");
}
}
}
pub(crate) fn render_json(
summaries: &[SessionSummary],
dropped: usize,
scope_top: usize,
scope_sub: usize,
) -> Result<()> {
use serde_json::json;
println!(
"{}",
serde_json::to_string(&crate::text::envelope_scope_header(
"list",
scope_top,
scope_sub,
json!({})
))?
);
let mut skipped_total = 0usize;
for s in summaries {
skipped_total += s.skipped_lines;
let obj = json!({
"kind": "session",
"session_id": s.session_id,
"is_subagent": s.is_subagent,
"parent_session_id": s.parent_session_id,
"path": s.path.to_string_lossy(),
"cwd": s.cwd,
"version": s.version,
"version_first": s.version_first,
"version_last": s.version,
"git_branch": s.git_branch,
"git_branch_first": s.git_branch_first,
"git_branch_last": s.git_branch,
"first_user": preview_json(s.first_user.as_ref()),
"last_user": preview_json(s.last_user.as_ref()),
"last_agent": preview_json(s.last_agent.as_ref()),
"skipped_lines": s.skipped_lines,
"sidecar_present": s.sidecar_present,
"pending_elicitations": s.pending_elicitations,
"with_elicitation_sidecar": !s.pending_elicitations.is_empty(),
"is_clone": s.clone_boundary_uuid.is_some(),
"clone_of": s.clone_of,
"clone_boundary_uuid": s.clone_boundary_uuid,
"minted_by": if s.minted_by_clear { json!("clear") } else { serde_json::Value::Null },
"cleared_from": s.cleared_from,
"cleared_from_distance_ms": s.cleared_from_distance_ms,
"cleared_from_candidates": s.cleared_from_candidates,
"continued_in": s.continued_in,
"session_kind": s.session_kind,
"session_kind_first_line": s.session_kind_first_line,
"session_kind_last_line": s.session_kind_last_line,
"lineage_scanned": s.lineage_scanned,
});
println!("{}", serde_json::to_string(&obj)?);
}
let summary = crate::text::envelope_summary(json!({
"sessions": summaries.len(),
"skipped_lines": skipped_total,
"dropped_by_cap": dropped,
}));
println!("{}", serde_json::to_string(&summary)?);
Ok(())
}
pub(crate) fn preview_json(preview: Option<&MessagePreview>) -> serde_json::Value {
use serde_json::json;
match preview {
None => serde_json::Value::Null,
Some(p) => {
let ts_local = p.timestamp_utc.as_deref().and_then(local_iso);
json!({
"ts_utc": p.timestamp_utc,
"ts_local": ts_local,
"excerpt": p.excerpt,
})
}
}
}