@@ -29,6 +29,7 @@ export const useCommentsStore = defineStore('comments', () => {
2929
3030 const isDebugging = false ;
3131 const debounceTimers = { } ;
32+ const trackedChangeResolutionSnapshots = new WeakMap ( ) ;
3233
3334 const COMMENT_EVENTS = comments_module_events ;
3435 const hasInitializedComments = ref ( false ) ;
@@ -188,12 +189,26 @@ export const useCommentsStore = defineStore('comments', () => {
188189
189190 const clearResolvedMetadata = ( comment ) => {
190191 if ( ! comment ) return ;
192+ if (
193+ comment . resolvedTime !== undefined ||
194+ comment . resolvedByEmail !== undefined ||
195+ comment . resolvedByName !== undefined
196+ ) {
197+ trackedChangeResolutionSnapshots . set ( comment , {
198+ resolvedTime : comment . resolvedTime ?? null ,
199+ resolvedByEmail : comment . resolvedByEmail ?? null ,
200+ resolvedByName : comment . resolvedByName ?? null ,
201+ } ) ;
202+ }
191203 // Sets the resolved state to null so it can be restored in the comments sidebar
192204 comment . resolvedTime = null ;
193205 comment . resolvedByEmail = null ;
194206 comment . resolvedByName = null ;
195207 } ;
196208
209+ const getCommentEventPayload = ( comment ) =>
210+ typeof comment ?. getValues === 'function' ? comment . getValues ( ) : { ...comment } ;
211+
197212 /**
198213 * Check if a comment originated from the super-editor (or has no explicit source).
199214 * Comments without a source are assumed to be editor-backed for backward compatibility.
@@ -512,6 +527,10 @@ export const useCommentsStore = defineStore('comments', () => {
512527 if ( event === 'add' ) {
513528 const existing = findTrackedChangeById ( ) ;
514529 if ( existing ) {
530+ // Undo/redo after accept/reject can rematerialize a previously resolved
531+ // tracked change. Reopen the thread so the bubble is actionable again.
532+ if ( existing . resolvedTime ) clearResolvedMetadata ( existing ) ;
533+
515534 // Already exists (e.g. created during batch import) — update instead of duplicating
516535 // Partial resolution can turn a replacement into insert-only/delete-only, so
517536 // clear fields explicitly when the updated payload no longer includes them.
@@ -533,6 +552,7 @@ export const useCommentsStore = defineStore('comments', () => {
533552 // If we have an update event, simply update the composable comment
534553 const existingTrackedChange = findTrackedChangeById ( ) ;
535554 if ( ! existingTrackedChange ) return ;
555+ if ( existingTrackedChange . resolvedTime ) clearResolvedMetadata ( existingTrackedChange ) ;
536556
537557 // Partial resolution can turn a replacement into insert-only/delete-only, so
538558 // clear fields explicitly when the updated payload no longer includes them.
@@ -960,21 +980,31 @@ export const useCommentsStore = defineStore('comments', () => {
960980 } , 0 ) ;
961981 } ;
962982
963- const createCommentForTrackChanges = ( editor , superdoc , trackedChangesOverride = null ) => {
983+ const createCommentForTrackChanges = ( editor , superdoc , trackedChangesOverride = null , options = { } ) => {
984+ const { reopenResolved = false } = options ;
964985 const trackedChanges = trackedChangesOverride ?? trackChangesHelpers . getTrackChanges ( editor . state ) ;
965986 const groupedChanges = groupChanges ( trackedChanges ) ;
966987 const activeDocumentId = editor ?. options ?. documentId != null ? String ( editor . options . documentId ) : null ;
967988 if ( ! activeDocumentId ) return ;
968989
969- // Build a Set of existing tracked-change IDs for O(1) lookup.
990+ // Build a Set of existing unresolved tracked-change IDs for O(1) lookup.
970991 // Include both runtime and imported IDs to avoid duplicate threads when
971992 // replay/import flows remap commentId but marks still reference importedId.
972- const existingIds = new Set ( ) ;
993+ // History replay can opt in to excluding resolved tracked-change threads so
994+ // undo/redo reopens them when their marks reappear. Initial import rebuilds
995+ // keep resolved IDs in the set so resolved DOCX threads do not reopen on load.
996+ const skipIds = new Set ( ) ;
973997 commentsList . value . forEach ( ( comment ) => {
974998 if ( ! comment ?. trackedChange ) return ;
975999 if ( ! belongsToDocument ( comment , activeDocumentId ) ) return ;
976- if ( comment . commentId != null ) existingIds . add ( String ( comment . commentId ) ) ;
977- if ( comment . importedId != null ) existingIds . add ( String ( comment . importedId ) ) ;
1000+ if ( comment . resolvedTime && ! reopenResolved ) {
1001+ if ( comment . commentId != null ) skipIds . add ( String ( comment . commentId ) ) ;
1002+ if ( comment . importedId != null ) skipIds . add ( String ( comment . importedId ) ) ;
1003+ return ;
1004+ }
1005+ if ( comment . resolvedTime ) return ;
1006+ if ( comment . commentId != null ) skipIds . add ( String ( comment . commentId ) ) ;
1007+ if ( comment . importedId != null ) skipIds . add ( String ( comment . importedId ) ) ;
9781008 } ) ;
9791009
9801010 // Build a Map of change ID → tracked change entries for O(1) lookup per group.
@@ -991,7 +1021,7 @@ export const useCommentsStore = defineStore('comments', () => {
9911021 // Build comment params directly from grouped changes — no PM dispatch needed
9921022 groupedChanges . forEach ( ( { insertedMark, deletionMark, formatMark } ) => {
9931023 const id = insertedMark ?. mark . attrs . id || deletionMark ?. mark . attrs . id || formatMark ?. mark . attrs . id ;
994- if ( ! id || existingIds . has ( id ) ) return ;
1024+ if ( ! id || skipIds . has ( id ) ) return ;
9951025
9961026 const marks = {
9971027 ...( insertedMark && { insertedMark : insertedMark . mark } ) ,
@@ -1012,9 +1042,9 @@ export const useCommentsStore = defineStore('comments', () => {
10121042
10131043 if ( params ) {
10141044 handleTrackedChangeUpdate ( { superdoc, params } ) ;
1015- existingIds . add ( String ( id ) ) ;
1016- if ( params . changeId != null ) existingIds . add ( String ( params . changeId ) ) ;
1017- if ( params . importedId != null ) existingIds . add ( String ( params . importedId ) ) ;
1045+ skipIds . add ( String ( id ) ) ;
1046+ if ( params . changeId != null ) skipIds . add ( String ( params . changeId ) ) ;
1047+ if ( params . importedId != null ) skipIds . add ( String ( params . importedId ) ) ;
10181048 }
10191049 } ) ;
10201050
@@ -1062,10 +1092,11 @@ export const useCommentsStore = defineStore('comments', () => {
10621092 * @param {string | null } activeDocumentId Document currently being synced.
10631093 * @returns {void }
10641094 */
1065- const pruneStaleTrackedChangeComments = ( liveTrackedChangeIds , activeDocumentId ) => {
1095+ const pruneStaleTrackedChangeComments = ( liveTrackedChangeIds , activeDocumentId , superdoc = null ) => {
10661096 if ( ! ( liveTrackedChangeIds instanceof Set ) || ! activeDocumentId ) return ;
10671097
10681098 const removedIds = new Set ( ) ;
1099+ const restoredComments = [ ] ;
10691100 const previousComments = [ ...commentsList . value ] ;
10701101
10711102 commentsList . value = commentsList . value . filter ( ( comment ) => {
@@ -1078,12 +1109,32 @@ export const useCommentsStore = defineStore('comments', () => {
10781109 const hasLiveImportedId = Boolean ( importedId && liveTrackedChangeIds . has ( importedId ) ) ;
10791110
10801111 if ( ( ! commentId && ! importedId ) || hasLiveCommentId || hasLiveImportedId ) return true ;
1112+ if ( comment . resolvedTime ) return true ;
1113+
1114+ const resolutionSnapshot = trackedChangeResolutionSnapshots . get ( comment ) ;
1115+ if ( resolutionSnapshot ) {
1116+ comment . resolvedTime = resolutionSnapshot . resolvedTime ?? Date . now ( ) ;
1117+ comment . resolvedByEmail = resolutionSnapshot . resolvedByEmail ?? null ;
1118+ comment . resolvedByName = resolutionSnapshot . resolvedByName ?? null ;
1119+ restoredComments . push ( comment ) ;
1120+ return true ;
1121+ }
10811122
10821123 if ( commentId ) removedIds . add ( commentId ) ;
10831124 if ( importedId ) removedIds . add ( importedId ) ;
10841125 return false ;
10851126 } ) ;
10861127
1128+ restoredComments . forEach ( ( comment ) => {
1129+ const payload = getCommentEventPayload ( comment ) ;
1130+ const event = {
1131+ type : COMMENT_EVENTS . UPDATE ,
1132+ comment : payload ,
1133+ } ;
1134+ syncCommentsToClients ( superdoc , event ) ;
1135+ superdoc ?. emit ?. ( 'comments-update' , event ) ;
1136+ } ) ;
1137+
10871138 if ( ! removedIds . size ) return ;
10881139
10891140 let didRemoveDescendants = true ;
@@ -1110,6 +1161,24 @@ export const useCommentsStore = defineStore('comments', () => {
11101161 } ) ;
11111162 }
11121163
1164+ const removedComments = previousComments . filter ( ( comment ) => {
1165+ if ( ! belongsToDocument ( comment , activeDocumentId ) ) return false ;
1166+ const commentId = comment . commentId != null ? String ( comment . commentId ) : null ;
1167+ const importedId = comment . importedId != null ? String ( comment . importedId ) : null ;
1168+ return ( commentId && removedIds . has ( commentId ) ) || ( importedId && removedIds . has ( importedId ) ) ;
1169+ } ) ;
1170+
1171+ removedComments . forEach ( ( comment ) => {
1172+ const payload = getCommentEventPayload ( comment ) ;
1173+ const event = {
1174+ type : COMMENT_EVENTS . DELETED ,
1175+ comment : payload ,
1176+ changes : [ { key : 'deleted' , commentId : payload . commentId , fileId : payload . fileId } ] ,
1177+ } ;
1178+ syncCommentsToClients ( superdoc , event ) ;
1179+ superdoc ?. emit ?. ( 'comments-update' , event ) ;
1180+ } ) ;
1181+
11131182 const activeCommentId = activeComment . value != null ? String ( activeComment . value ) : null ;
11141183 const activeCommentBelongsToActiveDocument = previousComments . some ( ( comment ) => {
11151184 const commentId = comment . commentId != null ? String ( comment . commentId ) : null ;
@@ -1148,8 +1217,8 @@ export const useCommentsStore = defineStore('comments', () => {
11481217 liveTrackedChangeIds . add ( String ( id ) ) ;
11491218 } ) ;
11501219
1151- pruneStaleTrackedChangeComments ( liveTrackedChangeIds , activeDocumentId ) ;
1152- createCommentForTrackChanges ( editor , superdoc , trackedChanges ) ;
1220+ pruneStaleTrackedChangeComments ( liveTrackedChangeIds , activeDocumentId , superdoc ) ;
1221+ createCommentForTrackChanges ( editor , superdoc , trackedChanges , { reopenResolved : true } ) ;
11531222 } ;
11541223
11551224 const normalizeDocxSchemaForExport = ( value ) => {
0 commit comments