aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-06 18:25:30 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-06 18:25:30 +0300
commit62b1f5e011c46403c3b78f661a3e5a385d2ec72c (patch)
tree95cf29d1f4b222a1e7fbc9029ce360829c304fbb /src
parent7ec046a4646e94311fced28a193ff1f10676a7a6 (diff)
feat: add compact agent detail to collapsed sidebar
Diffstat (limited to 'src')
-rw-r--r--src/app/input.rs135
-rw-r--r--src/ui.rs92
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")];
diff --git a/src/ui.rs b/src/ui.rs
index 1d5ebc9e..a949a90d 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -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);
}