wordinweb 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/collab.js CHANGED
@@ -1,21 +1,26 @@
1
1
  import {
2
+ ADDRESS_WIRE_FIELD,
2
3
  DocxDocument,
3
4
  DocxToolbar,
4
5
  DocxView,
6
+ REGISTERED_OPERATION_KINDS,
5
7
  acceptAllRevisions,
6
8
  acceptRevision,
7
9
  addComment,
8
10
  adjustIndent,
9
11
  applyDeleteRange,
12
+ applyDeleteSeparator,
13
+ applyInsertSeparator,
10
14
  applyInsertText,
15
+ applyRegisteredOperation,
11
16
  applyRunFormat,
12
17
  applySplitParagraph,
13
18
  applyTableOp,
14
- checkboxStateElement,
15
19
  collectRevisions,
16
20
  deleteComment,
17
21
  deleteMath,
18
22
  deleteSuggestedRange,
23
+ editCommentText,
19
24
  insertBlankPageAt,
20
25
  insertBookmarkAroundSelection,
21
26
  insertBookmarkAt,
@@ -32,9 +37,11 @@ import {
32
37
  insertSectionBreak,
33
38
  insertShapeAt,
34
39
  insertSmartArtAt,
35
- insertTableAfter,
36
40
  insertWordArtAt,
41
+ isInsertableFieldInstruction,
42
+ isRegisteredOperationKind,
37
43
  isSafeUrl,
44
+ isValidShapePreset,
38
45
  localName,
39
46
  markParagraphGlyph,
40
47
  mergeParagraphBackward,
@@ -42,6 +49,7 @@ import {
42
49
  moveTableTo,
43
50
  parseXml,
44
51
  recordedProvenance,
52
+ registeredOperation,
45
53
  rejectAllRevisions,
46
54
  rejectRevision,
47
55
  removeDrawingRun,
@@ -49,15 +57,17 @@ import {
49
57
  replyToComment,
50
58
  resizeDrawing,
51
59
  resizeTableColumn,
52
- resizeTableRow,
53
60
  resolveRunOffset,
54
61
  runWireLength,
62
+ separatorAtWireOffset,
55
63
  serializeXml,
56
64
  setChartData,
65
+ setCommentResolved,
57
66
  setDrawingFill,
58
67
  setDrawingLineStyle,
59
68
  setDrawingOrder,
60
69
  setDrawingRotation,
70
+ setDrawingWordArtStyle,
61
71
  setDrawingWordArtText,
62
72
  setDropCapAt,
63
73
  setFloatingPagePosition,
@@ -66,7 +76,6 @@ import {
66
76
  setLineNumbering,
67
77
  setLink,
68
78
  setListLevel,
69
- setListType,
70
79
  setMathLinear,
71
80
  setPageLayout,
72
81
  setParagraphAlignment,
@@ -77,24 +86,109 @@ import {
77
86
  setSmartArtFill,
78
87
  setSmartArtNodeText,
79
88
  setSmartArtTextFormat,
80
- toggleCheckbox,
81
- validatePastedOoxml
82
- } from "./chunk-ALLQ5KWP.js";
89
+ siblingParagraph,
90
+ suggestMeta,
91
+ validatePastedOoxml,
92
+ validateRegisteredOperation
93
+ } from "./chunk-VL6YPZSD.js";
83
94
  import "./chunk-7D4SUZUM.js";
84
95
 
85
96
  // src/collab.tsx
86
97
  import { useEffect, useMemo, useRef, useState, createElement } from "react";
87
98
 
88
99
  // ../collab/src/intents.ts
100
+ function isRegisteredIntent(intent) {
101
+ return isRegisteredOperationKind(intent.kind);
102
+ }
103
+ var HAND_WRITTEN_INTENT_KIND_MAP = {
104
+ insertText: true,
105
+ insertSeparator: true,
106
+ deleteSeparator: true,
107
+ deleteText: true,
108
+ splitParagraph: true,
109
+ formatRun: true,
110
+ formatParagraph: true,
111
+ formatRange: true,
112
+ tableOp: true,
113
+ mergeParagraph: true,
114
+ commentRun: true,
115
+ pasteBlocks: true,
116
+ insertImage: true,
117
+ suggestRevision: true,
118
+ insertBreak: true,
119
+ insertMath: true,
120
+ insertShape: true,
121
+ replyComment: true,
122
+ adjustIndent: true,
123
+ setSpacing: true,
124
+ insertPageField: true,
125
+ setLink: true,
126
+ insertFootnote: true,
127
+ setDropCap: true,
128
+ setDivider: true,
129
+ insertBookmark: true,
130
+ insertBlankPage: true,
131
+ insertSectionBreak: true,
132
+ insertCrossRef: true,
133
+ insertCoverPage: true,
134
+ setPageLayout: true,
135
+ setListLevel: true,
136
+ insertWordArt: true,
137
+ insertChart: true,
138
+ insertSmartArt: true,
139
+ setLineNumbering: true,
140
+ insertDateTimeField: true,
141
+ insertField: true,
142
+ setDrawingRotation: true,
143
+ setDrawingFill: true,
144
+ setChartData: true,
145
+ setSmartArtNodeText: true,
146
+ setDrawingWordArtText: true,
147
+ setDrawingWordArtStyle: true,
148
+ setDrawingLineStyle: true,
149
+ setImageAltText: true,
150
+ removeLink: true,
151
+ setImageWrap: true,
152
+ setDrawingOrder: true,
153
+ setSmartArtData: true,
154
+ setSmartArtFill: true,
155
+ setSmartArtTextFormat: true,
156
+ setFloatingPagePosition: true,
157
+ resizeDrawing: true,
158
+ resizeTableColumn: true,
159
+ moveTable: true,
160
+ removeDrawing: true,
161
+ setMathLinear: true,
162
+ deleteMath: true,
163
+ moveMath: true,
164
+ ensureHeaderFooter: true,
165
+ deleteComment: true,
166
+ resolveComment: true,
167
+ editComment: true,
168
+ insertBookmarkRange: true,
169
+ acceptRevision: true,
170
+ rejectRevision: true,
171
+ acceptAllRevisions: true,
172
+ rejectAllRevisions: true
173
+ };
174
+ var INTENT_KINDS = Object.freeze([
175
+ ...Object.keys(HAND_WRITTEN_INTENT_KIND_MAP),
176
+ ...REGISTERED_OPERATION_KINDS
177
+ ]);
89
178
  function idempotencyKey(i) {
90
179
  return `${i.clientId}:${i.clientSeq}`;
91
180
  }
92
181
 
93
182
  // ../collab/src/transform.ts
94
183
  function runEditsOf(intent) {
184
+ if (isRegisteredIntent(intent)) return [];
95
185
  switch (intent.kind) {
96
186
  case "insertText":
97
187
  return [{ runId: intent.at.runId, at: intent.at.offset, del: 0, ins: intent.text.length }];
188
+ case "insertSeparator":
189
+ return [{ runId: intent.at.runId, at: intent.at.offset, del: 0, ins: 1 }];
190
+ case "deleteSeparator":
191
+ return intent.suggest ? [] : [{ runId: intent.at.runId, at: intent.at.offset, del: 1, ins: 0 }];
98
192
  case "deleteText":
99
193
  return [{ runId: intent.runId, at: intent.start, del: intent.end - intent.start, ins: 0 }];
100
194
  case "splitParagraph":
@@ -103,8 +197,6 @@ function runEditsOf(intent) {
103
197
  return [];
104
198
  case "formatParagraph":
105
199
  return [];
106
- case "setListType":
107
- return [];
108
200
  case "formatRange":
109
201
  return [{
110
202
  runId: intent.runId,
@@ -159,6 +251,7 @@ function runEditsOf(intent) {
159
251
  case "setChartData":
160
252
  case "setSmartArtNodeText":
161
253
  case "setDrawingWordArtText":
254
+ case "setDrawingWordArtStyle":
162
255
  case "setDrawingLineStyle":
163
256
  case "setImageAltText":
164
257
  case "removeLink":
@@ -170,19 +263,18 @@ function runEditsOf(intent) {
170
263
  case "setFloatingPagePosition":
171
264
  case "resizeDrawing":
172
265
  case "resizeTableColumn":
173
- case "resizeTableRow":
174
266
  case "moveTable":
175
267
  case "removeDrawing":
176
268
  case "setMathLinear":
177
269
  case "deleteMath":
178
270
  case "ensureHeaderFooter":
179
271
  case "deleteComment":
272
+ case "resolveComment":
273
+ case "editComment":
180
274
  case "insertBookmarkRange":
181
- case "toggleCheckbox":
182
275
  return [];
183
276
  case "moveMath":
184
277
  return [];
185
- case "insertTable":
186
278
  case "acceptRevision":
187
279
  case "rejectRevision":
188
280
  case "acceptAllRevisions":
@@ -234,17 +326,20 @@ function transformPosition(pos, ahead, leftGravity = false) {
234
326
  }
235
327
  function transformIntent(intent, ahead) {
236
328
  const newBase = intent.base + ahead.length;
329
+ if (isRegisteredIntent(intent)) return { ...intent, base: newBase };
237
330
  switch (intent.kind) {
238
331
  case "insertText":
239
332
  return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
333
+ case "insertSeparator":
334
+ return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
335
+ case "deleteSeparator":
336
+ return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
240
337
  case "splitParagraph":
241
338
  return { ...intent, at: transformPosition(intent.at, ahead), base: newBase };
242
339
  case "formatRun":
243
340
  return { ...intent, base: newBase };
244
341
  case "formatParagraph":
245
342
  return { ...intent, base: newBase };
246
- case "setListType":
247
- return { ...intent, base: newBase };
248
343
  case "formatRange": {
249
344
  const s = transformPosition({ blockId: intent.blockId, runId: intent.runId, offset: intent.start }, ahead);
250
345
  const e = transformPosition({ blockId: intent.blockId, runId: intent.runId, offset: intent.end }, ahead);
@@ -305,6 +400,7 @@ function transformIntent(intent, ahead) {
305
400
  case "setChartData":
306
401
  case "setSmartArtNodeText":
307
402
  case "setDrawingWordArtText":
403
+ case "setDrawingWordArtStyle":
308
404
  case "setDrawingLineStyle":
309
405
  case "setImageAltText":
310
406
  case "removeLink":
@@ -316,16 +412,15 @@ function transformIntent(intent, ahead) {
316
412
  case "setFloatingPagePosition":
317
413
  case "resizeDrawing":
318
414
  case "resizeTableColumn":
319
- case "resizeTableRow":
320
415
  case "moveTable":
321
416
  case "removeDrawing":
322
417
  case "setMathLinear":
323
418
  case "deleteMath":
324
419
  case "ensureHeaderFooter":
325
420
  case "deleteComment":
421
+ case "resolveComment":
422
+ case "editComment":
326
423
  case "insertBookmarkRange":
327
- case "toggleCheckbox":
328
- case "insertTable":
329
424
  case "acceptRevision":
330
425
  case "rejectRevision":
331
426
  case "acceptAllRevisions":
@@ -387,6 +482,7 @@ function applyIntentInner(doc, ids, intent, out) {
387
482
  const ctx = { suggesting: false, revMeta: () => {
388
483
  throw new Error("suggesting mode unsupported headlessly");
389
484
  } };
485
+ if (isRegisteredIntent(intent)) return applyRegistered(doc, ids, runOf, intent);
390
486
  switch (intent.kind) {
391
487
  case "insertText": {
392
488
  const caret = resolveCaret(ids, runOf, intent.at);
@@ -404,6 +500,43 @@ function applyIntentInner(doc, ids, intent, out) {
404
500
  out.scope = blockScope(ids, intent.at.blockId, intent.at.runId);
405
501
  return true;
406
502
  }
503
+ case "insertSeparator": {
504
+ const caret = resolveCaret(ids, runOf, intent.at);
505
+ if (!caret) return false;
506
+ if (intent.suggest) {
507
+ const s = intent.suggest;
508
+ const suggestCtx = { suggesting: true, revMeta: () => ({ author: s.author, date: s.date, nextId: () => doc.nextRevisionId() }) };
509
+ const scope = blockScope(ids, intent.at.blockId, intent.at.runId);
510
+ if (!applyInsertSeparator(doc, caret, intent.separator, suggestCtx)) return false;
511
+ if (doc.stableIds) resyncScope(doc, ids, scope);
512
+ out.scope = scope;
513
+ return true;
514
+ }
515
+ if (!applyInsertSeparator(doc, caret, intent.separator, ctx)) return false;
516
+ out.scope = blockScope(ids, intent.at.blockId, intent.at.runId);
517
+ return true;
518
+ }
519
+ case "deleteSeparator": {
520
+ const runEl = ids.elOf(intent.at.runId);
521
+ if (!runEl) return false;
522
+ const sepEl = separatorAtWireOffset(runEl, intent.at.offset);
523
+ if (!sepEl) return false;
524
+ if (intent.suggest) {
525
+ const s = intent.suggest;
526
+ const suggestCtx = { suggesting: true, revMeta: () => ({ author: s.author, date: s.date, nextId: () => doc.nextRevisionId() }) };
527
+ const scope2 = blockScope(ids, intent.at.blockId, intent.at.runId);
528
+ if (!applyDeleteSeparator(doc, sepEl, suggestCtx)) return false;
529
+ if (doc.stableIds) resyncScope(doc, ids, scope2);
530
+ out.scope = scope2;
531
+ return true;
532
+ }
533
+ const scope = blockScope(ids, intent.at.blockId, intent.at.runId);
534
+ const res = applyDeleteSeparator(doc, sepEl, ctx);
535
+ if (!res) return false;
536
+ if (res.removedRun) ids.prune(doc.editableRoots());
537
+ out.scope = scope;
538
+ return true;
539
+ }
407
540
  case "deleteText": {
408
541
  if (intent.end <= intent.start) return false;
409
542
  const caret = resolveCaret(ids, runOf, { blockId: intent.blockId, runId: intent.runId, offset: intent.start });
@@ -439,24 +572,19 @@ function applyIntentInner(doc, ids, intent, out) {
439
572
  const entry = runOf(runEl);
440
573
  if (!entry) return false;
441
574
  const seg = { run: entry.run, t: null, start: 0, end: 0, props: entry.run.props };
442
- applyRunFormat(doc, [seg], intent.patch);
575
+ applyRunFormat(doc, [seg], intent.patch, suggestMeta(doc, intent.suggest));
443
576
  return true;
444
577
  }
445
578
  case "formatParagraph": {
446
579
  const blockEl = ids.elOf(intent.blockId);
447
580
  if (!blockEl) return false;
448
581
  const target = firstTextDescendant(blockEl) ?? blockEl;
582
+ const meta = suggestMeta(doc, intent.suggest);
449
583
  let changed = false;
450
- if (intent.align) changed = setParagraphAlignment(doc, [target], intent.align) || changed;
451
- if (intent.styleId !== void 0) changed = setParagraphStyle(doc, [target], intent.styleId) || changed;
584
+ if (intent.align) changed = setParagraphAlignment(doc, [target], intent.align, meta) || changed;
585
+ if (intent.styleId !== void 0) changed = setParagraphStyle(doc, [target], intent.styleId, meta) || changed;
452
586
  return changed;
453
587
  }
454
- case "setListType": {
455
- const blockEl = ids.elOf(intent.blockId);
456
- if (!blockEl) return false;
457
- const target = firstTextDescendant(blockEl) ?? blockEl;
458
- return setListType(doc, [target], intent.listKind);
459
- }
460
588
  case "formatRange": {
461
589
  if (intent.end <= intent.start) return false;
462
590
  const runEl = ids.elOf(intent.runId);
@@ -470,7 +598,7 @@ function applyIntentInner(doc, ids, intent, out) {
470
598
  const localEnd = hit.offset + (intent.end - intent.start);
471
599
  if (localEnd > hit.t.text.length) return false;
472
600
  const seg = { run: entry.run, t: hit.t, start: hit.offset, end: localEnd, props: entry.run.props };
473
- const formatted = applyRunFormat(doc, [seg], intent.patch);
601
+ const formatted = applyRunFormat(doc, [seg], intent.patch, suggestMeta(doc, intent.suggest));
474
602
  if (formatted.length === 0) return false;
475
603
  const middleT = formatted[0].t;
476
604
  const middleRun = doc.findParentOf(middleT);
@@ -494,10 +622,11 @@ function applyIntentInner(doc, ids, intent, out) {
494
622
  if (!paraEl) return false;
495
623
  const target = firstTextDescendant(paraEl) ?? paraEl;
496
624
  const isInsert = intent.op === "rowAbove" || intent.op === "rowBelow" || intent.op === "colLeft" || intent.op === "colRight";
497
- const before = isInsert ? trackedSet(ids, doc) : null;
498
- const ok = applyTableOp(doc, target, intent.op);
625
+ const createsNodes = isInsert || intent.op === "mergeDown" || intent.op === "splitCell";
626
+ const before = createsNodes ? trackedSet(ids, doc) : null;
627
+ const ok = applyTableOp(doc, target, intent.op, suggestMeta(doc, intent.suggest));
499
628
  if (!ok) return false;
500
- if (isInsert && before && intent.nodeIds) {
629
+ if (before && intent.nodeIds) {
501
630
  const fresh = [];
502
631
  walkTracked(doc, (el) => {
503
632
  if (!before.has(el)) fresh.push(el);
@@ -505,7 +634,8 @@ function applyIntentInner(doc, ids, intent, out) {
505
634
  for (let k = 0; k < fresh.length && k < intent.nodeIds.length; k++) {
506
635
  ids.reassign(fresh[k], intent.nodeIds[k]);
507
636
  }
508
- } else {
637
+ }
638
+ if (!isInsert) {
509
639
  ids.prune(doc.editableRoots());
510
640
  }
511
641
  return true;
@@ -513,6 +643,14 @@ function applyIntentInner(doc, ids, intent, out) {
513
643
  case "mergeParagraph": {
514
644
  const pEl = ids.elOf(intent.blockId);
515
645
  if (!pEl) return false;
646
+ const meta = suggestMeta(doc, intent.suggest);
647
+ if (meta) {
648
+ const previous = siblingParagraph(doc, pEl, -1);
649
+ if (!previous) return false;
650
+ markParagraphGlyph(previous, "del", meta);
651
+ doc.refresh();
652
+ return true;
653
+ }
516
654
  const ok = mergeParagraphBackward(doc, pEl);
517
655
  if (ok) ids.prune(doc.editableRoots());
518
656
  return ok;
@@ -599,7 +737,7 @@ function applyIntentInner(doc, ids, intent, out) {
599
737
  if (!runEl) return false;
600
738
  const entry = runOf(runEl);
601
739
  if (!entry || !entry.firstT) return false;
602
- const relId = doc.registerPendingImage(intent.blobSha, intent.ext, { iv: intent.iv });
740
+ const relId = doc.registerPendingImage(intent.blobSha, intent.ext, { iv: intent.iv }, runEl);
603
741
  const before = trackedSet(ids, doc);
604
742
  const newRun = insertImageAt(doc, entry.firstT, relId, intent.widthPx, intent.heightPx);
605
743
  if (!newRun) return false;
@@ -663,7 +801,7 @@ function applyIntentInner(doc, ids, intent, out) {
663
801
  const entry = runOf(runEl);
664
802
  if (!entry || !entry.firstT) return false;
665
803
  const before = trackedSet(ids, doc);
666
- const drawing = insertWordArtAt(doc, entry.firstT, intent.text, intent.preset);
804
+ const drawing = insertWordArtAt(doc, entry.firstT, intent.text, intent.preset, intent.style);
667
805
  if (!drawing) return false;
668
806
  assignFreshTracked(ids, doc, before, intent.nodeIds);
669
807
  return true;
@@ -715,49 +853,56 @@ function applyIntentInner(doc, ids, intent, out) {
715
853
  case "setDrawingRotation": {
716
854
  const runEl = ids.elOf(intent.runId);
717
855
  if (!runEl) return false;
718
- const drawing = firstDrawingIn(runEl);
856
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
719
857
  if (!drawing) return false;
720
858
  return setDrawingRotation(doc, drawing, intent.degrees);
721
859
  }
722
860
  case "setDrawingFill": {
723
861
  const runEl = ids.elOf(intent.runId);
724
862
  if (!runEl) return false;
725
- const drawing = firstDrawingIn(runEl);
863
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
726
864
  if (!drawing) return false;
727
865
  return setDrawingFill(doc, drawing, intent.color);
728
866
  }
729
867
  case "setChartData": {
730
868
  const runEl = ids.elOf(intent.runId);
731
869
  if (!runEl) return false;
732
- const drawing = firstDrawingIn(runEl);
870
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
733
871
  if (!drawing) return false;
734
872
  return setChartData(doc, drawing, intent.chart);
735
873
  }
736
874
  case "setSmartArtNodeText": {
737
875
  const runEl = ids.elOf(intent.runId);
738
876
  if (!runEl) return false;
739
- const drawing = firstDrawingIn(runEl);
877
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
740
878
  if (!drawing) return false;
741
879
  return setSmartArtNodeText(doc, drawing, intent.index, intent.text);
742
880
  }
743
881
  case "setDrawingWordArtText": {
744
882
  const runEl = ids.elOf(intent.runId);
745
883
  if (!runEl) return false;
746
- const drawing = firstDrawingIn(runEl);
884
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
747
885
  if (!drawing) return false;
748
886
  return setDrawingWordArtText(doc, drawing, intent.text);
749
887
  }
888
+ case "setDrawingWordArtStyle": {
889
+ const runEl = ids.elOf(intent.runId);
890
+ if (!runEl) return false;
891
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
892
+ if (!drawing) return false;
893
+ return setDrawingWordArtStyle(doc, drawing, intent.color, intent.opacity);
894
+ }
750
895
  case "setDrawingLineStyle": {
751
896
  const runEl = ids.elOf(intent.runId);
752
897
  if (!runEl) return false;
753
- const drawing = firstDrawingIn(runEl);
898
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
754
899
  if (!drawing) return false;
755
900
  return setDrawingLineStyle(doc, drawing, intent.color, intent.widthPx, intent.dash);
756
901
  }
757
902
  case "setImageAltText": {
758
903
  const runEl = ids.elOf(intent.runId);
759
904
  if (!runEl) return false;
760
- const drawing = firstDrawingIn(runEl);
905
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
761
906
  if (!drawing) return false;
762
907
  return setImageAltText(doc, drawing, intent.alt);
763
908
  }
@@ -771,49 +916,49 @@ function applyIntentInner(doc, ids, intent, out) {
771
916
  case "setImageWrap": {
772
917
  const runEl = ids.elOf(intent.runId);
773
918
  if (!runEl) return false;
774
- const drawing = firstDrawingIn(runEl);
919
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
775
920
  if (!drawing) return false;
776
921
  return setImageWrap(doc, drawing, intent.mode);
777
922
  }
778
923
  case "setDrawingOrder": {
779
924
  const runEl = ids.elOf(intent.runId);
780
925
  if (!runEl) return false;
781
- const drawing = firstDrawingIn(runEl);
926
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
782
927
  if (!drawing) return false;
783
928
  return setDrawingOrder(doc, drawing, intent.order);
784
929
  }
785
930
  case "setSmartArtData": {
786
931
  const runEl = ids.elOf(intent.runId);
787
932
  if (!runEl) return false;
788
- const drawing = firstDrawingIn(runEl);
933
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
789
934
  if (!drawing) return false;
790
935
  return setSmartArtData(doc, drawing, intent.smartArt);
791
936
  }
792
937
  case "setSmartArtFill": {
793
938
  const runEl = ids.elOf(intent.runId);
794
939
  if (!runEl) return false;
795
- const drawing = firstDrawingIn(runEl);
940
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
796
941
  if (!drawing) return false;
797
942
  return setSmartArtFill(doc, drawing, intent.color, intent.nodeIndex);
798
943
  }
799
944
  case "setSmartArtTextFormat": {
800
945
  const runEl = ids.elOf(intent.runId);
801
946
  if (!runEl) return false;
802
- const drawing = firstDrawingIn(runEl);
947
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
803
948
  if (!drawing) return false;
804
949
  return setSmartArtTextFormat(doc, drawing, intent.format, intent.nodeIndex);
805
950
  }
806
951
  case "setFloatingPagePosition": {
807
952
  const runEl = ids.elOf(intent.runId);
808
953
  if (!runEl) return false;
809
- const drawing = firstDrawingIn(runEl);
954
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
810
955
  if (!drawing) return false;
811
956
  return setFloatingPagePosition(doc, drawing, intent.xPx, intent.yPx);
812
957
  }
813
958
  case "resizeDrawing": {
814
959
  const runEl = ids.elOf(intent.runId);
815
960
  if (!runEl) return false;
816
- const drawing = firstDrawingIn(runEl);
961
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
817
962
  if (!drawing) return false;
818
963
  return resizeDrawing(doc, drawing, intent.widthPx, intent.heightPx);
819
964
  }
@@ -822,11 +967,6 @@ function applyIntentInner(doc, ids, intent, out) {
822
967
  if (!tbl) return false;
823
968
  return resizeTableColumn(doc, tbl, intent.boundary, intent.deltaPx, intent.renderedWidths);
824
969
  }
825
- case "resizeTableRow": {
826
- const tbl = tableOfParagraph(doc, ids, intent.cellParagraphId);
827
- if (!tbl) return false;
828
- return resizeTableRow(doc, tbl, intent.rowIdx, intent.heightPx);
829
- }
830
970
  case "moveTable": {
831
971
  const tbl = tableOfParagraph(doc, ids, intent.cellParagraphId);
832
972
  if (!tbl) return false;
@@ -835,7 +975,7 @@ function applyIntentInner(doc, ids, intent, out) {
835
975
  case "removeDrawing": {
836
976
  const runEl = ids.elOf(intent.runId);
837
977
  if (!runEl) return false;
838
- const drawing = firstDrawingIn(runEl);
978
+ const drawing = drawingIn(runEl, intent.objectIndex, runOf);
839
979
  if (!drawing) return false;
840
980
  const ok = removeDrawingRun(doc, drawing);
841
981
  if (ok) ids.prune(doc.editableRoots());
@@ -844,21 +984,21 @@ function applyIntentInner(doc, ids, intent, out) {
844
984
  case "setMathLinear": {
845
985
  const blockEl = ids.elOf(intent.blockId);
846
986
  if (!blockEl) return false;
847
- const math = firstMathIn(blockEl);
987
+ const math = mathIn(blockEl, intent.mathIndex);
848
988
  if (!math) return false;
849
989
  return setMathLinear(doc, math, intent.mathText);
850
990
  }
851
991
  case "deleteMath": {
852
992
  const blockEl = ids.elOf(intent.blockId);
853
993
  if (!blockEl) return false;
854
- const math = firstMathIn(blockEl);
994
+ const math = mathIn(blockEl, intent.mathIndex);
855
995
  if (!math) return false;
856
996
  return deleteMath(doc, math);
857
997
  }
858
998
  case "moveMath": {
859
999
  const blockEl = ids.elOf(intent.blockId);
860
1000
  if (!blockEl) return false;
861
- const math = firstMathIn(blockEl);
1001
+ const math = mathIn(blockEl, intent.mathIndex);
862
1002
  if (!math) return false;
863
1003
  const dest = resolveCaret(ids, runOf, intent.at);
864
1004
  if (!dest) return false;
@@ -884,6 +1024,10 @@ function applyIntentInner(doc, ids, intent, out) {
884
1024
  }
885
1025
  case "deleteComment":
886
1026
  return deleteComment(doc, intent.commentId);
1027
+ case "resolveComment":
1028
+ return setCommentResolved(doc, intent.commentId, intent.resolved, recordedProvenance({ paraIds: [intent.paraId] }));
1029
+ case "editComment":
1030
+ return editCommentText(doc, intent.commentId, intent.text);
887
1031
  case "insertBookmarkRange": {
888
1032
  const runEl = ids.elOf(intent.runId);
889
1033
  if (!runEl) return false;
@@ -897,16 +1041,6 @@ function applyIntentInner(doc, ids, intent, out) {
897
1041
  const seg = { run: entry.run, t: hit.t, start: hit.offset, end: localEnd, props: entry.run.props };
898
1042
  return insertBookmarkAroundSelection(doc, [seg], intent.name);
899
1043
  }
900
- case "toggleCheckbox": {
901
- const runEl = ids.elOf(intent.runId);
902
- if (!runEl) return false;
903
- const entry = runOf(runEl);
904
- if (!entry) return false;
905
- const cbEl = checkboxStateElement(entry.run, entry.firstT);
906
- if (!cbEl) return false;
907
- toggleCheckbox(doc, cbEl);
908
- return true;
909
- }
910
1044
  case "acceptRevision": {
911
1045
  const refs = collectRevisions(doc);
912
1046
  if (intent.index >= refs.length) return false;
@@ -918,19 +1052,9 @@ function applyIntentInner(doc, ids, intent, out) {
918
1052
  return rejectRevision(doc, refs[intent.index]);
919
1053
  }
920
1054
  case "acceptAllRevisions":
921
- return acceptAllRevisions(doc) > 0;
1055
+ return acceptAllRevisions(doc, intent.author) > 0;
922
1056
  case "rejectAllRevisions":
923
- return rejectAllRevisions(doc) > 0;
924
- case "insertTable": {
925
- const runEl = ids.elOf(intent.runId);
926
- if (!runEl) return false;
927
- const entry = runOf(runEl);
928
- if (!entry || !entry.firstT) return false;
929
- const before = trackedSet(ids, doc);
930
- const ok = insertTableAfter(doc, entry.firstT, intent.rows, intent.cols);
931
- if (ok) assignFreshTracked(ids, doc, before, intent.nodeIds);
932
- return ok;
933
- }
1057
+ return rejectAllRevisions(doc, intent.author) > 0;
934
1058
  case "setLink": {
935
1059
  if (!isSafeUrl(intent.url)) return false;
936
1060
  const runEl = ids.elOf(intent.runId);
@@ -980,13 +1104,13 @@ function applyIntentInner(doc, ids, intent, out) {
980
1104
  const blockEl = ids.elOf(intent.blockId);
981
1105
  if (!blockEl) return false;
982
1106
  const target = firstTextDescendant(blockEl) ?? blockEl;
983
- return adjustIndent(doc, [target], intent.direction);
1107
+ return adjustIndent(doc, [target], intent.direction, suggestMeta(doc, intent.suggest));
984
1108
  }
985
1109
  case "setSpacing": {
986
1110
  const blockEl = ids.elOf(intent.blockId);
987
1111
  if (!blockEl) return false;
988
1112
  const target = firstTextDescendant(blockEl) ?? blockEl;
989
- return setParagraphSpacing(doc, [target], intent.patch);
1113
+ return setParagraphSpacing(doc, [target], intent.patch, suggestMeta(doc, intent.suggest));
990
1114
  }
991
1115
  case "insertPageField": {
992
1116
  const runEl = ids.elOf(intent.runId);
@@ -1044,14 +1168,65 @@ function applyIntentInner(doc, ids, intent, out) {
1044
1168
  assignFreshTracked(ids, doc, before, intent.nodeIds);
1045
1169
  return true;
1046
1170
  }
1171
+ default: {
1172
+ const exhaustive = intent;
1173
+ return exhaustive;
1174
+ }
1047
1175
  }
1048
1176
  }
1177
+ function resolveOperationTarget(doc, ids, runOf, address, intent) {
1178
+ const empty = { t: null, run: null, cellParagraph: null, drawing: null };
1179
+ if (address === "document") return { ...empty, el: doc.docRoot };
1180
+ const addressId = intent[ADDRESS_WIRE_FIELD[address]];
1181
+ switch (address) {
1182
+ case "run": {
1183
+ const el = ids.elOf(addressId);
1184
+ if (!el) return null;
1185
+ const entry = runOf(el);
1186
+ if (!entry) return null;
1187
+ return { ...empty, el, t: entry.firstT ?? null, run: entry.run };
1188
+ }
1189
+ case "block": {
1190
+ const el = ids.elOf(addressId);
1191
+ if (!el) return null;
1192
+ return { ...empty, el, t: firstTextDescendant(el) };
1193
+ }
1194
+ case "cell": {
1195
+ const paraEl = ids.elOf(addressId) ?? null;
1196
+ const tbl = paraEl ? tableOf(doc, paraEl) : null;
1197
+ return tbl ? { ...empty, el: tbl, cellParagraph: paraEl } : null;
1198
+ }
1199
+ case "object": {
1200
+ const el = ids.elOf(addressId);
1201
+ if (!el) return null;
1202
+ const objectIndex = intent.objectIndex;
1203
+ const drawing = drawingIn(el, objectIndex, runOf);
1204
+ return drawing ? { ...empty, el, drawing } : null;
1205
+ }
1206
+ }
1207
+ }
1208
+ function applyRegistered(doc, ids, runOf, intent) {
1209
+ const definition = registeredOperation(intent.kind);
1210
+ if (!definition) return false;
1211
+ const target = resolveOperationTarget(doc, ids, runOf, definition.address, intent);
1212
+ if (!target) return false;
1213
+ const nodeIds = "nodeIds" in intent ? intent.nodeIds : void 0;
1214
+ const before = nodeIds ? trackedSet(ids, doc) : null;
1215
+ if (!applyRegisteredOperation(doc, target, intent)) return false;
1216
+ if (definition.prunesIds) ids.prune(doc.editableRoots());
1217
+ if (before && nodeIds) assignFreshTracked(ids, doc, before, nodeIds);
1218
+ return true;
1219
+ }
1049
1220
  function assignFreshTracked(ids, doc, before, nodeIds) {
1050
1221
  const fresh = [];
1051
1222
  walkTracked(doc, (el) => {
1052
1223
  if (!before.has(el)) fresh.push(el);
1053
1224
  });
1054
- for (let k = 0; k < fresh.length && k < nodeIds.length; k++) ids.reassign(fresh[k], nodeIds[k]);
1225
+ for (const el of fresh) ids.unassign(el);
1226
+ for (let k = 0; k < fresh.length; k++) {
1227
+ if (k < nodeIds.length) ids.reassign(fresh[k], nodeIds[k]);
1228
+ else ids.assign(fresh[k]);
1229
+ }
1055
1230
  }
1056
1231
  function localRun(el) {
1057
1232
  const n = el.name;
@@ -1080,27 +1255,41 @@ function firstTextDescendant(el) {
1080
1255
  }
1081
1256
  function tableOfParagraph(doc, ids, cellParagraphId) {
1082
1257
  const paraEl = ids.elOf(cellParagraphId);
1083
- if (!paraEl) return null;
1084
- for (let cur = paraEl; cur; cur = doc.findParentOf(cur) ?? null) {
1258
+ return paraEl ? tableOf(doc, paraEl) : null;
1259
+ }
1260
+ function tableOf(doc, el) {
1261
+ for (let cur = el; cur; cur = doc.findParentOf(cur) ?? null) {
1085
1262
  if (localName(cur.name) === "tbl") return cur;
1086
1263
  }
1087
1264
  return null;
1088
1265
  }
1089
1266
  function firstDrawingIn(el) {
1090
- if (["drawing", "shape", "rect"].includes(localName(el.name))) return el;
1267
+ if (["drawing", "shape", "rect", "line"].includes(localName(el.name))) return el;
1091
1268
  for (const c of el.children) {
1092
1269
  const found = firstDrawingIn(c);
1093
1270
  if (found) return found;
1094
1271
  }
1095
1272
  return null;
1096
1273
  }
1097
- function firstMathIn(el) {
1098
- if (localName(el.name) === "oMath") return el;
1099
- for (const c of el.children) {
1100
- const found = firstMathIn(c);
1101
- if (found) return found;
1102
- }
1103
- return null;
1274
+ function drawingSource(content) {
1275
+ if (content.kind === "image" || content.kind === "drawing") return content.srcDrawing;
1276
+ if (content.kind !== "anchor") return void 0;
1277
+ const shape = content.shape;
1278
+ if (shape.type === "wordart") return shape.src;
1279
+ if (shape.type === "line") return shape.src;
1280
+ return shape.srcDrawing;
1281
+ }
1282
+ function drawingIn(runEl, objectIndex, runOf) {
1283
+ if (objectIndex === void 0) return firstDrawingIn(runEl);
1284
+ const content = runOf(runEl)?.run.content[objectIndex];
1285
+ return content ? drawingSource(content) ?? null : null;
1286
+ }
1287
+ function mathsIn(el) {
1288
+ if (localName(el.name) === "oMath") return [el];
1289
+ return el.children.flatMap(mathsIn);
1290
+ }
1291
+ function mathIn(el, index = 0) {
1292
+ return mathsIn(el)[index] ?? null;
1104
1293
  }
1105
1294
  function runEntry(run) {
1106
1295
  return { run, firstT: run.content.find((c) => c.kind === "text")?.srcT };
@@ -1629,6 +1818,10 @@ function toSuggestions(tail, author, date) {
1629
1818
  for (const intent of tail) {
1630
1819
  if (intent.kind === "insertText") {
1631
1820
  suggestions.push({ kind: "insertText", at: intent.at, text: intent.text, suggest: { author, date } });
1821
+ } else if (intent.kind === "insertSeparator") {
1822
+ suggestions.push({ kind: "insertSeparator", at: intent.at, separator: intent.separator, suggest: { author, date } });
1823
+ } else if (intent.kind === "deleteSeparator") {
1824
+ suggestions.push({ kind: "deleteSeparator", at: intent.at, suggest: { author, date } });
1632
1825
  } else if (intent.kind === "deleteText") {
1633
1826
  suggestions.push({
1634
1827
  kind: "suggestRevision",
@@ -1656,7 +1849,7 @@ function arrivalMode(tailLength, diverged, opts = {}) {
1656
1849
  }
1657
1850
 
1658
1851
  // ../collab/src/protocol.ts
1659
- var ENGINE_VERSION = "e11";
1852
+ var ENGINE_VERSION = "e19";
1660
1853
  var PRESENCE_MAX_RANGES = 64;
1661
1854
  function sanitizePresencePosition(pos) {
1662
1855
  if (!pos || !pos.ranges) return pos;
@@ -1672,7 +1865,7 @@ function sanitizePresencePosition(pos) {
1672
1865
  if (clean.length === pos.ranges.length) return pos;
1673
1866
  return clean.length > 0 ? { ...pos, ranges: clean } : { ...pos, ranges: void 0 };
1674
1867
  }
1675
- var PROTOCOL_VERSION = 5;
1868
+ var PROTOCOL_VERSION = 6;
1676
1869
 
1677
1870
  // ../collab/src/id-allocator.ts
1678
1871
  var CarriedIdAllocator = class {
@@ -2223,9 +2416,19 @@ var _CollabConnection = class _CollabConnection {
2223
2416
  sinceSeq: 0,
2224
2417
  profile: opts?.profile,
2225
2418
  codeProof: opts?.codeProof,
2226
- ownerToken: opts?.ownerToken
2419
+ ownerToken: opts?.ownerToken,
2420
+ agentInvite: opts?.agentInvite
2227
2421
  });
2228
2422
  }
2423
+ registerAgentInvite(inviteId, token, expiresAt) {
2424
+ this.transport.send({ t: "agent-invite", inviteId, token, expiresAt });
2425
+ }
2426
+ revokeAgentInvite(inviteId) {
2427
+ this.transport.send({ t: "agent-invite-revoke", inviteId });
2428
+ }
2429
+ sendAgentChat(agentClientId, messageId, iv, ciphertext) {
2430
+ this.transport.send({ t: "agent-chat", agentClientId, messageId, iv, ciphertext });
2431
+ }
2229
2432
  /** Owner admin op (doc 14 §2.5): honored only if this connection proved
2230
2433
  * the owner token at hello — otherwise the server refuses `not-owner`. */
2231
2434
  admin(action) {
@@ -2491,6 +2694,22 @@ var _CollabConnection = class _CollabConnection {
2491
2694
  this.cb.onRoster?.(msg.roster);
2492
2695
  return;
2493
2696
  }
2697
+ case "agent-invite-registered": {
2698
+ this.cb.onAgentInviteRegistered?.({ inviteId: msg.inviteId, expiresAt: msg.expiresAt });
2699
+ return;
2700
+ }
2701
+ case "agent-invite-refused": {
2702
+ this.cb.onAgentInviteRefused?.({ inviteId: msg.inviteId, reason: msg.reason });
2703
+ return;
2704
+ }
2705
+ case "agent-connected": {
2706
+ this.cb.onAgentConnected?.(msg);
2707
+ return;
2708
+ }
2709
+ case "agent-chat": {
2710
+ this.cb.onAgentChat?.(msg);
2711
+ return;
2712
+ }
2494
2713
  case "session-warning": {
2495
2714
  this.cb.onSessionWarning?.({ reason: msg.reason, inMs: msg.inMs });
2496
2715
  return;
@@ -2591,7 +2810,12 @@ function invertIntent(doc, ids, intent) {
2591
2810
  }
2592
2811
  // Formatting / table / comment / merge inverses need prior-state capture
2593
2812
  // (was-bold?, the merged paragraph's structure, …) — a documented
2594
- // extension; not undoable yet.
2813
+ // extension; not undoable yet. That includes pasteBlocks: a rich paste in
2814
+ // a ROOM is not undoable (its inverse — remove N spliced blocks plus
2815
+ // re-merge the split — has no wire form yet), so Cmd+Z after a room paste
2816
+ // stops at it as a cannot-undo marker (see submitUndoNow). The
2817
+ // single-process session is unaffected: it replays local history
2818
+ // (EditorHost.onLocalHistory) and undoes a paste as one step.
2595
2819
  default:
2596
2820
  return null;
2597
2821
  }
@@ -2607,7 +2831,8 @@ var DEFAULT_INTENT_LIMITS = {
2607
2831
  };
2608
2832
  function chartError(c, who) {
2609
2833
  if (typeof c !== "object" || c === null) return `${who}: bad chart`;
2610
- if (!["column", "bar", "line", "pie"].includes(c.type)) return `${who}: bad type`;
2834
+ if (!["column", "bar", "line", "pie", "doughnut", "area", "scatter"].includes(c.type)) return `${who}: bad type`;
2835
+ if (c.grouping !== void 0 && !["clustered", "stacked", "percentStacked"].includes(c.grouping)) return `${who}: bad grouping`;
2611
2836
  if (!Array.isArray(c.categories) || c.categories.length === 0 || c.categories.length > 100) return `${who}: bad categories`;
2612
2837
  if (c.categories.some((x) => typeof x !== "string" || x.length > 200)) return `${who}: bad category`;
2613
2838
  if (c.title !== void 0 && (typeof c.title !== "string" || c.title.length > 200)) return `${who}: bad title`;
@@ -2625,8 +2850,49 @@ function smartArtError(a, who) {
2625
2850
  if (a.items.some((x) => typeof x !== "string" || x.length > 500)) return `${who}: bad item`;
2626
2851
  return null;
2627
2852
  }
2853
+ function badCharacterStyle(patch, who) {
2854
+ const id = patch?.characterStyleId;
2855
+ if (id === void 0 || id === null) return null;
2856
+ return typeof id === "string" && /^[A-Za-z0-9\-_]{1,253}$/.test(id) ? null : `${who}: bad characterStyleId`;
2857
+ }
2858
+ function badTextEffect(patch, who) {
2859
+ const effect = patch?.textEffect;
2860
+ if (effect === void 0 || effect === null) return null;
2861
+ if (typeof effect !== "object") return `${who}: bad textEffect`;
2862
+ const e = effect;
2863
+ if (e.shadow !== void 0 && typeof e.shadow !== "boolean") return `${who}: bad textEffect.shadow`;
2864
+ if (e.outline !== void 0 && e.outline !== null) {
2865
+ if (typeof e.outline !== "object") return `${who}: bad textEffect.outline`;
2866
+ const o = e.outline;
2867
+ if (typeof o.color !== "string" || !/^#?[0-9a-fA-F]{6}$/.test(o.color)) return `${who}: bad textEffect.outline.color`;
2868
+ if (typeof o.widthPt !== "number" || !Number.isFinite(o.widthPt) || o.widthPt <= 0 || o.widthPt > 50) {
2869
+ return `${who}: bad textEffect.outline.widthPt`;
2870
+ }
2871
+ }
2872
+ return null;
2873
+ }
2628
2874
  function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2629
2875
  const nonNegInt = (n) => Number.isInteger(n) && n >= 0;
2876
+ if (typeof intent.clientId !== "string" || intent.clientId.length === 0 || intent.clientId.length > 128) {
2877
+ return "intent: bad clientId";
2878
+ }
2879
+ if (!nonNegInt(intent.clientSeq)) return "intent: bad clientSeq";
2880
+ if (!nonNegInt(intent.base)) return "intent: bad base";
2881
+ const badMathIndex = (math, kind) => math.mathIndex === void 0 || nonNegInt(math.mathIndex) && math.mathIndex <= 1e3 ? null : `${kind}: bad mathIndex`;
2882
+ const badSuggest = (suggest) => {
2883
+ if (suggest === void 0) return null;
2884
+ if (typeof suggest.author !== "string" || suggest.author.length > 100) return `${intent.kind}: bad author`;
2885
+ if (typeof suggest.date !== "string" || suggest.date.length > 40) return `${intent.kind}: bad date`;
2886
+ return null;
2887
+ };
2888
+ if ("suggest" in intent && intent.kind !== "suggestRevision") {
2889
+ const bad = badSuggest(intent.suggest);
2890
+ if (bad) return bad;
2891
+ }
2892
+ if ("objectIndex" in intent && intent.objectIndex !== void 0 && (!Number.isInteger(intent.objectIndex) || intent.objectIndex < 0 || intent.objectIndex > 1e4)) {
2893
+ return `${intent.kind}: bad objectIndex`;
2894
+ }
2895
+ if (isRegisteredIntent(intent)) return validateRegisteredOperation(intent);
2630
2896
  switch (intent.kind) {
2631
2897
  case "insertText":
2632
2898
  if (typeof intent.text !== "string") return "insertText: text not a string";
@@ -2634,6 +2900,13 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2634
2900
  if (intent.text.length > limits.maxInsertLength) return "insertText: too long";
2635
2901
  if (!nonNegInt(intent.at.offset)) return "insertText: bad offset";
2636
2902
  return null;
2903
+ case "insertSeparator":
2904
+ if (intent.separator !== "br" && intent.separator !== "tab") return "insertSeparator: bad separator";
2905
+ if (!nonNegInt(intent.at.offset)) return "insertSeparator: bad offset";
2906
+ return null;
2907
+ case "deleteSeparator":
2908
+ if (!nonNegInt(intent.at.offset)) return "deleteSeparator: bad offset";
2909
+ return null;
2637
2910
  case "suggestRevision": {
2638
2911
  const nRanges = intent.ranges?.length ?? 0;
2639
2912
  const nMarks = intent.marks?.length ?? 0;
@@ -2658,15 +2931,11 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2658
2931
  case "splitParagraph":
2659
2932
  if (!nonNegInt(intent.at.offset)) return "splitParagraph: bad offset";
2660
2933
  if (!nonNegInt(intent.newBlockId) || !nonNegInt(intent.newRunId)) return "splitParagraph: bad ids";
2661
- if (intent.suggest !== void 0) {
2662
- if (typeof intent.suggest.author !== "string" || intent.suggest.author.length > 100) return "splitParagraph: bad author";
2663
- if (typeof intent.suggest.date !== "string" || intent.suggest.date.length > 40) return "splitParagraph: bad date";
2664
- }
2665
2934
  return null;
2666
2935
  case "formatRange":
2667
2936
  if (!nonNegInt(intent.start) || !nonNegInt(intent.end) || intent.end <= intent.start) return "formatRange: bad range";
2668
2937
  if (!nonNegInt(intent.middleId)) return "formatRange: bad middleId";
2669
- return null;
2938
+ return badCharacterStyle(intent.patch, "formatRange") ?? badTextEffect(intent.patch, "formatRange");
2670
2939
  case "commentRun":
2671
2940
  if (typeof intent.text !== "string" || intent.text.length === 0) return "commentRun: empty";
2672
2941
  if (intent.text.length > limits.maxCommentLength) return "commentRun: too long";
@@ -2692,11 +2961,9 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2692
2961
  if (typeof intent.mathText !== "string" || intent.mathText.length === 0) return "insertMath: empty";
2693
2962
  if (intent.mathText.length > 1e4) return "insertMath: too long";
2694
2963
  return null;
2695
- case "insertShape": {
2696
- const presets = ["line", "verticalLine", "rectangle", "roundedRectangle", "ellipse", "diamond", "textBox"];
2697
- if (!presets.includes(intent.preset)) return "insertShape: bad preset";
2964
+ case "insertShape":
2965
+ if (typeof intent.preset !== "string" || !isValidShapePreset(intent.preset)) return "insertShape: bad preset";
2698
2966
  return null;
2699
- }
2700
2967
  case "replyComment":
2701
2968
  if (typeof intent.text !== "string" || intent.text.length === 0) return "replyComment: empty";
2702
2969
  if (intent.text.length > 2e4) return "replyComment: too long";
@@ -2766,7 +3033,19 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2766
3033
  case "insertWordArt": {
2767
3034
  if (typeof intent.text !== "string" || intent.text.length === 0) return "insertWordArt: empty text";
2768
3035
  if (intent.text.length > 500) return "insertWordArt: text too long";
2769
- if (!["plain", "archUp", "archDown", "wave", "chevron"].includes(intent.preset)) return "insertWordArt: bad preset";
3036
+ if (!["plain", "archUp", "archDown", "wave", "chevron", "circle", "button", "chevronDown"].includes(intent.preset)) return "insertWordArt: bad preset";
3037
+ if (intent.style !== void 0) {
3038
+ const style = intent.style;
3039
+ if (typeof style !== "object" || style === null) return "insertWordArt: bad style";
3040
+ if (!/^[0-9a-fA-F]{6}$/.test(style.fill)) return "insertWordArt: bad style fill";
3041
+ if (style.shadow !== void 0 && typeof style.shadow !== "boolean") return "insertWordArt: bad style shadow";
3042
+ if (style.outline !== void 0) {
3043
+ if (typeof style.outline !== "object" || style.outline === null) return "insertWordArt: bad style outline";
3044
+ if (!/^[0-9a-fA-F]{6}$/.test(style.outline.color)) return "insertWordArt: bad outline color";
3045
+ const width = style.outline.widthPt;
3046
+ if (typeof width !== "number" || !Number.isFinite(width) || width <= 0 || width > 50) return "insertWordArt: bad outline width";
3047
+ }
3048
+ }
2770
3049
  return null;
2771
3050
  }
2772
3051
  case "insertChart":
@@ -2780,7 +3059,12 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2780
3059
  case "setDrawingWordArtText":
2781
3060
  if (typeof intent.text !== "string" || intent.text.length === 0 || intent.text.length > 500) return "setDrawingWordArtText: bad text";
2782
3061
  return null;
3062
+ case "setDrawingWordArtStyle":
3063
+ if (!/^[0-9a-fA-F]{6}$/.test(intent.color)) return "setDrawingWordArtStyle: bad color";
3064
+ if (typeof intent.opacity !== "number" || !Number.isFinite(intent.opacity) || intent.opacity < 0 || intent.opacity > 1) return "setDrawingWordArtStyle: bad opacity";
3065
+ return null;
2783
3066
  case "setDrawingLineStyle":
3067
+ if (intent.color === null) return null;
2784
3068
  if (!/^[0-9a-fA-F]{6}$/.test(intent.color)) return "setDrawingLineStyle: bad color";
2785
3069
  if (typeof intent.widthPx !== "number" || !Number.isFinite(intent.widthPx) || intent.widthPx <= 0 || intent.widthPx > 100) return "setDrawingLineStyle: bad width";
2786
3070
  if (!["solid", "dashed", "dotted"].includes(intent.dash)) return "setDrawingLineStyle: bad dash";
@@ -2834,11 +3118,6 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2834
3118
  }
2835
3119
  return null;
2836
3120
  }
2837
- case "resizeTableRow": {
2838
- if (!Number.isInteger(intent.rowIdx) || intent.rowIdx < 0 || intent.rowIdx > 5e3) return "resizeTableRow: bad row";
2839
- if (typeof intent.heightPx !== "number" || !Number.isFinite(intent.heightPx) || intent.heightPx < 1 || intent.heightPx > 2e4) return "resizeTableRow: bad height";
2840
- return null;
2841
- }
2842
3121
  case "moveTable": {
2843
3122
  const num = (v) => typeof v === "number" && Number.isFinite(v) && v >= -5e3 && v <= 2e4;
2844
3123
  if (!num(intent.xPx) || !num(intent.yPx)) return "moveTable: bad position";
@@ -2851,13 +3130,13 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2851
3130
  // run-addressed only; resolution is the whole check
2852
3131
  case "setMathLinear":
2853
3132
  if (typeof intent.mathText !== "string" || intent.mathText.length === 0 || intent.mathText.length > 1e3) return "setMathLinear: bad mathText";
2854
- return null;
3133
+ return badMathIndex(intent, "setMathLinear");
2855
3134
  case "deleteMath":
2856
- return null;
3135
+ return badMathIndex(intent, "deleteMath");
2857
3136
  case "moveMath":
2858
3137
  if (!nonNegInt(intent.at?.offset)) return "moveMath: bad offset";
2859
3138
  if (!Array.isArray(intent.nodeIds) || intent.nodeIds.length > 64) return "moveMath: bad nodeIds";
2860
- return null;
3139
+ return badMathIndex(intent, "moveMath");
2861
3140
  case "ensureHeaderFooter":
2862
3141
  if (intent.hfKind !== "header" && intent.hfKind !== "footer") return "ensureHeaderFooter: bad kind";
2863
3142
  if (!Array.isArray(intent.nodeIds) || intent.nodeIds.length > 64) return "ensureHeaderFooter: bad nodeIds";
@@ -2865,24 +3144,30 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2865
3144
  case "deleteComment":
2866
3145
  if (typeof intent.commentId !== "string" || intent.commentId.length === 0 || intent.commentId.length > 64) return "deleteComment: bad id";
2867
3146
  return null;
3147
+ case "resolveComment":
3148
+ if (typeof intent.commentId !== "string" || intent.commentId.length === 0 || intent.commentId.length > 64) return "resolveComment: bad id";
3149
+ if (typeof intent.resolved !== "boolean") return "resolveComment: bad resolved";
3150
+ if (typeof intent.paraId !== "string" || intent.paraId.length === 0 || intent.paraId.length > 16) return "resolveComment: bad paraId";
3151
+ return null;
3152
+ case "editComment":
3153
+ if (typeof intent.commentId !== "string" || intent.commentId.length === 0 || intent.commentId.length > 64) return "editComment: bad id";
3154
+ if (typeof intent.text !== "string" || intent.text.length === 0) return "editComment: empty";
3155
+ if (intent.text.length > limits.maxCommentLength) return "editComment: too long";
3156
+ return null;
2868
3157
  case "insertBookmarkRange":
2869
3158
  if (typeof intent.name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]{0,39}$/.test(intent.name)) return "insertBookmarkRange: bad name";
2870
3159
  if (!nonNegInt(intent.start) || !nonNegInt(intent.end) || intent.end <= intent.start) return "insertBookmarkRange: bad range";
2871
3160
  return null;
2872
- case "toggleCheckbox":
2873
- return null;
2874
3161
  case "acceptRevision":
2875
3162
  case "rejectRevision":
2876
3163
  if (!nonNegInt(intent.index) || intent.index > 1e5) return `${intent.kind}: bad index`;
2877
3164
  return null;
2878
3165
  case "acceptAllRevisions":
2879
3166
  case "rejectAllRevisions":
3167
+ if (intent.author !== void 0 && (typeof intent.author !== "string" || intent.author.length > 255)) {
3168
+ return `${intent.kind}: bad author`;
3169
+ }
2880
3170
  return null;
2881
- case "insertTable": {
2882
- const okDim = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 50;
2883
- if (!okDim(intent.rows) || !okDim(intent.cols)) return "insertTable: bad dimensions";
2884
- return null;
2885
- }
2886
3171
  case "setLineNumbering": {
2887
3172
  const p = intent.patch;
2888
3173
  if (typeof p !== "object" || p === null || typeof p.enabled !== "boolean") return "setLineNumbering: bad patch";
@@ -2898,38 +3183,8 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2898
3183
  return null;
2899
3184
  }
2900
3185
  case "insertField": {
2901
- if (typeof intent.instruction !== "string" || intent.instruction.length === 0 || intent.instruction.length > 200) return "insertField: bad instruction";
2902
3186
  if (intent.cachedResult !== void 0 && (typeof intent.cachedResult !== "string" || intent.cachedResult.length > 1e3)) return "insertField: bad cachedResult";
2903
- const SAFE_FIELDS = /* @__PURE__ */ new Set([
2904
- "PAGE",
2905
- "NUMPAGES",
2906
- "SECTIONPAGES",
2907
- "SECTION",
2908
- "DATE",
2909
- "TIME",
2910
- "CREATEDATE",
2911
- "SAVEDATE",
2912
- "PRINTDATE",
2913
- "AUTHOR",
2914
- "TITLE",
2915
- "SUBJECT",
2916
- "KEYWORDS",
2917
- "COMMENTS",
2918
- "FILENAME",
2919
- "NUMWORDS",
2920
- "NUMCHARS",
2921
- "PAGEREF",
2922
- "REF",
2923
- "SEQ",
2924
- "STYLEREF",
2925
- "TOC",
2926
- "INDEX",
2927
- "LISTNUM",
2928
- "QUOTE"
2929
- ]);
2930
- const type = intent.instruction.trim().split(/\s+/)[0]?.toUpperCase();
2931
- if (!type || !SAFE_FIELDS.has(type)) return "insertField: field type not allowed";
2932
- if (!/^[\x20-\x7e]+$/.test(intent.instruction)) return "insertField: illegal instruction char";
3187
+ if (!isInsertableFieldInstruction(intent.instruction)) return "insertField: instruction not allowed";
2933
3188
  return null;
2934
3189
  }
2935
3190
  case "setDrawingRotation":
@@ -2939,14 +3194,14 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2939
3194
  if (intent.color !== null && !/^[0-9a-fA-F]{6}$/.test(intent.color)) return "setDrawingFill: bad color";
2940
3195
  return null;
2941
3196
  case "formatRun":
3197
+ return badCharacterStyle(intent.patch, "formatRun") ?? badTextEffect(intent.patch, "formatRun");
2942
3198
  case "formatParagraph":
2943
- case "setListType":
2944
3199
  case "mergeParagraph":
2945
3200
  return null;
2946
3201
  case "tableOp": {
2947
3202
  const op = intent.op;
2948
3203
  if (typeof op === "string") {
2949
- return ["deleteRow", "deleteCol", "deleteTable", "rowAbove", "rowBelow", "colLeft", "colRight"].includes(op) ? null : "tableOp: bad operation";
3204
+ return ["deleteRow", "deleteCol", "deleteTable", "rowAbove", "rowBelow", "colLeft", "colRight", "mergeRight", "mergeDown", "splitCell"].includes(op) ? null : "tableOp: bad operation";
2950
3205
  }
2951
3206
  if (op.kind === "cellShading") {
2952
3207
  return op.fill === null || /^[0-9a-fA-F]{6}$/.test(op.fill) ? null : "tableOp: bad shading";
@@ -2958,6 +3213,10 @@ function validateIntent(intent, limits = DEFAULT_INTENT_LIMITS) {
2958
3213
  if (op.wrapping !== "none" && op.wrapping !== "around" || !Number.isFinite(op.xPx) || !Number.isFinite(op.yPx)) return "tableOp: bad text wrapping";
2959
3214
  return null;
2960
3215
  }
3216
+ default: {
3217
+ const exhaustive = intent;
3218
+ return exhaustive;
3219
+ }
2961
3220
  }
2962
3221
  }
2963
3222
 
@@ -3300,6 +3559,15 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3300
3559
  this.activity = [];
3301
3560
  /** Bundle being resumed from (consumed after the welcome replay). */
3302
3561
  this.resuming = null;
3562
+ this.queuedUndos = 0;
3563
+ this.undoDeadlineTimer = null;
3564
+ /**
3565
+ * How long held undos wait for the pending queue to drain before they are
3566
+ * dropped. A few seconds: long enough to cover an ordinary round trip under
3567
+ * load, short enough that a dead connection tells the user quickly. Settable
3568
+ * for tests.
3569
+ */
3570
+ this.undoDrainTimeoutMs = 5e3;
3303
3571
  /**
3304
3572
  * Throws on the ingest/submit chain — in practice a seal or transport send
3305
3573
  * that failed AFTER the edit was accepted locally and counted as pending.
@@ -3392,6 +3660,7 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3392
3660
  codeProof: opts?.codeProof,
3393
3661
  ownerToken: opts?.ownerToken,
3394
3662
  // owner-capability proof (doc 14 §2.5)
3663
+ agentInvite: opts?.agentInvite,
3395
3664
  engineVersion: ENGINE_VERSION
3396
3665
  // the fence for client-derived canon (doc 13 §2)
3397
3666
  });
@@ -3437,6 +3706,15 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3437
3706
  setProfile(profile) {
3438
3707
  this.transport.send({ t: "profile", profile });
3439
3708
  }
3709
+ registerAgentInvite(inviteId, token, expiresAt) {
3710
+ this.transport.send({ t: "agent-invite", inviteId, token, expiresAt });
3711
+ }
3712
+ revokeAgentInvite(inviteId) {
3713
+ this.transport.send({ t: "agent-invite-revoke", inviteId });
3714
+ }
3715
+ sendAgentChat(agentClientId, messageId, iv, ciphertext) {
3716
+ this.transport.send({ t: "agent-chat", agentClientId, messageId, iv, ciphertext });
3717
+ }
3440
3718
  /**
3441
3719
  * Rejoin from a persisted bundle (doc 12 §5, encrypted flavor). Identical
3442
3720
  * contract to the plaintext connection: clientSeq watermark restored
@@ -3462,7 +3740,9 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3462
3740
  * connection — the react layer's `as unknown as CollabConnection` cast
3463
3741
  * hid it from the typechecker, so the demo's owner controls were silent
3464
3742
  * TypeErrors on encrypted docs (the default), long misattributed to a
3465
- * Vite prebundle race ("stale session w/o admin"). */
3743
+ * Vite prebundle race ("stale session w/o admin"). That cast is gone: this
3744
+ * class now `implements CollabConnectionLike`, so the same omission is a
3745
+ * compile error rather than a runtime silence. */
3466
3746
  admin(action) {
3467
3747
  this.transport.send({ t: "admin", action });
3468
3748
  }
@@ -3509,7 +3789,87 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3509
3789
  this.submitFull(intent, true);
3510
3790
  }
3511
3791
  /**
3512
- * COLLABORATIVE UNDO (plan doc 03 Phase 8), encrypted flavour.
3792
+ * COLLABORATIVE UNDO, the entry point — a PENDING GATE in front of the real
3793
+ * work below.
3794
+ *
3795
+ * The undo stack lives on the MIRROR, which advances only by ingesting
3796
+ * sequenced envelopes, while this client paints its OWN edits optimistically
3797
+ * the moment they are made. So between a keystroke and its echo the mirror
3798
+ * has not seen it, and `submitUndoNow` would reverse the previous
3799
+ * CONFIRMED action instead — the user types, presses Ctrl+Z, and watches an
3800
+ * older edit disappear while the one they just made stays. Architecturally
3801
+ * correct and completely wrong to a person.
3802
+ *
3803
+ * The press is therefore HELD while `pendingCount > 0` and run on the drain,
3804
+ * against a mirror that by then holds the edit the user meant. Presses
3805
+ * ACCUMULATE (two quick Ctrl+Z's undo two actions), because that is what the
3806
+ * same two presses do outside a room.
3807
+ *
3808
+ * Held presses are not held forever: the deadline exists because a queue
3809
+ * that never drains means the connection stopped carrying edits to
3810
+ * confirmation, and running the undo then would reverse the wrong action
3811
+ * anyway. At the deadline the queue is DROPPED and onUndoQueue reports it,
3812
+ * beside the offline/writes-blocked affordances that are already up by then.
3813
+ * Each press restarts that deadline, so it measures the wait the user is
3814
+ * actually watching rather than the age of the oldest press.
3815
+ *
3816
+ * A held press targets whatever this client's newest action is WHEN IT RUNS,
3817
+ * so typing during the wait makes the new keystrokes the undo's target. It
3818
+ * cannot be otherwise — the whole reason the press is held is that the
3819
+ * target it would name is not in the mirror yet — and it stays "undo my
3820
+ * latest thing", which is what the key means.
3821
+ */
3822
+ undoLast() {
3823
+ if (!this.mirror || !this.replica || !this.keys || !this.genesisId) return "unavailable";
3824
+ if (this.replica.pendingCount === 0) return this.submitUndoNow();
3825
+ this.queuedUndos++;
3826
+ this.armUndoDeadline();
3827
+ this.cb.onUndoQueue?.({ queued: this.queuedUndos, expired: false });
3828
+ return "queued";
3829
+ }
3830
+ /** Undos held for the drain (see undoLast) — the count onUndoQueue reports. */
3831
+ get undoQueued() {
3832
+ return this.queuedUndos;
3833
+ }
3834
+ /**
3835
+ * Run ONE held undo now that the pending queue is empty. True when it
3836
+ * submitted — the queue is non-empty again, so the caller has nothing left
3837
+ * to schedule.
3838
+ */
3839
+ runQueuedUndo() {
3840
+ if (this.queuedUndos === 0) return false;
3841
+ while (this.queuedUndos > 0) {
3842
+ this.queuedUndos--;
3843
+ const outcome = this.submitUndoNow();
3844
+ if (outcome === "undone") {
3845
+ if (this.queuedUndos > 0) this.armUndoDeadline();
3846
+ else this.clearUndoDeadline();
3847
+ this.cb.onUndoQueue?.({ queued: this.queuedUndos, expired: false });
3848
+ return true;
3849
+ }
3850
+ if (outcome !== "changed-since") this.queuedUndos = 0;
3851
+ }
3852
+ this.clearUndoDeadline();
3853
+ this.cb.onUndoQueue?.({ queued: 0, expired: false });
3854
+ return false;
3855
+ }
3856
+ armUndoDeadline() {
3857
+ this.clearUndoDeadline();
3858
+ this.undoDeadlineTimer = setTimeout(() => {
3859
+ this.undoDeadlineTimer = null;
3860
+ if (this.queuedUndos === 0) return;
3861
+ this.queuedUndos = 0;
3862
+ this.cb.onUndoQueue?.({ queued: 0, expired: true });
3863
+ }, this.undoDrainTimeoutMs);
3864
+ }
3865
+ clearUndoDeadline() {
3866
+ if (this.undoDeadlineTimer !== null) clearTimeout(this.undoDeadlineTimer);
3867
+ this.undoDeadlineTimer = null;
3868
+ }
3869
+ /**
3870
+ * COLLABORATIVE UNDO (plan doc 03 Phase 8), encrypted flavour. Callers go
3871
+ * through `undoLast`, which holds the press until the mirror can see this
3872
+ * client's own edits; everything below assumes that already happened.
3513
3873
  *
3514
3874
  * The mirror is a real DocumentSession fed the canonical log, so it already
3515
3875
  * knows the inverse of this client's last action — no new wire message and
@@ -3526,7 +3886,7 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3526
3886
  * canonical id table first, so the common conflict (someone else removed
3527
3887
  * the thing) is declined up front rather than painted and yanked back.
3528
3888
  */
3529
- undoLast() {
3889
+ submitUndoNow() {
3530
3890
  if (!this.mirror || !this.replica || !this.keys || !this.genesisId) return "unavailable";
3531
3891
  const candidate = this.mirror.takeUndo(this.clientId);
3532
3892
  if (candidate.kind !== "undoable") return candidate.kind;
@@ -3743,6 +4103,22 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3743
4103
  this.cb.onRoster?.(msg.roster);
3744
4104
  return;
3745
4105
  }
4106
+ case "agent-invite-registered": {
4107
+ this.cb.onAgentInviteRegistered?.({ inviteId: msg.inviteId, expiresAt: msg.expiresAt });
4108
+ return;
4109
+ }
4110
+ case "agent-invite-refused": {
4111
+ this.cb.onAgentInviteRefused?.({ inviteId: msg.inviteId, reason: msg.reason });
4112
+ return;
4113
+ }
4114
+ case "agent-connected": {
4115
+ this.cb.onAgentConnected?.(msg);
4116
+ return;
4117
+ }
4118
+ case "agent-chat": {
4119
+ this.cb.onAgentChat?.(msg);
4120
+ return;
4121
+ }
3746
4122
  // Session lifecycle warnings are PLAINTEXT bookkeeping and identical in
3747
4123
  // both modes: the deadline is the server's own policy, not document
3748
4124
  // content, so a blind sequencer announces it in the clear. Dispatched
@@ -3816,6 +4192,7 @@ var _EncryptedCollabConnection = class _EncryptedCollabConnection {
3816
4192
  this.scheduleStuckCheck();
3817
4193
  return;
3818
4194
  }
4195
+ if (this.runQueuedUndo()) return;
3819
4196
  this.redriveBackoffMs = 0;
3820
4197
  if (this.redriveTimer !== null) {
3821
4198
  clearTimeout(this.redriveTimer);
@@ -4305,8 +4682,12 @@ function useCollab(opts) {
4305
4682
  const [notOwner, setNotOwner] = useState(false);
4306
4683
  const [epochChanged, setEpochChanged] = useState(null);
4307
4684
  const [roster, setRoster] = useState([]);
4685
+ const [agentConnections, setAgentConnections] = useState([]);
4686
+ const [agentChat, setAgentChat] = useState([]);
4687
+ const [agentInviteError, setAgentInviteError] = useState(null);
4308
4688
  const [arrival, setArrival] = useState(null);
4309
4689
  const [selfHeals, setSelfHeals] = useState(0);
4690
+ const [undoQueue, setUndoQueue] = useState({ queued: 0, expired: false });
4310
4691
  const [droppedPreReady, setDroppedPreReady] = useState(0);
4311
4692
  const [persistErrors, setPersistErrors] = useState(0);
4312
4693
  const [storeSlow, setStoreSlow] = useState(false);
@@ -4640,7 +5021,17 @@ function useCollab(opts) {
4640
5021
  // there is deliberately no per-reason branch here to get wrong.
4641
5022
  onSessionWarningCleared: () => setDeadlines((d) => d.idle === null ? d : { ...d, idle: null }),
4642
5023
  onRoster: (r) => setRoster(r),
5024
+ onAgentInviteRegistered: () => setAgentInviteError(null),
5025
+ onAgentInviteRefused: (invite) => setAgentInviteError(invite),
5026
+ onAgentConnected: (agent) => setAgentConnections((current) => {
5027
+ const next = current.filter((item) => item.inviteId !== agent.inviteId);
5028
+ return [...next, agent];
5029
+ }),
5030
+ onAgentChat: (message) => setAgentChat((current) => [...current.slice(-199), message]),
4643
5031
  onSelfHeal: () => setSelfHeals((n) => n + 1),
5032
+ // Held undos (see CollabSession.undoQueued): the count is authoritative
5033
+ // in the connection, so it is copied rather than derived here.
5034
+ onUndoQueue: (state) => setUndoQueue(state),
4644
5035
  // A submit that never left the client. Counted, never swallowed.
4645
5036
  onSubmitDropped: () => setDroppedPreReady((n) => n + 1),
4646
5037
  // Async failures with nowhere to return (seal, transport send). Surfaced
@@ -4829,7 +5220,13 @@ function useCollab(opts) {
4829
5220
  // at construction — a pre-existing modelling wrinkle, not this arc's), so
4830
5221
  // the access is structural. The TYPE is the real one now, so a change to
4831
5222
  // UndoOutcome breaks here rather than silently disagreeing.
4832
- undoLast: () => connectionRef.current === "live" ? connRef.current?.undoLast?.() ?? "unavailable" : "unavailable",
5223
+ undoLast: () => {
5224
+ if (connectionRef.current !== "live") return "unavailable";
5225
+ setUndoQueue((state) => state.expired ? { queued: state.queued, expired: false } : state);
5226
+ return connRef.current?.undoLast?.() ?? "unavailable";
5227
+ },
5228
+ undoQueued: undoQueue.queued,
5229
+ undoExpired: undoQueue.expired,
4833
5230
  presence: connection === "live" ? presence : EMPTY_PRESENCE,
4834
5231
  refused,
4835
5232
  sessionWarning,
@@ -4899,6 +5296,18 @@ function useCollab(opts) {
4899
5296
  retryWrites: () => setReadOnlyBlocked(false),
4900
5297
  epochChanged,
4901
5298
  roster,
5299
+ agentConnections,
5300
+ agentChat,
5301
+ agentInviteError,
5302
+ registerAgentInvite: (inviteId, inviteToken, expiresAt) => {
5303
+ if (connectionRef.current === "live") connRef.current?.registerAgentInvite(inviteId, inviteToken, expiresAt);
5304
+ },
5305
+ revokeAgentInvite: (inviteId) => {
5306
+ if (connectionRef.current === "live") connRef.current?.revokeAgentInvite(inviteId);
5307
+ },
5308
+ sendAgentChat: (agentClientId, messageId, iv, ciphertext) => {
5309
+ if (connectionRef.current === "live") connRef.current?.sendAgentChat(agentClientId, messageId, iv, ciphertext);
5310
+ },
4902
5311
  setProfile: (p) => {
4903
5312
  if (connectionRef.current === "live") connRef.current?.setProfile(p);
4904
5313
  },
@@ -4962,7 +5371,7 @@ function CollabEditor(opts) {
4962
5371
  const session = useCollab(opts);
4963
5372
  useEffect(() => {
4964
5373
  opts.onSession?.(session);
4965
- }, [session.version, session.ready, session.refused, session.roster, session.epochChanged, session.sessionWarning, session.writesBlocked, session.connection, session.persistErrors, session.storeSlow, session.notOwner, session.arrival, session.offline?.editsHeld, session.offline?.capped]);
5374
+ }, [session.version, session.ready, session.refused, session.roster, session.agentConnections, session.agentChat, session.agentInviteError, session.epochChanged, session.sessionWarning, session.writesBlocked, session.connection, session.persistErrors, session.storeSlow, session.notOwner, session.arrival, session.offline?.editsHeld, session.offline?.capped]);
4966
5375
  const [api, setApi] = useState(null);
4967
5376
  const pendingCaretRef = useRef(null);
4968
5377
  const seenEpochRef = useRef(session.docEpoch);
@@ -5077,6 +5486,8 @@ function CollabEditor(opts) {
5077
5486
  // and lost it, which is why the read-only banner could be on screen while
5078
5487
  // the user's text appeared and then vanished.
5079
5488
  editable: (opts.editable ?? true) && !session.writesBlocked,
5489
+ commentAuthor: opts.commentAuthor,
5490
+ commentMentions: opts.commentMentions,
5080
5491
  revisions: opts.revisions,
5081
5492
  // Re-key only on docEpoch (a document replacement), not on every change.
5082
5493
  // Between replacements the live doc mutates in place and the key stays stable,
@@ -5090,7 +5501,8 @@ function CollabEditor(opts) {
5090
5501
  createElement(DocxToolbar, {
5091
5502
  api,
5092
5503
  mode: opts.toolbarMode ?? "advanced",
5093
- features: { ...COLLAB_TOOLBAR_DEFAULTS, ...opts.toolbarFeatures }
5504
+ features: { ...COLLAB_TOOLBAR_DEFAULTS, ...opts.toolbarFeatures },
5505
+ commentMentions: opts.commentMentions
5094
5506
  }),
5095
5507
  createElement("div", { style: { flex: 1, minHeight: 0 } }, view)
5096
5508
  );