From 0bab01573a781abda0ddf08046e7e4ce4568d696 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 30 Jun 2026 20:29:57 +0300 Subject: [PATCH] refactor: route headless workspace tab mutations through runtime dispatch --- src/app/api.rs | 4 +- src/app/creation.rs | 5 + src/app/input/modal.rs | 14 +- src/app/input/navigate.rs | 22 +-- src/app/mod.rs | 6 +- src/app/runtime_mutations.rs | 30 ++-- src/app/worktrees.rs | 6 +- src/logging.rs | 1 + src/server/headless.rs | 334 +++++++++++++++++++++++++---------- 9 files changed, 287 insertions(+), 135 deletions(-) diff --git a/src/app/api.rs b/src/app/api.rs index fa6d1f6d..b1b5f4fd 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -27,7 +27,7 @@ enum RuntimeExitAction { } impl App { - pub(crate) fn dispatch_tui_api_request( + pub(crate) fn dispatch_api_request( &mut self, id: &'static str, method: crate::api::schema::Method, @@ -38,7 +38,7 @@ impl App { }) } - pub(crate) fn dispatch_tui_deferred_api_request( + pub(crate) fn dispatch_deferred_api_request( &mut self, id: &'static str, method: crate::api::schema::Method, diff --git a/src/app/creation.rs b/src/app/creation.rs index bdd1a43b..7131520e 100644 --- a/src/app/creation.rs +++ b/src/app/creation.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use crate::api::schema::{EventData, EventEnvelope, EventKind}; +#[cfg(test)] use tracing::error; use super::{ @@ -57,6 +58,7 @@ impl App { } /// Create a workspace with a real PTY (needs event_tx). + #[cfg(test)] pub(crate) fn create_workspace(&mut self) { let follow_cwd = self .workspace_creation_source() @@ -68,6 +70,7 @@ impl App { } } + #[cfg(test)] pub(crate) fn create_tab(&mut self) { let custom_name = self.state.requested_new_tab_name.take(); let active_before = self.state.active; @@ -109,6 +112,7 @@ impl App { } } + #[cfg(test)] pub(super) fn create_tab_with_options( &mut self, initial_cwd: PathBuf, @@ -154,6 +158,7 @@ impl App { self.create_workspace_with_launch_env(initial_cwd, focus, Vec::new()) } + #[cfg(test)] pub(crate) fn create_workspace_with_events( &mut self, initial_cwd: PathBuf, diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs index 51e19f78..daa84850 100644 --- a/src/app/input/modal.rs +++ b/src/app/input/modal.rs @@ -933,7 +933,7 @@ impl App { match self.state.mode { Mode::RenameWorkspace if !self.state.workspaces.is_empty() && !new_name.is_empty() => { let workspace_id = self.public_workspace_id(self.state.selected); - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.workspace.rename", crate::api::schema::Method::WorkspaceRename( crate::api::schema::WorkspaceRenameParams { @@ -950,7 +950,7 @@ impl App { } else { Some(new_name) }; - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.tab.create_named", crate::api::schema::Method::TabCreate(crate::api::schema::TabCreateParams { workspace_id: None, @@ -976,7 +976,7 @@ impl App { .is_some_and(|name| new_name == name); if !keep_auto_name { if let Some(tab_id) = self.public_tab_id(ws_idx, tab_idx) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.tab.rename", crate::api::schema::Method::TabRename( crate::api::schema::TabRenameParams { @@ -993,7 +993,7 @@ impl App { (self.state.active, self.state.rename_pane_target) { if let Some(pane_id) = self.public_pane_id(ws_idx, pane_id) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.rename", crate::api::schema::Method::PaneRename( crate::api::schema::PaneRenameParams { @@ -1058,7 +1058,7 @@ impl App { _ => None, }; if let Some(direction) = direction { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.resize", crate::api::schema::Method::PaneResize(crate::api::schema::PaneResizeParams { pane_id: None, @@ -1185,7 +1185,7 @@ impl App { Some("Clear pane name"), ) => { if let Some(pane_id) = self.public_pane_id(ws_idx, pane_id) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.clear_name", crate::api::schema::Method::PaneRename( crate::api::schema::PaneRenameParams { @@ -1211,7 +1211,7 @@ impl App { if let (Some(source_public_id), Some(target_public_id)) = (source_public_id, target_public_id) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.swap_exact", crate::api::schema::Method::PaneSwap(crate::api::schema::PaneSwapParams { pane_id: None, diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index cffa7bd6..35359cde 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -107,7 +107,7 @@ impl App { let previous_mode = self.state.mode; match action { NavigateAction::NewWorkspace => { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.key.workspace.create", crate::api::schema::Method::WorkspaceCreate( crate::api::schema::WorkspaceCreateParams { @@ -221,7 +221,7 @@ impl App { if self.state.prompt_new_tab_name { super::modal::open_new_tab_dialog(&mut self.state); } else { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.key.tab.create", crate::api::schema::Method::TabCreate( crate::api::schema::TabCreateParams { @@ -326,7 +326,7 @@ impl App { NavigateAction::Help => super::modal::open_keybind_help(&mut self.state), NavigateAction::Settings => super::settings::open_settings(&mut self.state), NavigateAction::ReloadConfig => { - self.dispatch_tui_api_request( + self.dispatch_runtime_mutation( "tui.server.reload_config", crate::api::schema::Method::ServerReloadConfig( crate::api::schema::EmptyParams::default(), @@ -364,7 +364,7 @@ impl App { pub(crate) fn move_workspace_via_api(&mut self, source_ws_idx: usize, insert_idx: usize) { let workspace_id = self.public_workspace_id(source_ws_idx); - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.workspace.move", crate::api::schema::Method::WorkspaceMove(crate::api::schema::WorkspaceMoveParams { workspace_id, @@ -403,7 +403,7 @@ impl App { let Some(tab_id) = self.public_tab_id(ws_idx, source_tab_idx) else { return; }; - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.tab.move", crate::api::schema::Method::TabMove(crate::api::schema::TabMoveParams { tab_id, @@ -428,7 +428,7 @@ impl App { self.focus_pane_internal_via_api(ws_idx, target); return; } - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.focus_direction", crate::api::schema::Method::PaneFocusDirection( crate::api::schema::PaneFocusDirectionParams { @@ -444,7 +444,7 @@ impl App { let source_pane_id = self.public_pane_id(ws_idx, source); let target_pane_id = self.public_pane_id(ws_idx, target); if let (Some(source_pane_id), Some(target_pane_id)) = (source_pane_id, target_pane_id) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.swap_exact", crate::api::schema::Method::PaneSwap(crate::api::schema::PaneSwapParams { pane_id: None, @@ -456,7 +456,7 @@ impl App { return; } } - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.swap", crate::api::schema::Method::PaneSwap(crate::api::schema::PaneSwapParams { pane_id: None, @@ -471,7 +471,7 @@ impl App { &mut self, direction: crate::api::schema::SplitDirection, ) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.split", crate::api::schema::Method::PaneSplit(crate::api::schema::PaneSplitParams { workspace_id: None, @@ -496,7 +496,7 @@ impl App { } pub(crate) fn zoom_focused_pane_via_api(&mut self) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.pane.zoom", crate::api::schema::Method::PaneZoom(crate::api::schema::PaneZoomParams { pane_id: None, @@ -506,7 +506,7 @@ impl App { } pub(crate) fn set_split_ratio_via_api(&mut self, path: Vec, ratio: f32) { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.layout.set_split_ratio", crate::api::schema::Method::LayoutSetSplitRatio( crate::api::schema::LayoutSetSplitRatioParams { diff --git a/src/app/mod.rs b/src/app/mod.rs index 04de4c84..713e0b02 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -887,7 +887,7 @@ impl App { if self.state.request_new_workspace { self.state.request_new_workspace = false; - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.workspace.create", crate::api::schema::Method::WorkspaceCreate( crate::api::schema::WorkspaceCreateParams { @@ -904,7 +904,7 @@ impl App { if self.state.request_new_tab { self.state.request_new_tab = false; let label = self.state.requested_new_tab_name.take(); - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.tab.create", crate::api::schema::Method::TabCreate(crate::api::schema::TabCreateParams { workspace_id: None, @@ -928,7 +928,7 @@ impl App { } if let Some(cwd) = self.state.request_new_workspace_cwd.take() { - self.dispatch_tui_runtime_mutation( + self.dispatch_runtime_mutation( "tui.workspace.create_cwd", crate::api::schema::Method::WorkspaceCreate( crate::api::schema::WorkspaceCreateParams { diff --git a/src/app/runtime_mutations.rs b/src/app/runtime_mutations.rs index fae52d60..4a94593c 100644 --- a/src/app/runtime_mutations.rs +++ b/src/app/runtime_mutations.rs @@ -3,20 +3,16 @@ use crate::api::schema::{Method, PaneTarget, TabTarget, WorkspaceTarget}; use super::App; impl App { - pub(crate) fn dispatch_tui_runtime_mutation( - &mut self, - id: &'static str, - method: Method, - ) -> String { - self.dispatch_tui_api_request(id, method) + pub(crate) fn dispatch_runtime_mutation(&mut self, id: &'static str, method: Method) -> String { + self.dispatch_api_request(id, method) } - pub(crate) fn dispatch_tui_deferred_runtime_mutation( + pub(crate) fn dispatch_deferred_runtime_mutation( &mut self, id: &'static str, method: Method, ) -> Option { - self.dispatch_tui_deferred_api_request(id, method) + self.dispatch_deferred_api_request(id, method) } pub(crate) fn runtime_workspace_focus( @@ -24,10 +20,7 @@ impl App { id: &'static str, workspace_id: String, ) -> String { - self.dispatch_tui_runtime_mutation( - id, - Method::WorkspaceFocus(WorkspaceTarget { workspace_id }), - ) + self.dispatch_runtime_mutation(id, Method::WorkspaceFocus(WorkspaceTarget { workspace_id })) } pub(crate) fn runtime_workspace_close( @@ -35,25 +28,22 @@ impl App { id: &'static str, workspace_id: String, ) -> String { - self.dispatch_tui_runtime_mutation( - id, - Method::WorkspaceClose(WorkspaceTarget { workspace_id }), - ) + self.dispatch_runtime_mutation(id, Method::WorkspaceClose(WorkspaceTarget { workspace_id })) } pub(crate) fn runtime_tab_focus(&mut self, id: &'static str, tab_id: String) -> String { - self.dispatch_tui_runtime_mutation(id, Method::TabFocus(TabTarget { tab_id })) + self.dispatch_runtime_mutation(id, Method::TabFocus(TabTarget { tab_id })) } pub(crate) fn runtime_tab_close(&mut self, id: &'static str, tab_id: String) -> String { - self.dispatch_tui_runtime_mutation(id, Method::TabClose(TabTarget { tab_id })) + self.dispatch_runtime_mutation(id, Method::TabClose(TabTarget { tab_id })) } pub(crate) fn runtime_pane_focus(&mut self, id: &'static str, pane_id: String) -> String { - self.dispatch_tui_runtime_mutation(id, Method::PaneFocus(PaneTarget { pane_id })) + self.dispatch_runtime_mutation(id, Method::PaneFocus(PaneTarget { pane_id })) } pub(crate) fn runtime_pane_close(&mut self, id: &'static str, pane_id: String) -> String { - self.dispatch_tui_runtime_mutation(id, Method::PaneClose(PaneTarget { pane_id })) + self.dispatch_runtime_mutation(id, Method::PaneClose(PaneTarget { pane_id })) } } diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs index ae195929..78448957 100644 --- a/src/app/worktrees.rs +++ b/src/app/worktrees.rs @@ -590,7 +590,7 @@ impl App { let workspace_id = create.source_workspace_id.clone(); let checkout_path = create.checkout_path.display().to_string(); - let immediate_response = self.dispatch_tui_deferred_runtime_mutation( + let immediate_response = self.dispatch_deferred_runtime_mutation( "tui.worktree.create", crate::api::schema::Method::WorktreeCreate(crate::api::schema::WorktreeCreateParams { workspace_id: Some(workspace_id), @@ -720,7 +720,7 @@ impl App { }; let source_workspace_id = open.source_workspace_id.clone(); - let response = self.dispatch_tui_runtime_mutation( + let response = self.dispatch_runtime_mutation( "tui.worktree.open", crate::api::schema::Method::WorktreeOpen(crate::api::schema::WorktreeOpenParams { workspace_id: Some(source_workspace_id), @@ -763,7 +763,7 @@ impl App { remove.error = None; let workspace_id = remove.workspace_id.clone(); let force = remove.force_confirmation; - let immediate_response = self.dispatch_tui_deferred_runtime_mutation( + let immediate_response = self.dispatch_deferred_runtime_mutation( "tui.worktree.remove", crate::api::schema::Method::WorktreeRemove(crate::api::schema::WorktreeRemoveParams { workspace_id, diff --git a/src/logging.rs b/src/logging.rs index 31dba911..b3f2f834 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -254,6 +254,7 @@ pub(crate) fn workspace_renamed(workspace_id: &str) { ); } +#[cfg(test)] pub(crate) fn tab_created(workspace_id: &str, tab_id: &str, root_pane_id: u32) { tracing::info!( event = "tab.create", diff --git a/src/server/headless.rs b/src/server/headless.rs index 5798913d..5521f59f 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -501,92 +501,9 @@ impl HeadlessServer { crate::render_prof::event("full_render_cause.scheduled_tasks"); } - // Handle deferred requests. - if self.app.state.request_complete_onboarding { - self.app.state.request_complete_onboarding = false; - self.app.open_settings_from_onboarding(); + if self.handle_deferred_requests_headless() { needs_render = true; needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_onboarding"); - } - - if self.app.state.request_new_workspace { - self.app.state.request_new_workspace = false; - self.app.create_workspace(); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_new_workspace"); - } - - if self.app.state.request_new_tab { - self.app.state.request_new_tab = false; - self.app.create_tab(); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_new_tab"); - } - - if let Some(ws_idx) = self.app.state.request_new_linked_worktree.take() { - self.app.open_new_linked_worktree_dialog(ws_idx); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_worktree_dialog"); - } - - if let Some(ws_idx) = self.app.state.request_open_existing_worktree.take() { - self.app.open_existing_worktree_dialog(ws_idx); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_worktree_dialog"); - } - - if let Some(cwd) = self.app.state.request_new_workspace_cwd.take() { - if let Err(err) = self.app.create_workspace_with_events(cwd, true) { - error!(err = %err, "failed to create workspace at requested cwd"); - self.app.state.mode = app::Mode::Navigate; - } - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_workspace_cwd"); - } - - if let Some(ws_idx) = self.app.state.request_remove_linked_worktree.take() { - self.app.open_remove_linked_worktree_confirmation(ws_idx); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_worktree_dialog"); - } - - if self.app.state.request_submit_worktree_create { - self.app.state.request_submit_worktree_create = false; - self.app.submit_worktree_create_via_api(); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_worktree_submit"); - } - - if self.app.state.request_submit_worktree_open { - self.app.state.request_submit_worktree_open = false; - self.app.submit_worktree_open_via_api(); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_worktree_submit"); - } - - if self.app.state.request_submit_worktree_remove { - self.app.state.request_submit_worktree_remove = false; - self.app.submit_worktree_remove_via_api(); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.deferred_worktree_submit"); - } - - if self.app.state.request_reload_config { - self.app.state.request_reload_config = false; - self.reload_server_config(true); - needs_render = true; - needs_full_render = true; - crate::render_prof::event("full_render_cause.config_reload"); } if latest_app_client(&self.clients).is_some() && self.app.ensure_default_workspace() { @@ -690,6 +607,164 @@ impl HeadlessServer { Ok(()) } + fn handle_deferred_requests_headless(&mut self) -> bool { + let mut needs_render = false; + + if self.app.state.request_complete_onboarding { + self.app.state.request_complete_onboarding = false; + self.app.open_settings_from_onboarding(); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_onboarding"); + } + + if self.app.state.request_new_workspace { + self.app.state.request_new_workspace = false; + let response = self.dispatch_headless_runtime_mutation( + "headless.workspace.create", + crate::api::schema::Method::WorkspaceCreate( + crate::api::schema::WorkspaceCreateParams { + cwd: None, + focus: true, + label: None, + env: Default::default(), + }, + ), + ); + if let Err(error) = response { + error!( + code = %error.code, + message = %error.message, + "failed to create workspace" + ); + } + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_new_workspace"); + } + + if self.app.state.request_new_tab { + self.app.state.request_new_tab = false; + let label = self.app.state.requested_new_tab_name.take(); + let response = self.dispatch_headless_runtime_mutation( + "headless.tab.create", + crate::api::schema::Method::TabCreate(crate::api::schema::TabCreateParams { + workspace_id: None, + cwd: None, + focus: true, + label, + env: Default::default(), + }), + ); + if let Err(error) = response { + error!( + code = %error.code, + message = %error.message, + "failed to create tab" + ); + } + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_new_tab"); + } + + if let Some(ws_idx) = self.app.state.request_new_linked_worktree.take() { + self.app.open_new_linked_worktree_dialog(ws_idx); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_worktree_dialog"); + } + + if let Some(ws_idx) = self.app.state.request_open_existing_worktree.take() { + self.app.open_existing_worktree_dialog(ws_idx); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_worktree_dialog"); + } + + if let Some(cwd) = self.app.state.request_new_workspace_cwd.take() { + let response = self.dispatch_headless_runtime_mutation( + "headless.workspace.create_cwd", + crate::api::schema::Method::WorkspaceCreate( + crate::api::schema::WorkspaceCreateParams { + cwd: Some(cwd.display().to_string()), + focus: true, + label: None, + env: Default::default(), + }, + ), + ); + if let Err(error) = response { + error!( + code = %error.code, + message = %error.message, + "failed to create workspace at requested cwd" + ); + self.app.state.mode = app::Mode::Navigate; + } + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_workspace_cwd"); + } + + if let Some(ws_idx) = self.app.state.request_remove_linked_worktree.take() { + self.app.open_remove_linked_worktree_confirmation(ws_idx); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_worktree_dialog"); + } + + if self.app.state.request_submit_worktree_create { + self.app.state.request_submit_worktree_create = false; + self.app.submit_worktree_create_via_api(); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_worktree_submit"); + } + + if self.app.state.request_submit_worktree_open { + self.app.state.request_submit_worktree_open = false; + self.app.submit_worktree_open_via_api(); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_worktree_submit"); + } + + if self.app.state.request_submit_worktree_remove { + self.app.state.request_submit_worktree_remove = false; + self.app.submit_worktree_remove_via_api(); + needs_render = true; + crate::render_prof::event("full_render_cause.deferred_worktree_submit"); + } + + if self.app.state.request_reload_config { + self.app.state.request_reload_config = false; + self.reload_server_config(true); + needs_render = true; + crate::render_prof::event("full_render_cause.config_reload"); + } + + needs_render + } + + fn dispatch_headless_runtime_mutation( + &mut self, + id: &'static str, + method: api::schema::Method, + ) -> Result<(), api::schema::ErrorBody> { + let (respond_to, response_rx) = std::sync::mpsc::channel(); + self.handle_api_request_with_shutdown_check_inner( + api::ApiRequestMessage { + request: api::schema::Request { + id: id.to_string(), + method, + }, + respond_to, + }, + true, + ); + match response_rx.recv_timeout(Duration::from_secs(5)) { + Ok(response) => serde_json::from_str::(&response) + .map(|response| Err(response.error)) + .unwrap_or(Ok(())), + Err(err) => Err(api::schema::ErrorBody { + code: "internal_error".into(), + message: format!("headless runtime mutation response failed: {err}"), + }), + } + } + fn allocate_activity_stamp(&mut self) -> u64 { let stamp = self.next_activity_stamp; self.next_activity_stamp = self.next_activity_stamp.saturating_add(1); @@ -2673,6 +2748,14 @@ impl HeadlessServer { /// trigger internal events that may set toast state or would normally /// play sounds — in headless mode we forward these to clients instead. fn handle_api_request_with_shutdown_check(&mut self, msg: api::ApiRequestMessage) -> bool { + self.handle_api_request_with_shutdown_check_inner(msg, false) + } + + fn handle_api_request_with_shutdown_check_inner( + &mut self, + msg: api::ApiRequestMessage, + skip_default_workspace_for_request: bool, + ) -> bool { if self.shutting_down { // During shutdown, respond with server_unavailable. let response = serde_json::to_string(&api::schema::ErrorResponse { @@ -2734,10 +2817,11 @@ impl HeadlessServer { } let mut changed = api::request_changes_ui(&msg.request); - let skip_default_workspace = matches!( - &msg.request.method, - api::schema::Method::ServerStop(_) | api::schema::Method::ServerLiveHandoff(_) - ); + let skip_default_workspace = skip_default_workspace_for_request + || matches!( + &msg.request.method, + api::schema::Method::ServerStop(_) | api::schema::Method::ServerLiveHandoff(_) + ); changed |= self.drain_all_internal_events_with_forwarding(); // Capture toast and effective pane states before the API call so we can @@ -4049,9 +4133,13 @@ mod tests { use crate::protocol::CursorState; fn test_headless_server() -> HeadlessServer { + test_headless_server_with_event_hub(api::EventHub::default()) + } + + fn test_headless_server_with_event_hub(event_hub: api::EventHub) -> HeadlessServer { let config = crate::config::Config::default(); let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); - let mut app = crate::app::App::new(&config, true, None, api_rx, api::EventHub::default()); + let mut app = crate::app::App::new(&config, true, None, api_rx, event_hub); app.state.local_sound_playback = false; app.local_terminal_notifications = false; @@ -4113,6 +4201,12 @@ mod tests { } } + fn shutdown_test_runtimes(server: &mut HeadlessServer) { + for (_, runtime) in server.app.terminal_runtimes.drain() { + runtime.shutdown(); + } + } + fn read_server_message(bytes: Vec) -> ServerMessage { let mut cursor = std::io::Cursor::new(bytes); protocol::read_message(&mut cursor, MAX_FRAME_SIZE).expect("decode server message") @@ -4183,6 +4277,68 @@ mod tests { assert!(server.app.event_rx.try_recv().is_err()); } + #[tokio::test] + async fn headless_deferred_workspace_create_uses_runtime_events() { + let event_hub = api::EventHub::default(); + let mut server = test_headless_server_with_event_hub(event_hub.clone()); + + server.app.state.request_new_workspace = true; + + assert!(server.handle_deferred_requests_headless()); + assert!(!server.app.state.request_new_workspace); + assert_eq!( + event_hub + .events_after(0) + .into_iter() + .map(|(_, event)| event.event) + .collect::>(), + vec![ + api::schema::EventKind::WorkspaceCreated, + api::schema::EventKind::TabCreated, + api::schema::EventKind::PaneCreated, + ] + ); + shutdown_test_runtimes(&mut server); + } + + #[tokio::test] + async fn headless_deferred_named_tab_create_uses_runtime_events() { + let event_hub = api::EventHub::default(); + let mut server = test_headless_server_with_event_hub(event_hub.clone()); + server + .app + .create_workspace_with_options(std::env::temp_dir(), true) + .unwrap(); + let after_setup = event_hub.current_sequence(); + + server.app.state.request_new_tab = true; + server.app.state.requested_new_tab_name = Some("ops".into()); + + assert!(server.handle_deferred_requests_headless()); + assert!(!server.app.state.request_new_tab); + assert_eq!(server.app.state.requested_new_tab_name, None); + let events = event_hub.events_after(after_setup); + assert_eq!( + events + .iter() + .map(|(_, event)| event.event) + .collect::>(), + vec![ + api::schema::EventKind::TabCreated, + api::schema::EventKind::PaneCreated, + ] + ); + let tab_created = events + .iter() + .find_map(|(_, event)| match &event.data { + api::schema::EventData::TabCreated { tab } => Some(tab), + _ => None, + }) + .expect("tab created event"); + assert_eq!(tab_created.label, "ops"); + shutdown_test_runtimes(&mut server); + } + fn test_client_writer() -> ( ClientWriter, std::sync::mpsc::Receiver>,