aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-17 16:08:43 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-17 16:08:43 +0300
commit54490254f70469cd154ee630bc4bec84da178f0c (patch)
tree450bf0397398b28932f636d33e37f73c011dd19d /src/ui
parentd998753efe506a04c80306795efc72bff60bb0ec (diff)
feat: sort agent panel by priority
refs #318
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/mobile.rs1
-rw-r--r--src/ui/sidebar.rs147
2 files changed, 79 insertions, 69 deletions
diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs
index d074b68f..80cee6b5 100644
--- a/src/ui/mobile.rs
+++ b/src/ui/mobile.rs
@@ -959,6 +959,7 @@ mod tests {
agent_label: agent_label.map(str::to_string),
state: AgentState::Idle,
seen: true,
+ last_agent_state_change_seq: None,
custom_status: None,
state_labels: std::collections::HashMap::new(),
}
diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs
index 0447b119..15c0a37e 100644
--- a/src/ui/sidebar.rs
+++ b/src/ui/sidebar.rs
@@ -8,7 +8,7 @@ use ratatui::{
use super::scrollbar::{render_scrollbar, should_show_scrollbar};
use super::status::{agent_icon, state_dot, state_label, state_label_color};
-use crate::app::state::{AgentPanelScope, Palette};
+use crate::app::state::{AgentPanelSort, Palette};
use crate::app::{AppState, Mode};
use crate::detect::AgentState;
use crate::terminal::TerminalRuntimeRegistry;
@@ -25,6 +25,7 @@ pub(crate) struct AgentPanelEntry {
pub agent_label: Option<String>,
pub state: AgentState,
pub seen: bool,
+ pub last_agent_state_change_seq: Option<u64>,
pub custom_status: Option<String>,
pub state_labels: std::collections::HashMap<String, String>,
}
@@ -68,39 +69,19 @@ pub(crate) fn sidebar_section_divider_rect(area: Rect, split_ratio: f32) -> Rect
Rect::new(content.x, content.y + ws_h, content.width, 1)
}
-fn agent_panel_current_workspace_idx(app: &AppState) -> Option<usize> {
- if matches!(
- app.mode,
- Mode::Navigate
- | Mode::RenameWorkspace
- | Mode::RenamePane
- | Mode::Resize
- | Mode::ConfirmClose
- | Mode::ContextMenu
- | Mode::Settings
- | Mode::GlobalMenu
- | Mode::KeybindHelp
- | Mode::ProductAnnouncement
- ) {
- Some(app.selected)
- } else {
- app.active
- }
-}
-
-fn agent_panel_toggle_label(scope: AgentPanelScope) -> &'static str {
- match scope {
- AgentPanelScope::CurrentWorkspace => "current",
- AgentPanelScope::AllWorkspaces => "all",
+fn agent_panel_sort_label(sort: AgentPanelSort) -> &'static str {
+ match sort {
+ AgentPanelSort::Spaces => "grouped",
+ AgentPanelSort::Priority => "priority",
}
}
-pub(crate) fn agent_panel_toggle_rect(area: Rect, scope: AgentPanelScope) -> Rect {
+pub(crate) fn agent_panel_toggle_rect(area: Rect, sort: AgentPanelSort) -> Rect {
if area.width == 0 || area.height < 2 {
return Rect::default();
}
- let label = agent_panel_toggle_label(scope);
+ let label = agent_panel_sort_label(sort);
let width = label.chars().count() as u16;
Rect::new(
area.x + area.width.saturating_sub(width),
@@ -134,54 +115,41 @@ fn agent_panel_entries_with_runtimes(
}
};
- match app.agent_panel_scope {
- AgentPanelScope::CurrentWorkspace => {
- let Some(ws_idx) = agent_panel_current_workspace_idx(app) else {
- return Vec::new();
- };
- let Some(ws) = app.workspaces.get(ws_idx) else {
- return Vec::new();
- };
+ let mut entries: Vec<_> = app
+ .workspaces
+ .iter()
+ .enumerate()
+ .flat_map(|(ws_idx, ws)| {
+ let multi_tab = ws.tabs.len() > 1;
+ let workspace_label = ws.display_name_from(&app.terminals, terminal_runtimes);
ws.pane_details(&app.terminals)
.into_iter()
- .map(|detail| AgentPanelEntry {
+ .map(move |detail| AgentPanelEntry {
ws_idx,
tab_idx: detail.tab_idx,
pane_id: detail.pane_id,
- primary_label: detail.label,
- primary_tab_label: None,
- agent_label: None,
+ primary_label: workspace_label.clone(),
+ primary_tab_label: multi_tab.then_some(detail.tab_label),
+ agent_label: Some(detail.agent_label),
state: detail.state,
seen: detail.seen,
+ last_agent_state_change_seq: detail.last_agent_state_change_seq,
custom_status: detail.custom_status,
state_labels: detail.state_labels,
})
- .collect()
- }
- AgentPanelScope::AllWorkspaces => app
- .workspaces
- .iter()
- .enumerate()
- .flat_map(|(ws_idx, ws)| {
- let multi_tab = ws.tabs.len() > 1;
- let workspace_label = ws.display_name_from(&app.terminals, terminal_runtimes);
- ws.pane_details(&app.terminals)
- .into_iter()
- .map(move |detail| AgentPanelEntry {
- ws_idx,
- tab_idx: detail.tab_idx,
- pane_id: detail.pane_id,
- primary_label: workspace_label.clone(),
- primary_tab_label: multi_tab.then_some(detail.tab_label),
- agent_label: Some(detail.agent_label),
- state: detail.state,
- seen: detail.seen,
- custom_status: detail.custom_status,
- state_labels: detail.state_labels,
- })
- })
- .collect(),
+ })
+ .collect();
+
+ if matches!(app.agent_panel_sort, AgentPanelSort::Priority) {
+ entries.sort_by_key(|entry| {
+ (
+ std::cmp::Reverse(workspace_attention_priority(entry.state, entry.seen)),
+ std::cmp::Reverse(entry.last_agent_state_change_seq),
+ )
+ });
}
+
+ entries
}
pub(super) fn agent_panel_status_key(state: AgentState, seen: bool) -> &'static str {
@@ -1067,11 +1035,11 @@ fn render_agent_detail(
)])),
Rect::new(area.x, area.y + 1, area.width, 1),
);
- let toggle_rect = agent_panel_toggle_rect(area, app.agent_panel_scope);
+ let toggle_rect = agent_panel_toggle_rect(area, app.agent_panel_sort);
if toggle_rect != Rect::default() {
frame.render_widget(
Paragraph::new(Span::styled(
- agent_panel_toggle_label(app.agent_panel_scope),
+ agent_panel_sort_label(app.agent_panel_sort),
Style::default().fg(p.overlay0).add_modifier(Modifier::BOLD),
))
.alignment(Alignment::Right),
@@ -1271,7 +1239,6 @@ mod tests {
.detected_agent = Some(Agent::Claude);
app.active = Some(0);
app.selected = 0;
- app.agent_panel_scope = AgentPanelScope::AllWorkspaces;
let entries = agent_panel_entries(&app);
assert_eq!(entries[0].primary_label, "one");
@@ -1282,6 +1249,49 @@ mod tests {
assert_eq!(entries[1].agent_label.as_deref(), Some("claude"));
}
+ #[test]
+ fn priority_agent_panel_sort_uses_attention_then_space_order() {
+ let mut app = crate::app::state::AppState::test_new();
+ app.workspaces = vec![
+ Workspace::test_new("one"),
+ Workspace::test_new("two"),
+ Workspace::test_new("three"),
+ Workspace::test_new("four"),
+ ];
+ app.ensure_test_terminals();
+ app.active = Some(0);
+ app.selected = 0;
+ app.agent_panel_sort = crate::app::state::AgentPanelSort::Priority;
+
+ let set_state = |app: &mut crate::app::state::AppState, ws_idx: usize, state| {
+ let pane = app.workspaces[ws_idx].tabs[0].root_pane;
+ let terminal_id = app.workspaces[ws_idx].tabs[0].panes[&pane]
+ .attached_terminal_id
+ .clone();
+ let terminal = app.terminals.get_mut(&terminal_id).unwrap();
+ terminal.detected_agent = Some(Agent::Claude);
+ terminal.state = state;
+ };
+ set_state(&mut app, 0, AgentState::Working);
+ set_state(&mut app, 1, AgentState::Idle);
+ set_state(&mut app, 2, AgentState::Working);
+ set_state(&mut app, 3, AgentState::Blocked);
+
+ let done_pane = app.workspaces[1].tabs[0].root_pane;
+ app.workspaces[1].tabs[0]
+ .panes
+ .get_mut(&done_pane)
+ .unwrap()
+ .seen = false;
+
+ let labels: Vec<String> = agent_panel_entries(&app)
+ .into_iter()
+ .map(|entry| entry.primary_label)
+ .collect();
+
+ assert_eq!(labels, ["four", "two", "one", "three"]);
+ }
+
#[cfg(unix)]
#[tokio::test]
async fn all_workspaces_agent_panel_entries_use_live_root_runtime_cwd_for_workspace_label() {
@@ -1315,7 +1325,6 @@ mod tests {
terminal.detected_agent = Some(Agent::Pi);
app.active = Some(0);
app.selected = 0;
- app.agent_panel_scope = AgentPanelScope::AllWorkspaces;
let (events, _) = tokio::sync::mpsc::channel(4);
let runtime = crate::terminal::TerminalRuntime::spawn(
@@ -1372,7 +1381,6 @@ mod tests {
.set_agent_name("planner".into());
app.active = Some(0);
app.selected = 0;
- app.agent_panel_scope = AgentPanelScope::AllWorkspaces;
let entries = agent_panel_entries(&app);
assert_eq!(entries[0].primary_label, "bridge");
@@ -1390,6 +1398,7 @@ mod tests {
agent_label: Some("claude".into()),
state: AgentState::Idle,
seen: true,
+ last_agent_state_change_seq: None,
custom_status: None,
state_labels: std::collections::HashMap::new(),
};