diff --git a/crates/nyaterm-desktop/src/app_shell/mod.rs b/crates/nyaterm-desktop/src/app_shell/mod.rs index 167ff92e1..a832676c6 100644 --- a/crates/nyaterm-desktop/src/app_shell/mod.rs +++ b/crates/nyaterm-desktop/src/app_shell/mod.rs @@ -8,8 +8,7 @@ use nyaterm_core::AppRuntime; use crate::{ entities::{ - OverlayStore, RuntimeStore, SessionStore, StartupRestoreStore, UiStoreHandles, - WindowRuntimeStore, WorkspaceStore, + OverlayStore, RuntimeStore, StartupRestoreStore, UiStoreHandles, WindowRuntimeStore, }, features::NyaTermApp, }; @@ -20,8 +19,6 @@ pub struct AppShell { runtime: Entity, window_runtime: Entity, startup_restore: Entity, - workspace: Entity, - sessions: Entity, overlays: Entity, _subscriptions: Vec, } @@ -30,13 +27,9 @@ impl AppShell { pub fn new(runtime: AppRuntime, cx: &mut Context) -> Self { let runtime_store = cx.new(|_| RuntimeStore::new(runtime.clone())); let startup_restore = cx.new(|_| StartupRestoreStore::default()); - let workspace = cx.new(|_| WorkspaceStore::default()); - let sessions = cx.new(|_| SessionStore::default()); let overlays = cx.new(|_| OverlayStore::default()); let stores = UiStoreHandles { startup_restore: startup_restore.clone(), - workspace: workspace.clone(), - sessions: sessions.clone(), overlays: overlays.clone(), }; let app = cx.new(|cx| NyaTermApp::new(runtime, stores, cx)); @@ -51,8 +44,6 @@ impl AppShell { runtime: runtime_store, window_runtime: cx.new(|_| WindowRuntimeStore::default()), startup_restore, - workspace, - sessions, overlays, _subscriptions: subscriptions, } diff --git a/crates/nyaterm-desktop/src/entities/handles.rs b/crates/nyaterm-desktop/src/entities/handles.rs index 319a5e306..2bb91d074 100644 --- a/crates/nyaterm-desktop/src/entities/handles.rs +++ b/crates/nyaterm-desktop/src/entities/handles.rs @@ -1,11 +1,9 @@ use gpui::Entity; -use super::{OverlayStore, SessionStore, StartupRestoreStore, WorkspaceStore}; +use super::{OverlayStore, StartupRestoreStore}; #[derive(Clone)] pub struct UiStoreHandles { pub startup_restore: Entity, - pub workspace: Entity, - pub sessions: Entity, pub overlays: Entity, } diff --git a/crates/nyaterm-desktop/src/entities/mod.rs b/crates/nyaterm-desktop/src/entities/mod.rs index c0cb432db..6d88525d8 100644 --- a/crates/nyaterm-desktop/src/entities/mod.rs +++ b/crates/nyaterm-desktop/src/entities/mod.rs @@ -1,25 +1,22 @@ //! GPUI entity-state boundaries for the native shell. //! -//! During the migration, `NyaTermApp` and its feature-state structs remain the -//! default authoritative UI state. These stores mostly hold read-only snapshot -//! projections published from the app; explicitly migrated domains, such as the -//! quick switch state in `OverlayStore`, are the exceptions. +//! `NyaTermApp` and its feature-state structs are the authoritative UI state. +//! Every store here owns something the app does not: the app runtime and native +//! services, the window runtime pump, the startup-restore queue, and the quick +//! switch overlay state. Read-only snapshot projections used to live here too; +//! they were removed once it turned out nothing consumed them. mod handles; mod overlay; mod runtime; -mod session; mod startup_restore; mod window_runtime; -mod workspace; #[cfg(test)] mod tests; pub use handles::UiStoreHandles; -pub use overlay::{OverlaySnapshot, OverlayStore, QuickSwitchState}; +pub use overlay::{OverlayStore, QuickSwitchState}; pub use runtime::RuntimeStore; -pub use session::{SessionSnapshot, SessionStore}; pub use startup_restore::StartupRestoreStore; pub use window_runtime::WindowRuntimeStore; -pub use workspace::{WorkspaceSnapshot, WorkspaceStore}; diff --git a/crates/nyaterm-desktop/src/entities/overlay.rs b/crates/nyaterm-desktop/src/entities/overlay.rs index 8d6179b8f..544a2e6d5 100644 --- a/crates/nyaterm-desktop/src/entities/overlay.rs +++ b/crates/nyaterm-desktop/src/entities/overlay.rs @@ -1,44 +1,3 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OverlaySnapshot { - pub tab_actions_open: bool, - pub rename_open: bool, - pub color_picker_open: bool, - pub session_info_open: bool, - pub startup_command_open: bool, - pub temporary_ssh_link_open: bool, - pub multi_line_paste_open: bool, - pub terminal_actions_open: bool, - pub terminal_context_menu_open: bool, - pub action_link_menu_open: bool, - pub action_link_tooltip_open: bool, - pub command_suggestions_open: bool, - pub credential_suggestions_open: bool, - pub close_all_sessions_confirm_open: bool, - pub locked: bool, -} - -impl Default for OverlaySnapshot { - fn default() -> Self { - Self { - tab_actions_open: false, - rename_open: false, - color_picker_open: false, - session_info_open: false, - startup_command_open: false, - temporary_ssh_link_open: false, - multi_line_paste_open: false, - terminal_actions_open: false, - terminal_context_menu_open: false, - action_link_menu_open: false, - action_link_tooltip_open: false, - command_suggestions_open: false, - credential_suggestions_open: false, - close_all_sessions_confirm_open: false, - locked: false, - } - } -} - #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct QuickSwitchState { open: bool, @@ -68,7 +27,6 @@ impl QuickSwitchState { #[derive(Debug, Default)] pub struct OverlayStore { quick_switch: QuickSwitchState, - snapshot: Option, } impl OverlayStore { @@ -76,18 +34,6 @@ impl OverlayStore { &self.quick_switch } - pub fn snapshot(&self) -> Option<&OverlaySnapshot> { - self.snapshot.as_ref() - } - - pub fn replace_snapshot(&mut self, snapshot: OverlaySnapshot) -> bool { - if self.snapshot.as_ref() == Some(&snapshot) { - return false; - } - self.snapshot = Some(snapshot); - true - } - pub fn open_quick_switch(&mut self) -> bool { let next = QuickSwitchState { open: true, @@ -166,46 +112,4 @@ impl OverlayStore { self.quick_switch.selected_index = 0; true } - - pub fn set_tab_actions_open(&mut self, open: bool) -> bool { - let snapshot = self.snapshot.get_or_insert_with(OverlaySnapshot::default); - if snapshot.tab_actions_open == open { - return false; - } - snapshot.tab_actions_open = open; - true - } - - pub fn set_terminal_context_menu_open(&mut self, open: bool) -> bool { - let snapshot = self.snapshot.get_or_insert_with(OverlaySnapshot::default); - if snapshot.terminal_context_menu_open == open { - return false; - } - snapshot.terminal_context_menu_open = open; - if open { - snapshot.action_link_menu_open = false; - } - true - } - - pub fn set_action_link_menu_open(&mut self, open: bool) -> bool { - let snapshot = self.snapshot.get_or_insert_with(OverlaySnapshot::default); - if snapshot.action_link_menu_open == open { - return false; - } - snapshot.action_link_menu_open = open; - if open { - snapshot.terminal_context_menu_open = false; - } - true - } - - pub fn set_locked(&mut self, locked: bool) -> bool { - let snapshot = self.snapshot.get_or_insert_with(OverlaySnapshot::default); - if snapshot.locked == locked { - return false; - } - snapshot.locked = locked; - true - } } diff --git a/crates/nyaterm-desktop/src/entities/session.rs b/crates/nyaterm-desktop/src/entities/session.rs deleted file mode 100644 index 83bcd3c20..000000000 --- a/crates/nyaterm-desktop/src/entities/session.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::collections::HashSet; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SessionSnapshot { - pub active_session_id: Option, - pub ordered_session_ids: Vec, - pub live_session_ids: Vec, - pub metadata_count: usize, - pub terminal_view_count: usize, - pub pending_start_count: usize, - pub host_prompt_active: bool, - pub credential_prompt_active: bool, - pub zmodem_session_count: usize, -} - -#[derive(Debug, Default)] -pub struct SessionStore { - live_sessions: HashSet, - active_session_id: Option, - ordered_session_ids: Vec, - snapshot: Option, -} - -impl SessionStore { - pub fn live_session_count(&self) -> usize { - self.live_sessions.len() - } - - pub fn is_live(&self, session_id: &str) -> bool { - self.live_sessions.contains(session_id) - } - - pub fn active_session_id(&self) -> Option<&str> { - self.active_session_id.as_deref() - } - - pub fn ordered_session_ids(&self) -> &[String] { - &self.ordered_session_ids - } - - pub fn mark_live(&mut self, session_id: impl Into) { - self.live_sessions.insert(session_id.into()); - } - - pub fn mark_closed(&mut self, session_id: &str) { - self.live_sessions.remove(session_id); - } - - pub fn snapshot(&self) -> Option<&SessionSnapshot> { - self.snapshot.as_ref() - } - - pub fn replace_snapshot(&mut self, snapshot: SessionSnapshot) -> bool { - if self.snapshot.as_ref() == Some(&snapshot) { - return false; - } - self.active_session_id = snapshot.active_session_id.clone(); - self.ordered_session_ids = snapshot.ordered_session_ids.clone(); - self.live_sessions = snapshot.live_session_ids.iter().cloned().collect(); - self.snapshot = Some(snapshot); - true - } - - pub fn activate(&mut self, session_id: impl Into) -> bool { - let session_id = session_id.into(); - if self.active_session_id.as_deref() == Some(session_id.as_str()) { - return false; - } - self.active_session_id = Some(session_id.clone()); - let snapshot = self.snapshot.get_or_insert_with(SessionSnapshot::default); - snapshot.active_session_id = Some(session_id); - true - } - - pub fn set_ordered_session_ids(&mut self, ordered_session_ids: Vec) -> bool { - if self.ordered_session_ids == ordered_session_ids { - return false; - } - self.ordered_session_ids = ordered_session_ids.clone(); - let snapshot = self.snapshot.get_or_insert_with(SessionSnapshot::default); - snapshot.ordered_session_ids = ordered_session_ids; - true - } - - pub fn move_session_to_index(&mut self, session_id: &str, index: usize) -> bool { - let Some(current_index) = self - .ordered_session_ids - .iter() - .position(|id| id == session_id) - else { - return false; - }; - let session_id = self.ordered_session_ids.remove(current_index); - let index = index.min(self.ordered_session_ids.len()); - self.ordered_session_ids.insert(index, session_id); - let snapshot = self.snapshot.get_or_insert_with(SessionSnapshot::default); - snapshot.ordered_session_ids = self.ordered_session_ids.clone(); - true - } - - pub fn remove_session(&mut self, session_id: &str) -> bool { - let before_len = self.ordered_session_ids.len(); - self.ordered_session_ids.retain(|id| id != session_id); - self.live_sessions.remove(session_id); - if self.active_session_id.as_deref() == Some(session_id) { - self.active_session_id = None; - } - let changed = before_len != self.ordered_session_ids.len(); - if changed { - let snapshot = self.snapshot.get_or_insert_with(SessionSnapshot::default); - snapshot.ordered_session_ids = self.ordered_session_ids.clone(); - snapshot.live_session_ids = self.live_sessions.iter().cloned().collect(); - snapshot.active_session_id = self.active_session_id.clone(); - } - changed - } -} - -impl Default for SessionSnapshot { - fn default() -> Self { - Self { - active_session_id: None, - ordered_session_ids: Vec::new(), - live_session_ids: Vec::new(), - metadata_count: 0, - terminal_view_count: 0, - pending_start_count: 0, - host_prompt_active: false, - credential_prompt_active: false, - zmodem_session_count: 0, - } - } -} diff --git a/crates/nyaterm-desktop/src/entities/tests.rs b/crates/nyaterm-desktop/src/entities/tests.rs index d816d1e20..905300713 100644 --- a/crates/nyaterm-desktop/src/entities/tests.rs +++ b/crates/nyaterm-desktop/src/entities/tests.rs @@ -1,29 +1,4 @@ -use super::{ - OverlaySnapshot, OverlayStore, SessionSnapshot, SessionStore, StartupRestoreStore, - WindowRuntimeStore, WorkspaceSnapshot, WorkspaceStore, -}; - -#[test] -fn workspace_store_tracks_active_session_and_tab_order() { - let mut store = WorkspaceStore::default(); - store.activate_session("session-a"); - store.set_ordered_tab_roots(vec!["session-a".into(), "session-b".into()]); - - assert_eq!(store.active_session_id(), Some("session-a")); - assert_eq!(store.ordered_tab_roots(), ["session-a", "session-b"]); -} - -#[test] -fn session_store_tracks_live_sessions() { - let mut store = SessionStore::default(); - store.mark_live("session-a"); - store.mark_live("session-b"); - store.mark_closed("session-a"); - - assert!(!store.is_live("session-a")); - assert!(store.is_live("session-b")); - assert_eq!(store.live_session_count(), 1); -} +use super::{OverlayStore, StartupRestoreStore, WindowRuntimeStore}; #[test] fn window_runtime_store_starts_pump_once() { @@ -63,127 +38,6 @@ fn startup_restore_store_tracks_queue_and_completion() { assert!(!store.can_pump_queue(false)); } -#[test] -fn workspace_store_notifies_only_for_changed_snapshots() { - let mut store = WorkspaceStore::default(); - let snapshot = WorkspaceSnapshot { - active_session_id: Some("session-a".into()), - ordered_tab_roots: vec!["session-a".into()], - selected_nav: "Workspace".into(), - main_mode: "Workspace".into(), - active_left_panel: Some("Saved Connections".into()), - active_right_panel: None, - left_sidebar_collapsed: false, - right_inspector_collapsed: false, - workspace_split_active: false, - terminal_windows_active: false, - }; - - assert!(store.replace_snapshot(snapshot.clone())); - assert!(!store.replace_snapshot(snapshot)); - assert_eq!(store.active_session_id(), Some("session-a")); - assert_eq!(store.ordered_tab_roots(), ["session-a"]); -} - -#[test] -fn workspace_store_mutates_navigation_and_left_panel() { - let mut store = WorkspaceStore::default(); - - assert!(store.select_nav("Settings", "Page")); - assert!(!store.select_nav("Settings", "Page")); - assert!(store.open_left_panel("File Explorer")); - assert!(!store.open_left_panel("File Explorer")); - assert!(store.close_left_panel()); - assert!(!store.close_left_panel()); - - let snapshot = store.snapshot().expect("workspace snapshot"); - assert_eq!(snapshot.selected_nav, "Settings"); - assert_eq!(snapshot.main_mode, "Page"); - assert!(snapshot.left_sidebar_collapsed); - assert_eq!(snapshot.active_left_panel, None); -} - -#[test] -fn session_store_notifies_only_for_changed_snapshots() { - let mut store = SessionStore::default(); - let snapshot = SessionSnapshot { - active_session_id: Some("session-a".into()), - ordered_session_ids: vec!["session-a".into(), "session-b".into()], - live_session_ids: vec!["session-b".into()], - metadata_count: 1, - terminal_view_count: 1, - pending_start_count: 0, - host_prompt_active: false, - credential_prompt_active: false, - zmodem_session_count: 0, - }; - - assert!(store.replace_snapshot(snapshot.clone())); - assert!(!store.replace_snapshot(snapshot)); - assert!(!store.is_live("session-a")); - assert!(store.is_live("session-b")); -} - -#[test] -fn session_store_mutates_active_order_and_removal() { - let mut store = SessionStore::default(); - - assert!(store.set_ordered_session_ids(vec![ - "session-a".into(), - "session-b".into(), - "session-c".into(), - ])); - assert!(store.activate("session-b")); - assert!(!store.activate("session-b")); - assert!(store.move_session_to_index("session-c", 0)); - assert_eq!( - store.ordered_session_ids(), - ["session-c", "session-a", "session-b"] - ); - assert!(store.remove_session("session-b")); - assert_eq!(store.active_session_id(), None); - assert_eq!(store.ordered_session_ids(), ["session-c", "session-a"]); -} - -#[test] -fn overlay_store_notifies_only_for_changed_snapshots() { - let mut store = OverlayStore::default(); - let snapshot = OverlaySnapshot { - tab_actions_open: false, - rename_open: false, - color_picker_open: false, - session_info_open: false, - startup_command_open: false, - temporary_ssh_link_open: false, - multi_line_paste_open: false, - terminal_actions_open: false, - terminal_context_menu_open: false, - action_link_menu_open: false, - action_link_tooltip_open: false, - command_suggestions_open: false, - credential_suggestions_open: false, - close_all_sessions_confirm_open: false, - locked: false, - }; - - assert!(store.replace_snapshot(snapshot.clone())); - assert!(!store.replace_snapshot(snapshot)); - assert!(store.snapshot().is_some()); -} - -#[test] -fn overlay_store_mutates_open_close_and_menu_exclusion() { - let mut store = OverlayStore::default(); - - assert!(store.set_terminal_context_menu_open(true)); - assert!(store.set_action_link_menu_open(true)); - let snapshot = store.snapshot().expect("overlay snapshot"); - assert!(snapshot.action_link_menu_open); - assert!(!snapshot.terminal_context_menu_open); - assert!(store.set_locked(true)); - assert!(!store.set_locked(true)); -} - #[test] fn overlay_store_owns_quick_switch_state() { let mut store = OverlayStore::default(); diff --git a/crates/nyaterm-desktop/src/entities/workspace.rs b/crates/nyaterm-desktop/src/entities/workspace.rs deleted file mode 100644 index 02d83d06c..000000000 --- a/crates/nyaterm-desktop/src/entities/workspace.rs +++ /dev/null @@ -1,102 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WorkspaceSnapshot { - pub active_session_id: Option, - pub ordered_tab_roots: Vec, - pub selected_nav: String, - pub main_mode: String, - pub active_left_panel: Option, - pub active_right_panel: Option, - pub left_sidebar_collapsed: bool, - pub right_inspector_collapsed: bool, - pub workspace_split_active: bool, - pub terminal_windows_active: bool, -} - -#[derive(Debug, Default)] -pub struct WorkspaceStore { - active_session_id: Option, - ordered_tab_roots: Vec, - snapshot: Option, -} - -impl WorkspaceStore { - pub fn active_session_id(&self) -> Option<&str> { - self.active_session_id.as_deref() - } - - pub fn ordered_tab_roots(&self) -> &[String] { - &self.ordered_tab_roots - } - - pub fn activate_session(&mut self, session_id: impl Into) { - self.active_session_id = Some(session_id.into()); - } - - pub fn set_ordered_tab_roots(&mut self, roots: Vec) { - self.ordered_tab_roots = roots; - } - - pub fn snapshot(&self) -> Option<&WorkspaceSnapshot> { - self.snapshot.as_ref() - } - - pub fn replace_snapshot(&mut self, snapshot: WorkspaceSnapshot) -> bool { - if self.snapshot.as_ref() == Some(&snapshot) { - return false; - } - self.active_session_id = snapshot.active_session_id.clone(); - self.ordered_tab_roots = snapshot.ordered_tab_roots.clone(); - self.snapshot = Some(snapshot); - true - } - - pub fn select_nav(&mut self, nav: impl Into, main_mode: impl Into) -> bool { - let snapshot = self.snapshot.get_or_insert_with(WorkspaceSnapshot::default); - let nav = nav.into(); - let main_mode = main_mode.into(); - if snapshot.selected_nav == nav && snapshot.main_mode == main_mode { - return false; - } - snapshot.selected_nav = nav; - snapshot.main_mode = main_mode; - true - } - - pub fn open_left_panel(&mut self, panel: impl Into) -> bool { - let snapshot = self.snapshot.get_or_insert_with(WorkspaceSnapshot::default); - let panel = Some(panel.into()); - if snapshot.active_left_panel == panel && !snapshot.left_sidebar_collapsed { - return false; - } - snapshot.active_left_panel = panel; - snapshot.left_sidebar_collapsed = false; - true - } - - pub fn close_left_panel(&mut self) -> bool { - let snapshot = self.snapshot.get_or_insert_with(WorkspaceSnapshot::default); - if snapshot.active_left_panel.is_none() && snapshot.left_sidebar_collapsed { - return false; - } - snapshot.active_left_panel = None; - snapshot.left_sidebar_collapsed = true; - true - } -} - -impl Default for WorkspaceSnapshot { - fn default() -> Self { - Self { - active_session_id: None, - ordered_tab_roots: Vec::new(), - selected_nav: "Workspace".to_string(), - main_mode: "Workspace".to_string(), - active_left_panel: None, - active_right_panel: None, - left_sidebar_collapsed: true, - right_inspector_collapsed: true, - workspace_split_active: false, - terminal_windows_active: false, - } - } -} diff --git a/crates/nyaterm-desktop/src/features/app_state/types.rs b/crates/nyaterm-desktop/src/features/app_state/types.rs index 03856daac..647b7dd15 100644 --- a/crates/nyaterm-desktop/src/features/app_state/types.rs +++ b/crates/nyaterm-desktop/src/features/app_state/types.rs @@ -28,7 +28,6 @@ pub(in crate::features) struct TerminalRuntimeUiState { pub session_event_last_output_event_count: usize, pub session_event_last_drained_output_bytes: usize, pub last_session_start_drain_duration: Duration, - pub last_store_snapshot_publish_at: Option, pub last_pending_session_status_at: Option, pub last_terminal_resize_at: Option, pub last_terminal_frame_apply_at: Option, @@ -171,7 +170,6 @@ impl Default for TerminalRuntimeUiState { session_event_last_output_event_count: 0, session_event_last_drained_output_bytes: 0, last_session_start_drain_duration: Duration::ZERO, - last_store_snapshot_publish_at: None, last_pending_session_status_at: None, last_terminal_resize_at: None, last_terminal_frame_apply_at: None, diff --git a/crates/nyaterm-desktop/src/features/root.rs b/crates/nyaterm-desktop/src/features/root.rs index a0c0966b4..1d6cd968c 100644 --- a/crates/nyaterm-desktop/src/features/root.rs +++ b/crates/nyaterm-desktop/src/features/root.rs @@ -9,6 +9,29 @@ use gpui::{ const WALLPAPER_TILE_ELEMENT_LIMIT: usize = 8192; const WALLPAPER_TILE_MIN_SIZE: f32 = 8.; +/// Which overlays the root chrome should render this frame. +/// +/// Computed from `NyaTermApp` directly. This used to be read back from a +/// snapshot the same `Render` pass had just published into `OverlayStore`, +/// with a fallback that recomputed exactly these expressions. +struct OverlayFlags { + tab_actions_open: bool, + rename_open: bool, + color_picker_open: bool, + session_info_open: bool, + startup_command_open: bool, + temporary_ssh_link_open: bool, + multi_line_paste_open: bool, + terminal_actions_open: bool, + terminal_context_menu_open: bool, + action_link_menu_open: bool, + action_link_tooltip_open: bool, + command_suggestions_open: bool, + credential_suggestions_open: bool, + close_all_sessions_confirm_open: bool, + locked: bool, +} + impl NyaTermApp { pub(crate) fn start_after_window_open(&mut self, window: &mut Window, cx: &mut Context) { self.refresh_window_render_inputs(window, cx); @@ -22,7 +45,6 @@ impl NyaTermApp { } self.ensure_terminal_focus_reporting(window, cx); - self.publish_store_snapshots(cx); } fn root_chrome(&mut self, window: &mut Window, cx: &mut Context) -> Stateful
{ @@ -404,27 +426,23 @@ impl NyaTermApp { window: &mut Window, cx: &mut Context, ) -> Stateful
{ - let overlay = self - .stores - .overlays - .read_with(cx, |store, _| store.snapshot().cloned()) - .unwrap_or_else(|| crate::entities::OverlaySnapshot { - tab_actions_open: self.tab_actions_session_id.is_some(), - rename_open: self.rename_session_id.is_some(), - color_picker_open: self.color_picker_open, - session_info_open: self.session_info_open, - startup_command_open: self.startup_command_open, - temporary_ssh_link_open: self.temporary_ssh_link_open, - multi_line_paste_open: self.multi_line_paste.is_some(), - terminal_actions_open: self.terminal.menus.actions_open, - terminal_context_menu_open: self.terminal.menus.context_menu.is_some(), - action_link_menu_open: self.action_link_menu.is_some(), - action_link_tooltip_open: self.action_link_tooltip.is_some(), - command_suggestions_open: self.command_suggestions.is_some(), - credential_suggestions_open: self.credential_suggestions.is_some(), - close_all_sessions_confirm_open: self.close_all_sessions_confirm_open, - locked: self.is_locked, - }); + let overlay = OverlayFlags { + tab_actions_open: self.tab_actions_session_id.is_some(), + rename_open: self.rename_session_id.is_some(), + color_picker_open: self.color_picker_open, + session_info_open: self.session_info_open, + startup_command_open: self.startup_command_open, + temporary_ssh_link_open: self.temporary_ssh_link_open, + multi_line_paste_open: self.multi_line_paste.is_some(), + terminal_actions_open: self.terminal.menus.actions_open, + terminal_context_menu_open: self.terminal.menus.context_menu.is_some(), + action_link_menu_open: self.action_link_menu.is_some(), + action_link_tooltip_open: self.action_link_tooltip.is_some(), + command_suggestions_open: self.command_suggestions.is_some(), + credential_suggestions_open: self.credential_suggestions.is_some(), + close_all_sessions_confirm_open: self.close_all_sessions_confirm_open, + locked: self.is_locked, + }; let quick_switch_open = self.quick_switch_open(cx); let transfer_properties_open = self .transfer diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs index 89181afe7..be62d92b9 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/helpers.rs @@ -33,7 +33,6 @@ pub(super) const TERMINAL_PERF_HEARTBEAT_INTERVAL: Duration = Duration::from_sec pub(super) const RUNTIME_TICK_SLOW_THRESHOLD: Duration = Duration::from_millis(40); pub(super) const SESSION_EVENT_DRAIN_SLOW_TOTAL: Duration = Duration::from_millis(20); pub(super) const SESSION_EVENT_DRAIN_SLOW_CHUNK: Duration = Duration::from_millis(8); -pub(super) const STORE_SNAPSHOT_HEARTBEAT: Duration = Duration::from_secs(1); pub(super) const PENDING_SESSION_STILL_CONNECTING_AFTER: Duration = Duration::from_secs(15); pub(super) const PENDING_SESSION_STATUS_INTERVAL: Duration = Duration::from_secs(1); @@ -127,22 +126,10 @@ pub(super) fn diagnostic_log_due( }) } -pub(super) fn store_snapshot_publish_due(last_at: Option, now: Instant) -> bool { - diagnostic_log_due(last_at, now, STORE_SNAPSHOT_HEARTBEAT) -} - pub(super) fn terminal_cell_metrics_refresh_needed(metrics: Option<(f32, f32)>) -> bool { metrics.is_none() } -pub(super) fn should_publish_store_snapshots( - visual_dirty: bool, - output_pressure: bool, - heartbeat_due: bool, -) -> bool { - !output_pressure && (visual_dirty || heartbeat_due) -} - pub(super) fn pending_session_status_message( name: &str, auth_wait: Option<&PendingSessionAuthWait>, @@ -476,30 +463,6 @@ mod tests { )); } - #[test] - fn store_snapshot_publish_due_uses_low_frequency_heartbeat() { - let start = Instant::now(); - - assert!(store_snapshot_publish_due(None, start)); - assert!(!store_snapshot_publish_due( - Some(start), - start + STORE_SNAPSHOT_HEARTBEAT - Duration::from_millis(1) - )); - assert!(store_snapshot_publish_due( - Some(start), - start + STORE_SNAPSHOT_HEARTBEAT - )); - } - - #[test] - fn store_snapshot_publish_waits_until_output_pressure_clears() { - assert!(should_publish_store_snapshots(true, false, false)); - assert!(should_publish_store_snapshots(false, false, true)); - assert!(!should_publish_store_snapshots(false, false, false)); - assert!(!should_publish_store_snapshots(true, true, false)); - assert!(!should_publish_store_snapshots(false, true, true)); - } - #[test] fn terminal_cell_metrics_refreshes_only_after_invalidation() { assert!(terminal_cell_metrics_refresh_needed(None)); diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs index f98765a42..268c46493 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs @@ -6,7 +6,6 @@ use crate::models::{TerminalSearchMode, terminal_frame_search_result_is_current} mod bridge; mod helpers; mod planes; -mod publish; mod session_events; use crate::features::terminal::terminal_runtime::TERMINAL_INPUT_LATENCY_WINDOW; diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs index 68d44e273..26be06441 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/planes.rs @@ -235,20 +235,7 @@ impl NyaTermApp { self.terminal.view.runtime.pending_ui_notify = false; } let notify_duration = notify_started_at.elapsed(); - let publish_started_at = Instant::now(); - // Planes above do not drain more output; reuse one final pressure sample. let output_pressure = self.runtime_output_pressure_active(); - let heartbeat_due = store_snapshot_publish_due( - self.terminal.view.runtime.last_store_snapshot_publish_at, - publish_started_at, - ); - let should_publish_snapshots = - should_publish_store_snapshots(visual_dirty, output_pressure, heartbeat_due); - if should_publish_snapshots { - // Core workspace/session/overlay always; sideband stores only on heartbeat. - self.publish_store_snapshots_with_scope(cx, heartbeat_due || !visual_dirty); - } - let publish_duration = publish_started_at.elapsed(); let tick_duration = tick_started_at.elapsed(); if tick_duration >= RUNTIME_TICK_SLOW_THRESHOLD && self.should_log_slow_diagnostic("runtime_tick", Instant::now()) @@ -280,7 +267,6 @@ impl NyaTermApp { visual_runtime_ms = visual.duration.as_millis(), pending_session_status_ms = pending_session_status_duration.as_millis(), notify_ms = notify_duration.as_millis(), - publish_snapshots_ms = publish_duration.as_millis(), queued_events = self.terminal.view.runtime.session_event_queued_events, queued_output_bytes = self.terminal.view.runtime.session_event_queued_output_bytes, frame_command_count = self.terminal.view.frame_pipeline.queued_command_count(), @@ -307,7 +293,6 @@ impl NyaTermApp { .terminal_chrome_frame_notify_count, surface_paint_count = terminal_surface_paint_count(), notify_requested = visual_dirty, - publish_snapshots = should_publish_snapshots, "slow runtime tick" ); } @@ -392,8 +377,6 @@ impl NyaTermApp { terminal_frames_deferred_for_pacing = data.terminal_frame_apply_paced, visual_runtime_ms = visual.duration.as_millis(), notify_ms = notify_duration.as_millis(), - publish_snapshots_ms = publish_duration.as_millis(), - publish_snapshots = should_publish_snapshots, queued_session_events = self.terminal.view.runtime.session_event_queued_events, queued_session_output_bytes = self.terminal.view.runtime.session_event_queued_output_bytes, diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/publish.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/publish.rs deleted file mode 100644 index 3c5f7ee96..000000000 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/publish.rs +++ /dev/null @@ -1,201 +0,0 @@ -use super::*; - -use crate::models::MainMode; - -impl NyaTermApp { - pub(in crate::features) fn publish_store_snapshots(&mut self, cx: &mut Context) { - self.publish_store_snapshots_with_scope(cx, true); - } - - pub(super) fn publish_store_snapshots_with_scope( - &mut self, - cx: &mut Context, - include_sideband: bool, - ) { - // Source of truth: NyaTermApp / FeatureState. Entity stores receive - // one-way read-model snapshots here; they do not drive app mutations. - if !include_sideband && self.published_core_store_snapshots_are_current(cx) { - return; - } - self.terminal.view.runtime.last_store_snapshot_publish_at = Some(Instant::now()); - - let workspace = crate::entities::WorkspaceSnapshot { - active_session_id: self.active_session_id.clone(), - // Local tab-root order only; avoid SessionManager::list_sessions on the UI tick. - ordered_tab_roots: self - .session_order - .iter() - .filter(|session_id| !self.is_secondary_pane_session(session_id)) - .cloned() - .collect(), - selected_nav: self.selected_nav.label().to_string(), - main_mode: match self.main_mode { - MainMode::Workspace => "Workspace", - MainMode::Page => "Page", - } - .to_string(), - active_left_panel: self.active_left_panel.map(|item| item.label().to_string()), - active_right_panel: self.active_right_panel.map(|item| item.label().to_string()), - left_sidebar_collapsed: self.left_sidebar_collapsed, - right_inspector_collapsed: self.right_inspector_collapsed, - workspace_split_active: self.workspace_split.is_some(), - terminal_windows_active: self.terminal.windows.tree.is_some(), - }; - - // Prefer local metadata over SessionManager::list_sessions so publish - // never takes the transport session map lock on the UI tick. - let live_session_ids = self - .session_metadata - .iter() - .filter(|(_, metadata)| !metadata.disconnected) - .map(|(session_id, _)| session_id.clone()) - .collect(); - let pending_start_count = - self.pending_session_starts.len() + self.pending_saved_connection_queue.len(); - let sessions = crate::entities::SessionSnapshot { - active_session_id: self.active_session_id.clone(), - ordered_session_ids: self.session_order.clone(), - live_session_ids, - metadata_count: self.session_metadata.len(), - terminal_view_count: self.terminal.view.views.len(), - pending_start_count, - host_prompt_active: self.active_host_key_prompt.is_some(), - credential_prompt_active: self.active_credential_prompt.is_some() - || self.active_keyboard_interactive_prompt.is_some(), - zmodem_session_count: self.zmodem_sessions.len(), - }; - - let overlays = crate::entities::OverlaySnapshot { - tab_actions_open: self.tab_actions_session_id.is_some(), - rename_open: self.rename_session_id.is_some(), - color_picker_open: self.color_picker_open, - session_info_open: self.session_info_open, - startup_command_open: self.startup_command_open, - temporary_ssh_link_open: self.temporary_ssh_link_open, - multi_line_paste_open: self.multi_line_paste.is_some(), - terminal_actions_open: self.terminal.menus.actions_open, - terminal_context_menu_open: self.terminal.menus.context_menu.is_some(), - action_link_menu_open: self.action_link_menu.is_some(), - action_link_tooltip_open: self.action_link_tooltip.is_some(), - command_suggestions_open: self.command_suggestions.is_some(), - credential_suggestions_open: self.credential_suggestions.is_some(), - close_all_sessions_confirm_open: self.close_all_sessions_confirm_open, - locked: self.is_locked, - }; - - self.stores.workspace.update(cx, |store, cx| { - if store.replace_snapshot(workspace) { - cx.notify(); - } - }); - self.stores.sessions.update(cx, |store, cx| { - if store.replace_snapshot(sessions) { - cx.notify(); - } - }); - self.stores.overlays.update(cx, |store, cx| { - if store.replace_snapshot(overlays) { - cx.notify(); - } - }); - } - - fn published_core_store_snapshots_are_current(&self, cx: &mut Context) -> bool { - let workspace_store = self.stores.workspace.clone(); - let sessions_store = self.stores.sessions.clone(); - let overlays_store = self.stores.overlays.clone(); - let workspace_current = workspace_store.read_with(cx, |store, _| { - store - .snapshot() - .is_some_and(|snapshot| self.workspace_snapshot_is_current(snapshot)) - }); - let sessions_current = - sessions_store.read_with(cx, |store, _| self.session_store_snapshot_is_current(store)); - let overlays_current = overlays_store.read_with(cx, |store, _| { - store - .snapshot() - .is_some_and(|snapshot| self.overlay_snapshot_is_current(snapshot)) - }); - workspace_current && sessions_current && overlays_current - } - - fn workspace_snapshot_is_current(&self, snapshot: &crate::entities::WorkspaceSnapshot) -> bool { - let ordered_tab_roots_current = snapshot.ordered_tab_roots.len() - == self - .session_order - .iter() - .filter(|session_id| !self.is_secondary_pane_session(session_id)) - .count() - && snapshot - .ordered_tab_roots - .iter() - .map(String::as_str) - .eq(self - .session_order - .iter() - .filter(|session_id| !self.is_secondary_pane_session(session_id)) - .map(String::as_str)); - snapshot.active_session_id == self.active_session_id - && ordered_tab_roots_current - && snapshot.selected_nav == self.selected_nav.label() - && snapshot.main_mode - == match self.main_mode { - MainMode::Workspace => "Workspace", - MainMode::Page => "Page", - } - && snapshot.active_left_panel.as_deref() - == self.active_left_panel.map(|item| item.label()) - && snapshot.active_right_panel.as_deref() - == self.active_right_panel.map(|item| item.label()) - && snapshot.left_sidebar_collapsed == self.left_sidebar_collapsed - && snapshot.right_inspector_collapsed == self.right_inspector_collapsed - && snapshot.workspace_split_active == self.workspace_split.is_some() - && snapshot.terminal_windows_active == self.terminal.windows.tree.is_some() - } - - fn session_store_snapshot_is_current(&self, store: &crate::entities::SessionStore) -> bool { - let Some(snapshot) = store.snapshot() else { - return false; - }; - let live_session_count = self - .session_metadata - .values() - .filter(|metadata| !metadata.disconnected) - .count(); - snapshot.active_session_id == self.active_session_id - && store.ordered_session_ids() == self.session_order.as_slice() - && store.live_session_count() == live_session_count - && self - .session_metadata - .iter() - .filter(|(_, metadata)| !metadata.disconnected) - .all(|(session_id, _)| store.is_live(session_id)) - && snapshot.metadata_count == self.session_metadata.len() - && snapshot.terminal_view_count == self.terminal.view.views.len() - && snapshot.pending_start_count - == self.pending_session_starts.len() + self.pending_saved_connection_queue.len() - && snapshot.host_prompt_active == self.active_host_key_prompt.is_some() - && snapshot.credential_prompt_active - == (self.active_credential_prompt.is_some() - || self.active_keyboard_interactive_prompt.is_some()) - && snapshot.zmodem_session_count == self.zmodem_sessions.len() - } - - fn overlay_snapshot_is_current(&self, snapshot: &crate::entities::OverlaySnapshot) -> bool { - snapshot.tab_actions_open == self.tab_actions_session_id.is_some() - && snapshot.rename_open == self.rename_session_id.is_some() - && snapshot.color_picker_open == self.color_picker_open - && snapshot.session_info_open == self.session_info_open - && snapshot.startup_command_open == self.startup_command_open - && snapshot.temporary_ssh_link_open == self.temporary_ssh_link_open - && snapshot.multi_line_paste_open == self.multi_line_paste.is_some() - && snapshot.terminal_actions_open == self.terminal.menus.actions_open - && snapshot.terminal_context_menu_open == self.terminal.menus.context_menu.is_some() - && snapshot.action_link_menu_open == self.action_link_menu.is_some() - && snapshot.action_link_tooltip_open == self.action_link_tooltip.is_some() - && snapshot.command_suggestions_open == self.command_suggestions.is_some() - && snapshot.credential_suggestions_open == self.credential_suggestions.is_some() - && snapshot.close_all_sessions_confirm_open == self.close_all_sessions_confirm_open - && snapshot.locked == self.is_locked - } -} diff --git a/docs/architecture/gpui-migration-status.md b/docs/architecture/gpui-migration-status.md index b31a6a78d..50251a16d 100644 --- a/docs/architecture/gpui-migration-status.md +++ b/docs/architecture/gpui-migration-status.md @@ -14,10 +14,10 @@ Last updated from the working tree on 2026-07-26. | `#[path = "..."]` declarations in desktop | 0 | Cleared. Every directory is a real module; the boundary script fails on any new occurrence. | | `use super::*` imports in desktop | 355 | Includes indented test-module imports; historical migration debt, do not add new occurrences. | | `features/prelude.rs` rough exported-token count | 230 | Still a broad shared prelude; two hundred fifteen low-frequency transport/core/http/model exports are now explicit imports. | -| Entity Store structs | 7 | `Runtime`, `WindowRuntime`, `StartupRestore` own real state; `Overlay` owns quick switch and is read for rendering; `Workspace` and `Session` remain projections. | -| Snapshot structs | 3 | Workspace, session, overlay. | -| `replace_snapshot` methods | 3 | Down from nine; the six write-only domain projections are gone. | -| Store snapshot publish calls | 3 | Published from `features/shell/event_pump/publish.rs`. | +| Entity Store structs | 4 | `Runtime`, `WindowRuntime`, `StartupRestore`, `Overlay`. Each owns state the app does not. | +| Snapshot structs | 0 | Cleared. No store is a projection of `NyaTermApp` any more. | +| `replace_snapshot` methods | 0 | Cleared. | +| Store snapshot publish calls | 0 | `publish.rs` and the publish throttle are gone. | Large files currently over 4,000 lines: @@ -117,31 +117,31 @@ these as staged extraction candidates, not as formatting-only refactor targets. the three phases the bar actually has: `composer` (payload and caret), `options` (how it is interpreted and delivered, plus the menus that set those), and `progress` (in-flight send, cancellation, counters). -- The projection-only Entity Stores are gone. Tracing consumers showed the six - domain stores (`Ai`, `CloudSync`, `Connections`, `RemoteOps`, `Settings`, - `Transfer`) were write-only: published on every qualifying tick and read by - nothing outside `entities/`, so they cost a snapshot build plus a `cx.notify` - and returned nothing. They and their snapshots are deleted, along with six - accessors and one `SettingsTab::label` that existed only to feed them. - What remains has a reason to exist: `RuntimeStore`, `WindowRuntimeStore` and - `StartupRestoreStore` own real state, and `OverlayStore` both owns quick - switch authoritatively and is read by `root.rs` for overlay rendering. - `WorkspaceStore` and `SessionStore` are still projections, and are the honest - remaining question — their snapshots are read only by - `published_core_store_snapshots_are_current`, which decides whether to - republish them. That loop is self-referential, but it also gates the overlay - publish, so untangling it is a separate change. +- The Entity Store projection layer is gone entirely, in two steps. - Two findings for whoever takes that on. First, `OverlaySnapshot` is a - same-render round-trip: `Render` publishes it in its prologue, then - `overlay_host` reads it back and, when it is absent, falls back to an - expression that recomputes every field from the same `self` fields. The - fallback is proof the renderer never needed the store. It is not a staleness - bug today only because publish runs before `overlay_host` in the same pass. - Second, `AppShell` deliberately does not observe the stores — there is a - comment explaining that store-observe was amplifying each publish into an - extra shell paint — so the `cx.notify()` in each publish currently has no - subscriber at all. + First, the six domain stores (`Ai`, `CloudSync`, `Connections`, `RemoteOps`, + `Settings`, `Transfer`) turned out to be write-only: outside `entities/` they + appeared only in `app_shell::new`, and nothing read their snapshots. Every + qualifying tick built six snapshot structs, compared them, and called + `cx.notify()` for a reader that did not exist. + + Then `Workspace`, `Session` and `OverlaySnapshot` went the same way once the + loop was traced end to end. `Workspace` and `Session` were read only by + `published_core_store_snapshots_are_current`, which decided whether to + republish them — a closed loop. `OverlaySnapshot` was a same-render + round-trip: `Render` published it in its prologue and `overlay_host` read it + back a few calls later, with a fallback that recomputed every field from the + same `self` fields. `overlay_host` now computes those flags directly into a + local `OverlayFlags`, which is what the fallback already did. + + `AppShell` never observed the stores — a comment there records that + store-observe was amplifying each publish into an extra shell paint — so the + `cx.notify()` in every publish had no subscriber either. + + What remains owns something the app does not: `RuntimeStore` (app runtime and + native services), `WindowRuntimeStore` (the window runtime pump), + `StartupRestoreStore` (the restore queue) and `OverlayStore` (quick switch + state, authoritative since the earlier migration). - The connections UI state has started moving out of scattered `NyaTermApp` fields and into `ConnectionFeatureState`. - The current connections state split separates list UI, import UI, editor @@ -858,9 +858,9 @@ real module tree first, then the remaining steps actually enforce something. and semantic methods, covered by GPUI-free pure state tests. This is the highest-value remaining item; roughly 590 fields and 236 `impl NyaTermApp` blocks are the reason most desktop modules can reach most desktop state. -4. Done for the write-only stores. What is left is `WorkspaceStore` and - `SessionStore`: decide whether the publish/skip loop earns its keep, or - collapse it so the skip check reads app state directly. +4. Done. No store is a projection any more; the four that remain own real + state. If a future domain wants Entity ownership, migrate it authoritatively + rather than reintroducing a published read model. 5. Continue extracting schema-neutral internal modules from `core/storage.rs` and schema/protocol-neutral modules from `nyaterm-transport/src/lib.rs`, by domain rather than by individual type. Table definitions, serialized records,