diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-29 21:36:48 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-29 21:36:48 +0300 |
| commit | 9150ed6268874d1f2a314b1ddbf531b66df1e105 (patch) | |
| tree | b8e8c35fb703b5b59babbdb8e4d732f906face41 /src | |
| parent | f36d8043594489508046ff0852989ec08f52188a (diff) | |
feat: add session snapshot api
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/schema.rs | 4 | ||||
| -rw-r--r-- | src/api/schema/response.rs | 4 | ||||
| -rw-r--r-- | src/api/schema/session.rs | 23 | ||||
| -rw-r--r-- | src/api/schema/tests.rs | 34 | ||||
| -rw-r--r-- | src/api/server.rs | 1 | ||||
| -rw-r--r-- | src/app/api.rs | 2 | ||||
| -rw-r--r-- | src/app/api/panes.rs | 6 | ||||
| -rw-r--r-- | src/app/api/session.rs | 112 |
8 files changed, 185 insertions, 1 deletions
diff --git a/src/api/schema.rs b/src/api/schema.rs index c3349c04..a404d56c 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -8,6 +8,7 @@ pub mod panes; pub mod plugins; pub mod response; pub mod server; +pub mod session; pub mod tabs; pub mod workspaces; pub mod worktrees; @@ -20,6 +21,7 @@ pub use panes::*; pub use plugins::*; pub use response::*; pub use server::*; +pub use session::*; pub use tabs::*; pub use workspaces::*; pub use worktrees::*; @@ -59,6 +61,8 @@ pub enum Method { ClientWindowTitleSet(ClientWindowTitleSetParams), #[serde(rename = "client.window_title.clear")] ClientWindowTitleClear(EmptyParams), + #[serde(rename = "session.snapshot")] + SessionSnapshot(EmptyParams), #[serde(rename = "workspace.create")] WorkspaceCreate(WorkspaceCreateParams), #[serde(rename = "workspace.list")] diff --git a/src/api/schema/response.rs b/src/api/schema/response.rs index 57ab215b..8ad41ec4 100644 --- a/src/api/schema/response.rs +++ b/src/api/schema/response.rs @@ -16,6 +16,7 @@ use super::plugins::{ PluginPaneInfo, }; use super::server::ServerCapabilities; +use super::session::SessionSnapshot; use super::tabs::TabInfo; use super::workspaces::WorkspaceInfo; use super::worktrees::{WorktreeInfo, WorktreeSourceInfo}; @@ -47,6 +48,9 @@ pub enum ResponseResult { #[serde(default)] capabilities: Option<ServerCapabilities>, }, + SessionSnapshot { + snapshot: Box<SessionSnapshot>, + }, WorkspaceInfo { workspace: WorkspaceInfo, }, diff --git a/src/api/schema/session.rs b/src/api/schema/session.rs new file mode 100644 index 00000000..b51f6969 --- /dev/null +++ b/src/api/schema/session.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +use super::agents::AgentInfo; +use super::panes::{PaneInfo, PaneLayoutSnapshot}; +use super::tabs::TabInfo; +use super::workspaces::WorkspaceInfo; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SessionSnapshot { + pub version: String, + pub protocol: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_workspace_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_tab_id: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub focused_pane_id: Option<String>, + pub workspaces: Vec<WorkspaceInfo>, + pub tabs: Vec<TabInfo>, + pub panes: Vec<PaneInfo>, + pub layouts: Vec<PaneLayoutSnapshot>, + pub agents: Vec<AgentInfo>, +} diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs index dd2004cf..5d61e972 100644 --- a/src/api/schema/tests.rs +++ b/src/api/schema/tests.rs @@ -377,6 +377,40 @@ fn success_response_round_trips() { } #[test] +fn session_snapshot_request_and_response_round_trip() { + let request = Request { + id: "req_snapshot".into(), + method: Method::SessionSnapshot(EmptyParams::default()), + }; + let json = serde_json::to_string(&request).unwrap(); + assert!(json.contains("\"method\":\"session.snapshot\"")); + let restored: Request = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, request); + + let response = SuccessResponse { + id: "req_snapshot".into(), + result: ResponseResult::SessionSnapshot { + snapshot: Box::new(SessionSnapshot { + version: "0.1.2".into(), + protocol: 15, + focused_workspace_id: None, + focused_tab_id: None, + focused_pane_id: None, + workspaces: Vec::new(), + tabs: Vec::new(), + panes: Vec::new(), + layouts: Vec::new(), + agents: Vec::new(), + }), + }, + }; + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("\"type\":\"session_snapshot\"")); + let restored: SuccessResponse = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, response); +} + +#[test] fn worktree_request_and_response_round_trip() { let request = Request { id: "req_worktree".into(), diff --git a/src/api/server.rs b/src/api/server.rs index 039ede12..d01f4199 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -281,6 +281,7 @@ fn api_method_name(method: &Method) -> &'static str { Method::NotificationShow(_) => "notification.show", Method::ClientWindowTitleSet(_) => "client.window_title.set", Method::ClientWindowTitleClear(_) => "client.window_title.clear", + Method::SessionSnapshot(_) => "session.snapshot", Method::WorkspaceCreate(_) => "workspace.create", Method::WorkspaceList(_) => "workspace.list", Method::WorkspaceGet(_) => "workspace.get", diff --git a/src/app/api.rs b/src/app/api.rs index cfff3619..b823a739 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -8,6 +8,7 @@ mod layouts; mod panes; pub(crate) mod plugins; mod responses; +mod session; mod tabs; mod workspaces; mod worktrees; @@ -836,6 +837,7 @@ impl App { }, ); } + Method::SessionSnapshot(_) => return self.handle_session_snapshot(request.id), Method::WorkspaceList(_) => return self.handle_workspace_list(request.id), Method::WorkspaceGet(target) => return self.handle_workspace_get(request.id, target), Method::WorkspaceCreate(params) => { diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs index b2f5030a..5723be1e 100644 --- a/src/app/api/panes.rs +++ b/src/app/api/panes.rs @@ -1634,7 +1634,11 @@ impl App { find_in_direction(source, direction.into(), &panes) } - fn pane_layout_snapshot(&self, ws_idx: usize, tab_idx: usize) -> Option<PaneLayoutSnapshot> { + pub(super) fn pane_layout_snapshot( + &self, + ws_idx: usize, + tab_idx: usize, + ) -> Option<PaneLayoutSnapshot> { let ws = self.state.workspaces.get(ws_idx)?; let tab = ws.tabs.get(tab_idx)?; let area = self.state.view.terminal_area; diff --git a/src/app/api/session.rs b/src/app/api/session.rs new file mode 100644 index 00000000..b2238faa --- /dev/null +++ b/src/app/api/session.rs @@ -0,0 +1,112 @@ +use crate::api::schema::{ResponseResult, SessionSnapshot}; +use crate::app::App; + +use super::responses::encode_success; + +impl App { + pub(super) fn handle_session_snapshot(&mut self, id: String) -> String { + encode_success( + id, + ResponseResult::SessionSnapshot { + snapshot: Box::new(self.session_snapshot()), + }, + ) + } + + fn session_snapshot(&self) -> SessionSnapshot { + let focused_workspace_id = self + .state + .active + .map(|ws_idx| self.public_workspace_id(ws_idx)); + let focused_tab_id = self.state.active.and_then(|ws_idx| { + let ws = self.state.workspaces.get(ws_idx)?; + self.public_tab_id(ws_idx, ws.active_tab) + }); + let focused_pane_id = self.state.active.and_then(|ws_idx| { + let ws = self.state.workspaces.get(ws_idx)?; + self.public_pane_id(ws_idx, ws.focused_pane_id()?) + }); + + let mut workspaces = Vec::new(); + let mut tabs = Vec::new(); + let mut layouts = Vec::new(); + for (ws_idx, ws) in self.state.workspaces.iter().enumerate() { + workspaces.push(self.workspace_info(ws_idx)); + for tab_idx in 0..ws.tabs.len() { + if let Some(tab) = self.tab_info(ws_idx, tab_idx) { + tabs.push(tab); + } + if let Some(layout) = self.pane_layout_snapshot(ws_idx, tab_idx) { + layouts.push(layout); + } + } + } + + SessionSnapshot { + version: crate::build_info::version(), + protocol: crate::protocol::PROTOCOL_VERSION, + focused_workspace_id, + focused_tab_id, + focused_pane_id, + workspaces, + tabs, + panes: self.collect_panes_for_workspace(None).unwrap_or_default(), + layouts, + agents: self.collect_agent_infos(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::api::schema::{EmptyParams, Method, ResponseResult, SuccessResponse}; + use crate::{config::Config, workspace::Workspace}; + + fn app_with_two_tabs() -> crate::app::App { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = crate::app::App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + let mut workspace = Workspace::test_new("snapshot"); + workspace.test_add_tab(None); + app.state.workspaces = vec![workspace]; + app.state.ensure_test_terminals(); + app.state.active = Some(0); + app + } + + #[test] + fn session_snapshot_bootstraps_runtime_resources() { + let mut app = app_with_two_tabs(); + let response = app.handle_api_request(crate::api::schema::Request { + id: "req_snapshot".into(), + method: Method::SessionSnapshot(EmptyParams::default()), + }); + + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::SessionSnapshot { snapshot } = success.result else { + panic!("expected session snapshot response"); + }; + assert_eq!(success.id, "req_snapshot"); + assert_eq!(snapshot.workspaces.len(), 1); + assert_eq!(snapshot.tabs.len(), 2); + assert_eq!(snapshot.panes.len(), 2); + assert_eq!(snapshot.layouts.len(), 2); + assert_eq!( + snapshot.focused_workspace_id.as_deref(), + Some(snapshot.workspaces[0].workspace_id.as_str()) + ); + assert_eq!( + snapshot.focused_tab_id.as_deref(), + Some(snapshot.tabs[0].tab_id.as_str()) + ); + assert_eq!( + snapshot.focused_pane_id.as_deref(), + Some(snapshot.panes[0].pane_id.as_str()) + ); + } +} |
