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`