aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 13:58:26 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 13:58:26 +0300
commitcf3769feb9837c6040820c3e6096c5cc2907adc0 (patch)
tree1bf055988874a2e1c871bf4830a920a240108bda /src/app
parent2c422e60f1724ac5639c80abb73dc1f10a91b5c6 (diff)
feat: follow focused pane cwd for new workspaces
refs #919
Diffstat (limited to 'src/app')
-rw-r--r--src/app/api.rs21
-rw-r--r--src/app/api/layouts.rs18
-rw-r--r--src/app/api/tabs.rs18
-rw-r--r--src/app/api/workspaces.rs89
4 files changed, 109 insertions, 37 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index eeb1127f..8743c08f 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -1158,6 +1158,27 @@ fn agent_manifest_info(
}
#[cfg(test)]
+pub(super) mod test_support {
+ pub(crate) fn exiting_test_command() -> &'static str {
+ #[cfg(windows)]
+ {
+ "C:\\Windows\\System32\\whoami.exe"
+ }
+ #[cfg(not(windows))]
+ {
+ "/usr/bin/true"
+ }
+ }
+
+ pub(crate) fn shutdown_test_runtimes(app: &mut crate::app::App) {
+ let runtimes: Vec<_> = app.terminal_runtimes.drain().collect();
+ for (_terminal_id, runtime) in runtimes {
+ runtime.shutdown();
+ }
+ }
+}
+
+#[cfg(test)]
mod tests {
use super::*;
use crate::detect::{Agent, AgentState};
diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs
index a2705acc..044731a4 100644
--- a/src/app/api/layouts.rs
+++ b/src/app/api/layouts.rs
@@ -586,6 +586,7 @@ fn validate_layout_node(
#[cfg(test)]
mod tests {
+ use super::super::test_support::{exiting_test_command, shutdown_test_runtimes};
use super::*;
use crate::{
api::schema::{ErrorResponse, ResponseResult, SuccessResponse},
@@ -593,16 +594,6 @@ mod tests {
workspace::Workspace,
};
- #[cfg(windows)]
- fn exiting_test_command() -> &'static str {
- "C:\\Windows\\System32\\whoami.exe"
- }
-
- #[cfg(not(windows))]
- fn exiting_test_command() -> &'static str {
- "/usr/bin/true"
- }
-
fn app_with_workspace() -> App {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
@@ -621,13 +612,6 @@ mod tests {
app
}
- fn shutdown_test_runtimes(app: &mut App) {
- let runtimes: Vec<_> = app.terminal_runtimes.drain().collect();
- for (_terminal_id, runtime) in runtimes {
- runtime.shutdown();
- }
- }
-
#[test]
fn layout_export_returns_portable_tree() {
let mut app = app_with_workspace();
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index ce126ca7..1f29f54b 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -292,6 +292,7 @@ fn tab_not_found(id: String, tab_id: &str) -> String {
#[cfg(test)]
mod tests {
+ use super::super::test_support::{exiting_test_command, shutdown_test_runtimes};
use super::*;
use crate::{
api::schema::SuccessResponse,
@@ -299,23 +300,6 @@ mod tests {
workspace::Workspace,
};
- #[cfg(windows)]
- fn exiting_test_command() -> &'static str {
- "C:\\Windows\\System32\\whoami.exe"
- }
-
- #[cfg(not(windows))]
- fn exiting_test_command() -> &'static str {
- "/usr/bin/true"
- }
-
- fn shutdown_test_runtimes(app: &mut App) {
- let runtimes: Vec<_> = app.terminal_runtimes.drain().collect();
- for (_terminal_id, runtime) in runtimes {
- runtime.shutdown();
- }
- }
-
#[test]
fn api_tab_move_reorders_tabs_in_target_workspace() {
let event_hub = crate::api::EventHub::default();
diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs
index f4189a04..19e4f6ae 100644
--- a/src/app/api/workspaces.rs
+++ b/src/app/api/workspaces.rs
@@ -40,9 +40,10 @@ impl App {
params: WorkspaceCreateParams,
) -> String {
let cwd = params.cwd.map(PathBuf::from).unwrap_or_else(|| {
- let follow_cwd = self
- .workspace_creation_source()
- .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx));
+ let follow_cwd = self.workspace_creation_source().and_then(|ws_idx| {
+ self.focused_pane_cwd_in_workspace(ws_idx)
+ .or_else(|| self.seed_cwd_from_workspace(ws_idx))
+ });
self.resolve_new_terminal_cwd(follow_cwd)
});
let extra_env = match super::env::normalize_launch_env(params.env) {
@@ -212,6 +213,88 @@ mod tests {
use super::*;
use crate::{api::schema::SuccessResponse, config::Config, workspace::Workspace};
+ // `new_cwd = follow` must anchor on the focused pane for every creation
+ // surface. Splits and tabs already do; a new workspace must follow the
+ // focused pane too, not the source workspace's first-tab root pane.
+ #[tokio::test]
+ async fn workspace_create_follows_focused_pane_cwd_not_first_tab_root() {
+ use super::super::test_support::{exiting_test_command, shutdown_test_runtimes};
+ use crate::config::ShellModeConfig;
+
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(
+ &Config::default(),
+ true,
+ None,
+ api_rx,
+ crate::api::EventHub::default(),
+ );
+ app.state.default_shell = exiting_test_command().into();
+ app.state.shell_mode = ShellModeConfig::NonLogin;
+ app.state.workspaces = vec![Workspace::test_new("spaces")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.ensure_test_terminals();
+
+ // Second tab becomes the focused pane, away from tab 1's root pane.
+ let response = app.handle_tab_create(
+ "tab".into(),
+ crate::api::schema::TabCreateParams {
+ workspace_id: None,
+ cwd: None,
+ focus: true,
+ label: None,
+ env: Default::default(),
+ },
+ );
+ let _: SuccessResponse = serde_json::from_str(&response).unwrap();
+ // Drop runtimes so cwd resolution deterministically uses cached state.
+ shutdown_test_runtimes(&mut app);
+
+ let focused_cwd = std::env::temp_dir().join(format!(
+ "herdr-ws-follow-{}-{}",
+ std::process::id(),
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ std::fs::create_dir_all(&focused_cwd).unwrap();
+ let ws = &app.state.workspaces[0];
+ let root_cwd = ws.identity_cwd.clone();
+ let focused_pane = ws.focused_pane_id().unwrap();
+ assert_ne!(focused_pane, ws.tabs[0].root_pane);
+ let terminal_id = ws.terminal_id(focused_pane).cloned().unwrap();
+ app.state.terminals.get_mut(&terminal_id).unwrap().cwd = focused_cwd.clone();
+
+ let response = app.handle_workspace_create(
+ "req".into(),
+ WorkspaceCreateParams {
+ cwd: None,
+ focus: false,
+ label: None,
+ env: Default::default(),
+ },
+ );
+
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ assert!(matches!(
+ success.result,
+ ResponseResult::WorkspaceCreated { .. }
+ ));
+ let created_cwd = &app.state.workspaces[1].identity_cwd;
+ assert_eq!(
+ crate::worktree::canonical_or_original(created_cwd),
+ crate::worktree::canonical_or_original(&focused_cwd)
+ );
+ assert_ne!(
+ crate::worktree::canonical_or_original(created_cwd),
+ crate::worktree::canonical_or_original(&root_cwd)
+ );
+ shutdown_test_runtimes(&mut app);
+ let _ = std::fs::remove_dir_all(&focused_cwd);
+ }
+
fn app_with_linked_worktree() -> App {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(