mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-08 18:33:28 +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> {
|
||||
for _ in 0..50 {
|
||||
for _ in 0..200 {
|
||||
if path.exists() {
|
||||
return fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read {}", path.display()));
|
||||
@@ -1392,7 +1392,7 @@ mod tests {
|
||||
"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(
|
||||
&db,
|
||||
&cfg,
|
||||
@@ -1419,10 +1419,6 @@ mod tests {
|
||||
&& 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
||||
(_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(),
|
||||
(_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(),
|
||||
(_, KeyCode::Char('n')) => dashboard.new_session().await,
|
||||
(_, KeyCode::Char('a')) => dashboard.assign_selected().await,
|
||||
(_, KeyCode::Char('s')) => dashboard.stop_selected().await,
|
||||
(_, KeyCode::Char('u')) => dashboard.resume_selected().await,
|
||||
(_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct Dashboard {
|
||||
selected_messages: Vec<SessionMessage>,
|
||||
selected_parent_session: Option<String>,
|
||||
selected_child_sessions: Vec<DelegatedChildSummary>,
|
||||
selected_team_summary: Option<TeamSummary>,
|
||||
logs: Vec<ToolLogEntry>,
|
||||
selected_diff_summary: Option<String>,
|
||||
selected_pane: Pane,
|
||||
@@ -95,6 +96,16 @@ struct DelegatedChildSummary {
|
||||
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 {
|
||||
pub fn new(db: StateStore, cfg: Config) -> Self {
|
||||
Self::with_output_store(db, cfg, SessionOutputStore::default())
|
||||
@@ -123,6 +134,7 @@ impl Dashboard {
|
||||
selected_messages: Vec::new(),
|
||||
selected_parent_session: None,
|
||||
selected_child_sessions: Vec::new(),
|
||||
selected_team_summary: None,
|
||||
logs: Vec::new(),
|
||||
selected_diff_summary: None,
|
||||
selected_pane: Pane::Sessions,
|
||||
@@ -379,7 +391,7 @@ impl Dashboard {
|
||||
|
||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||
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()
|
||||
);
|
||||
let aggregate = self.aggregate_usage();
|
||||
@@ -419,6 +431,7 @@ impl Dashboard {
|
||||
"Keyboard Shortcuts:",
|
||||
"",
|
||||
" n New session",
|
||||
" a Assign follow-up work from selected session",
|
||||
" s Stop selected session",
|
||||
" u Resume selected session",
|
||||
" x Cleanup selected worktree",
|
||||
@@ -602,6 +615,44 @@ impl Dashboard {
|
||||
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) {
|
||||
let Some(session) = self.sessions.get(self.selected_session) else {
|
||||
return;
|
||||
@@ -789,6 +840,7 @@ impl Dashboard {
|
||||
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
|
||||
self.selected_parent_session = None;
|
||||
self.selected_child_sessions.clear();
|
||||
self.selected_team_summary = None;
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -800,28 +852,51 @@ impl Dashboard {
|
||||
}
|
||||
};
|
||||
|
||||
self.selected_child_sessions = match self.db.delegated_children(&session_id, 3) {
|
||||
Ok(children) => children
|
||||
.into_iter()
|
||||
.filter_map(|child_id| match self.db.get_session(&child_id) {
|
||||
Ok(Some(session)) => Some(DelegatedChildSummary {
|
||||
unread_messages: self
|
||||
.unread_message_counts
|
||||
.get(&child_id)
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
state: session.state,
|
||||
session_id: child_id,
|
||||
}),
|
||||
Ok(None) => None,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to load delegated child session {}: {error}", child_id);
|
||||
None
|
||||
self.selected_child_sessions = match self.db.delegated_children(&session_id, 50) {
|
||||
Ok(children) => {
|
||||
let mut delegated = Vec::new();
|
||||
let mut team = TeamSummary::default();
|
||||
|
||||
for child_id in children {
|
||||
match self.db.get_session(&child_id) {
|
||||
Ok(Some(session)) => {
|
||||
team.total += 1;
|
||||
match session.state {
|
||||
SessionState::Idle => team.idle += 1,
|
||||
SessionState::Running => team.running += 1,
|
||||
SessionState::Pending => team.pending += 1,
|
||||
SessionState::Failed => team.failed += 1,
|
||||
SessionState::Stopped => team.stopped += 1,
|
||||
SessionState::Completed => {}
|
||||
}
|
||||
|
||||
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) => {
|
||||
tracing::warn!("Failed to load delegated child sessions: {error}");
|
||||
self.selected_team_summary = None;
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
@@ -916,6 +991,19 @@ impl Dashboard {
|
||||
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() {
|
||||
lines.push("Delegates".to_string());
|
||||
for child in &self.selected_child_sessions {
|
||||
@@ -1468,6 +1556,32 @@ mod tests {
|
||||
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]
|
||||
fn aggregate_cost_summary_mentions_total_cost() {
|
||||
let db = StateStore::open(Path::new(":memory:")).unwrap();
|
||||
@@ -1846,6 +1960,7 @@ mod tests {
|
||||
selected_messages: Vec::new(),
|
||||
selected_parent_session: None,
|
||||
selected_child_sessions: Vec::new(),
|
||||
selected_team_summary: None,
|
||||
logs: Vec::new(),
|
||||
selected_diff_summary: None,
|
||||
selected_pane: Pane::Sessions,
|
||||
|
||||
Reference in New Issue
Block a user