use cctop_core::loader::Loader;
use cctop_core::pricing::Plan;
use cctop_core::session::Session;
use cctop_core::session::lookup::{Lookup, candidate_lines, lookup};
pub const HELP: &str = "\
cctop why — why cctop thinks a session is running, or is not
USAGE:
cctop why [SESSION_ID]
With no argument it explains every process it can see. With one, it explains
that session: whether a process was matched to it, which one, and by which rule
— and, when nothing was, what else claimed the processes nearby.
A session id may be shortened, as long as it is unambiguous.
";
fn resolve<'a>(sessions: &'a [Session], id: &str) -> Result<Vec<&'a Session>, String> {
match lookup(sessions, id) {
Lookup::None => Err(format!(
"No session here starts with {id}.\n`cctop -l` lists them; `cctop doctor` \
says where they are read from."
)),
Lookup::Found(only) => Ok(vec![only]),
Lookup::Ambiguous(many) => Err(format!(
"`{id}` matches {} sessions:\n{}",
many.len(),
candidate_lines(&many)
)),
}
}
pub fn run(argv: &[String]) -> i32 {
if argv.iter().any(|a| a == "-h" || a == "--help") {
print!("{HELP}");
return 0;
}
let wanted = argv.iter().find(|a| !a.starts_with('-'));
let mut loader = Loader::new();
let sessions = loader.load(Plan::Retail);
let attributions = loader.attributions().to_vec();
let matching: Vec<&Session> = match wanted {
None => sessions.iter().collect(),
Some(id) => match resolve(&sessions, id) {
Ok(matched) => matched,
Err(why) => {
println!("{why}");
return 1;
}
},
};
println!("{} agent process(es) attributed:", attributions.len());
if attributions.is_empty() {
println!(" (none — nothing on this machine looks like a running agent)");
}
for a in &attributions {
println!("\n pid {} → {}", a.pid, a.key);
println!(" rule {}", a.rule);
if !a.matched.is_empty() {
println!(" matched {}", a.matched);
}
if !a.argv.is_empty() {
println!(" argv {}", cctop_core::util::truncate(&a.argv, 100));
}
}
println!();
for session in matching {
let key = session.key();
let mine: Vec<_> = attributions.iter().filter(|a| a.key == key).collect();
let running = session.is_running();
println!("{key}");
println!(
" running {}",
match (running, session.process.is_some()) {
(true, true) => "yes, a process was matched to it",
(true, false) => "yes, inferred from a transcript still growing",
(false, _) => "no",
}
);
if session.launched_as() != session.session_id {
println!(
" resumed this transcript was forked from {}, which is the id \
its process names",
session.launched_as()
);
}
println!(" last {}", session.last_active);
println!(" cwd {}", session.label_source);
for a in &mine {
println!(" pid {} {}", a.pid, a.rule);
}
if mine.is_empty() && !running {
let neighbours: Vec<_> = attributions
.iter()
.filter(|a| a.matched == session.label_source || a.key != key)
.take(3)
.collect();
match neighbours.is_empty() {
true => println!(" nothing on this machine looked like its process"),
false => {
println!(" no process was matched to it. What took the others:");
for a in neighbours {
println!(" pid {} → {} ({})", a.pid, a.key, a.rule);
}
}
}
}
println!();
}
0
}
#[cfg(test)]
mod tests {
use super::*;
use cctop_core::pricing::Provider;
fn sessions() -> Vec<Session> {
[
("11111111-aaaa-4000-8000-000000000001", Provider::Claude),
("22222222-bbbb-4000-8000-000000000002", Provider::Codex),
]
.into_iter()
.map(|(id, provider)| Session::new(provider, id.to_string()))
.collect()
}
#[test]
fn a_prefix_that_names_one_session_explains_it() {
let sessions = sessions();
let matched = resolve(&sessions, "2222").expect("an unambiguous prefix");
assert_eq!(matched.len(), 1);
assert_eq!(
matched[0].session_id,
"22222222-bbbb-4000-8000-000000000002"
);
}
#[test]
fn a_prefix_that_names_several_says_which_ones() {
let sessions = [("abc-1", Provider::Claude), ("abc-2", Provider::Codex)]
.into_iter()
.map(|(id, provider)| Session::new(provider, id.to_string()))
.collect::<Vec<_>>();
let why = resolve(&sessions, "abc").expect_err("two sessions share the prefix");
assert!(why.contains("2 sessions"), "{why}");
assert!(why.contains("abc-1 (claude)"), "{why}");
assert!(why.contains("abc-2 (codex)"), "{why}");
}
#[test]
fn a_prefix_that_names_nothing_says_where_to_look() {
let why = resolve(&sessions(), "zzz").expect_err("no such session");
assert!(why.contains("No session here starts with zzz"), "{why}");
assert!(why.contains("doctor"), "{why}");
}
}