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`