cli: Use Terminal Panel's window for `zed --add` (#45073)

James Cagalawan and Kirill Bulatov created

This makes the `zed --add <PATH>` command use the window it was run from
when run from a Terminal Panel. I run into this paper cut quite a lot
working with multiple projects and preferring to create new files
through the CLI instead of the GUI.


**Before**
With two windows open, running `zed --add a.txt` in one window's
terminal might open the file `a.txt` in the other window.

[Screencast from 12-17-2025 02:24:45 AM - zed --add bug
before.webm](https://github.com/user-attachments/assets/30816add-91e1-41c3-b2e3-6a0e6e88771a)


**After**
With this change, it will use the window of the Terminal Panel.

[Screencast from 12-17-2025 02:16:09 AM - zed --add bug
after.webm](https://github.com/user-attachments/assets/5141518e-5fb0-47d1-9281-54c0699ff7f5)



Release Notes:

- Improved `zed --add` command to prefer window it was run from

Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/workspace/src/workspace.rs   |   4 
crates/zed/src/zed/open_listener.rs | 168 ++++++++++++++++++++++++++++++
2 files changed, 170 insertions(+), 2 deletions(-)

Detailed changes

crates/workspace/src/workspace.rs 🔗

@@ -8575,7 +8575,9 @@ pub fn open_paths(
                 }
             });
 
-            if open_options.open_new_workspace.is_none()
+            if (open_options.open_new_workspace.is_none()
+                || (open_options.open_new_workspace == Some(false)
+                    && open_options.prefer_focused_window))
                 && (existing.is_none() || open_options.prefer_focused_window)
                 && all_metadatas.iter().all(|file| !file.is_dir)
             {

crates/zed/src/zed/open_listener.rs 🔗

@@ -619,7 +619,7 @@ async fn open_local_workspace(
         workspace::OpenOptions {
             open_new_workspace,
             replace_window,
-            prefer_focused_window: wait,
+            prefer_focused_window: wait || open_new_workspace == Some(false),
             env: env.cloned(),
             ..Default::default()
         },
@@ -1249,4 +1249,170 @@ mod tests {
             _ => panic!("Expected GitClone kind"),
         }
     }
+
+    #[gpui::test]
+    async fn test_add_flag_prefers_focused_window(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+
+        let root_dir = if cfg!(windows) { "C:\\root" } else { "/root" };
+        let file1_path = if cfg!(windows) {
+            "C:\\root\\file1.txt"
+        } else {
+            "/root/file1.txt"
+        };
+        let file2_path = if cfg!(windows) {
+            "C:\\root\\file2.txt"
+        } else {
+            "/root/file2.txt"
+        };
+
+        app_state.fs.create_dir(Path::new(root_dir)).await.unwrap();
+        app_state
+            .fs
+            .create_file(Path::new(file1_path), Default::default())
+            .await
+            .unwrap();
+        app_state
+            .fs
+            .save(
+                Path::new(file1_path),
+                &Rope::from("content1"),
+                LineEnding::Unix,
+            )
+            .await
+            .unwrap();
+        app_state
+            .fs
+            .create_file(Path::new(file2_path), Default::default())
+            .await
+            .unwrap();
+        app_state
+            .fs
+            .save(
+                Path::new(file2_path),
+                &Rope::from("content2"),
+                LineEnding::Unix,
+            )
+            .await
+            .unwrap();
+
+        let (response_tx, _response_rx) = ipc::channel::<CliResponse>().unwrap();
+
+        // Open first workspace
+        let workspace_paths_1 = vec![file1_path.to_string()];
+        let _errored = cx
+            .spawn({
+                let app_state = app_state.clone();
+                let response_tx = response_tx.clone();
+                |mut cx| async move {
+                    open_local_workspace(
+                        workspace_paths_1,
+                        Vec::new(),
+                        false,
+                        None,
+                        false,
+                        false,
+                        &response_tx,
+                        None,
+                        &app_state,
+                        &mut cx,
+                    )
+                    .await
+                }
+            })
+            .await;
+
+        assert_eq!(cx.windows().len(), 1);
+        let multi_workspace_1 = cx.windows()[0].downcast::<MultiWorkspace>().unwrap();
+
+        // Open second workspace in a new window
+        let workspace_paths_2 = vec![file2_path.to_string()];
+        let _errored = cx
+            .spawn({
+                let app_state = app_state.clone();
+                let response_tx = response_tx.clone();
+                |mut cx| async move {
+                    open_local_workspace(
+                        workspace_paths_2,
+                        Vec::new(),
+                        false,
+                        Some(true), // Force new window
+                        false,
+                        false,
+                        &response_tx,
+                        None,
+                        &app_state,
+                        &mut cx,
+                    )
+                    .await
+                }
+            })
+            .await;
+
+        assert_eq!(cx.windows().len(), 2);
+        let multi_workspace_2 = cx.windows()[1].downcast::<MultiWorkspace>().unwrap();
+
+        // Focus window2
+        multi_workspace_2
+            .update(cx, |_, window, _| {
+                window.activate_window();
+            })
+            .unwrap();
+
+        // Now use --add flag (open_new_workspace = Some(false)) to add a new file
+        // It should open in the focused window (window2), not an arbitrary window
+        let new_file_path = if cfg!(windows) {
+            "C:\\root\\new_file.txt"
+        } else {
+            "/root/new_file.txt"
+        };
+        app_state
+            .fs
+            .create_file(Path::new(new_file_path), Default::default())
+            .await
+            .unwrap();
+
+        let workspace_paths_add = vec![new_file_path.to_string()];
+        let _errored = cx
+            .spawn({
+                let app_state = app_state.clone();
+                let response_tx = response_tx.clone();
+                |mut cx| async move {
+                    open_local_workspace(
+                        workspace_paths_add,
+                        Vec::new(),
+                        false,
+                        Some(false), // --add flag: open_new_workspace = Some(false)
+                        false,
+                        false,
+                        &response_tx,
+                        None,
+                        &app_state,
+                        &mut cx,
+                    )
+                    .await
+                }
+            })
+            .await;
+
+        // Should still have 2 windows (file added to existing focused window)
+        assert_eq!(cx.windows().len(), 2);
+
+        // Verify the file was added to window2 (the focused one)
+        multi_workspace_2
+            .update(cx, |workspace, _, cx| {
+                let items = workspace.workspace().read(cx).items(cx).collect::<Vec<_>>();
+                // Should have 2 items now (file2.txt and new_file.txt)
+                assert_eq!(items.len(), 2, "Focused window should have 2 items");
+            })
+            .unwrap();
+
+        // Verify window1 still has only 1 item
+        multi_workspace_1
+            .update(cx, |workspace, _, cx| {
+                let items = workspace.workspace().read(cx).items(cx).collect::<Vec<_>>();
+                assert_eq!(items.len(), 1, "Other window should still have 1 item");
+            })
+            .unwrap();
+    }
 }