use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::common::{AgentStatus, ReadSource}; use super::panes::{PaneInfo, PaneReadResult, PaneScrollInfo}; use super::tabs::TabInfo; use super::workspaces::WorkspaceInfo; use super::worktrees::WorktreeInfo; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct EventsSubscribeParams { pub subscriptions: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(tag = "type")] pub enum Subscription { #[serde(rename = "workspace.created")] WorkspaceCreated {}, #[serde(rename = "workspace.updated")] WorkspaceUpdated {}, #[serde(rename = "workspace.metadata_updated")] WorkspaceMetadataUpdated {}, #[serde(rename = "workspace.renamed")] WorkspaceRenamed {}, #[serde(rename = "workspace.moved")] WorkspaceMoved {}, #[serde(rename = "workspace.reordered")] WorkspaceReordered {}, #[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 = "tab.moved")] TabMoved {}, #[serde(rename = "pane.created")] PaneCreated {}, #[serde(rename = "pane.closed")] PaneClosed {}, #[serde(rename = "pane.updated")] PaneUpdated {}, #[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, 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, }, #[serde(rename = "pane.scroll_changed")] PaneScrollChanged { pane_id: String }, #[serde(rename = "layout.updated")] LayoutUpdated {}, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct EventsWaitParams { pub match_event: EventMatch, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_ms: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct PaneWaitForOutputParams { pub pane_id: String, pub source: ReadSource, #[serde(default, skip_serializing_if = "Option::is_none")] pub lines: Option, pub r#match: OutputMatch, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeout_ms: Option, #[serde(default = "super::common::default_true")] pub strip_ansi: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum OutputMatch { Substring { value: String }, Regex { value: String }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(tag = "event", rename_all = "snake_case")] pub enum EventMatch { WorkspaceCreated { #[serde(default, skip_serializing_if = "Option::is_none")] workspace_id: Option, }, WorkspaceUpdated { workspace_id: String, }, WorkspaceClosed { workspace_id: String, }, WorkspaceRenamed { workspace_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] label: Option, }, WorkspaceMoved { workspace_id: String, }, WorkspaceFocused { workspace_id: String, }, TabCreated { #[serde(default, skip_serializing_if = "Option::is_none")] tab_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] workspace_id: Option, }, TabClosed { tab_id: String, }, TabRenamed { tab_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] label: Option, }, TabMoved { tab_id: String, }, TabFocused { tab_id: String, }, PaneCreated { #[serde(default, skip_serializing_if = "Option::is_none")] pane_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] workspace_id: Option, }, 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, }, PaneExited { pane_id: String, }, PaneAgentDetected { pane_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] agent: Option, }, PaneAgentStatusChanged { pane_id: String, agent_status: AgentStatus, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(rename_all = "snake_case")] pub enum EventKind { WorkspaceCreated, WorkspaceUpdated, WorkspaceMetadataUpdated, WorkspaceClosed, WorkspaceRenamed, WorkspaceMoved, WorkspaceReordered, WorkspaceFocused, WorktreeCreated, WorktreeOpened, WorktreeRemoved, TabCreated, TabClosed, TabRenamed, TabMoved, TabFocused, PaneCreated, PaneClosed, PaneUpdated, PaneFocused, PaneMoved, PaneOutputChanged, PaneExited, PaneAgentDetected, PaneAgentStatusChanged, LayoutUpdated, } impl EventKind { pub fn dot_name(self) -> &'static str { match self { EventKind::WorkspaceCreated => "workspace.created", EventKind::WorkspaceUpdated => "workspace.updated", EventKind::WorkspaceMetadataUpdated => "workspace.metadata_updated", EventKind::WorkspaceClosed => "workspace.closed", EventKind::WorkspaceRenamed => "workspace.renamed", EventKind::WorkspaceMoved => "workspace.moved", EventKind::WorkspaceReordered => "workspace.reordered", 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::TabMoved => "tab.moved", EventKind::TabFocused => "tab.focused", EventKind::PaneCreated => "pane.created", EventKind::PaneClosed => "pane.closed", EventKind::PaneUpdated => "pane.updated", 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", EventKind::LayoutUpdated => "layout.updated", } } } #[cfg(test)] pub const KNOWN_EVENT_KINDS: &[EventKind] = &[ EventKind::WorkspaceCreated, EventKind::WorkspaceUpdated, EventKind::WorkspaceMetadataUpdated, EventKind::WorkspaceClosed, EventKind::WorkspaceRenamed, EventKind::WorkspaceMoved, EventKind::WorkspaceReordered, EventKind::WorkspaceFocused, EventKind::WorktreeCreated, EventKind::WorktreeOpened, EventKind::WorktreeRemoved, EventKind::TabCreated, EventKind::TabClosed, EventKind::TabRenamed, EventKind::TabMoved, EventKind::TabFocused, EventKind::PaneCreated, EventKind::PaneClosed, EventKind::PaneUpdated, EventKind::PaneFocused, EventKind::PaneMoved, EventKind::PaneOutputChanged, EventKind::PaneExited, EventKind::PaneAgentDetected, EventKind::PaneAgentStatusChanged, EventKind::LayoutUpdated, ]; pub const PLUGIN_HOOK_EVENT_KINDS: &[EventKind] = &[ EventKind::WorkspaceCreated, EventKind::WorkspaceUpdated, EventKind::WorkspaceClosed, EventKind::WorkspaceRenamed, EventKind::WorkspaceMoved, EventKind::WorkspaceReordered, EventKind::WorkspaceFocused, EventKind::WorktreeCreated, EventKind::WorktreeOpened, EventKind::WorktreeRemoved, EventKind::TabCreated, EventKind::TabClosed, EventKind::TabRenamed, EventKind::TabMoved, 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::>(); 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_high_volume_events() { let names = plugin_hook_event_names(); assert!(!names.contains(&"pane.output_changed")); assert!(!names.contains(&"layout.updated")); assert!(!names.contains(&"workspace.metadata_updated")); assert!(!names.contains(&"pane.updated")); assert!(names.contains(&"pane.moved")); } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)] pub struct EventEnvelope { pub event: EventKind, pub data: EventData, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub enum SubscriptionEventKind { #[serde(rename = "pane.output_matched")] PaneOutputMatched, #[serde(rename = "pane.agent_status_changed")] PaneAgentStatusChanged, #[serde(rename = "pane.scroll_changed")] ScrollChanged, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct SubscriptionEventEnvelope { pub event: SubscriptionEventKind, pub data: SubscriptionEventData, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(untagged)] pub enum SubscriptionEventData { PaneOutputMatched(PaneOutputMatchedEvent), PaneAgentStatusChanged(PaneAgentStatusChangedEvent), ScrollChanged(PaneScrollChangedEvent), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct PaneOutputMatchedEvent { pub pane_id: String, pub matched_line: String, pub read: PaneReadResult, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] 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, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub display_agent: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub state_labels: HashMap, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)] pub struct PaneScrollChangedEvent { pub pane_id: String, pub workspace_id: String, pub scroll: PaneScrollInfo, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum EventData { WorkspaceCreated { workspace: WorkspaceInfo, }, WorkspaceUpdated { workspace: WorkspaceInfo, }, WorkspaceMetadataUpdated { workspace: WorkspaceInfo, }, WorkspaceClosed { workspace_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] workspace: Option, }, WorkspaceRenamed { workspace_id: String, label: String, }, WorkspaceMoved { workspace_id: String, insert_index: usize, workspaces: Vec, }, WorkspaceReordered { workspace_ids: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] before_workspace_id: Option, workspaces: Vec, }, WorkspaceFocused { workspace_id: String, }, WorktreeCreated { workspace: WorkspaceInfo, worktree: WorktreeInfo, }, WorktreeOpened { workspace: WorkspaceInfo, worktree: WorktreeInfo, already_open: bool, }, WorktreeRemoved { workspace_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] workspace: Option, worktree: WorktreeInfo, forced: bool, }, TabCreated { tab: TabInfo, }, TabClosed { tab_id: String, workspace_id: String, }, TabRenamed { tab_id: String, workspace_id: String, label: String, }, TabMoved { tab_id: String, workspace_id: String, insert_index: usize, tabs: Vec, }, TabFocused { tab_id: String, workspace_id: String, }, PaneCreated { pane: PaneInfo, }, PaneClosed { pane_id: String, workspace_id: String, }, PaneUpdated { pane: PaneInfo, }, PaneFocused { pane_id: String, workspace_id: String, }, PaneMoved { previous_pane_id: String, previous_workspace_id: String, previous_tab_id: String, pane: Box, #[serde(default, skip_serializing_if = "Option::is_none")] created_workspace: Option, #[serde(default, skip_serializing_if = "Option::is_none")] created_tab: Option, #[serde(default, skip_serializing_if = "Option::is_none")] closed_workspace_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] closed_tab_id: Option, }, 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, #[serde(default, skip_serializing_if = "std::ops::Not::not")] released: bool, #[serde(default, skip_serializing_if = "Option::is_none")] final_status: Option, }, PaneAgentStatusChanged { pane_id: String, workspace_id: String, agent_status: AgentStatus, #[serde(default, skip_serializing_if = "Option::is_none")] agent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] display_agent: Option, #[serde(default, skip_serializing_if = "HashMap::is_empty")] state_labels: HashMap, }, LayoutUpdated { layout: super::panes::PaneLayoutSnapshot, }, }