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## Phase 1: ACP Protocol Extension
 49
 50### Overview
 51Add new slash command RPC methods and capabilities to the agent-client-protocol crate, then integrate them into Zed's ACP connection layer.
 52
 53### Changes Required:
 54
 55#### 1. Protocol Types (External Crate)
 56**File**: `/Users/nathan/src/agent-client-protocol/rust/agent.rs`
 57**Changes**: Add new request/response types and trait methods
 58
 59```rust
 60// Add around line 108 after the cancel method in the Agent trait
 61/// Lists available custom commands for a session.
 62///
 63/// Returns all commands available in the agent's `.claude/commands` directory
 64/// or equivalent command registry. Commands can be executed via `run_command`.
 65fn list_commands(
 66    &self,
 67    arguments: ListCommandsRequest,
 68) -> impl Future<Output = Result<ListCommandsResponse, Error>>;
 69
 70/// Executes a custom command within a session.
 71///
 72/// Runs the specified command with optional arguments. The agent should
 73/// stream results back via session update notifications.
 74fn run_command(
 75    &self,
 76    arguments: RunCommandRequest,
 77) -> impl Future<Output = Result<(), Error>>;
 78
 79// Add around line 372 after PromptCapabilities
 80/// Request parameters for listing available commands.
 81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 82#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
 83#[serde(rename_all = "camelCase")]
 84pub struct ListCommandsRequest {
 85    /// The session ID to list commands for.
 86    pub session_id: SessionId,
 87}
 88
 89/// Response containing available commands.
 90#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 91#[schemars(extend("x-side" = "agent", "x-method" = "session/list_commands"))]
 92#[serde(rename_all = "camelCase")]
 93pub struct ListCommandsResponse {
 94    /// List of available commands.
 95    pub commands: Vec<CommandInfo>,
 96}
 97
 98/// Information about a custom command.
 99#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
100#[serde(rename_all = "camelCase")]
101pub struct CommandInfo {
102    /// Command name (e.g., "create_plan", "research_codebase").
103    pub name: String,
104    /// Human-readable description of what the command does.
105    pub description: String,
106    /// Whether this command requires arguments from the user.
107    pub requires_argument: bool,
108}
109
110/// Request parameters for executing a command.
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
112#[schemars(extend("x-side" = "agent", "x-method" = "session/run_command"))]
113#[serde(rename_all = "camelCase")]
114pub struct RunCommandRequest {
115    /// The session ID to execute the command in.
116    pub session_id: SessionId,
117    /// Name of the command to execute.
118    pub command: String,
119    /// Optional arguments for the command.
120    pub args: Option<String>,
121}
122```
123
124#### 2. Capability Extension
125**File**: `/Users/nathan/src/agent-client-protocol/rust/agent.rs`
126**Changes**: Extend PromptCapabilities with custom command support
127
128```rust
129// Modify PromptCapabilities struct around line 358
130#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
131#[serde(rename_all = "camelCase")]
132pub struct PromptCapabilities {
133    /// Agent supports [`ContentBlock::Image`].
134    #[serde(default)]
135    pub image: bool,
136    /// Agent supports [`ContentBlock::Audio`].
137    #[serde(default)]
138    pub audio: bool,
139    /// Agent supports embedded context in `session/prompt` requests.
140    #[serde(default)]
141    pub embedded_context: bool,
142    /// Agent supports custom slash commands via `list_commands` and `run_command`.
143    #[serde(default)]
144    pub supports_custom_commands: bool,
145}
146```
147
148#### 3. AgentConnection Trait Extension
149**File**: `crates/acp_thread/src/connection.rs`
150**Changes**: Add new methods to trait definition
151
152```rust
153// Add these methods to AgentConnection trait around line 80
154fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>>;
155fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>>;
156```
157
158#### 4. ACP Connection Implementation  
159**File**: `crates/agent_servers/src/acp.rs`
160**Changes**: Implement new trait methods in AcpConnection
161
162```rust
163// Add around line 340 after existing method implementations
164fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
165    let conn = self.connection.clone();
166    let session_id = session_id.clone();
167    cx.foreground_executor().spawn(async move {
168        conn.list_commands(acp::ListCommandsRequest { session_id }).await
169    })
170}
171
172fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>> {
173    let conn = self.connection.clone();
174    cx.foreground_executor().spawn(async move {
175        conn.run_command(request).await
176    })
177}
178```
179
180#### 5. Capability Detection
181**File**: `crates/acp_thread/src/acp_thread.rs`
182**Changes**: Add capability checking helper
183
184```rust
185// Add around line 280 after other capability methods
186pub fn supports_custom_commands(&self, cx: &App) -> bool {
187    self.prompt_capabilities.get(cx).supports_custom_commands
188}
189```
190
191### Success Criteria:
192
193#### Automated Verification:
194- [ ] Protocol crate compiles: `cd /Users/nathan/src/agent-client-protocol && cargo check`
195- [ ] Protocol tests pass: `cd /Users/nathan/src/agent-client-protocol && cargo test`
196- [ ] Schema generation works: `cd /Users/nathan/src/agent-client-protocol && cargo run --bin generate`
197- [ ] Zed compiles successfully: `./script/clippy`
198- [ ] No linting errors: `cargo clippy --package agent_servers --package acp_thread`
199
200#### Manual Verification:
201- [ ] New trait methods are properly defined in connection interface
202- [ ] ACP connection implements new methods correctly
203- [ ] Capability detection helper is available for UI layer
204- [ ] JSON schema includes new request/response types
205
206---
207
208## Phase 2: Slash Command Menu UI
209
210### Overview
211Add "/" detection and command menu to the ACP message editor, following the existing "@" completion pattern.
212
213### Changes Required:
214
215#### 1. Command Info Types
216**File**: `crates/agent_ui/src/acp/completion_provider.rs`
217**Changes**: Add command completion types
218
219```rust
220// Add around line 50 after existing completion types
221#[derive(Debug, Clone)]
222pub struct SlashCommandCompletion {
223    pub name: String,
224    pub description: String,
225    pub requires_argument: bool,
226    pub source_range: Range<usize>,
227    pub command_range: Range<usize>,
228}
229
230impl SlashCommandCompletion {
231    fn try_parse(line: &str, cursor_offset: usize) -> Option<Self> {
232        // Parse "/" followed by optional command name
233        if let Some(remainder) = line.strip_prefix('/') {
234            let mut chars = remainder.char_indices().peekable();
235            let mut command_end = 0;
236            
237            // Find end of command name (alphanumeric + underscore)
238            while let Some((i, ch)) = chars.next() {
239                if ch.is_alphanumeric() || ch == '_' {
240                    command_end = i + ch.len_utf8();
241                } else {
242                    break;
243                }
244            }
245            
246            Some(SlashCommandCompletion {
247                name: remainder[..command_end].to_string(),
248                description: String::new(),
249                requires_argument: false,
250                source_range: 0..cursor_offset,
251                command_range: 1..command_end + 1, // Skip the "/"
252            })
253        } else {
254            None
255        }
256    }
257}
258```
259
260#### 2. Completion Trigger Detection
261**File**: `crates/agent_ui/src/acp/completion_provider.rs`
262**Changes**: Extend `is_completion_trigger()` method
263
264```rust
265// Modify around line 763 to add slash command detection
266pub fn is_completion_trigger(
267    &self,
268    buffer: &Entity<Buffer>,
269    position: language::Anchor,
270    text: &str,
271    _trigger_in_words: bool,
272    cx: &mut Context<Editor>,
273) -> bool {
274    // Existing @ mention logic...
275    if let Some(_) = MentionCompletion::try_parse(&line, position.column) {
276        return true;
277    }
278    
279    // Add slash command detection
280    if let Some(thread) = &self.thread {
281        if thread.read(cx).supports_custom_commands(cx) {
282            if let Some(_) = SlashCommandCompletion::try_parse(&line, position.column) {
283                return true;
284            }
285        }
286    }
287    
288    false
289}
290```
291
292#### 3. Command Completion Generation
293**File**: `crates/agent_ui/src/acp/completion_provider.rs`
294**Changes**: Extend `completions()` method
295
296```rust
297// Add around line 700 in completions() method after mention handling
298// Handle slash command completions
299if let Some(thread) = &self.thread {
300    if thread.read(cx).supports_custom_commands(cx) {
301        if let Some(slash_completion) = SlashCommandCompletion::try_parse(&line, cursor_offset) {
302            return self.complete_slash_commands(
303                slash_completion,
304                buffer.clone(),
305                cursor_anchor,
306                cx,
307            );
308        }
309    }
310}
311
312// Add new method around line 850
313fn complete_slash_commands(
314    &self,
315    completion: SlashCommandCompletion,
316    buffer: Entity<Buffer>,
317    cursor_anchor: language::Anchor,
318    cx: &mut Context<Editor>,
319) -> Task<Result<Vec<project::CompletionResponse>>> {
320    let Some(thread) = self.thread.clone() else {
321        return Task::ready(Ok(Vec::new()));
322    };
323    
324    cx.spawn(async move |cx| {
325        let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone())?;
326        let connection = thread.read_with(cx, |thread, _| thread.connection().clone())?;
327        
328        // Fetch commands from agent
329        let commands = connection.list_commands(&session_id, cx).await?;
330        
331        // Filter commands matching typed prefix
332        let matching_commands: Vec<_> = commands
333            .into_iter()
334            .filter(|cmd| cmd.name.starts_with(&completion.name))
335            .collect();
336        
337        // Convert to completion responses
338        let mut completions = Vec::new();
339        for command in matching_commands {
340            let new_text = format!("/{}", command.name);
341            let completion = project::Completion {
342                old_range: completion.source_range.clone(),
343                new_text,
344                label: command.name.clone().into(),
345                server_id: language::LanguageServerId(0), // Not from language server
346                kind: Some(language::CompletionKind::Function),
347                documentation: if !command.description.is_empty() {
348                    Some(language::Documentation::SingleLine(command.description.clone()))
349                } else {
350                    None
351                },
352                confirm: Some(Arc::new(SlashCommandConfirmation {
353                    command: command.name,
354                    requires_argument: command.requires_argument,
355                    thread: thread.downgrade(),
356                })),
357                ..Default::default()
358            };
359            completions.push(completion);
360        }
361        
362        Ok(vec![project::CompletionResponse {
363            completions,
364            is_incomplete: false,
365        }])
366    })
367}
368```
369
370#### 4. Command Confirmation Handler
371**File**: `crates/agent_ui/src/acp/completion_provider.rs`
372**Changes**: Add confirmation handler for slash commands
373
374```rust
375// Add around line 950
376#[derive(Debug)]
377struct SlashCommandConfirmation {
378    command: String,
379    requires_argument: bool,
380    thread: WeakEntity<AcpThread>,
381}
382
383impl language::CompletionConfirm for SlashCommandConfirmation {
384    fn confirm(
385        &self,
386        completion: &project::Completion,
387        buffer: &mut Buffer,
388        mut cursor_positions: Vec<language::Anchor>,
389        trigger_text: &str,
390        _workspace: Option<&Workspace>,
391        window: &mut Window,
392        cx: &mut Context<Buffer>,
393    ) -> Option<Task<Result<Vec<language::Anchor>>>> {
394        if self.requires_argument {
395            // Keep cursor after command name for argument input
396            return None; // Let default behavior handle text insertion
397        }
398        
399        // Execute command immediately
400        let Some(thread) = self.thread.upgrade() else {
401            return None;
402        };
403        
404        let command = self.command.clone();
405        let task = cx.spawn(async move |cx| {
406            thread
407                .update(cx, |thread, cx| {
408                    thread.run_command(command, None, cx)
409                })
410                .ok();
411            Ok(cursor_positions)
412        });
413        
414        Some(task)
415    }
416}
417```
418
419#### 5. Command Execution Method
420**File**: `crates/acp_thread/src/acp_thread.rs`
421**Changes**: Add command execution method
422
423```rust
424// Add around line 450 after other public methods
425pub fn run_command(
426    &mut self,
427    command: String,
428    args: Option<String>,
429    cx: &mut Context<Self>,
430) -> Task<Result<()>> {
431    let session_id = self.session_id.clone();
432    let connection = self.connection.clone();
433    
434    cx.spawn(async move |this, cx| {
435        let request = acp::RunCommandRequest {
436            session_id,
437            command,
438            args,
439        };
440        
441        connection.run_command(request, cx).await?;
442        
443        // The agent will send back results via SessionUpdate notifications
444        // which will be handled by existing handle_session_update() logic
445        Ok(())
446    })
447}
448```
449
450### Success Criteria:
451
452#### Automated Verification:
453- [ ] Code compiles successfully: `./script/clippy`
454- [ ] No linting errors: `cargo clippy --package agent_ui --package acp_thread`
455- [ ] Type checking passes: `cargo check --package agent_ui --package acp_thread`
456
457#### Manual Verification:
458- [ ] Typing "/" in agent panel triggers completion when agent supports commands
459- [ ] No "/" completion appears when agent doesn't support commands
460- [ ] Command list fetched from agent via `list_commands()` RPC
461- [ ] Command selection triggers `run_command()` RPC
462- [ ] Menu dismisses properly on Escape or click-outside
463
464---
465
466## Phase 3: Agent Implementation Support
467
468### Overview
469Prepare the Claude Code ACP adapter to implement the new slash command RPC methods by adding command parsing and execution.
470
471### Changes Required:
472
473#### 1. Command Parsing Module
474**File**: `claude-code-acp/src/command-parser.ts` (new file)
475**Changes**: Add markdown command parser
476
477```typescript
478import * as fs from 'fs';
479import * as path from 'path';
480
481export interface CommandInfo {
482  name: string;
483  description: string;
484  requires_argument: boolean;
485  content?: string; // Full command content for execution
486}
487
488export class CommandParser {
489  private commandsDir: string;
490  private cachedCommands?: CommandInfo[];
491
492  constructor(cwd: string) {
493    this.commandsDir = path.join(cwd, '.claude', 'commands');
494  }
495
496  async listCommands(): Promise<CommandInfo[]> {
497    if (this.cachedCommands) {
498      return this.cachedCommands;
499    }
500
501    try {
502      if (!fs.existsSync(this.commandsDir)) {
503        return [];
504      }
505
506      const files = fs.readdirSync(this.commandsDir)
507        .filter(file => file.endsWith('.md'));
508
509      const commands: CommandInfo[] = [];
510      for (const file of files) {
511        const filePath = path.join(this.commandsDir, file);
512        const content = fs.readFileSync(filePath, 'utf-8');
513        const commandInfo = this.parseCommandFile(content, file);
514        if (commandInfo) {
515          commands.push(commandInfo);
516        }
517      }
518
519      this.cachedCommands = commands;
520      return commands;
521    } catch (error) {
522      console.error('Failed to list commands:', error);
523      return [];
524    }
525  }
526
527  private parseCommandFile(content: string, filename: string): CommandInfo | null {
528    const lines = content.split('\n');
529    let name = '';
530    let description = '';
531    let requires_argument = false;
532
533    // Extract command name from H1 title
534    const titleMatch = lines.find(line => line.startsWith('# '));
535    if (titleMatch) {
536      name = titleMatch.replace('# ', '').trim().toLowerCase().replace(/\s+/g, '_');
537    } else {
538      // Fall back to filename without extension
539      name = path.basename(filename, '.md');
540    }
541
542    // Extract description (text after H1, before first H2)
543    const titleIndex = lines.findIndex(line => line.startsWith('# '));
544    if (titleIndex >= 0) {
545      const nextHeaderIndex = lines.findIndex((line, i) => 
546        i > titleIndex && line.startsWith('## '));
547      const endIndex = nextHeaderIndex >= 0 ? nextHeaderIndex : lines.length;
548      
549      description = lines
550        .slice(titleIndex + 1, endIndex)
551        .join('\n')
552        .trim()
553        .split('\n')[0] || ''; // First non-empty line as description
554    }
555
556    // Check if command requires arguments (heuristic)
557    requires_argument = content.includes('arguments') || 
558                      content.includes('parameter') ||
559                      content.includes('[arg]') ||
560                      content.includes('{arg}');
561
562    return {
563      name,
564      description,
565      requires_argument,
566      content
567    };
568  }
569
570  async getCommand(name: string): Promise<CommandInfo | null> {
571    const commands = await this.listCommands();
572    return commands.find(cmd => cmd.name === name) || null;
573  }
574
575  // Clear cache when commands directory changes
576  invalidateCache(): void {
577    this.cachedCommands = undefined;
578  }
579}
580```
581
582#### 2. ACP Agent Method Implementation
583**File**: `claude-code-acp/src/acp-agent.ts`
584**Changes**: Add new RPC method handlers
585
586```typescript
587// Add import
588import { CommandParser, CommandInfo } from './command-parser';
589
590// Add to ClaudeAcpAgent class around line 50
591private commandParser?: CommandParser;
592
593// Modify constructor around line 100
594constructor(options: ClaudeAcpAgentOptions) {
595  // ... existing initialization
596  
597  // Initialize command parser if .claude/commands exists
598  if (options.cwd) {
599    this.commandParser = new CommandParser(options.cwd);
600  }
601}
602
603// Add capability declaration in initialize() around line 150
604async initialize(request: InitializeRequest): Promise<InitializeResponse> {
605  return {
606    protocol_version: VERSION,
607    agent_capabilities: {
608      prompt_capabilities: {
609        image: true,
610        audio: false,
611        embedded_context: true,
612        supports_custom_commands: !!this.commandParser, // Enable if commands exist
613      },
614    },
615    auth_methods: ['claude-code'],
616  };
617}
618
619// Add new RPC method handlers around line 400
620async listCommands(request: ListCommandsRequest): Promise<ListCommandsResponse> {
621  if (!this.commandParser) {
622    return { commands: [] };
623  }
624
625  try {
626    const commands = await this.commandParser.listCommands();
627    return {
628      commands: commands.map(cmd => ({
629        name: cmd.name,
630        description: cmd.description,
631        requires_argument: cmd.requires_argument,
632      }))
633    };
634  } catch (error) {
635    console.error('Failed to list commands:', error);
636    return { commands: [] };
637  }
638}
639
640async runCommand(request: RunCommandRequest): Promise<void> {
641  if (!this.commandParser) {
642    throw new Error('Commands not supported');
643  }
644
645  const command = await this.commandParser.getCommand(request.command);
646  if (!command) {
647    throw new Error(`Command not found: ${request.command}`);
648  }
649
650  // Execute command by sending its content as a system prompt to Claude SDK
651  const session = this.sessions.get(request.session_id);
652  if (!session) {
653    throw new Error('Session not found');
654  }
655
656  try {
657    let systemPrompt = command.content;
658    
659    // If command requires arguments and args provided, append them
660    if (command.requires_argument && request.args) {
661      systemPrompt += `\n\nArguments: ${request.args}`;
662    }
663
664    // Create new query with command content as system prompt
665    const query = query({
666      prompt: systemPrompt,
667      options: {
668        cwd: session.cwd,
669        mcpServers: session.mcpServers,
670        allowedTools: session.allowedTools,
671        disallowedTools: session.disallowedTools,
672      },
673    });
674
675    // Stream results back to session
676    for await (const chunk of query) {
677      // Convert query response to session update format
678      const update = this.convertQueryChunkToSessionUpdate(chunk);
679      await this.sendSessionUpdate(request.session_id, update);
680    }
681
682  } catch (error) {
683    console.error('Command execution failed:', error);
684    // Send error as session update
685    await this.sendSessionUpdate(request.session_id, {
686      type: 'agent_message_chunk',
687      content: {
688        type: 'text',
689        text: `Error executing command: ${error.message}`,
690      },
691    });
692  }
693}
694```
695
696### Success Criteria:
697
698#### Automated Verification:
699- [ ] TypeScript compilation passes: `npm run typecheck` (in claude-code-acp)
700- [ ] ESLint passes: `npm run lint` (in claude-code-acp)
701- [ ] Command parser unit tests pass: `npm test command-parser` 
702
703#### Manual Verification:
704- [ ] `.claude/commands/*.md` files are correctly parsed for metadata
705- [ ] `list_commands()` returns available commands with descriptions
706- [ ] `run_command()` executes command content via Claude SDK
707- [ ] Command results stream back as session updates
708- [ ] Error handling works for missing commands or execution failures
709
710---
711
712## Testing Strategy
713
714### Unit Tests:
715- Command parser correctly extracts name, description, and argument requirements
716- Slash completion parsing handles various input formats
717- Capability detection works with different agent configurations
718
719### Integration Tests:  
720- End-to-end slash command flow from "/" keystroke to command execution
721- Menu appearance/dismissal based on agent capabilities
722- Command completion filtering and selection
723
724### Manual Testing Steps:
7251. Connect to agent without custom command support → verify no "/" menu
7262. Connect to agent with custom command support → verify "/" shows menu
7273. Type "/cr" → verify "create_plan" command appears in filtered list
7284. Select command with arguments → verify argument input continues
7295. Select command without arguments → verify immediate execution
7306. Press Escape during menu → verify menu dismisses
7317. Click outside menu → verify menu dismisses
732
733## Performance Considerations
734
735- Command list caching in agent to avoid repeated filesystem reads
736- Debounced completion triggers to avoid excessive RPC calls  
737- Async command execution to prevent UI blocking
738- Menu virtualization for large command lists (if needed)
739
740## Migration Notes
741
742No migration needed - this is a new feature that gracefully degrades for agents that don't support custom commands. Existing agent panel behavior is preserved.
743
744## References
745
746- Original research: `thoughts/shared/research/2025-08-28_15-34-28_custom-slash-commands-acp.md`
747- Text thread slash command picker: `crates/agent_ui/src/slash_command_picker.rs:54-348`
748- ACP completion provider: `crates/agent_ui/src/acp/completion_provider.rs:763`
749- Agent capability negotiation: `crates/agent_servers/src/acp.rs:131-156`