@@ -41,7 +41,7 @@ import { Config } from "@/config/config"
4141import { Todo } from "@/session/todo"
4242import { z } from "zod"
4343import { LoadAPIKeyError } from "ai"
44- import type { AssistantMessage , Event , OpencodeClient , SessionMessageResponse } from "@opencode-ai/sdk/v2"
44+ import type { AssistantMessage , Event , OpencodeClient , SessionMessageResponse , ToolPart } from "@opencode-ai/sdk/v2"
4545import { applyPatch } from "diff"
4646
4747type ModeOption = { id : string ; name : string ; description ?: string }
@@ -135,6 +135,8 @@ export namespace ACP {
135135 private sessionManager : ACPSessionManager
136136 private eventAbort = new AbortController ( )
137137 private eventStarted = false
138+ private bashSnapshots = new Map < string , string > ( )
139+ private toolStarts = new Set < string > ( )
138140 private permissionQueues = new Map < string , Promise < void > > ( )
139141 private permissionOptions : PermissionOption [ ] = [
140142 { optionId : "once" , kind : "allow_once" , name : "Allow once" } ,
@@ -266,47 +268,68 @@ export namespace ACP {
266268 const session = this . sessionManager . tryGet ( part . sessionID )
267269 if ( ! session ) return
268270 const sessionId = session . id
269- const directory = session . cwd
270-
271- const message = await this . sdk . session
272- . message (
273- {
274- sessionID : part . sessionID ,
275- messageID : part . messageID ,
276- directory,
277- } ,
278- { throwOnError : true } ,
279- )
280- . then ( ( x ) => x . data )
281- . catch ( ( error ) => {
282- log . error ( "unexpected error when fetching message" , { error } )
283- return undefined
284- } )
285-
286- if ( ! message || message . info . role !== "assistant" ) return
287271
288272 if ( part . type === "tool" ) {
273+ if ( ! this . toolStarts . has ( part . callID ) ) {
274+ this . toolStarts . add ( part . callID )
275+ await this . connection
276+ . sessionUpdate ( {
277+ sessionId,
278+ update : {
279+ sessionUpdate : "tool_call" ,
280+ toolCallId : part . callID ,
281+ title : part . tool ,
282+ kind : toToolKind ( part . tool ) ,
283+ status : "pending" ,
284+ locations : [ ] ,
285+ rawInput : { } ,
286+ } ,
287+ } )
288+ . catch ( ( error ) => {
289+ log . error ( "failed to send tool pending to ACP" , { error } )
290+ } )
291+ }
292+
289293 switch ( part . state . status ) {
290294 case "pending" :
291- await this . connection
292- . sessionUpdate ( {
293- sessionId,
294- update : {
295- sessionUpdate : "tool_call" ,
296- toolCallId : part . callID ,
297- title : part . tool ,
298- kind : toToolKind ( part . tool ) ,
299- status : "pending" ,
300- locations : [ ] ,
301- rawInput : { } ,
302- } ,
303- } )
304- . catch ( ( error ) => {
305- log . error ( "failed to send tool pending to ACP" , { error } )
306- } )
295+ this . bashSnapshots . delete ( part . callID )
307296 return
308297
309298 case "running" :
299+ const output = this . bashOutput ( part )
300+ const content : ToolCallContent [ ] = [ ]
301+ if ( output ) {
302+ const hash = String ( Bun . hash ( output ) )
303+ if ( part . tool === "bash" ) {
304+ if ( this . bashSnapshots . get ( part . callID ) === hash ) {
305+ await this . connection
306+ . sessionUpdate ( {
307+ sessionId,
308+ update : {
309+ sessionUpdate : "tool_call_update" ,
310+ toolCallId : part . callID ,
311+ status : "in_progress" ,
312+ kind : toToolKind ( part . tool ) ,
313+ title : part . tool ,
314+ locations : toLocations ( part . tool , part . state . input ) ,
315+ rawInput : part . state . input ,
316+ } ,
317+ } )
318+ . catch ( ( error ) => {
319+ log . error ( "failed to send tool in_progress to ACP" , { error } )
320+ } )
321+ return
322+ }
323+ this . bashSnapshots . set ( part . callID , hash )
324+ }
325+ content . push ( {
326+ type : "content" ,
327+ content : {
328+ type : "text" ,
329+ text : output ,
330+ } ,
331+ } )
332+ }
310333 await this . connection
311334 . sessionUpdate ( {
312335 sessionId,
@@ -318,6 +341,7 @@ export namespace ACP {
318341 title : part . tool ,
319342 locations : toLocations ( part . tool , part . state . input ) ,
320343 rawInput : part . state . input ,
344+ ...( content . length > 0 && { content } ) ,
321345 } ,
322346 } )
323347 . catch ( ( error ) => {
@@ -326,6 +350,8 @@ export namespace ACP {
326350 return
327351
328352 case "completed" : {
353+ this . toolStarts . delete ( part . callID )
354+ this . bashSnapshots . delete ( part . callID )
329355 const kind = toToolKind ( part . tool )
330356 const content : ToolCallContent [ ] = [
331357 {
@@ -405,6 +431,8 @@ export namespace ACP {
405431 return
406432 }
407433 case "error" :
434+ this . toolStarts . delete ( part . callID )
435+ this . bashSnapshots . delete ( part . callID )
408436 await this . connection
409437 . sessionUpdate ( {
410438 sessionId,
@@ -426,6 +454,7 @@ export namespace ACP {
426454 ] ,
427455 rawOutput : {
428456 error : part . state . error ,
457+ metadata : part . state . metadata ,
429458 } ,
430459 } ,
431460 } )
@@ -802,6 +831,7 @@ export namespace ACP {
802831 if ( part . type === "tool" ) {
803832 switch ( part . state . status ) {
804833 case "pending" :
834+ this . bashSnapshots . delete ( part . callID )
805835 await this . connection
806836 . sessionUpdate ( {
807837 sessionId,
@@ -820,6 +850,17 @@ export namespace ACP {
820850 } )
821851 break
822852 case "running" :
853+ const output = this . bashOutput ( part )
854+ const runningContent : ToolCallContent [ ] = [ ]
855+ if ( output ) {
856+ runningContent . push ( {
857+ type : "content" ,
858+ content : {
859+ type : "text" ,
860+ text : output ,
861+ } ,
862+ } )
863+ }
823864 await this . connection
824865 . sessionUpdate ( {
825866 sessionId,
@@ -831,13 +872,15 @@ export namespace ACP {
831872 title : part . tool ,
832873 locations : toLocations ( part . tool , part . state . input ) ,
833874 rawInput : part . state . input ,
875+ ...( runningContent . length > 0 && { content : runningContent } ) ,
834876 } ,
835877 } )
836878 . catch ( ( err ) => {
837879 log . error ( "failed to send tool in_progress to ACP" , { error : err } )
838880 } )
839881 break
840882 case "completed" :
883+ this . bashSnapshots . delete ( part . callID )
841884 const kind = toToolKind ( part . tool )
842885 const content : ToolCallContent [ ] = [
843886 {
@@ -916,6 +959,7 @@ export namespace ACP {
916959 } )
917960 break
918961 case "error" :
962+ this . bashSnapshots . delete ( part . callID )
919963 await this . connection
920964 . sessionUpdate ( {
921965 sessionId,
@@ -937,6 +981,7 @@ export namespace ACP {
937981 ] ,
938982 rawOutput : {
939983 error : part . state . error ,
984+ metadata : part . state . metadata ,
940985 } ,
941986 } ,
942987 } )
@@ -1063,6 +1108,14 @@ export namespace ACP {
10631108 }
10641109 }
10651110
1111+ private bashOutput ( part : ToolPart ) {
1112+ if ( part . tool !== "bash" ) return
1113+ if ( ! ( "metadata" in part . state ) || ! part . state . metadata || typeof part . state . metadata !== "object" ) return
1114+ const output = part . state . metadata [ "output" ]
1115+ if ( typeof output !== "string" ) return
1116+ return output
1117+ }
1118+
10661119 private async loadAvailableModes ( directory : string ) : Promise < ModeOption [ ] > {
10671120 const agents = await this . config . sdk . app
10681121 . agents (
0 commit comments