//! Wire protocol envelopes and error types for Galaxy local control. use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub use crate::catalog::{ ActionImplementationStatus, ActionKind, ActionMetadata, ActionParameterSpec, ActionResultSpec, PROTOCOL_VERSION, TargetScope, }; pub use crate::selectors::{ PaneSelector, PaneTarget, SessionSelector, SessionTarget, TabSelector, TabTarget, TargetSelector, WindowSelector, WindowTarget, }; /// Common layout direction values accepted by pane and tab mutations. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Direction { Left, Right, Up, Down, Previous, Next, } /// Tab type accepted by `tab.create` and `window.create`. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TabType { Terminal, Agent, Default, } /// Mode accepted by `tab.activate`. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TabActivationMode { Target, Previous, Next, Last, } /// Mode accepted by `tab.close`. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TabCloseMode { Target, Active, Others, RightOf, } /// Empty parameters for actions whose catalog parameter spec is `none`. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct EmptyParams {} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ActionNameParams { pub action: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct BindingNameParams { pub binding_name: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct BooleanValueParams { pub value: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ColorValueParams { pub color: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DirectionParams { pub direction: Direction, } /// Parameters for opening a file in Galaxy's app/editor state. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FileOpenParams { pub path: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub line: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub column: Option, #[serde(default)] pub new_tab: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct KeyParams { pub key: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct KeyValueParams { pub key: String, pub value: serde_json::Value, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NamespaceParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub namespace: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PageQueryParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub page: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub query: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct QueryParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub query: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct RenameParams { pub title: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ResizeParams { pub direction: Direction, #[serde(default, skip_serializing_if = "Option::is_none")] pub amount: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TabActivateParams { pub mode: TabActivationMode, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TabCloseParams { pub mode: TabCloseMode, } /// Parameters for `tab.create` and `window.create` shell/profile options. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TabCreateParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub tab_type: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub shell: Option, } /// Parameters for submitting a command to an idle terminal session. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TerminalExecuteParams { pub command: String, } /// Parameters for interrupting the current command in a terminal session. /// /// `block_id` is required as a compare-and-swap guard so a delayed request /// cannot interrupt a newer command. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TerminalInterruptParams { pub block_id: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TextParams { pub text: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ThemeNameParams { pub theme_name: String, } pub type KeybindingGetParams = BindingNameParams; pub type KeybindingListParams = EmptyParams; pub type SettingGetParams = KeyParams; pub type SettingListParams = NamespaceParams; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ActionListResult { pub actions: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ActionInspectResult { pub action: ActionMetadata, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ActiveTargetChain { #[serde(default, skip_serializing_if = "Option::is_none")] pub instance_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub window_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tab_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub pane_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub session_id: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ThemeSummary { pub name: String, pub is_current: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ThemeListResult { pub themes: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ThemeStateResult { pub name: String, pub follow_system_theme: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub light_theme: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dark_theme: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AppearanceStateResult { #[serde(default, skip_serializing_if = "Option::is_none")] pub theme: Option, pub follow_system_theme: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub light_theme: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub dark_theme: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub font_size: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub ui_zoom_percent: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SettingSummary { pub key: String, pub value: serde_json::Value, pub value_type: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SettingListResult { pub settings: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct SettingGetResult { pub setting: SettingSummary, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct KeybindingSummary { pub name: String, pub description: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub group: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub keystroke: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub normalized_keystroke: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct KeybindingListResult { pub keybindings: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct KeybindingGetResult { pub keybinding: KeybindingSummary, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SurfaceSummary { pub name: String, pub is_available: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub unavailable_reason: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SurfaceListResult { pub surfaces: Vec, } /// Snapshot of the active command block in a terminal session. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TerminalStatusResult { pub action: ActionKind, pub session_id: String, pub active_block_id: String, pub is_executing: bool, pub is_command_pending: bool, pub is_long_running: bool, pub is_agent_in_control: bool, pub is_idle: bool, /// Elapsed wall-clock time for the active running command. #[serde(default, skip_serializing_if = "Option::is_none")] pub running_for_ms: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub command_summary: Option, } /// Typed success payloads for catalog actions that need stable structured data. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ControlResult { Acknowledgement { action: ActionKind }, Metadata { data: serde_json::Value }, Content { data: serde_json::Value }, } /// Top-level request sent by a local-control client to a Galaxy instance. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RequestEnvelope { pub protocol_version: u32, pub request_id: Uuid, #[serde(default)] pub target: TargetSelector, pub action: Action, } impl RequestEnvelope { pub fn new(action: Action) -> Self { Self { protocol_version: PROTOCOL_VERSION, request_id: Uuid::new_v4(), target: TargetSelector::default(), action, } } } /// Requested action and action-specific JSON parameters. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Action { pub kind: ActionKind, #[serde(default)] pub params: serde_json::Value, } impl Action { pub fn new(kind: ActionKind) -> Self { Self { kind, params: serde_json::Value::Object(Default::default()), } } pub fn with_params(kind: ActionKind, params: T) -> Result { Ok(Self { kind, params: serde_json::to_value(params).map_err(|err| { ControlError::with_details( ErrorCode::InvalidParams, format!("failed to serialize {} parameters", kind.as_str()), err.to_string(), ) })?, }) } pub fn params_as(&self) -> Result { serde_json::from_value(self.params.clone()).map_err(|err| { ControlError::with_details( ErrorCode::InvalidParams, format!("failed to decode {} parameters", self.kind.as_str()), err.to_string(), ) }) } } /// Top-level response returned by a Galaxy instance for a control request. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ResponseEnvelope { pub protocol_version: u32, pub request_id: Uuid, pub response: ControlResponse, } impl ResponseEnvelope { pub fn ok(request_id: Uuid, data: serde_json::Value) -> Self { Self { protocol_version: PROTOCOL_VERSION, request_id, response: ControlResponse::Ok { data }, } } pub fn error(request_id: Uuid, error: ControlError) -> Self { Self { protocol_version: PROTOCOL_VERSION, request_id, response: ControlResponse::Error { error }, } } } /// Success or error payload for a control response. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "status", rename_all = "snake_case")] pub enum ControlResponse { Ok { data: serde_json::Value }, Error { error: ControlError }, } /// Error envelope used when a request cannot be decoded into a full request envelope. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ErrorResponseEnvelope { pub protocol_version: u32, pub error: ControlError, } impl ErrorResponseEnvelope { pub fn new(error: ControlError) -> Self { Self { protocol_version: PROTOCOL_VERSION, error, } } } /// Structured error returned by local-control protocol and transport layers. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)] #[error("{code}: {message}")] pub struct ControlError { pub code: ErrorCode, pub message: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub details: Option, } impl ControlError { pub fn new(code: ErrorCode, message: impl Into) -> Self { Self { code, message: message.into(), details: None, } } pub fn with_details( code: ErrorCode, message: impl Into, details: impl Into, ) -> Self { Self { code, message: message.into(), details: Some(details.into()), } } } /// Stable error code surfaced to CLI clients and automation. #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ErrorCode { LocalControlDisabled, UnauthorizedLocalClient, InsufficientPermissions, ProtocolVersionUnsupported, InvalidRequest, InvalidSelector, InvalidParams, NoInstance, AmbiguousInstance, AmbiguousTarget, StaleTarget, TargetStateConflict, MissingTarget, TransportUnavailable, BridgeUnavailable, UnsupportedAction, NotAllowlisted, Internal, } impl std::fmt::Display for ErrorCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = serde_json::to_value(self).map_err(|_| std::fmt::Error)?; let Some(value) = value.as_str() else { return Err(std::fmt::Error); }; f.write_str(value) } } #[cfg(test)] #[path = "protocol_tests.rs"] mod tests;