@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.
- package/package.json +1 -1
- package/src/api/public-types.ts +28 -21
- package/src/api/v3/ai/resolve.ts +13 -7
- package/src/api/v3/runtime/workflow.ts +0 -9
- package/src/api/v3/ui/chrome-composition.ts +10 -2
- package/src/core/commands/add-scope.ts +110 -84
- package/src/runtime/formatting/formatting-types.ts +16 -0
- package/src/runtime/formatting/revision-display.ts +16 -10
- package/src/runtime/scopes/compile-scope-bundle.ts +9 -1
- package/src/runtime/scopes/compile-scope.ts +16 -0
- package/src/runtime/scopes/enumerate-scopes.ts +116 -3
- package/src/runtime/scopes/replaceability.ts +16 -0
- package/src/runtime/scopes/replacement/apply.ts +13 -3
- package/src/runtime/scopes/resolve-reference.ts +5 -0
- package/src/runtime/scopes/scope-kinds/scope.ts +87 -0
- package/src/runtime/scopes/scope-range.ts +11 -0
- package/src/runtime/workflow/coordinator.ts +3 -6
- package/src/runtime/workflow/scope-writer.ts +5 -26
- package/src/ui/WordReviewEditor.tsx +62 -3
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +10 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +153 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
- package/src/ui-tailwind/chrome/use-context-menu-controller.ts +15 -10
- package/src/ui-tailwind/review-workspace/types.ts +1 -0
- package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +46 -6
- package/src/ui-tailwind/theme/editor-theme.css +10 -1
- 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
|
|
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
|
-
|
|
204
|
-
?
|
|
205
|
-
:
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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 (
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
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
|
-
{
|
|
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
|
-
{
|
|
3424
|
-
|
|
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;
|