2025-08-28_20-04-42_agent-panel-slash-command-menu.md

   1# Agent Panel Slash Command Menu Implementation Plan
   2
   3## Overview
   4
   5Add a searchable "/" command menu to the agent panel that appears when users type "/" - but only if the connected ACP agent supports custom slash commands. This enables users to discover and execute commands from `.claude/commands/` directory through a clean UI interface.
   6
   7## Current State Analysis
   8
   9**Agent Panel Message Editor**: Uses sophisticated completion system for "@" mentions with `PopoverMenu + Picker` pattern. Has existing slash command detection (`parse_slash_command()`) but only for highlighting/prevention, not menus.
  10
  11**ACP Capability System**: Well-established capability negotiation during `initialize()` with `PromptCapabilities`. UI adapts reactively via `AcpThreadEvent::PromptCapabilitiesUpdated`.
  12
  13**UI Patterns**: Perfect existing patterns in text thread slash command picker using `PopoverMenu + Picker` with `SlashCommandSelector` and `PickerDelegate` traits.
  14
  15**ACP Protocol**: Clear extension patterns via external `agent-client-protocol` crate with request/response enums and method dispatch.
  16
  17### Key Discoveries:
  18- `crates/agent_ui/src/acp/message_editor.rs:1573` - Existing slash detection foundation
  19- `crates/agent_ui/src/acp/completion_provider.rs:763` - Pattern for "@" completion triggers  
  20- `crates/agent_ui/src/slash_command_picker.rs:54` - Exact UI pattern we need to follow
  21- `crates/agent_servers/src/acp.rs:152` - Capability storage and distribution
  22
  23## Desired End State
  24
  25When user types "/" in agent panel message editor:
  26- **Agent supports commands**: Searchable menu appears with commands from `.claude/commands/`
  27- **Agent doesn't support commands**: No menu appears (preserves current behavior)
  28- **Command execution**: Selected commands execute via ACP protocol, stream results to thread view
  29- **Keyboard navigation**: Arrow keys, Enter to select, Escape to dismiss
  30
  31### Verification:
  32- Menu appears only when `supports_custom_commands` capability is true
  33- Commands populated from ACP `list_commands()` RPC call
  34- Selected commands execute via ACP `run_command()` RPC call
  35- Results stream back as `SessionUpdate` notifications
  36
  37## What We're NOT Doing
  38
  39- NOT modifying existing assistant/text thread slash commands
  40- NOT implementing command parsing/execution logic in Zed (that's agent-side)
  41- NOT adding command discovery beyond what agents provide
  42- NOT changing the UI for agents that don't support custom commands
  43
  44## Implementation Approach
  45
  46Follow 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.
  47
  48## Repository Dependencies & PR Strategy
  49
  50### **Multi-Repository Architecture**
  51This feature spans three repositories that must be coordinated:
  52
  531. **`agent-client-protocol`**: External crate defining the protocol
  542. **`zed`**: Main editor with ACP client integration  
  553. **`claude-code-acp`**: Reference agent implementation
  56
  57### **Dependency Chain**
  58```
  59agent-client-protocol (Phase 1) 
  60  61zed (Phase 2) - temporarily depends on local ACP changes
  62  63claude-code-acp (Phase 3) - uses published ACP version
  64```
  65
  66### **Development Workflow**
  67
  68#### Step 1: Local Development Setup
  69```bash
  70# Work on ACP protocol extension locally
  71cd /Users/nathan/src/agent-client-protocol
  72# Make Phase 1 changes...
  73
  74# Point Zed to local ACP version for testing
  75cd /Users/nathan/src/zed
  76# Update Cargo.toml to reference local path:
  77# agent-client-protocol = { path = "../agent-client-protocol" }
  78```
  79
  80#### Step 2: Testing & Validation
  81- Test all phases end-to-end with local dependencies
  82- Verify Phase 1+2 integration works correctly
  83- Validate Phase 3 against local ACP changes
  84
  85#### Step 3: PR Sequence
  861. **First PR**: `agent-client-protocol` with new slash command methods
  872. **Second PR**: `zed` referencing published ACP version (after #1 merges)
  883. **Third PR**: `claude-code-acp` using new ACP capabilities
  89
  90### **Temporary Dependency Management**
  91During development, Zed's `Cargo.toml` will need:
  92```toml
  93[dependencies]
  94# Temporary local reference for development/testing
  95agent-client-protocol = { path = "../agent-client-protocol" }
  96
  97# After ACP PR merges, switch to:  
  98agent-client-protocol = "0.2.0-alpha.1"  # or appropriate version
  99```
 100
 101### **Cross-Repository Verification**
 102Before opening PRs:
 103- [x] ACP protocol extension compiles and tests pass
 104- [x] Zed compiles against local ACP changes
 105- [ ] End-to-end slash command flow works locally
 106- [ ] Claude ACP adapter works with generated types
 107
 108## Phase 1: ACP Protocol Extension
 109
 110### Overview
 111Add new slash command RPC methods and capabilities to the agent-client-protocol crate, then integrate them into Zed's ACP connection layer.
 112
 113### Changes Required:
 114
 115#### 1. Protocol Types (External Crate)
 116**File**: `/Users/nathan/src/agent-client-protocol/rust/agent.rs`
 117**Changes**: Add new request/response types and trait methods following exact ACP patterns
 118
 119**Step 1: Add Method Constants** (after line 415):
 120```rust
 121/// Method name for listing custom commands in a session.
 122pub const SESSION_LIST_COMMANDS: &str = "session/list_commands";
 123/// Method name for running a custom command in a session.  
 124pub const SESSION_RUN_COMMAND: &str = "session/run_command";
 125```
 126
 127**Step 2: Add Request/Response Structs** (after PromptCapabilities at line 371):
 128```rust
 129/// Request parameters for listing available commands.
 130#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 131#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
 132#[serde(rename_all = "camelCase")]
 133pub struct ListCommandsRequest {
 134    /// The session ID to list commands for.
 135    pub session_id: SessionId,
 136}
 137
 138/// Response containing available commands.
 139#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 140#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
 141#[serde(rename_all = "camelCase")]
 142pub struct ListCommandsResponse {
 143    /// List of available commands.
 144    pub commands: Vec<CommandInfo>,
 145}
 146
 147/// Information about a custom command.
 148#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 149#[serde(rename_all = "camelCase")]
 150pub struct CommandInfo {
 151    /// Command name (e.g., "create_plan", "research_codebase").
 152    pub name: String,
 153    /// Human-readable description of what the command does.
 154    pub description: String,
 155    /// Whether this command requires arguments from the user.
 156    pub requires_argument: bool,
 157}
 158
 159/// Request parameters for executing a command.
 160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 161#[schemars(extend("x-side" = "agent", "x-method" = "session/run_command"))]
 162#[serde(rename_all = "camelCase")]
 163pub struct RunCommandRequest {
 164    /// The session ID to execute the command in.
 165    pub session_id: SessionId,
 166    /// Name of the command to execute.
 167    pub command: String,
 168    /// Optional arguments for the command.
 169    pub args: Option<String>,
 170}
 171```
 172
 173**Step 3: Add Agent Trait Methods** (after `cancel()` method at line 107):
 174```rust
 175/// Lists available custom commands for a session.
 176///
 177/// Returns all commands available in the agent's `.claude/commands` directory
 178/// or equivalent command registry. Commands can be executed via `run_command`.
 179fn list_commands(
 180    &self,
 181    arguments: ListCommandsRequest,
 182) -> impl Future<Output = Result<ListCommandsResponse, Error>>;
 183
 184/// Executes a custom command within a session.
 185///
 186/// Runs the specified command with optional arguments. The agent should
 187/// stream results back via session update notifications.
 188fn run_command(
 189    &self,
 190    arguments: RunCommandRequest,
 191) -> impl Future<Output = Result<(), Error>>;
 192```
 193
 194**Step 4: Add Enum Routing Variants** (to `ClientRequest` enum around line 423):
 195```rust
 196#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 197#[serde(untagged)]
 198pub enum ClientRequest {
 199    InitializeRequest(InitializeRequest),
 200    AuthenticateRequest(AuthenticateRequest),
 201    NewSessionRequest(NewSessionRequest),
 202    LoadSessionRequest(LoadSessionRequest),
 203    PromptRequest(PromptRequest),
 204    ListCommandsRequest(ListCommandsRequest),  // ADD THIS
 205    RunCommandRequest(RunCommandRequest),      // ADD THIS
 206}
 207```
 208
 209**Step 5: Add AgentResponse Enum Variant** (find AgentResponse enum):
 210```rust
 211ListCommandsResponse(ListCommandsResponse),  // ADD THIS
 212```
 213
 214#### 2. Capability Extension
 215**File**: `/Users/nathan/src/agent-client-protocol/rust/agent.rs`
 216**Changes**: Extend PromptCapabilities with custom command support
 217
 218```rust
 219// Modify PromptCapabilities struct around line 358
 220#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
 221#[serde(rename_all = "camelCase")]
 222pub struct PromptCapabilities {
 223    /// Agent supports [`ContentBlock::Image`].
 224    #[serde(default)]
 225    pub image: bool,
 226    /// Agent supports [`ContentBlock::Audio`].
 227    #[serde(default)]
 228    pub audio: bool,
 229    /// Agent supports embedded context in `session/prompt` requests.
 230    #[serde(default)]
 231    pub embedded_context: bool,
 232    /// Agent supports custom slash commands via `list_commands` and `run_command`.
 233    #[serde(default)]
 234    pub supports_custom_commands: bool,
 235}
 236```
 237
 238#### 3. AgentConnection Trait Extension
 239**File**: `crates/acp_thread/src/connection.rs`
 240**Changes**: Add new methods to trait definition
 241
 242```rust
 243// Add these methods to AgentConnection trait around line 80
 244fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>>;
 245fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>>;
 246```
 247
 248#### 4. ACP Connection Implementation  
 249**File**: `crates/agent_servers/src/acp.rs`
 250**Changes**: Implement new trait methods in AcpConnection following existing patterns
 251
 252**Step 1: Add ClientSideConnection Methods** (after existing methods around line 340):
 253```rust
 254impl acp::ClientSideConnection {
 255    /// Lists available custom commands for a session.
 256    pub async fn list_commands(
 257        &self,
 258        request: acp::ListCommandsRequest,
 259    ) -> Result<acp::ListCommandsResponse, acp::Error> {
 260        self.connection
 261            .request(acp::ClientRequest::ListCommandsRequest(request))
 262            .await
 263            .and_then(|response| match response {
 264                acp::AgentResponse::ListCommandsResponse(response) => Ok(response),
 265                _ => Err(acp::Error::internal_error("Invalid response type")),
 266            })
 267    }
 268
 269    /// Executes a custom command in a session.
 270    pub async fn run_command(
 271        &self,
 272        request: acp::RunCommandRequest,
 273    ) -> Result<(), acp::Error> {
 274        self.connection
 275            .request(acp::ClientRequest::RunCommandRequest(request))
 276            .await
 277            .map(|_| ())
 278    }
 279}
 280```
 281
 282**Step 2: Implement AgentConnection Trait Methods** (for AcpConnection struct):
 283```rust
 284fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
 285    let conn = self.connection.clone();
 286    let session_id = session_id.clone();
 287    cx.foreground_executor().spawn(async move {
 288        conn.list_commands(acp::ListCommandsRequest { session_id }).await
 289    })
 290}
 291
 292fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>> {
 293    let conn = self.connection.clone();
 294    cx.foreground_executor().spawn(async move {
 295        conn.run_command(request).await
 296    })
 297}
 298```
 299
 300**Step 3: Update Message Dispatch Logic** (in `AgentSide::decode_request()` around line 493):
 301```rust
 302// Add cases to the match statement:
 303acp::SESSION_LIST_COMMANDS => {
 304    if let Ok(request) = serde_json::from_value::<acp::ListCommandsRequest>(params) {
 305        Ok(acp::ClientRequest::ListCommandsRequest(request))
 306    } else {
 307        Err(acp::Error::invalid_params("Invalid list_commands parameters"))
 308    }
 309}
 310acp::SESSION_RUN_COMMAND => {
 311    if let Ok(request) = serde_json::from_value::<acp::RunCommandRequest>(params) {
 312        Ok(acp::ClientRequest::RunCommandRequest(request))
 313    } else {
 314        Err(acp::Error::invalid_params("Invalid run_command parameters"))
 315    }
 316}
 317```
 318
 319#### 5. Capability Detection
 320**File**: `crates/acp_thread/src/acp_thread.rs`
 321**Changes**: Add capability checking helper
 322
 323```rust
 324// Add around line 280 after other capability methods
 325pub fn supports_custom_commands(&self, cx: &App) -> bool {
 326    self.prompt_capabilities.get(cx).supports_custom_commands
 327}
 328```
 329
 330### Success Criteria:
 331
 332#### Automated Verification:
 333- [x] Protocol crate compiles: `cd /Users/nathan/src/agent-client-protocol && cargo check`
 334- [x] Protocol tests pass: `cd /Users/nathan/src/agent-client-protocol && cargo test`
 335- [ ] Schema generation works: `cd /Users/nathan/src/agent-client-protocol && cargo run --bin generate`
 336- [ ] Schema includes new methods: `grep -A5 -B5 "session/list_commands\|session/run_command" /Users/nathan/src/agent-client-protocol/schema/schema.json`
 337- [x] Zed compiles successfully: `./script/clippy`
 338- [x] No linting errors: `cargo clippy --package agent_servers --package acp_thread`
 339- [x] ACP thread capability method compiles: `cargo check --package acp_thread`
 340
 341#### Manual Verification:
 342- [x] New trait methods are properly defined in Agent trait (`/Users/nathan/src/agent-client-protocol/rust/agent.rs`)
 343  - Verify `list_commands()` method signature at line ~115
 344  - Verify `run_command()` method signature at line ~127
 345- [x] Request/response enums updated in ClientRequest (`agent.rs:~423`) and AgentResponse enums
 346- [x] Method constants added (`SESSION_LIST_COMMANDS`, `SESSION_RUN_COMMAND`) after line 415
 347- [x] PromptCapabilities extended with `supports_custom_commands: bool` field
 348- [x] ClientSideConnection methods implemented with proper error handling
 349- [ ] Message dispatch logic updated in `AgentSide::decode_request()`
 350- [x] AgentConnection trait extends with new methods (`crates/acp_thread/src/connection.rs`)
 351- [x] AcpConnection implements trait methods (`crates/agent_servers/src/acp.rs`)
 352- [x] AcpThread has `supports_custom_commands()` helper method
 353
 354---
 355
 356## Phase 2: Slash Command Menu UI
 357
 358### Overview
 359Add "/" detection and command menu to the ACP message editor, following the existing "@" completion pattern.
 360
 361### Changes Required:
 362
 363#### 1. Command Info Types
 364**File**: `crates/agent_ui/src/acp/completion_provider.rs`
 365**Changes**: Add command completion types
 366
 367```rust
 368// Add around line 50 after existing completion types
 369#[derive(Debug, Clone)]
 370pub struct SlashCommandCompletion {
 371    pub name: String,
 372    pub description: String,
 373    pub requires_argument: bool,
 374    pub source_range: Range<usize>,
 375    pub command_range: Range<usize>,
 376}
 377
 378impl SlashCommandCompletion {
 379    fn try_parse(line: &str, cursor_offset: usize) -> Option<Self> {
 380        // Parse "/" followed by optional command name
 381        if let Some(remainder) = line.strip_prefix('/') {
 382            let mut chars = remainder.char_indices().peekable();
 383            let mut command_end = 0;
 384            
 385            // Find end of command name (alphanumeric + underscore)
 386            while let Some((i, ch)) = chars.next() {
 387                if ch.is_alphanumeric() || ch == '_' {
 388                    command_end = i + ch.len_utf8();
 389                } else {
 390                    break;
 391                }
 392            }
 393            
 394            Some(SlashCommandCompletion {
 395                name: remainder[..command_end].to_string(),
 396                description: String::new(),
 397                requires_argument: false,
 398                source_range: 0..cursor_offset,
 399                command_range: 1..command_end + 1, // Skip the "/"
 400            })
 401        } else {
 402            None
 403        }
 404    }
 405}
 406```
 407
 408#### 2. Completion Trigger Detection
 409**File**: `crates/agent_ui/src/acp/completion_provider.rs`
 410**Changes**: Extend `is_completion_trigger()` method following existing patterns
 411
 412**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.
 413
 414```rust
 415// Modify the existing is_completion_trigger() method around line 763
 416impl CompletionProvider for ContextPickerCompletionProvider {
 417    fn is_completion_trigger(
 418        &self,
 419        buffer: &Entity<Buffer>,
 420        position: language::Anchor,
 421        text: &str,
 422        _trigger_in_words: bool,
 423        cx: &mut Context<Editor>,
 424    ) -> bool {
 425        let buffer = buffer.read(cx);
 426        let position = position.to_point(&buffer);
 427        let line_start = Point::new(position.row, 0);
 428        let mut lines = buffer.text_for_range(line_start..position).lines();
 429        let Some(line) = lines.next() else {
 430            return false;
 431        };
 432
 433        // Existing @ mention logic - KEEP THIS
 434        if let Some(_) = MentionCompletion::try_parse(&line, position.column) {
 435            return true;
 436        }
 437        
 438        // ADD: Slash command detection (only if agent supports commands)
 439        if let Some(thread) = &self.thread {
 440            if thread.read(cx).supports_custom_commands(cx) {
 441                if let Some(_) = SlashCommandCompletion::try_parse(&line, position.column) {
 442                    return true;
 443                }
 444            }
 445        }
 446        
 447        false
 448    }
 449}
 450```
 451
 452**Pattern Notes**: 
 453- Integrates with existing `@` mention system without conflicts
 454- Only triggers when agent capability `supports_custom_commands` is true
 455- Uses same line parsing approach as existing mention system
 456- Maintains backward compatibility
 457
 458#### 3. Command Completion Generation
 459**File**: `crates/agent_ui/src/acp/completion_provider.rs`
 460**Changes**: Extend `completions()` method using existing async patterns
 461
 462**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.
 463
 464```rust
 465// Modify the existing completions() method around line 700
 466// ADD this after the existing mention completion logic:
 467
 468// Handle slash command completions (only if agent supports them)
 469if let Some(thread) = &self.thread {
 470    if thread.read(cx).supports_custom_commands(cx) {
 471        if let Some(slash_completion) = SlashCommandCompletion::try_parse(&line, cursor_offset) {
 472            return self.complete_slash_commands(
 473                slash_completion,
 474                buffer.clone(),
 475                cursor_anchor,
 476                cx,
 477            );
 478        }
 479    }
 480}
 481
 482// ADD new method following existing async patterns (around line 850):
 483fn complete_slash_commands(
 484    &self,
 485    completion: SlashCommandCompletion,
 486    buffer: Entity<Buffer>,
 487    cursor_anchor: language::Anchor,
 488    cx: &mut Context<Editor>,
 489) -> Task<Result<Vec<project::CompletionResponse>>> {
 490    let Some(thread) = self.thread.clone() else {
 491        return Task::ready(Ok(Vec::new()));
 492    };
 493    
 494    cx.spawn(async move |cx| {
 495        // Get session info using existing patterns
 496        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone())?;
 497        let connection = thread.read_with(cx, |thread, _| thread.connection().clone())?;
 498        
 499        // Fetch commands from agent via new ACP method
 500        let response = connection.list_commands(&session_id, cx).await?;
 501        
 502        // Filter commands matching typed prefix (fuzzy matching like mentions)
 503        let matching_commands: Vec<_> = response.commands
 504            .into_iter()
 505            .filter(|cmd| {
 506                // Support both prefix matching and fuzzy matching
 507                cmd.name.starts_with(&completion.name) ||
 508                cmd.name.to_lowercase().contains(&completion.name.to_lowercase())
 509            })
 510            .collect();
 511        
 512        // Convert to project::Completion following existing patterns
 513        let mut completions = Vec::new();
 514        for command in matching_commands {
 515            let new_text = format!("/{}", command.name);
 516            let completion_item = project::Completion {
 517                old_range: completion.source_range.clone(),
 518                new_text,
 519                label: command.name.clone().into(),
 520                server_id: language::LanguageServerId(0), // Not from language server
 521                kind: Some(language::CompletionKind::Function),
 522                documentation: if !command.description.is_empty() {
 523                    Some(language::Documentation::SingleLine(command.description.clone()))
 524                } else {
 525                    None
 526                },
 527                // Custom confirmation handler for command execution
 528                confirm: Some(Arc::new(SlashCommandConfirmation {
 529                    command: command.name,
 530                    requires_argument: command.requires_argument,
 531                    thread: thread.downgrade(),
 532                })),
 533                ..Default::default()
 534            };
 535            completions.push(completion_item);
 536        }
 537        
 538        // Return single completion response (like existing mentions)
 539        Ok(vec![project::CompletionResponse {
 540            completions,
 541            is_incomplete: false,
 542        }])
 543    })
 544}
 545```
 546
 547**Integration Notes**:
 548- Follows same async pattern as existing mention completions at line 639
 549- Uses `thread.read_with()` pattern for safe entity access
 550- Implements fuzzy matching similar to existing completion types
 551- Returns single `CompletionResponse` following established patterns
 552- Integrates custom confirmation handler via `confirm` field
 553
 554#### 4. Command Confirmation Handler
 555**File**: `crates/agent_ui/src/acp/completion_provider.rs`
 556**Changes**: Add confirmation handler for slash commands
 557
 558```rust
 559// Add around line 950
 560#[derive(Debug)]
 561struct SlashCommandConfirmation {
 562    command: String,
 563    requires_argument: bool,
 564    thread: WeakEntity<AcpThread>,
 565}
 566
 567impl language::CompletionConfirm for SlashCommandConfirmation {
 568    fn confirm(
 569        &self,
 570        completion: &project::Completion,
 571        buffer: &mut Buffer,
 572        mut cursor_positions: Vec<language::Anchor>,
 573        trigger_text: &str,
 574        _workspace: Option<&Workspace>,
 575        window: &mut Window,
 576        cx: &mut Context<Buffer>,
 577    ) -> Option<Task<Result<Vec<language::Anchor>>>> {
 578        if self.requires_argument {
 579            // Keep cursor after command name for argument input
 580            return None; // Let default behavior handle text insertion
 581        }
 582        
 583        // Execute command immediately
 584        let Some(thread) = self.thread.upgrade() else {
 585            return None;
 586        };
 587        
 588        let command = self.command.clone();
 589        let task = cx.spawn(async move |cx| {
 590            thread
 591                .update(cx, |thread, cx| {
 592                    thread.run_command(command, None, cx)
 593                })
 594                .ok();
 595            Ok(cursor_positions)
 596        });
 597        
 598        Some(task)
 599    }
 600}
 601```
 602
 603#### 5. Command Execution Method
 604**File**: `crates/acp_thread/src/acp_thread.rs`
 605**Changes**: Add command execution method
 606
 607```rust
 608// Add around line 450 after other public methods
 609pub fn run_command(
 610    &mut self,
 611    command: String,
 612    args: Option<String>,
 613    cx: &mut Context<Self>,
 614) -> Task<Result<()>> {
 615    let session_id = self.session_id.clone();
 616    let connection = self.connection.clone();
 617    
 618    cx.spawn(async move |this, cx| {
 619        let request = acp::RunCommandRequest {
 620            session_id,
 621            command,
 622            args,
 623        };
 624        
 625        connection.run_command(request, cx).await?;
 626        
 627        // The agent will send back results via SessionUpdate notifications
 628        // which will be handled by existing handle_session_update() logic
 629        Ok(())
 630    })
 631}
 632```
 633
 634### Success Criteria:
 635
 636#### Automated Verification:
 637- [x] Code compiles successfully: `./script/clippy`
 638- [x] No linting errors: `cargo clippy --package agent_ui --package acp_thread`
 639- [x] Type checking passes: `cargo check --package agent_ui --package acp_thread`
 640- [x] Completion provider compiles: `cargo check --package agent_ui --lib`
 641- [ ] Slash command parsing works: Test `SlashCommandCompletion::try_parse()` with various inputs
 642
 643#### Manual Verification (REVISED - Simpler Approach):
 644- [x] **Refactored to Simpler Architecture**: 
 645  - [x] Remove complex `CompositeCompletionProvider` and `AgentSlashCommandCompletionProvider`
 646  - [x] Extend existing `ContextPickerCompletionProvider` with optional thread field
 647  - [x] Add `set_thread()` method for lifecycle management
 648  - [ ] Add slash command detection to `is_completion_trigger()`
 649  - [ ] Add slash command completion to `completions()` method
 650- [ ] **Slash Command Integration**: 
 651  - [ ] Parse slash commands using existing `SlashCommandLine` from assistant_slash_command
 652  - [ ] Fetch commands via ACP `list_commands()` RPC when thread supports it
 653  - [ ] Execute commands via ACP `run_command()` RPC with proper confirmation
 654  - [ ] Only show slash completions when `supports_custom_commands = true`
 655- [x] **MessageEditor Integration**:
 656  - [x] Add `set_thread()` method to update completion provider when thread is ready
 657  - [ ] Call `set_thread()` in ThreadView when thread transitions to Ready state
 658- [ ] Integration Testing:
 659  - [ ] Typing "/" in agent panel triggers completion when `supports_custom_commands = true`
 660  - [ ] No "/" completion appears when `supports_custom_commands = false`
 661  - [ ] Command list fetched from agent via `list_commands()` RPC call
 662  - [ ] Command selection triggers `run_command()` RPC call
 663  - [ ] Menu shows command descriptions from agent
 664  - [ ] Fuzzy matching works (typing "/cr" shows "create_plan")
 665  - [ ] Menu dismisses properly on Escape or click-outside
 666  - [ ] Commands execute and stream results back to thread view
 667
 668---
 669
 670## Phase 3: Agent Implementation Support
 671
 672### Overview
 673Prepare the Claude Code ACP adapter to implement the new slash command RPC methods by adding command parsing and execution.
 674
 675### **CRITICAL ARCHITECTURE NOTE** 
 676The TypeScript types in `claude-code-acp` are **automatically generated** from the Rust protocol definitions. The ACP repository uses a code generation pipeline:
 677
 6781. **Rust → JSON Schema**: `cargo run --bin generate` creates `schema/schema.json` from Rust types
 6792. **JSON Schema → TypeScript**: `node typescript/generate.js` creates TypeScript types from the schema
 680
 681**This means the new `ListCommandsRequest`, `RunCommandRequest`, etc. types will be automatically available in TypeScript after we extend the Rust protocol in Phase 1.**
 682
 683### Changes Required:
 684
 685#### 1. Command Parsing Module
 686**File**: `claude-code-acp/src/command-parser.ts` (new file)
 687**Changes**: Add markdown command parser (TypeScript types will be auto-generated from Phase 1)
 688
 689```typescript
 690import * as fs from 'fs';
 691import * as path from 'path';
 692
 693export interface CommandInfo {
 694  name: string;
 695  description: string;
 696  requires_argument: boolean;
 697  content?: string; // Full command content for execution
 698}
 699
 700export class CommandParser {
 701  private commandsDir: string;
 702  private cachedCommands?: CommandInfo[];
 703
 704  constructor(cwd: string) {
 705    this.commandsDir = path.join(cwd, '.claude', 'commands');
 706  }
 707
 708  async listCommands(): Promise<CommandInfo[]> {
 709    if (this.cachedCommands) {
 710      return this.cachedCommands;
 711    }
 712
 713    try {
 714      if (!fs.existsSync(this.commandsDir)) {
 715        return [];
 716      }
 717
 718      const files = fs.readdirSync(this.commandsDir)
 719        .filter(file => file.endsWith('.md'));
 720
 721      const commands: CommandInfo[] = [];
 722      for (const file of files) {
 723        const filePath = path.join(this.commandsDir, file);
 724        const content = fs.readFileSync(filePath, 'utf-8');
 725        const commandInfo = this.parseCommandFile(content, file);
 726        if (commandInfo) {
 727          commands.push(commandInfo);
 728        }
 729      }
 730
 731      this.cachedCommands = commands;
 732      return commands;
 733    } catch (error) {
 734      console.error('Failed to list commands:', error);
 735      return [];
 736    }
 737  }
 738
 739  private parseCommandFile(content: string, filename: string): CommandInfo | null {
 740    const lines = content.split('\n');
 741    let name = '';
 742    let description = '';
 743    let requires_argument = false;
 744
 745    // Extract command name from H1 title
 746    const titleMatch = lines.find(line => line.startsWith('# '));
 747    if (titleMatch) {
 748      name = titleMatch.replace('# ', '').trim().toLowerCase().replace(/\s+/g, '_');
 749    } else {
 750      // Fall back to filename without extension
 751      name = path.basename(filename, '.md');
 752    }
 753
 754    // Extract description (text after H1, before first H2)
 755    const titleIndex = lines.findIndex(line => line.startsWith('# '));
 756    if (titleIndex >= 0) {
 757      const nextHeaderIndex = lines.findIndex((line, i) => 
 758        i > titleIndex && line.startsWith('## '));
 759      const endIndex = nextHeaderIndex >= 0 ? nextHeaderIndex : lines.length;
 760      
 761      description = lines
 762        .slice(titleIndex + 1, endIndex)
 763        .join('\n')
 764        .trim()
 765        .split('\n')[0] || ''; // First non-empty line as description
 766    }
 767
 768    // Check if command requires arguments (heuristic)
 769    requires_argument = content.includes('arguments') || 
 770                      content.includes('parameter') ||
 771                      content.includes('[arg]') ||
 772                      content.includes('{arg}');
 773
 774    return {
 775      name,
 776      description,
 777      requires_argument,
 778      content
 779    };
 780  }
 781
 782  async getCommand(name: string): Promise<CommandInfo | null> {
 783    const commands = await this.listCommands();
 784    return commands.find(cmd => cmd.name === name) || null;
 785  }
 786
 787  // Clear cache when commands directory changes
 788  invalidateCache(): void {
 789    this.cachedCommands = undefined;
 790  }
 791}
 792```
 793
 794#### 2. Regenerate TypeScript Types
 795**Prerequisites**: After completing Phase 1 Rust protocol extension
 796**Commands**: Generate TypeScript types from updated Rust definitions
 797
 798```bash
 799# From agent-client-protocol repository root:
 800cd /Users/nathan/src/agent-client-protocol
 801npm run generate
 802```
 803
 804This will automatically create TypeScript types for:
 805- `ListCommandsRequest`
 806- `ListCommandsResponse` 
 807- `RunCommandRequest`
 808- `CommandInfo`
 809- Updated `PromptCapabilities` with `supports_custom_commands`
 810
 811#### 3. ACP Agent Method Implementation
 812**File**: `claude-code-acp/src/acp-agent.ts`
 813**Changes**: Add new RPC method handlers following existing session management patterns
 814
 815**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.
 816
 817```typescript
 818// Step 1: Add import for command parser and auto-generated ACP types
 819import { CommandParser, CommandInfo } from './command-parser';
 820import type { 
 821  ListCommandsRequest, 
 822  ListCommandsResponse, 
 823  RunCommandRequest 
 824} from '@zed-industries/agent-client-protocol';
 825
 826// Step 2: Extend ClaudeAcpAgent class (add to class definition around line 51)
 827export class ClaudeAcpAgent implements Agent {
 828  private sessions: Map<string, Session> = new Map();
 829  private client: Client;
 830  private commandParser?: CommandParser;  // ADD THIS
 831
 832  // Step 3: Modify constructor to initialize command parser (around line 60)
 833  constructor(
 834    client: Client,
 835    options: { cwd?: string } = {}
 836  ) {
 837    this.client = client;
 838    
 839    // Initialize command parser if .claude/commands directory exists
 840    if (options.cwd && fs.existsSync(path.join(options.cwd, '.claude', 'commands'))) {
 841      this.commandParser = new CommandParser(options.cwd);
 842    }
 843  }
 844
 845  // Step 4: Update initialize() method to advertise capability (around line 68)
 846  async initialize(request: InitializeRequest): Promise<InitializeResponse> {
 847    return {
 848      protocol_version: VERSION,
 849      agent_capabilities: {
 850        prompt_capabilities: {
 851          image: true,
 852          audio: false,
 853          embedded_context: true,
 854          supports_custom_commands: !!this.commandParser, // Advertise support
 855        },
 856      },
 857      auth_methods: [/* existing auth methods */],
 858    };
 859  }
 860
 861  // Step 5: Implement listCommands following existing async patterns (after line 218)
 862  async listCommands(request: ListCommandsRequest): Promise<ListCommandsResponse> {
 863    if (!this.commandParser) {
 864      return { commands: [] };
 865    }
 866
 867    try {
 868      const commands = await this.commandParser.listCommands();
 869      return {
 870        commands: commands.map(cmd => ({
 871          name: cmd.name,
 872          description: cmd.description,
 873          requires_argument: cmd.requires_argument,
 874        }))
 875      };
 876    } catch (error) {
 877      console.error('Failed to list commands:', error);
 878      return { commands: [] };
 879    }
 880  }
 881
 882  // Step 6: Implement runCommand integrating with existing session flow
 883  async runCommand(request: RunCommandRequest): Promise<void> {
 884    if (!this.commandParser) {
 885      throw new Error('Commands not supported');
 886    }
 887
 888    const command = await this.commandParser.getCommand(request.command);
 889    if (!command) {
 890      throw new Error(`Command not found: ${request.command}`);
 891    }
 892
 893    const session = this.sessions.get(request.session_id);
 894    if (!session) {
 895      throw new Error('Session not found');
 896    }
 897
 898    try {
 899      // Build prompt from command content following existing patterns
 900      let commandPrompt = command.content;
 901      
 902      if (command.requires_argument && request.args) {
 903        commandPrompt += `\n\nArguments: ${request.args}`;
 904      }
 905
 906      // Execute via existing session input stream (recommended approach)
 907      // This integrates with existing prompt() flow and MCP proxy
 908      session.input.push({
 909        role: 'user',
 910        content: commandPrompt
 911      });
 912
 913      // Results will be streamed back via existing query execution loop
 914      // at line 150 in prompt() method, no additional streaming needed
 915
 916    } catch (error) {
 917      console.error('Command execution failed:', error);
 918      // Send error via existing session update mechanism
 919      await this.client.sessionUpdate({
 920        session_id: request.session_id,
 921        type: 'agent_message_chunk',
 922        content: {
 923          type: 'text',
 924          text: `Error executing command: ${error.message}`,
 925        },
 926      });
 927    }
 928  }
 929}
 930```
 931
 932**Integration Notes**:
 933- **Auto-Generated Types**: All ACP protocol types are automatically generated from Rust definitions
 934- **Session Reuse**: Uses existing session's input stream and MCP configuration
 935- **Result Streaming**: Leverages existing `prompt()` method's streaming loop at line 150
 936- **Error Handling**: Uses established session update patterns from line 191
 937- **Tool Access**: Commands inherit session's MCP server and tool configurations
 938
 939### Success Criteria:
 940
 941#### Automated Verification:
 942- [ ] **Prerequisites completed**: Phase 1 Rust protocol extension must be completed first
 943- [ ] **TypeScript types generated**: `cd /Users/nathan/src/agent-client-protocol && npm run generate`
 944- [ ] **Types available**: Verify new types exist in `agent-client-protocol/typescript/schema.ts`
 945- [ ] TypeScript compilation passes: `cd /Users/nathan/src/claude-code-acp && npm run typecheck`
 946- [ ] ESLint passes: `cd /Users/nathan/src/claude-code-acp && npm run lint`
 947- [ ] Agent compiles: `cd /Users/nathan/src/claude-code-acp && npm run build`
 948- [ ] Command parser unit tests pass: `cd /Users/nathan/src/claude-code-acp && npm test -- --testNamePattern="command-parser"`
 949
 950#### Manual Verification:
 951- [ ] **Code Generation Pipeline**:
 952  - [ ] Rust protocol changes trigger successful schema generation: `cargo run --bin generate`
 953  - [ ] JSON schema contains new method definitions: `grep -A5 -B5 "session/list_commands\|session/run_command" /Users/nathan/src/agent-client-protocol/schema/schema.json`
 954  - [ ] TypeScript types generated correctly: Check for `ListCommandsRequest`, `RunCommandRequest` types in schema.ts
 955- [ ] CommandParser class implemented (`claude-code-acp/src/command-parser.ts`):
 956  - `listCommands()` method reads `.claude/commands/*.md` files
 957  - `parseCommandFile()` extracts H1 titles and descriptions correctly
 958  - `getCommand()` returns full command content for execution
 959  - Proper error handling for missing directories and files
 960  - Command caching works correctly
 961- [ ] ClaudeAcpAgent class extended (`claude-code-acp/src/acp-agent.ts`):
 962  - Constructor initializes `commandParser` when `.claude/commands` exists
 963  - `initialize()` method advertises `supports_custom_commands` capability correctly
 964  - `listCommands()` method implemented and returns properly formatted response
 965  - `runCommand()` method integrated with existing session management
 966  - Command execution uses existing session input stream 
 967  - Error handling streams errors back via session updates
 968- [ ] **Type Integration**:
 969  - [ ] Auto-generated types imported correctly from `@zed-industries/agent-client-protocol`
 970  - [ ] TypeScript compiler recognizes new protocol method signatures
 971  - [ ] No type errors when implementing new agent methods
 972- [ ] Integration Testing:
 973  - [ ] Agent advertises `supports_custom_commands = true` when `.claude/commands` directory exists
 974  - [ ] Agent advertises `supports_custom_commands = false` when directory doesn't exist
 975  - [ ] `list_commands()` RPC returns commands from `.claude/commands/*.md` files  
 976  - [ ] Commands include correct name, description, requires_argument fields
 977  - [ ] `run_command()` executes command content via Claude SDK integration
 978  - [ ] Command results stream back as session updates to ACP client
 979  - [ ] Commands have access to session's MCP servers and tool permissions
 980  - [ ] Error handling works for missing commands, directories, execution failures
 981  - [ ] Command arguments are properly appended when provided
 982- [ ] End-to-End Testing:
 983  - [ ] Create test `.claude/commands/test.md` file with sample command
 984  - [ ] Verify command appears in Zed's "/" completion menu
 985  - [ ] Verify command executes and streams results to agent panel
 986  - [ ] Verify commands work with existing MCP proxy and tool permissions
 987
 988---
 989
 990## Testing Strategy
 991
 992### Unit Tests:
 993- Command parser correctly extracts name, description, and argument requirements
 994- Slash completion parsing handles various input formats
 995- Capability detection works with different agent configurations
 996
 997### Integration Tests:  
 998- End-to-end slash command flow from "/" keystroke to command execution
 999- Menu appearance/dismissal based on agent capabilities
1000- Command completion filtering and selection
1001
1002### Manual Testing Steps:
10031. Connect to agent without custom command support → verify no "/" menu
10042. Connect to agent with custom command support → verify "/" shows menu
10053. Type "/cr" → verify "create_plan" command appears in filtered list
10064. Select command with arguments → verify argument input continues
10075. Select command without arguments → verify immediate execution
10086. Press Escape during menu → verify menu dismisses
10097. Click outside menu → verify menu dismisses
1010
1011## Performance Considerations
1012
1013- Command list caching in agent to avoid repeated filesystem reads
1014- Debounced completion triggers to avoid excessive RPC calls  
1015- Async command execution to prevent UI blocking
1016- Menu virtualization for large command lists (if needed)
1017
1018## Migration Notes
1019
1020### User Experience
1021No migration needed - this is a new feature that gracefully degrades for agents that don't support custom commands. Existing agent panel behavior is preserved.
1022
1023### Developer Coordination
1024**Important**: This feature requires coordinated releases across multiple repositories:
1025
10261. **ACP Protocol**: Must be released first with new slash command methods
10272. **Zed**: Can only merge after ACP release is available  
10283. **Agent Implementations**: Can adopt new capabilities independently
1029
1030### Version Compatibility
1031- **Backward Compatible**: Old agents continue working without slash command menus
1032- **Forward Compatible**: New Zed version works with old agents (feature simply disabled)
1033- **Graceful Degradation**: UI adapts based on agent-advertised capabilities
1034
1035### Rollout Strategy
10361. **Phase 1 Release**: ACP protocol extension (no visible user changes)
10372. **Phase 2 Release**: Zed UI implementation (menu appears only with compatible agents)
10383. **Phase 3+ Rollout**: Agent implementations adopt new capabilities over time
1039
1040## References
1041
1042- Original research: `thoughts/shared/research/2025-08-28_15-34-28_custom-slash-commands-acp.md`
1043- Text thread slash command picker: `crates/agent_ui/src/slash_command_picker.rs:54-348`
1044- ACP completion provider: `crates/agent_ui/src/acp/completion_provider.rs:763`
1045- Agent capability negotiation: `crates/agent_servers/src/acp.rs:131-156`