Update agent panel slash command implementation plan with multi-repo coordination
- Add critical architecture note about TypeScript code generation from Rust - Add repository dependency chain and PR coordination strategy - Update Phase 3 to reflect auto-generated ACP types - Add migration notes for multi-repository rollout - Include local development workflow with path dependencies 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,66 @@ When user types "/" in agent panel message editor:
|
||||
|
||||
Follow the existing "@" mention completion pattern but trigger on "/" instead. Use capability negotiation to control menu visibility. Extend ACP integration to call new RPC methods when available.
|
||||
|
||||
## Repository Dependencies & PR Strategy
|
||||
|
||||
### **Multi-Repository Architecture**
|
||||
This feature spans three repositories that must be coordinated:
|
||||
|
||||
1. **`agent-client-protocol`**: External crate defining the protocol
|
||||
2. **`zed`**: Main editor with ACP client integration
|
||||
3. **`claude-code-acp`**: Reference agent implementation
|
||||
|
||||
### **Dependency Chain**
|
||||
```
|
||||
agent-client-protocol (Phase 1)
|
||||
↓
|
||||
zed (Phase 2) - temporarily depends on local ACP changes
|
||||
↓
|
||||
claude-code-acp (Phase 3) - uses published ACP version
|
||||
```
|
||||
|
||||
### **Development Workflow**
|
||||
|
||||
#### Step 1: Local Development Setup
|
||||
```bash
|
||||
# Work on ACP protocol extension locally
|
||||
cd /Users/nathan/src/agent-client-protocol
|
||||
# Make Phase 1 changes...
|
||||
|
||||
# Point Zed to local ACP version for testing
|
||||
cd /Users/nathan/src/zed
|
||||
# Update Cargo.toml to reference local path:
|
||||
# agent-client-protocol = { path = "../agent-client-protocol" }
|
||||
```
|
||||
|
||||
#### Step 2: Testing & Validation
|
||||
- Test all phases end-to-end with local dependencies
|
||||
- Verify Phase 1+2 integration works correctly
|
||||
- Validate Phase 3 against local ACP changes
|
||||
|
||||
#### Step 3: PR Sequence
|
||||
1. **First PR**: `agent-client-protocol` with new slash command methods
|
||||
2. **Second PR**: `zed` referencing published ACP version (after #1 merges)
|
||||
3. **Third PR**: `claude-code-acp` using new ACP capabilities
|
||||
|
||||
### **Temporary Dependency Management**
|
||||
During development, Zed's `Cargo.toml` will need:
|
||||
```toml
|
||||
[dependencies]
|
||||
# Temporary local reference for development/testing
|
||||
agent-client-protocol = { path = "../agent-client-protocol" }
|
||||
|
||||
# After ACP PR merges, switch to:
|
||||
agent-client-protocol = "0.2.0-alpha.1" # or appropriate version
|
||||
```
|
||||
|
||||
### **Cross-Repository Verification**
|
||||
Before opening PRs:
|
||||
- [ ] ACP protocol extension compiles and tests pass
|
||||
- [ ] Zed compiles against local ACP changes
|
||||
- [ ] End-to-end slash command flow works locally
|
||||
- [ ] Claude ACP adapter works with generated types
|
||||
|
||||
## Phase 1: ACP Protocol Extension
|
||||
|
||||
### Overview
|
||||
@@ -54,29 +114,18 @@ Add new slash command RPC methods and capabilities to the agent-client-protocol
|
||||
|
||||
#### 1. Protocol Types (External Crate)
|
||||
**File**: `/Users/nathan/src/agent-client-protocol/rust/agent.rs`
|
||||
**Changes**: Add new request/response types and trait methods
|
||||
**Changes**: Add new request/response types and trait methods following exact ACP patterns
|
||||
|
||||
**Step 1: Add Method Constants** (after line 415):
|
||||
```rust
|
||||
// Add around line 108 after the cancel method in the Agent trait
|
||||
/// Lists available custom commands for a session.
|
||||
///
|
||||
/// Returns all commands available in the agent's `.claude/commands` directory
|
||||
/// or equivalent command registry. Commands can be executed via `run_command`.
|
||||
fn list_commands(
|
||||
&self,
|
||||
arguments: ListCommandsRequest,
|
||||
) -> impl Future<Output = Result<ListCommandsResponse, Error>>;
|
||||
/// Method name for listing custom commands in a session.
|
||||
pub const SESSION_LIST_COMMANDS: &str = "session/list_commands";
|
||||
/// Method name for running a custom command in a session.
|
||||
pub const SESSION_RUN_COMMAND: &str = "session/run_command";
|
||||
```
|
||||
|
||||
/// Executes a custom command within a session.
|
||||
///
|
||||
/// Runs the specified command with optional arguments. The agent should
|
||||
/// stream results back via session update notifications.
|
||||
fn run_command(
|
||||
&self,
|
||||
arguments: RunCommandRequest,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
|
||||
// Add around line 372 after PromptCapabilities
|
||||
**Step 2: Add Request/Response Structs** (after PromptCapabilities at line 371):
|
||||
```rust
|
||||
/// Request parameters for listing available commands.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
|
||||
@@ -121,6 +170,47 @@ pub struct RunCommandRequest {
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add Agent Trait Methods** (after `cancel()` method at line 107):
|
||||
```rust
|
||||
/// Lists available custom commands for a session.
|
||||
///
|
||||
/// Returns all commands available in the agent's `.claude/commands` directory
|
||||
/// or equivalent command registry. Commands can be executed via `run_command`.
|
||||
fn list_commands(
|
||||
&self,
|
||||
arguments: ListCommandsRequest,
|
||||
) -> impl Future<Output = Result<ListCommandsResponse, Error>>;
|
||||
|
||||
/// Executes a custom command within a session.
|
||||
///
|
||||
/// Runs the specified command with optional arguments. The agent should
|
||||
/// stream results back via session update notifications.
|
||||
fn run_command(
|
||||
&self,
|
||||
arguments: RunCommandRequest,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
```
|
||||
|
||||
**Step 4: Add Enum Routing Variants** (to `ClientRequest` enum around line 423):
|
||||
```rust
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum ClientRequest {
|
||||
InitializeRequest(InitializeRequest),
|
||||
AuthenticateRequest(AuthenticateRequest),
|
||||
NewSessionRequest(NewSessionRequest),
|
||||
LoadSessionRequest(LoadSessionRequest),
|
||||
PromptRequest(PromptRequest),
|
||||
ListCommandsRequest(ListCommandsRequest), // ADD THIS
|
||||
RunCommandRequest(RunCommandRequest), // ADD THIS
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Add AgentResponse Enum Variant** (find AgentResponse enum):
|
||||
```rust
|
||||
ListCommandsResponse(ListCommandsResponse), // ADD THIS
|
||||
```
|
||||
|
||||
#### 2. Capability Extension
|
||||
**File**: `/Users/nathan/src/agent-client-protocol/rust/agent.rs`
|
||||
**Changes**: Extend PromptCapabilities with custom command support
|
||||
@@ -157,10 +247,40 @@ fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Res
|
||||
|
||||
#### 4. ACP Connection Implementation
|
||||
**File**: `crates/agent_servers/src/acp.rs`
|
||||
**Changes**: Implement new trait methods in AcpConnection
|
||||
**Changes**: Implement new trait methods in AcpConnection following existing patterns
|
||||
|
||||
**Step 1: Add ClientSideConnection Methods** (after existing methods around line 340):
|
||||
```rust
|
||||
impl acp::ClientSideConnection {
|
||||
/// Lists available custom commands for a session.
|
||||
pub async fn list_commands(
|
||||
&self,
|
||||
request: acp::ListCommandsRequest,
|
||||
) -> Result<acp::ListCommandsResponse, acp::Error> {
|
||||
self.connection
|
||||
.request(acp::ClientRequest::ListCommandsRequest(request))
|
||||
.await
|
||||
.and_then(|response| match response {
|
||||
acp::AgentResponse::ListCommandsResponse(response) => Ok(response),
|
||||
_ => Err(acp::Error::internal_error("Invalid response type")),
|
||||
})
|
||||
}
|
||||
|
||||
/// Executes a custom command in a session.
|
||||
pub async fn run_command(
|
||||
&self,
|
||||
request: acp::RunCommandRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
self.connection
|
||||
.request(acp::ClientRequest::RunCommandRequest(request))
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Implement AgentConnection Trait Methods** (for AcpConnection struct):
|
||||
```rust
|
||||
// Add around line 340 after existing method implementations
|
||||
fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
|
||||
let conn = self.connection.clone();
|
||||
let session_id = session_id.clone();
|
||||
@@ -177,6 +297,25 @@ fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Res
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Update Message Dispatch Logic** (in `AgentSide::decode_request()` around line 493):
|
||||
```rust
|
||||
// Add cases to the match statement:
|
||||
acp::SESSION_LIST_COMMANDS => {
|
||||
if let Ok(request) = serde_json::from_value::<acp::ListCommandsRequest>(params) {
|
||||
Ok(acp::ClientRequest::ListCommandsRequest(request))
|
||||
} else {
|
||||
Err(acp::Error::invalid_params("Invalid list_commands parameters"))
|
||||
}
|
||||
}
|
||||
acp::SESSION_RUN_COMMAND => {
|
||||
if let Ok(request) = serde_json::from_value::<acp::RunCommandRequest>(params) {
|
||||
Ok(acp::ClientRequest::RunCommandRequest(request))
|
||||
} else {
|
||||
Err(acp::Error::invalid_params("Invalid run_command parameters"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Capability Detection
|
||||
**File**: `crates/acp_thread/src/acp_thread.rs`
|
||||
**Changes**: Add capability checking helper
|
||||
@@ -194,14 +333,23 @@ pub fn supports_custom_commands(&self, cx: &App) -> bool {
|
||||
- [ ] Protocol crate compiles: `cd /Users/nathan/src/agent-client-protocol && cargo check`
|
||||
- [ ] Protocol tests pass: `cd /Users/nathan/src/agent-client-protocol && cargo test`
|
||||
- [ ] Schema generation works: `cd /Users/nathan/src/agent-client-protocol && cargo run --bin generate`
|
||||
- [ ] Schema includes new methods: `grep -A5 -B5 "session/list_commands\|session/run_command" /Users/nathan/src/agent-client-protocol/schema/schema.json`
|
||||
- [ ] Zed compiles successfully: `./script/clippy`
|
||||
- [ ] No linting errors: `cargo clippy --package agent_servers --package acp_thread`
|
||||
- [ ] ACP thread capability method compiles: `cargo check --package acp_thread`
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] New trait methods are properly defined in connection interface
|
||||
- [ ] ACP connection implements new methods correctly
|
||||
- [ ] Capability detection helper is available for UI layer
|
||||
- [ ] JSON schema includes new request/response types
|
||||
- [ ] New trait methods are properly defined in Agent trait (`/Users/nathan/src/agent-client-protocol/rust/agent.rs`)
|
||||
- Verify `list_commands()` method signature at line ~115
|
||||
- Verify `run_command()` method signature at line ~127
|
||||
- [ ] Request/response enums updated in ClientRequest (`agent.rs:~423`) and AgentResponse enums
|
||||
- [ ] Method constants added (`SESSION_LIST_COMMANDS`, `SESSION_RUN_COMMAND`) after line 415
|
||||
- [ ] PromptCapabilities extended with `supports_custom_commands: bool` field
|
||||
- [ ] ClientSideConnection methods implemented with proper error handling
|
||||
- [ ] Message dispatch logic updated in `AgentSide::decode_request()`
|
||||
- [ ] AgentConnection trait extends with new methods (`crates/acp_thread/src/connection.rs`)
|
||||
- [ ] AcpConnection implements trait methods (`crates/agent_servers/src/acp.rs`)
|
||||
- [ ] AcpThread has `supports_custom_commands()` helper method
|
||||
|
||||
---
|
||||
|
||||
@@ -259,43 +407,65 @@ impl SlashCommandCompletion {
|
||||
|
||||
#### 2. Completion Trigger Detection
|
||||
**File**: `crates/agent_ui/src/acp/completion_provider.rs`
|
||||
**Changes**: Extend `is_completion_trigger()` method
|
||||
**Changes**: Extend `is_completion_trigger()` method following existing patterns
|
||||
|
||||
**Current Pattern Analysis**: The completion provider implements the `CompletionProvider` trait and integrates with the editor's completion system. The `is_completion_trigger()` method at line 763 currently only handles "@" mentions.
|
||||
|
||||
```rust
|
||||
// Modify around line 763 to add slash command detection
|
||||
pub fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
// Existing @ mention logic...
|
||||
if let Some(_) = MentionCompletion::try_parse(&line, position.column) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add slash command detection
|
||||
if let Some(thread) = &self.thread {
|
||||
if thread.read(cx).supports_custom_commands(cx) {
|
||||
if let Some(_) = SlashCommandCompletion::try_parse(&line, position.column) {
|
||||
return true;
|
||||
// Modify the existing is_completion_trigger() method around line 763
|
||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(&buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let Some(line) = lines.next() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Existing @ mention logic - KEEP THIS
|
||||
if let Some(_) = MentionCompletion::try_parse(&line, position.column) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ADD: Slash command detection (only if agent supports commands)
|
||||
if let Some(thread) = &self.thread {
|
||||
if thread.read(cx).supports_custom_commands(cx) {
|
||||
if let Some(_) = SlashCommandCompletion::try_parse(&line, position.column) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern Notes**:
|
||||
- Integrates with existing `@` mention system without conflicts
|
||||
- Only triggers when agent capability `supports_custom_commands` is true
|
||||
- Uses same line parsing approach as existing mention system
|
||||
- Maintains backward compatibility
|
||||
|
||||
#### 3. Command Completion Generation
|
||||
**File**: `crates/agent_ui/src/acp/completion_provider.rs`
|
||||
**Changes**: Extend `completions()` method
|
||||
**Changes**: Extend `completions()` method using existing async patterns
|
||||
|
||||
**Current Pattern Analysis**: The completion provider's `completions()` method at line 639 returns `Task<Result<Vec<project::CompletionResponse>>>` and uses `cx.spawn()` for async operations. It handles different completion types via pattern matching.
|
||||
|
||||
```rust
|
||||
// Add around line 700 in completions() method after mention handling
|
||||
// Handle slash command completions
|
||||
// Modify the existing completions() method around line 700
|
||||
// ADD this after the existing mention completion logic:
|
||||
|
||||
// Handle slash command completions (only if agent supports them)
|
||||
if let Some(thread) = &self.thread {
|
||||
if thread.read(cx).supports_custom_commands(cx) {
|
||||
if let Some(slash_completion) = SlashCommandCompletion::try_parse(&line, cursor_offset) {
|
||||
@@ -309,7 +479,7 @@ if let Some(thread) = &self.thread {
|
||||
}
|
||||
}
|
||||
|
||||
// Add new method around line 850
|
||||
// ADD new method following existing async patterns (around line 850):
|
||||
fn complete_slash_commands(
|
||||
&self,
|
||||
completion: SlashCommandCompletion,
|
||||
@@ -322,23 +492,28 @@ fn complete_slash_commands(
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
// Get session info using existing patterns
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone())?;
|
||||
let connection = thread.read_with(cx, |thread, _| thread.connection().clone())?;
|
||||
|
||||
// Fetch commands from agent
|
||||
let commands = connection.list_commands(&session_id, cx).await?;
|
||||
// Fetch commands from agent via new ACP method
|
||||
let response = connection.list_commands(&session_id, cx).await?;
|
||||
|
||||
// Filter commands matching typed prefix
|
||||
let matching_commands: Vec<_> = commands
|
||||
// Filter commands matching typed prefix (fuzzy matching like mentions)
|
||||
let matching_commands: Vec<_> = response.commands
|
||||
.into_iter()
|
||||
.filter(|cmd| cmd.name.starts_with(&completion.name))
|
||||
.filter(|cmd| {
|
||||
// Support both prefix matching and fuzzy matching
|
||||
cmd.name.starts_with(&completion.name) ||
|
||||
cmd.name.to_lowercase().contains(&completion.name.to_lowercase())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Convert to completion responses
|
||||
// Convert to project::Completion following existing patterns
|
||||
let mut completions = Vec::new();
|
||||
for command in matching_commands {
|
||||
let new_text = format!("/{}", command.name);
|
||||
let completion = project::Completion {
|
||||
let completion_item = project::Completion {
|
||||
old_range: completion.source_range.clone(),
|
||||
new_text,
|
||||
label: command.name.clone().into(),
|
||||
@@ -349,6 +524,7 @@ fn complete_slash_commands(
|
||||
} else {
|
||||
None
|
||||
},
|
||||
// Custom confirmation handler for command execution
|
||||
confirm: Some(Arc::new(SlashCommandConfirmation {
|
||||
command: command.name,
|
||||
requires_argument: command.requires_argument,
|
||||
@@ -356,9 +532,10 @@ fn complete_slash_commands(
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
completions.push(completion);
|
||||
completions.push(completion_item);
|
||||
}
|
||||
|
||||
// Return single completion response (like existing mentions)
|
||||
Ok(vec![project::CompletionResponse {
|
||||
completions,
|
||||
is_incomplete: false,
|
||||
@@ -367,6 +544,13 @@ fn complete_slash_commands(
|
||||
}
|
||||
```
|
||||
|
||||
**Integration Notes**:
|
||||
- Follows same async pattern as existing mention completions at line 639
|
||||
- Uses `thread.read_with()` pattern for safe entity access
|
||||
- Implements fuzzy matching similar to existing completion types
|
||||
- Returns single `CompletionResponse` following established patterns
|
||||
- Integrates custom confirmation handler via `confirm` field
|
||||
|
||||
#### 4. Command Confirmation Handler
|
||||
**File**: `crates/agent_ui/src/acp/completion_provider.rs`
|
||||
**Changes**: Add confirmation handler for slash commands
|
||||
@@ -453,13 +637,30 @@ pub fn run_command(
|
||||
- [ ] Code compiles successfully: `./script/clippy`
|
||||
- [ ] No linting errors: `cargo clippy --package agent_ui --package acp_thread`
|
||||
- [ ] Type checking passes: `cargo check --package agent_ui --package acp_thread`
|
||||
- [ ] Completion provider compiles: `cargo check --package agent_ui --lib`
|
||||
- [ ] Slash command parsing works: Test `SlashCommandCompletion::try_parse()` with various inputs
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] Typing "/" in agent panel triggers completion when agent supports commands
|
||||
- [ ] No "/" completion appears when agent doesn't support commands
|
||||
- [ ] Command list fetched from agent via `list_commands()` RPC
|
||||
- [ ] Command selection triggers `run_command()` RPC
|
||||
- [ ] Menu dismisses properly on Escape or click-outside
|
||||
- [ ] SlashCommandCompletion struct added to `crates/agent_ui/src/acp/completion_provider.rs`
|
||||
- Verify `try_parse()` method implementation
|
||||
- Test parsing of "/", "/create", "/research_codebase" patterns
|
||||
- [ ] ContextPickerCompletionProvider updated:
|
||||
- `is_completion_trigger()` method extended (around line 763)
|
||||
- `completions()` method handles slash commands (around line 700)
|
||||
- `complete_slash_commands()` method added (around line 850)
|
||||
- [ ] SlashCommandConfirmation struct implements CompletionConfirm trait
|
||||
- Handles immediate execution for commands without arguments
|
||||
- Allows continued typing for commands requiring arguments
|
||||
- [ ] AcpThread has `run_command()` method for command execution
|
||||
- [ ] Integration Testing:
|
||||
- [ ] Typing "/" in agent panel triggers completion when `supports_custom_commands = true`
|
||||
- [ ] No "/" completion appears when `supports_custom_commands = false`
|
||||
- [ ] Command list fetched from agent via `list_commands()` RPC call
|
||||
- [ ] Command selection triggers `run_command()` RPC call
|
||||
- [ ] Menu shows command descriptions from agent
|
||||
- [ ] Fuzzy matching works (typing "/cr" shows "create_plan")
|
||||
- [ ] Menu dismisses properly on Escape or click-outside
|
||||
- [ ] Commands execute and stream results back to thread view
|
||||
|
||||
---
|
||||
|
||||
@@ -468,11 +669,19 @@ pub fn run_command(
|
||||
### Overview
|
||||
Prepare the Claude Code ACP adapter to implement the new slash command RPC methods by adding command parsing and execution.
|
||||
|
||||
### **CRITICAL ARCHITECTURE NOTE**
|
||||
The TypeScript types in `claude-code-acp` are **automatically generated** from the Rust protocol definitions. The ACP repository uses a code generation pipeline:
|
||||
|
||||
1. **Rust → JSON Schema**: `cargo run --bin generate` creates `schema/schema.json` from Rust types
|
||||
2. **JSON Schema → TypeScript**: `node typescript/generate.js` creates TypeScript types from the schema
|
||||
|
||||
**This means the new `ListCommandsRequest`, `RunCommandRequest`, etc. types will be automatically available in TypeScript after we extend the Rust protocol in Phase 1.**
|
||||
|
||||
### Changes Required:
|
||||
|
||||
#### 1. Command Parsing Module
|
||||
**File**: `claude-code-acp/src/command-parser.ts` (new file)
|
||||
**Changes**: Add markdown command parser
|
||||
**Changes**: Add markdown command parser (TypeScript types will be auto-generated from Phase 1)
|
||||
|
||||
```typescript
|
||||
import * as fs from 'fs';
|
||||
@@ -579,133 +788,199 @@ export class CommandParser {
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. ACP Agent Method Implementation
|
||||
#### 2. Regenerate TypeScript Types
|
||||
**Prerequisites**: After completing Phase 1 Rust protocol extension
|
||||
**Commands**: Generate TypeScript types from updated Rust definitions
|
||||
|
||||
```bash
|
||||
# From agent-client-protocol repository root:
|
||||
cd /Users/nathan/src/agent-client-protocol
|
||||
npm run generate
|
||||
```
|
||||
|
||||
This will automatically create TypeScript types for:
|
||||
- `ListCommandsRequest`
|
||||
- `ListCommandsResponse`
|
||||
- `RunCommandRequest`
|
||||
- `CommandInfo`
|
||||
- Updated `PromptCapabilities` with `supports_custom_commands`
|
||||
|
||||
#### 3. ACP Agent Method Implementation
|
||||
**File**: `claude-code-acp/src/acp-agent.ts`
|
||||
**Changes**: Add new RPC method handlers
|
||||
**Changes**: Add new RPC method handlers following existing session management patterns
|
||||
|
||||
**Current Architecture Analysis**: The `ClaudeAcpAgent` at line 51 implements the ACP `Agent` interface with UUID-based session management. Sessions use Claude SDK `Query` objects with MCP proxy integration. The `prompt()` method at line 140 shows the pattern for query execution and result streaming.
|
||||
|
||||
```typescript
|
||||
// Add import
|
||||
// Step 1: Add import for command parser and auto-generated ACP types
|
||||
import { CommandParser, CommandInfo } from './command-parser';
|
||||
import type {
|
||||
ListCommandsRequest,
|
||||
ListCommandsResponse,
|
||||
RunCommandRequest
|
||||
} from '@zed-industries/agent-client-protocol';
|
||||
|
||||
// Add to ClaudeAcpAgent class around line 50
|
||||
private commandParser?: CommandParser;
|
||||
// Step 2: Extend ClaudeAcpAgent class (add to class definition around line 51)
|
||||
export class ClaudeAcpAgent implements Agent {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private client: Client;
|
||||
private commandParser?: CommandParser; // ADD THIS
|
||||
|
||||
// Modify constructor around line 100
|
||||
constructor(options: ClaudeAcpAgentOptions) {
|
||||
// ... existing initialization
|
||||
|
||||
// Initialize command parser if .claude/commands exists
|
||||
if (options.cwd) {
|
||||
this.commandParser = new CommandParser(options.cwd);
|
||||
}
|
||||
}
|
||||
|
||||
// Add capability declaration in initialize() around line 150
|
||||
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
|
||||
return {
|
||||
protocol_version: VERSION,
|
||||
agent_capabilities: {
|
||||
prompt_capabilities: {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
supports_custom_commands: !!this.commandParser, // Enable if commands exist
|
||||
},
|
||||
},
|
||||
auth_methods: ['claude-code'],
|
||||
};
|
||||
}
|
||||
|
||||
// Add new RPC method handlers around line 400
|
||||
async listCommands(request: ListCommandsRequest): Promise<ListCommandsResponse> {
|
||||
if (!this.commandParser) {
|
||||
return { commands: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const commands = await this.commandParser.listCommands();
|
||||
return {
|
||||
commands: commands.map(cmd => ({
|
||||
name: cmd.name,
|
||||
description: cmd.description,
|
||||
requires_argument: cmd.requires_argument,
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to list commands:', error);
|
||||
return { commands: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async runCommand(request: RunCommandRequest): Promise<void> {
|
||||
if (!this.commandParser) {
|
||||
throw new Error('Commands not supported');
|
||||
}
|
||||
|
||||
const command = await this.commandParser.getCommand(request.command);
|
||||
if (!command) {
|
||||
throw new Error(`Command not found: ${request.command}`);
|
||||
}
|
||||
|
||||
// Execute command by sending its content as a system prompt to Claude SDK
|
||||
const session = this.sessions.get(request.session_id);
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
|
||||
try {
|
||||
let systemPrompt = command.content;
|
||||
// Step 3: Modify constructor to initialize command parser (around line 60)
|
||||
constructor(
|
||||
client: Client,
|
||||
options: { cwd?: string } = {}
|
||||
) {
|
||||
this.client = client;
|
||||
|
||||
// If command requires arguments and args provided, append them
|
||||
if (command.requires_argument && request.args) {
|
||||
systemPrompt += `\n\nArguments: ${request.args}`;
|
||||
// Initialize command parser if .claude/commands directory exists
|
||||
if (options.cwd && fs.existsSync(path.join(options.cwd, '.claude', 'commands'))) {
|
||||
this.commandParser = new CommandParser(options.cwd);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Update initialize() method to advertise capability (around line 68)
|
||||
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
|
||||
return {
|
||||
protocol_version: VERSION,
|
||||
agent_capabilities: {
|
||||
prompt_capabilities: {
|
||||
image: true,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
supports_custom_commands: !!this.commandParser, // Advertise support
|
||||
},
|
||||
},
|
||||
auth_methods: [/* existing auth methods */],
|
||||
};
|
||||
}
|
||||
|
||||
// Step 5: Implement listCommands following existing async patterns (after line 218)
|
||||
async listCommands(request: ListCommandsRequest): Promise<ListCommandsResponse> {
|
||||
if (!this.commandParser) {
|
||||
return { commands: [] };
|
||||
}
|
||||
|
||||
// Create new query with command content as system prompt
|
||||
const query = query({
|
||||
prompt: systemPrompt,
|
||||
options: {
|
||||
cwd: session.cwd,
|
||||
mcpServers: session.mcpServers,
|
||||
allowedTools: session.allowedTools,
|
||||
disallowedTools: session.disallowedTools,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const commands = await this.commandParser.listCommands();
|
||||
return {
|
||||
commands: commands.map(cmd => ({
|
||||
name: cmd.name,
|
||||
description: cmd.description,
|
||||
requires_argument: cmd.requires_argument,
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to list commands:', error);
|
||||
return { commands: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Stream results back to session
|
||||
for await (const chunk of query) {
|
||||
// Convert query response to session update format
|
||||
const update = this.convertQueryChunkToSessionUpdate(chunk);
|
||||
await this.sendSessionUpdate(request.session_id, update);
|
||||
// Step 6: Implement runCommand integrating with existing session flow
|
||||
async runCommand(request: RunCommandRequest): Promise<void> {
|
||||
if (!this.commandParser) {
|
||||
throw new Error('Commands not supported');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Command execution failed:', error);
|
||||
// Send error as session update
|
||||
await this.sendSessionUpdate(request.session_id, {
|
||||
type: 'agent_message_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Error executing command: ${error.message}`,
|
||||
},
|
||||
});
|
||||
const command = await this.commandParser.getCommand(request.command);
|
||||
if (!command) {
|
||||
throw new Error(`Command not found: ${request.command}`);
|
||||
}
|
||||
|
||||
const session = this.sessions.get(request.session_id);
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
|
||||
try {
|
||||
// Build prompt from command content following existing patterns
|
||||
let commandPrompt = command.content;
|
||||
|
||||
if (command.requires_argument && request.args) {
|
||||
commandPrompt += `\n\nArguments: ${request.args}`;
|
||||
}
|
||||
|
||||
// Execute via existing session input stream (recommended approach)
|
||||
// This integrates with existing prompt() flow and MCP proxy
|
||||
session.input.push({
|
||||
role: 'user',
|
||||
content: commandPrompt
|
||||
});
|
||||
|
||||
// Results will be streamed back via existing query execution loop
|
||||
// at line 150 in prompt() method, no additional streaming needed
|
||||
|
||||
} catch (error) {
|
||||
console.error('Command execution failed:', error);
|
||||
// Send error via existing session update mechanism
|
||||
await this.client.sessionUpdate({
|
||||
session_id: request.session_id,
|
||||
type: 'agent_message_chunk',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Error executing command: ${error.message}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Integration Notes**:
|
||||
- **Auto-Generated Types**: All ACP protocol types are automatically generated from Rust definitions
|
||||
- **Session Reuse**: Uses existing session's input stream and MCP configuration
|
||||
- **Result Streaming**: Leverages existing `prompt()` method's streaming loop at line 150
|
||||
- **Error Handling**: Uses established session update patterns from line 191
|
||||
- **Tool Access**: Commands inherit session's MCP server and tool configurations
|
||||
|
||||
### Success Criteria:
|
||||
|
||||
#### Automated Verification:
|
||||
- [ ] TypeScript compilation passes: `npm run typecheck` (in claude-code-acp)
|
||||
- [ ] ESLint passes: `npm run lint` (in claude-code-acp)
|
||||
- [ ] Command parser unit tests pass: `npm test command-parser`
|
||||
- [ ] **Prerequisites completed**: Phase 1 Rust protocol extension must be completed first
|
||||
- [ ] **TypeScript types generated**: `cd /Users/nathan/src/agent-client-protocol && npm run generate`
|
||||
- [ ] **Types available**: Verify new types exist in `agent-client-protocol/typescript/schema.ts`
|
||||
- [ ] TypeScript compilation passes: `cd /Users/nathan/src/claude-code-acp && npm run typecheck`
|
||||
- [ ] ESLint passes: `cd /Users/nathan/src/claude-code-acp && npm run lint`
|
||||
- [ ] Agent compiles: `cd /Users/nathan/src/claude-code-acp && npm run build`
|
||||
- [ ] Command parser unit tests pass: `cd /Users/nathan/src/claude-code-acp && npm test -- --testNamePattern="command-parser"`
|
||||
|
||||
#### Manual Verification:
|
||||
- [ ] `.claude/commands/*.md` files are correctly parsed for metadata
|
||||
- [ ] `list_commands()` returns available commands with descriptions
|
||||
- [ ] `run_command()` executes command content via Claude SDK
|
||||
- [ ] Command results stream back as session updates
|
||||
- [ ] Error handling works for missing commands or execution failures
|
||||
- [ ] **Code Generation Pipeline**:
|
||||
- [ ] Rust protocol changes trigger successful schema generation: `cargo run --bin generate`
|
||||
- [ ] JSON schema contains new method definitions: `grep -A5 -B5 "session/list_commands\|session/run_command" /Users/nathan/src/agent-client-protocol/schema/schema.json`
|
||||
- [ ] TypeScript types generated correctly: Check for `ListCommandsRequest`, `RunCommandRequest` types in schema.ts
|
||||
- [ ] CommandParser class implemented (`claude-code-acp/src/command-parser.ts`):
|
||||
- `listCommands()` method reads `.claude/commands/*.md` files
|
||||
- `parseCommandFile()` extracts H1 titles and descriptions correctly
|
||||
- `getCommand()` returns full command content for execution
|
||||
- Proper error handling for missing directories and files
|
||||
- Command caching works correctly
|
||||
- [ ] ClaudeAcpAgent class extended (`claude-code-acp/src/acp-agent.ts`):
|
||||
- Constructor initializes `commandParser` when `.claude/commands` exists
|
||||
- `initialize()` method advertises `supports_custom_commands` capability correctly
|
||||
- `listCommands()` method implemented and returns properly formatted response
|
||||
- `runCommand()` method integrated with existing session management
|
||||
- Command execution uses existing session input stream
|
||||
- Error handling streams errors back via session updates
|
||||
- [ ] **Type Integration**:
|
||||
- [ ] Auto-generated types imported correctly from `@zed-industries/agent-client-protocol`
|
||||
- [ ] TypeScript compiler recognizes new protocol method signatures
|
||||
- [ ] No type errors when implementing new agent methods
|
||||
- [ ] Integration Testing:
|
||||
- [ ] Agent advertises `supports_custom_commands = true` when `.claude/commands` directory exists
|
||||
- [ ] Agent advertises `supports_custom_commands = false` when directory doesn't exist
|
||||
- [ ] `list_commands()` RPC returns commands from `.claude/commands/*.md` files
|
||||
- [ ] Commands include correct name, description, requires_argument fields
|
||||
- [ ] `run_command()` executes command content via Claude SDK integration
|
||||
- [ ] Command results stream back as session updates to ACP client
|
||||
- [ ] Commands have access to session's MCP servers and tool permissions
|
||||
- [ ] Error handling works for missing commands, directories, execution failures
|
||||
- [ ] Command arguments are properly appended when provided
|
||||
- [ ] End-to-End Testing:
|
||||
- [ ] Create test `.claude/commands/test.md` file with sample command
|
||||
- [ ] Verify command appears in Zed's "/" completion menu
|
||||
- [ ] Verify command executes and streams results to agent panel
|
||||
- [ ] Verify commands work with existing MCP proxy and tool permissions
|
||||
|
||||
---
|
||||
|
||||
@@ -739,8 +1014,26 @@ async runCommand(request: RunCommandRequest): Promise<void> {
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### User Experience
|
||||
No migration needed - this is a new feature that gracefully degrades for agents that don't support custom commands. Existing agent panel behavior is preserved.
|
||||
|
||||
### Developer Coordination
|
||||
**Important**: This feature requires coordinated releases across multiple repositories:
|
||||
|
||||
1. **ACP Protocol**: Must be released first with new slash command methods
|
||||
2. **Zed**: Can only merge after ACP release is available
|
||||
3. **Agent Implementations**: Can adopt new capabilities independently
|
||||
|
||||
### Version Compatibility
|
||||
- **Backward Compatible**: Old agents continue working without slash command menus
|
||||
- **Forward Compatible**: New Zed version works with old agents (feature simply disabled)
|
||||
- **Graceful Degradation**: UI adapts based on agent-advertised capabilities
|
||||
|
||||
### Rollout Strategy
|
||||
1. **Phase 1 Release**: ACP protocol extension (no visible user changes)
|
||||
2. **Phase 2 Release**: Zed UI implementation (menu appears only with compatible agents)
|
||||
3. **Phase 3+ Rollout**: Agent implementations adopt new capabilities over time
|
||||
|
||||
## References
|
||||
|
||||
- Original research: `thoughts/shared/research/2025-08-28_15-34-28_custom-slash-commands-acp.md`
|
||||
|
||||
Reference in New Issue
Block a user