From 43124bf62a253dbb59cd4ad01c5071de02a42e4d Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Wed, 20 May 2026 02:53:45 +0300 Subject: [PATCH] feat: detect kiro cli agents refs #185 --- docs/next/CHANGELOG.md | 1 + docs/next/README.md | 1 + docs/next/website/src/content/docs/agents.mdx | 1 + src/config/sound.rs | 3 + src/detect.rs | 73 +++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 43af669f..36188a11 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Added an integrations tab in settings and first-run onboarding so users can install recommended agent integrations from inside Herdr. - Added `terminal.default_shell` to choose the executable used for new interactive panes. When unset, Herdr still falls back to `$SHELL`, then `/bin/sh`. (#196) +- Added native Kiro CLI detection with idle and working state heuristics. (#185) ### Fixed - Remote clients now bridge local clipboard images into the remote pane by staging them as temporary image files and pasting the remote path, so Claude Code image paste works over `herdr --remote`. (#205) diff --git a/docs/next/README.md b/docs/next/README.md index 68f2f112..f32fbca9 100644 --- a/docs/next/README.md +++ b/docs/next/README.md @@ -179,6 +179,7 @@ automatic detection works out of the box. process name matching plus terminal ou | [opencode](https://github.com/anomalyco/opencode) | ✓ | ✓ | ✓ | | [grok cli](https://x.ai/grok) | ✓ | ✓ | ✓ | | [hermes agent](https://github.com/NousResearch/hermes-agent) | ✓ | ✓ | ✓ | +| [kiro cli](https://kiro.dev/docs/cli/) | ✓ | ✓ | — | detected but not fully tested: gemini cli, cursor agent, cline, kimi, github copilot cli. diff --git a/docs/next/website/src/content/docs/agents.mdx b/docs/next/website/src/content/docs/agents.mdx index e3d2bc0c..f3e1a6c4 100644 --- a/docs/next/website/src/content/docs/agents.mdx +++ b/docs/next/website/src/content/docs/agents.mdx @@ -19,6 +19,7 @@ Automatic detection works out of the box for common coding agents. | OpenCode | yes | yes | yes | | Grok CLI | yes | yes | yes | | Hermes Agent | yes | yes | yes | +| Kiro CLI | yes | yes | no | | GitHub Copilot CLI | yes | yes | yes | Detected but less thoroughly tested: Gemini CLI, Cursor Agent, Cline, and Kimi. diff --git a/src/config/sound.rs b/src/config/sound.rs index 09d85c18..4ec8fd1e 100644 --- a/src/config/sound.rs +++ b/src/config/sound.rs @@ -34,6 +34,7 @@ pub struct AgentSoundOverrides { pub open_code: AgentSoundSetting, pub github_copilot: AgentSoundSetting, pub kimi: AgentSoundSetting, + pub kiro: AgentSoundSetting, pub droid: AgentSoundSetting, pub amp: AgentSoundSetting, pub grok: AgentSoundSetting, @@ -122,6 +123,7 @@ impl AgentSoundOverrides { Some(Agent::OpenCode) => self.open_code, Some(Agent::GithubCopilot) => self.github_copilot, Some(Agent::Kimi) => self.kimi, + Some(Agent::Kiro) => self.kiro, Some(Agent::Droid) => self.droid, Some(Agent::Amp) => self.amp, Some(Agent::Grok) => self.grok, @@ -155,6 +157,7 @@ impl Default for AgentSoundOverrides { open_code: AgentSoundSetting::Default, github_copilot: AgentSoundSetting::Default, kimi: AgentSoundSetting::Default, + kiro: AgentSoundSetting::Default, droid: AgentSoundSetting::Off, amp: AgentSoundSetting::Default, grok: AgentSoundSetting::Default, diff --git a/src/detect.rs b/src/detect.rs index 595e81f1..4f4cfbe1 100644 --- a/src/detect.rs +++ b/src/detect.rs @@ -28,6 +28,7 @@ pub enum Agent { OpenCode, GithubCopilot, Kimi, + Kiro, Droid, Amp, Grok, @@ -45,6 +46,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::OpenCode => "opencode", Agent::GithubCopilot => "copilot", Agent::Kimi => "kimi", + Agent::Kiro => "kiro", Agent::Droid => "droid", Agent::Amp => "amp", Agent::Grok => "grok", @@ -64,6 +66,7 @@ pub fn parse_agent_label(agent: &str) -> Option { "opencode" | "open-code" => Some(Agent::OpenCode), "copilot" | "github-copilot" | "ghcs" => Some(Agent::GithubCopilot), "kimi" => Some(Agent::Kimi), + "kiro" | "kiro-cli" => Some(Agent::Kiro), "droid" => Some(Agent::Droid), "amp" | "amp-local" => Some(Agent::Amp), "grok" | "grok-build" => Some(Agent::Grok), @@ -87,6 +90,7 @@ pub fn identify_agent(process_name: &str) -> Option { "opencode" | "open-code" => Some(Agent::OpenCode), "copilot" | "github-copilot" | "ghcs" => Some(Agent::GithubCopilot), "kimi" => Some(Agent::Kimi), + "kiro" | "kiro-cli" => Some(Agent::Kiro), "droid" => Some(Agent::Droid), "amp" | "amp-local" => Some(Agent::Amp), "grok" | "grok-build" => Some(Agent::Grok), @@ -141,6 +145,7 @@ pub fn detect_state(agent: Option, screen_content: &str) -> AgentState { Agent::OpenCode => detect_opencode(screen_content), Agent::GithubCopilot => detect_github_copilot(screen_content), Agent::Kimi => detect_kimi(screen_content), + Agent::Kiro => detect_kiro(screen_content), Agent::Droid => detect_droid(screen_content), Agent::Amp => detect_amp(screen_content), Agent::Grok => detect_grok(screen_content), @@ -364,6 +369,23 @@ fn detect_kimi(content: &str) -> AgentState { AgentState::Idle } +/// Kiro CLI detection. +/// +/// Kiro exposes reliable working and idle terminal markers. Confirmation +/// prompts currently render as normal inline conversation followed by the input +/// prompt, so they are intentionally treated as idle instead of guessing. +fn detect_kiro(content: &str) -> AgentState { + let lower = content.to_lowercase(); + + if lower.contains("kiro is working") + || (lower.contains("esc to cancel") && has_kiro_tool_spinner(content)) + { + return AgentState::Working; + } + + AgentState::Idle +} + /// Droid detection. /// /// Working: braille spinner line (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) + "Thinking..." + "(Press ESC to stop)" @@ -516,6 +538,21 @@ fn has_braille_spinner(content: &str) -> bool { false } +fn has_kiro_tool_spinner(content: &str) -> bool { + content.lines().any(|line| { + let trimmed = line.trim_start(); + let mut chars = trimmed.chars(); + let Some(first) = chars.next() else { + return false; + }; + if !matches!(first, '◔' | '◑' | '◕' | '●') { + return false; + } + let rest = chars.as_str().trim_start(); + rest.chars().next().is_some_and(char::is_alphabetic) + }) +} + // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- @@ -768,6 +805,8 @@ mod tests { assert_eq!(identify_agent("cline"), Some(Agent::Cline)); assert_eq!(identify_agent("opencode"), Some(Agent::OpenCode)); assert_eq!(identify_agent("kimi"), Some(Agent::Kimi)); + assert_eq!(identify_agent("kiro"), Some(Agent::Kiro)); + assert_eq!(identify_agent("kiro-cli"), Some(Agent::Kiro)); assert_eq!(identify_agent("copilot"), Some(Agent::GithubCopilot)); assert_eq!(identify_agent("ghcs"), Some(Agent::GithubCopilot)); assert_eq!(identify_agent("grok"), Some(Agent::Grok)); @@ -786,6 +825,7 @@ mod tests { Some(Agent::GithubCopilot) ); assert_eq!(parse_agent_label("amp-local"), Some(Agent::Amp)); + 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)); } @@ -795,6 +835,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::Kiro), "kiro"); assert_eq!(agent_label(Agent::Grok), "grok"); assert_eq!(agent_label(Agent::Hermes), "hermes"); } @@ -1344,6 +1385,38 @@ mod tests { assert_eq!(detect_kimi("> "), AgentState::Idle); } + // ---- Kiro ---- + + #[test] + fn kiro_working_on_status_bar() { + let screen = "◕ Shell\n esc to cancel\n● 1 MCP failure — see /mcp\n─────────────────────────────────────────────────────\nKiro · auto · ◔ 6% ~\n\n Kiro is working · type to queue a message"; + assert_eq!(detect_state(Some(Agent::Kiro), screen), AgentState::Working); + } + + #[test] + fn kiro_working_on_tool_spinner_and_cancel_hint() { + let screen = "◕ Shell\n esc to cancel\n─────────────────────────────────────────────────────\nKiro · auto · ◔ 6%"; + assert_eq!(detect_state(Some(Agent::Kiro), screen), AgentState::Working); + } + + #[test] + fn kiro_idle_at_prompt() { + let screen = "● 1 MCP failure — see /mcp\n──────────────────────────────────────────────────────────────────────────────────────\nKiro · auto · ◔ 6% ~\n\n ask a question or describe a task ↵\n /copy to clipboard"; + assert_eq!(detect_state(Some(Agent::Kiro), screen), AgentState::Idle); + } + + #[test] + fn kiro_does_not_treat_stale_failure_spinner_as_working() { + let screen = "● 1 MCP failure — see /mcp\n─────────────────────────────────────────────────────\nKiro · auto · ◔ 6%\n\n ask a question or describe a task ↵"; + assert_eq!(detect_state(Some(Agent::Kiro), screen), AgentState::Idle); + } + + #[test] + fn kiro_identified_by_process_name() { + assert_eq!(identify_agent("kiro"), Some(Agent::Kiro)); + assert_eq!(identify_agent("kiro-cli"), Some(Agent::Kiro)); + } + // ---- Droid ---- #[test]