diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 20:29:57 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 20:29:57 +0300 |
| commit | 0bab01573a781abda0ddf08046e7e4ce4568d696 (patch) | |
| tree | e3be41d834fab62d71cc8fa5b0f13f30260922e4 /src/server | |
| parent | 9a91a2ac41a5b2fbc2811922750a782b45115219 (diff) | |
refactor: route headless workspace tab mutations through runtime dispatch
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 334 |
1 files changed, 245 insertions, 89 deletions
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::<api::schema::ErrorResponse>(&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<u8>) -> 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<_>>(), + 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<_>>(), + 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<Vec<u8>>, |
