mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 12:03:31 +08:00
feat: add ecc2 delegated team board
This commit is contained in:
@@ -57,6 +57,14 @@ enum Commands {
|
|||||||
/// Session ID or alias
|
/// Session ID or alias
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// Show delegated team board for a session
|
||||||
|
Team {
|
||||||
|
/// Lead session ID or alias
|
||||||
|
session_id: Option<String>,
|
||||||
|
/// Delegation depth to traverse
|
||||||
|
#[arg(long, default_value_t = 2)]
|
||||||
|
depth: usize,
|
||||||
|
},
|
||||||
/// Stop a running session
|
/// Stop a running session
|
||||||
Stop {
|
Stop {
|
||||||
/// Session ID or alias
|
/// Session ID or alias
|
||||||
@@ -188,6 +196,11 @@ async fn main() -> Result<()> {
|
|||||||
let status = session::manager::get_status(&db, &id)?;
|
let status = session::manager::get_status(&db, &id)?;
|
||||||
println!("{status}");
|
println!("{status}");
|
||||||
}
|
}
|
||||||
|
Some(Commands::Team { session_id, depth }) => {
|
||||||
|
let id = session_id.unwrap_or_else(|| "latest".to_string());
|
||||||
|
let team = session::manager::get_team_status(&db, &id, depth)?;
|
||||||
|
println!("{team}");
|
||||||
|
}
|
||||||
Some(Commands::Stop { session_id }) => {
|
Some(Commands::Stop { session_id }) => {
|
||||||
session::manager::stop_session(&db, &session_id).await?;
|
session::manager::stop_session(&db, &session_id).await?;
|
||||||
println!("Session stopped: {session_id}");
|
println!("Session stopped: {session_id}");
|
||||||
@@ -446,4 +459,18 @@ mod tests {
|
|||||||
_ => panic!("expected delegate subcommand"),
|
_ => panic!("expected delegate subcommand"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_parses_team_command() {
|
||||||
|
let cli = Cli::try_parse_from(["ecc", "team", "planner", "--depth", "3"])
|
||||||
|
.expect("team should parse");
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Some(Commands::Team { session_id, depth }) => {
|
||||||
|
assert_eq!(session_id.as_deref(), Some("planner"));
|
||||||
|
assert_eq!(depth, 3);
|
||||||
|
}
|
||||||
|
_ => panic!("expected team subcommand"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
@@ -38,6 +39,30 @@ pub fn get_status(db: &StateStore, id: &str) -> Result<SessionStatus> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_team_status(db: &StateStore, id: &str, depth: usize) -> Result<TeamStatus> {
|
||||||
|
let root = resolve_session(db, id)?;
|
||||||
|
let unread_counts = db.unread_message_counts()?;
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
visited.insert(root.id.clone());
|
||||||
|
|
||||||
|
let mut descendants = Vec::new();
|
||||||
|
collect_delegation_descendants(
|
||||||
|
db,
|
||||||
|
&root.id,
|
||||||
|
depth,
|
||||||
|
1,
|
||||||
|
&unread_counts,
|
||||||
|
&mut visited,
|
||||||
|
&mut descendants,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(TeamStatus {
|
||||||
|
root,
|
||||||
|
unread_messages: unread_counts,
|
||||||
|
descendants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
|
pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
|
||||||
stop_session_with_options(db, id, true).await
|
stop_session_with_options(db, id, true).await
|
||||||
}
|
}
|
||||||
@@ -116,6 +141,48 @@ async fn resume_session_with_program(
|
|||||||
Ok(session.id)
|
Ok(session.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_delegation_descendants(
|
||||||
|
db: &StateStore,
|
||||||
|
session_id: &str,
|
||||||
|
remaining_depth: usize,
|
||||||
|
current_depth: usize,
|
||||||
|
unread_counts: &std::collections::HashMap<String, usize>,
|
||||||
|
visited: &mut HashSet<String>,
|
||||||
|
descendants: &mut Vec<DelegatedSessionSummary>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if remaining_depth == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for child_id in db.delegated_children(session_id, 50)? {
|
||||||
|
if !visited.insert(child_id.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(session) = db.get_session(&child_id)? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
descendants.push(DelegatedSessionSummary {
|
||||||
|
depth: current_depth,
|
||||||
|
unread_messages: unread_counts.get(&child_id).copied().unwrap_or(0),
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
|
||||||
|
collect_delegation_descendants(
|
||||||
|
db,
|
||||||
|
&child_id,
|
||||||
|
remaining_depth.saturating_sub(1),
|
||||||
|
current_depth + 1,
|
||||||
|
unread_counts,
|
||||||
|
visited,
|
||||||
|
descendants,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn cleanup_session_worktree(db: &StateStore, id: &str) -> Result<()> {
|
pub async fn cleanup_session_worktree(db: &StateStore, id: &str) -> Result<()> {
|
||||||
let session = resolve_session(db, id)?;
|
let session = resolve_session(db, id)?;
|
||||||
|
|
||||||
@@ -460,6 +527,18 @@ pub struct SessionStatus {
|
|||||||
delegated_children: Vec<String>,
|
delegated_children: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TeamStatus {
|
||||||
|
root: Session,
|
||||||
|
unread_messages: std::collections::HashMap<String, usize>,
|
||||||
|
descendants: Vec<DelegatedSessionSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DelegatedSessionSummary {
|
||||||
|
depth: usize,
|
||||||
|
unread_messages: usize,
|
||||||
|
session: Session,
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for SessionStatus {
|
impl fmt::Display for SessionStatus {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let s = &self.session;
|
let s = &self.session;
|
||||||
@@ -489,6 +568,71 @@ impl fmt::Display for SessionStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TeamStatus {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
writeln!(f, "Lead: {} [{}]", self.root.id, self.root.state)?;
|
||||||
|
writeln!(f, "Task: {}", self.root.task)?;
|
||||||
|
writeln!(f, "Agent: {}", self.root.agent_type)?;
|
||||||
|
if let Some(worktree) = self.root.worktree.as_ref() {
|
||||||
|
writeln!(f, "Branch: {}", worktree.branch)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lead_unread = self.unread_messages.get(&self.root.id).copied().unwrap_or(0);
|
||||||
|
writeln!(f, "Inbox: {}", lead_unread)?;
|
||||||
|
|
||||||
|
if self.descendants.is_empty() {
|
||||||
|
return write!(f, "Board: no delegated sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "Board:")?;
|
||||||
|
let mut lanes: BTreeMap<&'static str, Vec<&DelegatedSessionSummary>> = BTreeMap::new();
|
||||||
|
for summary in &self.descendants {
|
||||||
|
lanes.entry(session_state_label(&summary.session.state))
|
||||||
|
.or_default()
|
||||||
|
.push(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
for lane in [
|
||||||
|
"Running",
|
||||||
|
"Idle",
|
||||||
|
"Pending",
|
||||||
|
"Failed",
|
||||||
|
"Stopped",
|
||||||
|
"Completed",
|
||||||
|
] {
|
||||||
|
let Some(items) = lanes.get(lane) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(f, " {lane}:")?;
|
||||||
|
for item in items {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" - {}{} [{}] | inbox {} | {}",
|
||||||
|
" ".repeat(item.depth.saturating_sub(1)),
|
||||||
|
item.session.id,
|
||||||
|
item.session.agent_type,
|
||||||
|
item.unread_messages,
|
||||||
|
item.session.task
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_state_label(state: &SessionState) -> &'static str {
|
||||||
|
match state {
|
||||||
|
SessionState::Pending => "Pending",
|
||||||
|
SessionState::Running => "Running",
|
||||||
|
SessionState::Idle => "Idle",
|
||||||
|
SessionState::Completed => "Completed",
|
||||||
|
SessionState::Failed => "Failed",
|
||||||
|
SessionState::Stopped => "Stopped",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -904,4 +1048,49 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_team_status_groups_delegated_children() -> Result<()> {
|
||||||
|
let tempdir = TestDir::new("manager-team-status")?;
|
||||||
|
let _cfg = build_config(tempdir.path());
|
||||||
|
let db = StateStore::open(&tempdir.path().join("state.db"))?;
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
db.insert_session(&build_session("lead", SessionState::Running, now - Duration::minutes(3)))?;
|
||||||
|
db.insert_session(&build_session("worker-a", SessionState::Running, now - Duration::minutes(2)))?;
|
||||||
|
db.insert_session(&build_session("worker-b", SessionState::Pending, now - Duration::minutes(1)))?;
|
||||||
|
db.insert_session(&build_session("reviewer", SessionState::Completed, now))?;
|
||||||
|
|
||||||
|
db.send_message(
|
||||||
|
"lead",
|
||||||
|
"worker-a",
|
||||||
|
"{\"task\":\"Implement auth\",\"context\":\"Delegated from lead\"}",
|
||||||
|
"task_handoff",
|
||||||
|
)?;
|
||||||
|
db.send_message(
|
||||||
|
"lead",
|
||||||
|
"worker-b",
|
||||||
|
"{\"task\":\"Check billing\",\"context\":\"Delegated from lead\"}",
|
||||||
|
"task_handoff",
|
||||||
|
)?;
|
||||||
|
db.send_message(
|
||||||
|
"worker-a",
|
||||||
|
"reviewer",
|
||||||
|
"{\"task\":\"Review auth\",\"context\":\"Delegated from worker-a\"}",
|
||||||
|
"task_handoff",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let team = get_team_status(&db, "lead", 2)?;
|
||||||
|
let rendered = team.to_string();
|
||||||
|
|
||||||
|
assert!(rendered.contains("Lead: lead [running]"));
|
||||||
|
assert!(rendered.contains("Running:"));
|
||||||
|
assert!(rendered.contains("Pending:"));
|
||||||
|
assert!(rendered.contains("Completed:"));
|
||||||
|
assert!(rendered.contains("worker-a"));
|
||||||
|
assert!(rendered.contains("worker-b"));
|
||||||
|
assert!(rendered.contains("reviewer"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user