@@ -47,6 +47,11 @@ async function debugLog(client, message, extra) {
4747}
4848
4949async function sendToBeacon ( client , payload ) {
50+ const testSender = globalThis [ Symbol . for ( "beacon.opencode.testSender" ) ]
51+ if ( typeof testSender === "function" ) {
52+ await testSender ( payload )
53+ return
54+ }
5055 let proc
5156 try {
5257 proc = Bun . spawn ( [ "/bin/sh" , "-lc" , beaconCommand ] , {
@@ -87,6 +92,7 @@ function sessionID(value) {
8792}
8893
8994function modelName ( value ) {
95+ if ( value ?. providerID && value ?. modelID ) return value . providerID + "/" + value . modelID
9096 const model = value ?. model || value ?. modelInfo
9197 if ( ! model || typeof model === "string" ) return model || ""
9298 if ( model . providerID && model . modelID ) return model . providerID + "/" + model . modelID
@@ -118,6 +124,9 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
118124 const pendingParts = new Map ( )
119125 const partDeltas = new Map ( )
120126 const messageModels = new Map ( )
127+ const completedMessages = new Set ( )
128+ const permissionRequests = new Map ( )
129+ const sessionStates = new Map ( )
121130 const recentFilePaths = new Map ( )
122131 let eventQueue = Promise . resolve ( )
123132
@@ -128,10 +137,10 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
128137 . catch ( ( err ) => debugLog ( client , "Beacon event queue failed" , { error : String ( err ) , type : value ?. type } ) )
129138 return eventQueue
130139 }
131- const rememberFilePath = ( tool , args ) => {
140+ const rememberFilePath = ( tool , args , sid , callID ) => {
132141 if ( ! isFileMutationTool ( tool ) ) return
133142 const path = filePath ( args )
134- if ( path ) recentFilePaths . set ( path , Date . now ( ) )
143+ if ( path ) recentFilePaths . set ( path , { time : Date . now ( ) , sessionID : sid , callID } )
135144 }
136145 const partKey = ( part ) => `${ part ?. sessionID || "" } :${ part ?. messageID || "" } :${ part ?. id || "" } `
137146 const emitPart = ( part , sid , model ) => {
@@ -208,7 +217,7 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
208217 "tool.execute.after" : async ( input , output ) => {
209218 const active = activeCalls . get ( input ?. callID )
210219 const args = input ?. args || active ?. args || { }
211- rememberFilePath ( input ?. tool , args )
220+ rememberFilePath ( input ?. tool , args , sessionID ( input ) , input ?. callID )
212221 await sendToBeacon (
213222 client ,
214223 payload ( "tool.execute.after" , {
@@ -235,6 +244,10 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
235244 const model = modelName ( info )
236245 if ( info ?. id && model ) messageModels . set ( info . id , model )
237246 if ( ! info ?. time ?. completed && ! info ?. error ) return
247+ if ( info ?. id ) {
248+ if ( completedMessages . has ( info . id ) ) return
249+ completedMessages . add ( info . id )
250+ }
238251 }
239252 if ( type === "message.part.delta" ) {
240253 const key = `${ sid } :${ properties ?. messageID || "" } :${ properties ?. partID || "" } `
@@ -246,27 +259,51 @@ export const BeaconEndpointPlugin = async ({ project, directory, worktree, clien
246259 emitPart ( part , sid , messageModels . get ( part ?. messageID ) || "" )
247260 return
248261 }
249- if ( type === "session.status" && properties ?. status ?. type === "idle" ) flushParts ( sid )
250- if ( type === "session.idle" ) flushParts ( sid )
262+ if ( type === "session.status" ) {
263+ const status = properties ?. status ?. type || ""
264+ sessionStates . set ( sid , status )
265+ if ( status === "idle" ) flushParts ( sid )
266+ }
267+ if ( type === "session.idle" ) {
268+ flushParts ( sid )
269+ if ( sessionStates . get ( sid ) === "idle" ) return
270+ sessionStates . set ( sid , "idle" )
271+ }
272+ if ( type === "permission.asked" || type === "permission.v2.asked" ) {
273+ if ( properties ?. id ) {
274+ permissionRequests . set ( properties . id , { tool : properties ?. tool , permission : properties ?. permission } )
275+ }
276+ }
277+ if ( type === "permission.replied" || type === "permission.v2.replied" ) {
278+ const request = permissionRequests . get ( properties ?. requestID )
279+ if ( request ?. tool && ! properties . tool ) properties . tool = request . tool
280+ if ( request ?. permission && ! properties . permission ) properties . permission = request . permission
281+ permissionRequests . delete ( properties ?. requestID )
282+ }
251283 if ( type === "session.diff" ) {
252284 const diffs = Array . isArray ( properties ?. diff ) ? properties . diff : [ ]
253285 const now = Date . now ( )
254286 const filtered = diffs . filter ( ( item ) => {
255287 const path = filePath ( item )
256288 if ( ! path ) return false
257289 const seen = recentFilePaths . get ( path )
258- if ( seen && now - seen < 5000 ) return false
290+ if ( seen && now - seen . time < 5000 ) return false
259291 return item ?. before !== item ?. after || item ?. additions || item ?. deletions
260292 } )
261293 if ( filtered . length === 0 ) return
262294 properties . diff = filtered
263295 }
264296 if ( type === "file.edited" || type === "file.watcher.updated" ) {
265- if ( ! sid ) return
297+ if ( ! sid ) {
298+ const seen = recentFilePaths . get ( filePath ( properties ) )
299+ if ( ! seen || Date . now ( ) - seen . time >= 5000 ) return
300+ properties . sessionID = seen . sessionID
301+ properties . callID = seen . callID
302+ }
266303 }
267304 void enqueue (
268305 payload ( type , {
269- session_id : sid ,
306+ session_id : sessionID ( properties ) || sid ,
270307 model : modelName ( info || properties ) ,
271308 properties,
272309 event,
0 commit comments