diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-06 18:25:30 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-06 18:25:30 +0300 |
| commit | 62b1f5e011c46403c3b78f661a3e5a385d2ec72c (patch) | |
| tree | 95cf29d1f4b222a1e7fbc9029ce360829c304fbb /src | |
| parent | 7ec046a4646e94311fced28a193ff1f10676a7a6 (diff) | |
feat: add compact agent detail to collapsed sidebar
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/input.rs | 135 | ||||
| -rw-r--r-- | src/ui.rs | 92 |
2 files changed, 199 insertions, 28 deletions
diff --git a/src/app/input.rs b/src/app/input.rs index b2a721a6..81089217 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -1971,10 +1971,21 @@ impl AppState { if in_sidebar { if self.sidebar_collapsed { - let idx = (mouse.row - sidebar.y) as usize; - if idx < self.workspaces.len() { + if let Some(idx) = self.collapsed_workspace_at_row(mouse.row) { self.switch_workspace(idx); self.mode = Mode::Terminal; + return None; + } + + if let Some((ws_idx, tab_idx, pane_id)) = + self.collapsed_agent_detail_target_at(mouse.row) + { + self.switch_workspace(ws_idx); + if let Some(ws) = self.workspaces.get_mut(ws_idx) { + ws.switch_tab(tab_idx); + ws.layout.focus_pane(pane_id); + } + self.mode = Mode::Terminal; } return None; } @@ -2346,6 +2357,68 @@ impl AppState { }) } + fn collapsed_workspace_at_row(&self, row: u16) -> Option<usize> { + if !self.sidebar_collapsed { + return None; + } + + let (ws_area, _, _) = crate::ui::collapsed_sidebar_sections(self.view.sidebar_rect); + if ws_area == Rect::default() || row < ws_area.y || row >= ws_area.y + ws_area.height { + return None; + } + + let idx = (row - ws_area.y) as usize; + (idx < self.workspaces.len()).then_some(idx) + } + + fn collapsed_detail_workspace_idx(&self) -> Option<usize> { + if matches!( + self.mode, + Mode::Navigate + | Mode::RenameWorkspace + | Mode::Resize + | Mode::ConfirmClose + | Mode::ContextMenu + | Mode::Settings + | Mode::GlobalMenu + | Mode::KeybindHelp + ) { + Some(self.selected) + } else { + self.active + } + } + + fn collapsed_agent_detail_target_at( + &self, + row: u16, + ) -> Option<(usize, usize, crate::layout::PaneId)> { + if !self.sidebar_collapsed { + return None; + } + + let (_, _, detail_area) = crate::ui::collapsed_sidebar_sections(self.view.sidebar_rect); + let detail_content_area = Rect::new( + detail_area.x, + detail_area.y, + detail_area.width, + detail_area.height.saturating_sub(1), + ); + if detail_content_area == Rect::default() + || row < detail_content_area.y + || row >= detail_content_area.y + detail_content_area.height + { + return None; + } + + let ws_idx = self.collapsed_detail_workspace_idx()?; + let ws = self.workspaces.get(ws_idx)?; + let detail_idx = (row - detail_content_area.y) as usize; + let details = ws.pane_details(); + let detail = details.get(detail_idx)?; + Some((ws_idx, detail.tab_idx, detail.pane_id)) + } + fn workspace_drop_index_at_row(&self, row: u16) -> Option<usize> { let area = self.workspace_list_rect(); let footer = self.sidebar_footer_rect(); @@ -2415,22 +2488,7 @@ impl AppState { return None; } - let detail_ws_idx = if matches!( - self.mode, - Mode::Navigate - | Mode::RenameWorkspace - | Mode::Resize - | Mode::ConfirmClose - | Mode::ContextMenu - | Mode::Settings - | Mode::GlobalMenu - | Mode::KeybindHelp - ) { - self.selected - } else { - self.active? - }; - + let detail_ws_idx = self.collapsed_detail_workspace_idx()?; let ws = self.workspaces.get(detail_ws_idx)?; let relative_row = row - (detail_area.y + 3); let entry_height = 3; @@ -3455,6 +3513,47 @@ mod tests { } #[test] + fn clicking_collapsed_agent_row_switches_to_correct_tab_and_pane() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let first_pane = ws.tabs[0].root_pane; + ws.tabs[0] + .panes + .get_mut(&first_pane) + .unwrap() + .detected_agent = Some(Agent::Pi); + let second_tab = ws.test_add_tab(Some("logs")); + let second_pane = ws.tabs[second_tab].root_pane; + ws.tabs[second_tab] + .panes + .get_mut(&second_pane) + .unwrap() + .detected_agent = Some(Agent::Claude); + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.sidebar_collapsed = true; + app.state.view.sidebar_rect = Rect::new(0, 0, 4, 20); + app.state.view.terminal_area = Rect::new(4, 0, 80, 20); + + let (_, _, detail_area) = + crate::ui::collapsed_sidebar_sections(app.state.view.sidebar_rect); + app.handle_mouse(mouse( + MouseEventKind::Down(MouseButton::Left), + detail_area.x, + detail_area.y + 1, + )); + + assert_eq!(app.state.workspaces[0].active_tab, 1); + assert_eq!( + app.state.workspaces[0].tabs[1].layout.focused(), + second_pane + ); + assert_eq!(app.state.mode, Mode::Terminal); + } + + #[test] fn clicking_workspace_switches_on_mouse_up() { let mut app = app_for_mouse_test(); app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")]; @@ -447,7 +447,30 @@ fn compute_sidebar_width(app: &AppState) -> u16 { .clamp(MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH) } -/// Collapsed sidebar: pure glance mode. +pub(crate) fn collapsed_sidebar_sections(area: Rect) -> (Rect, Option<u16>, Rect) { + let content = Rect::new(area.x, area.y, area.width.saturating_sub(1), area.height); + if content.width == 0 || content.height == 0 { + return (Rect::default(), None, Rect::default()); + } + + if content.height < 7 { + return (content, None, Rect::default()); + } + + let total_h = content.height as usize; + let ws_h = (total_h + 1) / 2; + let detail_h = total_h.saturating_sub(ws_h + 1); + if ws_h == 0 || detail_h == 0 { + return (content, None, Rect::default()); + } + + let divider_y = content.y + ws_h as u16; + let ws_area = Rect::new(content.x, content.y, content.width, ws_h as u16); + let detail_area = Rect::new(content.x, divider_y + 1, content.width, detail_h as u16); + (ws_area, Some(divider_y), detail_area) +} + +/// Collapsed sidebar: workspace glance on top, compact agent list below. fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) { let is_navigating = matches!(app.mode, Mode::Navigate); @@ -464,17 +487,20 @@ fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) { buf[(sep_x, y)].set_style(sep_style); } - let content_w = area.width.saturating_sub(1); - let bottom_y = area.y + area.height.saturating_sub(1); + let (ws_area, divider_y, detail_area) = collapsed_sidebar_sections(area); + if ws_area == Rect::default() { + render_sidebar_toggle(app, frame, area, true, p); + return; + } - for (i, ws) in app.workspaces.iter().enumerate() { - let y = area.y + i as u16; - if y >= bottom_y { + for (visible_idx, ws) in app.workspaces.iter().enumerate() { + let y = ws_area.y + visible_idx as u16; + if y >= ws_area.y + ws_area.height { break; } let (agg_state, agg_seen) = ws.aggregate_state(); let (icon, icon_style) = state_dot(agg_state, agg_seen, p); - let is_selected = i == app.selected && is_navigating; + let is_selected = visible_idx == app.selected && is_navigating; let row_style = if is_selected { Style::default().bg(p.surface0) } else { @@ -488,21 +514,67 @@ fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) { if is_selected { let buf = frame.buffer_mut(); - for x in area.x..area.x + content_w { + for x in ws_area.x..ws_area.x + ws_area.width { buf[(x, y)].set_style(row_style); } } frame.render_widget( Paragraph::new(Line::from(vec![ - Span::styled(format!("{}", i + 1), num_style), + Span::styled(format!("{}", visible_idx + 1), num_style), Span::styled(" ", row_style), Span::styled(icon, icon_style), ])), - Rect::new(area.x, y, content_w, 1), + Rect::new(ws_area.x, y, ws_area.width, 1), ); } + if let Some(divider_y) = divider_y { + let buf = frame.buffer_mut(); + for x in ws_area.x..ws_area.x + ws_area.width { + buf[(x, divider_y)].set_symbol("─"); + buf[(x, divider_y)].set_style(Style::default().fg(p.surface_dim)); + } + } + + let detail_ws_idx = if is_navigating { + Some(app.selected) + } else { + app.active + }; + let detail_content_area = Rect::new( + detail_area.x, + detail_area.y, + detail_area.width, + detail_area.height.saturating_sub(1), + ); + if detail_content_area != Rect::default() { + if let Some(ws_idx) = detail_ws_idx { + if let Some(ws) = app.workspaces.get(ws_idx) { + for (detail_idx, detail) in ws.pane_details().iter().enumerate() { + let y = detail_content_area.y + detail_idx as u16; + if y >= detail_content_area.y + detail_content_area.height { + break; + } + let pane_num = ws + .public_pane_number(detail.pane_id) + .unwrap_or(detail_idx + 1); + let pane_style = Style::default().fg(p.overlay0); + let (icon, icon_style) = + agent_icon(detail.state, detail.seen, app.spinner_tick, p); + frame.render_widget( + Paragraph::new(Line::from(vec![ + Span::styled(format!("{pane_num}"), pane_style), + Span::styled(" ", pane_style), + Span::styled(icon, icon_style), + ])), + Rect::new(detail_content_area.x, y, detail_content_area.width, 1), + ); + } + } + } + } + render_sidebar_toggle(app, frame, area, true, p); } |
