mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
* fix: scope agent views to the selected machine refs #3732 * fix: preserve stable agent view ordering refs #3732 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
440 lines
16 KiB
Rust
440 lines
16 KiB
Rust
//! Endpoint-qualified rows shared by aggregate navigation surfaces.
|
|
|
|
use super::*;
|
|
use crate::protocol::ClientShellAgent;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(super) struct CachedEndpointSnapshot<'a> {
|
|
pub(super) endpoint_index: usize,
|
|
pub(super) endpoint_id: &'a ClientEndpointId,
|
|
pub(super) label: &'a str,
|
|
pub(super) status: ClientEndpointStatus,
|
|
pub(super) snapshot: &'a ClientShellSnapshot,
|
|
pub(super) agent_recency: &'a HashMap<String, u64>,
|
|
pub(super) agent_presentation: &'a super::endpoint_agent_state::EndpointAgentPresentation,
|
|
}
|
|
|
|
impl CachedEndpointSnapshot<'_> {
|
|
pub(super) fn stale(self) -> bool {
|
|
self.status != ClientEndpointStatus::Online
|
|
}
|
|
}
|
|
|
|
pub(super) fn cached_endpoint_snapshots(
|
|
endpoints: &[ClientShellEndpoint],
|
|
) -> impl Iterator<Item = CachedEndpointSnapshot<'_>> {
|
|
endpoints
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(endpoint_index, endpoint)| {
|
|
endpoint
|
|
.snapshot
|
|
.as_deref()
|
|
.map(|snapshot| CachedEndpointSnapshot {
|
|
endpoint_index,
|
|
endpoint_id: &endpoint.endpoint_id,
|
|
label: &endpoint.label,
|
|
status: endpoint.status,
|
|
snapshot,
|
|
agent_recency: &endpoint.agent_recency,
|
|
agent_presentation: &endpoint.agent_presentation,
|
|
})
|
|
})
|
|
}
|
|
|
|
pub(super) struct AggregateAgentRow<'a> {
|
|
pub(super) endpoint: CachedEndpointSnapshot<'a>,
|
|
pub(super) agent: &'a ClientShellAgent,
|
|
pub(super) recency: u64,
|
|
}
|
|
|
|
pub(super) struct AggregateAgentTarget {
|
|
pub(super) endpoint_id: ClientEndpointId,
|
|
pub(super) pane_id: String,
|
|
}
|
|
|
|
pub(super) fn aggregate_agent_rows<'a>(
|
|
endpoints: &'a [ClientShellEndpoint],
|
|
active_endpoint_id: &ClientEndpointId,
|
|
sort: crate::config::AgentPanelSortConfig,
|
|
) -> Vec<AggregateAgentRow<'a>> {
|
|
let active_index = endpoints
|
|
.iter()
|
|
.position(|endpoint| &endpoint.endpoint_id == active_endpoint_id);
|
|
let active_view = active_index.and_then(|index| {
|
|
let endpoint = &endpoints[index];
|
|
if !endpoint.agent_view_projection_supported {
|
|
return None;
|
|
}
|
|
match ClientShellState::endpoint_agent_view(endpoint) {
|
|
Some(Ok(view)) => Some(Ok(view.as_ref())),
|
|
Some(Err(())) => Some(Err(())),
|
|
None if endpoint
|
|
.snapshot
|
|
.as_deref()
|
|
.is_some_and(|snapshot| snapshot.agent_view_label.is_none()) =>
|
|
{
|
|
Some(Ok(None))
|
|
}
|
|
None => Some(Err(())),
|
|
}
|
|
});
|
|
|
|
if let Some(Ok(view)) = active_view {
|
|
let mut rows = cached_endpoint_snapshots(endpoints)
|
|
.flat_map(|endpoint| {
|
|
endpoint
|
|
.snapshot
|
|
.agents
|
|
.iter()
|
|
.map(move |agent| AggregateAgentRow {
|
|
recency: endpoint
|
|
.agent_recency
|
|
.get(&agent.pane_id)
|
|
.copied()
|
|
.unwrap_or_default(),
|
|
endpoint,
|
|
agent,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if let Some(view) = view {
|
|
let context = active_index
|
|
.and_then(|index| endpoints[index].snapshot.as_deref())
|
|
.map(|snapshot| crate::agent_view_eval::AgentViewContext {
|
|
scope: active_index.unwrap_or_default(),
|
|
workspace_id: snapshot.focused_workspace_id.clone(),
|
|
tab_id: snapshot.focused_tab_id.clone(),
|
|
});
|
|
if let (Some(context), Some(filter)) = (context.as_ref(), view.filter.as_ref()) {
|
|
rows.retain(|row| {
|
|
crate::agent_view_eval::matches_filter(
|
|
context,
|
|
&ClientAgentViewEntry::new(row),
|
|
filter,
|
|
)
|
|
});
|
|
}
|
|
if !view.sort.is_empty() {
|
|
rows.sort_by(|left, right| {
|
|
crate::agent_view_eval::compare_entries(
|
|
&ClientAgentViewEntry::new(left),
|
|
&ClientAgentViewEntry::new(right),
|
|
&view.sort,
|
|
)
|
|
});
|
|
return rows;
|
|
}
|
|
}
|
|
sort_aggregate_rows(&mut rows, sort);
|
|
return rows;
|
|
}
|
|
|
|
let mut rows = cached_endpoint_snapshots(endpoints)
|
|
.flat_map(|endpoint| {
|
|
super::agent_sidebar::ordered_agent_pane_ids(endpoint.snapshot, sort)
|
|
.into_iter()
|
|
.filter_map(move |pane_id| {
|
|
let agent = endpoint
|
|
.snapshot
|
|
.agents
|
|
.iter()
|
|
.find(|agent| agent.pane_id == pane_id)?;
|
|
Some(AggregateAgentRow {
|
|
recency: endpoint
|
|
.agent_recency
|
|
.get(&pane_id)
|
|
.copied()
|
|
.unwrap_or_default(),
|
|
endpoint,
|
|
agent,
|
|
})
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
sort_aggregate_rows(&mut rows, sort);
|
|
rows
|
|
}
|
|
|
|
fn sort_aggregate_rows(
|
|
rows: &mut [AggregateAgentRow<'_>],
|
|
sort: crate::config::AgentPanelSortConfig,
|
|
) {
|
|
if sort == crate::config::AgentPanelSortConfig::Priority {
|
|
rows.sort_by_key(|row| {
|
|
(
|
|
row.endpoint.stale(),
|
|
std::cmp::Reverse(status_priority(row.agent.agent_status)),
|
|
std::cmp::Reverse(row.recency),
|
|
)
|
|
});
|
|
}
|
|
}
|
|
|
|
struct ClientAgentViewEntry<'a> {
|
|
endpoint_index: usize,
|
|
snapshot: &'a ClientShellSnapshot,
|
|
agent: &'a ClientShellAgent,
|
|
seen: bool,
|
|
}
|
|
|
|
impl<'a> ClientAgentViewEntry<'a> {
|
|
fn new(row: &AggregateAgentRow<'a>) -> Self {
|
|
Self {
|
|
endpoint_index: row.endpoint.endpoint_index,
|
|
snapshot: row.endpoint.snapshot,
|
|
agent: row.agent,
|
|
seen: row.endpoint.agent_presentation.seen(row.agent),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl crate::agent_view_eval::AgentViewEntry for ClientAgentViewEntry<'_> {
|
|
fn scope(&self) -> usize {
|
|
self.endpoint_index
|
|
}
|
|
|
|
fn status(&self) -> &'static str {
|
|
status_text(self.agent.agent_status)
|
|
}
|
|
|
|
fn workspace_id(&self) -> Option<std::borrow::Cow<'_, str>> {
|
|
Some(std::borrow::Cow::Borrowed(&self.agent.workspace_id))
|
|
}
|
|
|
|
fn tab_id(&self) -> Option<std::borrow::Cow<'_, str>> {
|
|
Some(std::borrow::Cow::Borrowed(&self.agent.tab_id))
|
|
}
|
|
|
|
fn pane_id(&self) -> Option<std::borrow::Cow<'_, str>> {
|
|
Some(std::borrow::Cow::Borrowed(&self.agent.pane_id))
|
|
}
|
|
|
|
fn agent(&self) -> Option<&str> {
|
|
self.agent.agent.as_deref()
|
|
}
|
|
|
|
fn seen(&self) -> bool {
|
|
self.seen
|
|
}
|
|
|
|
fn state_change_seq(&self) -> Option<u64> {
|
|
Some(self.agent.state_change_seq)
|
|
}
|
|
|
|
fn token(&self, token: &str) -> Option<&str> {
|
|
self.agent
|
|
.tokens
|
|
.iter()
|
|
.find(|(name, _)| name == token)
|
|
.map(|(_, value)| value.as_str())
|
|
}
|
|
|
|
fn workspace_order(&self) -> Option<u64> {
|
|
self.snapshot
|
|
.workspaces
|
|
.iter()
|
|
.position(|workspace| workspace.workspace_id == self.agent.workspace_id)
|
|
.map(|index| index as u64)
|
|
}
|
|
|
|
fn tab_order(&self) -> Option<u64> {
|
|
self.snapshot
|
|
.tabs
|
|
.iter()
|
|
.find(|tab| tab.tab_id == self.agent.tab_id)
|
|
.map(|tab| tab.number as u64)
|
|
}
|
|
|
|
fn pane_order(&self) -> Option<u64> {
|
|
let suffix = self
|
|
.agent
|
|
.pane_id
|
|
.strip_prefix(&self.agent.workspace_id)?
|
|
.strip_prefix(":p")?;
|
|
crate::workspace::decode_public_number(suffix).map(|number| number as u64)
|
|
}
|
|
|
|
fn attention(&self) -> u64 {
|
|
u64::from(status_priority(self.agent.agent_status))
|
|
}
|
|
}
|
|
|
|
pub(super) fn online_agent_targets(
|
|
endpoints: &[ClientShellEndpoint],
|
|
active_endpoint_id: &ClientEndpointId,
|
|
sort: crate::config::AgentPanelSortConfig,
|
|
) -> Vec<AggregateAgentTarget> {
|
|
aggregate_agent_rows(endpoints, active_endpoint_id, sort)
|
|
.into_iter()
|
|
.filter(|row| !row.endpoint.stale())
|
|
.map(|row| AggregateAgentTarget {
|
|
endpoint_id: row.endpoint.endpoint_id.clone(),
|
|
pane_id: row.agent.pane_id.clone(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(super) fn navigator_rows(
|
|
endpoints: &[ClientShellEndpoint],
|
|
active_endpoint_id: &ClientEndpointId,
|
|
navigator: &ClientNavigatorOverlay,
|
|
) -> Vec<ClientNavigatorRow> {
|
|
let query = navigator.query.trim().to_lowercase();
|
|
let filter = |status| match navigator.filter {
|
|
Some(ClientNavigatorFilter::Blocked) => status == crate::api::schema::AgentStatus::Blocked,
|
|
Some(ClientNavigatorFilter::Working) => status == crate::api::schema::AgentStatus::Working,
|
|
Some(ClientNavigatorFilter::Idle) => status == crate::api::schema::AgentStatus::Idle,
|
|
Some(ClientNavigatorFilter::Done) => status == crate::api::schema::AgentStatus::Done,
|
|
None => true,
|
|
};
|
|
let text = |value: &str| query.is_empty() || value.to_lowercase().contains(&query);
|
|
let filtering = navigator.filter.is_some() || !query.is_empty();
|
|
let federated = endpoints.len() > 1;
|
|
let depth_offset = u8::from(federated);
|
|
let mut rows = Vec::new();
|
|
|
|
for endpoint in endpoints {
|
|
let stale = endpoint.status != ClientEndpointStatus::Online;
|
|
let endpoint_query_matches = !query.is_empty() && text(&endpoint.label);
|
|
let mut endpoint_rows = Vec::new();
|
|
if let Some(snapshot) = endpoint.snapshot.as_deref() {
|
|
for workspace in &snapshot.workspaces {
|
|
let workspace_meta = workspace.branch.clone().unwrap_or_default();
|
|
let mut children = Vec::new();
|
|
for tab in snapshot
|
|
.tabs
|
|
.iter()
|
|
.filter(|tab| tab.workspace_id == workspace.workspace_id)
|
|
{
|
|
let mut panes = Vec::new();
|
|
for (index, pane) in snapshot
|
|
.panes
|
|
.iter()
|
|
.filter(|pane| pane.tab_id == tab.tab_id)
|
|
.enumerate()
|
|
{
|
|
let agent = snapshot
|
|
.agents
|
|
.iter()
|
|
.find(|agent| agent.pane_id == pane.pane_id);
|
|
let status = agent
|
|
.map_or(crate::api::schema::AgentStatus::Unknown, |agent| {
|
|
agent.agent_status
|
|
});
|
|
let label = pane
|
|
.label
|
|
.clone()
|
|
.or_else(|| agent.and_then(|agent| agent.name.clone()))
|
|
.or_else(|| agent.and_then(|agent| agent.display_agent.clone()))
|
|
.or_else(|| agent.and_then(|agent| agent.title.clone()))
|
|
.unwrap_or_else(|| format!("pane {}", index + 1));
|
|
let meta = pane
|
|
.foreground_cwd
|
|
.clone()
|
|
.or_else(|| pane.cwd.clone())
|
|
.unwrap_or_default();
|
|
if !filtering
|
|
|| filter(status)
|
|
&& (endpoint_query_matches || text(&label) || text(&meta))
|
|
{
|
|
panes.push(ClientNavigatorRow {
|
|
depth: 2 + depth_offset,
|
|
label,
|
|
meta,
|
|
status: Some(status),
|
|
stale,
|
|
current: endpoint.endpoint_id == *active_endpoint_id
|
|
&& snapshot.focused_pane_id.as_deref() == Some(&pane.pane_id),
|
|
target: ClientNavigatorTarget::Pane {
|
|
endpoint_id: endpoint.endpoint_id.clone(),
|
|
pane_id: pane.pane_id.clone(),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
if !filtering
|
|
|| filter(tab.agent_status) && (endpoint_query_matches || text(&tab.label))
|
|
|| !panes.is_empty()
|
|
{
|
|
children.push(ClientNavigatorRow {
|
|
depth: 1 + depth_offset,
|
|
label: tab.label.clone(),
|
|
meta: format!(
|
|
"{} panes",
|
|
snapshot
|
|
.panes
|
|
.iter()
|
|
.filter(|pane| pane.tab_id == tab.tab_id)
|
|
.count()
|
|
),
|
|
status: None,
|
|
stale,
|
|
current: false,
|
|
target: ClientNavigatorTarget::Tab {
|
|
endpoint_id: endpoint.endpoint_id.clone(),
|
|
tab_id: tab.tab_id.clone(),
|
|
},
|
|
});
|
|
children.extend(panes);
|
|
}
|
|
}
|
|
let workspace_matches = filter(workspace.agent_status)
|
|
&& (endpoint_query_matches || text(&workspace.label) || text(&workspace_meta));
|
|
if !filtering || workspace_matches || !children.is_empty() {
|
|
let key = (endpoint.endpoint_id.clone(), workspace.workspace_id.clone());
|
|
endpoint_rows.push(ClientNavigatorRow {
|
|
depth: depth_offset,
|
|
label: workspace.label.clone(),
|
|
meta: workspace_meta,
|
|
status: None,
|
|
stale,
|
|
current: false,
|
|
target: ClientNavigatorTarget::Workspace {
|
|
endpoint_id: endpoint.endpoint_id.clone(),
|
|
workspace_id: workspace.workspace_id.clone(),
|
|
},
|
|
});
|
|
if navigator.expanded_workspaces.contains(&key) || filtering {
|
|
endpoint_rows.extend(children);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !filtering || endpoint_query_matches || !endpoint_rows.is_empty() {
|
|
if federated {
|
|
rows.push(ClientNavigatorRow {
|
|
depth: 0,
|
|
label: endpoint.label.to_owned(),
|
|
meta: String::new(),
|
|
status: None,
|
|
stale,
|
|
current: false,
|
|
target: ClientNavigatorTarget::Machine {
|
|
endpoint_id: endpoint.endpoint_id.clone(),
|
|
},
|
|
});
|
|
}
|
|
rows.extend(endpoint_rows);
|
|
}
|
|
}
|
|
rows
|
|
}
|
|
|
|
pub(super) fn navigator_selected_index(
|
|
rows: &[ClientNavigatorRow],
|
|
navigator: &ClientNavigatorOverlay,
|
|
) -> Option<usize> {
|
|
match navigator.selected.as_ref() {
|
|
Some(target) => rows.iter().position(|row| row.target == *target),
|
|
None => (!rows.is_empty()).then_some(0),
|
|
}
|
|
}
|
|
|
|
pub(super) fn selected_navigator_target(
|
|
rows: &[ClientNavigatorRow],
|
|
navigator: &ClientNavigatorOverlay,
|
|
) -> Option<ClientNavigatorTarget> {
|
|
navigator_selected_index(rows, navigator).map(|index| rows[index].target.clone())
|
|
}
|