aboutsummaryrefslogtreecommitdiff
path: root/src/workspace/aggregate.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/workspace/aggregate.rs')
-rw-r--r--src/workspace/aggregate.rs193
1 files changed, 193 insertions, 0 deletions
diff --git a/src/workspace/aggregate.rs b/src/workspace/aggregate.rs
new file mode 100644
index 00000000..53e08920
--- /dev/null
+++ b/src/workspace/aggregate.rs
@@ -0,0 +1,193 @@
+use crate::detect::{Agent, AgentState};
+use crate::layout::PaneId;
+
+use super::{Tab, Workspace};
+
+/// Detail info for a single pane, used by the agent detail panel.
+pub struct PaneDetail {
+ pub pane_id: PaneId,
+ pub tab_idx: usize,
+ pub tab_label: String,
+ pub label: String,
+ pub agent_label: String,
+ #[allow(dead_code)]
+ pub agent: Option<Agent>,
+ pub state: AgentState,
+ pub seen: bool,
+}
+
+impl Tab {
+ pub fn has_working_pane(&self) -> bool {
+ self.panes
+ .values()
+ .any(|pane| pane.state == AgentState::Working)
+ }
+
+ pub fn pane_details(&self) -> Vec<PaneDetail> {
+ self.layout
+ .pane_ids()
+ .iter()
+ .filter_map(|id| {
+ let pane = self.panes.get(id)?;
+ let agent_label = pane.effective_agent_label()?.to_string();
+ Some(PaneDetail {
+ pane_id: *id,
+ tab_idx: self.number.saturating_sub(1),
+ tab_label: self.display_name(),
+ label: agent_label.clone(),
+ agent_label,
+ agent: pane.effective_known_agent(),
+ state: pane.state,
+ seen: pane.seen,
+ })
+ })
+ .collect()
+ }
+}
+
+fn pane_attention_priority(state: AgentState, seen: bool) -> u8 {
+ match (state, seen) {
+ (AgentState::Blocked, _) => 4,
+ (AgentState::Idle, false) => 3,
+ (AgentState::Working, _) => 2,
+ (AgentState::Idle, true) => 1,
+ (AgentState::Unknown, _) => 0,
+ }
+}
+
+impl Workspace {
+ pub fn aggregate_state(&self) -> (AgentState, bool) {
+ self.tabs
+ .iter()
+ .flat_map(|tab| tab.panes.values())
+ .map(|pane| (pane.state, pane.seen))
+ .max_by_key(|(state, seen)| pane_attention_priority(*state, *seen))
+ .unwrap_or((AgentState::Unknown, true))
+ }
+
+ pub fn has_working_pane(&self) -> bool {
+ self.tabs.iter().any(Tab::has_working_pane)
+ }
+
+ pub fn pane_details(&self) -> Vec<PaneDetail> {
+ let multi_tab = self.tabs.len() > 1;
+ self.tabs
+ .iter()
+ .flat_map(Tab::pane_details)
+ .map(|mut detail| {
+ if multi_tab {
+ detail.label = format!("{}·{}", detail.tab_label, detail.agent_label);
+ }
+ detail
+ })
+ .collect()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use ratatui::layout::Direction;
+
+ use super::*;
+ use crate::detect::Agent;
+
+ #[test]
+ fn aggregate_state_all_unknown() {
+ let ws = Workspace::test_new("test");
+ let (state, seen) = ws.aggregate_state();
+ assert_eq!(state, AgentState::Unknown);
+ assert!(seen);
+ }
+
+ #[test]
+ fn aggregate_state_priority() {
+ let mut ws = Workspace::test_new("test");
+ let id2 = ws.test_split(Direction::Horizontal);
+ let root_id = ws.tabs[0]
+ .panes
+ .keys()
+ .find(|id| **id != id2)
+ .copied()
+ .unwrap();
+ ws.tabs[0].panes.get_mut(&root_id).unwrap().state = AgentState::Idle;
+ ws.tabs[0].panes.get_mut(&id2).unwrap().state = AgentState::Working;
+
+ let (state, seen) = ws.aggregate_state();
+ assert_eq!(state, AgentState::Working);
+ assert!(seen);
+ }
+
+ #[test]
+ fn aggregate_state_done_unseen_beats_working() {
+ let mut ws = Workspace::test_new("test");
+ let id2 = ws.test_split(Direction::Horizontal);
+ let root_id = ws.tabs[0]
+ .panes
+ .keys()
+ .find(|id| **id != id2)
+ .copied()
+ .unwrap();
+ let root = ws.tabs[0].panes.get_mut(&root_id).unwrap();
+ root.state = AgentState::Idle;
+ root.seen = false;
+ ws.tabs[0].panes.get_mut(&id2).unwrap().state = AgentState::Working;
+
+ let (state, seen) = ws.aggregate_state();
+ assert_eq!(state, AgentState::Idle);
+ assert!(!seen);
+ }
+
+ #[test]
+ fn pane_details_hide_plain_shells() {
+ let mut ws = Workspace::test_new("test");
+ let root_pane = ws.tabs[0].root_pane;
+ ws.tabs[0].panes.get_mut(&root_pane).unwrap().detected_agent = Some(Agent::Pi);
+ ws.test_split(Direction::Horizontal);
+
+ let details = ws.pane_details();
+ assert_eq!(details.len(), 1);
+ assert_eq!(details[0].label, "pi");
+ }
+
+ #[test]
+ fn pane_details_include_tab_context_when_workspace_has_multiple_tabs() {
+ let mut ws = Workspace::test_new("test");
+ ws.tabs[0].set_custom_name("main".into());
+ let root_pane = ws.tabs[0].root_pane;
+ ws.tabs[0].panes.get_mut(&root_pane).unwrap().detected_agent = Some(Agent::Pi);
+
+ let tab_idx = ws.test_add_tab(Some("logs"));
+ let second_root_pane = ws.tabs[tab_idx].root_pane;
+ ws.tabs[tab_idx]
+ .panes
+ .get_mut(&second_root_pane)
+ .unwrap()
+ .detected_agent = Some(Agent::Claude);
+
+ let details = ws.pane_details();
+ assert_eq!(details.len(), 2);
+ assert!(details.iter().any(|detail| detail.label == "main·pi"));
+ assert!(details.iter().any(|detail| detail.label == "logs·claude"));
+ }
+
+ #[test]
+ fn pane_details_include_hook_reported_unknown_agents() {
+ let mut ws = Workspace::test_new("test");
+ let root_pane = ws.tabs[0].root_pane;
+ ws.tabs[0]
+ .panes
+ .get_mut(&root_pane)
+ .unwrap()
+ .set_hook_authority(
+ "custom:hermes".into(),
+ "hermes".into(),
+ AgentState::Working,
+ None,
+ );
+
+ let details = ws.pane_details();
+ assert_eq!(details.len(), 1);
+ assert_eq!(details[0].agent_label, "hermes");
+ assert_eq!(details[0].agent, None);
+ }
+}