дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
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]>
Цей коміт міститься в:
1 джерело
f5a95dcc21
коміт
2ad979af66
4 files changed
+78
-3
No files matched your search
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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": "当前分支",
|
||||
|
||||
Reference in new issue
Заблокувати користувача