diff options
| author | zhugehyuk <icedac@users.noreply.github.com> | 2026-05-25 03:28:33 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-24 21:28:33 +0300 |
| commit | 43c6ca3a5dbdcefc6c6dc29be9ce81d990baf39e (patch) | |
| tree | 73be12f81129dab324ea08901c733a6a56ec0e61 /src/persist/snapshot.rs | |
| parent | 5ce738cf5397d0d8a50ed1aa6335297073cc0f0f (diff) | |
feat: persist pane history across restarts (#248)
refs #217
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/persist/snapshot.rs')
| -rw-r--r-- | src/persist/snapshot.rs | 210 |
1 files changed, 209 insertions, 1 deletions
diff --git a/src/persist/snapshot.rs b/src/persist/snapshot.rs index a223513d..36df83b1 100644 --- a/src/persist/snapshot.rs +++ b/src/persist/snapshot.rs @@ -31,6 +31,24 @@ pub struct SessionSnapshot { } #[derive(Serialize, Deserialize)] +pub struct SessionHistorySnapshot { + /// Format version follows the matching session snapshot version. + #[serde(default)] + pub version: u32, + pub workspaces: Vec<WorkspaceHistorySnapshot>, +} + +#[derive(Serialize, Deserialize)] +pub struct WorkspaceHistorySnapshot { + pub tabs: Vec<TabHistorySnapshot>, +} + +#[derive(Serialize, Deserialize)] +pub struct TabHistorySnapshot { + pub panes: HashMap<u32, PaneHistorySnapshot>, +} + +#[derive(Serialize, Deserialize)] pub struct WorkspaceSnapshot { #[serde(default)] pub id: Option<String>, @@ -89,6 +107,12 @@ pub struct PaneAgentSessionSnapshot { pub value: String, } +#[derive(Serialize, Deserialize)] +pub struct PaneHistorySnapshot { + pub ansi: String, + pub lines: usize, +} + /// Serializable BSP tree. #[derive(Serialize, Deserialize)] pub enum LayoutSnapshot { @@ -334,6 +358,52 @@ fn capture_tab( } } +/// Capture pane screen history separately from the structural session snapshot. +pub fn capture_history( + workspaces: &[Workspace], + terminal_runtimes: &TerminalRuntimeRegistry, +) -> SessionHistorySnapshot { + SessionHistorySnapshot { + version: SNAPSHOT_VERSION, + workspaces: workspaces + .iter() + .map(|workspace| WorkspaceHistorySnapshot { + tabs: workspace + .tabs + .iter() + .map(|tab| TabHistorySnapshot { + panes: capture_tab_history(tab, terminal_runtimes), + }) + .collect(), + }) + .collect(), + } +} + +fn capture_tab_history( + tab: &crate::workspace::Tab, + terminal_runtimes: &TerminalRuntimeRegistry, +) -> HashMap<u32, PaneHistorySnapshot> { + let mut panes = HashMap::new(); + for (id, pane) in &tab.panes { + if let Some(history) = capture_pane_history(Some(pane), terminal_runtimes) { + panes.insert(id.raw(), history); + } + } + panes +} + +fn capture_pane_history( + pane: Option<&crate::pane::PaneState>, + terminal_runtimes: &TerminalRuntimeRegistry, +) -> Option<PaneHistorySnapshot> { + let ansi = terminal_runtimes + .get(&pane?.attached_terminal_id)? + .snapshot_history()?; + let lines = ansi.lines().count(); + Some(PaneHistorySnapshot { ansi, lines }) +} + pub(super) fn capture_node(node: &Node) -> LayoutSnapshot { match node { Node::Pane(id) => LayoutSnapshot::Pane(id.raw()), @@ -365,6 +435,18 @@ pub(super) fn parse_snapshot(content: &str) -> Result<SessionSnapshot, String> { migrate_snapshot(raw) } +pub(super) fn parse_history_snapshot(content: &str) -> Result<SessionHistorySnapshot, String> { + let snapshot = + serde_json::from_str::<SessionHistorySnapshot>(content).map_err(|e| e.to_string())?; + if snapshot.version > SNAPSHOT_VERSION { + return Err(format!( + "history snapshot version {} is newer than supported {}", + snapshot.version, SNAPSHOT_VERSION + )); + } + Ok(snapshot) +} + pub(super) fn snapshot_file_version(content: &str) -> Option<u32> { serde_json::from_str::<RawSessionSnapshot>(content) .ok() @@ -412,10 +494,17 @@ mod tests { fn capture_from_state(state: &AppState) -> SessionSnapshot { let terminal_runtimes = TerminalRuntimeRegistry::new(); + capture_from_state_with_runtimes(state, &terminal_runtimes) + } + + fn capture_from_state_with_runtimes( + state: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + ) -> SessionSnapshot { capture( &state.workspaces, &state.terminals, - &terminal_runtimes, + terminal_runtimes, state.active, state.selected, state.agent_panel_scope, @@ -425,6 +514,13 @@ mod tests { ) } + fn capture_history_from_state_with_runtimes( + state: &AppState, + terminal_runtimes: &TerminalRuntimeRegistry, + ) -> SessionHistorySnapshot { + capture_history(&state.workspaces, terminal_runtimes) + } + fn root_split_ratio(tab: &TabSnapshot) -> Option<f32> { match &tab.layout { LayoutSnapshot::Split { ratio, .. } => Some(*ratio), @@ -601,6 +697,42 @@ mod tests { } #[test] + fn old_pane_snapshot_with_embedded_history_is_ignored() { + let json = serde_json::json!({ + "version": SNAPSHOT_VERSION, + "workspaces": [{ + "id": "wtest", + "identity_cwd": "/tmp", + "tabs": [{ + "layout": { "Pane": 0 }, + "panes": { + "0": { + "cwd": "/tmp", + "history": { + "ansi": "legacy-secret", + "lines": 1 + } + } + }, + "zoomed": false, + "focused": 0, + "root_pane": 0 + }], + "active_tab": 0 + }], + "active": 0, + "selected": 0 + }) + .to_string(); + + let restored = parse_snapshot(&json).unwrap(); + + let encoded = serde_json::to_string(&restored).unwrap(); + assert!(!encoded.contains("legacy-secret")); + assert!(!encoded.contains("\"history\"")); + } + + #[test] fn legacy_workspace_snapshot_migrates_to_single_tab() { let snap = parse_snapshot(session_fixture("legacy-pre-tabs-v2")).unwrap(); let ws = &snap.workspaces[0]; @@ -801,6 +933,82 @@ mod tests { assert_eq!(tab.panes[&second.raw()].cwd, PathBuf::from("/tmp/herdr")); } + #[tokio::test] + async fn capture_contract_tracks_pane_history_from_runtime() { + let state = state_with_workspaces(&["one"]); + let root = state.workspaces[0].tabs[0].root_pane; + let terminal_id = state.workspaces[0].tabs[0].panes[&root] + .attached_terminal_id + .clone(); + let mut terminal_runtimes = TerminalRuntimeRegistry::new(); + terminal_runtimes.insert( + terminal_id, + crate::terminal::TerminalRuntime::test_with_scrollback_bytes( + 20, + 3, + 4096, + b"alpha\r\nbeta\r\ngamma\r\n", + ), + ); + + let snapshot = capture_from_state_with_runtimes(&state, &terminal_runtimes); + let encoded = serde_json::to_string(&snapshot).unwrap(); + assert!(!encoded.contains("alpha")); + assert!(!encoded.contains("\"history\"")); + + let history_snapshot = capture_history_from_state_with_runtimes(&state, &terminal_runtimes); + let history = &history_snapshot.workspaces[0].tabs[0].panes[&root.raw()]; + + assert!(history.ansi.contains("alpha")); + assert!(history.ansi.contains("gamma")); + assert!(history.lines >= 3); + } + + #[tokio::test] + async fn capture_contract_tracks_history_for_each_pane() { + let mut state = state_with_workspaces(&["one"]); + let first = state.workspaces[0].tabs[0].root_pane; + let second = state.workspaces[0].test_split(Direction::Horizontal); + let first_terminal_id = state.workspaces[0].tabs[0].panes[&first] + .attached_terminal_id + .clone(); + let second_terminal_id = state.workspaces[0].tabs[0].panes[&second] + .attached_terminal_id + .clone(); + let mut terminal_runtimes = TerminalRuntimeRegistry::new(); + terminal_runtimes.insert( + first_terminal_id, + crate::terminal::TerminalRuntime::test_with_scrollback_bytes( + 20, + 3, + 4096, + b"first-pane-history\r\n", + ), + ); + terminal_runtimes.insert( + second_terminal_id, + crate::terminal::TerminalRuntime::test_with_scrollback_bytes( + 20, + 3, + 4096, + b"second-pane-history\r\n", + ), + ); + + let snapshot = capture_from_state_with_runtimes(&state, &terminal_runtimes); + let encoded = serde_json::to_string(&snapshot).unwrap(); + assert!(!encoded.contains("first-pane-history")); + assert!(!encoded.contains("second-pane-history")); + + let history_snapshot = capture_history_from_state_with_runtimes(&state, &terminal_runtimes); + let tab = &history_snapshot.workspaces[0].tabs[0]; + let first_history = &tab.panes[&first.raw()]; + let second_history = &tab.panes[&second.raw()]; + + assert!(first_history.ansi.contains("first-pane-history")); + assert!(second_history.ansi.contains("second-pane-history")); + } + #[test] fn capture_contract_tracks_hook_authority_agent_session() { let mut state = state_with_workspaces(&["one"]); |
