aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-29 18:57:13 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-29 18:57:13 +0300
commit97f78226bf1f34dcdef164291a9c50fddf448c6e (patch)
tree20f8793644f314521c6e064bef8ba4b25576524b /src
parent1a4e94e57e181379e2ed42d63a49fbc56ed1dba4 (diff)
fix: route tui modal mutations through api authority
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs1
-rw-r--r--src/app/api/worktrees.rs1
-rw-r--r--src/app/input/mod.rs10
-rw-r--r--src/app/input/modal.rs73
-rw-r--r--src/app/input/mouse.rs98
-rw-r--r--src/app/mod.rs53
-rw-r--r--src/app/worktrees.rs94
-rw-r--r--src/server/headless.rs6
8 files changed, 278 insertions, 58 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 56f6e789..d791da03 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -1587,6 +1587,7 @@ impl AppState {
}
}
+ #[cfg(test)]
pub fn resize_pane(&mut self, direction: NavDirection) {
if let Some(first) = self.view.pane_infos.first() {
let area = self
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs
index 111d50cf..69f43c1b 100644
--- a/src/app/api/worktrees.rs
+++ b/src/app/api/worktrees.rs
@@ -611,6 +611,7 @@ impl App {
});
}
+ #[cfg(test)]
pub(crate) fn emit_worktree_opened_for_workspace(&mut self, ws_idx: usize, already_open: bool) {
let Some(worktree) = self.worktree_info_for_workspace(ws_idx) else {
return;
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index 31a99c23..164c4dd4 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -45,8 +45,7 @@ mod terminal;
pub(crate) use self::{
modal::{
- handle_confirm_close_key, handle_context_menu_key, handle_global_menu_key,
- handle_keybind_help_key, handle_navigator_key, handle_rename_key, handle_resize_key,
+ handle_global_menu_key, handle_keybind_help_key, handle_navigator_key,
insert_navigator_search_text, insert_rename_input_text,
},
navigate::terminal_direct_navigation_action,
@@ -318,6 +317,13 @@ impl App {
MouseAction::SetSplitRatio { path, ratio } => {
self.set_split_ratio_via_api(path, ratio)
}
+ MouseAction::RenameModal(action) => {
+ self.apply_rename_mouse_action_via_api(action)
+ }
+ MouseAction::ConfirmCloseAccept => self.confirm_close_accept_via_api(),
+ MouseAction::ContextMenu { menu, idx } => {
+ self.apply_context_menu_action_via_api(menu, idx)
+ }
}
}
}
diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs
index 2c3f4e4a..e63cdb70 100644
--- a/src/app/input/modal.rs
+++ b/src/app/input/modal.rs
@@ -1,5 +1,7 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
-use ratatui::layout::{Direction, Rect};
+#[cfg(test)]
+use ratatui::layout::Direction;
+use ratatui::layout::Rect;
use crate::{
app::{
@@ -426,6 +428,7 @@ pub(super) const SETTINGS_ACTIONS: &[ModalActionSpec<ModalAction>] = &[
},
];
+#[cfg(test)]
pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
match action {
ModalAction::Save => {
@@ -588,12 +591,7 @@ fn delete_rename_input_word(state: &mut AppState) {
}
}
-pub(crate) fn handle_rename_key(state: &mut AppState, key: KeyEvent) {
- if let Some(action) = modal_action_from_key(&key, RENAME_ACTIONS) {
- apply_rename_action(state, action);
- return;
- }
-
+fn handle_rename_edit_key(state: &mut AppState, key: KeyEvent) {
match key.code {
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
clear_rename_input(state);
@@ -618,6 +616,17 @@ pub(crate) fn handle_rename_key(state: &mut AppState, key: KeyEvent) {
}
}
+#[cfg(test)]
+pub(crate) fn handle_rename_key(state: &mut AppState, key: KeyEvent) {
+ if let Some(action) = modal_action_from_key(&key, RENAME_ACTIONS) {
+ apply_rename_action(state, action);
+ return;
+ }
+
+ handle_rename_edit_key(state, key);
+}
+
+#[cfg(test)]
pub(crate) fn handle_resize_key(state: &mut AppState, raw_key: TerminalKey) {
let key = raw_key.as_key_event();
if key.code == KeyCode::Esc
@@ -646,6 +655,7 @@ pub(super) fn open_confirm_close(state: &mut AppState) {
state.mode = Mode::ConfirmClose;
}
+#[cfg(test)]
pub(super) fn confirm_close_accept(state: &mut AppState) {
state.close_selected_workspace();
if state.workspaces.is_empty() {
@@ -659,6 +669,7 @@ pub(super) fn confirm_close_cancel(state: &mut AppState) {
state.mode = Mode::Navigate;
}
+#[cfg(test)]
pub(crate) fn handle_confirm_close_key(state: &mut AppState, key: KeyEvent) {
match modal_action_from_key(&key, CONFIRM_CLOSE_ACTIONS) {
Some(ModalAction::Confirm) => confirm_close_accept(state),
@@ -667,6 +678,7 @@ pub(crate) fn handle_confirm_close_key(state: &mut AppState, key: KeyEvent) {
}
}
+#[cfg(test)]
pub(super) fn apply_context_menu_action(
state: &mut AppState,
terminal_runtimes: &mut crate::terminal::TerminalRuntimeRegistry,
@@ -870,6 +882,7 @@ pub(super) fn apply_context_menu_action(
}
}
+#[cfg(test)]
pub(crate) fn handle_context_menu_key(
state: &mut AppState,
terminal_runtimes: &mut crate::terminal::TerminalRuntimeRegistry,
@@ -903,19 +916,11 @@ pub(crate) fn handle_context_menu_key(
impl App {
pub(crate) fn handle_rename_key_via_api(&mut self, key: KeyEvent) {
if let Some(action) = modal_action_from_key(&key, RENAME_ACTIONS) {
- match action {
- ModalAction::Save => self.save_rename_modal_via_api(),
- ModalAction::Clear => {
- self.state.name_input.clear();
- self.state.name_input_replace_on_type = false;
- }
- ModalAction::Cancel => cancel_rename_modal(&mut self.state),
- _ => {}
- }
+ self.apply_rename_mouse_action_via_api(action);
return;
}
- handle_rename_key(&mut self.state, key);
+ handle_rename_edit_key(&mut self.state, key);
}
fn save_rename_modal_via_api(&mut self) {
@@ -1006,6 +1011,30 @@ impl App {
cancel_rename_modal(&mut self.state);
}
+ pub(super) fn apply_rename_mouse_action_via_api(&mut self, action: ModalAction) {
+ match action {
+ ModalAction::Save => self.save_rename_modal_via_api(),
+ ModalAction::Clear => {
+ self.state.name_input.clear();
+ self.state.name_input_replace_on_type = false;
+ }
+ ModalAction::Cancel => cancel_rename_modal(&mut self.state),
+ _ => {}
+ }
+ }
+
+ pub(super) fn confirm_close_accept_via_api(&mut self) {
+ let ws_idx = self.state.selected;
+ if ws_idx < self.state.workspaces.len() {
+ self.close_workspace_idx_via_api(ws_idx);
+ }
+ self.state.mode = if self.state.active.is_some() {
+ Mode::Terminal
+ } else {
+ Mode::Navigate
+ };
+ }
+
pub(crate) fn handle_resize_key_via_api(&mut self, raw_key: TerminalKey) {
let key = raw_key.as_key_event();
if key.code == KeyCode::Esc
@@ -1043,15 +1072,7 @@ impl App {
pub(crate) fn handle_confirm_close_key_via_api(&mut self, key: KeyEvent) {
match modal_action_from_key(&key, CONFIRM_CLOSE_ACTIONS) {
Some(ModalAction::Confirm) => {
- let ws_idx = self.state.selected;
- if ws_idx < self.state.workspaces.len() {
- self.close_workspace_idx_via_api(ws_idx);
- }
- self.state.mode = if self.state.active.is_some() {
- Mode::Terminal
- } else {
- Mode::Navigate
- };
+ self.confirm_close_accept_via_api();
}
Some(ModalAction::Cancel) => confirm_close_cancel(&mut self.state),
_ => {}
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index 71c0cfbd..7f9f9cde 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -18,8 +18,7 @@ use crate::{
use super::WheelRouting;
use super::{
modal::{
- apply_context_menu_action, apply_global_menu_action, apply_rename_action,
- confirm_close_accept, confirm_close_cancel, global_menu_actions, leave_modal,
+ apply_global_menu_action, confirm_close_cancel, global_menu_actions, leave_modal,
modal_action_from_buttons, open_global_menu, open_new_tab_dialog, ModalAction,
},
settings::SettingsAction,
@@ -52,6 +51,12 @@ pub(super) enum MouseAction {
path: Vec<bool>,
ratio: f32,
},
+ RenameModal(ModalAction),
+ ConfirmCloseAccept,
+ ContextMenu {
+ menu: ContextMenuState,
+ idx: usize,
+ },
}
enum MobileMouseResult {
@@ -234,7 +239,9 @@ impl AppState {
(cancel, ModalAction::Cancel),
],
) {
- Some(ModalAction::Confirm) => confirm_close_accept(self),
+ Some(ModalAction::Confirm) => {
+ return Some(MouseAction::ConfirmCloseAccept);
+ }
Some(ModalAction::Cancel) | None => confirm_close_cancel(self),
_ => {}
}
@@ -396,15 +403,14 @@ impl AppState {
)
})
.unwrap_or(ModalAction::Cancel);
- apply_rename_action(self, action);
- return None;
+ return Some(MouseAction::RenameModal(action));
}
if self.mode == Mode::ContextMenu {
let item_idx = self.context_menu_item_at(mouse.column, mouse.row);
if let Some(menu) = self.context_menu.take() {
if let Some(idx) = item_idx {
- apply_context_menu_action(self, terminal_runtimes, menu, idx);
+ return Some(MouseAction::ContextMenu { menu, idx });
} else {
leave_modal(self);
}
@@ -900,8 +906,26 @@ impl AppState {
if self.on_tab_bar(mouse.column, mouse.row) =>
{
match mouse.kind {
- MouseEventKind::ScrollUp => self.previous_tab(),
- MouseEventKind::ScrollDown => self.next_tab(),
+ MouseEventKind::ScrollUp => {
+ if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
+ if !ws.tabs.is_empty() {
+ let prev = if ws.active_tab == 0 {
+ ws.tabs.len() - 1
+ } else {
+ ws.active_tab - 1
+ };
+ return Some(MouseAction::FocusTab { tab_idx: prev });
+ }
+ }
+ }
+ MouseEventKind::ScrollDown => {
+ if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
+ if !ws.tabs.is_empty() {
+ let next = (ws.active_tab + 1) % ws.tabs.len();
+ return Some(MouseAction::FocusTab { tab_idx: next });
+ }
+ }
+ }
_ => {}
}
}
@@ -1814,10 +1838,10 @@ mod tests {
use ratatui::layout::{Direction, Rect};
use super::super::{
- app_for_mouse_test, capture_snapshot, handle_context_menu_key, mouse, numbered_lines_bytes,
- root_layout_ratio,
+ app_for_mouse_test, capture_snapshot, mouse, numbered_lines_bytes, root_layout_ratio,
};
use super::*;
+ use crate::app::input::modal::handle_context_menu_key;
use crate::{
app::state::{ContextMenuKind, ContextMenuState, MenuListState, Mode, ViewLayout},
detect::{Agent, AgentState},
@@ -2450,6 +2474,31 @@ mod tests {
}
#[test]
+ fn clicking_rename_save_submits_workspace_rename_through_api_path() {
+ let mut app = app_for_mouse_test();
+ app.state.workspaces = vec![Workspace::test_new("old")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::RenameWorkspace;
+ app.state.name_input = "new".into();
+
+ crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 24));
+ let inner = app.state.rename_modal_inner().unwrap();
+ let (save, _, _) = crate::ui::rename_button_rects(inner);
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ save.x,
+ save.y,
+ ));
+
+ assert_eq!(app.state.workspaces[0].custom_name.as_deref(), Some("new"));
+ assert!(app.event_hub.events_after(0).iter().any(|(_, event)| {
+ matches!(event.event, crate::api::schema::EventKind::WorkspaceRenamed)
+ }));
+ }
+
+ #[test]
fn clicking_open_worktree_row_selects_and_requests_open() {
let mut app = app_for_mouse_test();
app.state.mode = Mode::OpenExistingWorktree;
@@ -2615,6 +2664,35 @@ mod tests {
assert_eq!(app.state.workspaces[0].display_name(), "a");
}
+ #[test]
+ fn clicking_context_menu_close_routes_through_api_path() {
+ let mut app = app_for_mouse_test();
+ app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.confirm_close = false;
+ app.state.context_menu = Some(ContextMenuState {
+ kind: ContextMenuKind::Workspace { ws_idx: 1 },
+ x: 2,
+ y: 2,
+ list: MenuListState::new(1),
+ });
+ app.state.mode = Mode::ContextMenu;
+
+ let menu = app.state.context_menu_rect().unwrap();
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ menu.x + 2,
+ menu.y + 2,
+ ));
+
+ assert_eq!(app.state.workspaces.len(), 1);
+ assert_eq!(app.state.workspaces[0].display_name(), "a");
+ assert!(app.event_hub.events_after(0).iter().any(|(_, event)| {
+ matches!(event.event, crate::api::schema::EventKind::WorkspaceClosed)
+ }));
+ }
+
#[cfg(unix)]
#[tokio::test]
async fn keyboard_context_menu_split_keeps_new_runtime() {
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 65863fd8..2860664f 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1619,7 +1619,7 @@ impl App {
self.handle_copy_mode_key(key);
}
Mode::RenameWorkspace | Mode::RenameTab | Mode::RenamePane => {
- input::handle_rename_key(&mut self.state, key_event);
+ self.handle_rename_key_via_api(key_event);
}
Mode::NewLinkedWorktree => {
self.handle_worktree_create_key(key_event);
@@ -1631,17 +1631,13 @@ impl App {
self.handle_worktree_remove_key(key_event);
}
Mode::Resize => {
- input::handle_resize_key(&mut self.state, key);
+ self.handle_resize_key_via_api(key);
}
Mode::ConfirmClose => {
- input::handle_confirm_close_key(&mut self.state, key_event);
+ self.handle_confirm_close_key_via_api(key_event);
}
Mode::ContextMenu => {
- input::handle_context_menu_key(
- &mut self.state,
- &mut self.terminal_runtimes,
- key_event,
- );
+ self.handle_context_menu_key_via_api(key_event);
}
Mode::KeybindHelp => {
input::handle_keybind_help_key(&mut self.state, key_event);
@@ -4364,6 +4360,47 @@ last_pane = "prefix+tab"
}
#[test]
+ fn route_client_input_rename_enter_submits_through_api_path() {
+ let mut app = test_app();
+ app.state.workspaces = vec![Workspace::test_new("old")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::RenameWorkspace;
+ app.state.name_input = "new".into();
+
+ app.route_client_input(b"\r".to_vec());
+
+ assert_eq!(app.state.workspaces[0].custom_name.as_deref(), Some("new"));
+ assert!(app.event_hub.events_after(0).iter().any(|(_, event)| {
+ matches!(event.event, crate::api::schema::EventKind::WorkspaceRenamed)
+ }));
+ }
+
+ #[test]
+ fn route_client_input_context_menu_enter_submits_through_api_path() {
+ let mut app = test_app();
+ app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.confirm_close = false;
+ app.state.context_menu = Some(state::ContextMenuState {
+ kind: state::ContextMenuKind::Workspace { ws_idx: 1 },
+ x: 2,
+ y: 2,
+ list: state::MenuListState::new(1),
+ });
+ app.state.mode = Mode::ContextMenu;
+
+ app.route_client_input(b"\r".to_vec());
+
+ assert_eq!(app.state.workspaces.len(), 1);
+ assert_eq!(app.state.workspaces[0].display_name(), "a");
+ assert!(app.event_hub.events_after(0).iter().any(|(_, event)| {
+ matches!(event.event, crate::api::schema::EventKind::WorkspaceClosed)
+ }));
+ }
+
+ #[test]
fn raw_ctrl_v_decodes_as_modal_paste_shortcut() {
let events = crate::raw_input::parse_raw_input_bytes_sync(&[0x16]);
let Some(crate::raw_input::RawInputEvent::Key(key)) = events.first() else {
diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs
index d33e7f3c..ce8be5ec 100644
--- a/src/app/worktrees.rs
+++ b/src/app/worktrees.rs
@@ -7,7 +7,9 @@ use super::{
state::{WorktreeCreateState, WorktreeOpenEntry, WorktreeOpenState, WorktreeRemoveState},
App, Mode,
};
-use crate::events::{AppEvent, WorktreeAddResult, WorktreeRemoveResult};
+#[cfg(test)]
+use crate::events::AppEvent;
+use crate::events::{WorktreeAddResult, WorktreeRemoveResult};
impl App {
fn worktree_source_metadata(
@@ -245,7 +247,7 @@ impl App {
}
self.close_worktree_create_dialog();
}
- KeyCode::Enter => self.start_worktree_add(),
+ KeyCode::Enter => self.submit_worktree_create_via_api(),
KeyCode::Backspace => {
if self.state.name_input_replace_on_type {
self.state.name_input.clear();
@@ -324,7 +326,7 @@ impl App {
open.normalize_selection();
}
}
- KeyCode::Enter => self.open_selected_existing_worktree(),
+ KeyCode::Enter => self.submit_worktree_open_via_api(),
_ => {}
}
}
@@ -340,6 +342,7 @@ impl App {
open.normalize_selection();
}
+ #[cfg(test)]
pub(crate) fn open_selected_existing_worktree(&mut self) {
let Some(open) = self.state.worktree_open.as_ref() else {
return;
@@ -432,6 +435,7 @@ impl App {
// The caller has already extracted the open-worktree dialog state; keeping the
// membership fields explicit here avoids borrowing AppState across workspace creation.
#[allow(clippy::too_many_arguments)]
+ #[cfg(test)]
fn mark_opened_existing_worktree_membership(
&mut self,
source_workspace_id: &str,
@@ -497,6 +501,7 @@ impl App {
create.error = None;
}
+ #[cfg(test)]
pub(crate) fn start_worktree_add(&mut self) {
self.sync_worktree_branch_from_input();
let Some(create) = &mut self.state.worktree_create else {
@@ -623,11 +628,12 @@ impl App {
Mode::Navigate
};
}
- KeyCode::Enter => self.start_worktree_remove(),
+ KeyCode::Enter => self.submit_worktree_remove_via_api(),
_ => {}
}
}
+ #[cfg(test)]
pub(crate) fn start_worktree_remove(&mut self) {
let Some((workspace_id, repo_root, path, force)) =
self.state.worktree_remove.as_mut().and_then(|remove| {
@@ -1519,7 +1525,7 @@ mod tests {
}
#[test]
- fn submit_worktree_create_via_api_restores_modal_on_immediate_rejection() {
+ fn worktree_create_enter_submits_through_api_path() {
let mut app = app_for_worktree_tests();
app.state.worktree_directory = std::path::PathBuf::from("/w");
app.state.workspaces = vec![crate::workspace::Workspace::test_new("source")];
@@ -1531,12 +1537,13 @@ mod tests {
checkout_path: "/repo/herdr".into(),
is_linked_worktree: false,
};
- let branch = "issue/191";
+ let branch = "issue/195";
let checkout_path =
crate::worktree::default_checkout_path(&app.state.worktree_directory, "herdr", branch);
let checkout_key = crate::worktree::canonical_or_original(&checkout_path);
app.pending_api_worktree_creates.insert(checkout_key, 1);
app.state.workspaces[0].worktree_space = Some(source_membership.clone());
+ app.state.mode = Mode::NewLinkedWorktree;
app.state.name_input = branch.into();
app.state.worktree_create = Some(WorktreeCreateState {
source_workspace_id,
@@ -1551,7 +1558,7 @@ mod tests {
creating: false,
});
- app.submit_worktree_create_via_api();
+ app.handle_worktree_create_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
let create = app.state.worktree_create.as_ref().unwrap();
assert!(!create.creating);
@@ -1561,8 +1568,77 @@ mod tests {
);
}
+ #[tokio::test]
+ async fn worktree_open_enter_submits_through_api_path() {
+ let repo = create_committed_repo("app-worktree-open-enter-repo");
+ let checkout = unique_temp_path("app-worktree-open-enter-checkout");
+ run_git(
+ &repo,
+ &[
+ "worktree",
+ "add",
+ "--quiet",
+ "-b",
+ "worktree/open-enter",
+ checkout.to_str().unwrap(),
+ "HEAD",
+ ],
+ );
+
+ let event_hub = crate::api::EventHub::default();
+ let mut app = app_for_worktree_tests_with_event_hub(event_hub.clone());
+ app.state.workspaces = vec![crate::workspace::Workspace::test_new("source")];
+ let source_workspace_id = app.state.workspaces[0].id.clone();
+ let source_membership = crate::workspace::WorktreeSpaceMembership {
+ key: "repo-key".into(),
+ label: "herdr".into(),
+ repo_root: repo.clone(),
+ checkout_path: repo.clone(),
+ is_linked_worktree: false,
+ };
+ app.state.workspaces[0].worktree_space = Some(source_membership.clone());
+ app.state.mode = Mode::OpenExistingWorktree;
+ app.state.worktree_open = Some(WorktreeOpenState {
+ source_workspace_id,
+ source_existing_membership: Some(source_membership),
+ source_checkout_path: repo.clone(),
+ source_repo_root: repo.clone(),
+ repo_key: "repo-key".into(),
+ repo_name: "herdr".into(),
+ entries: vec![WorktreeOpenEntry {
+ path: checkout.clone(),
+ branch: Some("worktree/open-enter".into()),
+ is_linked_worktree: true,
+ already_open_ws_idx: None,
+ }],
+ selected: 0,
+ query: String::new(),
+ search_focused: false,
+ error: None,
+ });
+
+ app.handle_worktree_open_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
+
+ assert!(app.state.worktree_open.is_none());
+ assert_eq!(
+ event_kinds(&event_hub),
+ vec![
+ crate::api::schema::EventKind::WorkspaceCreated,
+ crate::api::schema::EventKind::TabCreated,
+ crate::api::schema::EventKind::PaneCreated,
+ crate::api::schema::EventKind::WorktreeOpened,
+ ]
+ );
+ shutdown_test_runtimes(&mut app);
+ run_git(
+ &repo,
+ &["worktree", "remove", "--force", checkout.to_str().unwrap()],
+ );
+ let _ = std::fs::remove_dir_all(repo);
+ }
+
#[test]
- fn submit_worktree_remove_via_api_restores_modal_on_immediate_rejection() {
+ fn worktree_remove_enter_submits_through_api_path() {
let mut app = app_for_worktree_tests();
app.state.workspaces = vec![crate::workspace::Workspace::test_new("issue")];
let workspace_id = app.state.workspaces[0].id.clone();
@@ -1577,7 +1653,7 @@ mod tests {
app.pending_api_worktree_removes
.insert(workspace_id.clone(), 1);
- app.submit_worktree_remove_via_api();
+ app.handle_worktree_remove_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
let remove = app.state.worktree_remove.as_ref().unwrap();
assert_eq!(remove.workspace_id, workspace_id);
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 36519d47..469320f9 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -559,7 +559,7 @@ impl HeadlessServer {
if self.app.state.request_submit_worktree_create {
self.app.state.request_submit_worktree_create = false;
- self.app.start_worktree_add();
+ self.app.submit_worktree_create_via_api();
needs_render = true;
needs_full_render = true;
crate::render_prof::event("full_render_cause.deferred_worktree_submit");
@@ -567,7 +567,7 @@ impl HeadlessServer {
if self.app.state.request_submit_worktree_open {
self.app.state.request_submit_worktree_open = false;
- self.app.open_selected_existing_worktree();
+ self.app.submit_worktree_open_via_api();
needs_render = true;
needs_full_render = true;
crate::render_prof::event("full_render_cause.deferred_worktree_submit");
@@ -575,7 +575,7 @@ impl HeadlessServer {
if self.app.state.request_submit_worktree_remove {
self.app.state.request_submit_worktree_remove = false;
- self.app.start_worktree_remove();
+ self.app.submit_worktree_remove_via_api();
needs_render = true;
needs_full_render = true;
crate::render_prof::event("full_render_cause.deferred_worktree_submit");