diff options
| author | Eric Yue <yue1995pro@gmail.com> | 2026-06-15 21:50:24 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-15 16:50:24 +0300 |
| commit | 07261e09be60ac0c145685fd11f94351cc0e5ba6 (patch) | |
| tree | 771b0180c7a1560189547e6d23ef004ec1cf6d5c /src/detect | |
| parent | 08bb0768ae224c8db04bbe016b050353ce13eee7 (diff) | |
feat: add devin cli detection and restore support (#622)
* feat: add devin cli detection and restore support
refs #606
* fix: keep devin integration session-only
---------
Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/detect')
| -rw-r--r-- | src/detect/manifest.rs | 1 | ||||
| -rw-r--r-- | src/detect/manifest/tests.rs | 60 | ||||
| -rw-r--r-- | src/detect/manifests/devin.toml | 86 | ||||
| -rw-r--r-- | src/detect/mod.rs | 12 |
4 files changed, 158 insertions, 1 deletions
diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index 06ed4df6..a6da1277 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -243,6 +243,7 @@ const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("cline", include_str!("manifests/cline.toml")), ("codex", include_str!("manifests/codex.toml")), ("cursor", include_str!("manifests/cursor.toml")), + ("devin", include_str!("manifests/devin.toml")), ("droid", include_str!("manifests/droid.toml")), ("gemini", include_str!("manifests/gemini.toml")), ("grok", include_str!("manifests/grok.toml")), diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index 8811777b..2570aa03 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -297,6 +297,7 @@ fn all_bundled_manifests_parse_and_validate() { Agent::Codex, Agent::Gemini, Agent::Cursor, + Agent::Devin, Agent::Antigravity, Agent::Cline, Agent::OpenCode, @@ -321,6 +322,65 @@ fn all_bundled_manifests_parse_and_validate() { } #[test] +fn devin_manifest_detects_idle_working_and_blocked_states() { + let idle = explain( + Agent::Devin, + "─────────────────────────────────────────────────────\n❭ Ask Devin to build features, fix bugs, or work on\n your code\n─────────────────────────────────────────────────────\nSWE-1.6 Context: 16k / 200k tokens (7%)", + ); + assert_eq!(idle.state, AgentState::Idle); + assert!(idle.visible_idle); + + let live_footer_idle = explain( + Agent::Devin, + "Done.\n\n────────────────────────────────────────────────── (bypass permissions on) ─\n❭\n────────────────────────────────────────────────────────────────────────────\nClaude Opus 4.6 Thinking Context: 38k / 200k tokens (18%)", + ); + assert_eq!(live_footer_idle.state, AgentState::Idle); + assert_eq!( + live_footer_idle + .matched_rule + .as_ref() + .map(|rule| rule.id.as_str()), + Some("live_prompt_footer") + ); + assert!(live_footer_idle.visible_idle); + + let welcome_footer_idle = explain( + Agent::Devin, + "⠀⠀⠀⠀⠀⣴⣾⣶⡄⠀⠀⠀⠀\n⠀⣴⣾⣶⡾⠛⠿⠟⠃⣴⣾⣶⡄ Devin CLI\n⠀⠛⠿⠟⠃⣴⣾⣶⡾⠛⠿⠟⠃ v2026.5.26-8\n⠀⣤⣶⣦⡄⠻⢿⠿⢷⣤⣶⣦⡄\n⠀⠻⢿⠿⢷⣤⣶⣦⡄⠻⢿⠿⠃ Hybrid\n⠀⠀⠀⠀⠀⠻⢿⠿⠃⠀⠀⠀⠀\n\n───────────────────────────\n❭ Ask Devin to build\n features, fix bugs, or\n work on your code\n───────────────────────────\nClaude Opus Looking for\n4.6 Thinkingplan mode? /\n plan", + ); + assert_eq!(welcome_footer_idle.state, AgentState::Idle); + assert_eq!( + welcome_footer_idle + .matched_rule + .as_ref() + .map(|rule| rule.id.as_str()), + Some("welcome_prompt_footer") + ); + assert!(welcome_footer_idle.visible_idle); + + let working = explain( + Agent::Devin, + "◔ Reading shell 91b655\n │ Timeout: 35s\n\n⠀⡆ Running tools · 27s (esc to interrupt)\n─────────────────────────────────────────────────────\n❭ Guide Devin while it works", + ); + assert_eq!(working.state, AgentState::Working); + assert!(working.visible_working); + + let trust_prompt = explain( + Agent::Devin, + "Do you trust the authors of this directory?\nFor security, devin should not be run in directories\nwith untrusted content.\n❭ 1 Yes, trust /private/tmp/devin-hook-probe\n· 2 No, exit", + ); + assert_eq!(trust_prompt.state, AgentState::Blocked); + assert!(trust_prompt.visible_blocker); + + let permission_prompt = explain( + Agent::Devin, + "⏺ Running command\n └ $ sleep 30\n\n❭ 1 Yes (Approve once)\n· 2 Yes, allow `sleep` commands\n· 3 Yes, always allow `sleep` commands\n· 4 No\n↑↓ select · ↵ confirm · esc cancel", + ); + assert_eq!(permission_prompt.state, AgentState::Blocked); + assert!(permission_prompt.visible_blocker); +} + +#[test] fn manifest_validation_rejects_unknown_fields_empty_rules_invalid_regions_and_regexes() { assert!(parse_manifest( r#" diff --git a/src/detect/manifests/devin.toml b/src/detect/manifests/devin.toml new file mode 100644 index 00000000..c9564f7c --- /dev/null +++ b/src/detect/manifests/devin.toml @@ -0,0 +1,86 @@ +id = "devin" +version = "2026.06.15.1" +min_engine_version = 1 +updated_at = "2026-06-15T00:00:00Z" +aliases = ["devin-cli", "devin cli"] + +[[rules]] +id = "workspace_trust_prompt" +state = "blocked" +priority = 300 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +contains = [ + "do you trust the authors of this directory?", + "with untrusted content.", + "yes, trust ", +] + +[[rules]] +id = "permission_prompt" +state = "blocked" +priority = 290 +region = "bottom_non_empty_lines(8)" +visible_blocker = true +contains = ["approve once", "select", "confirm", "esc cancel"] + +[[rules]] +id = "running_tools_footer" +state = "working" +priority = 200 +region = "bottom_non_empty_lines(8)" +visible_working = true +contains = ["running tools", "esc to interrupt"] +not = [ + { contains = ["approve once", "esc cancel"] }, +] + +[[rules]] +id = "guide_while_working" +state = "working" +priority = 190 +region = "bottom_non_empty_lines(6)" +visible_working = true +contains = ["guide devin while it works"] +not = [ + { contains = ["approve once", "esc cancel"] }, +] + +[[rules]] +id = "tool_reading_timeout" +state = "working" +priority = 180 +region = "bottom_non_empty_lines(8)" +visible_working = true +contains = ["reading shell ", "timeout:"] +not = [ + { contains = ["approve once", "esc cancel"] }, +] + +[[rules]] +id = "welcome_prompt_footer" +state = "idle" +priority = 120 +region = "bottom_non_empty_lines(8)" +visible_idle = true +contains = ["ask devin to build", "features, fix bugs", "your code"] +line_regex = ['^\s*❭ Ask Devin to build'] +not = [ + { contains = ["approve once", "esc cancel"] }, + { contains = ["running tools", "esc to interrupt"] }, + { contains = ["guide devin while it works"] }, +] + +[[rules]] +id = "live_prompt_footer" +state = "idle" +priority = 100 +region = "bottom_non_empty_lines(6)" +visible_idle = true +contains = ["context:"] +line_regex = ['^\s*❭'] +not = [ + { contains = ["approve once", "esc cancel"] }, + { contains = ["running tools", "esc to interrupt"] }, + { contains = ["guide devin while it works"] }, +] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index 6a8a25c5..66443877 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -46,6 +46,7 @@ pub enum Agent { Codex, Gemini, Cursor, + Devin, Antigravity, Cline, OpenCode, @@ -61,12 +62,13 @@ pub enum Agent { } impl Agent { - pub const ALL: [Self; 17] = [ + pub const ALL: [Self; 18] = [ Self::Pi, Self::Claude, Self::Codex, Self::Gemini, Self::Cursor, + Self::Devin, Self::Antigravity, Self::Cline, Self::OpenCode, @@ -89,6 +91,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::Codex => "codex", Agent::Gemini => "gemini", Agent::Cursor => "cursor", + Agent::Devin => "devin", Agent::Antigravity => "agy", Agent::Cline => "cline", Agent::OpenCode => "opencode", @@ -112,6 +115,7 @@ pub fn parse_agent_label(agent: &str) -> Option<Agent> { "codex" => Some(Agent::Codex), "gemini" => Some(Agent::Gemini), "cursor" | "cursor-agent" => Some(Agent::Cursor), + "devin" | "devin-cli" | "devin cli" => Some(Agent::Devin), "agy" | "antigravity" | "antigravity-cli" => Some(Agent::Antigravity), "cline" => Some(Agent::Cline), "opencode" | "open-code" => Some(Agent::OpenCode), @@ -139,6 +143,7 @@ pub fn identify_agent(process_name: &str) -> Option<Agent> { "codex" => Some(Agent::Codex), "gemini" => Some(Agent::Gemini), "cursor" | "cursor-agent" => Some(Agent::Cursor), + "devin" | "devin-cli" | "devin cli" => Some(Agent::Devin), "agy" | "antigravity" | "antigravity-cli" => Some(Agent::Antigravity), "cline" => Some(Agent::Cline), "opencode" | "open-code" => Some(Agent::OpenCode), @@ -582,6 +587,8 @@ mod tests { assert_eq!(identify_agent("gemini"), Some(Agent::Gemini)); assert_eq!(identify_agent("cursor"), Some(Agent::Cursor)); assert_eq!(identify_agent("cursor-agent"), Some(Agent::Cursor)); + assert_eq!(identify_agent("devin"), Some(Agent::Devin)); + assert_eq!(identify_agent("devin-cli"), Some(Agent::Devin)); assert_eq!(identify_agent("agy"), Some(Agent::Antigravity)); assert_eq!(identify_agent("antigravity-cli"), Some(Agent::Antigravity)); assert_eq!(identify_agent("cline"), Some(Agent::Cline)); @@ -606,6 +613,7 @@ mod tests { assert_eq!(parse_agent_label("pi"), Some(Agent::Pi)); assert_eq!(parse_agent_label("claude"), Some(Agent::Claude)); assert_eq!(parse_agent_label("cursor-agent"), Some(Agent::Cursor)); + assert_eq!(parse_agent_label("devin-cli"), Some(Agent::Devin)); assert_eq!(parse_agent_label("agy"), Some(Agent::Antigravity)); assert_eq!(parse_agent_label("antigravity"), Some(Agent::Antigravity)); assert_eq!(parse_agent_label("opencode.exe"), Some(Agent::OpenCode)); @@ -627,6 +635,7 @@ mod tests { assert_eq!(agent_label(Agent::Pi), "pi"); assert_eq!(agent_label(Agent::GithubCopilot), "copilot"); assert_eq!(agent_label(Agent::OpenCode), "opencode"); + assert_eq!(agent_label(Agent::Devin), "devin"); assert_eq!(agent_label(Agent::Antigravity), "agy"); assert_eq!(agent_label(Agent::Kiro), "kiro"); assert_eq!(agent_label(Agent::Grok), "grok"); @@ -647,6 +656,7 @@ mod tests { assert_eq!(identify_agent("Pi"), Some(Agent::Pi)); assert_eq!(identify_agent("CLAUDE"), Some(Agent::Claude)); assert_eq!(identify_agent("Codex"), Some(Agent::Codex)); + assert_eq!(identify_agent("Devin"), Some(Agent::Devin)); } #[test] |
