diff options
| author | akbash <akbash@herdr.dev> | 2026-09-14 01:11:07 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-14 02:11:07 +0400 |
| commit | 3b478e69dcbe8e416ddd46f0e24156392d4e71da (patch) | |
| tree | e4ae8adfc09dce6eb09bd036fb727cc7f19edff7 /src/agent_view_eval.rs | |
| parent | 4e1096b1018496a23fb9de7f9ae737d3792bb38b (diff) | |
fix: scope agent views to the selected machine (#3784)
* 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>
Diffstat (limited to 'src/agent_view_eval.rs')
| -rw-r--r-- | src/agent_view_eval.rs | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/src/agent_view_eval.rs b/src/agent_view_eval.rs new file mode 100644 index 00000000..5d3fe844 --- /dev/null +++ b/src/agent_view_eval.rs @@ -0,0 +1,213 @@ +use std::borrow::Cow; +use std::cmp::Ordering; + +use crate::api::schema::{ + AgentViewBuiltinField, AgentViewBuiltinSortField, AgentViewField, AgentViewFilter, + AgentViewSort, AgentViewSortField, AgentViewSortOrder, AgentViewValue, +}; + +pub(crate) struct AgentViewContext { + pub(crate) scope: usize, + pub(crate) workspace_id: Option<String>, + pub(crate) tab_id: Option<String>, +} + +pub(crate) trait AgentViewEntry { + fn scope(&self) -> usize; + fn status(&self) -> &'static str; + fn workspace_id(&self) -> Option<Cow<'_, str>>; + fn tab_id(&self) -> Option<Cow<'_, str>>; + fn pane_id(&self) -> Option<Cow<'_, str>>; + fn agent(&self) -> Option<&str>; + fn seen(&self) -> bool; + fn state_change_seq(&self) -> Option<u64>; + fn token(&self, token: &str) -> Option<&str>; + fn workspace_order(&self) -> Option<u64>; + fn tab_order(&self) -> Option<u64>; + fn pane_order(&self) -> Option<u64>; + fn attention(&self) -> u64; +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +enum EvalValue<'a> { + String(Cow<'a, str>), + ScopedId { scope: usize, value: Cow<'a, str> }, + Bool(bool), + Number(u64), +} + +pub(crate) fn matches_filter<E: AgentViewEntry + ?Sized>( + context: &AgentViewContext, + entry: &E, + filter: &AgentViewFilter, +) -> bool { + match filter { + AgentViewFilter::All { filters } => filters + .iter() + .all(|filter| matches_filter(context, entry, filter)), + AgentViewFilter::Any { filters } => filters + .iter() + .any(|filter| matches_filter(context, entry, filter)), + AgentViewFilter::Not { filter } => !matches_filter(context, entry, filter), + AgentViewFilter::Eq { field, value } => { + field_value(entry, field) == operand_value(context, field, value) + } + AgentViewFilter::In { field, values } => { + let actual = field_value(entry, field); + values + .iter() + .any(|value| actual == operand_value(context, field, value)) + } + AgentViewFilter::Exists { field } => field_value(entry, field).is_some(), + } +} + +pub(crate) fn compare_entries<E: AgentViewEntry + ?Sized>( + left: &E, + right: &E, + sorts: &[AgentViewSort], +) -> Ordering { + for sort in sorts { + let ordering = compare_optional_values( + sort_value(left, &sort.field), + sort_value(right, &sort.field), + sort.order, + ); + if ordering != Ordering::Equal { + return ordering; + } + } + Ordering::Equal +} + +fn field_value<'a, E: AgentViewEntry + ?Sized>( + entry: &'a E, + field: &AgentViewField, +) -> Option<EvalValue<'a>> { + match field { + AgentViewField::Builtin(AgentViewBuiltinField::Status) => { + Some(EvalValue::String(Cow::Borrowed(entry.status()))) + } + AgentViewField::Builtin(AgentViewBuiltinField::WorkspaceId) => { + entry.workspace_id().map(|value| EvalValue::ScopedId { + scope: entry.scope(), + value, + }) + } + AgentViewField::Builtin(AgentViewBuiltinField::TabId) => { + entry.tab_id().map(|value| EvalValue::ScopedId { + scope: entry.scope(), + value, + }) + } + AgentViewField::Builtin(AgentViewBuiltinField::PaneId) => { + entry.pane_id().map(|value| EvalValue::ScopedId { + scope: entry.scope(), + value, + }) + } + AgentViewField::Builtin(AgentViewBuiltinField::Agent) => entry + .agent() + .map(|value| EvalValue::String(Cow::Borrowed(value))), + AgentViewField::Builtin(AgentViewBuiltinField::Seen) => Some(EvalValue::Bool(entry.seen())), + AgentViewField::Builtin(AgentViewBuiltinField::StateChangeSeq) => { + entry.state_change_seq().map(EvalValue::Number) + } + AgentViewField::Token { token } => entry + .token(token) + .map(|value| EvalValue::String(Cow::Borrowed(value))), + } +} + +fn operand_value<'a>( + context: &'a AgentViewContext, + field: &AgentViewField, + value: &'a AgentViewValue, +) -> Option<EvalValue<'a>> { + match value { + AgentViewValue::String(value) if is_id_field(field) => Some(EvalValue::ScopedId { + scope: context.scope, + value: Cow::Borrowed(value), + }), + AgentViewValue::String(value) => Some(EvalValue::String(Cow::Borrowed(value))), + AgentViewValue::Bool(value) => Some(EvalValue::Bool(*value)), + AgentViewValue::Number(value) => Some(EvalValue::Number(*value)), + AgentViewValue::Context { + context: view_context, + } => match view_context { + crate::api::schema::AgentViewContext::CurrentWorkspaceId => context + .workspace_id + .as_deref() + .map(|value| EvalValue::ScopedId { + scope: context.scope, + value: Cow::Borrowed(value), + }), + crate::api::schema::AgentViewContext::CurrentTabId => { + context.tab_id.as_deref().map(|value| EvalValue::ScopedId { + scope: context.scope, + value: Cow::Borrowed(value), + }) + } + }, + } +} + +fn is_id_field(field: &AgentViewField) -> bool { + matches!( + field, + AgentViewField::Builtin( + AgentViewBuiltinField::WorkspaceId + | AgentViewBuiltinField::TabId + | AgentViewBuiltinField::PaneId + ) + ) +} + +fn sort_value<'a, E: AgentViewEntry + ?Sized>( + entry: &'a E, + field: &AgentViewSortField, +) -> Option<EvalValue<'a>> { + match field { + AgentViewSortField::Token { token } => entry + .token(token) + .map(|value| EvalValue::String(Cow::Borrowed(value))), + AgentViewSortField::Builtin(field) => match field { + AgentViewBuiltinSortField::WorkspaceOrder => { + entry.workspace_order().map(EvalValue::Number) + } + AgentViewBuiltinSortField::TabOrder => entry.tab_order().map(EvalValue::Number), + AgentViewBuiltinSortField::PaneOrder => entry.pane_order().map(EvalValue::Number), + AgentViewBuiltinSortField::Attention => Some(EvalValue::Number(entry.attention())), + AgentViewBuiltinSortField::Status => { + Some(EvalValue::String(Cow::Borrowed(entry.status()))) + } + AgentViewBuiltinSortField::Agent => entry + .agent() + .map(|value| EvalValue::String(Cow::Borrowed(value))), + AgentViewBuiltinSortField::Seen => Some(EvalValue::Bool(entry.seen())), + AgentViewBuiltinSortField::StateChangeSeq => { + entry.state_change_seq().map(EvalValue::Number) + } + }, + } +} + +fn compare_optional_values( + left: Option<EvalValue<'_>>, + right: Option<EvalValue<'_>>, + order: AgentViewSortOrder, +) -> Ordering { + match (left, right) { + (Some(left), Some(right)) => { + let ordering = left.cmp(&right); + if matches!(order, AgentViewSortOrder::Desc) { + ordering.reverse() + } else { + ordering + } + } + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => Ordering::Equal, + } +} |
