refactor: route headless workspace tab mutations through runtime dispatch
This commit is contained in:
parent
9a91a2ac41
commit
0bab01573a
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<bool>, 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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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 }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue