diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-08-13 03:00:35 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-13 03:00:35 +0300 |
| commit | a4d52ab60282b2ad021cb90978a7ed4d3898704b (patch) | |
| tree | cdd5f55d6d392d54541b86da7f1a00a48c211d03 /src/detect | |
| parent | 8b635c918066dd184bacc29b4c06c40a3efea3f5 (diff) | |
feat: add qwen code detection and restore (#2743)
refs #2730
Diffstat (limited to 'src/detect')
| -rw-r--r-- | src/detect/manifest.rs | 1 | ||||
| -rw-r--r-- | src/detect/manifests/qwen.toml | 89 | ||||
| -rw-r--r-- | src/detect/mod.rs | 54 |
3 files changed, 141 insertions, 3 deletions
diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index ed3de507..b9fe786a 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -255,6 +255,7 @@ const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("opencode", include_str!("manifests/opencode.toml")), ("pi", include_str!("manifests/pi.toml")), ("qodercli", include_str!("manifests/qodercli.toml")), + ("qwen", include_str!("manifests/qwen.toml")), ("copilot", include_str!("manifests/github-copilot.toml")), ]; diff --git a/src/detect/manifests/qwen.toml b/src/detect/manifests/qwen.toml new file mode 100644 index 00000000..52ab242a --- /dev/null +++ b/src/detect/manifests/qwen.toml @@ -0,0 +1,89 @@ +id = "qwen" +version = "2026.08.12.1" +min_engine_version = 2 +updated_at = "2026-08-12T00:00:00Z" +aliases = ["qwen-code", "qwen code"] + +# Qwen Code keeps its composer visible while responding, so the prompt box is +# not idle evidence by itself. Active turns show an "esc to cancel" status +# line; confirmation flows replace that line with a stable waiting phrase. +# OSC 9;4 is supplemental because Qwen emits it only while a tool is executing +# and only in terminals that advertise progress support. + +[[rules]] +id = "waiting_for_confirmation" +state = "blocked" +priority = 1000 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["waiting for user confirmation"] + +[[rules]] +id = "tool_confirmation" +state = "blocked" +priority = 990 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["yes, allow once"] +any = [ + { contains = ["apply this change?"] }, + { contains = ["allow execution of:"] }, + { contains = ["allow execution of mcp tool"] }, + { contains = ["do you want to proceed?"] }, + { contains = ["shell command execution"] }, +] + +[[rules]] +id = "question_dialog" +state = "blocked" +priority = 980 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["enter: select", "esc: cancel"] +line_regex = ['^\s*[❯›]\s*1\.\s+'] + +[[rules]] +id = "folder_trust_dialog" +state = "blocked" +priority = 970 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["do you trust this folder?", "trust folder (", "don't trust (esc)"] + +[[rules]] +id = "cancel_hint_working" +state = "working" +priority = 900 +region = "bottom_non_empty_lines(8)" +visible_working = true +line_regex = ['^\s*(?:[⠁-⣿]|\.{1,2})\s+.*\(\d+(?:m(?:\s+\d+s)?|s).*\s·\sesc to cancel\)\s*$'] + +[[rules]] +id = "narrow_cancel_hint_working" +state = "working" +priority = 890 +region = "bottom_non_empty_lines(8)" +visible_working = true +line_regex = ['(?i)^\s*\(esc to cancel\)\s*$'] + +[[rules]] +id = "osc_tool_progress_working" +state = "working" +priority = 850 +region = "osc_progress" +visible_working = true +regex = ['^4;3(?:;|$)'] + +[[rules]] +id = "composer_idle" +state = "idle" +priority = 100 +region = "bottom_non_empty_lines(30)" +visible_idle = true +line_regex = ['^\s*>\s*(?:type\s*)?.*$'] +any = [ + { contains = ["type your message"] }, + { contains = ["your message or @path/to/file"] }, + { contains = ["@path/to/file"] }, + { contains = ["type", "mes", "sage", "@pat", "h/to", "/fil"] }, +] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index b9f9655f..f78567f6 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -61,11 +61,12 @@ pub enum Agent { Hermes, Kilo, Qodercli, + Qwen, Maki, } impl Agent { - pub const ALL: [Self; 21] = [ + pub const ALL: [Self; 22] = [ Self::Pi, Self::Claude, Self::Codex, @@ -86,10 +87,11 @@ impl Agent { Self::Hermes, Self::Kilo, Self::Qodercli, + Self::Qwen, Self::Maki, ]; - pub const SCREEN_MANIFEST_AGENTS: [Self; 19] = [ + pub const SCREEN_MANIFEST_AGENTS: [Self; 20] = [ Self::Pi, Self::Claude, Self::Codex, @@ -108,6 +110,7 @@ impl Agent { Self::Hermes, Self::Kilo, Self::Qodercli, + Self::Qwen, Self::Maki, ]; } @@ -134,6 +137,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::Hermes => "hermes", Agent::Kilo => "kilo", Agent::Qodercli => "qodercli", + Agent::Qwen => "qwen", Agent::Maki => "maki", } } @@ -166,6 +170,7 @@ pub fn interactive_agent_executable(agent: Agent) -> &'static str { Agent::Hermes => "hermes", Agent::Kilo => "kilo", Agent::Qodercli => "qodercli", + Agent::Qwen => "qwen", Agent::Maki => "maki", } } @@ -202,6 +207,7 @@ fn lookup_agent(name: &str) -> Option<Agent> { "hermes" | "hermes-agent" => Some(Agent::Hermes), "kilo" | "kilo-code" | "kilo code" => Some(Agent::Kilo), "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), + "qwen" | "qwen-code" | "qwen code" => Some(Agent::Qwen), "maki" => Some(Agent::Maki), _ => None, } @@ -301,7 +307,7 @@ pub(crate) fn full_lifecycle_hook_authority(source: &str, agent_label: &str) -> pub(crate) fn session_identity_only_integration(source: &str, agent_label: &str) -> bool { matches!( (source, agent_label), - ("herdr:hermes", "hermes") | ("herdr:antigravity_cli", "agy") + ("herdr:hermes", "hermes") | ("herdr:qwen", "qwen") | ("herdr:antigravity_cli", "agy") ) } @@ -345,6 +351,19 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri return effective.to_string(); } + if let Some(runtime) = process.argv.as_deref().and_then(|argv| argv.first()) { + let runtime_name = normalized_agent_lookup_name(path_basename(runtime)); + if matches!(runtime_name.as_str(), "node" | "bun") { + if let Some(wrapped_agent) = + wrapped_agent_name_from_runtime_argv(runtime, process.argv.as_deref()) + { + if identify_agent(&wrapped_agent) == Some(Agent::Qwen) { + return wrapped_agent; + } + } + } + } + if let Some(wrapped_agent) = argv0_agent_name(process.argv.as_deref()) .or_else(|| cmdline_argv0_agent_name(process.cmdline.as_deref().unwrap_or_default())) { @@ -550,6 +569,9 @@ fn agent_name_from_known_package_path(path: &str) -> Option<String> { { return Some(agent_label(Agent::Pi).to_string()); } + if window == ["node_modules", "@qwen-code", "qwen-code", "dist", "index"] { + return Some(agent_label(Agent::Qwen).to_string()); + } } for window in components.windows(4) { if window == ["node_modules", "mastracode", "dist", "cli"] { @@ -709,6 +731,8 @@ mod tests { assert_eq!(identify_agent("hermes-agent"), Some(Agent::Hermes)); assert_eq!(identify_agent("kilo"), Some(Agent::Kilo)); assert_eq!(identify_agent("kilo-code"), Some(Agent::Kilo)); + assert_eq!(identify_agent("qwen"), Some(Agent::Qwen)); + assert_eq!(identify_agent("Qwen Code"), Some(Agent::Qwen)); assert_eq!(identify_agent("maki"), Some(Agent::Maki)); } @@ -734,6 +758,7 @@ mod tests { assert_eq!(parse_agent_label("kiro-cli"), Some(Agent::Kiro)); assert_eq!(parse_agent_label("grok-build"), Some(Agent::Grok)); assert_eq!(parse_agent_label("hermes-agent"), Some(Agent::Hermes)); + assert_eq!(parse_agent_label("qwen-code"), Some(Agent::Qwen)); assert_eq!(parse_agent_label("maki"), Some(Agent::Maki)); assert_eq!(parse_agent_label("kilo-code"), Some(Agent::Kilo)); } @@ -777,6 +802,7 @@ mod tests { (Agent::Hermes, "hermes"), (Agent::Kilo, "kilo"), (Agent::Qodercli, "qodercli"), + (Agent::Qwen, "qwen"), (Agent::Maki, "maki"), ]; assert_eq!(expected.len(), Agent::ALL.len()); @@ -806,6 +832,7 @@ mod tests { fn session_identity_integrations_leave_state_to_screen_detection() { for (source, label, agent) in [ ("herdr:hermes", "hermes", Agent::Hermes), + ("herdr:qwen", "qwen", Agent::Qwen), ("herdr:antigravity_cli", "agy", Agent::Antigravity), ] { assert!(!full_lifecycle_hook_authority(source, label)); @@ -847,6 +874,27 @@ mod tests { } #[test] + fn identify_agent_in_job_detects_node_wrapped_qwen() { + for argv in [ + vec!["node", "/home/user/.fnm/bin/qwen"], + vec![ + "node.exe", + r"C:\Users\user\AppData\Roaming\npm\node_modules\@qwen-code\qwen-code\dist\index.js", + ], + ] { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process(123, "MainThread", &argv)], + }; + + assert_eq!( + identify_agent_in_job(&job), + Some((Agent::Qwen, "qwen".to_string())) + ); + } + } + + #[test] fn identify_agent_in_job_prefers_recognized_process_group_leader() { let job = crate::platform::ForegroundJob { process_group_id: 42, |
