use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tmai_core::agents::{AgentMode, AgentStatus, AgentType, DetectionSource, MonitoredAgent};
use tmai_core::monitor::PollMessage;
use tmai_core::state::SharedState;
use super::content;
use super::scenario::{self, DemoScenario};
#[derive(Debug)]
pub enum DemoAction {
Approve { target: String },
SelectChoice { target: String, choice_num: usize },
Reject { target: String },
}
pub struct DemoPoller {
state: SharedState,
action_rx: mpsc::Receiver<DemoAction>,
}
struct AgentRuntime {
wait_for_action: bool,
paused_at: Option<Duration>,
pause_offset: Duration,
next_event_idx: Option<usize>,
content_key: &'static str,
status: AgentStatus,
}
impl DemoPoller {
pub fn new(state: SharedState) -> (Self, mpsc::Sender<DemoAction>) {
let (action_tx, action_rx) = mpsc::channel(32);
(Self { state, action_rx }, action_tx)
}
pub fn start(self) -> mpsc::Receiver<PollMessage> {
let (tx, rx) = mpsc::channel(32);
tokio::spawn(async move {
self.run(tx).await;
});
rx
}
async fn run(mut self, tx: mpsc::Sender<PollMessage>) {
let scenario = scenario::default_scenario();
let start = Instant::now();
let mut runtimes: Vec<AgentRuntime> = scenario
.agents
.iter()
.enumerate()
.map(|(idx, _)| {
let first = scenario.timeline.iter().position(|e| e.agent_idx == idx);
AgentRuntime {
wait_for_action: false,
paused_at: None,
pause_offset: Duration::ZERO,
next_event_idx: first,
content_key: "idle",
status: AgentStatus::Idle,
}
})
.collect();
let agent_event_indices: Vec<Vec<usize>> = (0..scenario.agents.len())
.map(|agent_idx| {
scenario
.timeline
.iter()
.enumerate()
.filter(|(_, e)| e.agent_idx == agent_idx)
.map(|(i, _)| i)
.collect()
})
.collect();
let agents = self.build_agents(&scenario, &runtimes);
let _ = tx.send(PollMessage::AgentsUpdated(agents)).await;
let mut interval = tokio::time::interval(Duration::from_millis(100));
let mut quit_scheduled: Option<Instant> = None;
loop {
interval.tick().await;
while let Ok(action) = self.action_rx.try_recv() {
self.handle_action(
&action,
&scenario,
&mut runtimes,
&agent_event_indices,
start,
);
}
let elapsed = start.elapsed();
let mut changed = false;
for (agent_idx, runtime) in runtimes.iter_mut().enumerate() {
if runtime.wait_for_action {
continue; }
let effective = elapsed.saturating_sub(runtime.pause_offset);
if let Some(event_idx) = runtime.next_event_idx {
let event = &scenario.timeline[event_idx];
if effective >= event.at {
if event.content_key == "quit" {
quit_scheduled = Some(Instant::now());
runtime.next_event_idx =
Self::next_agent_event(event_idx, agent_idx, &agent_event_indices);
continue;
}
runtime.status = event.status.clone();
runtime.content_key = event.content_key;
if event.wait_for_action {
runtime.wait_for_action = true;
runtime.paused_at = Some(elapsed);
}
runtime.next_event_idx =
Self::next_agent_event(event_idx, agent_idx, &agent_event_indices);
changed = true;
}
}
}
if let Some(quit_at) = quit_scheduled {
if quit_at.elapsed() >= Duration::from_secs(2) {
let mut state = self.state.write();
state.running = false;
break;
}
}
if changed {
let agents = self.build_agents(&scenario, &runtimes);
let _ = tx.send(PollMessage::AgentsUpdated(agents)).await;
}
}
}
fn next_agent_event(
current_event_idx: usize,
agent_idx: usize,
agent_event_indices: &[Vec<usize>],
) -> Option<usize> {
let indices = &agent_event_indices[agent_idx];
let pos = indices.iter().position(|&i| i == current_event_idx);
pos.and_then(|p| indices.get(p + 1).copied())
}
fn handle_action(
&self,
action: &DemoAction,
scenario: &DemoScenario,
runtimes: &mut [AgentRuntime],
_agent_event_indices: &[Vec<usize>],
start: Instant,
) {
let target = match action {
DemoAction::Approve { target }
| DemoAction::SelectChoice { target, .. }
| DemoAction::Reject { target } => target,
};
let agent_idx = scenario.agents.iter().position(|a| a.target == *target);
let Some(agent_idx) = agent_idx else {
return;
};
let runtime = &mut runtimes[agent_idx];
if !runtime.wait_for_action {
return; }
runtime.wait_for_action = false;
if let Some(paused_at) = runtime.paused_at.take() {
let now = start.elapsed();
let pause_duration = now.saturating_sub(paused_at);
runtime.pause_offset += pause_duration;
}
runtime.status = AgentStatus::Processing {
activity: String::new(),
};
runtime.content_key = match action {
DemoAction::Approve { .. } => "processing_read",
DemoAction::SelectChoice { .. } => "processing_read",
DemoAction::Reject { .. } => "idle",
};
}
pub fn build_initial_agents() -> Vec<MonitoredAgent> {
let scenario = scenario::default_scenario();
let runtimes: Vec<AgentRuntime> = scenario
.agents
.iter()
.enumerate()
.map(|(idx, _)| {
let first = scenario.timeline.iter().position(|e| e.agent_idx == idx);
AgentRuntime {
wait_for_action: false,
paused_at: None,
pause_offset: Duration::ZERO,
next_event_idx: first,
content_key: "idle",
status: AgentStatus::Idle,
}
})
.collect();
Self::build_agents_static(&scenario, &runtimes)
}
fn build_agents(
&self,
scenario: &DemoScenario,
runtimes: &[AgentRuntime],
) -> Vec<MonitoredAgent> {
Self::build_agents_static(scenario, runtimes)
}
fn build_agents_static(
scenario: &DemoScenario,
runtimes: &[AgentRuntime],
) -> Vec<MonitoredAgent> {
scenario
.agents
.iter()
.zip(runtimes.iter())
.map(|(def, rt)| {
let content = content::get_content(rt.content_key).to_string();
let mut agent = MonitoredAgent::new(
def.target.clone(),
def.agent_type.clone(),
make_title(&def.agent_type, &rt.status),
def.cwd.clone(),
0, def.session.clone(),
String::new(),
def.window_index,
def.pane_index,
);
agent.status = rt.status.clone();
agent.last_content = strip_ansi(&content);
agent.last_content_ansi = content;
agent.detection_source = DetectionSource::IpcSocket;
agent.git_branch = def.git_branch.clone();
agent.mode = AgentMode::Default;
agent
})
.collect()
}
}
fn make_title(agent_type: &AgentType, status: &AgentStatus) -> String {
match agent_type {
AgentType::ClaudeCode => match status {
AgentStatus::Idle => "\u{2733} Claude Code".to_string(),
AgentStatus::Processing { .. } => "\u{2810} Claude Code".to_string(),
AgentStatus::AwaitingApproval { .. } => "\u{2733} Claude Code".to_string(),
_ => "Claude Code".to_string(),
},
AgentType::GeminiCli => "Gemini CLI".to_string(),
other => other.short_name().to_string(),
}
}
fn strip_ansi(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
while let Some(&next) = chars.peek() {
chars.next();
if next == 'm' {
break;
}
}
} else {
result.push(c);
}
}
result
}