mirror of
https://github.com/nyakang/nyaterm.git
synced 2026-10-04 08:00:50 +00:00
refactor(desktop): collapse the store snapshot publish loop
The remaining three snapshots turned out to be a closed loop, so the whole publish path goes. `WorkspaceSnapshot` and `SessionSnapshot` were read only by `published_core_store_snapshots_are_current`, which decided whether to republish them. `OverlaySnapshot` was a same-render round-trip: `Render` published it in its prologue and `overlay_host` read it back a few calls later, falling back — when the store was empty — to an expression that recomputed every field from the same `self` fields. `overlay_host` now evaluates those flags directly into a local `OverlayFlags`, which is exactly what the fallback did. None of it had an observer. `AppShell` deliberately does not observe the stores; the comment there records that store-observe was amplifying each publish into an extra shell paint. So every `cx.notify()` in the publish path was landing on nothing. Gone with it: `publish.rs`, the publish throttle (`should_publish_store_snapshots`, `store_snapshot_publish_due`, `STORE_SNAPSHOT_HEARTBEAT`, `last_store_snapshot_publish_at`), `WorkspaceStore`, `SessionStore`, and the snapshot half of `OverlayStore`. The runtime tick loses a per-tick snapshot build and comparison; the two slow-tick diagnostic fields that reported on it are dropped, and `output_pressure` is still computed for the rest. The four surviving stores each own something `NyaTermApp` does not: `Runtime` (app runtime and native services), `WindowRuntime` (the pump), `StartupRestore` (the restore queue) and `Overlay` (quick switch state). `entities/` drops from 959 to 412 lines and the projection layer is gone. Ten entity tests covered only the deleted stores; 548 tests become 538.
This commit is contained in:
@@ -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<RuntimeStore>,
|
||||
window_runtime: Entity<WindowRuntimeStore>,
|
||||
startup_restore: Entity<StartupRestoreStore>,
|
||||
workspace: Entity<WorkspaceStore>,
|
||||
sessions: Entity<SessionStore>,
|
||||
overlays: Entity<OverlayStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -30,13 +27,9 @@ impl AppShell {
|
||||
pub fn new(runtime: AppRuntime, cx: &mut Context<Self>) -> 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,
|
||||
}
|
||||
|
||||
@@ -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<StartupRestoreStore>,
|
||||
pub workspace: Entity<WorkspaceStore>,
|
||||
pub sessions: Entity<SessionStore>,
|
||||
pub overlays: Entity<OverlayStore>,
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<OverlaySnapshot>,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionSnapshot {
|
||||
pub active_session_id: Option<String>,
|
||||
pub ordered_session_ids: Vec<String>,
|
||||
pub live_session_ids: Vec<String>,
|
||||
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<String>,
|
||||
active_session_id: Option<String>,
|
||||
ordered_session_ids: Vec<String>,
|
||||
snapshot: Option<SessionSnapshot>,
|
||||
}
|
||||
|
||||
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<String>) {
|
||||
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<String>) -> 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<String>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceSnapshot {
|
||||
pub active_session_id: Option<String>,
|
||||
pub ordered_tab_roots: Vec<String>,
|
||||
pub selected_nav: String,
|
||||
pub main_mode: String,
|
||||
pub active_left_panel: Option<String>,
|
||||
pub active_right_panel: Option<String>,
|
||||
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<String>,
|
||||
ordered_tab_roots: Vec<String>,
|
||||
snapshot: Option<WorkspaceSnapshot>,
|
||||
}
|
||||
|
||||
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<String>) {
|
||||
self.active_session_id = Some(session_id.into());
|
||||
}
|
||||
|
||||
pub fn set_ordered_tab_roots(&mut self, roots: Vec<String>) {
|
||||
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<String>, main_mode: impl Into<String>) -> 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<String>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Instant>,
|
||||
pub last_pending_session_status_at: Option<Instant>,
|
||||
pub last_terminal_resize_at: Option<Instant>,
|
||||
pub last_terminal_frame_apply_at: Option<Instant>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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>) {
|
||||
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<Self>) -> Stateful<Div> {
|
||||
@@ -404,27 +426,23 @@ impl NyaTermApp {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Stateful<Div> {
|
||||
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
|
||||
|
||||
@@ -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<Instant>, 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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>) {
|
||||
self.publish_store_snapshots_with_scope(cx, true);
|
||||
}
|
||||
|
||||
pub(super) fn publish_store_snapshots_with_scope(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
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<Self>) -> 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user