aboutsummaryrefslogtreecommitdiff
path: root/src/detect/agents
diff options
context:
space:
mode:
authorqer <wbxl2000@outlook.com>2026-06-04 18:52:29 +0800
committerGitHub <noreply@github.com>2026-06-04 13:52:29 +0300
commit66b0f6b2d6e1c63c3a7aa0d26cff91cf750232aa (patch)
treec114ee4571705357540d7df3ac94349234c77ff3 /src/detect/agents
parentcb18fe52b55f1a627387c829074e9d24b9cefb28 (diff)
feat: add kimi code cli integration (#463)
* feat: add kimi code cli integration refs #431 * fix: improve kimi code screen detection refs #431
Diffstat (limited to 'src/detect/agents')
-rw-r--r--src/detect/agents/kimi.rs101
-rw-r--r--src/detect/agents/mod.rs3
2 files changed, 103 insertions, 1 deletions
diff --git a/src/detect/agents/kimi.rs b/src/detect/agents/kimi.rs
index 73d1a8b0..b42bf885 100644
--- a/src/detect/agents/kimi.rs
+++ b/src/detect/agents/kimi.rs
@@ -12,7 +12,23 @@ pub(super) fn detect(content: &str) -> AgentState {
AgentState::Idle
}
+pub(super) fn has_visible_blocker(content: &str) -> bool {
+ has_current_approval_panel(content) || has_question_panel(content)
+}
+
+pub(super) fn has_prompt_box(content: &str) -> bool {
+ has_editor_prompt_box(content) && has_footer_context(content)
+}
+
+pub(super) fn has_visible_working(content: &str) -> bool {
+ has_kimi_working_status(content)
+}
+
fn has_kimi_blocked_prompt(content: &str) -> bool {
+ if has_visible_blocker(content) {
+ return true;
+ }
+
let lower = content.to_lowercase();
lower.contains("requesting approval")
&& (lower.contains("approve once") || lower.contains("approve for this session"))
@@ -43,6 +59,89 @@ fn has_kimi_working_status(content: &str) -> bool {
.trim_start_matches(|c| ('\u{2800}'..='\u{28FF}').contains(&c))
.trim_start()
.to_lowercase();
- rest.starts_with("thinking...") || rest.starts_with("using ")
+ rest.starts_with("thinking...")
+ || rest.starts_with("working...")
+ || rest.starts_with("using ")
})
}
+
+fn has_current_approval_panel(content: &str) -> bool {
+ let lower = content.to_lowercase();
+ has_approval_title(&lower)
+ && has_numeric_choose_hint(&lower)
+ && lower.contains("↵ confirm")
+ && (lower.contains("approve") || lower.contains("reject") || lower.contains("revise"))
+}
+
+fn has_approval_title(lower_content: &str) -> bool {
+ lower_content.contains("run this command?")
+ || lower_content.contains("write this file?")
+ || lower_content.contains("apply these edits?")
+ || lower_content.contains("stop this task?")
+ || lower_content.contains("ready to build with this plan?")
+ || lower_content.lines().any(|line| {
+ let trimmed = line.trim_start_matches(|c: char| c == '▶' || c.is_whitespace());
+ trimmed.starts_with("approve ") && trimmed.ends_with('?')
+ })
+}
+
+fn has_question_panel(content: &str) -> bool {
+ let lower = content.to_lowercase();
+ content.lines().any(|line| line.trim() == "question")
+ && content
+ .lines()
+ .any(|line| line.trim_start().starts_with("? "))
+ && lower.contains("↑↓ select")
+ && (lower.contains("↵ choose") || lower.contains("↵ toggle") || lower.contains("↵ save"))
+ && lower.contains("esc cancel")
+}
+
+fn has_numeric_choose_hint(lower_content: &str) -> bool {
+ lower_content.contains(" choose") && lower_content.contains('1') && lower_content.contains('2')
+}
+
+fn has_editor_prompt_box(content: &str) -> bool {
+ let lines: Vec<&str> = content.lines().collect();
+
+ for top_index in 0..lines.len() {
+ if !is_editor_top_border(lines[top_index]) {
+ continue;
+ }
+
+ let mut saw_prompt = false;
+ for line in lines.iter().skip(top_index + 1) {
+ if is_editor_bottom_border(line) {
+ if saw_prompt {
+ return true;
+ }
+ break;
+ }
+
+ saw_prompt |= is_editor_prompt_line(line);
+ }
+ }
+
+ false
+}
+
+fn is_editor_top_border(line: &str) -> bool {
+ let trimmed = line.trim();
+ ((trimmed.starts_with('╭') && trimmed.ends_with('╮'))
+ || (trimmed.starts_with('├') && trimmed.ends_with('┤')))
+ && trimmed.contains('─')
+}
+
+fn is_editor_bottom_border(line: &str) -> bool {
+ let trimmed = line.trim();
+ trimmed.starts_with('╰') && trimmed.ends_with('╯') && trimmed.contains('─')
+}
+
+fn is_editor_prompt_line(line: &str) -> bool {
+ let trimmed = line.trim_start();
+ let inner = trimmed.strip_prefix('│').unwrap_or(trimmed).trim_start();
+ inner.starts_with('>')
+}
+
+fn has_footer_context(content: &str) -> bool {
+ content.to_lowercase().contains("context: ")
+}
diff --git a/src/detect/agents/mod.rs b/src/detect/agents/mod.rs
index a89b2f37..46a25f66 100644
--- a/src/detect/agents/mod.rs
+++ b/src/detect/agents/mod.rs
@@ -79,6 +79,7 @@ fn has_visible_blocker(agent: Agent, content: &str, state: AgentState) -> bool {
// is known to be structural and live.
Agent::Claude => claude_code::has_visible_blocker(content),
Agent::Codex => codex::has_visible_blocker(content),
+ Agent::Kimi => kimi::has_visible_blocker(content),
_ => false,
}
}
@@ -91,6 +92,7 @@ fn has_visible_idle(agent: Agent, content: &str, state: AgentState) -> bool {
match agent {
Agent::Claude => claude_code::has_prompt_box(content),
Agent::Codex => codex::has_prompt(content),
+ Agent::Kimi => kimi::has_prompt_box(content),
_ => false,
}
}
@@ -103,6 +105,7 @@ fn has_visible_working(agent: Agent, content: &str, state: AgentState) -> bool {
match agent {
Agent::Claude => claude_code::has_working_chrome(content),
Agent::Codex => codex::has_visible_working(content),
+ Agent::Kimi => kimi::has_visible_working(content),
_ => false,
}
}