aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/agent_view.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-20 00:54:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-20 00:55:36 +0300
commitd30ab1b54f6b6be95a0cb47ddc38da1d5e088700 (patch)
treeeb2002b408c856d9fcd7a573afe73b917f22cd97 /src/app/api/agent_view.rs
parent3f80947678a9723434aa7fddfe23750b5393e24c (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.rs185
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")
+ );
+ }
+}