Skip to content
Closed
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
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

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

4 changes: 3 additions & 1 deletion codex-rs/acp/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ The ACP registry in `@/codex-rs/acp/src/registry.rs` is **model-centric** rather
- `provider_info`: Embedded `AcpProviderInfo` with provider configuration (name, retry settings, timeouts)
- Model names are normalized to lowercase for case-insensitive matching (e.g., "Gemini-2.5-Flash" → "gemini-2.5-flash")
- Uses exact matching only (no prefix matching) - each model must be explicitly registered
- The `provider_slug` field enables future optimization to determine when existing subprocess can be reused vs when new one must be spawned when switching models
- The `provider_slug` field enables determining when existing subprocess can be reused vs when new one must be spawned when switching models
- Claude ACP is registered for both "claude" and "claude-acp" model names, using `npx @zed-industries/claude-code-acp` command with no arguments
- `list_available_agents()` returns all available ACP agent configurations for UI presentation (used by the `/agent` picker)
- Unit test `test_get_claude_model_config()` verifies Claude ACP registry configuration
- Unit tests `test_list_available_agents_*` verify agent enumeration and uniqueness

### Embedded Provider Info

Expand Down
1 change: 1 addition & 0 deletions codex-rs/acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub use connection::ApprovalRequest;
pub use registry::AcpAgentConfig;
pub use registry::AcpProviderInfo;
pub use registry::get_agent_config;
pub use registry::list_available_agents;
pub use tracing_setup::init_file_tracing;
pub use translator::TranslatedEvent;
pub use translator::translate_session_update;
Expand Down
94 changes: 94 additions & 0 deletions codex-rs/acp/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,48 @@ pub fn get_agent_config(model_name: &str) -> Result<AcpAgentConfig> {
}
}

/// Returns all available ACP agent configurations.
///
/// This is used by the agent picker UI to show available agents.
/// Each agent has a unique provider_slug for identification.
pub fn list_available_agents() -> Vec<AcpAgentConfig> {
vec![
// Mock agent - uses resolved binary path
get_agent_config("mock-model").unwrap_or_else(|_| AcpAgentConfig {
provider_slug: "mock-acp".to_string(),
command: "mock_acp_agent".to_string(),
args: vec![],
provider_info: AcpProviderInfo {
name: "Mock ACP".to_string(),
..Default::default()
},
}),
// Gemini ACP agent
AcpAgentConfig {
provider_slug: "gemini-acp".to_string(),
command: "npx".to_string(),
args: vec![
"@google/gemini-cli".to_string(),
"--experimental-acp".to_string(),
],
provider_info: AcpProviderInfo {
name: "Gemini ACP".to_string(),
..Default::default()
},
},
// Claude ACP agent
AcpAgentConfig {
provider_slug: "claude-acp".to_string(),
command: "npx".to_string(),
args: vec!["@zed-industries/claude-code-acp".to_string()],
provider_info: AcpProviderInfo {
name: "Claude ACP".to_string(),
..Default::default()
},
},
]
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -230,4 +272,56 @@ mod tests {
"Error message should contain original input"
);
}

#[test]
fn test_list_available_agents_returns_all_agents() {
let agents = list_available_agents();

// Should return at least the known agents
assert!(
agents.len() >= 3,
"Should have at least 3 agents (mock, gemini, claude), got: {}",
agents.len()
);

// Should include mock-model agent
let mock_agent = agents
.iter()
.find(|a| a.provider_slug == "mock-acp")
.expect("Should include mock-acp agent");
assert!(
mock_agent.command.contains("mock_acp_agent"),
"Mock agent command should contain mock_acp_agent"
);

// Should include gemini agent
let gemini_agent = agents
.iter()
.find(|a| a.provider_slug == "gemini-acp")
.expect("Should include gemini-acp agent");
assert_eq!(gemini_agent.command, "npx");
assert_eq!(gemini_agent.provider_info.name, "Gemini ACP");

// Should include claude agent
let claude_agent = agents
.iter()
.find(|a| a.provider_slug == "claude-acp")
.expect("Should include claude-acp agent");
assert_eq!(claude_agent.command, "npx");
assert_eq!(claude_agent.provider_info.name, "Claude ACP");
}

#[test]
fn test_list_available_agents_has_unique_provider_slugs() {
let agents = list_available_agents();
let mut seen_slugs = std::collections::HashSet::new();

for agent in &agents {
assert!(
seen_slugs.insert(&agent.provider_slug),
"Duplicate provider_slug found: {}",
agent.provider_slug
);
}
}
}
1 change: 1 addition & 0 deletions codex-rs/tui-pty-e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ libc = "0.2"

[dev-dependencies]
tempfile = "3"
regex = "1"
16 changes: 16 additions & 0 deletions codex-rs/tui-pty-e2e/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ The main API provides:
- `wait_for_text(needle, timeout)` - Poll screen until text appears
- `wait_for(predicate, timeout)` - Poll screen until condition matches
- `screen_contents()` - Get current terminal screen as string
- `acp_log_path()` - Get path to the `.codex-acp.log` file for this session
- `read_acp_log()` - Read the ACP tracing log contents (useful for verifying subprocess behavior)

**Debugging Aids:**

Expand Down Expand Up @@ -145,6 +147,7 @@ This delay allows the PTY subprocess time to process input and update the displa
| `@/codex-rs/tui-pty-e2e/tests/input_handling.rs` | Text editing, backspace, Ctrl-C clearing, arrow key navigation with snapshot testing |
| `@/codex-rs/tui-pty-e2e/tests/streaming.rs` | Prompt submission with timing delays, agent response streaming |
| `@/codex-rs/tui-pty-e2e/tests/acp_mode.rs` | ACP mode startup, response flow, and approval bridging - validates TUI works with ACP wire API and mock agent; includes test for permission request display |
| `@/codex-rs/tui-pty-e2e/tests/agent_switching.rs` | ACP agent/model switching: `/agent` picker display, pending selection behavior, subprocess lifecycle during picker navigation, `/model` disabled state in ACP mode |
| `@/codex-rs/tui-pty-e2e/tests/live_acp.rs` | Live authenticated ACP tests for Gemini and Claude with real API connections (opt-in, marked `#[ignore]`) |

**Snapshot Files:**
Expand Down Expand Up @@ -232,6 +235,19 @@ Tests control mock agent behavior via environment variables:

See `@/codex-rs/mock-acp-agent/docs.md` for full list of env vars.

**Agent Switching Tests:**

The `agent_switching.rs` tests verify the ACP agent/model picker feature:
- Uses `extract_mock_agent_pids_from_log()` to parse PIDs from ACP tracing logs
- `process_exists_and_not_zombie()` verifies subprocess lifecycle via `/proc/<pid>/status`
- Tests verify the "pending selection" pattern where agent changes are deferred until prompt submission
- Key test cases:
- `/agent` picker displays available agents and marks current
- Selecting an agent does NOT immediately spawn new subprocess
- Prompt submission with pending selection triggers actual switch
- Picker navigation during active streaming does not kill subprocess
- `/model` command shows disabled state in ACP mode

**Binary Discovery:**

`codex_binary_path()` locates the compiled binary:
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/tui-pty-e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,22 @@ name = "Mock ACP provider for tests"
self.writer.write_all(&key.to_escape_sequence())?;
self.writer.flush()
}

/// Get the path to the ACP log file for this session.
/// Returns None if no temp directory was created.
pub fn acp_log_path(&self) -> Option<std::path::PathBuf> {
self._temp_dir
.as_ref()
.map(|dir| dir.path().join(".codex-acp.log"))
}

/// Read the ACP log contents.
/// Returns empty string if log doesn't exist or temp dir not available.
pub fn read_acp_log(&self) -> String {
self.acp_log_path()
.and_then(|path| std::fs::read_to_string(path).ok())
.unwrap_or_default()
}
}

/// Sandbox policy for codex session
Expand Down
Loading