mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-09 02:43:29 +08:00
feat: surface ecc2 session handoff lineage
This commit is contained in:
@@ -30,7 +30,12 @@ pub fn list_sessions(db: &StateStore) -> Result<Vec<Session>> {
|
||||
|
||||
pub fn get_status(db: &StateStore, id: &str) -> Result<SessionStatus> {
|
||||
let session = resolve_session(db, id)?;
|
||||
Ok(SessionStatus(session))
|
||||
let session_id = session.id.clone();
|
||||
Ok(SessionStatus {
|
||||
session,
|
||||
parent_session: db.latest_task_handoff_source(&session_id)?,
|
||||
delegated_children: db.delegated_children(&session_id, 5)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
|
||||
@@ -449,15 +454,22 @@ async fn kill_process(pid: u32) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionStatus(Session);
|
||||
pub struct SessionStatus {
|
||||
session: Session,
|
||||
parent_session: Option<String>,
|
||||
delegated_children: Vec<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = &self.0;
|
||||
let s = &self.session;
|
||||
writeln!(f, "Session: {}", s.id)?;
|
||||
writeln!(f, "Task: {}", s.task)?;
|
||||
writeln!(f, "Agent: {}", s.agent_type)?;
|
||||
writeln!(f, "State: {}", s.state)?;
|
||||
if let Some(parent) = self.parent_session.as_ref() {
|
||||
writeln!(f, "Parent: {}", parent)?;
|
||||
}
|
||||
if let Some(pid) = s.pid {
|
||||
writeln!(f, "PID: {}", pid)?;
|
||||
}
|
||||
@@ -469,6 +481,9 @@ impl fmt::Display for SessionStatus {
|
||||
writeln!(f, "Tools: {}", s.metrics.tool_calls)?;
|
||||
writeln!(f, "Files: {}", s.metrics.files_changed)?;
|
||||
writeln!(f, "Cost: ${:.4}", s.metrics.cost_usd)?;
|
||||
if !self.delegated_children.is_empty() {
|
||||
writeln!(f, "Children: {}", self.delegated_children.join(", "))?;
|
||||
}
|
||||
writeln!(f, "Created: {}", s.created_at)?;
|
||||
write!(f, "Updated: {}", s.updated_at)
|
||||
}
|
||||
@@ -848,7 +863,44 @@ mod tests {
|
||||
db.insert_session(&build_session("newer", SessionState::Idle, newer))?;
|
||||
|
||||
let status = get_status(&db, "latest")?;
|
||||
assert_eq!(status.0.id, "newer");
|
||||
assert_eq!(status.session.id, "newer");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_status_surfaces_handoff_lineage() -> Result<()> {
|
||||
let tempdir = TestDir::new("manager-status-lineage")?;
|
||||
let cfg = build_config(tempdir.path());
|
||||
let db = StateStore::open(&cfg.db_path)?;
|
||||
let now = Utc::now();
|
||||
|
||||
db.insert_session(&build_session("parent", SessionState::Running, now - Duration::minutes(2)))?;
|
||||
db.insert_session(&build_session("child", SessionState::Pending, now - Duration::minutes(1)))?;
|
||||
db.insert_session(&build_session("sibling", SessionState::Idle, now))?;
|
||||
|
||||
db.send_message(
|
||||
"parent",
|
||||
"child",
|
||||
"{\"task\":\"Review auth flow\",\"context\":\"Delegated from parent\"}",
|
||||
"task_handoff",
|
||||
)?;
|
||||
db.send_message(
|
||||
"parent",
|
||||
"sibling",
|
||||
"{\"task\":\"Check billing\",\"context\":\"Delegated from parent\"}",
|
||||
"task_handoff",
|
||||
)?;
|
||||
|
||||
let status = get_status(&db, "parent")?;
|
||||
let rendered = status.to_string();
|
||||
|
||||
assert!(rendered.contains("Children:"));
|
||||
assert!(rendered.contains("child"));
|
||||
assert!(rendered.contains("sibling"));
|
||||
|
||||
let child_status = get_status(&db, "child")?;
|
||||
assert_eq!(child_status.parent_session.as_deref(), Some("parent"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -411,6 +411,40 @@ impl StateStore {
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub fn latest_task_handoff_source(&self, session_id: &str) -> Result<Option<String>> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT from_session
|
||||
FROM messages
|
||||
WHERE to_session = ?1 AND msg_type = 'task_handoff'
|
||||
ORDER BY id DESC
|
||||
LIMIT 1",
|
||||
rusqlite::params![session_id],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn delegated_children(&self, session_id: &str, limit: usize) -> Result<Vec<String>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT to_session
|
||||
FROM messages
|
||||
WHERE from_session = ?1 AND msg_type = 'task_handoff'
|
||||
GROUP BY to_session
|
||||
ORDER BY MAX(id) DESC
|
||||
LIMIT ?2",
|
||||
)?;
|
||||
|
||||
let children = stmt
|
||||
.query_map(rusqlite::params![session_id, limit as i64], |row| {
|
||||
row.get::<_, String>(0)
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
pub fn append_output_line(
|
||||
&self,
|
||||
session_id: &str,
|
||||
@@ -725,6 +759,31 @@ mod tests {
|
||||
assert_eq!(unread_after.get("worker"), None);
|
||||
assert_eq!(unread_after.get("planner"), Some(&1));
|
||||
|
||||
db.send_message(
|
||||
"planner",
|
||||
"worker-2",
|
||||
"{\"task\":\"Review auth flow\",\"context\":\"Delegated from planner\"}",
|
||||
"task_handoff",
|
||||
)?;
|
||||
db.send_message(
|
||||
"planner",
|
||||
"worker-3",
|
||||
"{\"task\":\"Check billing\",\"context\":\"Delegated from planner\"}",
|
||||
"task_handoff",
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
db.latest_task_handoff_source("worker-2")?,
|
||||
Some("planner".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
db.delegated_children("planner", 10)?,
|
||||
vec![
|
||||
"worker-3".to_string(),
|
||||
"worker-2".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct Dashboard {
|
||||
session_output_cache: HashMap<String, Vec<OutputLine>>,
|
||||
unread_message_counts: HashMap<String, usize>,
|
||||
selected_messages: Vec<SessionMessage>,
|
||||
selected_parent_session: Option<String>,
|
||||
selected_child_sessions: Vec<String>,
|
||||
logs: Vec<ToolLogEntry>,
|
||||
selected_diff_summary: Option<String>,
|
||||
selected_pane: Pane,
|
||||
@@ -112,6 +114,8 @@ impl Dashboard {
|
||||
session_output_cache: HashMap::new(),
|
||||
unread_message_counts: HashMap::new(),
|
||||
selected_messages: Vec::new(),
|
||||
selected_parent_session: None,
|
||||
selected_child_sessions: Vec::new(),
|
||||
logs: Vec::new(),
|
||||
selected_diff_summary: None,
|
||||
selected_pane: Pane::Sessions,
|
||||
@@ -127,6 +131,7 @@ impl Dashboard {
|
||||
dashboard.sync_selected_output();
|
||||
dashboard.sync_selected_diff();
|
||||
dashboard.sync_selected_messages();
|
||||
dashboard.sync_selected_lineage();
|
||||
dashboard.refresh_logs();
|
||||
dashboard
|
||||
}
|
||||
@@ -474,6 +479,7 @@ impl Dashboard {
|
||||
self.sync_selected_output();
|
||||
self.sync_selected_diff();
|
||||
self.sync_selected_messages();
|
||||
self.sync_selected_lineage();
|
||||
self.refresh_logs();
|
||||
}
|
||||
Pane::Output => {
|
||||
@@ -507,6 +513,7 @@ impl Dashboard {
|
||||
self.sync_selected_output();
|
||||
self.sync_selected_diff();
|
||||
self.sync_selected_messages();
|
||||
self.sync_selected_lineage();
|
||||
self.refresh_logs();
|
||||
}
|
||||
Pane::Output => {
|
||||
@@ -584,6 +591,7 @@ impl Dashboard {
|
||||
self.sync_selected_output();
|
||||
self.sync_selected_diff();
|
||||
self.sync_selected_messages();
|
||||
self.sync_selected_lineage();
|
||||
self.refresh_logs();
|
||||
}
|
||||
|
||||
@@ -685,6 +693,7 @@ impl Dashboard {
|
||||
self.sync_selected_output();
|
||||
self.sync_selected_diff();
|
||||
self.sync_selected_messages();
|
||||
self.sync_selected_lineage();
|
||||
self.refresh_logs();
|
||||
}
|
||||
|
||||
@@ -769,6 +778,30 @@ impl Dashboard {
|
||||
};
|
||||
}
|
||||
|
||||
fn sync_selected_lineage(&mut self) {
|
||||
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
|
||||
self.selected_parent_session = None;
|
||||
self.selected_child_sessions.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
self.selected_parent_session = match self.db.latest_task_handoff_source(&session_id) {
|
||||
Ok(parent) => parent,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to load session parent linkage: {error}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
self.selected_child_sessions = match self.db.delegated_children(&session_id, 3) {
|
||||
Ok(children) => children,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to load delegated child sessions: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn selected_session_id(&self) -> Option<&str> {
|
||||
self.sessions
|
||||
.get(self.selected_session)
|
||||
@@ -854,6 +887,21 @@ impl Dashboard {
|
||||
format!("Task {}", session.task),
|
||||
];
|
||||
|
||||
if let Some(parent) = self.selected_parent_session.as_ref() {
|
||||
lines.push(format!("Delegated from {}", format_session_id(parent)));
|
||||
}
|
||||
|
||||
if !self.selected_child_sessions.is_empty() {
|
||||
lines.push(format!(
|
||||
"Delegates {}",
|
||||
self.selected_child_sessions
|
||||
.iter()
|
||||
.map(|session_id| format_session_id(session_id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(worktree) = session.worktree.as_ref() {
|
||||
lines.push(format!(
|
||||
"Branch {} | Base {}",
|
||||
@@ -1770,6 +1818,8 @@ mod tests {
|
||||
session_output_cache: HashMap::new(),
|
||||
unread_message_counts: HashMap::new(),
|
||||
selected_messages: Vec::new(),
|
||||
selected_parent_session: None,
|
||||
selected_child_sessions: Vec::new(),
|
||||
logs: Vec::new(),
|
||||
selected_diff_summary: None,
|
||||
selected_pane: Pane::Sessions,
|
||||
|
||||
Reference in New Issue
Block a user