pub mod jsonl;
pub mod pane;
pub mod plan;
use std::path::PathBuf;
use serde::Serialize;
use crate::backend;
use crate::error::Result;
use crate::session::Session;
use crate::tmux;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum State {
Spawning,
Working { tools: Vec<String> },
AwaitingAnswer { question: String },
PlanReady {
plan_file: Option<PathBuf>,
plan: Option<String>,
},
Blocked {
gate: String,
prompt: String,
options: Vec<String>,
},
IdleDone { text: String },
Dead,
Unknown,
}
pub fn detect(session: &Session) -> Result<State> {
if !tmux::has_session(&session.name)? {
return Ok(State::Dead);
}
let backend = backend::resolve(&session.backend)?;
let pane = tmux::capture_pane(&session.name)?;
if pane::contains_any(&pane, backend.plan_markers()) {
let (plan_file, plan) = match plan::latest_plan_file(session.created)? {
Some((path, content)) => (Some(path), content),
None => (None, None),
};
return Ok(State::PlanReady { plan_file, plan });
}
if pane::contains_any(&pane, backend.trust_markers()) {
return Ok(State::Blocked {
gate: "trust".to_string(),
prompt: first_marker(&pane, backend.trust_markers()),
options: pane::parse_menu_options(&pane),
});
}
if pane::contains_any(&pane, backend.permission_markers()) {
return Ok(State::Blocked {
gate: "permission".to_string(),
prompt: first_marker(&pane, backend.permission_markers()),
options: pane::parse_menu_options(&pane),
});
}
let events = if session.jsonl_path.exists() {
jsonl::load(&session.jsonl_path)?
} else {
Vec::new()
};
Ok(map_jsonl(jsonl::classify(&events), session))
}
fn map_jsonl(state: jsonl::JsonlState, session: &Session) -> State {
use jsonl::JsonlState;
match state {
JsonlState::AwaitingAnswer { question } => State::AwaitingAnswer { question },
JsonlState::PlanReady { plan } => {
let plan_file = plan::latest_plan_file(session.created)
.ok()
.flatten()
.map(|(path, _)| path);
State::PlanReady { plan_file, plan }
}
JsonlState::ToolPending { tools } => State::Working { tools },
JsonlState::IdleDone { text } => State::IdleDone { text },
JsonlState::Working => State::Working { tools: Vec::new() },
JsonlState::Unknown => State::Spawning,
}
}
fn first_marker(pane: &str, markers: &[&str]) -> String {
markers
.iter()
.find(|m| pane.contains(**m))
.map(|m| m.to_string())
.unwrap_or_default()
}