@beyondwork/docx-react-component 1.0.83 → 1.0.85
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/internal/build-ref-projections.ts +3 -0
- package/src/api/public-types.ts +86 -4
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/content.ts +148 -1
- package/src/api/v3/runtime/formatting.ts +41 -0
- package/src/api/v3/runtime/review.ts +98 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/header-footer-reference.ts +38 -0
- package/src/io/ooxml/parse-headers-footers.ts +11 -23
- package/src/io/ooxml/parse-main-document.ts +7 -10
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +718 -68
- package/src/runtime/formatting/field/resolver.ts +73 -8
- package/src/runtime/layout/layout-engine-version.ts +31 -12
- package/src/runtime/layout/paginated-layout-engine.ts +18 -11
- package/src/runtime/layout/public-facet.ts +119 -16
- package/src/runtime/layout/resolve-page-fields.ts +68 -6
- package/src/runtime/layout/resolve-page-previews.ts +1 -1
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +54 -45
- package/src/runtime/scopes/workflow-overlap.ts +41 -9
- package/src/runtime/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +10 -0
- package/src/ui/WordReviewEditor.tsx +99 -9
- package/src/ui/editor-command-bag.ts +3 -1
- package/src/ui/headless/revision-decoration-model.ts +13 -0
- package/src/ui/headless/selection-tool-types.ts +2 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/types.ts +3 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.85",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
|
@@ -52,6 +52,9 @@ export function buildRefProjections(
|
|
|
52
52
|
rejectAll: () => getRef().rejectAllChanges(),
|
|
53
53
|
acceptGroup: (groupId) => getRef().acceptSuggestionGroup(groupId),
|
|
54
54
|
rejectGroup: (groupId) => getRef().rejectSuggestionGroup(groupId),
|
|
55
|
+
getCommentThread: (changeId) => getRef().getCommentThreadForChange(changeId),
|
|
56
|
+
ensureCommentThread: (changeId) => getRef().ensureCommentThreadForChange(changeId),
|
|
57
|
+
addReply: (changeId, body) => getRef().addReplyToChange(changeId, body),
|
|
55
58
|
scrollTo: (revisionId) => getRef().scrollToRevision(revisionId),
|
|
56
59
|
get: () => getRef().getTrackedChanges(),
|
|
57
60
|
getSuggestions: () => getRef().getSuggestionsSnapshot(),
|
package/src/api/public-types.ts
CHANGED
|
@@ -592,6 +592,8 @@ export interface CommentSidebarThreadSnapshot {
|
|
|
592
592
|
createdBy: string;
|
|
593
593
|
warningCount: number;
|
|
594
594
|
anchorLabel: string;
|
|
595
|
+
/** Present when this thread is the discussion attached to a tracked change. */
|
|
596
|
+
linkedRevisionId?: string;
|
|
595
597
|
detachedReason?: "incomplete-markers" | "multi-paragraph" | "opaque-region" | "revision-overlap";
|
|
596
598
|
actionabilityNote?: string;
|
|
597
599
|
isActive: boolean;
|
|
@@ -630,6 +632,10 @@ export interface TrackedChangeEntrySnapshot {
|
|
|
630
632
|
warningCount: number;
|
|
631
633
|
canAccept: boolean;
|
|
632
634
|
canReject: boolean;
|
|
635
|
+
/** Comment threads whose metadata links them to this revision. */
|
|
636
|
+
commentThreadIds?: string[];
|
|
637
|
+
/** Number of reply entries across linked comment threads. */
|
|
638
|
+
replyCount?: number;
|
|
633
639
|
preserveOnlyReason?: string;
|
|
634
640
|
excerpt?: string;
|
|
635
641
|
detail?: string;
|
|
@@ -672,6 +678,10 @@ export interface SuggestionEntrySnapshot {
|
|
|
672
678
|
preserveOnlyReason?: string;
|
|
673
679
|
excerpt?: string;
|
|
674
680
|
detail?: string;
|
|
681
|
+
/** Comment threads linked to any revision in this suggestion unit. */
|
|
682
|
+
commentThreadIds?: string[];
|
|
683
|
+
/** Number of reply entries across linked comment threads. */
|
|
684
|
+
replyCount?: number;
|
|
675
685
|
/**
|
|
676
686
|
* R3 — scope-card-overlay P2. When present, links this entry to a
|
|
677
687
|
* `SuggestionGroup` via `SuggestionsSnapshot.groups[].groupId`.
|
|
@@ -1311,6 +1321,12 @@ export type SurfaceInlineSegment =
|
|
|
1311
1321
|
instruction: string;
|
|
1312
1322
|
refreshStatus: FieldRefreshStatus;
|
|
1313
1323
|
label: string;
|
|
1324
|
+
/**
|
|
1325
|
+
* Current display text for fields whose visible value is computed by
|
|
1326
|
+
* layout context (PAGE / NUMPAGES / SECTIONPAGES). Consumers fall back
|
|
1327
|
+
* to `label` when absent.
|
|
1328
|
+
*/
|
|
1329
|
+
displayText?: string;
|
|
1314
1330
|
}
|
|
1315
1331
|
| {
|
|
1316
1332
|
/**
|
|
@@ -2512,6 +2528,16 @@ export interface AddScopeParams {
|
|
|
2512
2528
|
storyTarget?: EditorStoryTarget;
|
|
2513
2529
|
/** Optional display label for the scope card / rail. */
|
|
2514
2530
|
label?: string;
|
|
2531
|
+
/**
|
|
2532
|
+
* Optional per-scope visibility. Controls chrome/query presentation only;
|
|
2533
|
+
* edit enforcement is controlled by `guardPolicy`.
|
|
2534
|
+
*/
|
|
2535
|
+
visibility?: ScopeVisibility;
|
|
2536
|
+
/**
|
|
2537
|
+
* Optional edit-enforcement posture. Absent means advisory for
|
|
2538
|
+
* edit/suggest/comment scopes and read-only for `mode: "view"`.
|
|
2539
|
+
*/
|
|
2540
|
+
guardPolicy?: WorkflowScopeGuardPolicy;
|
|
2515
2541
|
}
|
|
2516
2542
|
|
|
2517
2543
|
export interface AddScopeResult {
|
|
@@ -2591,6 +2617,19 @@ export interface ExportResult {
|
|
|
2591
2617
|
|
|
2592
2618
|
export type WorkflowScopeMode = "edit" | "suggest" | "comment" | "view";
|
|
2593
2619
|
|
|
2620
|
+
/**
|
|
2621
|
+
* Explicit scope enforcement axis. Visibility is presentation-only.
|
|
2622
|
+
*
|
|
2623
|
+
* - `"none"`: advisory/metadata/chrome scope; direct editing is unaffected.
|
|
2624
|
+
* - `"insert-only"`: allowlist scope; when any active insert-only scope
|
|
2625
|
+
* exists, selection-driven edits outside insert-only scopes are blocked.
|
|
2626
|
+
* - `"read-only"`: deny edits inside this scope; outside content is unaffected.
|
|
2627
|
+
*
|
|
2628
|
+
* When absent, `mode: "view"` scopes default to `"read-only"` for
|
|
2629
|
+
* compatibility; all other modes default to `"none"`.
|
|
2630
|
+
*/
|
|
2631
|
+
export type WorkflowScopeGuardPolicy = "none" | "insert-only" | "read-only";
|
|
2632
|
+
|
|
2594
2633
|
/**
|
|
2595
2634
|
* §C7 — Local chrome visibility mode. Never collab-replicated.
|
|
2596
2635
|
* - `"all"`: show all scope rail entries + decorations (default).
|
|
@@ -2615,8 +2654,8 @@ export interface ScopeChromeVisibilityState {
|
|
|
2615
2654
|
*
|
|
2616
2655
|
* - `"visible"`: rail entry + card + inline decoration (current behavior).
|
|
2617
2656
|
* - `"hidden"`: in the rail but muted; card opens on explicit click; no decoration.
|
|
2618
|
-
* - `"invisible"`: never rendered; only queryable via API.
|
|
2619
|
-
* to InteractionGuard
|
|
2657
|
+
* - `"invisible"`: never rendered; only queryable via API. Visibility never
|
|
2658
|
+
* contributes to InteractionGuard.
|
|
2620
2659
|
*/
|
|
2621
2660
|
export type ScopeVisibility = "visible" | "hidden" | "invisible";
|
|
2622
2661
|
|
|
@@ -2639,6 +2678,12 @@ export interface WorkflowScope {
|
|
|
2639
2678
|
domain?: "legal" | "commercial" | "finance" | "other";
|
|
2640
2679
|
metadataRefs?: string[];
|
|
2641
2680
|
metadata?: WorkflowScopeMetadataField[];
|
|
2681
|
+
/**
|
|
2682
|
+
* Explicit edit-enforcement posture. Absent means:
|
|
2683
|
+
* - `mode: "view"` -> `"read-only"` (legacy read-only behavior).
|
|
2684
|
+
* - all other modes -> `"none"` (advisory/chrome/metadata only).
|
|
2685
|
+
*/
|
|
2686
|
+
guardPolicy?: WorkflowScopeGuardPolicy;
|
|
2642
2687
|
/**
|
|
2643
2688
|
* Schema 1.1 — override the overlay default for this scope.
|
|
2644
2689
|
* `"inherit"` defers to the overlay; absent is equivalent to
|
|
@@ -4402,6 +4447,9 @@ export interface WordReviewEditorChangesFacet {
|
|
|
4402
4447
|
rejectAll(): void;
|
|
4403
4448
|
acceptGroup(groupId: string): void;
|
|
4404
4449
|
rejectGroup(groupId: string): void;
|
|
4450
|
+
getCommentThread(changeId: string): CommentSidebarThreadSnapshot | null;
|
|
4451
|
+
ensureCommentThread(changeId: string): AddCommentResult | null;
|
|
4452
|
+
addReply(changeId: string, body: string): AddCommentReplyResult | null;
|
|
4405
4453
|
scrollTo(revisionId: string): void;
|
|
4406
4454
|
get(): TrackedChangesSnapshot;
|
|
4407
4455
|
getSuggestions(): SuggestionsSnapshot;
|
|
@@ -4549,6 +4597,25 @@ export interface WordReviewEditorRef {
|
|
|
4549
4597
|
commentId: string,
|
|
4550
4598
|
body: string,
|
|
4551
4599
|
): AddCommentReplyResult | null;
|
|
4600
|
+
/**
|
|
4601
|
+
* Return the open/resolved comment thread linked to a tracked change, if
|
|
4602
|
+
* one exists. This is the read side for "reply to tracked change" UX.
|
|
4603
|
+
*/
|
|
4604
|
+
getCommentThreadForChange(changeId: string): CommentSidebarThreadSnapshot | null;
|
|
4605
|
+
/**
|
|
4606
|
+
* Create or return the discussion thread attached to a tracked change.
|
|
4607
|
+
* Newly-created threads are anchored to the revision range and opened in
|
|
4608
|
+
* the comments rail by mounted UI callers.
|
|
4609
|
+
*/
|
|
4610
|
+
ensureCommentThreadForChange(changeId: string): AddCommentResult | null;
|
|
4611
|
+
/**
|
|
4612
|
+
* Append a reply to a tracked change's linked discussion thread, creating
|
|
4613
|
+
* that thread first when needed.
|
|
4614
|
+
*/
|
|
4615
|
+
addReplyToChange(
|
|
4616
|
+
changeId: string,
|
|
4617
|
+
body: string,
|
|
4618
|
+
): AddCommentReplyResult | null;
|
|
4552
4619
|
editCommentBody(commentId: string, body: string): void;
|
|
4553
4620
|
deleteComment(commentId: string): void;
|
|
4554
4621
|
/**
|
|
@@ -4571,8 +4638,9 @@ export interface WordReviewEditorRef {
|
|
|
4571
4638
|
removeScope(scopeId: string): void;
|
|
4572
4639
|
/**
|
|
4573
4640
|
* §C8 — Convenience: adds a scope with `visibility: "invisible"` atomically.
|
|
4574
|
-
* Mode defaults to `"comment"
|
|
4575
|
-
*
|
|
4641
|
+
* Mode defaults to `"comment"`. Visibility is presentation-only; edit
|
|
4642
|
+
* gating is controlled by `guardPolicy` (`mode: "view"` still defaults to
|
|
4643
|
+
* read-only for compatibility unless `guardPolicy: "none"` is set).
|
|
4576
4644
|
*/
|
|
4577
4645
|
addInvisibleScope(
|
|
4578
4646
|
params: Omit<AddScopeParams, "mode"> & { mode?: WorkflowScopeMode },
|
|
@@ -4587,6 +4655,20 @@ export interface WordReviewEditorRef {
|
|
|
4587
4655
|
* scopes or when the `visibility` field has never been set.
|
|
4588
4656
|
*/
|
|
4589
4657
|
getScopeVisibility(scopeId: string): ScopeVisibility;
|
|
4658
|
+
/**
|
|
4659
|
+
* Set a scope's edit-enforcement posture. Collab-replicated through the
|
|
4660
|
+
* workflow overlay; visibility remains purely presentational.
|
|
4661
|
+
*/
|
|
4662
|
+
setScopeGuardPolicy(
|
|
4663
|
+
scopeId: string,
|
|
4664
|
+
guardPolicy: WorkflowScopeGuardPolicy,
|
|
4665
|
+
): void;
|
|
4666
|
+
/**
|
|
4667
|
+
* Get a scope's effective guard policy. Returns `"read-only"` for legacy
|
|
4668
|
+
* `mode: "view"` scopes with no explicit policy and `"none"` for unknown
|
|
4669
|
+
* scopes.
|
|
4670
|
+
*/
|
|
4671
|
+
getScopeGuardPolicy(scopeId: string): WorkflowScopeGuardPolicy;
|
|
4590
4672
|
/**
|
|
4591
4673
|
* §C7 — Set the local chrome-visibility state. Local view-state only —
|
|
4592
4674
|
* never collab-replicated. Controls whether the scope rail / decorations
|
|
@@ -53,12 +53,18 @@ export type RuntimeApiHandle = Pick<
|
|
|
53
53
|
| "getCanonicalDocument"
|
|
54
54
|
// Content search + selection (runtime.content + ai.bundle families)
|
|
55
55
|
| "findAllText"
|
|
56
|
+
| "replaceText"
|
|
57
|
+
// Formatting mutations (runtime.formatting.apply)
|
|
58
|
+
| "applyFormattingOperation"
|
|
56
59
|
// Review (runtime.review family)
|
|
57
60
|
| "getReviewWorkSnapshot"
|
|
58
61
|
| "getSuggestionsSnapshot"
|
|
59
62
|
| "acceptChange"
|
|
60
63
|
| "rejectChange"
|
|
61
64
|
| "resolveComment"
|
|
65
|
+
| "getCommentThreadForChange"
|
|
66
|
+
| "ensureCommentThreadForChange"
|
|
67
|
+
| "addReplyToChange"
|
|
62
68
|
// Workflow (runtime.workflow + ai.inspect families)
|
|
63
69
|
| "queryScopes"
|
|
64
70
|
| "getWorkflowMarkupSnapshot"
|
|
@@ -70,6 +76,8 @@ export type RuntimeApiHandle = Pick<
|
|
|
70
76
|
// live seam. `getWorkflowMetadataSnapshot` is the read side the
|
|
71
77
|
// metadata writer inspects before merging its entry.
|
|
72
78
|
| "addScope"
|
|
79
|
+
| "setScopeGuardPolicy"
|
|
80
|
+
| "getScopeGuardPolicy"
|
|
73
81
|
| "setWorkflowMetadataEntries"
|
|
74
82
|
| "getWorkflowMetadataSnapshot"
|
|
75
83
|
// W10 overlay-visibility policy (state-classes X1). Class-A canonical
|
|
@@ -145,16 +153,23 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
|
|
|
145
153
|
getRenderSnapshot: true,
|
|
146
154
|
getCanonicalDocument: true,
|
|
147
155
|
findAllText: true,
|
|
156
|
+
replaceText: true,
|
|
157
|
+
applyFormattingOperation: true,
|
|
148
158
|
getReviewWorkSnapshot: true,
|
|
149
159
|
getSuggestionsSnapshot: true,
|
|
150
160
|
acceptChange: true,
|
|
151
161
|
rejectChange: true,
|
|
152
162
|
resolveComment: true,
|
|
163
|
+
getCommentThreadForChange: true,
|
|
164
|
+
ensureCommentThreadForChange: true,
|
|
165
|
+
addReplyToChange: true,
|
|
153
166
|
queryScopes: true,
|
|
154
167
|
getWorkflowMarkupSnapshot: true,
|
|
155
168
|
getInteractionGuardSnapshot: true,
|
|
156
169
|
getWorkflowOverlay: true,
|
|
157
170
|
addScope: true,
|
|
171
|
+
setScopeGuardPolicy: true,
|
|
172
|
+
getScopeGuardPolicy: true,
|
|
158
173
|
setWorkflowMetadataEntries: true,
|
|
159
174
|
getWorkflowMetadataSnapshot: true,
|
|
160
175
|
getVisibilityPolicy: true,
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
14
14
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
CanonicalDocumentFragment,
|
|
17
|
+
EditorAnchorProjection,
|
|
18
|
+
SurfaceBlockSnapshot,
|
|
19
|
+
SurfaceInlineSegment,
|
|
20
|
+
} from "../../public-types.ts";
|
|
16
21
|
import { emitUxResponse } from "../_ux-response.ts";
|
|
17
22
|
import { createScopeCompilerService } from "../../../runtime/scopes/index.ts";
|
|
18
23
|
|
|
@@ -179,6 +184,18 @@ export function createContentFamily(runtime: RuntimeApiHandle) {
|
|
|
179
184
|
// Layer-08 compiler-service facade. Same pipeline as
|
|
180
185
|
// ai.applyReplacementScope; the host/UI path omits actionId (not
|
|
181
186
|
// an agent action) and tags origin:"ui" + actorId:"user".
|
|
187
|
+
const tableCellResult = replaceTableCellText(runtime, input);
|
|
188
|
+
if (tableCellResult) {
|
|
189
|
+
emitUxResponse(runtime, {
|
|
190
|
+
apiFn: replaceTextMetadata.name,
|
|
191
|
+
intent: replaceTextMetadata.uxIntent.expectedDelta ?? "",
|
|
192
|
+
mockOrLive: "live",
|
|
193
|
+
uiVisible: true,
|
|
194
|
+
expectedDelta: replaceTextMetadata.uxIntent.expectedDelta,
|
|
195
|
+
});
|
|
196
|
+
return tableCellResult;
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
const result = compiler.applyReplacement({
|
|
183
200
|
targetScopeId: input.scopeId,
|
|
184
201
|
operation: "replace",
|
|
@@ -234,3 +251,133 @@ export function createContentFamily(runtime: RuntimeApiHandle) {
|
|
|
234
251
|
},
|
|
235
252
|
};
|
|
236
253
|
}
|
|
254
|
+
|
|
255
|
+
function replaceTableCellText(
|
|
256
|
+
runtime: RuntimeApiHandle,
|
|
257
|
+
input: ReplaceTextInput,
|
|
258
|
+
): ReplaceTextResult | null {
|
|
259
|
+
const parsed = parseTableCellScopeId(input.scopeId);
|
|
260
|
+
if (!parsed) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
265
|
+
if (!snapshot.isReady || snapshot.readOnly || snapshot.fatalError) {
|
|
266
|
+
return { applied: false, reason: "document-not-editable" };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const document = runtime.getCanonicalDocument();
|
|
270
|
+
const rootChild = document.content.children[parsed.blockIndex];
|
|
271
|
+
if (!rootChild || rootChild.type !== "table") {
|
|
272
|
+
return { applied: false, reason: "table-cell-scope-not-resolvable" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tableOrdinal = document.content.children
|
|
276
|
+
.slice(0, parsed.blockIndex)
|
|
277
|
+
.filter((block) => block.type === "table").length;
|
|
278
|
+
const tableBlock =
|
|
279
|
+
snapshot.surface?.blocks.find(
|
|
280
|
+
(block): block is Extract<SurfaceBlockSnapshot, { kind: "table" }> =>
|
|
281
|
+
block.kind === "table" && block.blockId === `table-${tableOrdinal}`,
|
|
282
|
+
) ??
|
|
283
|
+
(snapshot.surface?.blocks[parsed.blockIndex]?.kind === "table"
|
|
284
|
+
? snapshot.surface.blocks[parsed.blockIndex]
|
|
285
|
+
: null);
|
|
286
|
+
if (!tableBlock || tableBlock.kind !== "table") {
|
|
287
|
+
return { applied: false, reason: "table-cell-surface-not-realized" };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const cell = tableBlock.rows[parsed.rowIndex]?.cells[parsed.cellIndex];
|
|
291
|
+
if (!cell) {
|
|
292
|
+
return { applied: false, reason: "table-cell-scope-not-resolvable" };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const range = resolveTableCellTextRange(cell.content);
|
|
296
|
+
if (range.status === "empty") {
|
|
297
|
+
return { applied: false, reason: "table-cell-empty-text" };
|
|
298
|
+
}
|
|
299
|
+
if (range.status === "ambiguous") {
|
|
300
|
+
return { applied: false, reason: "table-cell-text-scope-ambiguous" };
|
|
301
|
+
}
|
|
302
|
+
const { from, to } = range;
|
|
303
|
+
|
|
304
|
+
const anchor: EditorAnchorProjection = {
|
|
305
|
+
kind: "range",
|
|
306
|
+
from,
|
|
307
|
+
to,
|
|
308
|
+
assoc: { start: -1, end: 1 },
|
|
309
|
+
};
|
|
310
|
+
const beforeRevisionToken = snapshot.revisionToken;
|
|
311
|
+
runtime.replaceText(input.replacement, anchor);
|
|
312
|
+
const afterRevisionToken = runtime.getRenderSnapshot().revisionToken;
|
|
313
|
+
if (afterRevisionToken === beforeRevisionToken) {
|
|
314
|
+
return { applied: false, reason: "replace-text-not-applied" };
|
|
315
|
+
}
|
|
316
|
+
return { applied: true };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseTableCellScopeId(
|
|
320
|
+
scopeId: string,
|
|
321
|
+
): { blockIndex: number; rowIndex: number; cellIndex: number } | null {
|
|
322
|
+
const match = /^cell:(\d+):(\d+):(\d+)$/u.exec(scopeId);
|
|
323
|
+
if (!match) return null;
|
|
324
|
+
return {
|
|
325
|
+
blockIndex: Number(match[1]),
|
|
326
|
+
rowIndex: Number(match[2]),
|
|
327
|
+
cellIndex: Number(match[3]),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
type TableCellTextRangeResult =
|
|
332
|
+
| { status: "ok"; from: number; to: number }
|
|
333
|
+
| { status: "empty" }
|
|
334
|
+
| { status: "ambiguous" };
|
|
335
|
+
|
|
336
|
+
function resolveTableCellTextRange(
|
|
337
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
338
|
+
): TableCellTextRangeResult {
|
|
339
|
+
const paragraphRanges = collectTableCellParagraphTextRanges(blocks);
|
|
340
|
+
if (paragraphRanges.length === 0) {
|
|
341
|
+
return { status: "empty" };
|
|
342
|
+
}
|
|
343
|
+
if (paragraphRanges.length > 1) {
|
|
344
|
+
return { status: "ambiguous" };
|
|
345
|
+
}
|
|
346
|
+
const [range] = paragraphRanges;
|
|
347
|
+
return range && range.from < range.to
|
|
348
|
+
? { status: "ok", from: range.from, to: range.to }
|
|
349
|
+
: { status: "empty" };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function collectTableCellParagraphTextRanges(
|
|
353
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
354
|
+
output: Array<{ from: number; to: number }> = [],
|
|
355
|
+
): Array<{ from: number; to: number }> {
|
|
356
|
+
for (const block of blocks) {
|
|
357
|
+
if (block.kind === "paragraph") {
|
|
358
|
+
const textSegments = block.segments.filter(
|
|
359
|
+
(segment): segment is Extract<SurfaceInlineSegment, { kind: "text" }> =>
|
|
360
|
+
segment.kind === "text" && segment.from < segment.to,
|
|
361
|
+
);
|
|
362
|
+
if (textSegments.length > 0) {
|
|
363
|
+
output.push({
|
|
364
|
+
from: Math.min(...textSegments.map((segment) => segment.from)),
|
|
365
|
+
to: Math.max(...textSegments.map((segment) => segment.to)),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (block.kind === "table") {
|
|
371
|
+
for (const row of block.rows) {
|
|
372
|
+
for (const cell of row.cells) {
|
|
373
|
+
collectTableCellParagraphTextRanges(cell.content, output);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (block.kind === "sdt_block") {
|
|
379
|
+
collectTableCellParagraphTextRanges(block.children, output);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return output;
|
|
383
|
+
}
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
15
15
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
16
|
+
import { emitUxResponse } from "../_ux-response.ts";
|
|
16
17
|
import {
|
|
17
18
|
resolveEffectiveFormatting,
|
|
18
19
|
createFormattingContext,
|
|
19
20
|
type EffectiveFormatting,
|
|
20
21
|
type RunProvenance,
|
|
21
22
|
} from "../../../runtime/formatting/index.ts";
|
|
23
|
+
import type { FormattingOperation } from "../../../core/commands/formatting-commands.ts";
|
|
22
24
|
import {
|
|
23
25
|
findParagraphByBlockId,
|
|
24
26
|
resolveDirectRunFormattingAtSegment,
|
|
@@ -132,8 +134,47 @@ export const resolveRunWithProvenanceMetadata: ApiV3FnMetadata = {
|
|
|
132
134
|
"§Runtime API § runtime.formatting.resolveRunWithProvenance. Agent-facing provenance view — `source ∈ {direct, characterStyle, style, docDefaults}` plus `sourceId` for style tiers. Substrate for ai.explainFormatting (L09).",
|
|
133
135
|
};
|
|
134
136
|
|
|
137
|
+
export const applyMetadata: ApiV3FnMetadata = {
|
|
138
|
+
name: "runtime.formatting.apply",
|
|
139
|
+
status: "live",
|
|
140
|
+
sourceLayer: "runtime-core",
|
|
141
|
+
liveEvidence: {
|
|
142
|
+
runnerTest: "test/api/v3/runtime/formatting-adapter.test.ts",
|
|
143
|
+
commit: "refactor-03-tracked-changes-v1-formatting-apply-2026-04-23",
|
|
144
|
+
},
|
|
145
|
+
stateClass: "A-canonical",
|
|
146
|
+
persistsTo: "canonical",
|
|
147
|
+
broadcastsVia: "crdt",
|
|
148
|
+
uxIntent: {
|
|
149
|
+
uiVisible: true,
|
|
150
|
+
expectsUxResponse: "inline-change",
|
|
151
|
+
expectedDelta: "formatting changes in the active selection",
|
|
152
|
+
},
|
|
153
|
+
agentMetadata: {
|
|
154
|
+
readOrMutate: "mutate",
|
|
155
|
+
boundedScope: "selection",
|
|
156
|
+
auditCategory: "formatting-write",
|
|
157
|
+
},
|
|
158
|
+
rwdReference:
|
|
159
|
+
"§Runtime API § runtime.formatting.apply. Live adapter over DocumentRuntime.applyFormattingOperation(); supports direct formatting/paragraph alignment/indentation and authors bounded property-change suggestions when the effective mode is suggesting. Style suggestions are intentionally not accepted by this operation shape.",
|
|
160
|
+
};
|
|
161
|
+
|
|
135
162
|
export function createFormattingFamily(runtime: RuntimeApiHandle) {
|
|
136
163
|
return {
|
|
164
|
+
apply(operation: FormattingOperation): void {
|
|
165
|
+
// @endStateApi — live. Delegates to the runtime-owned mutation seam
|
|
166
|
+
// so direct edits, workflow blocking, and suggesting-mode property-
|
|
167
|
+
// change authorship all share one implementation.
|
|
168
|
+
runtime.applyFormattingOperation(operation);
|
|
169
|
+
emitUxResponse(runtime, {
|
|
170
|
+
apiFn: applyMetadata.name,
|
|
171
|
+
intent: applyMetadata.uxIntent.expectedDelta ?? "",
|
|
172
|
+
mockOrLive: "live",
|
|
173
|
+
uiVisible: true,
|
|
174
|
+
expectedDelta: applyMetadata.uxIntent.expectedDelta,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
|
|
137
178
|
getEffective(
|
|
138
179
|
nodeRef: FormattingNodeRef,
|
|
139
180
|
opts?: FormattingGetEffectiveOpts,
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
9
9
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
10
10
|
import type {
|
|
11
|
+
AddCommentReplyResult,
|
|
12
|
+
AddCommentResult,
|
|
11
13
|
CommentSidebarThreadSnapshot,
|
|
12
14
|
SuggestionsSnapshot,
|
|
13
15
|
TrackedChangeEntrySnapshot,
|
|
@@ -120,6 +122,56 @@ export const resolveCommentMetadata: ApiV3FnMetadata = {
|
|
|
120
122
|
rwdReference: "§Runtime API § runtime.review.resolveComment",
|
|
121
123
|
};
|
|
122
124
|
|
|
125
|
+
export const getCommentThreadForChangeMetadata: ApiV3FnMetadata = {
|
|
126
|
+
name: "runtime.review.getCommentThreadForChange",
|
|
127
|
+
status: "live",
|
|
128
|
+
sourceLayer: "workflow-review",
|
|
129
|
+
liveEvidence: {
|
|
130
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
131
|
+
commit: "refactor-03-redline-revision-thread-link",
|
|
132
|
+
},
|
|
133
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
134
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "scope", auditCategory: "review-read" },
|
|
135
|
+
stateClass: "A-canonical",
|
|
136
|
+
persistsTo: "canonical",
|
|
137
|
+
rwdReference:
|
|
138
|
+
"§Runtime API § runtime.review.getCommentThreadForChange. Live adapter over runtime linked revision comment threads.",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const ensureCommentThreadForChangeMetadata: ApiV3FnMetadata = {
|
|
142
|
+
name: "runtime.review.ensureCommentThreadForChange",
|
|
143
|
+
status: "live",
|
|
144
|
+
sourceLayer: "workflow-review",
|
|
145
|
+
liveEvidence: {
|
|
146
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
147
|
+
commit: "refactor-03-redline-revision-thread-link",
|
|
148
|
+
},
|
|
149
|
+
uxIntent: { uiVisible: true, expectsUxResponse: "inline-change", expectedDelta: "comment thread opens for tracked change" },
|
|
150
|
+
agentMetadata: { readOrMutate: "mutate", boundedScope: "scope", auditCategory: "comment-add" },
|
|
151
|
+
stateClass: "A-canonical",
|
|
152
|
+
persistsTo: "canonical",
|
|
153
|
+
broadcastsVia: "crdt",
|
|
154
|
+
rwdReference:
|
|
155
|
+
"§Runtime API § runtime.review.ensureCommentThreadForChange. Creates or returns the comment thread attached to a tracked change.",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const addReplyToChangeMetadata: ApiV3FnMetadata = {
|
|
159
|
+
name: "runtime.review.addReplyToChange",
|
|
160
|
+
status: "live",
|
|
161
|
+
sourceLayer: "workflow-review",
|
|
162
|
+
liveEvidence: {
|
|
163
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
164
|
+
commit: "refactor-03-redline-revision-thread-link",
|
|
165
|
+
},
|
|
166
|
+
uxIntent: { uiVisible: true, expectsUxResponse: "inline-change", expectedDelta: "reply appears on tracked-change thread" },
|
|
167
|
+
agentMetadata: { readOrMutate: "mutate", boundedScope: "scope", auditCategory: "comment-reply" },
|
|
168
|
+
stateClass: "A-canonical",
|
|
169
|
+
persistsTo: "canonical",
|
|
170
|
+
broadcastsVia: "crdt",
|
|
171
|
+
rwdReference:
|
|
172
|
+
"§Runtime API § runtime.review.addReplyToChange. Appends a reply to the comment thread linked to a tracked change.",
|
|
173
|
+
};
|
|
174
|
+
|
|
123
175
|
export function createReviewFamily(runtime: RuntimeApiHandle) {
|
|
124
176
|
return {
|
|
125
177
|
getComments(): readonly CommentSidebarThreadSnapshot[] {
|
|
@@ -140,6 +192,52 @@ export function createReviewFamily(runtime: RuntimeApiHandle) {
|
|
|
140
192
|
return runtime.getSuggestionsSnapshot();
|
|
141
193
|
},
|
|
142
194
|
|
|
195
|
+
getCommentThreadForChange(changeId: string): CommentSidebarThreadSnapshot | null {
|
|
196
|
+
// @endStateApi — live. Reads the comment thread linked to a tracked
|
|
197
|
+
// change from the canonical comment projection.
|
|
198
|
+
return runtime.getCommentThreadForChange(changeId);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
ensureCommentThreadForChange(changeId: string): AddCommentResult | null {
|
|
202
|
+
// @endStateApi — live. Ensures a canonical comment thread exists for
|
|
203
|
+
// a tracked change and emits an inline UX response when created/found.
|
|
204
|
+
const result = runtime.ensureCommentThreadForChange(changeId);
|
|
205
|
+
if (result) {
|
|
206
|
+
emitUxResponse(runtime, {
|
|
207
|
+
apiFn: ensureCommentThreadForChangeMetadata.name,
|
|
208
|
+
intent: ensureCommentThreadForChangeMetadata.uxIntent.expectedDelta ?? "",
|
|
209
|
+
mockOrLive: "live",
|
|
210
|
+
uiVisible: true,
|
|
211
|
+
expectedDelta: ensureCommentThreadForChangeMetadata.uxIntent.expectedDelta,
|
|
212
|
+
actualDelta: {
|
|
213
|
+
kind: "inline-change",
|
|
214
|
+
payload: { changeId, commentId: result.commentId },
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
addReplyToChange(changeId: string, body: string): AddCommentReplyResult | null {
|
|
222
|
+
// @endStateApi — live. Appends a canonical comment reply to the
|
|
223
|
+
// thread linked to a tracked change.
|
|
224
|
+
const result = runtime.addReplyToChange(changeId, body);
|
|
225
|
+
if (result) {
|
|
226
|
+
emitUxResponse(runtime, {
|
|
227
|
+
apiFn: addReplyToChangeMetadata.name,
|
|
228
|
+
intent: addReplyToChangeMetadata.uxIntent.expectedDelta ?? "",
|
|
229
|
+
mockOrLive: "live",
|
|
230
|
+
uiVisible: true,
|
|
231
|
+
expectedDelta: addReplyToChangeMetadata.uxIntent.expectedDelta,
|
|
232
|
+
actualDelta: {
|
|
233
|
+
kind: "inline-change",
|
|
234
|
+
payload: { changeId, commentId: result.commentId, entryId: result.entryId },
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
},
|
|
240
|
+
|
|
143
241
|
acceptChange(changeId: string): void {
|
|
144
242
|
// @endStateApi — live. Delegates.
|
|
145
243
|
runtime.acceptChange(changeId);
|