From fbd20ad6e488fb3b9efc1612fdffee6e672976c1 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sun, 14 Jun 2026 02:43:45 +0300 Subject: feat: add plugin v1 system Add manifest-driven plugin actions, event hooks, pane entrypoints, link handlers, and host APIs. Support plugin link, GitHub install, build commands, installed plugin registry, pane movement, runtime context, and plugin docs. --- src/api/schema/plugins.rs | 343 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 src/api/schema/plugins.rs (limited to 'src/api/schema/plugins.rs') diff --git a/src/api/schema/plugins.rs b/src/api/schema/plugins.rs new file mode 100644 index 00000000..535733ec --- /dev/null +++ b/src/api/schema/plugins.rs @@ -0,0 +1,343 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::common::AgentStatus; +use super::common::SplitDirection; +use super::panes::PaneInfo; +use super::workspaces::WorkspaceWorktreeInfo; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginLinkParams { + pub path: String, + #[serde(default = "super::common::default_true")] + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct PluginListParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub plugin_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginUnlinkParams { + pub plugin_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginSetEnabledParams { + pub plugin_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstalledPluginInfo { + pub plugin_id: String, + pub name: String, + pub version: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub manifest_path: String, + pub plugin_root: String, + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub build: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub actions: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub events: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub panes: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub link_handlers: Vec, + #[serde(default)] + pub source: PluginSourceInfo, + /// Warnings collected at link time or on registry load (e.g. unknown event names, + /// missing manifest file). Non-fatal — the entry is kept and surfaced by plugin.list. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub warnings: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginSourceInfo { + #[serde(default)] + pub kind: PluginSourceKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub owner: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repo: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subdir: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub requested_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resolved_commit: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub managed_path: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub installed_unix_ms: Option, +} + +impl Default for PluginSourceInfo { + fn default() -> Self { + Self { + kind: PluginSourceKind::Local, + owner: None, + repo: None, + subdir: None, + requested_ref: None, + resolved_commit: None, + managed_path: None, + installed_unix_ms: None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum PluginSourceKind { + #[default] + Local, + Github, +} + +pub(crate) fn plugin_managed_path_component(value: &str) -> String { + value + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') { + ch + } else { + '_' + } + }) + .collect() +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginManifestBuild { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, + pub command: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginManifestAction { + pub id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub contexts: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, + pub command: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginManifestEventHook { + pub on: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, + pub command: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginManifestPane { + pub id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, + #[serde(default)] + pub placement: PluginPanePlacement, + pub command: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginManifestLinkHandler { + pub id: String, + pub title: String, + pub pattern: String, + pub action: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct PluginActionListParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub plugin_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct PluginLogListParams { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub plugin_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginActionInvokeParams { + pub action_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub plugin_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub context: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginCommandLogInfo { + pub log_id: String, + pub plugin_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub action_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub event: Option, + pub command: Vec, + pub status: PluginCommandStatus, + pub started_unix_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub finished_unix_ms: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stdout: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stderr: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PluginCommandStatus { + Running, + Succeeded, + Failed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PluginPlatform { + Linux, + Macos, + Windows, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum PluginActionContext { + Global, + Workspace, + Tab, + Pane, + Selection, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginInvocationContext { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worktree: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tab_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tab_label: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_pane_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_pane_cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_pane_agent: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_pane_status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub selected_text: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub invocation_source: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub correlation_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub clicked_url: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub link_handler_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginActionInfo { + pub plugin_id: String, + pub action_id: String, + pub title: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub contexts: Vec, + pub command: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub platforms: Option>, +} + +impl PluginActionInfo { + pub fn qualified_id(&self) -> String { + format!("{}.{}", self.plugin_id, self.action_id) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginPaneOpenParams { + pub plugin_id: String, + pub entrypoint: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub placement: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target_pane_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub direction: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cwd: Option, + #[serde(default)] + pub focus: bool, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub env: HashMap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum PluginPanePlacement { + #[default] + Overlay, + Split, + Tab, + Zoomed, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginPaneFocusParams { + pub pane_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginPaneCloseParams { + pub pane_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginPaneInfo { + pub plugin_id: String, + pub entrypoint: String, + pub pane: PaneInfo, +} -- cgit v1.2.3