@beyondwork/docx-react-component 1.0.79 → 1.0.81

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +28 -21
  3. package/src/api/v3/ai/resolve.ts +13 -7
  4. package/src/api/v3/runtime/workflow.ts +0 -9
  5. package/src/api/v3/ui/chrome-composition.ts +10 -2
  6. package/src/core/commands/add-scope.ts +110 -84
  7. package/src/runtime/formatting/formatting-types.ts +16 -0
  8. package/src/runtime/formatting/revision-display.ts +16 -10
  9. package/src/runtime/scopes/compile-scope-bundle.ts +9 -1
  10. package/src/runtime/scopes/compile-scope.ts +16 -0
  11. package/src/runtime/scopes/enumerate-scopes.ts +116 -3
  12. package/src/runtime/scopes/replaceability.ts +16 -0
  13. package/src/runtime/scopes/replacement/apply.ts +13 -3
  14. package/src/runtime/scopes/resolve-reference.ts +5 -0
  15. package/src/runtime/scopes/scope-kinds/scope.ts +87 -0
  16. package/src/runtime/scopes/scope-range.ts +11 -0
  17. package/src/runtime/workflow/coordinator.ts +3 -6
  18. package/src/runtime/workflow/scope-writer.ts +5 -26
  19. package/src/ui/WordReviewEditor.tsx +62 -3
  20. package/src/ui/editor-shell-view.tsx +1 -0
  21. package/src/ui/headless/revision-decoration-model.ts +10 -0
  22. package/src/ui-tailwind/chrome/editor-action-registry.ts +153 -0
  23. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +2 -0
  24. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  25. package/src/ui-tailwind/chrome/use-context-menu-controller.ts +15 -10
  26. package/src/ui-tailwind/review-workspace/types.ts +1 -0
  27. package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +46 -6
  28. package/src/ui-tailwind/theme/editor-theme.css +10 -1
  29. package/src/ui-tailwind/tw-review-workspace.tsx +114 -14
@@ -121,11 +121,29 @@ export interface RevisionEnumeratedScope {
121
121
  readonly classifications: readonly string[];
122
122
  }
123
123
 
124
+ /**
125
+ * Marker-backed scope whose start + end markers live in different
126
+ * top-level paragraphs. The reserved `"scope"` kind slot in the 13-kind
127
+ * taxonomy (`SemanticScopeKind`) exists to carry these cross-paragraph
128
+ * pairs without forcing them through the paragraph arm — the
129
+ * start-bearing paragraph continues to enumerate as `kind: "paragraph"`
130
+ * + `provenance: "derived"`, and this entry represents the pair as a
131
+ * whole.
132
+ */
133
+ export interface ScopeEnumeratedScope {
134
+ readonly kind: "scope";
135
+ readonly handle: ScopeHandle;
136
+ readonly startBlockIndex: number;
137
+ readonly endBlockIndex: number;
138
+ readonly classifications: readonly string[];
139
+ }
140
+
124
141
  /**
125
142
  * Discriminated by `kind`. Paragraph-bearing entries carry `paragraph`;
126
143
  * table-bearing entries carry the matching canonical node; field entries
127
144
  * carry the inline `FieldNode` + containing paragraph; review-store
128
- * entries carry the thread / revision record directly.
145
+ * entries carry the thread / revision record directly; multi-paragraph
146
+ * marker pairs carry the pair of block indices.
129
147
  */
130
148
  export type EnumeratedScope =
131
149
  | ParagraphLikeEnumeratedScope
@@ -134,7 +152,8 @@ export type EnumeratedScope =
134
152
  | TableCellEnumeratedScope
135
153
  | FieldEnumeratedScope
136
154
  | CommentThreadEnumeratedScope
137
- | RevisionEnumeratedScope;
155
+ | RevisionEnumeratedScope
156
+ | ScopeEnumeratedScope;
138
157
 
139
158
  export interface EnumerateScopesInputs {
140
159
  readonly overlay?: WorkflowOverlay | null;
@@ -424,6 +443,57 @@ function enumerateRevisions(
424
443
  });
425
444
  }
426
445
 
446
+ /**
447
+ * Pre-pass: for each paired marker across multiple paragraphs, return
448
+ * { scopeId, startBlockIndex, endBlockIndex }. Same-paragraph pairs are
449
+ * NOT returned here — they continue to enumerate through the paragraph
450
+ * arm as `kind: "paragraph"` + `provenance: "marker-backed"`. An
451
+ * unmatched marker (only start or only end in the doc) is skipped here;
452
+ * detachment reporting lives in `resolveScope`, not in enumeration.
453
+ */
454
+ function locateMultiParagraphMarkerPairs(
455
+ root: DocumentRootNode,
456
+ ): Array<{ scopeId: string; startBlockIndex: number; endBlockIndex: number }> {
457
+ type Open = { scopeId: string; blockIndex: number };
458
+ const open = new Map<string, Open>();
459
+ const pairs: Array<{
460
+ scopeId: string;
461
+ startBlockIndex: number;
462
+ endBlockIndex: number;
463
+ }> = [];
464
+ for (let i = 0; i < root.children.length; i += 1) {
465
+ const block = root.children[i];
466
+ if (!block || block.type !== "paragraph") continue;
467
+ for (const child of block.children) {
468
+ if (child.type === "scope_marker_start") {
469
+ open.set(child.scopeId, { scopeId: child.scopeId, blockIndex: i });
470
+ } else if (child.type === "scope_marker_end") {
471
+ const opener = open.get(child.scopeId);
472
+ if (!opener) continue;
473
+ if (opener.blockIndex !== i) {
474
+ pairs.push({
475
+ scopeId: child.scopeId,
476
+ startBlockIndex: opener.blockIndex,
477
+ endBlockIndex: i,
478
+ });
479
+ }
480
+ open.delete(child.scopeId);
481
+ }
482
+ }
483
+ }
484
+ // Pairs are pushed in close-order by the walk above — for nested or
485
+ // partially-overlapping pairs the inner/earlier-closing pair appears
486
+ // first. Sort by startBlockIndex (break ties on scopeId) so downstream
487
+ // consumers see entries in document order, and S3 determinism is
488
+ // explicit in the sort rather than implicit in walk timing.
489
+ pairs.sort(
490
+ (a, b) =>
491
+ a.startBlockIndex - b.startBlockIndex ||
492
+ a.scopeId.localeCompare(b.scopeId),
493
+ );
494
+ return pairs;
495
+ }
496
+
427
497
  export function enumerateScopes(
428
498
  document: Pick<CanonicalDocument, "content" | "docId" | "review"> | CanonicalDocumentEnvelope,
429
499
  inputs: EnumerateScopesInputs = {},
@@ -436,6 +506,10 @@ export function enumerateScopes(
436
506
  const documentId = (envelope.docId as unknown as string) ?? "";
437
507
  const classificationIndex = buildClassificationIndex(inputs.overlay);
438
508
  const knownOverlayScopeIds = new Set(classificationIndex.keys());
509
+ const multiParagraphPairs = locateMultiParagraphMarkerPairs(root);
510
+ const multiParagraphScopeIds = new Set(
511
+ multiParagraphPairs.map((p) => p.scopeId),
512
+ );
439
513
 
440
514
  const results: EnumeratedScope[] = [];
441
515
  for (let index = 0; index < root.children.length; index += 1) {
@@ -443,10 +517,19 @@ export function enumerateScopes(
443
517
  if (!block) continue;
444
518
 
445
519
  if (block.type === "paragraph") {
446
- const markerScopeId = paragraphFirstMarkerStart(
520
+ const rawMarkerScopeId = paragraphFirstMarkerStart(
447
521
  block,
448
522
  knownOverlayScopeIds,
449
523
  );
524
+ // Multi-paragraph pairs are emitted below as kind: "scope" and
525
+ // must NOT also promote their start-bearing paragraph to
526
+ // marker-backed — the paragraph stays derived and the separate
527
+ // `scope` entry represents the pair as a whole.
528
+ const markerScopeId =
529
+ rawMarkerScopeId !== null &&
530
+ !multiParagraphScopeIds.has(rawMarkerScopeId)
531
+ ? rawMarkerScopeId
532
+ : null;
450
533
  const kind = detectParagraphKind(block);
451
534
  const semanticPath = buildParagraphSemanticPath(kind, index, block);
452
535
  const scopeId =
@@ -587,6 +670,36 @@ export function enumerateScopes(
587
670
  }
588
671
  }
589
672
 
673
+ // Cross-paragraph marker pairs — emit one `kind: "scope"` entry per
674
+ // pair, ordered by start-block index (preserving document order and
675
+ // S3 determinism across compiles).
676
+ for (const pair of multiParagraphPairs) {
677
+ const semanticPath = ["body", "scope", pair.scopeId];
678
+ const hint = stableRefHintForScopeId(pair.scopeId, inputs.overlay);
679
+ const stableRef: ScopeHandle["stableRef"] =
680
+ hint === "semantic-path"
681
+ ? { kind: "semantic-path", value: semanticPath.join("/") }
682
+ : { kind: "scope-id", value: pair.scopeId };
683
+ const handle: ScopeHandle = {
684
+ scopeId: pair.scopeId,
685
+ documentId,
686
+ storyTarget: MAIN_STORY,
687
+ semanticPath,
688
+ stableRef,
689
+ provenance: "marker-backed",
690
+ rangePrecision: "marker-backed",
691
+ };
692
+ const classifications =
693
+ classificationIndex.get(pair.scopeId) ?? Object.freeze<string[]>([]);
694
+ results.push({
695
+ kind: "scope",
696
+ handle,
697
+ startBlockIndex: pair.startBlockIndex,
698
+ endBlockIndex: pair.endBlockIndex,
699
+ classifications,
700
+ } satisfies ScopeEnumeratedScope);
701
+ }
702
+
590
703
  // Review-store scopes — threads + revisions — enumerate after document
591
704
  // walk so their block ordering in `results` stays stable (all block scopes
592
705
  // first, then review).
@@ -47,6 +47,22 @@ export function deriveReplaceability(
47
47
  reason: "marker-backed-preserves-anchor",
48
48
  };
49
49
  }
50
+ // Multi-paragraph marker-backed scopes — the `scope` kind slot. Replace
51
+ // semantics across multiple blocks are not yet compiler-backed; callers
52
+ // may read but should not full-replace until Task N of the
53
+ // multi-paragraph plan wires block-granular replacement.
54
+ if (kind === "scope") {
55
+ if (provenance === "marker-backed") {
56
+ return {
57
+ level: "preserve-only",
58
+ reason: "multi-paragraph-replace-not-implemented",
59
+ };
60
+ }
61
+ return {
62
+ level: "blocked",
63
+ reason: "scope-kind-requires-markers",
64
+ };
65
+ }
50
66
  switch (kind) {
51
67
  case "paragraph":
52
68
  return { level: "full", reason: "derived-default" };
@@ -199,10 +199,20 @@ export function applyScopeReplacement(
199
199
  resolvedScope.kind === "paragraph" ||
200
200
  resolvedScope.kind === "heading" ||
201
201
  resolvedScope.kind === "list-item";
202
+ // Multi-paragraph `scope` kind (Task 7 of the 2026-04-24 plan): the
203
+ // compiler has no replacement lowering for cross-paragraph marker
204
+ // spans yet. Replaceability already declares `preserve-only` with
205
+ // `reason: "multi-paragraph-replace-not-implemented"`; apply mirrors
206
+ // that reason into the refusal taxonomy suffix so consumers reading
207
+ // `blockers[0]` / `reason` see the actionable sub-reason directly
208
+ // (rather than the bare `compile-refused:scope`). Grammar matches
209
+ // §10 `compile-refused:<kind>:<sub-reason>` (74a45eaf, 2026-04-23).
202
210
  const blocker =
203
- paragraphLike && proposed.operation !== "replace"
204
- ? `compile-refused:${resolvedScope.kind}:operation-not-implemented:${proposed.operation}`
205
- : `compile-refused:${resolvedScope.kind}`;
211
+ resolvedScope.kind === "scope"
212
+ ? "compile-refused:scope:multi-paragraph-replace-not-implemented"
213
+ : paragraphLike && proposed.operation !== "replace"
214
+ ? `compile-refused:${resolvedScope.kind}:operation-not-implemented:${proposed.operation}`
215
+ : `compile-refused:${resolvedScope.kind}`;
206
216
  const refused: ValidationResult = {
207
217
  safe: false,
208
218
  blockedReasons: Object.freeze([blocker]),
@@ -307,6 +307,11 @@ function extractNLHaystack(entry: EnumeratedScope): string {
307
307
  case "revision":
308
308
  return `${entry.revision.kind} ${entry.revision.authorId ?? ""}`
309
309
  .toLowerCase();
310
+ case "scope":
311
+ // Cross-paragraph marker pair — no inline text of its own;
312
+ // semantic-path matching (`body/scope/<id>`) covers it, and the
313
+ // per-paragraph entries inside the pair carry their own haystacks.
314
+ return "";
310
315
  default: {
311
316
  const _never: never = entry;
312
317
  void _never;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Compile `kind: "scope"` entries — multi-paragraph marker-backed scopes.
3
+ *
4
+ * Aggregates the spanning paragraphs' text into `content.text` (joined by
5
+ * `\n`), projects a bounded formatting summary (no single paragraphStyleId
6
+ * is authoritative across multiple paragraphs; we surface none), and stays
7
+ * `partial: true` because layout + geometry projections are not yet
8
+ * compiler-backed for multi-block scopes.
9
+ *
10
+ * Replaceability is `"preserve-only"` for now — see
11
+ * `replaceability.ts::deriveReplaceability`.
12
+ *
13
+ * Determinism (S3): pure projection of (spanning paragraphs' text,
14
+ * classifications, provenance, sectionIndex). No ambient state.
15
+ */
16
+
17
+ import type {
18
+ CanonicalDocument,
19
+ DocumentRootNode,
20
+ ParagraphNode,
21
+ } from "../../../model/canonical-document.ts";
22
+ import type { CanonicalDocumentEnvelope } from "../../../core/state/editor-state.ts";
23
+ import type { ScopeEnumeratedScope } from "../enumerate-scopes.ts";
24
+ import { deriveReplaceability } from "../replaceability.ts";
25
+ import type {
26
+ SemanticScope,
27
+ SemanticScopeWorkflow,
28
+ } from "../semantic-scope-types.ts";
29
+
30
+ import { extractParagraphText, buildExcerpt } from "./_paragraph-text.ts";
31
+
32
+ export interface CompileScopeKindOptions {
33
+ readonly document: CanonicalDocument | CanonicalDocumentEnvelope;
34
+ readonly workflow?: SemanticScopeWorkflow;
35
+ /**
36
+ * 0-based section index of the scope's **first** spanning block
37
+ * (matches paragraph-kind semantics; agents reading layout.sectionIndex
38
+ * for routing get the scope's home section).
39
+ */
40
+ readonly sectionIndex?: number;
41
+ }
42
+
43
+ export function compileScopeKind(
44
+ entry: ScopeEnumeratedScope,
45
+ options: CompileScopeKindOptions,
46
+ ): SemanticScope {
47
+ const envelope = options.document as CanonicalDocumentEnvelope;
48
+ const root: DocumentRootNode =
49
+ "content" in envelope
50
+ ? (envelope.content as DocumentRootNode)
51
+ : (options.document as unknown as DocumentRootNode);
52
+
53
+ const texts: string[] = [];
54
+ for (let i = entry.startBlockIndex; i <= entry.endBlockIndex; i += 1) {
55
+ const block = root.children[i];
56
+ if (!block || block.type !== "paragraph") continue;
57
+ texts.push(extractParagraphText(block as ParagraphNode));
58
+ }
59
+ const text = texts.join("\n");
60
+
61
+ return {
62
+ handle: entry.handle,
63
+ kind: "scope",
64
+ classifications: entry.classifications,
65
+ content: {
66
+ text,
67
+ excerpt: buildExcerpt(text),
68
+ },
69
+ formatting: {},
70
+ layout:
71
+ typeof options.sectionIndex === "number"
72
+ ? { sectionIndex: options.sectionIndex }
73
+ : {},
74
+ geometry: {},
75
+ workflow: options.workflow ?? { scopeIds: [], effectiveMode: "edit" },
76
+ replaceability: deriveReplaceability("scope", entry.handle.provenance),
77
+ audit: {
78
+ source: "runtime",
79
+ derivedFrom:
80
+ entry.classifications.length > 0
81
+ ? ["canonical", "workflow-overlay"]
82
+ : ["canonical"],
83
+ confidence: "medium",
84
+ },
85
+ partial: true,
86
+ };
87
+ }
@@ -165,6 +165,17 @@ export function resolveScopeRange(
165
165
  return anchorToRange(entry.thread.anchor);
166
166
  case "revision":
167
167
  return anchorToRange(entry.revision.anchor);
168
+ case "scope": {
169
+ // Cross-paragraph marker pair. The marker-range lookup at the top
170
+ // of this function (stableRef.kind === "scope-id") normally wins
171
+ // first. This branch handles the case where the handle's stableRef
172
+ // was overridden to `semantic-path` via the `stableRefHint` seam —
173
+ // we fall back to spanning the start-block low to end-block high.
174
+ const startRange = positionMap.blocks.get(entry.startBlockIndex);
175
+ const endRange = positionMap.blocks.get(entry.endBlockIndex);
176
+ if (!startRange || !endRange) return null;
177
+ return { from: startRange.from, to: endRange.to };
178
+ }
168
179
  default: {
169
180
  const never: never = entry;
170
181
  void never;
@@ -824,12 +824,9 @@ export function createWorkflowCoordinator(deps: CoordinatorDeps): WorkflowCoordi
824
824
  plantStatus: {
825
825
  planted: false,
826
826
  reason: plantResult.status,
827
- ...(plantResult.status === "cross-paragraph-range"
828
- ? {
829
- fromBlockIndex: plantResult.fromBlockIndex,
830
- toBlockIndex: plantResult.toBlockIndex,
831
- }
832
- : {}),
827
+ // Cross-paragraph ranges now plant successfully (2026-04-24
828
+ // multi-paragraph-scopes slice) — that refusal variant is
829
+ // retired. Remaining failure reasons carry diagnostic fields:
833
830
  ...(plantResult.status === "non-paragraph-target"
834
831
  ? {
835
832
  blockIndex: plantResult.blockIndex,
@@ -277,15 +277,11 @@ export type CreateScopeFromAnchorResult =
277
277
  | "from-negative"
278
278
  | "to-less-than-from"
279
279
  | "range-exceeds-story-length"
280
- | "cross-paragraph-range"
281
280
  | "non-paragraph-target"
282
281
  | "empty-document";
283
282
  readonly from: number;
284
283
  readonly to: number;
285
284
  readonly storyLength: number;
286
- /** Cross-paragraph only — the two block indices the range straddled. */
287
- readonly fromBlockIndex?: number;
288
- readonly toBlockIndex?: number;
289
285
  /** Non-paragraph target only — the offending block's index and kind. */
290
286
  readonly blockIndex?: number;
291
287
  readonly blockKind?: string;
@@ -301,7 +297,6 @@ export type CreateScopeFromAnchorResult =
301
297
  * don't want to pattern-match on `reason`. Examples:
302
298
  * "clamp-from-to-zero", "swap-from-and-to",
303
299
  * "clamp-to-to-storyLength-or-pick-a-different-range",
304
- * "narrow-to-single-paragraph",
305
300
  * "pick-a-paragraph-target".
306
301
  */
307
302
  readonly nextStep: string;
@@ -432,31 +427,15 @@ export function createScopeFromAnchor(
432
427
  });
433
428
 
434
429
  // Pre-2026-04-24 the coordinator silently returned a minted scopeId
435
- // even when insertScopeMarkers refused to plant (cross-paragraph
436
- // range, non-paragraph target, out-of-bounds after the story-length
437
- // check passed). Now the coordinator surfaces `plantStatus.planted:
438
- // false`; translate each reason into the same `range-invalid` shape
430
+ // even when insertScopeMarkers refused to plant (non-paragraph
431
+ // target, out-of-bounds after the story-length check passed).
432
+ // Cross-paragraph ranges now plant successfully (2026-04-24
433
+ // multi-paragraph-scopes slice), so that refusal variant is retired.
434
+ // Remaining reasons translate into the same `range-invalid` shape
439
435
  // used by the bounds checks above so the caller gets one uniform
440
436
  // discriminator to branch on.
441
437
  if (result.plantStatus && result.plantStatus.planted === false) {
442
438
  const ps = result.plantStatus;
443
- if (ps.reason === "cross-paragraph-range") {
444
- return {
445
- status: "range-invalid",
446
- reason: "cross-paragraph-range",
447
- from,
448
- to,
449
- storyLength,
450
- fromBlockIndex: ps.fromBlockIndex ?? -1,
451
- toBlockIndex: ps.toBlockIndex ?? -1,
452
- message:
453
- `createScopeFromAnchor refused: range [${from}, ${to}] straddles ` +
454
- `paragraphs ${ps.fromBlockIndex} and ${ps.toBlockIndex}. Marker-backed ` +
455
- `scopes only plant inside a single paragraph today. Narrow the range to ` +
456
- `land inside one paragraph, or create two separate scopes.`,
457
- nextStep: "narrow-to-single-paragraph",
458
- };
459
- }
460
439
  if (ps.reason === "non-paragraph-target") {
461
440
  return {
462
441
  status: "range-invalid",
@@ -186,6 +186,7 @@ import { EditorShellView } from "./editor-shell-view.tsx";
186
186
  import { TwDebugPresentation } from "../ui-tailwind/debug/index.ts";
187
187
  import { shellPasteFragmentParser as SHELL_PASTE_FRAGMENT_PARSER } from "../shell/paste-adapter.ts";
188
188
  import { EditorSurfaceController } from "./editor-surface-controller.tsx";
189
+ import type { EditorActionHostCallbacks } from "../ui-tailwind/chrome/editor-action-registry";
189
190
  import type { TwWorkspaceChromeHostController } from "../ui-tailwind/chrome/tw-workspace-chrome-host";
190
191
  import {
191
192
  resolveChromePreset,
@@ -3306,6 +3307,63 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3306
3307
  },
3307
3308
  });
3308
3309
 
3310
+ const productEditorActionHost = useMemo<EditorActionHostCallbacks>(() => {
3311
+ const runClipboardCommand = (command: "copy" | "cut") => {
3312
+ try {
3313
+ globalThis.document?.execCommand?.(command);
3314
+ } catch {
3315
+ // Browser-native clipboard commands are best-effort fallbacks.
3316
+ }
3317
+ };
3318
+
3319
+ const defaultHost: EditorActionHostCallbacks = {
3320
+ onUndo: commands.onUndo,
3321
+ onRedo: commands.onRedo,
3322
+ onCut: () => runClipboardCommand("cut"),
3323
+ onCopy: () => runClipboardCommand("copy"),
3324
+ onPaste: () => {
3325
+ const readText = globalThis.navigator?.clipboard?.readText;
3326
+ if (typeof readText !== "function") return;
3327
+ void readText.call(globalThis.navigator.clipboard)
3328
+ .then((text: string) => {
3329
+ if (!text) return;
3330
+ dispatchTextCommand(
3331
+ activeRuntime,
3332
+ { type: "insert-text", text },
3333
+ DISPATCH_CONTEXT,
3334
+ );
3335
+ })
3336
+ .catch(() => {
3337
+ // Clipboard permission failures should not break the menu.
3338
+ });
3339
+ },
3340
+ onToggleBold: commands.onToggleBold,
3341
+ onToggleItalic: commands.onToggleItalic,
3342
+ onToggleUnderline: commands.onToggleUnderline,
3343
+ onToggleStrikethrough: commands.onToggleStrikethrough,
3344
+ onToggleBulletedList: commands.onToggleBulletedList,
3345
+ onToggleNumberedList: commands.onToggleNumberedList,
3346
+ onOutdent: commands.onOutdent,
3347
+ onIndent: commands.onIndent,
3348
+ onSetAlignment: (alignment) => commands.onSetAlignment?.(alignment),
3349
+ onInsertTable: commands.onInsertTable,
3350
+ onAddComment: commands.onAddComment,
3351
+ onInsertRowAbove: commands.onAddRowBefore,
3352
+ onInsertRowBelow: commands.onAddRowAfter,
3353
+ onInsertColumnBefore: commands.onAddColumnBefore,
3354
+ onInsertColumnAfter: commands.onAddColumnAfter,
3355
+ onDeleteRow: commands.onDeleteRow,
3356
+ onDeleteColumn: commands.onDeleteColumn,
3357
+ onDeleteTable: commands.onDeleteTable,
3358
+ onMergeCells: commands.onMergeCells,
3359
+ onSplitCell: commands.onSplitCell,
3360
+ };
3361
+
3362
+ return editorActionHost
3363
+ ? { ...defaultHost, ...editorActionHost }
3364
+ : defaultHost;
3365
+ }, [activeRuntime, commands, editorActionHost]);
3366
+
3309
3367
  const harnessShowUnsupportedPreviews = readHarnessDebugPortsFlag(__harnessDebugPorts, "unsupportedObjectPreviews");
3310
3368
  const effectiveShowUnsupportedPreviews = computeEffectiveShowUnsupportedPreviews({
3311
3369
  harnessShowUnsupportedPreviews,
@@ -3342,7 +3400,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3342
3400
  activeWorkflowScopeIds={workflowScopeSnapshot?.activeWorkItem?.scopeIds ?? []}
3343
3401
  workflowMetadata={workflowMarkupSnapshot?.metadata}
3344
3402
  onSelectionToolbarAnchorChange={handleSelectionToolbarAnchorChange}
3345
- {...(editorActionHost ? { onContextMenuRequested: handleContextMenuRequested } : {})}
3403
+ onContextMenuRequested={handleContextMenuRequested}
3346
3404
  {...editorCallbacks}
3347
3405
  dispatchRuntimeCommand={(command) =>
3348
3406
  activeRuntime.applyActiveStoryTextCommand(command as never)
@@ -3420,8 +3478,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3420
3478
  interactionGuardSnapshot={interactionGuardSnapshot}
3421
3479
  chromePreset={effectiveChromePreset}
3422
3480
  chromeOptions={chromeOptions}
3423
- {...(editorActionHost ? { editorActionHost } : {})}
3424
- {...(editorActionHost ? { chromeControllerRef: teedChromeControllerRef } : {})}
3481
+ density={density}
3482
+ editorActionHost={productEditorActionHost}
3483
+ chromeControllerRef={teedChromeControllerRef}
3425
3484
  {...(commandPaletteDisabled !== undefined
3426
3485
  ? { commandPaletteDisabled }
3427
3486
  : {})}
@@ -83,6 +83,7 @@ export interface EditorShellViewProps {
83
83
  interactionGuardSnapshot?: InteractionGuardSnapshot;
84
84
  chromePreset?: WordReviewEditorChromePreset;
85
85
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;
86
+ density?: "compact" | "standard" | "comfortable";
86
87
  editorActionHost?: import("../ui-tailwind/chrome/editor-action-registry.ts").EditorActionHostCallbacks;
87
88
  chromeControllerRef?: React.Ref<
88
89
  import("../ui-tailwind/chrome/tw-workspace-chrome-host.tsx").TwWorkspaceChromeHostController
@@ -155,6 +155,16 @@ export function getRevisionRangeState(
155
155
  * by `test/runtime/formatting/production-boundary.test.ts`.
156
156
  */
157
157
  export interface RevisionDisplayFlags {
158
+ /**
159
+ * Identity of the attached revision. Mirrors
160
+ * `SurfaceInlineSegment.revisionDisplay.revisionId`.
161
+ */
162
+ revisionId: string;
163
+ /**
164
+ * Mirrors `RevisionRecord.kind`. Consumers branch on insertion /
165
+ * deletion variants without reading the review store.
166
+ */
167
+ kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
158
168
  markupMode: "clean" | "simple" | "all";
159
169
  hidden?: boolean;
160
170
  strikethrough?: boolean;