Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:47:46.355Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

terminals.rs

801 lines · 30.2 KB · rust
1use anyhow::Result;
2use collections::HashMap;
3use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
4
5use async_channel::bounded;
6use futures::{FutureExt, future::Shared};
7use itertools::Itertools as _;
8use language::LanguageName;
9use remote::{Interactive, RemoteClient};
10use settings::{Settings, SettingsLocation};
11use std::{
12    borrow::Cow,
13    path::{Path, PathBuf},
14    sync::Arc,
15};
16use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
17use terminal::{
18    TaskState, TaskStatus, Terminal, TerminalBuilder, insert_zed_terminal_env,
19    terminal_settings::TerminalSettings,
20};
21use util::{
22    command::new_std_command, get_default_system_shell, get_system_shell, maybe, rel_path::RelPath,
23};
24
25use crate::{Project, ProjectPath};
26
27pub struct Terminals {
28    pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
29}
30
31impl Project {
32    pub fn active_entry_directory(&self, cx: &App) -> Option<PathBuf> {
33        let entry_id = self.active_entry()?;
34        let worktree = self.worktree_for_entry(entry_id, cx)?;
35        let worktree = worktree.read(cx);
36        let entry = worktree.entry_for_id(entry_id)?;
37
38        let absolute_path = worktree.absolutize(entry.path.as_ref());
39        if entry.is_dir() {
40            Some(absolute_path)
41        } else {
42            absolute_path.parent().map(|p| p.to_path_buf())
43        }
44    }
45
46    pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
47        self.active_entry()
48            .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
49            .into_iter()
50            .chain(self.worktrees(cx))
51            .find_map(|tree| tree.read(cx).root_dir())
52    }
53
54    pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> {
55        let worktree = self.worktrees(cx).next()?;
56        let worktree = worktree.read(cx);
57        if worktree.root_entry()?.is_dir() {
58            Some(worktree.abs_path().to_path_buf())
59        } else {
60            None
61        }
62    }
63
64    pub fn create_terminal_task(
65        &mut self,
66        spawn_task: SpawnInTerminal,
67        cx: &mut Context<Self>,
68    ) -> Task<Result<Entity<Terminal>>> {
69        let is_via_remote = self.remote_client.is_some();
70
71        let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
72            if is_via_remote {
73                Some(Arc::from(cwd.as_ref()))
74            } else {
75                let cwd = cwd.to_string_lossy();
76                let tilde_substituted = shellexpand::tilde(&cwd);
77                Some(Arc::from(Path::new(tilde_substituted.as_ref())))
78            }
79        } else {
80            self.active_project_directory(cx)
81        };
82
83        let mut settings_location = None;
84        if let Some(path) = path.as_ref()
85            && let Some((worktree, _)) = self.find_worktree(path, cx)
86        {
87            settings_location = Some(SettingsLocation {
88                worktree_id: worktree.read(cx).id(),
89                path: RelPath::empty(),
90            });
91        }
92        let settings = TerminalSettings::get(settings_location, cx).clone();
93        let detect_venv = settings.detect_venv.as_option().is_some();
94
95        let (completion_tx, completion_rx) = bounded(1);
96
97        let local_path = if is_via_remote { None } else { path.clone() };
98        let task_state = Some(TaskState {
99            spawned_task: spawn_task.clone(),
100            status: TaskStatus::Running,
101            completion_rx,
102        });
103        let remote_client = self.remote_client.clone();
104        let shell = match &remote_client {
105            Some(remote_client) => remote_client
106                .read(cx)
107                .shell()
108                .unwrap_or_else(get_default_system_shell),
109            None => get_system_shell(),
110        };
111        let path_style = self.path_style(cx);
112        let shell_kind = ShellKind::new(&shell, path_style.is_windows());
113
114        // Prepare a task for resolving the environment
115        let env_task =
116            self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx);
117
118        // Scope the toolchain lookup to the worktree the terminal is being
119        // spawned in. Previously this iterated the active editor's worktree
120        // and then every visible worktree, so a Python toolchain persisted
121        // for worktree A would leak into a terminal opened in worktree B and
122        // inject (e.g.) `conda activate base` into a shell that has no
123        // business with conda.
124        let project_path_contexts: Vec<ProjectPath> = path
125            .as_ref()
126            .and_then(|p| self.find_worktree(p, cx))
127            .map(|(worktree, relative_path)| ProjectPath {
128                worktree_id: worktree.read(cx).id(),
129                path: relative_path,
130            })
131            .into_iter()
132            .collect();
133        let toolchains = project_path_contexts
134            .into_iter()
135            .filter(|_| detect_venv)
136            .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
137            .collect::<Vec<_>>();
138        let lang_registry = self.languages.clone();
139        cx.spawn(async move |project, cx| {
140            let mut env = env_task.await.unwrap_or_default();
141            env.extend(settings.env);
142
143            let activation_script = maybe!(async {
144                for toolchain in toolchains {
145                    let Some(toolchain) = toolchain.await else {
146                        continue;
147                    };
148                    let language = lang_registry
149                        .language_for_name(&toolchain.language_name.0)
150                        .await
151                        .ok();
152                    let lister = language?.toolchain_lister()?;
153                    let future =
154                        cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx));
155                    return Some(future.await);
156                }
157                None
158            })
159            .await
160            .unwrap_or_default();
161
162            let builder = project
163                .update(cx, move |_, cx| {
164                    let format_to_run = |spawn_task: &SpawnInTerminal| {
165                        format_task_for_activation(
166                            spawn_task,
167                            shell_kind,
168                            &shell,
169                            path_style.is_windows(),
170                        )
171                    };
172
173                    let (shell, env) = {
174                        let to_run =
175                            (!activation_script.is_empty()).then(|| format_to_run(&spawn_task));
176                        env.extend(spawn_task.env);
177                        match remote_client {
178                            Some(remote_client) => match activation_script.clone() {
179                                activation_script if !activation_script.is_empty() => {
180                                    let separator = shell_kind.sequential_commands_separator();
181                                    let activation_script =
182                                        activation_script.join(&format!("{separator} "));
183                                    let to_run = to_run.expect("activation command was formatted");
184
185                                    let arg = format!("{activation_script}{separator} {to_run}");
186                                    let args = shell_kind.args_for_shell(true, arg);
187                                    let shell = remote_client
188                                        .read(cx)
189                                        .shell()
190                                        .unwrap_or_else(get_default_system_shell);
191
192                                    create_remote_shell(
193                                        Some((&shell, &args)),
194                                        env,
195                                        path,
196                                        remote_client,
197                                        cx,
198                                    )?
199                                }
200                                _ => create_remote_shell(
201                                    spawn_task
202                                        .command
203                                        .as_ref()
204                                        .map(|command| (command, &spawn_task.args)),
205                                    env,
206                                    path,
207                                    remote_client,
208                                    cx,
209                                )?,
210                            },
211                            None => match activation_script.clone() {
212                                activation_script if !activation_script.is_empty() => {
213                                    let separator = shell_kind.sequential_commands_separator();
214                                    let activation_script =
215                                        activation_script.join(&format!("{separator} "));
216                                    let to_run = to_run.expect("activation command was formatted");
217
218                                    let arg = format!("{activation_script}{separator} {to_run}");
219                                    let args = shell_kind.args_for_shell(true, arg);
220
221                                    (
222                                        Shell::WithArguments {
223                                            program: shell,
224                                            args,
225                                            title_override: None,
226                                        },
227                                        env,
228                                    )
229                                }
230                                _ => (
231                                    if let Some(program) = spawn_task.command {
232                                        Shell::WithArguments {
233                                            program,
234                                            args: spawn_task.args,
235                                            title_override: None,
236                                        }
237                                    } else {
238                                        Shell::System
239                                    },
240                                    env,
241                                ),
242                            },
243                        }
244                    };
245                    anyhow::Ok(TerminalBuilder::new(
246                        local_path.map(|path| path.to_path_buf()),
247                        task_state,
248                        shell,
249                        env,
250                        settings.cursor_shape,
251                        settings.alternate_scroll,
252                        settings.max_scroll_history_lines,
253                        settings.path_hyperlink_regexes,
254                        settings.path_hyperlink_timeout_ms,
255                        is_via_remote,
256                        cx.entity_id().as_u64(),
257                        Some(completion_tx),
258                        cx,
259                        activation_script,
260                        path_style,
261                    ))
262                })??
263                .await?;
264            project.update(cx, move |this, cx| {
265                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
266
267                this.terminals
268                    .local_handles
269                    .push(terminal_handle.downgrade());
270
271                let id = terminal_handle.entity_id();
272                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
273                    let handles = &mut project.terminals.local_handles;
274
275                    if let Some(index) = handles
276                        .iter()
277                        .position(|terminal| terminal.entity_id() == id)
278                    {
279                        handles.remove(index);
280                        cx.notify();
281                    }
282                })
283                .detach();
284
285                terminal_handle
286            })
287        })
288    }
289
290    pub fn create_terminal_shell(
291        &mut self,
292        cwd: Option<PathBuf>,
293        cx: &mut Context<Self>,
294    ) -> Task<Result<Entity<Terminal>>> {
295        self.create_terminal_shell_internal(cwd, false, cx)
296    }
297
298    /// Creates a local terminal even if the project is remote.
299    /// In remote projects: opens in Zed's launch directory (bypasses SSH).
300    /// In local projects: opens in the project directory (same as regular terminals).
301    pub fn create_local_terminal(
302        &mut self,
303        cx: &mut Context<Self>,
304    ) -> Task<Result<Entity<Terminal>>> {
305        let working_directory = if self.remote_client.is_some() {
306            // Remote project: don't use remote paths, let shell use Zed's cwd
307            None
308        } else {
309            // Local project: use project directory like normal terminals
310            self.active_project_directory(cx).map(|p| p.to_path_buf())
311        };
312        self.create_terminal_shell_internal(working_directory, true, cx)
313    }
314
315    /// Internal method for creating terminal shells.
316    /// If force_local is true, creates a local terminal even if the project has a remote client.
317    /// This allows "breaking out" to a local shell in remote projects.
318    fn create_terminal_shell_internal(
319        &mut self,
320        cwd: Option<PathBuf>,
321        force_local: bool,
322        cx: &mut Context<Self>,
323    ) -> Task<Result<Entity<Terminal>>> {
324        let path = cwd.map(|p| Arc::from(&*p));
325        let is_via_remote = !force_local && self.remote_client.is_some();
326
327        let mut settings_location = None;
328        if let Some(path) = path.as_ref()
329            && let Some((worktree, _)) = self.find_worktree(path, cx)
330        {
331            settings_location = Some(SettingsLocation {
332                worktree_id: worktree.read(cx).id(),
333                path: RelPath::empty(),
334            });
335        }
336        let settings = TerminalSettings::get(settings_location, cx).clone();
337        let detect_venv = settings.detect_venv.as_option().is_some();
338        let local_path = if is_via_remote { None } else { path.clone() };
339
340        // See create_terminal_task: scope the toolchain lookup to the
341        // worktree the terminal is opened in, not the active editor's
342        // worktree or other visible worktrees.
343        let project_path_contexts: Vec<ProjectPath> = path
344            .as_ref()
345            .and_then(|p| self.find_worktree(p, cx))
346            .map(|(worktree, relative_path)| ProjectPath {
347                worktree_id: worktree.read(cx).id(),
348                path: relative_path,
349            })
350            .into_iter()
351            .collect();
352        let toolchains = project_path_contexts
353            .into_iter()
354            .filter(|_| detect_venv)
355            .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
356            .collect::<Vec<_>>();
357        let remote_client = if force_local {
358            None
359        } else {
360            self.remote_client.clone()
361        };
362        let shell = match &remote_client {
363            Some(remote_client) => remote_client
364                .read(cx)
365                .shell()
366                .unwrap_or_else(get_default_system_shell),
367            None => settings.shell.program(),
368        };
369        let env_shell = match &remote_client {
370            Some(_) => shell.clone(),
371            None => get_system_shell(),
372        };
373
374        let path_style = self.path_style(cx);
375
376        // Prepare a task for resolving the environment
377        let env_task =
378            self.resolve_directory_environment(&env_shell, path.clone(), remote_client.clone(), cx);
379
380        let lang_registry = self.languages.clone();
381        cx.spawn(async move |project, cx| {
382            let shell_kind = ShellKind::new(&shell, path_style.is_windows());
383            let mut env = env_task.await.unwrap_or_default();
384            env.extend(settings.env);
385
386            let activation_script = maybe!(async {
387                for toolchain in toolchains {
388                    let Some(toolchain) = toolchain.await else {
389                        continue;
390                    };
391                    let language = lang_registry
392                        .language_for_name(&toolchain.language_name.0)
393                        .await
394                        .ok();
395                    let lister = language?.toolchain_lister()?;
396                    let future =
397                        cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx));
398                    return Some(future.await);
399                }
400                None
401            })
402            .await
403            .unwrap_or_default();
404
405            let builder = project
406                .update(cx, move |_, cx| {
407                    let (shell, env) = {
408                        match remote_client {
409                            Some(remote_client) => {
410                                create_remote_shell(None, env, path, remote_client, cx)?
411                            }
412                            None => (settings.shell, env),
413                        }
414                    };
415                    anyhow::Ok(TerminalBuilder::new(
416                        local_path.map(|path| path.to_path_buf()),
417                        None,
418                        shell,
419                        env,
420                        settings.cursor_shape,
421                        settings.alternate_scroll,
422                        settings.max_scroll_history_lines,
423                        settings.path_hyperlink_regexes,
424                        settings.path_hyperlink_timeout_ms,
425                        is_via_remote,
426                        cx.entity_id().as_u64(),
427                        None,
428                        cx,
429                        activation_script,
430                        path_style,
431                    ))
432                })??
433                .await?;
434            project.update(cx, move |this, cx| {
435                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
436
437                this.terminals
438                    .local_handles
439                    .push(terminal_handle.downgrade());
440
441                let id = terminal_handle.entity_id();
442                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
443                    let handles = &mut project.terminals.local_handles;
444
445                    if let Some(index) = handles
446                        .iter()
447                        .position(|terminal| terminal.entity_id() == id)
448                    {
449                        handles.remove(index);
450                        cx.notify();
451                    }
452                })
453                .detach();
454
455                terminal_handle
456            })
457        })
458    }
459
460    pub fn clone_terminal(
461        &mut self,
462        terminal: &Entity<Terminal>,
463        cx: &mut Context<'_, Project>,
464        cwd: Option<PathBuf>,
465    ) -> Task<Result<Entity<Terminal>>> {
466        // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
467        // For now, create a new shell instead.
468        if terminal.read(cx).task().is_some() {
469            return self.create_terminal_shell(cwd, cx);
470        }
471        let local_path = if self.is_via_remote_server() {
472            None
473        } else {
474            cwd
475        };
476
477        let builder = terminal.read(cx).clone_builder(cx, local_path);
478        cx.spawn(async |project, cx| {
479            let terminal = builder.await?;
480            project.update(cx, |project, cx| {
481                let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
482
483                project
484                    .terminals
485                    .local_handles
486                    .push(terminal_handle.downgrade());
487
488                let id = terminal_handle.entity_id();
489                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
490                    let handles = &mut project.terminals.local_handles;
491
492                    if let Some(index) = handles
493                        .iter()
494                        .position(|terminal| terminal.entity_id() == id)
495                    {
496                        handles.remove(index);
497                        cx.notify();
498                    }
499                })
500                .detach();
501
502                terminal_handle
503            })
504        })
505    }
506
507    pub fn terminal_settings<'a>(
508        &'a self,
509        path: &'a Option<PathBuf>,
510        cx: &'a App,
511    ) -> &'a TerminalSettings {
512        let mut settings_location = None;
513        if let Some(path) = path.as_ref()
514            && let Some((worktree, _)) = self.find_worktree(path, cx)
515        {
516            settings_location = Some(SettingsLocation {
517                worktree_id: worktree.read(cx).id(),
518                path: RelPath::empty(),
519            });
520        }
521        TerminalSettings::get(settings_location, cx)
522    }
523
524    pub fn exec_in_shell(
525        &self,
526        command: String,
527        cx: &mut Context<Self>,
528    ) -> Task<Result<smol::process::Command>> {
529        let path = self.first_project_directory(cx);
530        let remote_client = self.remote_client.clone();
531        let settings = self.terminal_settings(&path, cx).clone();
532        let shell = remote_client
533            .as_ref()
534            .and_then(|remote_client| remote_client.read(cx).shell())
535            .map(Shell::Program)
536            .unwrap_or(Shell::System);
537        let is_windows = self.path_style(cx).is_windows();
538        let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
539        let (command, args) = builder.build(Some(command), &Vec::new());
540
541        let env_task = self.resolve_directory_environment(
542            &shell.program(),
543            path.as_ref().map(|p| Arc::from(&**p)),
544            remote_client.clone(),
545            cx,
546        );
547
548        cx.spawn(async move |project, cx| {
549            let mut env = env_task.await.unwrap_or_default();
550            env.extend(settings.env);
551
552            project.update(cx, move |_, cx| {
553                match remote_client {
554                    Some(remote_client) => {
555                        let command_template = remote_client.read(cx).build_command(
556                            Some(command),
557                            &args,
558                            &env,
559                            None,
560                            None,
561                            Interactive::Yes,
562                        )?;
563                        let mut command = new_std_command(command_template.program);
564                        command.args(command_template.args);
565                        command.envs(command_template.env);
566                        Ok(command)
567                    }
568                    None => {
569                        let mut command = new_std_command(command);
570                        command.args(args);
571                        command.envs(env);
572                        if let Some(path) = path {
573                            command.current_dir(path);
574                        }
575                        Ok(command)
576                    }
577                }
578                .map(|mut process| {
579                    util::set_pre_exec_to_start_new_session(&mut process);
580                    smol::process::Command::from(process)
581                })
582            })?
583        })
584    }
585
586    pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
587        &self.terminals.local_handles
588    }
589
590    fn resolve_directory_environment(
591        &self,
592        shell: &str,
593        path: Option<Arc<Path>>,
594        remote_client: Option<Entity<RemoteClient>>,
595        cx: &mut App,
596    ) -> Shared<Task<Option<HashMap<String, String>>>> {
597        if let Some(path) = &path {
598            let shell = Shell::Program(shell.to_string());
599            self.environment
600                .update(cx, |project_env, cx| match &remote_client {
601                    Some(remote_client) => project_env.remote_directory_environment(
602                        &shell,
603                        path.clone(),
604                        remote_client.clone(),
605                        cx,
606                    ),
607                    None => project_env.local_directory_environment(&shell, path.clone(), cx),
608                })
609        } else {
610            Task::ready(None).shared()
611        }
612    }
613}
614
615fn create_remote_shell(
616    spawn_command: Option<(&String, &Vec<String>)>,
617    mut env: HashMap<String, String>,
618    working_directory: Option<Arc<Path>>,
619    remote_client: Entity<RemoteClient>,
620    cx: &mut App,
621) -> Result<(Shell, HashMap<String, String>)> {
622    insert_zed_terminal_env(&mut env, &release_channel::AppVersion::global(cx));
623
624    let (program, args) = match spawn_command {
625        Some((program, args)) => (Some(program.clone()), args),
626        None => (None, &Vec::new()),
627    };
628
629    let command = remote_client.read(cx).build_command(
630        program,
631        args.as_slice(),
632        &env,
633        working_directory.map(|path| path.display().to_string()),
634        None,
635        Interactive::Yes,
636    )?;
637
638    log::debug!("Connecting to a remote server: {:?}", command.program);
639    let host = remote_client.read(cx).connection_options().display_name();
640
641    Ok((
642        Shell::WithArguments {
643            program: command.program,
644            args: command.args,
645            title_override: Some(format!("{} — Terminal", host)),
646        },
647        command.env,
648    ))
649}
650
651fn format_task_for_activation(
652    spawn_task: &SpawnInTerminal,
653    shell_kind: ShellKind,
654    shell: &str,
655    is_windows: bool,
656) -> String {
657    if let Some(command) = &spawn_task.command {
658        let command = shell_kind.prepend_command_prefix(command);
659        let command = shell_kind.try_quote_prefix_aware(&command);
660        let args = spawn_task
661            .args
662            .iter()
663            .enumerate()
664            .filter_map(|(index, arg)| {
665                quote_prepared_task_arg_for_activation(
666                    spawn_task, shell_kind, arg, index, is_windows,
667                )
668            });
669
670        command.into_iter().chain(args).join(" ")
671    } else {
672        // todo: this breaks for remotes to windows
673        format!("exec {shell} -l")
674    }
675}
676
677fn quote_prepared_task_arg_for_activation<'a>(
678    spawn_task: &SpawnInTerminal,
679    shell_kind: ShellKind,
680    arg: &'a str,
681    index: usize,
682    is_windows: bool,
683) -> Option<Cow<'a, str>> {
684    if spawn_task.shell.shell_kind(is_windows) == ShellKind::Cmd
685        && index >= 2
686        && spawn_task
687            .args
688            .get(index - 2)
689            .is_some_and(|arg| arg.eq_ignore_ascii_case("/S"))
690        && spawn_task
691            .args
692            .get(index - 1)
693            .is_some_and(|arg| arg.eq_ignore_ascii_case("/C"))
694    {
695        // The /C argument is already a cmd command string from prepare_task_for_spawn.
696        // Quoting it again for venv activation makes cmd see the quotes as literals.
697        return quote_cmd_command_arg_for_outer_shell(arg, shell_kind).map(Cow::Owned);
698    }
699
700    shell_kind.try_quote(arg)
701}
702
703fn quote_cmd_command_arg_for_outer_shell(arg: &str, shell_kind: ShellKind) -> Option<String> {
704    match shell_kind {
705        ShellKind::PowerShell | ShellKind::Pwsh => Some(format!("'{}'", arg.replace('\'', "''"))),
706        ShellKind::Cmd => Some(arg.to_string()),
707        ShellKind::Posix
708        | ShellKind::Csh
709        | ShellKind::Tcsh
710        | ShellKind::Fish
711        | ShellKind::Nushell
712        | ShellKind::Rc
713        | ShellKind::Xonsh
714        | ShellKind::Elvish => shell_kind.try_quote(arg).map(Cow::into_owned),
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721    use pretty_assertions::assert_eq;
722
723    fn prepared_cmd_task(command_arg: &str) -> SpawnInTerminal {
724        SpawnInTerminal {
725            command: Some("cmd.exe".to_string()),
726            args: vec!["/S".to_string(), "/C".to_string(), command_arg.to_string()],
727            shell: Shell::Program("cmd.exe".to_string()),
728            ..SpawnInTerminal::default()
729        }
730    }
731
732    #[test]
733    fn formats_prepared_cmd_task_for_powershell_activation() {
734        let task = prepared_cmd_task("\"echo Hi there\"");
735
736        assert_eq!(
737            format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
738            "&cmd.exe /S /C '\"echo Hi there\"'"
739        );
740    }
741
742    #[test]
743    fn formats_prepared_cmd_task_for_cmd_activation() {
744        let task = prepared_cmd_task("\"echo Hi there\"");
745
746        assert_eq!(
747            format_task_for_activation(&task, ShellKind::Cmd, "cmd.exe", true),
748            "cmd.exe /S /C \"echo Hi there\""
749        );
750    }
751
752    #[test]
753    fn formats_prepared_cmd_task_with_shell_args_for_activation() {
754        let task = SpawnInTerminal {
755            command: Some("cmd.exe".to_string()),
756            args: vec![
757                "/D".to_string(),
758                "/S".to_string(),
759                "/C".to_string(),
760                "\"echo Hi there\"".to_string(),
761            ],
762            shell: Shell::WithArguments {
763                program: "cmd.exe".to_string(),
764                args: vec!["/D".to_string()],
765                title_override: None,
766            },
767            ..SpawnInTerminal::default()
768        };
769
770        assert_eq!(
771            format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
772            "&cmd.exe /D /S /C '\"echo Hi there\"'"
773        );
774    }
775
776    #[test]
777    fn formats_prepared_cmd_task_with_single_quote_for_powershell_activation() {
778        let task = prepared_cmd_task("\"echo It's fine\"");
779
780        assert_eq!(
781            format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
782            "&cmd.exe /S /C '\"echo It''s fine\"'"
783        );
784    }
785
786    #[test]
787    fn formats_non_cmd_task_for_activation() {
788        let task = SpawnInTerminal {
789            command: Some("cargo".to_string()),
790            args: vec!["test".to_string(), "some test".to_string()],
791            shell: Shell::System,
792            ..SpawnInTerminal::default()
793        };
794
795        assert_eq!(
796            format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
797            "&cargo test 'some test'"
798        );
799    }
800}
801
Served at tenant.openagents/omega Member data and write actions are omitted.