@@ -90,6 +90,7 @@ struct AsyncProcess {
9090 stderr_lines : Arc < Mutex < Vec < String > > > ,
9191 exit_code : Arc < Mutex < Option < i32 > > > ,
9292 command : String ,
93+ revert_command : Option < String > , // None = cannot revert, Some("") = no state change
9394 started_at : chrono:: DateTime < chrono:: Utc > ,
9495 // Keep child for kill
9596 child : Arc < Mutex < Option < Child > > > ,
@@ -105,7 +106,7 @@ impl ProcessRegistry {
105106 Self { processes : HashMap :: new ( ) }
106107 }
107108
108- async fn start ( & mut self , command : & str , working_dir : Option < & str > , env : Option < HashMap < String , String > > ) -> anyhow:: Result < String > {
109+ async fn start ( & mut self , command : & str , working_dir : Option < & str > , env : Option < HashMap < String , String > > , revert_command : Option < String > ) -> anyhow:: Result < String > {
109110 let id = Uuid :: new_v4 ( ) . to_string ( ) ;
110111
111112 let mut cmd = Command :: new ( "sh" ) ;
@@ -186,6 +187,7 @@ impl ProcessRegistry {
186187 stderr_lines,
187188 exit_code,
188189 command : command. to_string ( ) ,
190+ revert_command,
189191 started_at : chrono:: Utc :: now ( ) ,
190192 child : child_arc,
191193 } ;
@@ -237,16 +239,20 @@ impl ProcessRegistry {
237239 false
238240 }
239241
240- fn list ( & self ) -> Vec < ( String , String , String , bool ) > {
242+ fn list ( & self ) -> Vec < ( String , String , String , bool , Option < String > ) > {
241243 self . processes . iter ( ) . map ( |( id, p) | {
242244 let running = p. exit_code . try_lock ( ) . map ( |e| e. is_none ( ) ) . unwrap_or ( true ) ;
243- ( id. clone ( ) , p. command . clone ( ) , p. started_at . to_rfc3339 ( ) , running)
245+ ( id. clone ( ) , p. command . clone ( ) , p. started_at . to_rfc3339 ( ) , running, p . revert_command . clone ( ) )
244246 } ) . collect ( )
245247 }
246248
247249 fn remove ( & mut self , id : & str ) -> bool {
248250 self . processes . remove ( id) . is_some ( )
249251 }
252+
253+ fn get_revert_command ( & self , id : & str ) -> Option < Option < String > > {
254+ self . processes . get ( id) . map ( |p| p. revert_command . clone ( ) )
255+ }
250256}
251257
252258lazy_static ! {
@@ -257,7 +263,7 @@ lazy_static! {
257263pub async fn local_process_counts ( ) -> ( usize , usize ) {
258264 let registry = REGISTRY . lock ( ) . await ;
259265 let list = registry. list ( ) ;
260- let running = list. iter ( ) . filter ( |( _, _, _, r) | * r) . count ( ) ;
266+ let running = list. iter ( ) . filter ( |( _, _, _, r, _ ) | * r) . count ( ) ;
261267 ( running, list. len ( ) - running)
262268}
263269
@@ -275,7 +281,11 @@ impl Tool for TerminalRunAsyncTool {
275281 "properties" : {
276282 "command" : { "type" : "string" , "description" : "Shell command to run" } ,
277283 "working_dir" : { "type" : "string" , "description" : "Working directory" } ,
278- "env" : { "type" : "object" , "description" : "Environment variables" }
284+ "env" : { "type" : "object" , "description" : "Environment variables" } ,
285+ "revert_command" : {
286+ "type" : [ "string" , "null" ] ,
287+ "description" : "Command to revert this command's changes. Empty string = no state change, null = cannot revert"
288+ }
279289 } ,
280290 "required" : [ "command" ]
281291 } )
@@ -288,6 +298,7 @@ impl Tool for TerminalRunAsyncTool {
288298 command : String ,
289299 working_dir : Option < String > ,
290300 env : Option < HashMap < String , String > > ,
301+ revert_command : Option < String > ,
291302 session_id : Option < String > ,
292303 }
293304
@@ -300,6 +311,7 @@ impl Tool for TerminalRunAsyncTool {
300311 let mut remote_args = serde_json:: json!( { "command" : args. command} ) ;
301312 if let Some ( ref dir) = args. working_dir { remote_args[ "working_dir" ] = serde_json:: json!( dir) ; }
302313 if let Some ( ref env) = args. env { remote_args[ "env" ] = serde_json:: json!( env) ; }
314+ if let Some ( ref revert) = args. revert_command { remote_args[ "revert_command" ] = serde_json:: json!( revert) ; }
303315 return match session:: call_tool_in_session ( sid, "terminal_run_async" , remote_args) . await {
304316 Ok ( result) => {
305317 // Persist handle → session_id so later commands auto-resolve
@@ -321,7 +333,7 @@ impl Tool for TerminalRunAsyncTool {
321333 }
322334
323335 let mut registry = REGISTRY . lock ( ) . await ;
324- match registry. start ( & args. command , args. working_dir . as_deref ( ) , args. env ) . await {
336+ match registry. start ( & args. command , args. working_dir . as_deref ( ) , args. env , args . revert_command ) . await {
325337 Ok ( id) => ToolResult :: ok ( serde_json:: json!( { "handle" : id} ) . to_string ( ) ) ,
326338 Err ( e) => ToolResult :: err ( e. to_string ( ) ) ,
327339 }
@@ -466,9 +478,14 @@ impl Tool for TerminalListTool {
466478 let list = registry. list ( ) ;
467479 if !list. is_empty ( ) {
468480 out. push_str ( "Local:\n " ) ;
469- for ( id, cmd, started, running) in list {
481+ for ( id, cmd, started, running, revert ) in list {
470482 let status = if running { "running" } else { "complete" } ;
471- out. push_str ( & format ! ( " {} [{}] {} ({})\n " , id, status, cmd, started) ) ;
483+ let revert_info = match & revert {
484+ Some ( s) if s. is_empty ( ) => " [no state change]" ,
485+ Some ( _) => " [revertible]" ,
486+ None => " [non-revertible]" ,
487+ } ;
488+ out. push_str ( & format ! ( " {} [{}]{} {} ({})\n " , id, status, revert_info, cmd, started) ) ;
472489 }
473490 }
474491 }
0 commit comments