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/api/mod.rs | 4 ++ src/api/schema.rs | 4 ++ src/api/schema/agents.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++ src/api/schema/plugins.rs | 9 ++++ src/api/schema/response.rs | 7 +++ src/api/schema/tests.rs | 44 +++++++++++++++++ src/api/server.rs | 2 + 7 files changed, 184 insertions(+) (limited to 'src/api') diff --git a/src/api/mod.rs b/src/api/mod.rs index badf1707..994b2d87 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -42,6 +42,8 @@ pub(crate) fn request_changes_ui(request: &Request) -> bool { | Method::LayoutApply(_) | Method::LayoutSetSplitRatio(_) | Method::AgentRename(_) + | Method::AgentViewSet(_) + | Method::AgentViewClear(_) | Method::AgentFocus(_) | Method::AgentStart(_) | Method::AgentPrompt(_) @@ -67,6 +69,8 @@ pub(crate) fn request_changes_ui(request: &Request) -> bool { | Method::PaneReleaseAgent(_) | Method::PaneClose(_) | Method::PopupClose(_) + | Method::PluginUnlink(_) + | Method::PluginDisable(_) | Method::PluginActionInvoke(_) | Method::PluginPaneOpen(_) | Method::PluginPaneFocus(_) diff --git a/src/api/schema.rs b/src/api/schema.rs index efaee54c..72d9e53a 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -113,6 +113,10 @@ pub enum Method { AgentSendKeys(AgentSendKeysParams), #[serde(rename = "agent.rename")] AgentRename(AgentRenameParams), + #[serde(rename = "agent.view.set")] + AgentViewSet(AgentViewSetParams), + #[serde(rename = "agent.view.clear")] + AgentViewClear(AgentViewClearParams), #[serde(rename = "agent.focus")] AgentFocus(AgentTarget), #[serde(rename = "agent.start")] diff --git a/src/api/schema/agents.rs b/src/api/schema/agents.rs index 59949293..9e1e7b15 100644 --- a/src/api/schema/agents.rs +++ b/src/api/schema/agents.rs @@ -46,6 +46,120 @@ pub struct AgentRenameParams { pub name: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct AgentViewSetParams { + pub source: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub filter: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub sort: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default)] +pub struct AgentViewClearParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(tag = "op", rename_all = "snake_case")] +pub enum AgentViewFilter { + All { + filters: Vec, + }, + Any { + filters: Vec, + }, + Not { + filter: Box, + }, + Eq { + field: AgentViewField, + value: AgentViewValue, + }, + In { + field: AgentViewField, + values: Vec, + }, + Exists { + field: AgentViewField, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(untagged)] +pub enum AgentViewField { + Builtin(AgentViewBuiltinField), + Token { token: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AgentViewBuiltinField { + Status, + WorkspaceId, + TabId, + PaneId, + Agent, + Seen, + StateChangeSeq, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(untagged)] +pub enum AgentViewValue { + String(String), + Bool(bool), + Number(u64), + Context { context: AgentViewContext }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AgentViewContext { + CurrentWorkspaceId, + CurrentTabId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct AgentViewSort { + pub field: AgentViewSortField, + #[serde(default)] + pub order: AgentViewSortOrder, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(untagged)] +pub enum AgentViewSortField { + Builtin(AgentViewBuiltinSortField), + Token { token: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AgentViewBuiltinSortField { + WorkspaceOrder, + TabOrder, + PaneOrder, + Attention, + Status, + Agent, + Seen, + StateChangeSeq, +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default, +)] +#[serde(rename_all = "snake_case")] +pub enum AgentViewSortOrder { + #[default] + Asc, + Desc, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct AgentStartParams { pub name: String, diff --git a/src/api/schema/plugins.rs b/src/api/schema/plugins.rs index 75b40f97..52df3ca6 100644 --- a/src/api/schema/plugins.rs +++ b/src/api/schema/plugins.rs @@ -50,6 +50,8 @@ pub struct InstalledPluginInfo { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub build: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub startup: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub actions: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub events: Vec, @@ -231,6 +233,13 @@ pub struct PluginManifestBuild { pub command: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PluginManifestStartup { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, + pub command: Vec, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct PluginManifestAction { pub id: String, diff --git a/src/api/schema/response.rs b/src/api/schema/response.rs index e1f47efd..db8cfc7b 100644 --- a/src/api/schema/response.rs +++ b/src/api/schema/response.rs @@ -107,6 +107,13 @@ pub enum ResponseResult { AgentList { agents: Vec, }, + AgentView { + active: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + source: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + label: Option, + }, PaneInfo { pane: PaneInfo, }, diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index 236d4f12..9fcc03b0 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -295,6 +295,49 @@ fn client_window_title_requests_round_trip() { assert_eq!(restored, clear); } +#[test] +fn agent_view_requests_round_trip() { + let set_json = serde_json::json!({ + "id": "view-set", + "method": "agent.view.set", + "params": { + "source": "example.views", + "label": "current + attention", + "filter": { + "op": "any", + "filters": [ + { + "op": "eq", + "field": "workspace_id", + "value": {"context": "current_workspace_id"} + }, + { + "op": "in", + "field": "status", + "values": ["blocked", "done"] + } + ] + }, + "sort": [ + {"field": "attention", "order": "desc"}, + {"field": "state_change_seq", "order": "desc"} + ] + } + }); + let request: Request = serde_json::from_value(set_json.clone()).unwrap(); + assert!(matches!(request.method, Method::AgentViewSet(_))); + assert_eq!(serde_json::to_value(request).unwrap(), set_json); + + let clear_json = serde_json::json!({ + "id": "view-clear", + "method": "agent.view.clear", + "params": {"source": "example.views"} + }); + let request: Request = serde_json::from_value(clear_json.clone()).unwrap(); + assert!(matches!(request.method, Method::AgentViewClear(_))); + assert_eq!(serde_json::to_value(request).unwrap(), clear_json); +} + #[test] fn unknown_method_is_rejected() { let json = r#"{"id":"req_1","method":"nope","params":{}}"#; @@ -856,6 +899,7 @@ fn plugin_link_list_unlink_round_trip() { platforms: None, command: vec!["bun".into(), "install".into()], }], + startup: vec![], actions: vec![PluginManifestAction { id: "bootstrap".into(), title: "Bootstrap worktree".into(), diff --git a/src/api/server.rs b/src/api/server.rs index 0a36e2fa..5bc90ef9 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -378,6 +378,8 @@ fn api_method_name(method: &Method) -> &'static str { Method::AgentExplain(_) => "agent.explain", Method::AgentSendKeys(_) => "agent.send_keys", Method::AgentRename(_) => "agent.rename", + Method::AgentViewSet(_) => "agent.view.set", + Method::AgentViewClear(_) => "agent.view.clear", Method::AgentFocus(_) => "agent.focus", Method::AgentStart(_) => "agent.start", Method::AgentPrompt(_) => "agent.prompt", -- cgit v1.2.3