From d30ab1b54f6b6be95a0cb47ddc38da1d5e088700 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Mon, 20 Jul 2026 00:54:05 +0300 Subject: feat: add plugin-driven agent views and startup hooks --- src/app/api/plugins/mod.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) (limited to 'src/app/api/plugins/mod.rs') diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs index 83a6065e..e48a1506 100644 --- a/src/app/api/plugins/mod.rs +++ b/src/app/api/plugins/mod.rs @@ -13,9 +13,9 @@ use crate::api::schema::{ ResponseResult, }; use crate::app::App; +pub(super) use manifest::normalize_plugin_id; use manifest::{ - effective_platforms, ensure_platform_supported, normalize_action_id, normalize_plugin_id, - normalize_plugin_source, + effective_platforms, ensure_platform_supported, normalize_action_id, normalize_plugin_source, }; #[cfg(test)] @@ -110,6 +110,7 @@ impl App { } return encode_error(id, "plugin_registry_save_failed", err.to_string()); } + self.clear_agent_view_for_source(&format!("plugin:{plugin_id}")); } encode_success(id, ResponseResult::PluginUnlinked { plugin_id, removed }) } @@ -593,6 +594,9 @@ impl App { let Some(plugin) = self.state.installed_plugins.get(&plugin_id).cloned() else { return encode_error(id, "plugin_not_found", "plugin not found"); }; + if !enabled { + self.clear_agent_view_for_source(&format!("plugin:{plugin_id}")); + } if enabled { encode_success(id, ResponseResult::PluginEnabled { plugin }) } else { @@ -1211,6 +1215,38 @@ command = ["echo", "b"] let _ = std::fs::remove_dir_all(root); } + #[test] + fn startup_hook_manifest_loads() { + let root = unique_temp_path("plugin-startup-manifest"); + write_manifest_content( + &root, + r#" +id = "example.startup-manifest" +name = "Startup Manifest" +version = "0.1.0" +min_herdr_version = "0.6.10" +platforms = ["linux", "macos", "windows"] + +[[startup]] +command = ["node", "restore.js"] +platforms = ["linux", "macos"] +"#, + ); + + let plugin = load_plugin_manifest(&root.display().to_string(), true).unwrap(); + + assert_eq!(plugin.startup.len(), 1); + assert_eq!(plugin.startup[0].command, ["node", "restore.js"]); + assert_eq!( + plugin.startup[0].platforms, + Some(vec![ + crate::api::schema::PluginPlatform::Linux, + crate::api::schema::PluginPlatform::Macos, + ]) + ); + let _ = std::fs::remove_dir_all(root); + } + #[test] fn smoke_fixture_manifest_loads() { let plugin = load_plugin_manifest("tests/fixtures/plugin-smoke", true) @@ -1235,6 +1271,12 @@ command = ["echo", "b"] let root = unique_temp_path("plugin-enable-disable"); write_manifest(&root); link_manifest(&mut app, &root); + app.state.agent_view_override = Some(crate::api::schema::AgentViewSetParams { + source: "plugin:example.worktree-bootstrap".into(), + label: None, + filter: None, + sort: Vec::new(), + }); let disabled = app.handle_api_request(Request { id: "disable".into(), @@ -1246,6 +1288,19 @@ command = ["echo", "b"] panic!("expected disabled response: {disabled}"); }; assert!(!plugin.enabled); + assert!(app.state.agent_view_override.is_none()); + let delayed_restore = app.handle_api_request(Request { + id: "delayed-startup-restore".into(), + method: Method::AgentViewSet(crate::api::schema::AgentViewSetParams { + source: "plugin:example.worktree-bootstrap".into(), + label: None, + filter: None, + sort: Vec::new(), + }), + }); + let delayed_restore: crate::api::schema::ErrorResponse = + serde_json::from_str(&delayed_restore).unwrap(); + assert_eq!(delayed_restore.error.code, "plugin_disabled"); let enabled = app.handle_api_request(Request { id: "enable".into(), @@ -2264,6 +2319,43 @@ command = ["sh", "-c", "printf '%s\n%s\n%s' \"$HERDR_PLUGIN_ROOT\" \"$HERDR_PLUG assert_eq!(context.selected_text.as_deref(), Some("hello")); } + #[cfg(unix)] + #[test] + fn startup_hooks_run_once_with_plugin_environment() { + let mut app = test_app(); + let root = unique_temp_path("plugin-startup-hook"); + let capture = root.join("startup.txt"); + write_manifest_content( + &root, + &format!( + r#" +id = "example.startup" +name = "Startup" +version = "0.1.0" +min_herdr_version = "0.6.10" +platforms = ["linux", "macos"] + +[[startup]] +command = ["sh", "-c", "printf '%s:%s' \"$HERDR_PLUGIN_ID\" \"$HERDR_PLUGIN_EVENT\" > {}"] +"#, + capture.display() + ), + ); + link_manifest(&mut app, &root); + + app.run_plugin_startup_hooks(); + + assert_eq!( + read_capture_when_ready(&capture, || { + app.drain_all_internal_events(); + }), + "example.startup:startup" + ); + let plugin = app.state.installed_plugins.get("example.startup").unwrap(); + assert_eq!(plugin.startup.len(), 1); + let _ = std::fs::remove_dir_all(root); + } + #[cfg(unix)] #[test] fn event_hooks_use_event_target_context() { -- cgit v1.2.3