1pub mod jsonl;
6pub mod pane;
7pub mod plan;
8
9use std::path::PathBuf;
10
11use serde::Serialize;
12
13use crate::backend;
14use crate::error::Result;
15use crate::session::Session;
16use crate::tmux;
17
18#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
20#[serde(tag = "status", rename_all = "snake_case")]
21pub enum State {
22 Spawning,
24 Working { tools: Vec<String> },
26 AwaitingAnswer { question: String },
28 PlanReady {
30 plan_file: Option<PathBuf>,
31 plan: Option<String>,
32 },
33 Blocked {
35 gate: String,
36 prompt: String,
37 options: Vec<String>,
38 },
39 IdleDone { text: String },
41 Dead,
43 Unknown,
45}
46
47pub fn detect(session: &Session) -> Result<State> {
49 if !tmux::has_session(&session.name)? {
50 return Ok(State::Dead);
51 }
52
53 let backend = backend::resolve(&session.backend)?;
54 let pane = tmux::capture_pane(&session.name)?;
55
56 if pane::contains_any(&pane, backend.plan_markers()) {
58 let (plan_file, plan) = match plan::latest_plan_file(session.created)? {
59 Some((path, content)) => (Some(path), content),
60 None => (None, None),
61 };
62 return Ok(State::PlanReady { plan_file, plan });
63 }
64 if pane::contains_any(&pane, backend.trust_markers()) {
66 return Ok(State::Blocked {
67 gate: "trust".to_string(),
68 prompt: first_marker(&pane, backend.trust_markers()),
69 options: pane::parse_menu_options(&pane),
70 });
71 }
72 if pane::contains_any(&pane, backend.permission_markers()) {
73 return Ok(State::Blocked {
74 gate: "permission".to_string(),
75 prompt: first_marker(&pane, backend.permission_markers()),
76 options: pane::parse_menu_options(&pane),
77 });
78 }
79
80 let events = if session.jsonl_path.exists() {
82 jsonl::load(&session.jsonl_path)?
83 } else {
84 Vec::new()
85 };
86 Ok(map_jsonl(jsonl::classify(&events), session))
87}
88
89fn map_jsonl(state: jsonl::JsonlState, session: &Session) -> State {
90 use jsonl::JsonlState;
91 match state {
92 JsonlState::AwaitingAnswer { question } => State::AwaitingAnswer { question },
93 JsonlState::PlanReady { plan } => {
94 let plan_file = plan::latest_plan_file(session.created)
95 .ok()
96 .flatten()
97 .map(|(path, _)| path);
98 State::PlanReady { plan_file, plan }
99 }
100 JsonlState::ToolPending { tools } => State::Working { tools },
101 JsonlState::IdleDone { text } => State::IdleDone { text },
102 JsonlState::Working => State::Working { tools: Vec::new() },
103 JsonlState::Unknown => State::Spawning,
105 }
106}
107
108fn first_marker(pane: &str, markers: &[&str]) -> String {
110 markers
111 .iter()
112 .find(|m| pane.contains(**m))
113 .map(|m| m.to_string())
114 .unwrap_or_default()
115}