@@ -34,6 +34,15 @@ import type {
3434 ModelConfigParams ,
3535 ModelConfigResult ,
3636 RequestUserInputRespondParams ,
37+ WorkspaceChangeCoverage ,
38+ WorkspaceChangeScope ,
39+ WorkspaceChangeSetStatus ,
40+ WorkspaceChangeStats ,
41+ WorkspaceChangeViewStatus ,
42+ WorkspaceChangesReadParams ,
43+ WorkspaceChangesReadResult ,
44+ WorkspaceChangesUpdatedPayload ,
45+ WorkspaceDiffDetail ,
3746} from "./generated/protocol"
3847import {
3948 ProtocolValidationError ,
@@ -122,6 +131,48 @@ export type ToolState = any
122131export type ToolStateCompleted = any
123132export type UserMessage = any
124133export type Worktree = any
134+ export type {
135+ WorkspaceChangeAttribution ,
136+ WorkspaceChangeBase ,
137+ WorkspaceChangeCoverage ,
138+ WorkspaceChangeScope ,
139+ WorkspaceChangeSetStatus ,
140+ WorkspaceChangeStats ,
141+ WorkspaceChangeView ,
142+ WorkspaceChangeViewStatus ,
143+ WorkspaceChangedFile ,
144+ WorkspaceChangedFileStatus ,
145+ WorkspaceChangesReadParams ,
146+ WorkspaceChangesReadResult ,
147+ WorkspaceChangesUpdatedPayload ,
148+ WorkspaceDiffDetail ,
149+ } from "./generated/protocol"
150+
151+ export type WorkspaceChangesReadOptions = {
152+ sessionID : string
153+ cwd ?: string
154+ scopes : WorkspaceChangeScope [ ]
155+ baseBranch ?: string
156+ turnID ?: string
157+ diffDetail ?: WorkspaceDiffDetail
158+ maxDiffBytes ?: number | bigint
159+ }
160+
161+ export type WorkspaceChangesUpdatedEventProperties = {
162+ sessionID : string
163+ turnID : string
164+ scope : WorkspaceChangeScope
165+ status : WorkspaceChangeViewStatus
166+ coverage : WorkspaceChangeCoverage
167+ changeSetStatus : WorkspaceChangeSetStatus
168+ stats : {
169+ filesChanged : number
170+ additions : number
171+ deletions : number
172+ }
173+ version : number
174+ generatedAt : string
175+ }
125176
126177interface GlobalEvent {
127178 directory : string
@@ -153,6 +204,71 @@ function sessionMeta(value: unknown): Record<string, unknown> | undefined {
153204 return objectRecord ( meta ?. [ "devo/session" ] )
154205}
155206
207+ function numberFromProtocol ( value : unknown ) : number {
208+ if ( typeof value === "number" && Number . isFinite ( value ) ) return value
209+ if ( typeof value === "bigint" ) return Number ( value )
210+ if ( typeof value === "string" ) {
211+ const parsed = Number ( value )
212+ if ( Number . isFinite ( parsed ) ) return parsed
213+ }
214+ return 0
215+ }
216+
217+ function workspaceChangeStats ( value : unknown ) : WorkspaceChangeStats {
218+ const stats = objectRecord ( value )
219+ return {
220+ files_changed : numberFromProtocol ( stats ?. files_changed ?? stats ?. filesChanged ) ,
221+ additions : numberFromProtocol ( stats ?. additions ) ,
222+ deletions : numberFromProtocol ( stats ?. deletions ) ,
223+ }
224+ }
225+
226+ function workspaceChangesUpdatedFromOriginalEvent (
227+ original : unknown ,
228+ ) : WorkspaceChangesUpdatedPayload | null {
229+ const event = objectRecord ( original )
230+ if ( ! event ) return null
231+ const payload =
232+ event . kind === "workspace_changes_updated"
233+ ? event
234+ : objectRecord ( event . WorkspaceChangesUpdated ) ??
235+ objectRecord ( event . workspace_changes_updated )
236+ if ( ! payload ) return null
237+ return {
238+ session_id : String ( payload . session_id ?? payload . sessionId ?? "" ) ,
239+ turn_id : String ( payload . turn_id ?? payload . turnId ?? "" ) ,
240+ scope : String ( payload . scope ?? "turn" ) as WorkspaceChangeScope ,
241+ status : String ( payload . status ?? "ready" ) as WorkspaceChangeViewStatus ,
242+ coverage : String ( payload . coverage ?? "none" ) as WorkspaceChangeCoverage ,
243+ change_set_status : String (
244+ payload . change_set_status ?? payload . changeSetStatus ?? "finalized" ,
245+ ) as WorkspaceChangeSetStatus ,
246+ stats : workspaceChangeStats ( payload . stats ) ,
247+ version : numberFromProtocol ( payload . version ) ,
248+ generated_at : String ( payload . generated_at ?? payload . generatedAt ?? "" ) ,
249+ }
250+ }
251+
252+ function workspaceChangesUpdatedEventProperties (
253+ payload : WorkspaceChangesUpdatedPayload ,
254+ ) : WorkspaceChangesUpdatedEventProperties {
255+ return {
256+ sessionID : payload . session_id ,
257+ turnID : payload . turn_id ,
258+ scope : payload . scope ,
259+ status : payload . status ,
260+ coverage : payload . coverage ,
261+ changeSetStatus : payload . change_set_status ,
262+ stats : {
263+ filesChanged : numberFromProtocol ( payload . stats . files_changed ) ,
264+ additions : numberFromProtocol ( payload . stats . additions ) ,
265+ deletions : numberFromProtocol ( payload . stats . deletions ) ,
266+ } ,
267+ version : numberFromProtocol ( payload . version ) ,
268+ generatedAt : payload . generated_at ,
269+ }
270+ }
271+
156272function parseTimestampMs ( value : unknown ) : number | undefined {
157273 if ( typeof value !== "string" ) return undefined
158274 const parsed = Date . parse ( value )
@@ -441,6 +557,29 @@ class AcpClient {
441557 } ,
442558 }
443559
560+ workspace = {
561+ changes : {
562+ read : async ( params : WorkspaceChangesReadOptions ) => {
563+ const wireParams : WorkspaceChangesReadParams = {
564+ session_id : params . sessionID ,
565+ scopes : params . scopes ,
566+ diff_detail : params . diffDetail ?? "summary" ,
567+ }
568+ if ( params . cwd !== undefined ) wireParams . cwd = params . cwd
569+ if ( params . baseBranch !== undefined ) wireParams . base_branch = params . baseBranch
570+ if ( params . turnID !== undefined ) wireParams . turn_id = params . turnID
571+ if ( params . maxDiffBytes !== undefined ) {
572+ wireParams . max_diff_bytes = Number ( params . maxDiffBytes )
573+ }
574+ const data = ( await this . request (
575+ "_devo/workspace/changes/read" ,
576+ wireParams ,
577+ ) ) as WorkspaceChangesReadResult
578+ return { data }
579+ } ,
580+ } ,
581+ }
582+
444583 command = {
445584 list : async ( ) => ( { data : [ { name : "compact" , description : "Compact the session" } ] } ) ,
446585 }
@@ -718,6 +857,21 @@ class AcpClient {
718857 this . handleSessionUpdate ( notification )
719858 return
720859 }
860+ if (
861+ event . type === "notification" &&
862+ ( event . method === "workspace/changes/updated" ||
863+ event . method === "_devo/workspace/changes/updated" ) &&
864+ event . params
865+ ) {
866+ const payload = this . validateTransportPayload < WorkspaceChangesUpdatedPayload > (
867+ event . method ,
868+ "incomingNotification" ,
869+ event . params ,
870+ )
871+ if ( ! payload ) return
872+ this . handleWorkspaceChangesUpdated ( payload )
873+ return
874+ }
721875 if ( event . type === "request" && event . id !== undefined && event . method ) {
722876 const params = this . validateTransportPayload ( event . method , "incomingRequest" , event . params )
723877 if ( ! params ) return
@@ -973,6 +1127,10 @@ class AcpClient {
9731127 const payload = ( original as { RequestUserInput : Record < string , unknown > } ) . RequestUserInput
9741128 this . handleRequestUserInput ( sessionId , directory , payload )
9751129 }
1130+ const workspaceChanges = workspaceChangesUpdatedFromOriginalEvent ( original )
1131+ if ( workspaceChanges ) {
1132+ this . handleWorkspaceChangesUpdated ( workspaceChanges , directory )
1133+ }
9761134 if ( "ServerRequestResolved" in original ) {
9771135 const payload = ( original as { ServerRequestResolved : Record < string , unknown > } )
9781136 . ServerRequestResolved
@@ -987,6 +1145,20 @@ class AcpClient {
9871145 }
9881146 }
9891147
1148+ private handleWorkspaceChangesUpdated (
1149+ payload : WorkspaceChangesUpdatedPayload ,
1150+ directory ?: string ,
1151+ ) : void {
1152+ const event = workspaceChangesUpdatedEventProperties ( payload )
1153+ if ( ! event . sessionID ) return
1154+ const emitDirectory =
1155+ directory ?? this . sessionDirectories . get ( event . sessionID ) ?? this . options . directory ?? defaultCwd ( )
1156+ this . emit ( emitDirectory , {
1157+ type : "workspace.changes.updated" ,
1158+ properties : event ,
1159+ } )
1160+ }
1161+
9901162 private handleRequestUserInput (
9911163 sessionId : string ,
9921164 directory : string ,
0 commit comments