Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ fn default_agent_args(command: &str) -> Option<Vec<String>> {
"goose" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
"kimi" => Some(vec!["acp".to_string()]),
_ => None,
}
}
Expand All @@ -477,6 +478,20 @@ pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<Strin
.into_iter()
.map(|arg| arg.trim().to_string())
.filter(|arg| !arg.is_empty())
.map(|arg| {
// Canonicalize a stray `--acp`/`-acp` flag form to the bare `acp`
// subcommand. Subcommand-style harnesses (kimi, omp, opencode,
// devin, cursor-agent, goose…) reject the flag form outright
// ("unknown option '--acp'"), and earlier releases persisted
// `--acp` into harness definitions and instance args. The flag
// form is never meaningful input for a flag-style CLI, so this is
// a pure repair with no behaviour change for flag-style agents.
if arg.eq_ignore_ascii_case("--acp") || arg.eq_ignore_ascii_case("-acp") {
"acp".to_string()
} else {
arg
}
})
.collect::<Vec<_>>();

let Some(default_args) = default_agent_args(command) else {
Expand Down
64 changes: 64 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,70 @@ fn normalizes_buzz_agent_args_to_empty() {
);
}

#[test]
fn canonicalizes_stray_acp_flag_to_subcommand() {
// Regression for #4106: subcommand-style harnesses (kimi, omp, opencode,
// devin, cursor-agent…) reject the `--acp` flag form with
// "unknown option '--acp'". Earlier releases persisted `--acp` into
// harness definitions and instance args; normalize it to the subcommand.
assert_eq!(
normalize_agent_args("kimi", vec!["--acp".into()]),
vec!["acp".to_string()]
);
assert_eq!(
normalize_agent_args("kimi", vec!["-acp".into()]),
vec!["acp".to_string()]
);
assert_eq!(
normalize_agent_args("/Users/dev/.kimi-code/bin/kimi", vec!["--acp".into()]),
vec!["acp".to_string()]
);
assert_eq!(
normalize_agent_args("opencode", vec!["--acp".into()]),
vec!["acp".to_string()]
);
// Case-insensitive, and extra args are preserved.
assert_eq!(
normalize_agent_args("kimi", vec!["--ACP".into(), "--verbose".into()]),
vec!["acp".to_string(), "--verbose".to_string()]
);
// The correct subcommand form passes through unchanged.
assert_eq!(
normalize_agent_args("kimi", vec!["acp".into()]),
vec!["acp".to_string()]
);
// Flag-style agents (codex/claude/buzz-agent) still strip the ACP arg.
assert_eq!(
normalize_agent_args("codex-acp", vec!["--acp".into()]),
Vec::<String>::new()
);
// Unrelated flags are left alone for subcommand-style CLIs.
assert_eq!(
normalize_agent_args("kimi", vec!["--model".into(), "kimi-k3".into()]),
vec!["--model".to_string(), "kimi-k3".to_string()]
);
}

#[test]
fn kimi_default_args_fallback_covers_empty_args() {
// When no args are persisted at all (empty instance args + empty harness
// definition args), kimi still launches as `kimi acp` via the same
// known-harness fallback that covers goose.
assert_eq!(
normalize_agent_args("kimi", Vec::new()),
vec!["acp".to_string()]
);
assert_eq!(
normalize_agent_args("kimi", vec![" ".into(), "".into()]),
vec!["acp".to_string()]
);
// Explicit instance args still win over the fallback.
assert_eq!(
normalize_agent_args("kimi", vec!["--verbose".into()]),
vec!["--verbose".to_string()]
);
}

#[test]
fn login_shell_lookup_treats_command_as_data() {
let marker =
Expand Down