Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@

### Added

- **Named agents via a config file.** Register ACP agents in a small TOML file
(`[agents.<name>]` with `command` / `args` / optional `env`, mirroring Zed's
`agent_servers`) and launch one by name: `acp-mux --agent claude` /
`rooms --agent claude`. Default config path is
`$XDG_CONFIG_HOME/acp-mux/agents.toml` (or `~/.config/acp-mux/agents.toml`);
override with `--config <path>`. `--list-agents` prints the configured
agents. `--agent` and the raw `--agent-cmd` escape hatch are mutually
exclusive. Per-agent `env` is layered on top of the inherited process
environment, so the agent also sees the parent shell's variables.
- **Two-crate workspace: core mux vs Rooms layer.** The repo is now a Cargo
workspace with a hard, compiler-enforced boundary:
- `acp-mux` (lib `acp_mux`, binary `acp-mux`) — the standalone generic 1→N
Expand Down
82 changes: 82 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,33 @@ proxy-local `session/attach` to receive a shaped snapshot/history:
ws://127.0.0.1:8765/acp?room=demo&peer_id=desktop&replay=skip
```

## Named Agents

Instead of passing a raw `--agent-cmd`, register agents in a TOML config (shape
mirrors Zed's `agent_servers`) and launch one by name:

```toml
# ~/.config/acp-mux/agents.toml (override with --config <path>)
[agents.claude]
command = "npx"
args = ["-y", "@agentclientprotocol/claude-agent-acp"]
# env = { ANTHROPIC_API_KEY = "x" } # optional; use a real value only in your private config

[agents.gemini]
command = "gemini"
args = ["acp"]
```

```sh
acp-mux --agent claude # or: rooms --agent claude
acp-mux --list-agents # show configured agents
```

Default config path is `$XDG_CONFIG_HOME/acp-mux/agents.toml` (falling back to
`~/.config/acp-mux/agents.toml`). `--agent` and `--agent-cmd` are mutually
exclusive; `--agent-cmd` remains the raw escape hatch. A copyable example lives
at [`docs/examples/agents.toml`](docs/examples/agents.toml).

## Tests

```sh
Expand Down
1 change: 1 addition & 0 deletions crates/acp-mux/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ futures = "0.3.32"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
thiserror = "2.0.18"
toml = "0.8.19"
tokio = { version = "1.52.3", features = ["full"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] }
Expand Down
11 changes: 7 additions & 4 deletions crates/acp-mux/src/agent/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ pub struct AgentProcess {
}

impl AgentProcess {
pub async fn spawn(program: &str, args: &[String]) -> Result<Self> {
pub async fn spawn(program: &str, args: &[String], env: &[(String, String)]) -> Result<Self> {
let mut child = Command::new(program)
.args(args)
.envs(env.iter().map(|(k, v)| (k, v)))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
Expand Down Expand Up @@ -199,7 +200,9 @@ mod tests {
/// `cat` echoes stdin to stdout — a deterministic NDJSON loopback.
#[tokio::test]
async fn cat_loopback_roundtrip() {
let mut proc = AgentProcess::spawn("cat", &[]).await.expect("spawn cat");
let mut proc = AgentProcess::spawn("cat", &[], &[])
.await
.expect("spawn cat");

proc.send(br#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#)
.await
Expand Down Expand Up @@ -238,7 +241,7 @@ mod tests {
"for i in $(seq 1 {}); do echo noise $i >&2; done; exec cat",
STDERR_CAPACITY * 4,
);
let mut proc = AgentProcess::spawn("sh", &["-c".into(), burst])
let mut proc = AgentProcess::spawn("sh", &["-c".into(), burst], &[])
.await
.expect("spawn sh");

Expand All @@ -257,7 +260,7 @@ mod tests {
#[tokio::test]
async fn shutdown_kills_unresponsive_child() {
// `sleep 30` never exits on its own within our timeout.
let proc = AgentProcess::spawn("sleep", &["30".into()])
let proc = AgentProcess::spawn("sleep", &["30".into()], &[])
.await
.expect("spawn sleep");
proc.shutdown(Duration::from_millis(200)).await.unwrap();
Expand Down
Loading
Loading