aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-29 21:36:48 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-29 21:36:48 +0300
commit9150ed6268874d1f2a314b1ddbf531b66df1e105 (patch)
treeb8e8c35fb703b5b59babbdb8e4d732f906face41 /src
parentf36d8043594489508046ff0852989ec08f52188a (diff)
feat: add session snapshot api
Diffstat (limited to 'src')
-rw-r--r--src/api/schema.rs4
-rw-r--r--src/api/schema/response.rs4
-rw-r--r--src/api/schema/session.rs23
-rw-r--r--src/api/schema/tests.rs34
-rw-r--r--src/api/server.rs1
-rw-r--r--src/app/api.rs2
-rw-r--r--src/app/api/panes.rs6
-rw-r--r--src/app/api/session.rs112
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())
+ );
+ }
+}