diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-30 14:54:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-03-30 14:54:21 +0300 |
| commit | 55684493890b4f67c73ea07b5af5911ec1b4d80e (patch) | |
| tree | 60d84536c377bc9a124c11a4b02f9e6da854caba /src | |
| parent | ca93e6e2511d13b7041504e0665fff2d7a402c28 (diff) | |
refactor sidebar status rollup and preserve pane order
Diffstat (limited to 'src')
| -rw-r--r-- | src/detect.rs | 40 | ||||
| -rw-r--r-- | src/input/tests.rs | 5 | ||||
| -rw-r--r-- | src/ui.rs | 24 | ||||
| -rw-r--r-- | src/workspace.rs | 124 |
4 files changed, 76 insertions, 117 deletions
diff --git a/src/detect.rs b/src/detect.rs index ae7559bd..bc59fe25 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -16,19 +16,6 @@ pub enum AgentState { Unknown, } -impl AgentState { - /// Priority for rolling up pane states to a workspace indicator. - /// Higher = more urgent. - pub fn priority(self) -> u8 { - match self { - Self::Unknown => 0, - Self::Idle => 1, - Self::Working => 2, - Self::Blocked => 3, - } - } -} - /// Which agent we detected running in a pane. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Agent { @@ -106,16 +93,6 @@ pub fn detect_state(agent: Option<Agent>, screen_content: &str) -> AgentState { } } -/// Roll up multiple pane states into a single workspace state. -/// Returns the highest-priority state among all panes. -pub fn workspace_state(pane_states: &[AgentState]) -> AgentState { - pane_states - .iter() - .max_by_key(|s| s.priority()) - .copied() - .unwrap_or(AgentState::Unknown) -} - // --------------------------------------------------------------------------- // Per-agent detectors // --------------------------------------------------------------------------- @@ -622,23 +599,6 @@ mod tests { // ---- Workspace state rollup ---- - #[test] - fn workspace_state_blocked_wins() { - let states = [AgentState::Idle, AgentState::Working, AgentState::Blocked]; - assert_eq!(workspace_state(&states), AgentState::Blocked); - } - - #[test] - fn workspace_state_working_over_idle() { - let states = [AgentState::Idle, AgentState::Working, AgentState::Unknown]; - assert_eq!(workspace_state(&states), AgentState::Working); - } - - #[test] - fn workspace_state_empty() { - assert_eq!(workspace_state(&[]), AgentState::Unknown); - } - // ---- No agent → Unknown ---- #[test] diff --git a/src/input/tests.rs b/src/input/tests.rs index d9b5c101..a4f32626 100644 --- a/src/input/tests.rs +++ b/src/input/tests.rs @@ -278,7 +278,10 @@ fn kitty_shift_letter_release_does_not_emit_text() { fn kitty_shifted_symbol_sends_text() { let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT) .with_shifted_codepoint('!' as u32); - assert_eq!(encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }), b"!"); + assert_eq!( + encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }), + b"!" + ); } #[test] @@ -176,13 +176,8 @@ fn compute_sidebar_width(app: &AppState) -> u16 { .map(|(i, ws)| { let name_len = ws.display_name().len(); let number_len = (i + 1).to_string().len(); - let pane_dots = if ws.layout.pane_count() > 1 { - ws.layout.pane_count() - } else { - 1 - }; - // marker + number + space + name + spaces + dots - let line1 = 3 + number_len + name_len + 2 + pane_dots; + // marker + number + space + name + spaces + aggregate dot + let line1 = 3 + number_len + name_len + 3; // branch line: " branch" let line2 = ws.branch().map(|b| 3 + b.len()).unwrap_or(0); line1.max(line2) @@ -381,7 +376,7 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig Style::default().fg(p.overlay0) }; - // Line 1: marker-space + number + name + state dots + // Line 1: marker-space + number + name + aggregate state dot let marker = if is_active { " " } else { " " }; // accent bar handles active indicator let mut line1 = vec![ Span::styled(marker, Style::default()), @@ -390,16 +385,8 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig Span::styled(" ", Style::default()), ]; - // State dots (one per pane, or single aggregate) - if ws.layout.pane_count() == 1 { - let (icon, icon_style) = state_dot(agg_state, agg_seen, p); - line1.push(Span::styled(icon, icon_style)); - } else { - for (pane_state, pane_seen) in ws.pane_states() { - let (icon, icon_style) = state_dot(pane_state, pane_seen, p); - line1.push(Span::styled(icon, icon_style)); - } - } + let (icon, icon_style) = state_dot(agg_state, agg_seen, p); + line1.push(Span::styled(icon, icon_style)); frame.render_widget( Paragraph::new(Line::from(line1)), @@ -1652,6 +1639,7 @@ fn render_toast_notification( /// /// | State | Icon | Color | /// |--------------------|------|--------| +/// | Blocked | ● | Red | /// | Working | ● | Yellow | /// | Done (idle+unseen) | ● | Blue | /// | Idle (seen) | ○ | Green | diff --git a/src/workspace.rs b/src/workspace.rs index 909c4315..68b4e624 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -5,7 +5,7 @@ use ratatui::layout::Direction; use tokio::sync::mpsc; use tracing::info; -use crate::detect::{self, Agent, AgentState}; +use crate::detect::{Agent, AgentState}; use crate::events::AppEvent; use crate::layout::{PaneId, TileLayout}; use crate::pane::{PaneRuntime, PaneState}; @@ -160,44 +160,18 @@ impl Workspace { self.runtimes.get(&self.root_pane).and_then(|rt| rt.cwd()) } - /// Agent name summary (e.g. "claude, amp"). Used by collapsed sidebar. - pub fn agent_summary(&self) -> Option<String> { - let mut names = Vec::new(); - - for id in self.layout.pane_ids() { - if let Some(agent) = self.panes.get(&id).and_then(|p| p.detected_agent) { - let name = agent_name(agent); - if !names.iter().any(|n| *n == name) { - names.push(name); - } - } - } - - if names.is_empty() { - if self.root_cwd().is_some() { - Some("shell".to_string()) - } else { - None - } - } else if names.len() == 1 { - Some(names[0].to_string()) - } else if names.len() == 2 { - Some(format!("{}, {}", names[0], names[1])) - } else { - Some(format!("{}, {} +{}", names[0], names[1], names.len() - 2)) - } - } - - /// Aggregate state + seen across all panes. - /// Returns the highest-priority state and the worst-case seen flag. + /// Aggregate workspace signal for sidebar triage. + /// Returns the most urgent pane's state + seen flag. pub fn aggregate_state(&self) -> (AgentState, bool) { - let states: Vec<AgentState> = self.panes.values().map(|p| p.state).collect(); - let state = detect::workspace_state(&states); - let seen = self.panes.values().all(|p| p.seen); - (state, seen) + self.panes + .values() + .map(|pane| (pane.state, pane.seen)) + .max_by_key(|(state, seen)| pane_attention_priority(*state, *seen)) + .unwrap_or((AgentState::Unknown, true)) } /// Per-pane (state, seen) in BSP tree order (left-to-right, top-to-bottom). + #[cfg(test)] pub fn pane_states(&self) -> Vec<(AgentState, bool)> { self.layout .pane_ids() @@ -211,10 +185,9 @@ impl Workspace { .collect() } - /// Per-pane detail for the agent detail panel, ordered by urgency. + /// Per-pane detail for the agent detail panel, in stable layout order. pub fn pane_details(&self) -> Vec<PaneDetail> { - let mut details: Vec<PaneDetail> = self - .layout + self.layout .pane_ids() .iter() .map(|id| { @@ -232,10 +205,7 @@ impl Workspace { seen, } }) - .collect(); - // Sort by urgency: blocked > working > idle(unseen) > idle > unknown - details.sort_by(|a, b| b.urgency().cmp(&a.urgency())); - details + .collect() } /// Get the git branch for this workspace's root cwd. @@ -264,16 +234,13 @@ pub struct PaneDetail { pub seen: bool, } -impl PaneDetail { - /// Urgency score for sort ordering (higher = more urgent, shown first). - fn urgency(&self) -> u8 { - match (self.state, self.seen) { - (AgentState::Blocked, _) => 4, - (AgentState::Working, _) => 3, - (AgentState::Idle, false) => 2, // unseen/done - (AgentState::Idle, true) => 1, - (AgentState::Unknown, _) => 0, - } +fn pane_attention_priority(state: AgentState, seen: bool) -> u8 { + match (state, seen) { + (AgentState::Blocked, _) => 4, + (AgentState::Idle, false) => 3, // done, waiting for you to look + (AgentState::Working, _) => 2, + (AgentState::Idle, true) => 1, + (AgentState::Unknown, _) => 0, } } @@ -430,22 +397,44 @@ mod tests { ws.panes.get_mut(&root_id).unwrap().state = AgentState::Idle; ws.panes.get_mut(&id2).unwrap().state = AgentState::Working; - let (state, _) = ws.aggregate_state(); + let (state, seen) = ws.aggregate_state(); assert_eq!(state, AgentState::Working); + assert!(seen); } #[test] - fn aggregate_seen_any_unseen_means_unseen() { + fn aggregate_state_done_unseen_beats_working() { let mut ws = Workspace::test_new("test"); let id2 = ws.test_split(Direction::Horizontal); - ws.panes.get_mut(&id2).unwrap().seen = false; + let root_id = *ws.panes.keys().find(|id| **id != id2).unwrap(); + let root = ws.panes.get_mut(&root_id).unwrap(); + root.state = AgentState::Idle; + root.seen = false; + ws.panes.get_mut(&id2).unwrap().state = AgentState::Working; - let (_, seen) = ws.aggregate_state(); + let (state, seen) = ws.aggregate_state(); + assert_eq!(state, AgentState::Idle); assert!(!seen); } #[test] + fn aggregate_state_blocked_beats_done_unseen() { + let mut ws = Workspace::test_new("test"); + let id2 = ws.test_split(Direction::Horizontal); + + let root_id = *ws.panes.keys().find(|id| **id != id2).unwrap(); + let root = ws.panes.get_mut(&root_id).unwrap(); + root.state = AgentState::Idle; + root.seen = false; + ws.panes.get_mut(&id2).unwrap().state = AgentState::Blocked; + + let (state, seen) = ws.aggregate_state(); + assert_eq!(state, AgentState::Blocked); + assert!(seen); + } + + #[test] fn close_focused_removes_pane() { let mut ws = Workspace::test_new("test"); let _id2 = ws.test_split(Direction::Horizontal); @@ -475,8 +464,27 @@ mod tests { let states = ws.pane_states(); assert_eq!(states.len(), 2); - assert!(states.iter().any(|(s, _)| *s == AgentState::Blocked)); - assert!(states.iter().any(|(s, _)| *s == AgentState::Unknown)); + assert_eq!(states[0].0, AgentState::Unknown); + assert_eq!(states[1].0, AgentState::Blocked); + } + + #[test] + fn pane_details_stay_in_layout_order() { + let mut ws = Workspace::test_new("test"); + let id2 = ws.test_split(Direction::Horizontal); + + let root_id = *ws.panes.keys().find(|id| **id != id2).unwrap(); + ws.panes.get_mut(&root_id).unwrap().detected_agent = Some(Agent::Pi); + ws.panes.get_mut(&root_id).unwrap().state = AgentState::Working; + ws.panes.get_mut(&id2).unwrap().detected_agent = Some(Agent::Claude); + ws.panes.get_mut(&id2).unwrap().state = AgentState::Blocked; + + let details = ws.pane_details(); + assert_eq!(details.len(), 2); + assert_eq!(details[0].label, "pi"); + assert_eq!(details[0].state, AgentState::Working); + assert_eq!(details[1].label, "claude"); + assert_eq!(details[1].state, AgentState::Blocked); } #[test] |
