use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;
use crate::id::AgentId;
use crate::source::Activity;
pub mod reducer;
pub const MAX_FLOORS: usize = 5;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActivityState {
Idle,
Active {
activity: Activity,
tool_use_id: Option<Arc<str>>,
detail: Option<Arc<str>>,
},
Waiting {
reason: Arc<str>,
},
}
#[derive(Debug, Clone)]
pub struct AgentSlot {
pub agent_id: AgentId,
pub source: Arc<str>,
pub session_id: Arc<str>,
pub cwd: Arc<Path>,
pub label: Arc<str>,
pub state: ActivityState,
pub state_started_at: SystemTime,
pub last_event_at: SystemTime,
pub created_at: SystemTime,
pub exiting_at: Option<SystemTime>,
pub pending_idle_at: Option<SystemTime>,
pub desk_index: usize,
pub tool_call_count: u32,
pub active_ms: u64,
pub unknown_cwd: bool,
pub parent_id: Option<AgentId>,
}
#[derive(Debug, Default, Clone)]
pub struct SceneState {
pub agents: BTreeMap<AgentId, AgentSlot>,
pub max_desks: usize,
}
impl SceneState {
pub fn new(max_desks: usize) -> Self {
Self {
agents: BTreeMap::new(),
max_desks,
}
}
pub fn next_free_desk(&self) -> Option<usize> {
let occupied: std::collections::BTreeSet<usize> =
self.agents.values().map(|a| a.desk_index).collect();
(0..self.max_desks.saturating_mul(MAX_FLOORS)).find(|i| !occupied.contains(i))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn next_free_desk_starts_at_zero() {
let s = SceneState::new(4);
assert_eq!(s.next_free_desk(), Some(0));
}
#[test]
fn next_free_desk_returns_none_when_full() {
let mut s = SceneState::new(2);
let now = SystemTime::now();
for i in 0..(2 * MAX_FLOORS) {
let id = AgentId::from_transcript_path(&format!("p{i}"));
s.agents.insert(
id,
AgentSlot {
agent_id: id,
source: Arc::from("claude-code"),
session_id: Arc::from(format!("s{i}").as_str()),
cwd: Arc::from(Path::new("/")),
label: Arc::from(format!("cc#{i}").as_str()),
state: ActivityState::Idle,
state_started_at: now,
created_at: now,
last_event_at: now,
exiting_at: None,
pending_idle_at: None,
desk_index: i,
tool_call_count: 0,
active_ms: 0,
unknown_cwd: false,
parent_id: None,
},
);
}
assert_eq!(s.next_free_desk(), None);
}
#[test]
fn next_free_desk_overflows_to_second_floor() {
let mut s = SceneState::new(4);
let now = SystemTime::now();
for i in 0..4 {
let id = AgentId::from_transcript_path(&format!("f{i}"));
s.agents.insert(
id,
AgentSlot {
agent_id: id,
source: Arc::from("cc"),
session_id: Arc::from(format!("s{i}").as_str()),
cwd: Arc::from(Path::new("/repo")),
label: Arc::from(format!("a{i}").as_str()),
state: ActivityState::Idle,
state_started_at: now,
created_at: now,
last_event_at: now,
exiting_at: None,
pending_idle_at: None,
desk_index: i,
tool_call_count: 0,
active_ms: 0,
unknown_cwd: false,
parent_id: None,
},
);
}
assert_eq!(
s.next_free_desk(),
Some(4),
"should overflow to desk 4 (floor 1)"
);
}
}