@@ -7,6 +7,8 @@ import type { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
7
7
import type { ExecutedWsMessage } from '@/types/apiTypes'
8
8
import { useExecutionStore } from '@/stores/executionStore'
9
9
import _ from 'lodash'
10
+ import * as jsondiffpatch from 'jsondiffpatch'
11
+ import log from 'loglevel'
10
12
11
13
function clone ( obj : any ) {
12
14
try {
@@ -20,6 +22,10 @@ function clone(obj: any) {
20
22
return JSON . parse ( JSON . stringify ( obj ) )
21
23
}
22
24
25
+ const logger = log . getLogger ( 'ChangeTracker' )
26
+ // Change to debug for more verbose logging
27
+ logger . setLevel ( 'info' )
28
+
23
29
export class ChangeTracker {
24
30
static MAX_HISTORY = 50
25
31
/**
@@ -29,6 +35,10 @@ export class ChangeTracker {
29
35
undoQueue : ComfyWorkflowJSON [ ] = [ ]
30
36
redoQueue : ComfyWorkflowJSON [ ] = [ ]
31
37
changeCount : number = 0
38
+ /**
39
+ * Whether the redo/undo restoring is in progress.
40
+ */
41
+ private restoringState : boolean = false
32
42
33
43
ds ?: { scale : number ; offset : [ number , number ] }
34
44
nodeOutputs ?: Record < string , any >
@@ -55,8 +65,12 @@ export class ChangeTracker {
55
65
* Save the current state as the initial state.
56
66
*/
57
67
reset ( state ?: ComfyWorkflowJSON ) {
68
+ // Do not reset the state if we are restoring.
69
+ if ( this . restoringState ) return
70
+
71
+ logger . debug ( 'Reset State' )
58
72
this . activeState = state ?? this . activeState
59
- this . initialState = this . activeState
73
+ this . initialState = clone ( this . activeState )
60
74
}
61
75
62
76
store ( ) {
@@ -85,6 +99,13 @@ export class ChangeTracker {
85
99
this . initialState ,
86
100
this . activeState
87
101
)
102
+ if ( workflow . isModified ) {
103
+ const diff = ChangeTracker . graphDiff (
104
+ this . initialState ,
105
+ this . activeState
106
+ )
107
+ logger . debug ( 'Graph diff:' , diff )
108
+ }
88
109
}
89
110
}
90
111
@@ -101,6 +122,8 @@ export class ChangeTracker {
101
122
if ( this . undoQueue . length > ChangeTracker . MAX_HISTORY ) {
102
123
this . undoQueue . shift ( )
103
124
}
125
+ logger . debug ( 'Diff detected. Undo queue length:' , this . undoQueue . length )
126
+
104
127
this . activeState = clone ( currentState )
105
128
this . redoQueue . length = 0
106
129
api . dispatchEvent (
@@ -114,21 +137,38 @@ export class ChangeTracker {
114
137
const prevState = source . pop ( )
115
138
if ( prevState ) {
116
139
target . push ( this . activeState ! )
117
- await this . app . loadGraphData ( prevState , false , false , this . workflow , {
118
- showMissingModelsDialog : false ,
119
- showMissingNodesDialog : false
120
- } )
121
- this . activeState = prevState
122
- this . updateModified ( )
140
+ this . restoringState = true
141
+ try {
142
+ await this . app . loadGraphData ( prevState , false , false , this . workflow , {
143
+ showMissingModelsDialog : false ,
144
+ showMissingNodesDialog : false
145
+ } )
146
+ this . activeState = prevState
147
+ this . updateModified ( )
148
+ } finally {
149
+ this . restoringState = false
150
+ }
123
151
}
124
152
}
125
153
126
154
async undo ( ) {
127
155
await this . updateState ( this . undoQueue , this . redoQueue )
156
+ logger . debug (
157
+ 'Undo. Undo queue length:' ,
158
+ this . undoQueue . length ,
159
+ 'Redo queue length:' ,
160
+ this . redoQueue . length
161
+ )
128
162
}
129
163
130
164
async redo ( ) {
131
165
await this . updateState ( this . redoQueue , this . undoQueue )
166
+ logger . debug (
167
+ 'Redo. Undo queue length:' ,
168
+ this . undoQueue . length ,
169
+ 'Redo queue length:' ,
170
+ this . redoQueue . length
171
+ )
132
172
}
133
173
134
174
async undoRedo ( e : KeyboardEvent ) {
@@ -198,6 +238,7 @@ export class ChangeTracker {
198
238
199
239
// If our active element is some type of input then handle changes after they're done
200
240
if ( ChangeTracker . bindInput ( app , bindInputEl ) ) return
241
+ logger . debug ( 'checkState on keydown' )
201
242
changeTracker . checkState ( )
202
243
} )
203
244
} ,
@@ -207,34 +248,40 @@ export class ChangeTracker {
207
248
window . addEventListener ( 'keyup' , ( e ) => {
208
249
if ( keyIgnored ) {
209
250
keyIgnored = false
251
+ logger . debug ( 'checkState on keyup' )
210
252
checkState ( )
211
253
}
212
254
} )
213
255
214
256
// Handle clicking DOM elements (e.g. widgets)
215
257
window . addEventListener ( 'mouseup' , ( ) => {
258
+ logger . debug ( 'checkState on mouseup' )
216
259
checkState ( )
217
260
} )
218
261
219
262
// Handle prompt queue event for dynamic widget changes
220
263
api . addEventListener ( 'promptQueued' , ( ) => {
264
+ logger . debug ( 'checkState on promptQueued' )
221
265
checkState ( )
222
266
} )
223
267
224
268
api . addEventListener ( 'graphCleared' , ( ) => {
269
+ logger . debug ( 'checkState on graphCleared' )
225
270
checkState ( )
226
271
} )
227
272
228
273
// Handle litegraph clicks
229
274
const processMouseUp = LGraphCanvas . prototype . processMouseUp
230
275
LGraphCanvas . prototype . processMouseUp = function ( e ) {
231
276
const v = processMouseUp . apply ( this , [ e ] )
277
+ logger . debug ( 'checkState on processMouseUp' )
232
278
checkState ( )
233
279
return v
234
280
}
235
281
const processMouseDown = LGraphCanvas . prototype . processMouseDown
236
282
LGraphCanvas . prototype . processMouseDown = function ( e ) {
237
283
const v = processMouseDown . apply ( this , [ e ] )
284
+ logger . debug ( 'checkState on processMouseDown' )
238
285
checkState ( )
239
286
return v
240
287
}
@@ -251,13 +298,15 @@ export class ChangeTracker {
251
298
callback ( v )
252
299
checkState ( )
253
300
}
301
+ logger . debug ( 'checkState on prompt' )
254
302
return prompt . apply ( this , [ title , value , extendedCallback , event ] )
255
303
}
256
304
257
305
// Handle litegraph context menu for COMBO widgets
258
306
const close = LiteGraph . ContextMenu . prototype . close
259
307
LiteGraph . ContextMenu . prototype . close = function ( e : MouseEvent ) {
260
308
const v = close . apply ( this , [ e ] )
309
+ logger . debug ( 'checkState on contextMenuClose' )
261
310
checkState ( )
262
311
return v
263
312
}
@@ -267,6 +316,7 @@ export class ChangeTracker {
267
316
LiteGraph . LGraph . prototype . onNodeAdded = function ( node : LGraphNode ) {
268
317
const v = onNodeAdded ?. apply ( this , [ node ] )
269
318
if ( ! app ?. configuringGraph ) {
319
+ logger . debug ( 'checkState on onNodeAdded' )
270
320
checkState ( )
271
321
}
272
322
return v
@@ -357,4 +407,20 @@ export class ChangeTracker {
357
407
358
408
return false
359
409
}
410
+
411
+ static graphDiff ( a : ComfyWorkflowJSON , b : ComfyWorkflowJSON ) {
412
+ function sortGraphNodes ( graph : ComfyWorkflowJSON ) {
413
+ return {
414
+ links : graph . links ,
415
+ groups : graph . groups ,
416
+ nodes : graph . nodes . sort ( ( a , b ) => {
417
+ if ( typeof a . id === 'number' && typeof b . id === 'number' ) {
418
+ return a . id - b . id
419
+ }
420
+ return 0
421
+ } )
422
+ }
423
+ }
424
+ return jsondiffpatch . diff ( sortGraphNodes ( a ) , sortGraphNodes ( b ) )
425
+ }
360
426
}
0 commit comments