diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index ac30bfb06c..f2ff49a039 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Fixed +- Windows panes now keep bare `cursor-agent` launches detected after Cursor hands off to its bundled Node process. (#3032) - Claude Code panes now use visible turn, background shell, and background agent activity as working-state fallbacks when OSC titles are unavailable or disabled. (#1630, #2241) - Tab bar status commands now remove ESC-prefixed terminal control sequences instead of displaying their sequence bodies as text. (#3001) - Unix plugin pane commands now default `PWD` to their resolved working directory, so direct popup tools open at explicit `--cwd` paths while preserving caller-provided `PWD` values. (#2984) diff --git a/src/detect/mod.rs b/src/detect/mod.rs index f78567f6ac..def440dcf0 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -378,7 +378,9 @@ fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) let runtime_name = normalized_agent_lookup_name(path_basename(runtime)); match runtime_name.as_str() { - "node" | "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]), + "node" => cursor_agent_name_from_bundled_node_argv(argv) + .or_else(|| script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[])), + "bun" => script_arg_agent_name(argv, &["-e", "--eval", "-p", "--print"], &[]), name if is_python_runtime(name) => script_arg_agent_name(argv, &["-c"], &["-m"]), "sh" | "bash" | "zsh" | "fish" => script_arg_agent_name(argv, &["-c"], &[]), "cmd" => windows_cmd_arg_agent_name(argv), @@ -388,6 +390,36 @@ fn wrapped_agent_name_from_runtime_argv(runtime: &str, argv: Option<&[String]>) } } +fn cursor_agent_name_from_bundled_node_argv(argv: &[String]) -> Option { + let (runtime_parent, runtime_name) = path_parent_and_basename(argv.first()?)?; + let (script_parent, script_name) = path_parent_and_basename(argv.get(1)?)?; + if !runtime_name.eq_ignore_ascii_case("node.exe") + || !script_name.eq_ignore_ascii_case("index.js") + || !runtime_parent.eq_ignore_ascii_case(script_parent) + { + return None; + } + + let mut tail = runtime_parent + .rsplit(['/', '\\']) + .filter(|component| !component.is_empty()); + let (Some(version), Some(versions), Some(package)) = (tail.next(), tail.next(), tail.next()) + else { + return None; + }; + (package.eq_ignore_ascii_case("cursor-agent") + && versions.eq_ignore_ascii_case("versions") + && !version.trim().is_empty()) + .then(|| agent_label(Agent::Cursor).to_string()) +} + +fn path_parent_and_basename(path: &str) -> Option<(&str, &str)> { + let split = path.rfind(['/', '\\'])?; + let parent = path[..split].trim_end_matches(['/', '\\']); + let basename = &path[split + 1..]; + (!parent.is_empty() && !basename.is_empty()).then_some((parent, basename)) +} + fn windows_cmd_arg_agent_name(argv: &[String]) -> Option { let mut args = argv.iter().skip(1); while let Some(arg) = args.next() { @@ -894,6 +926,62 @@ mod tests { } } + #[test] + fn identify_agent_in_job_detects_windows_cursor_install() { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 123, + "node.exe", + &[ + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\node.exe", + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index.js", + ], + )], + }; + + assert_eq!( + identify_agent_in_job(&job), + Some((Agent::Cursor, "cursor".to_string())) + ); + } + + #[test] + fn identify_agent_in_job_ignores_invalid_windows_cursor_install_paths() { + for script in [ + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\scripts\postinstall.js", + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index", + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\index.exe", + ] { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 123, + "node.exe", + &[ + r"C:\Users\user\AppData\Local\cursor-agent\versions\2026.08.11-e8db854\node.exe", + script, + ], + )], + }; + + assert_eq!(identify_agent_in_job(&job), None, "script: {script}"); + } + + let lookalike = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process( + 123, + "node.exe", + &[ + r"C:\Program Files\nodejs\node.exe", + r"C:\workspace\cursor-agent\versions\test\index.js", + ], + )], + }; + assert_eq!(identify_agent_in_job(&lookalike), None); + } + #[test] fn identify_agent_in_job_prefers_recognized_process_group_leader() { let job = crate::platform::ForegroundJob {