feat(agents): complete OpenCode's /compact builtin (#257)

OpenCode handles /compact through its ACP session but does not list it in availableCommands, so the composer never offered it. Agent builtins the agent implements without advertising are now merged into slash-command completion without duplicating listed commands; the table holds only commands verified against the agent (OpenCode /compact). Claude Code already advertises /compact. Executable lookup keeps using the shared PATHEXT-aware resolution on main, with a test that the newest install across PATH entries is chosen.

Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
arookieofcтаa-rookie-of-C-language authored and GitHub committed 2026-10-30 12:22:04 +08:00
1 джерело f5a95dcc21
коміт 2ad979af66
4 files changed
+78 -3

No files matched your search

+20 -2
Переглянути файл
@@ -314,8 +314,8 @@ impl AgentManager {
}
/// The slash commands to complete for `provider`, in `task`'s directory or (without one)
/// in the project: what the agent offered last time there, plus, for Claude Code, the
/// skills and custom commands on disk it has not listed.
/// in the project: what the agent offered last time there, unadvertised builtins,
/// and, for Claude Code, skills and custom commands on disk it has not listed.
pub async fn commands(&self, provider: &str, task: Option<&str>) -> IdeResult<Vec<AgentCommand>> {
let (provider, offered, cwd) = match task {
Some(id) => {
@@ -325,6 +325,14 @@ impl AgentManager {
}
None => (provider.to_owned(), self.shared.known_commands(provider), self.shared.paths.project_root.clone()),
};
// Commands the agent implements itself and never lists (its CLI builtins):
// merged without duplicating what `availableCommands` or the disk offer.
let mut offered = offered;
for &(name, description) in builtin_commands(&provider) {
if !offered.iter().any(|c| c.name == name) {
offered.push(AgentCommand { name: name.to_owned(), description: description.to_owned(), hint: None });
}
}
if provider != "claude" && provider != "claude-acp" {
return Ok(offered);
}
@@ -1629,6 +1637,16 @@ fn model_of(config: &[AgentConfigOption]) -> Option<String> {
config.iter().find(|o| o.category.as_deref() == Some("model")).and_then(|o| o.current.clone())
}
/// Commands the agent implements on its own and never lists in `availableCommands` —
/// its CLI builtins, passed through as prompt text (e.g. `/compact`). `(name, description)`.
fn builtin_commands(provider: &str) -> &'static [(&'static str, &'static str)] {
match provider {
// OpenCode ACP handles `/compact` via session.summarize but omits it from its command list.
"opencode" => &[("compact", "compact the conversation")],
_ => &[],
}
}
fn file_name(path: &Path) -> String {
path.file_name().map_or_else(|| path.to_string_lossy().into_owned(), |n| n.to_string_lossy().into_owned())
}
+20
Переглянути файл
@@ -124,4 +124,24 @@ mod tests {
assert_eq!(newest_install_in("nope", &search), "nope");
assert_eq!(newest_install_in("bin/claude", &search), "bin/claude");
}
#[cfg(unix)]
#[test]
fn newest_install_compares_versions_across_path_entries() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let old = dir.path().join("old");
let new = dir.path().join("new");
for (path, version) in [(&old, "2.9.0"), (&new, "2.10.0")] {
std::fs::create_dir_all(path).unwrap();
let executable = path.join("agent");
std::fs::write(&executable, format!("#!/bin/sh\necho {version}\n")).unwrap();
std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap();
}
for paths in [[&old, &new], [&new, &old]] {
let search = std::env::join_paths(paths).unwrap();
assert_eq!(newest_install_in("agent", &search), new.join("agent").to_string_lossy());
}
}
}
+37 -1
Переглянути файл
@@ -210,7 +210,7 @@ async fn commands_are_remembered_per_project_and_merged_with_skills_on_disk() {
let names = |c: Vec<AgentCommand>| c.into_iter().map(|c| c.name).collect::<Vec<_>>();
let mgr_a = manager_at(&a, &models, Arc::new(FakeAgent::default()));
// Before any session: the skills on disk.
// Before any session: only the skills on disk.
assert_eq!(names(mgr_a.commands("claude", None).await.unwrap()), ["triage"]);
let task = mgr_a
.create(NewAgentTask {
@@ -239,6 +239,42 @@ async fn commands_are_remembered_per_project_and_merged_with_skills_on_disk() {
assert_eq!(names(mgr_a.providers().into_iter().find(|p| p.id == "claude").unwrap().commands), ["review"]);
}
#[tokio::test(flavor = "multi_thread")]
async fn builtin_commands_only_fill_verified_gaps_and_preserve_advertised_metadata() {
let tmp = tempfile::tempdir().unwrap();
let agent = Arc::new(FakeAgent::default());
let mgr = manager_at(tmp.path(), &tmp.path().join("models.json"), agent);
for provider in ["claude", "claude-acp", "opencode2", "codex", "custom"] {
assert!(mgr.commands(provider, None).await.unwrap().is_empty(), "{provider}");
}
let commands = mgr.commands("opencode", None).await.unwrap();
assert_eq!(commands.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), ["compact"]);
let task = mgr
.create(NewAgentTask {
prompt: text("Hello"),
provider: "opencode".into(),
config: Vec::new(),
isolation: Isolation::Local,
base: None,
})
.await
.unwrap();
wait_for(&mgr, &task.id, AgentStatus::NeedsInput).await;
let advertised = AgentCommand {
name: "compact".into(),
description: "Agent supplied description".into(),
hint: Some("instructions".into()),
};
mgr.task(&task.id).unwrap().state.lock().task.commands = vec![advertised.clone()];
// The task's provider wins over the caller's provider; metadata is not replaced.
let commands = mgr.commands("claude", Some(&task.id)).await.unwrap();
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].name, advertised.name);
assert_eq!(commands[0].description, advertised.description);
assert_eq!(commands[0].hint, advertised.hint);
mgr.shutdown();
}
async fn wait_for(mgr: &AgentManager, id: &str, status: AgentStatus) {
for _ in 0..500 {
let task = mgr.task(id).unwrap();
+1
Переглянути файл
@@ -2278,6 +2278,7 @@
"cargo run configurations and rust-analyzer use this toolchain (PATH, RUSTUP_TOOLCHAIN).": "cargo 运行配置和 rust-analyzer 使用此工具链(PATH、RUSTUP_TOOLCHAIN)。",
"chord prefix": "组合键前缀",
"command line tool. Install it, then run": "命令行工具。请安装它,然后运行",
"compact the conversation": "压缩对话上下文",
"configured in": "配置位置:",
"const declaration": "const 声明",
"current branch": "当前分支",