@@ -2,12 +2,16 @@ import type * as acp from "@agentclientprotocol/sdk";
22import type { AgentSideConnection , AvailableCommand } from "@agentclientprotocol/sdk" ;
33import { ACPSessionConnection } from "./ACPSessionConnection" ;
44import type { CodexAcpClient } from "./CodexAcpClient" ;
5- import type { RateLimitSnapshot , SkillsListEntry } from "./app-server/v2" ;
5+ import type { RateLimitSnapshot , ReviewTarget , SkillsListEntry , TurnCompletedNotification } from "./app-server/v2" ;
66import type { SessionState } from "./CodexAcpServer" ;
77import type { RateLimitsMap } from "./RateLimitsMap" ;
88import type { TokenCount } from "./TokenCount" ;
99import { logger } from "./Logger" ;
1010
11+ type CommandHandlingResult =
12+ | { handled : false }
13+ | { handled : true ; turnCompleted ?: TurnCompletedNotification } ;
14+
1115export class CodexCommands {
1216 private readonly connection : AgentSideConnection ;
1317 private readonly codexAcpClient : CodexAcpClient ;
@@ -73,6 +77,11 @@ export class CodexCommands {
7377 description : "List configured Model Context Protocol (MCP) tools." ,
7478 input : null
7579 } ,
80+ {
81+ name : "review" ,
82+ description : "Review uncommitted changes." ,
83+ input : null
84+ } ,
7685 {
7786 name : "skills" ,
7887 description : "List available skills." ,
@@ -91,7 +100,7 @@ export class CodexCommands {
91100 ] ;
92101 }
93102
94- private getCommandName ( prompt : acp . ContentBlock [ ] ) : string | null {
103+ private parseCommand ( prompt : acp . ContentBlock [ ] ) : ParsedCommand | null {
95104 const firstBlock = prompt [ 0 ] ;
96105 if ( ! firstBlock || firstBlock . type != "text" ) return null ;
97106
@@ -101,25 +110,33 @@ export class CodexCommands {
101110 const commandText = text . slice ( 1 ) . trim ( ) ;
102111 if ( commandText . length === 0 ) return null ;
103112
104- const [ name ] = commandText . split ( / \s + / ) ;
105- return name ?. toLowerCase ( ) ?? null ;
113+ const separatorIndex = commandText . search ( / \s / ) ;
114+ if ( separatorIndex === - 1 ) {
115+ return { name : commandText . toLowerCase ( ) , input : null } ;
116+ }
117+ const name = commandText . slice ( 0 , separatorIndex ) . toLowerCase ( ) ;
118+ const input = commandText . slice ( separatorIndex ) . trim ( ) ;
119+ return { name, input : input . length > 0 ? input : null } ;
106120 }
107121
108- async tryHandleCommand ( prompt : acp . ContentBlock [ ] , sessionState : SessionState ) : Promise < boolean > {
109- const commandName = this . getCommandName ( prompt ) ;
110- if ( commandName === null ) return false ;
111- if ( commandName . startsWith ( "$" ) ) return false ;
122+ async tryHandleCommand (
123+ prompt : acp . ContentBlock [ ] ,
124+ sessionState : SessionState
125+ ) : Promise < CommandHandlingResult > {
126+ const command = this . parseCommand ( prompt ) ;
127+ if ( command === null ) return { handled : false } ;
128+ if ( command . name . startsWith ( "$" ) ) return { handled : false } ;
112129
113130 const sessionId = sessionState . sessionId ;
114- switch ( commandName ) {
131+ switch ( command . name ) {
115132 case "status" : {
116133 const session = new ACPSessionConnection ( this . connection , sessionId ) ;
117134 const message = this . buildStatusMessage ( sessionState ) ;
118135 await session . update ( {
119136 sessionUpdate : "agent_message_chunk" ,
120137 content : { type : "text" , text : message }
121138 } ) ;
122- return true ;
139+ return { handled : true } ;
123140 }
124141 case "logout" : {
125142 await this . runWithProcessCheck ( ( ) => this . codexAcpClient . logout ( ) ) ;
@@ -128,7 +145,7 @@ export class CodexCommands {
128145 sessionUpdate : "agent_message_chunk" ,
129146 content : { type : "text" , text : "Logged out from Codex account." }
130147 } ) ;
131- return true ;
148+ return { handled : true } ;
132149 }
133150 case "skills" : {
134151 const response = await this . runWithProcessCheck ( ( ) => this . codexAcpClient . listSkills ( ) ) ;
@@ -145,7 +162,7 @@ export class CodexCommands {
145162 sessionUpdate : "agent_message_chunk" ,
146163 content : { type : "text" , text }
147164 } ) ;
148- return true ;
165+ return { handled : true } ;
149166 }
150167 case "mcp" : {
151168 const servers = await this . runWithProcessCheck ( ( ) => this . codexAcpClient . listMcpServers ( ) ) ;
@@ -166,14 +183,33 @@ export class CodexCommands {
166183 sessionUpdate : "agent_message_chunk" ,
167184 content : { type : "text" , text }
168185 } ) ;
169- return true ;
186+ return { handled : true } ;
187+ }
188+ case "review" : {
189+ if ( command . input !== null ) {
190+ await this . sendCommandMessage ( sessionId , "/review does not accept arguments yet." ) ;
191+ return { handled : true } ;
192+ }
193+ const target : ReviewTarget = { type : "uncommittedChanges" } ;
194+ const turnCompleted = await this . runWithProcessCheck ( ( ) =>
195+ this . codexAcpClient . runReview ( sessionId , target )
196+ ) ;
197+ return { handled : true , turnCompleted} ;
170198 }
171199 default :
172- await this . sendUnknownCommandMessage ( commandName , sessionId ) ;
173- return true ;
200+ await this . sendUnknownCommandMessage ( command . name , sessionId ) ;
201+ return { handled : true } ;
174202 }
175203 }
176204
205+ private async sendCommandMessage ( sessionId : string , text : string ) : Promise < void > {
206+ const session = new ACPSessionConnection ( this . connection , sessionId ) ;
207+ await session . update ( {
208+ sessionUpdate : "agent_message_chunk" ,
209+ content : { type : "text" , text}
210+ } ) ;
211+ }
212+
177213 private async sendUnknownCommandMessage ( name : string , sessionId : string ) : Promise < void > {
178214 const lines = this . getBuiltinCommands ( ) . map ( command => `- /${ command . name } : ${ command . description } ` ) ;
179215 const text = [
@@ -343,4 +379,7 @@ export class CodexCommands {
343379 }
344380}
345381
346- type ParsedCommand = { name : string ; } ;
382+ type ParsedCommand = {
383+ name : string ;
384+ input : string | null ;
385+ } ;
0 commit comments