diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-20 00:54:05 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-20 00:55:36 +0300 |
| commit | d30ab1b54f6b6be95a0cb47ddc38da1d5e088700 (patch) | |
| tree | eb2002b408c856d9fcd7a573afe73b917f22cd97 /src/app/api/agent_view.rs | |
| parent | 3f80947678a9723434aa7fddfe23750b5393e24c (diff) | |
feat: add plugin-driven agent views and startup hooks
Diffstat (limited to 'src/app/api/agent_view.rs')
| -rw-r--r-- | src/app/api/agent_view.rs | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/src/app/api/agent_view.rs b/src/app/api/agent_view.rs new file mode 100644 index 00000000..e14a933d --- /dev/null +++ b/src/app/api/agent_view.rs @@ -0,0 +1,185 @@ +use crate::api::schema::{AgentViewClearParams, AgentViewSetParams, ResponseResult}; +use crate::app::App; + +use super::responses::{encode_error, encode_success}; + +impl App { + pub(super) fn handle_agent_view_set( + &mut self, + id: String, + mut params: AgentViewSetParams, + ) -> String { + if let Err(message) = crate::app::agent_view::validate_agent_view(&mut params) { + return encode_error(id, "invalid_agent_view", message); + } + if let Some(plugin_id) = params.source.strip_prefix("plugin:") { + let Some(plugin_id) = super::plugins::normalize_plugin_id(plugin_id) else { + return encode_error( + id, + "invalid_agent_view", + "plugin-owned agent view source has an invalid plugin id", + ); + }; + let Some(plugin) = self.state.installed_plugins.get(&plugin_id) else { + return encode_error(id, "plugin_not_found", "plugin not found"); + }; + if !plugin.enabled { + return encode_error(id, "plugin_disabled", "plugin is disabled"); + } + } + let source = params.source.clone(); + let label = params.label.clone(); + self.replace_agent_view_override(Some(params)); + encode_success( + id, + ResponseResult::AgentView { + active: true, + source: Some(source), + label, + }, + ) + } + + pub(super) fn handle_agent_view_clear( + &mut self, + id: String, + params: AgentViewClearParams, + ) -> String { + let source = match params.source { + Some(source) => match crate::app::agent_view::validate_agent_view_source(&source) { + Ok(source) => Some(source), + Err(message) => return encode_error(id, "invalid_agent_view", message), + }, + None => None, + }; + if source.as_deref().is_none_or(|source| { + self.state + .agent_view_override + .as_ref() + .is_some_and(|active| active.source == source) + }) { + self.replace_agent_view_override(None); + } + let active = self.state.agent_view_override.as_ref(); + encode_success( + id, + ResponseResult::AgentView { + active: active.is_some(), + source: active.map(|view| view.source.clone()), + label: active.and_then(|view| view.label.clone()), + }, + ) + } + + pub(crate) fn clear_agent_view_for_source(&mut self, source: &str) -> bool { + if self + .state + .agent_view_override + .as_ref() + .is_some_and(|active| active.source == source) + { + self.replace_agent_view_override(None); + true + } else { + false + } + } + + fn replace_agent_view_override(&mut self, view: Option<AgentViewSetParams>) { + self.state.agent_view_override = view; + self.state.agent_panel_scroll = 0; + self.state.mobile_switcher_scroll = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::schema::{ + AgentViewBuiltinField, AgentViewField, AgentViewFilter, AgentViewValue, + }; + + fn test_app() -> App { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + App::new( + &crate::config::Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ) + } + + fn working_view(source: &str) -> AgentViewSetParams { + AgentViewSetParams { + source: source.to_string(), + label: Some("working".to_string()), + filter: Some(AgentViewFilter::Eq { + field: AgentViewField::Builtin(AgentViewBuiltinField::Status), + value: AgentViewValue::String("working".to_string()), + }), + sort: Vec::new(), + } + } + + #[test] + fn set_and_source_guarded_clear_replace_transient_view() { + let mut app = test_app(); + + let set = app.handle_agent_view_set("set".to_string(), working_view("example.views")); + let set: crate::api::schema::SuccessResponse = serde_json::from_str(&set).unwrap(); + assert_eq!( + set.result, + ResponseResult::AgentView { + active: true, + source: Some("example.views".to_string()), + label: Some("working".to_string()), + } + ); + assert_eq!( + app.state + .agent_view_override + .as_ref() + .map(|view| view.source.as_str()), + Some("example.views") + ); + + app.handle_agent_view_clear( + "wrong-source".to_string(), + AgentViewClearParams { + source: Some("other.views".to_string()), + }, + ); + assert!(app.state.agent_view_override.is_some()); + + app.handle_agent_view_clear( + "right-source".to_string(), + AgentViewClearParams { + source: Some("example.views".to_string()), + }, + ); + assert!(app.state.agent_view_override.is_none()); + } + + #[test] + fn invalid_view_does_not_replace_active_view() { + let mut app = test_app(); + app.handle_agent_view_set("set".to_string(), working_view("example.views")); + + let mut invalid = working_view("example.other"); + invalid.filter = Some(AgentViewFilter::Any { + filters: Vec::new(), + }); + let response = app.handle_agent_view_set("invalid".to_string(), invalid); + let response: crate::api::schema::ErrorResponse = serde_json::from_str(&response).unwrap(); + + assert_eq!(response.error.code, "invalid_agent_view"); + assert_eq!( + app.state + .agent_view_override + .as_ref() + .map(|view| view.source.as_str()), + Some("example.views") + ); + } +} |
