aboutsummaryrefslogtreecommitdiff
path: root/src/detect
diff options
context:
space:
mode:
authorEric Yue <yue1995pro@gmail.com>2026-06-15 21:50:24 +0800
committerGitHub <noreply@github.com>2026-06-15 16:50:24 +0300
commit07261e09be60ac0c145685fd11f94351cc0e5ba6 (patch)
tree771b0180c7a1560189547e6d23ef004ec1cf6d5c /src/detect
parent08bb0768ae224c8db04bbe016b050353ce13eee7 (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.rs1
-rw-r--r--src/detect/manifest/tests.rs60
-rw-r--r--src/detect/manifests/devin.toml86
-rw-r--r--src/detect/mod.rs12
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]