parent
d998753efe
commit
54490254f7
|
|
@ -287,7 +287,7 @@ mouse_scroll_lines = 3
|
|||
confirm_close = true
|
||||
prompt_new_tab_name = true
|
||||
show_agent_labels_on_pane_borders = false
|
||||
agent_panel_scope = "all"
|
||||
agent_panel_sort = "spaces"
|
||||
accent = "cyan"
|
||||
```
|
||||
|
||||
|
|
@ -295,7 +295,7 @@ accent = "cyan"
|
|||
|
||||
`mobile_width_threshold` controls the terminal width at or below which Herdr uses the mobile single-column layout. The default is 64 columns; increase it for foldables, tablets, or wide phone terminals.
|
||||
|
||||
`agent_panel_scope` can be `all` or `current`. Use `current` if you only want the agent panel to show agents in the active workspace.
|
||||
The agent panel shows all agents across all spaces. `agent_panel_sort` can be `spaces` or `priority`; `workspaces` is accepted as an alias for `spaces`. `spaces` keeps agents grouped by space order. `priority` sorts by attention priority: blocked, done, working, idle, then unknown. Within the same status, agents that most recently changed state appear first.
|
||||
|
||||
`confirm_close` controls whether closing a workspace asks for confirmation. `prompt_new_tab_name` controls whether new tabs ask for a label first.
|
||||
|
||||
|
|
|
|||
|
|
@ -951,12 +951,6 @@ impl AppState {
|
|||
let workspace_id = self.workspaces[idx].id.clone();
|
||||
crate::logging::workspace_focused(&workspace_id);
|
||||
self.mark_session_dirty();
|
||||
if matches!(
|
||||
self.agent_panel_scope,
|
||||
crate::app::state::AgentPanelScope::CurrentWorkspace
|
||||
) {
|
||||
self.agent_panel_scroll = 0;
|
||||
}
|
||||
self.ensure_workspace_visible(idx);
|
||||
if let Some(ws) = self.workspaces.get_mut(idx) {
|
||||
let active_tab = ws.active_tab;
|
||||
|
|
@ -994,14 +988,6 @@ impl AppState {
|
|||
crate::logging::workspace_focused(&workspace_id);
|
||||
}
|
||||
self.mark_session_dirty();
|
||||
if workspace_changed
|
||||
&& matches!(
|
||||
self.agent_panel_scope,
|
||||
crate::app::state::AgentPanelScope::CurrentWorkspace
|
||||
)
|
||||
{
|
||||
self.agent_panel_scroll = 0;
|
||||
}
|
||||
self.ensure_workspace_visible(ws_idx);
|
||||
if let Some(ws) = self.workspaces.get_mut(ws_idx) {
|
||||
ws.switch_tab(tab_idx);
|
||||
|
|
@ -2587,6 +2573,12 @@ impl AppState {
|
|||
self.mark_session_dirty();
|
||||
}
|
||||
let change = mutation.effective_state_change?;
|
||||
if change.previous_state != change.state {
|
||||
self.next_agent_state_change_seq += 1;
|
||||
if let Some(terminal) = self.terminals.get_mut(&terminal_id) {
|
||||
terminal.last_agent_state_change_seq = Some(self.next_agent_state_change_seq);
|
||||
}
|
||||
}
|
||||
let seen = self.apply_pane_state_change(ws_idx, pane_id, &change)?;
|
||||
let update = PaneStateUpdate {
|
||||
pane_id,
|
||||
|
|
@ -3542,6 +3534,16 @@ mod tests {
|
|||
}
|
||||
|
||||
fn mark_agent(state: &mut AppState, ws_idx: usize, tab_idx: usize, pane_id: PaneId) {
|
||||
set_agent_state(state, ws_idx, tab_idx, pane_id, AgentState::Idle);
|
||||
}
|
||||
|
||||
fn set_agent_state(
|
||||
state: &mut AppState,
|
||||
ws_idx: usize,
|
||||
tab_idx: usize,
|
||||
pane_id: PaneId,
|
||||
agent_state: AgentState,
|
||||
) {
|
||||
state.ensure_test_terminals();
|
||||
let terminal_id = state.workspaces[ws_idx].tabs[tab_idx]
|
||||
.panes
|
||||
|
|
@ -3550,12 +3552,28 @@ mod tests {
|
|||
.attached_terminal_id
|
||||
.clone();
|
||||
if let Some(terminal) = state.terminals.get_mut(&terminal_id) {
|
||||
terminal.set_detected_state(Some(Agent::Pi), AgentState::Idle);
|
||||
terminal.set_detected_state(Some(Agent::Pi), agent_state);
|
||||
}
|
||||
}
|
||||
|
||||
fn transition_agent_state(state: &mut AppState, pane_id: PaneId, agent_state: AgentState) {
|
||||
state
|
||||
.update_terminal_state(pane_id, |terminal| {
|
||||
Some(terminal.set_detected_state_with_screen_signals_at(
|
||||
Some(Agent::Pi),
|
||||
agent_state,
|
||||
matches!(agent_state, AgentState::Blocked),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
std::time::Instant::now(),
|
||||
))
|
||||
})
|
||||
.expect("agent state transition should update pane state");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_agent_cycles_agent_panel_entries_in_all_scope() {
|
||||
fn next_agent_cycles_agent_panel_entries() {
|
||||
let mut first = Workspace::test_new("one");
|
||||
let first_root = first.tabs[0].root_pane;
|
||||
let first_second = first.test_split(Direction::Horizontal);
|
||||
|
|
@ -3569,7 +3587,6 @@ mod tests {
|
|||
state.active = Some(0);
|
||||
state.selected = 0;
|
||||
state.mode = Mode::Terminal;
|
||||
state.agent_panel_scope = crate::app::state::AgentPanelScope::AllWorkspaces;
|
||||
mark_agent(&mut state, 0, 0, first_root);
|
||||
mark_agent(&mut state, 0, 0, first_second);
|
||||
mark_agent(&mut state, 1, 0, second_root);
|
||||
|
|
@ -3602,7 +3619,6 @@ mod tests {
|
|||
state.active = Some(0);
|
||||
state.selected = 0;
|
||||
state.mode = Mode::Terminal;
|
||||
state.agent_panel_scope = crate::app::state::AgentPanelScope::AllWorkspaces;
|
||||
mark_agent(&mut state, 0, 0, first_root);
|
||||
mark_agent(&mut state, 0, 0, first_second);
|
||||
mark_agent(&mut state, 1, 0, second_root);
|
||||
|
|
@ -3618,7 +3634,6 @@ mod tests {
|
|||
fn focus_agent_entry_succeeds_for_already_focused_agent() {
|
||||
let mut state = app_with_workspaces(&["one"]);
|
||||
let root = state.workspaces[0].tabs[0].root_pane;
|
||||
state.agent_panel_scope = crate::app::state::AgentPanelScope::AllWorkspaces;
|
||||
mark_agent(&mut state, 0, 0, root);
|
||||
|
||||
assert!(state.focus_agent_entry(0));
|
||||
|
|
@ -3628,11 +3643,11 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn next_agent_cycles_only_current_scope_entries() {
|
||||
fn next_agent_cycles_priority_sorted_agent_panel_entries() {
|
||||
let mut first = Workspace::test_new("one");
|
||||
let first_root = first.tabs[0].root_pane;
|
||||
let first_second = first.test_split(Direction::Horizontal);
|
||||
first.tabs[0].layout.focus_pane(first_second);
|
||||
first.tabs[0].layout.focus_pane(first_root);
|
||||
let second = Workspace::test_new("two");
|
||||
let second_root = second.tabs[0].root_pane;
|
||||
|
||||
|
|
@ -3642,15 +3657,40 @@ mod tests {
|
|||
state.active = Some(0);
|
||||
state.selected = 0;
|
||||
state.mode = Mode::Terminal;
|
||||
state.agent_panel_scope = crate::app::state::AgentPanelScope::CurrentWorkspace;
|
||||
mark_agent(&mut state, 0, 0, first_root);
|
||||
mark_agent(&mut state, 0, 0, first_second);
|
||||
mark_agent(&mut state, 1, 0, second_root);
|
||||
state.agent_panel_sort = crate::app::state::AgentPanelSort::Priority;
|
||||
set_agent_state(&mut state, 0, 0, first_root, AgentState::Idle);
|
||||
set_agent_state(&mut state, 0, 0, first_second, AgentState::Working);
|
||||
set_agent_state(&mut state, 1, 0, second_root, AgentState::Blocked);
|
||||
|
||||
state.next_agent();
|
||||
|
||||
assert_eq!(state.active, Some(0));
|
||||
assert_eq!(state.workspaces[0].focused_pane_id(), Some(first_root));
|
||||
assert_eq!(state.active, Some(1));
|
||||
assert_eq!(state.workspaces[1].focused_pane_id(), Some(second_root));
|
||||
state.assert_invariants_for_test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn priority_sort_keeps_recently_changed_idle_agent_above_older_idle_agent() {
|
||||
let mut workspace = Workspace::test_new("one");
|
||||
let first = workspace.tabs[0].root_pane;
|
||||
let second = workspace.test_split(Direction::Horizontal);
|
||||
workspace.tabs[0].layout.focus_pane(first);
|
||||
|
||||
let mut state = AppState::test_new();
|
||||
state.workspaces = vec![workspace];
|
||||
state.ensure_test_terminals();
|
||||
state.active = Some(0);
|
||||
state.selected = 0;
|
||||
state.mode = Mode::Terminal;
|
||||
state.agent_panel_sort = crate::app::state::AgentPanelSort::Priority;
|
||||
|
||||
transition_agent_state(&mut state, first, AgentState::Idle);
|
||||
transition_agent_state(&mut state, second, AgentState::Working);
|
||||
assert_eq!(crate::ui::agent_panel_entries(&state)[0].pane_id, second);
|
||||
|
||||
transition_agent_state(&mut state, second, AgentState::Idle);
|
||||
|
||||
assert_eq!(crate::ui::agent_panel_entries(&state)[0].pane_id, second);
|
||||
state.assert_invariants_for_test();
|
||||
}
|
||||
|
||||
|
|
@ -3668,7 +3708,6 @@ mod tests {
|
|||
state.active = Some(0);
|
||||
state.selected = 0;
|
||||
state.mode = Mode::Terminal;
|
||||
state.agent_panel_scope = crate::app::state::AgentPanelScope::CurrentWorkspace;
|
||||
for tab_idx in 0..state.workspaces[0].tabs.len() {
|
||||
let pane_id = state.workspaces[0].tabs[tab_idx].root_pane;
|
||||
mark_agent(&mut state, 0, tab_idx, pane_id);
|
||||
|
|
|
|||
|
|
@ -107,20 +107,20 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn save_agent_panel_scope(&mut self, scope: crate::app::state::AgentPanelScope) {
|
||||
let value = match scope {
|
||||
crate::app::state::AgentPanelScope::CurrentWorkspace => {
|
||||
crate::config::AgentPanelScopeConfig::Current.as_str()
|
||||
pub(super) fn save_agent_panel_sort(&mut self, sort: crate::app::state::AgentPanelSort) {
|
||||
let value = match sort {
|
||||
crate::app::state::AgentPanelSort::Spaces => {
|
||||
crate::config::AgentPanelSortConfig::Spaces.as_str()
|
||||
}
|
||||
crate::app::state::AgentPanelScope::AllWorkspaces => {
|
||||
crate::config::AgentPanelScopeConfig::All.as_str()
|
||||
crate::app::state::AgentPanelSort::Priority => {
|
||||
crate::config::AgentPanelSortConfig::Priority.as_str()
|
||||
}
|
||||
};
|
||||
if self.update_config_file("agent panel scope", |content| {
|
||||
if self.update_config_file("agent panel sort", |content| {
|
||||
crate::config::upsert_section_value(
|
||||
content,
|
||||
"ui",
|
||||
"agent_panel_scope",
|
||||
"agent_panel_sort",
|
||||
&format!("\"{value}\""),
|
||||
)
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ impl App {
|
|||
|
||||
let handled_pane_double_click = self.handle_pane_double_click(mouse);
|
||||
|
||||
let previous_agent_panel_scope = self.state.agent_panel_scope;
|
||||
let previous_agent_panel_sort = self.state.agent_panel_sort;
|
||||
let previous_settings_section = self.state.settings.section;
|
||||
if !handled_pane_double_click {
|
||||
if let Some(action) = self.state.handle_mouse(&mut self.terminal_runtimes, mouse) {
|
||||
|
|
@ -286,8 +286,8 @@ impl App {
|
|||
{
|
||||
self.refresh_integration_recommendations();
|
||||
}
|
||||
if self.state.agent_panel_scope != previous_agent_panel_scope {
|
||||
self.save_agent_panel_scope(self.state.agent_panel_scope);
|
||||
if self.state.agent_panel_sort != previous_agent_panel_sort {
|
||||
self.save_agent_panel_sort(self.state.agent_panel_sort);
|
||||
}
|
||||
|
||||
if let Some(content) = self.state.request_clipboard_write.take() {
|
||||
|
|
@ -590,7 +590,6 @@ fn capture_snapshot(state: &AppState) -> crate::persist::SessionSnapshot {
|
|||
&terminal_runtimes,
|
||||
state.active,
|
||||
state.selected,
|
||||
state.agent_panel_scope,
|
||||
state.sidebar_width,
|
||||
state.sidebar_section_split,
|
||||
state.collapsed_space_keys.clone(),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use tracing::warn;
|
|||
|
||||
use crate::{
|
||||
app::state::{
|
||||
AgentPanelScope, AppState, ContextMenuKind, ContextMenuState, DragState, DragTarget,
|
||||
AgentPanelSort, AppState, ContextMenuKind, ContextMenuState, DragState, DragTarget,
|
||||
MenuListState, Mode, RightClickPassthroughGesture, TabPressState, ViewLayout,
|
||||
WorkspacePressState,
|
||||
},
|
||||
|
|
@ -540,10 +540,10 @@ impl AppState {
|
|||
return None;
|
||||
}
|
||||
|
||||
if self.on_agent_panel_scope_toggle(mouse.column, mouse.row) {
|
||||
self.agent_panel_scope = match self.agent_panel_scope {
|
||||
AgentPanelScope::CurrentWorkspace => AgentPanelScope::AllWorkspaces,
|
||||
AgentPanelScope::AllWorkspaces => AgentPanelScope::CurrentWorkspace,
|
||||
if self.on_agent_panel_sort_toggle(mouse.column, mouse.row) {
|
||||
self.agent_panel_sort = match self.agent_panel_sort {
|
||||
AgentPanelSort::Spaces => AgentPanelSort::Priority,
|
||||
AgentPanelSort::Priority => AgentPanelSort::Spaces,
|
||||
};
|
||||
self.agent_panel_scroll = 0;
|
||||
self.mark_session_dirty();
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ impl AppState {
|
|||
best.map(|(insert_idx, _)| insert_idx)
|
||||
}
|
||||
|
||||
pub(super) fn on_agent_panel_scope_toggle(&self, col: u16, row: u16) -> bool {
|
||||
pub(super) fn on_agent_panel_sort_toggle(&self, col: u16, row: u16) -> bool {
|
||||
if self.sidebar_collapsed {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -441,7 +441,7 @@ impl AppState {
|
|||
self.view.sidebar_rect,
|
||||
self.sidebar_section_split,
|
||||
);
|
||||
let rect = crate::ui::agent_panel_toggle_rect(detail_area, self.agent_panel_scope);
|
||||
let rect = crate::ui::agent_panel_toggle_rect(detail_area, self.agent_panel_sort);
|
||||
rect.width > 0
|
||||
&& col >= rect.x
|
||||
&& col < rect.x + rect.width
|
||||
|
|
@ -496,7 +496,7 @@ mod tests {
|
|||
|
||||
use super::super::{app_for_mouse_test, capture_snapshot, mouse, unique_temp_path};
|
||||
use crate::{
|
||||
app::state::{AgentPanelScope, DragTarget, Mode},
|
||||
app::state::{AgentPanelSort, DragTarget, Mode},
|
||||
detect::Agent,
|
||||
workspace::Workspace,
|
||||
};
|
||||
|
|
@ -711,7 +711,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn clicking_agent_panel_toggle_switches_scope() {
|
||||
fn clicking_agent_panel_toggle_switches_sort() {
|
||||
let mut app = app_for_mouse_test();
|
||||
app.state.workspaces = vec![Workspace::test_new("test")];
|
||||
app.state.active = Some(0);
|
||||
|
|
@ -723,23 +723,15 @@ mod tests {
|
|||
app.state.view.sidebar_rect,
|
||||
app.state.sidebar_section_split,
|
||||
);
|
||||
let toggle = crate::ui::agent_panel_toggle_rect(detail_area, app.state.agent_panel_scope);
|
||||
let toggle = crate::ui::agent_panel_toggle_rect(detail_area, app.state.agent_panel_sort);
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
toggle.x,
|
||||
toggle.y,
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
app.state.agent_panel_scope,
|
||||
AgentPanelScope::CurrentWorkspace
|
||||
);
|
||||
assert_eq!(app.state.agent_panel_sort, AgentPanelSort::Priority);
|
||||
assert_eq!(app.state.agent_panel_scroll, 0);
|
||||
let snapshot = capture_snapshot(&app.state);
|
||||
assert_eq!(
|
||||
snapshot.agent_panel_scope,
|
||||
AgentPanelScope::CurrentWorkspace
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -772,7 +764,6 @@ mod tests {
|
|||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
app.state.agent_panel_scope = AgentPanelScope::AllWorkspaces;
|
||||
|
||||
let (_, detail_area) = crate::ui::expanded_sidebar_sections(
|
||||
app.state.view.sidebar_rect,
|
||||
|
|
|
|||
|
|
@ -204,12 +204,12 @@ fn load_plugin_registry(no_session: bool) -> crate::app::state::InstalledPluginR
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn agent_panel_scope_from_config(
|
||||
scope: crate::config::AgentPanelScopeConfig,
|
||||
) -> state::AgentPanelScope {
|
||||
match scope {
|
||||
crate::config::AgentPanelScopeConfig::Current => state::AgentPanelScope::CurrentWorkspace,
|
||||
crate::config::AgentPanelScopeConfig::All => state::AgentPanelScope::AllWorkspaces,
|
||||
fn agent_panel_sort_from_config(
|
||||
sort: crate::config::AgentPanelSortConfig,
|
||||
) -> state::AgentPanelSort {
|
||||
match sort {
|
||||
crate::config::AgentPanelSortConfig::Spaces => state::AgentPanelSort::Spaces,
|
||||
crate::config::AgentPanelSortConfig::Priority => state::AgentPanelSort::Priority,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +289,6 @@ impl App {
|
|||
workspaces,
|
||||
active,
|
||||
selected,
|
||||
_restored_agent_panel_scope,
|
||||
sidebar_width,
|
||||
sidebar_width_source,
|
||||
sidebar_section_split,
|
||||
|
|
@ -299,7 +298,6 @@ impl App {
|
|||
Vec::new(),
|
||||
None,
|
||||
0,
|
||||
state::AgentPanelScope::CurrentWorkspace,
|
||||
config.ui.sidebar_width,
|
||||
state::SidebarWidthSource::ConfigDefault,
|
||||
0.5_f32,
|
||||
|
|
@ -332,7 +330,6 @@ impl App {
|
|||
Vec::new(),
|
||||
None,
|
||||
0,
|
||||
snap.agent_panel_scope,
|
||||
snap.sidebar_width.unwrap_or(config.ui.sidebar_width),
|
||||
if snap.sidebar_width.is_some() {
|
||||
state::SidebarWidthSource::Persisted
|
||||
|
|
@ -350,7 +347,6 @@ impl App {
|
|||
ws,
|
||||
active,
|
||||
selected,
|
||||
snap.agent_panel_scope,
|
||||
snap.sidebar_width.unwrap_or(config.ui.sidebar_width),
|
||||
if snap.sidebar_width.is_some() {
|
||||
state::SidebarWidthSource::Persisted
|
||||
|
|
@ -366,7 +362,6 @@ impl App {
|
|||
Vec::new(),
|
||||
None,
|
||||
0,
|
||||
state::AgentPanelScope::CurrentWorkspace,
|
||||
config.ui.sidebar_width,
|
||||
state::SidebarWidthSource::ConfigDefault,
|
||||
0.5_f32,
|
||||
|
|
@ -374,7 +369,7 @@ impl App {
|
|||
)
|
||||
};
|
||||
|
||||
let agent_panel_scope = agent_panel_scope_from_config(config.ui.agent_panel_scope);
|
||||
let agent_panel_sort = agent_panel_sort_from_config(config.ui.agent_panel_sort);
|
||||
|
||||
// Validate sidebar bounds before they reach any `u16::clamp(min, max)`
|
||||
// call: `clamp` panics when `min > max`. On bad config, fall back to
|
||||
|
|
@ -519,7 +514,8 @@ impl App {
|
|||
sidebar_width_auto: false,
|
||||
sidebar_collapsed: false,
|
||||
sidebar_section_split,
|
||||
agent_panel_scope,
|
||||
agent_panel_sort,
|
||||
next_agent_state_change_seq: 0,
|
||||
mouse_capture: config.ui.mouse_capture,
|
||||
right_click_passthrough_modifiers: config.ui.right_click_passthrough_modifiers(),
|
||||
right_click_passthrough: None,
|
||||
|
|
@ -693,7 +689,6 @@ impl App {
|
|||
app.state.selected = snapshot
|
||||
.selected
|
||||
.min(app.state.workspaces.len().saturating_sub(1));
|
||||
app.state.agent_panel_scope = snapshot.agent_panel_scope;
|
||||
if let Some(width) = snapshot.sidebar_width {
|
||||
app.state.sidebar_width = width;
|
||||
app.state.sidebar_width_source = state::SidebarWidthSource::Persisted;
|
||||
|
|
@ -1246,8 +1241,8 @@ impl App {
|
|||
self.state.prompt_new_tab_name = config.ui.prompt_new_tab_name;
|
||||
self.state.show_agent_labels_on_pane_borders =
|
||||
config.ui.show_agent_labels_on_pane_borders;
|
||||
self.state.agent_panel_scope =
|
||||
agent_panel_scope_from_config(config.ui.agent_panel_scope);
|
||||
self.state.agent_panel_sort =
|
||||
agent_panel_sort_from_config(config.ui.agent_panel_sort);
|
||||
self.state.agent_panel_scroll = 0;
|
||||
self.state.accent = crate::config::parse_color(&config.ui.accent);
|
||||
if !self.state.local_sound_playback && self.state.sound != config.ui.sound {
|
||||
|
|
@ -2065,17 +2060,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn startup_uses_configured_agent_panel_scope() {
|
||||
fn startup_uses_configured_agent_panel_sort() {
|
||||
let mut config = Config::default();
|
||||
config.ui.agent_panel_scope = crate::config::AgentPanelScopeConfig::Current;
|
||||
config.ui.agent_panel_sort = crate::config::AgentPanelSortConfig::Priority;
|
||||
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let app = App::new(&config, true, None, api_rx, crate::api::EventHub::default());
|
||||
|
||||
assert_eq!(
|
||||
app.state.agent_panel_scope,
|
||||
state::AgentPanelScope::CurrentWorkspace
|
||||
);
|
||||
assert_eq!(app.state.agent_panel_sort, state::AgentPanelSort::Priority);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2199,7 +2191,7 @@ mod tests {
|
|||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
&path,
|
||||
"[terminal]\ndefault_shell = \"nu\"\nshell_mode = \"non_login\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+m\"\nprefix = \"ctrl+a\"\n[ui]\nagent_panel_scope = \"current\"\nredraw_on_focus_gained = false\nright_click_passthrough_modifier = \"ctrl\"\n[ui.toast]\ndelivery = \"herdr\"\n[experimental]\nswitch_ascii_input_source_in_prefix = true\n",
|
||||
"[terminal]\ndefault_shell = \"nu\"\nshell_mode = \"non_login\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+m\"\nprefix = \"ctrl+a\"\n[ui]\nagent_panel_scope = \"current\"\nagent_panel_sort = \"priority\"\nredraw_on_focus_gained = false\nright_click_passthrough_modifier = \"ctrl\"\n[ui.toast]\ndelivery = \"herdr\"\n[experimental]\nswitch_ascii_input_source_in_prefix = true\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path);
|
||||
|
|
@ -2219,10 +2211,7 @@ mod tests {
|
|||
app.state.toast_config.delivery,
|
||||
crate::config::ToastDelivery::Herdr
|
||||
);
|
||||
assert_eq!(
|
||||
app.state.agent_panel_scope,
|
||||
state::AgentPanelScope::CurrentWorkspace
|
||||
);
|
||||
assert_eq!(app.state.agent_panel_sort, state::AgentPanelSort::Priority);
|
||||
assert!(!app.state.redraw_on_focus_gained);
|
||||
assert_eq!(
|
||||
app.state.right_click_passthrough_modifiers,
|
||||
|
|
@ -2561,27 +2550,21 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn save_agent_panel_scope_persists_then_applies_live_config() {
|
||||
fn save_agent_panel_sort_persists_then_applies_live_config() {
|
||||
let _guard = config_env_lock().lock().unwrap();
|
||||
let path = temp_config_path("save-agent-panel-scope");
|
||||
let path = temp_config_path("save-agent-panel-sort");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, "onboarding = false\n").unwrap();
|
||||
std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path);
|
||||
|
||||
let mut app = test_app();
|
||||
assert_eq!(
|
||||
app.state.agent_panel_scope,
|
||||
state::AgentPanelScope::AllWorkspaces
|
||||
);
|
||||
assert_eq!(app.state.agent_panel_sort, state::AgentPanelSort::Spaces);
|
||||
|
||||
app.save_agent_panel_scope(state::AgentPanelScope::CurrentWorkspace);
|
||||
app.save_agent_panel_sort(state::AgentPanelSort::Priority);
|
||||
|
||||
assert_eq!(
|
||||
app.state.agent_panel_scope,
|
||||
state::AgentPanelScope::CurrentWorkspace
|
||||
);
|
||||
assert_eq!(app.state.agent_panel_sort, state::AgentPanelSort::Priority);
|
||||
let content = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(content.contains("agent_panel_scope = \"current\""));
|
||||
assert!(content.contains("agent_panel_sort = \"priority\""));
|
||||
assert!(app.state.config_diagnostic.is_none());
|
||||
|
||||
std::env::remove_var(crate::config::CONFIG_PATH_ENV_VAR);
|
||||
|
|
|
|||
|
|
@ -336,18 +336,10 @@ impl App {
|
|||
}
|
||||
|
||||
fn agent_panel_has_animation(&self) -> bool {
|
||||
match self.state.agent_panel_scope {
|
||||
crate::app::state::AgentPanelScope::CurrentWorkspace => self
|
||||
.state
|
||||
.active
|
||||
.and_then(|idx| self.state.workspaces.get(idx))
|
||||
.is_some_and(|ws| ws.has_working_pane(&self.state.terminals)),
|
||||
crate::app::state::AgentPanelScope::AllWorkspaces => self
|
||||
.state
|
||||
.workspaces
|
||||
.iter()
|
||||
.any(|ws| ws.has_working_pane(&self.state.terminals)),
|
||||
}
|
||||
self.state
|
||||
.workspaces
|
||||
.iter()
|
||||
.any(|ws| ws.has_working_pane(&self.state.terminals))
|
||||
}
|
||||
|
||||
pub(crate) fn tick_selection_autoscroll(&mut self, now: Instant) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ impl App {
|
|||
&self.terminal_runtimes,
|
||||
self.state.active,
|
||||
self.state.selected,
|
||||
self.state.agent_panel_scope,
|
||||
self.state.sidebar_width,
|
||||
self.state.sidebar_section_split,
|
||||
self.state.collapsed_space_keys.clone(),
|
||||
|
|
|
|||
|
|
@ -832,11 +832,11 @@ pub(crate) enum CopyModeSelection {
|
|||
Linewise { anchor_row: u32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AgentPanelScope {
|
||||
CurrentWorkspace,
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum AgentPanelSort {
|
||||
#[default]
|
||||
AllWorkspaces,
|
||||
Spaces,
|
||||
Priority,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -1340,7 +1340,8 @@ pub struct AppState {
|
|||
pub sidebar_collapsed: bool,
|
||||
/// Ratio of sidebar height allocated to the workspaces section.
|
||||
pub sidebar_section_split: f32,
|
||||
pub agent_panel_scope: AgentPanelScope,
|
||||
pub agent_panel_sort: AgentPanelSort,
|
||||
pub next_agent_state_change_seq: u64,
|
||||
/// Capture mouse input for Herdr's own mouse UI. When false, Herdr only
|
||||
/// captures mouse while the focused pane app requests mouse reporting.
|
||||
pub mouse_capture: bool,
|
||||
|
|
@ -1689,7 +1690,8 @@ impl AppState {
|
|||
sidebar_width_auto: false,
|
||||
sidebar_collapsed: false,
|
||||
sidebar_section_split: 0.5,
|
||||
agent_panel_scope: AgentPanelScope::AllWorkspaces,
|
||||
agent_panel_sort: AgentPanelSort::Spaces,
|
||||
next_agent_state_change_seq: 0,
|
||||
mouse_capture: true,
|
||||
right_click_passthrough_modifiers: None,
|
||||
right_click_passthrough: None,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub use self::{
|
|||
IndexedKeybind, Keybinds, LiveKeybindConfig,
|
||||
},
|
||||
model::{
|
||||
validated_sidebar_bounds, AgentPanelScopeConfig, Config, ConfigReloadReport,
|
||||
validated_sidebar_bounds, AgentPanelSortConfig, Config, ConfigReloadReport,
|
||||
ConfigReloadStatus, KeysConfig, NewTerminalCwdConfig, ShellModeConfig,
|
||||
ToastClipboardPosition, ToastConfig, ToastDelivery, ToastHerdrPosition,
|
||||
UpdateChannelConfig, MAX_TOAST_DELAY_SECONDS,
|
||||
|
|
|
|||
|
|
@ -84,17 +84,18 @@ pub enum ToastClipboardPosition {
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AgentPanelScopeConfig {
|
||||
Current,
|
||||
pub enum AgentPanelSortConfig {
|
||||
#[default]
|
||||
All,
|
||||
#[serde(alias = "workspaces")]
|
||||
Spaces,
|
||||
Priority,
|
||||
}
|
||||
|
||||
impl AgentPanelScopeConfig {
|
||||
impl AgentPanelSortConfig {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Current => "current",
|
||||
Self::All => "all",
|
||||
Self::Spaces => "spaces",
|
||||
Self::Priority => "priority",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -440,8 +441,8 @@ pub struct UiConfig {
|
|||
pub prompt_new_tab_name: bool,
|
||||
/// Show agent labels in split pane borders when no manual pane label is set. Default: false.
|
||||
pub show_agent_labels_on_pane_borders: bool,
|
||||
/// Agent sidebar scope. Saved values are "current" or "all". Default: "all".
|
||||
pub agent_panel_scope: AgentPanelScopeConfig,
|
||||
/// Agent sidebar ordering. Saved values are "spaces" or "priority". Default: "spaces".
|
||||
pub agent_panel_sort: AgentPanelSortConfig,
|
||||
/// Accent color for highlights, borders, and navigation UI.
|
||||
/// Accepts hex (#89b4fa), named colors (cyan, blue), or RGB (rgb(137,180,250)).
|
||||
pub accent: String,
|
||||
|
|
@ -626,7 +627,7 @@ impl Default for UiConfig {
|
|||
confirm_close: true,
|
||||
prompt_new_tab_name: true,
|
||||
show_agent_labels_on_pane_borders: false,
|
||||
agent_panel_scope: AgentPanelScopeConfig::All,
|
||||
agent_panel_sort: AgentPanelSortConfig::Spaces,
|
||||
accent: "cyan".into(),
|
||||
toast: ToastConfig::default(),
|
||||
sound: SoundConfig::default(),
|
||||
|
|
@ -797,13 +798,32 @@ resume_agents_on_restore = false
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn agent_panel_scope_config_parses() {
|
||||
fn agent_panel_sort_config_parses_alias_and_defaults() {
|
||||
assert_eq!(
|
||||
Config::default().ui.agent_panel_sort,
|
||||
AgentPanelSortConfig::Spaces
|
||||
);
|
||||
|
||||
let toml = r#"
|
||||
[ui]
|
||||
agent_panel_scope = "all"
|
||||
agent_panel_sort = "priority"
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.agent_panel_scope, AgentPanelScopeConfig::All);
|
||||
assert_eq!(config.ui.agent_panel_sort, AgentPanelSortConfig::Priority);
|
||||
|
||||
let toml = r#"
|
||||
[ui]
|
||||
agent_panel_sort = "workspaces"
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.agent_panel_sort, AgentPanelSortConfig::Spaces);
|
||||
|
||||
let toml = r#"
|
||||
[ui]
|
||||
agent_panel_scope = "current"
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.agent_panel_sort, AgentPanelSortConfig::Spaces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -250,8 +250,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
|
|||
# Show detected/reported agent labels in split pane borders when no manual pane name is set.
|
||||
# show_agent_labels_on_pane_borders = false
|
||||
|
||||
# Agent panel scope: "current" or "all". Toggling it in the sidebar saves this setting.
|
||||
# agent_panel_scope = "all"
|
||||
# Agent panel ordering: "spaces" (grouped by space) or "priority" (attention queue).
|
||||
# "workspaces" is accepted as an alias for "spaces".
|
||||
# agent_panel_sort = "spaces"
|
||||
|
||||
# Accent color for highlights, borders, and navigation UI.
|
||||
# Accepts: hex (#89b4fa), named colors (cyan, blue, magenta), or rgb(r,g,b)
|
||||
|
|
|
|||
|
|
@ -175,7 +175,6 @@ pub fn load_history() -> Option<SessionHistorySnapshot> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::state::AgentPanelScope;
|
||||
use crate::persist::snapshot::{
|
||||
PaneHistorySnapshot, TabHistorySnapshot, WorkspaceHistorySnapshot,
|
||||
};
|
||||
|
|
@ -205,7 +204,6 @@ mod tests {
|
|||
workspaces: vec![],
|
||||
active: None,
|
||||
selected: 0,
|
||||
agent_panel_scope: AgentPanelScope::CurrentWorkspace,
|
||||
sidebar_width: Some(26),
|
||||
sidebar_section_split: Some(0.5),
|
||||
collapsed_space_keys: std::collections::HashSet::new(),
|
||||
|
|
|
|||
|
|
@ -1188,7 +1188,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: Default::default(),
|
||||
sidebar_width: None,
|
||||
sidebar_section_split: None,
|
||||
collapsed_space_keys: Default::default(),
|
||||
|
|
@ -1278,7 +1277,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: Default::default(),
|
||||
sidebar_width: None,
|
||||
sidebar_section_split: None,
|
||||
collapsed_space_keys: Default::default(),
|
||||
|
|
@ -1384,7 +1382,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: Default::default(),
|
||||
sidebar_width: None,
|
||||
sidebar_section_split: None,
|
||||
collapsed_space_keys: Default::default(),
|
||||
|
|
@ -1496,7 +1493,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: Default::default(),
|
||||
sidebar_width: None,
|
||||
sidebar_section_split: None,
|
||||
collapsed_space_keys: Default::default(),
|
||||
|
|
@ -1687,7 +1683,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: crate::app::state::AgentPanelScope::CurrentWorkspace,
|
||||
sidebar_width: Some(26),
|
||||
sidebar_section_split: Some(0.5),
|
||||
collapsed_space_keys: Default::default(),
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ pub struct SessionSnapshot {
|
|||
pub active: Option<usize>,
|
||||
pub selected: usize,
|
||||
#[serde(default)]
|
||||
pub agent_panel_scope: crate::app::state::AgentPanelScope,
|
||||
#[serde(default)]
|
||||
pub sidebar_width: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub sidebar_section_split: Option<f32>,
|
||||
|
|
@ -179,8 +177,6 @@ struct RawSessionSnapshot {
|
|||
#[serde(default)]
|
||||
selected: usize,
|
||||
#[serde(default)]
|
||||
agent_panel_scope: crate::app::state::AgentPanelScope,
|
||||
#[serde(default)]
|
||||
sidebar_width: Option<u16>,
|
||||
#[serde(default)]
|
||||
sidebar_section_split: Option<f32>,
|
||||
|
|
@ -198,7 +194,6 @@ fn migrate_snapshot(raw: RawSessionSnapshot) -> Result<SessionSnapshot, String>
|
|||
.collect::<Result<Vec<_>, _>>()?,
|
||||
active: raw.active,
|
||||
selected: raw.selected,
|
||||
agent_panel_scope: raw.agent_panel_scope,
|
||||
sidebar_width: raw.sidebar_width,
|
||||
sidebar_section_split: raw.sidebar_section_split,
|
||||
collapsed_space_keys: raw.collapsed_space_keys,
|
||||
|
|
@ -261,7 +256,6 @@ pub fn capture(
|
|||
terminal_runtimes: &TerminalRuntimeRegistry,
|
||||
active: Option<usize>,
|
||||
selected: usize,
|
||||
agent_panel_scope: crate::app::state::AgentPanelScope,
|
||||
sidebar_width: u16,
|
||||
sidebar_section_split: f32,
|
||||
collapsed_space_keys: std::collections::HashSet<String>,
|
||||
|
|
@ -274,7 +268,6 @@ pub fn capture(
|
|||
.collect(),
|
||||
active,
|
||||
selected,
|
||||
agent_panel_scope,
|
||||
sidebar_width: Some(sidebar_width),
|
||||
sidebar_section_split: Some(sidebar_section_split),
|
||||
collapsed_space_keys,
|
||||
|
|
@ -489,7 +482,7 @@ mod tests {
|
|||
use ratatui::layout::{Direction, Rect};
|
||||
|
||||
use super::*;
|
||||
use crate::app::{state::AgentPanelScope, AppState, Mode};
|
||||
use crate::app::{AppState, Mode};
|
||||
use crate::layout::NavDirection;
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
|
|
@ -543,7 +536,6 @@ mod tests {
|
|||
terminal_runtimes,
|
||||
state.active,
|
||||
state.selected,
|
||||
state.agent_panel_scope,
|
||||
state.sidebar_width,
|
||||
state.sidebar_section_split,
|
||||
state.collapsed_space_keys.clone(),
|
||||
|
|
@ -571,7 +563,6 @@ mod tests {
|
|||
workspaces: vec![],
|
||||
active: None,
|
||||
selected: 0,
|
||||
agent_panel_scope: AgentPanelScope::CurrentWorkspace,
|
||||
sidebar_width: Some(26),
|
||||
sidebar_section_split: Some(0.5),
|
||||
collapsed_space_keys: std::collections::HashSet::new(),
|
||||
|
|
@ -657,7 +648,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: AgentPanelScope::CurrentWorkspace,
|
||||
sidebar_width: Some(26),
|
||||
sidebar_section_split: Some(0.5),
|
||||
collapsed_space_keys: std::collections::HashSet::new(),
|
||||
|
|
@ -683,10 +673,6 @@ mod tests {
|
|||
restored.workspaces[0].tabs[0].panes[&1].label.as_deref(),
|
||||
Some("website")
|
||||
);
|
||||
assert_eq!(
|
||||
restored.agent_panel_scope,
|
||||
AgentPanelScope::CurrentWorkspace
|
||||
);
|
||||
assert_eq!(restored.sidebar_width, Some(26));
|
||||
assert_eq!(restored.sidebar_section_split, Some(0.5));
|
||||
}
|
||||
|
|
@ -699,7 +685,6 @@ mod tests {
|
|||
assert_eq!(snap.workspaces.len(), 2);
|
||||
assert_eq!(snap.active, Some(0));
|
||||
assert_eq!(snap.selected, 0);
|
||||
assert_eq!(snap.agent_panel_scope, AgentPanelScope::AllWorkspaces);
|
||||
assert_eq!(snap.sidebar_width, None);
|
||||
assert_eq!(snap.sidebar_section_split, None);
|
||||
assert_eq!(snap.workspaces[0].tabs.len(), 2);
|
||||
|
|
@ -715,14 +700,13 @@ mod tests {
|
|||
|
||||
assert_eq!(snap.version, 3);
|
||||
assert_eq!(snap.workspaces.len(), 2);
|
||||
assert_eq!(snap.agent_panel_scope, AgentPanelScope::CurrentWorkspace);
|
||||
assert_eq!(snap.sidebar_section_split, Some(0.4));
|
||||
assert_eq!(snap.workspaces[0].active_tab, 1);
|
||||
assert_eq!(snap.workspaces[1].tabs[0].panes.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_snapshot_defaults_agent_panel_scope() {
|
||||
fn old_snapshot_defaults_sidebar_fields() {
|
||||
let json = serde_json::json!({
|
||||
"version": SNAPSHOT_VERSION,
|
||||
"workspaces": [],
|
||||
|
|
@ -733,7 +717,6 @@ mod tests {
|
|||
|
||||
let restored = parse_snapshot(&json).unwrap();
|
||||
|
||||
assert_eq!(restored.agent_panel_scope, AgentPanelScope::AllWorkspaces);
|
||||
assert_eq!(restored.sidebar_width, None);
|
||||
assert_eq!(restored.sidebar_section_split, None);
|
||||
}
|
||||
|
|
@ -847,13 +830,11 @@ mod tests {
|
|||
let mut state = state_with_workspaces(&["one"]);
|
||||
state.sidebar_width = 31;
|
||||
state.sidebar_section_split = 0.4;
|
||||
state.agent_panel_scope = AgentPanelScope::AllWorkspaces;
|
||||
state.collapsed_space_keys.insert("repo-key".into());
|
||||
|
||||
let snapshot = capture_from_state(&state);
|
||||
assert_eq!(snapshot.sidebar_width, Some(31));
|
||||
assert_eq!(snapshot.sidebar_section_split, Some(0.4));
|
||||
assert_eq!(snapshot.agent_panel_scope, AgentPanelScope::AllWorkspaces);
|
||||
assert!(snapshot.collapsed_space_keys.contains("repo-key"));
|
||||
}
|
||||
|
||||
|
|
@ -1222,7 +1203,6 @@ mod tests {
|
|||
}],
|
||||
active: Some(0),
|
||||
selected: 0,
|
||||
agent_panel_scope: AgentPanelScope::CurrentWorkspace,
|
||||
sidebar_width: Some(26),
|
||||
sidebar_section_split: Some(0.5),
|
||||
collapsed_space_keys: std::collections::HashSet::new(),
|
||||
|
|
|
|||
|
|
@ -861,7 +861,6 @@ impl HeadlessServer {
|
|||
&self.app.terminal_runtimes,
|
||||
self.app.state.active,
|
||||
self.app.state.selected,
|
||||
self.app.state.agent_panel_scope,
|
||||
self.app.state.sidebar_width,
|
||||
self.app.state.sidebar_section_split,
|
||||
self.app.state.collapsed_space_keys.clone(),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ pub struct TerminalState {
|
|||
suppressed_full_lifecycle_hook_reports: HashMap<String, SuppressedFullLifecycleHookReport>,
|
||||
metadata_report_sequences: HashMap<String, u64>,
|
||||
pub state: AgentState,
|
||||
pub last_agent_state_change_seq: Option<u64>,
|
||||
pub revision: u64,
|
||||
pub launch_argv: Option<Vec<String>>,
|
||||
pub respawn_shell_on_exit: bool,
|
||||
|
|
@ -97,6 +98,7 @@ impl TerminalState {
|
|||
suppressed_full_lifecycle_hook_reports: HashMap::new(),
|
||||
metadata_report_sequences: HashMap::new(),
|
||||
state: AgentState::Unknown,
|
||||
last_agent_state_change_seq: None,
|
||||
revision: 0,
|
||||
launch_argv: None,
|
||||
respawn_shell_on_exit: false,
|
||||
|
|
@ -904,6 +906,7 @@ impl TerminalState {
|
|||
self.agent_metadata.clear();
|
||||
self.suppressed_full_lifecycle_hook_reports.clear();
|
||||
self.state = AgentState::Unknown;
|
||||
self.last_agent_state_change_seq = None;
|
||||
self.launch_argv = None;
|
||||
self.respawn_shell_on_exit = false;
|
||||
self.pending_agent_resume_plan = None;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_sort_label(sort: AgentPanelSort) -> &'static str {
|
||||
match sort {
|
||||
AgentPanelSort::Spaces => "grouped",
|
||||
AgentPanelSort::Priority => "priority",
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_panel_toggle_label(scope: AgentPanelScope) -> &'static str {
|
||||
match scope {
|
||||
AgentPanelScope::CurrentWorkspace => "current",
|
||||
AgentPanelScope::AllWorkspaces => "all",
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub struct PaneDetail {
|
|||
pub agent: Option<Agent>,
|
||||
pub state: AgentState,
|
||||
pub seen: bool,
|
||||
pub last_agent_state_change_seq: Option<u64>,
|
||||
pub custom_status: Option<String>,
|
||||
pub state_labels: HashMap<String, String>,
|
||||
}
|
||||
|
|
@ -60,6 +61,7 @@ impl Tab {
|
|||
agent: terminal.effective_known_agent(),
|
||||
state: terminal.state,
|
||||
seen: pane.seen,
|
||||
last_agent_state_change_seq: terminal.last_agent_state_change_seq,
|
||||
custom_status: presentation.custom_status,
|
||||
state_labels: presentation.state_labels,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue