@@ -41,9 +41,11 @@ import * as fs from 'node:fs/promises';
4141import * as path from 'node:path' ;
4242import { z } from 'zod' ;
4343import { getErrorMessage } from '../../utils/errors.js' ;
44+ import { normalizePartList } from '../../utils/nonInteractiveHelpers.js' ;
4445import {
4546 handleSlashCommand ,
4647 getAvailableCommands ,
48+ type NonInteractiveSlashCommandResult ,
4749} from '../../nonInteractiveCliCommands.js' ;
4850import type {
4951 AvailableCommand ,
@@ -63,12 +65,6 @@ import { PlanEmitter } from './emitters/PlanEmitter.js';
6365import { MessageEmitter } from './emitters/MessageEmitter.js' ;
6466import { SubAgentTracker } from './SubAgentTracker.js' ;
6567
66- /**
67- * Built-in commands that are allowed in ACP integration mode.
68- * Only safe, read-only commands that don't require interactive UI.
69- */
70- export const ALLOWED_BUILTIN_COMMANDS_FOR_ACP = [ 'init' ] ;
71-
7268/**
7369 * Session represents an active conversation session with the AI model.
7470 * It uses modular components for consistent event emission:
@@ -167,24 +163,26 @@ export class Session implements SessionContext {
167163 const firstTextBlock = params . prompt . find ( ( block ) => block . type === 'text' ) ;
168164 const inputText = firstTextBlock ?. text || '' ;
169165
170- let parts : Part [ ] ;
166+ let parts : Part [ ] | null ;
171167
172168 if ( isSlashCommand ( inputText ) ) {
173- // Handle slash command - allow specific built-in commands for ACP integration
169+ // Handle slash command - uses default allowed commands (init, summary, compress)
174170 const slashCommandResult = await handleSlashCommand (
175171 inputText ,
176172 pendingSend ,
177173 this . config ,
178174 this . settings ,
179- ALLOWED_BUILTIN_COMMANDS_FOR_ACP ,
180175 ) ;
181176
182- if ( slashCommandResult ) {
183- // Use the result from the slash command
184- parts = slashCommandResult as Part [ ] ;
185- } else {
186- // Slash command didn't return a prompt, continue with normal processing
187- parts = await this . #resolvePrompt( params . prompt , pendingSend . signal ) ;
177+ parts = await this . #processSlashCommandResult(
178+ slashCommandResult ,
179+ params . prompt ,
180+ ) ;
181+
182+ // If parts is null, the command was fully handled (e.g., /summary completed)
183+ // Return early without sending to the model
184+ if ( parts === null ) {
185+ return { stopReason : 'end_turn' } ;
188186 }
189187 } else {
190188 // Normal processing for non-slash commands
@@ -295,11 +293,10 @@ export class Session implements SessionContext {
295293 async sendAvailableCommandsUpdate ( ) : Promise < void > {
296294 const abortController = new AbortController ( ) ;
297295 try {
296+ // Use default allowed commands from getAvailableCommands
298297 const slashCommands = await getAvailableCommands (
299298 this . config ,
300- this . settings ,
301299 abortController . signal ,
302- ALLOWED_BUILTIN_COMMANDS_FOR_ACP ,
303300 ) ;
304301
305302 // Convert SlashCommand[] to AvailableCommand[] format for ACP protocol
@@ -647,6 +644,103 @@ export class Session implements SessionContext {
647644 }
648645 }
649646
647+ /**
648+ * Processes the result of a slash command execution.
649+ *
650+ * Supported result types in ACP mode:
651+ * - submit_prompt: Submits content to the model
652+ * - stream_messages: Streams multiple messages to the client (ACP-specific)
653+ * - unsupported: Command cannot be executed in ACP mode
654+ * - no_command: No command was found, use original prompt
655+ *
656+ * Note: 'message' type is not supported in ACP mode - commands should use
657+ * 'stream_messages' instead for consistent async handling.
658+ *
659+ * @param result The result from handleSlashCommand
660+ * @param originalPrompt The original prompt blocks
661+ * @returns Parts to use for the prompt, or null if command was handled without needing model interaction
662+ */
663+ async #processSlashCommandResult(
664+ result : NonInteractiveSlashCommandResult ,
665+ originalPrompt : acp . ContentBlock [ ] ,
666+ ) : Promise < Part [ ] | null > {
667+ switch ( result . type ) {
668+ case 'submit_prompt' :
669+ // Command wants to submit a prompt to the model
670+ // Convert PartListUnion to Part[]
671+ return normalizePartList ( result . content ) ;
672+
673+ case 'message' : {
674+ // 'message' type is not ideal for ACP mode, but we handle it for compatibility
675+ // by converting it to a stream_messages-like notification
676+ await this . client . sendCustomNotification ( '_qwencode/slash_command' , {
677+ sessionId : this . sessionId ,
678+ command : originalPrompt
679+ . filter ( ( block ) => block . type === 'text' )
680+ . map ( ( block ) => ( block . type === 'text' ? block . text : '' ) )
681+ . join ( ' ' ) ,
682+ messageType : result . messageType ,
683+ message : result . content || '' ,
684+ } ) ;
685+
686+ if ( result . messageType === 'error' ) {
687+ // Throw error to stop execution
688+ throw new Error ( result . content || 'Slash command failed.' ) ;
689+ }
690+ // For info messages, return null to indicate command was handled
691+ return null ;
692+ }
693+
694+ case 'stream_messages' : {
695+ // Command returns multiple messages via async generator (ACP-preferred)
696+ const command = originalPrompt
697+ . filter ( ( block ) => block . type === 'text' )
698+ . map ( ( block ) => ( block . type === 'text' ? block . text : '' ) )
699+ . join ( ' ' ) ;
700+
701+ // Stream all messages to the client
702+ for await ( const msg of result . messages ) {
703+ await this . client . sendCustomNotification ( '_qwencode/slash_command' , {
704+ sessionId : this . sessionId ,
705+ command,
706+ messageType : msg . messageType ,
707+ message : msg . content ,
708+ } ) ;
709+
710+ // If we encounter an error message, throw after sending
711+ if ( msg . messageType === 'error' ) {
712+ throw new Error ( msg . content || 'Slash command failed.' ) ;
713+ }
714+ }
715+
716+ // All messages sent successfully, return null to indicate command was handled
717+ return null ;
718+ }
719+
720+ case 'unsupported' : {
721+ // Command returned an unsupported result type
722+ const unsupportedError = `Slash command not supported in ACP integration: ${ result . reason } ` ;
723+ throw new Error ( unsupportedError ) ;
724+ }
725+
726+ case 'no_command' :
727+ // No command was found or executed, use original prompt
728+ return originalPrompt . map ( ( block ) => {
729+ if ( block . type === 'text' ) {
730+ return { text : block . text } ;
731+ }
732+ throw new Error ( `Unsupported block type: ${ block . type } ` ) ;
733+ } ) ;
734+
735+ default : {
736+ // Exhaustiveness check
737+ const _exhaustive : never = result ;
738+ const unknownError = `Unknown slash command result type: ${ ( _exhaustive as NonInteractiveSlashCommandResult ) . type } ` ;
739+ throw new Error ( unknownError ) ;
740+ }
741+ }
742+ }
743+
650744 async #resolvePrompt(
651745 message : acp . ContentBlock [ ] ,
652746 abortSignal : AbortSignal ,
0 commit comments