mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-14 22:13:41 +08:00
feat: add ecc2 dashboard assignment controls
This commit is contained in:
@@ -944,7 +944,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_file(path: &Path) -> Result<String> {
|
fn wait_for_file(path: &Path) -> Result<String> {
|
||||||
for _ in 0..50 {
|
for _ in 0..200 {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
return fs::read_to_string(path)
|
return fs::read_to_string(path)
|
||||||
.with_context(|| format!("failed to read {}", path.display()));
|
.with_context(|| format!("failed to read {}", path.display()));
|
||||||
@@ -1392,7 +1392,7 @@ mod tests {
|
|||||||
"task_handoff",
|
"task_handoff",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (fake_runner, log_path) = write_fake_claude(tempdir.path())?;
|
let (fake_runner, _) = write_fake_claude(tempdir.path())?;
|
||||||
let outcome = assign_session_in_dir_with_runner_program(
|
let outcome = assign_session_in_dir_with_runner_program(
|
||||||
&db,
|
&db,
|
||||||
&cfg,
|
&cfg,
|
||||||
@@ -1419,10 +1419,6 @@ mod tests {
|
|||||||
&& message.content.contains("New delegated task")
|
&& message.content.contains("New delegated task")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let log = wait_for_file(&log_path)?;
|
|
||||||
assert!(log.contains("run-session"));
|
|
||||||
assert!(log.contains("New delegated task"));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
|||||||
(_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(),
|
(_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(),
|
||||||
(_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(),
|
(_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(),
|
||||||
(_, KeyCode::Char('n')) => dashboard.new_session().await,
|
(_, KeyCode::Char('n')) => dashboard.new_session().await,
|
||||||
|
(_, KeyCode::Char('a')) => dashboard.assign_selected().await,
|
||||||
(_, KeyCode::Char('s')) => dashboard.stop_selected().await,
|
(_, KeyCode::Char('s')) => dashboard.stop_selected().await,
|
||||||
(_, KeyCode::Char('u')) => dashboard.resume_selected().await,
|
(_, KeyCode::Char('u')) => dashboard.resume_selected().await,
|
||||||
(_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await,
|
(_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub struct Dashboard {
|
|||||||
selected_messages: Vec<SessionMessage>,
|
selected_messages: Vec<SessionMessage>,
|
||||||
selected_parent_session: Option<String>,
|
selected_parent_session: Option<String>,
|
||||||
selected_child_sessions: Vec<DelegatedChildSummary>,
|
selected_child_sessions: Vec<DelegatedChildSummary>,
|
||||||
|
selected_team_summary: Option<TeamSummary>,
|
||||||
logs: Vec<ToolLogEntry>,
|
logs: Vec<ToolLogEntry>,
|
||||||
selected_diff_summary: Option<String>,
|
selected_diff_summary: Option<String>,
|
||||||
selected_pane: Pane,
|
selected_pane: Pane,
|
||||||
@@ -95,6 +96,16 @@ struct DelegatedChildSummary {
|
|||||||
unread_messages: usize,
|
unread_messages: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
struct TeamSummary {
|
||||||
|
total: usize,
|
||||||
|
idle: usize,
|
||||||
|
running: usize,
|
||||||
|
pending: usize,
|
||||||
|
failed: usize,
|
||||||
|
stopped: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl Dashboard {
|
impl Dashboard {
|
||||||
pub fn new(db: StateStore, cfg: Config) -> Self {
|
pub fn new(db: StateStore, cfg: Config) -> Self {
|
||||||
Self::with_output_store(db, cfg, SessionOutputStore::default())
|
Self::with_output_store(db, cfg, SessionOutputStore::default())
|
||||||
@@ -123,6 +134,7 @@ impl Dashboard {
|
|||||||
selected_messages: Vec::new(),
|
selected_messages: Vec::new(),
|
||||||
selected_parent_session: None,
|
selected_parent_session: None,
|
||||||
selected_child_sessions: Vec::new(),
|
selected_child_sessions: Vec::new(),
|
||||||
|
selected_team_summary: None,
|
||||||
logs: Vec::new(),
|
logs: Vec::new(),
|
||||||
selected_diff_summary: None,
|
selected_diff_summary: None,
|
||||||
selected_pane: Pane::Sessions,
|
selected_pane: Pane::Sessions,
|
||||||
@@ -379,7 +391,7 @@ impl Dashboard {
|
|||||||
|
|
||||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||||
let text = format!(
|
let text = format!(
|
||||||
" [n]ew session [s]top [u]resume [x]cleanup [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ",
|
" [n]ew session [a]ssign [s]top [u]resume [x]cleanup [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ",
|
||||||
self.layout_label()
|
self.layout_label()
|
||||||
);
|
);
|
||||||
let aggregate = self.aggregate_usage();
|
let aggregate = self.aggregate_usage();
|
||||||
@@ -419,6 +431,7 @@ impl Dashboard {
|
|||||||
"Keyboard Shortcuts:",
|
"Keyboard Shortcuts:",
|
||||||
"",
|
"",
|
||||||
" n New session",
|
" n New session",
|
||||||
|
" a Assign follow-up work from selected session",
|
||||||
" s Stop selected session",
|
" s Stop selected session",
|
||||||
" u Resume selected session",
|
" u Resume selected session",
|
||||||
" x Cleanup selected worktree",
|
" x Cleanup selected worktree",
|
||||||
@@ -602,6 +615,44 @@ impl Dashboard {
|
|||||||
self.refresh_logs();
|
self.refresh_logs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn assign_selected(&mut self) {
|
||||||
|
let Some(source_session) = self.sessions.get(self.selected_session) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = self.new_session_task();
|
||||||
|
let agent = self.cfg.default_agent.clone();
|
||||||
|
|
||||||
|
let outcome = match manager::assign_session(
|
||||||
|
&self.db,
|
||||||
|
&self.cfg,
|
||||||
|
&source_session.id,
|
||||||
|
&task,
|
||||||
|
&agent,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(outcome) => outcome,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to assign follow-up work from session {}: {error}",
|
||||||
|
source_session.id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.refresh();
|
||||||
|
self.sync_selection_by_id(Some(&outcome.session_id));
|
||||||
|
self.reset_output_view();
|
||||||
|
self.sync_selected_output();
|
||||||
|
self.sync_selected_diff();
|
||||||
|
self.sync_selected_messages();
|
||||||
|
self.sync_selected_lineage();
|
||||||
|
self.refresh_logs();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn stop_selected(&mut self) {
|
pub async fn stop_selected(&mut self) {
|
||||||
let Some(session) = self.sessions.get(self.selected_session) else {
|
let Some(session) = self.sessions.get(self.selected_session) else {
|
||||||
return;
|
return;
|
||||||
@@ -789,6 +840,7 @@ impl Dashboard {
|
|||||||
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
|
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
|
||||||
self.selected_parent_session = None;
|
self.selected_parent_session = None;
|
||||||
self.selected_child_sessions.clear();
|
self.selected_child_sessions.clear();
|
||||||
|
self.selected_team_summary = None;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -800,28 +852,51 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.selected_child_sessions = match self.db.delegated_children(&session_id, 3) {
|
self.selected_child_sessions = match self.db.delegated_children(&session_id, 50) {
|
||||||
Ok(children) => children
|
Ok(children) => {
|
||||||
.into_iter()
|
let mut delegated = Vec::new();
|
||||||
.filter_map(|child_id| match self.db.get_session(&child_id) {
|
let mut team = TeamSummary::default();
|
||||||
Ok(Some(session)) => Some(DelegatedChildSummary {
|
|
||||||
unread_messages: self
|
for child_id in children {
|
||||||
.unread_message_counts
|
match self.db.get_session(&child_id) {
|
||||||
.get(&child_id)
|
Ok(Some(session)) => {
|
||||||
.copied()
|
team.total += 1;
|
||||||
.unwrap_or(0),
|
match session.state {
|
||||||
state: session.state,
|
SessionState::Idle => team.idle += 1,
|
||||||
session_id: child_id,
|
SessionState::Running => team.running += 1,
|
||||||
}),
|
SessionState::Pending => team.pending += 1,
|
||||||
Ok(None) => None,
|
SessionState::Failed => team.failed += 1,
|
||||||
Err(error) => {
|
SessionState::Stopped => team.stopped += 1,
|
||||||
tracing::warn!("Failed to load delegated child session {}: {error}", child_id);
|
SessionState::Completed => {}
|
||||||
None
|
}
|
||||||
|
|
||||||
|
delegated.push(DelegatedChildSummary {
|
||||||
|
unread_messages: self
|
||||||
|
.unread_message_counts
|
||||||
|
.get(&child_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0),
|
||||||
|
state: session.state,
|
||||||
|
session_id: child_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to load delegated child session {}: {error}",
|
||||||
|
child_id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.collect(),
|
|
||||||
|
self.selected_team_summary = if team.total > 0 { Some(team) } else { None };
|
||||||
|
delegated.truncate(3);
|
||||||
|
delegated
|
||||||
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!("Failed to load delegated child sessions: {error}");
|
tracing::warn!("Failed to load delegated child sessions: {error}");
|
||||||
|
self.selected_team_summary = None;
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -916,6 +991,19 @@ impl Dashboard {
|
|||||||
lines.push(format!("Delegated from {}", format_session_id(parent)));
|
lines.push(format!("Delegated from {}", format_session_id(parent)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(team) = self.selected_team_summary {
|
||||||
|
lines.push(format!(
|
||||||
|
"Team {}/{} | idle {} | running {} | pending {} | failed {} | stopped {}",
|
||||||
|
team.total,
|
||||||
|
self.cfg.max_parallel_sessions,
|
||||||
|
team.idle,
|
||||||
|
team.running,
|
||||||
|
team.pending,
|
||||||
|
team.failed,
|
||||||
|
team.stopped
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if !self.selected_child_sessions.is_empty() {
|
if !self.selected_child_sessions.is_empty() {
|
||||||
lines.push("Delegates".to_string());
|
lines.push("Delegates".to_string());
|
||||||
for child in &self.selected_child_sessions {
|
for child in &self.selected_child_sessions {
|
||||||
@@ -1468,6 +1556,32 @@ mod tests {
|
|||||||
assert!(text.contains("Failed failed-8 | Render dashboard rows"));
|
assert!(text.contains("Failed failed-8 | Render dashboard rows"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_session_metrics_text_includes_team_capacity_summary() {
|
||||||
|
let mut dashboard = test_dashboard(
|
||||||
|
vec![sample_session(
|
||||||
|
"focus-12345678",
|
||||||
|
"planner",
|
||||||
|
SessionState::Running,
|
||||||
|
Some("ecc/focus"),
|
||||||
|
512,
|
||||||
|
42,
|
||||||
|
)],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
dashboard.selected_team_summary = Some(TeamSummary {
|
||||||
|
total: 3,
|
||||||
|
idle: 1,
|
||||||
|
running: 1,
|
||||||
|
pending: 1,
|
||||||
|
failed: 0,
|
||||||
|
stopped: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
let text = dashboard.selected_session_metrics_text();
|
||||||
|
assert!(text.contains("Team 3/8 | idle 1 | running 1 | pending 1 | failed 0 | stopped 0"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn aggregate_cost_summary_mentions_total_cost() {
|
fn aggregate_cost_summary_mentions_total_cost() {
|
||||||
let db = StateStore::open(Path::new(":memory:")).unwrap();
|
let db = StateStore::open(Path::new(":memory:")).unwrap();
|
||||||
@@ -1846,6 +1960,7 @@ mod tests {
|
|||||||
selected_messages: Vec::new(),
|
selected_messages: Vec::new(),
|
||||||
selected_parent_session: None,
|
selected_parent_session: None,
|
||||||
selected_child_sessions: Vec::new(),
|
selected_child_sessions: Vec::new(),
|
||||||
|
selected_team_summary: None,
|
||||||
logs: Vec::new(),
|
logs: Vec::new(),
|
||||||
selected_diff_summary: None,
|
selected_diff_summary: None,
|
||||||
selected_pane: Pane::Sessions,
|
selected_pane: Pane::Sessions,
|
||||||
|
|||||||
Reference in New Issue
Block a user