diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 02:43:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-14 03:47:07 +0300 |
| commit | fbd20ad6e488fb3b9efc1612fdffee6e672976c1 (patch) | |
| tree | d21daf93241786f8aa6fde23b41cdb9e15182b79 /src/api/schema/events.rs | |
| parent | d35c642ef36ee5ac2afe7c1b18ad5a717bc807d8 (diff) | |
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.
Diffstat (limited to 'src/api/schema/events.rs')
| -rw-r--r-- | src/api/schema/events.rs | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/src/api/schema/events.rs b/src/api/schema/events.rs new file mode 100644 index 00000000..353f75b9 --- /dev/null +++ b/src/api/schema/events.rs @@ -0,0 +1,474 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use super::common::{AgentStatus, ReadSource}; +use super::panes::{PaneInfo, PaneReadResult}; +use super::tabs::TabInfo; +use super::workspaces::WorkspaceInfo; +use super::worktrees::WorktreeInfo; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EventsSubscribeParams { + pub subscriptions: Vec<Subscription>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum Subscription { + #[serde(rename = "workspace.created")] + WorkspaceCreated {}, + #[serde(rename = "workspace.updated")] + WorkspaceUpdated {}, + #[serde(rename = "workspace.renamed")] + WorkspaceRenamed {}, + #[serde(rename = "workspace.closed")] + WorkspaceClosed {}, + #[serde(rename = "workspace.focused")] + WorkspaceFocused {}, + #[serde(rename = "worktree.created")] + WorktreeCreated {}, + #[serde(rename = "worktree.opened")] + WorktreeOpened {}, + #[serde(rename = "worktree.removed")] + WorktreeRemoved {}, + #[serde(rename = "tab.created")] + TabCreated {}, + #[serde(rename = "tab.closed")] + TabClosed {}, + #[serde(rename = "tab.focused")] + TabFocused {}, + #[serde(rename = "tab.renamed")] + TabRenamed {}, + #[serde(rename = "pane.created")] + PaneCreated {}, + #[serde(rename = "pane.closed")] + PaneClosed {}, + #[serde(rename = "pane.focused")] + PaneFocused {}, + #[serde(rename = "pane.moved")] + PaneMoved {}, + #[serde(rename = "pane.exited")] + PaneExited {}, + #[serde(rename = "pane.agent_detected")] + PaneAgentDetected {}, + #[serde(rename = "pane.output_matched")] + PaneOutputMatched { + pane_id: String, + source: ReadSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + lines: Option<u32>, + r#match: OutputMatch, + #[serde(default = "super::common::default_true")] + strip_ansi: bool, + }, + #[serde(rename = "pane.agent_status_changed")] + PaneAgentStatusChanged { + pane_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + agent_status: Option<AgentStatus>, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EventsWaitParams { + pub match_event: EventMatch, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout_ms: Option<u64>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaneWaitForOutputParams { + pub pane_id: String, + pub source: ReadSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub lines: Option<u32>, + pub r#match: OutputMatch, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timeout_ms: Option<u64>, + #[serde(default = "super::common::default_true")] + pub strip_ansi: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum OutputMatch { + Substring { value: String }, + Regex { value: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "event", rename_all = "snake_case")] +pub enum EventMatch { + WorkspaceCreated { + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace_id: Option<String>, + }, + WorkspaceUpdated { + workspace_id: String, + }, + WorkspaceClosed { + workspace_id: String, + }, + WorkspaceRenamed { + workspace_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + label: Option<String>, + }, + WorkspaceFocused { + workspace_id: String, + }, + TabCreated { + #[serde(default, skip_serializing_if = "Option::is_none")] + tab_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace_id: Option<String>, + }, + TabClosed { + tab_id: String, + }, + TabRenamed { + tab_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + label: Option<String>, + }, + TabFocused { + tab_id: String, + }, + PaneCreated { + #[serde(default, skip_serializing_if = "Option::is_none")] + pane_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace_id: Option<String>, + }, + PaneClosed { + pane_id: String, + }, + PaneFocused { + pane_id: String, + }, + PaneMoved { + pane_id: String, + }, + PaneOutputChanged { + pane_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + min_revision: Option<u64>, + }, + PaneExited { + pane_id: String, + }, + PaneAgentDetected { + pane_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + agent: Option<String>, + }, + PaneAgentStatusChanged { + pane_id: String, + agent_status: AgentStatus, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum EventKind { + WorkspaceCreated, + WorkspaceUpdated, + WorkspaceClosed, + WorkspaceRenamed, + WorkspaceFocused, + WorktreeCreated, + WorktreeOpened, + WorktreeRemoved, + TabCreated, + TabClosed, + TabRenamed, + TabFocused, + PaneCreated, + PaneClosed, + PaneFocused, + PaneMoved, + PaneOutputChanged, + PaneExited, + PaneAgentDetected, + PaneAgentStatusChanged, +} + +impl EventKind { + pub fn dot_name(self) -> &'static str { + match self { + EventKind::WorkspaceCreated => "workspace.created", + EventKind::WorkspaceUpdated => "workspace.updated", + EventKind::WorkspaceClosed => "workspace.closed", + EventKind::WorkspaceRenamed => "workspace.renamed", + EventKind::WorkspaceFocused => "workspace.focused", + EventKind::WorktreeCreated => "worktree.created", + EventKind::WorktreeOpened => "worktree.opened", + EventKind::WorktreeRemoved => "worktree.removed", + EventKind::TabCreated => "tab.created", + EventKind::TabClosed => "tab.closed", + EventKind::TabRenamed => "tab.renamed", + EventKind::TabFocused => "tab.focused", + EventKind::PaneCreated => "pane.created", + EventKind::PaneClosed => "pane.closed", + EventKind::PaneFocused => "pane.focused", + EventKind::PaneMoved => "pane.moved", + EventKind::PaneOutputChanged => "pane.output_changed", + EventKind::PaneExited => "pane.exited", + EventKind::PaneAgentDetected => "pane.agent_detected", + EventKind::PaneAgentStatusChanged => "pane.agent_status_changed", + } + } +} + +#[cfg(test)] +pub const KNOWN_EVENT_KINDS: &[EventKind] = &[ + EventKind::WorkspaceCreated, + EventKind::WorkspaceUpdated, + EventKind::WorkspaceClosed, + EventKind::WorkspaceRenamed, + EventKind::WorkspaceFocused, + EventKind::WorktreeCreated, + EventKind::WorktreeOpened, + EventKind::WorktreeRemoved, + EventKind::TabCreated, + EventKind::TabClosed, + EventKind::TabRenamed, + EventKind::TabFocused, + EventKind::PaneCreated, + EventKind::PaneClosed, + EventKind::PaneFocused, + EventKind::PaneMoved, + EventKind::PaneOutputChanged, + EventKind::PaneExited, + EventKind::PaneAgentDetected, + EventKind::PaneAgentStatusChanged, +]; + +pub const PLUGIN_HOOK_EVENT_KINDS: &[EventKind] = &[ + EventKind::WorkspaceCreated, + EventKind::WorkspaceUpdated, + EventKind::WorkspaceClosed, + EventKind::WorkspaceRenamed, + EventKind::WorkspaceFocused, + EventKind::WorktreeCreated, + EventKind::WorktreeOpened, + EventKind::WorktreeRemoved, + EventKind::TabCreated, + EventKind::TabClosed, + EventKind::TabRenamed, + EventKind::TabFocused, + EventKind::PaneCreated, + EventKind::PaneClosed, + EventKind::PaneFocused, + EventKind::PaneMoved, + EventKind::PaneExited, + EventKind::PaneAgentDetected, + EventKind::PaneAgentStatusChanged, +]; + +#[cfg(test)] +pub fn known_event_names() -> Vec<&'static str> { + KNOWN_EVENT_KINDS + .iter() + .copied() + .map(EventKind::dot_name) + .collect() +} + +/// Event names that manifest `[[events]] on` hooks can reference. This is +/// intentionally narrower than `EventKind` until high-volume output-change hook +/// semantics are implemented. +pub fn plugin_hook_event_names() -> Vec<&'static str> { + PLUGIN_HOOK_EVENT_KINDS + .iter() + .copied() + .map(EventKind::dot_name) + .collect() +} + +#[cfg(test)] +mod known_event_name_tests { + use super::*; + + #[test] + fn known_event_names_stay_in_sync_with_event_kind() { + let mut from_kind = KNOWN_EVENT_KINDS + .iter() + .map(|kind| kind.dot_name()) + .collect::<Vec<_>>(); + from_kind.sort_unstable(); + let mut known = known_event_names(); + known.sort_unstable(); + assert_eq!( + from_kind, known, + "known_event_names() out of sync with EventKind" + ); + } + + #[test] + fn plugin_hook_event_names_exclude_unemitted_output_change() { + let names = plugin_hook_event_names(); + assert!(!names.contains(&"pane.output_changed")); + assert!(names.contains(&"pane.moved")); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EventEnvelope { + pub event: EventKind, + pub data: EventData, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SubscriptionEventKind { + #[serde(rename = "pane.output_matched")] + PaneOutputMatched, + #[serde(rename = "pane.agent_status_changed")] + PaneAgentStatusChanged, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SubscriptionEventEnvelope { + pub event: SubscriptionEventKind, + pub data: SubscriptionEventData, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SubscriptionEventData { + PaneOutputMatched(PaneOutputMatchedEvent), + PaneAgentStatusChanged(PaneAgentStatusChangedEvent), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaneOutputMatchedEvent { + pub pane_id: String, + pub matched_line: String, + pub read: PaneReadResult, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PaneAgentStatusChangedEvent { + pub pane_id: String, + pub workspace_id: String, + pub agent_status: AgentStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub custom_status: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub display_agent: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub state_labels: HashMap<String, String>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum EventData { + WorkspaceCreated { + workspace: WorkspaceInfo, + }, + WorkspaceUpdated { + workspace: WorkspaceInfo, + }, + WorkspaceClosed { + workspace_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + workspace: Option<WorkspaceInfo>, + }, + WorkspaceRenamed { + workspace_id: String, + label: String, + }, + WorkspaceFocused { + workspace_id: String, + }, + WorktreeCreated { + workspace: WorkspaceInfo, + worktree: WorktreeInfo, + }, + WorktreeOpened { + workspace: WorkspaceInfo, + worktree: WorktreeInfo, + already_open: bool, + }, + WorktreeRemoved { + workspace_id: String, + worktree: WorktreeInfo, + forced: bool, + }, + TabCreated { + tab: TabInfo, + }, + TabClosed { + tab_id: String, + workspace_id: String, + }, + TabRenamed { + tab_id: String, + workspace_id: String, + label: String, + }, + TabFocused { + tab_id: String, + workspace_id: String, + }, + PaneCreated { + pane: PaneInfo, + }, + PaneClosed { + pane_id: String, + workspace_id: String, + }, + PaneFocused { + pane_id: String, + workspace_id: String, + }, + PaneMoved { + previous_pane_id: String, + previous_workspace_id: String, + previous_tab_id: String, + pane: Box<PaneInfo>, + #[serde(default, skip_serializing_if = "Option::is_none")] + created_workspace: Option<WorkspaceInfo>, + #[serde(default, skip_serializing_if = "Option::is_none")] + created_tab: Option<TabInfo>, + #[serde(default, skip_serializing_if = "Option::is_none")] + closed_workspace_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + closed_tab_id: Option<String>, + }, + PaneOutputChanged { + pane_id: String, + workspace_id: String, + revision: u64, + }, + PaneExited { + pane_id: String, + workspace_id: String, + }, + PaneAgentDetected { + pane_id: String, + workspace_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + agent: Option<String>, + }, + PaneAgentStatusChanged { + pane_id: String, + workspace_id: String, + agent_status: AgentStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + title: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + display_agent: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + custom_status: Option<String>, + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + state_labels: HashMap<String, String>, + }, +} |
