diff options
| author | qer <wbxl2000@outlook.com> | 2026-06-04 18:52:29 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-04 13:52:29 +0300 |
| commit | 66b0f6b2d6e1c63c3a7aa0d26cff91cf750232aa (patch) | |
| tree | c114ee4571705357540d7df3ac94349234c77ff3 /src/detect/agents | |
| parent | cb18fe52b55f1a627387c829074e9d24b9cefb28 (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.rs | 101 | ||||
| -rw-r--r-- | src/detect/agents/mod.rs | 3 |
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, } } |
