@beyondwork/docx-react-component 1.0.84 → 1.0.86
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 +38 -0
- package/src/api/v3/_runtime-handle.ts +11 -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/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- 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/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +677 -54
- 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/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +8 -0
- package/src/ui/WordReviewEditor.tsx +192 -35
- package/src/ui/editor-command-bag.ts +7 -1
- package/src/ui/editor-shell-view.tsx +1 -0
- 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/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- 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/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +46 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +7 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -4
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.86",
|
|
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
|
/**
|
|
@@ -4431,6 +4447,9 @@ export interface WordReviewEditorChangesFacet {
|
|
|
4431
4447
|
rejectAll(): void;
|
|
4432
4448
|
acceptGroup(groupId: string): void;
|
|
4433
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;
|
|
4434
4453
|
scrollTo(revisionId: string): void;
|
|
4435
4454
|
get(): TrackedChangesSnapshot;
|
|
4436
4455
|
getSuggestions(): SuggestionsSnapshot;
|
|
@@ -4578,6 +4597,25 @@ export interface WordReviewEditorRef {
|
|
|
4578
4597
|
commentId: string,
|
|
4579
4598
|
body: string,
|
|
4580
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;
|
|
4581
4619
|
editCommentBody(commentId: string, body: string): void;
|
|
4582
4620
|
deleteComment(commentId: string): void;
|
|
4583
4621
|
/**
|
|
@@ -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"
|
|
@@ -147,11 +153,16 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
|
|
|
147
153
|
getRenderSnapshot: true,
|
|
148
154
|
getCanonicalDocument: true,
|
|
149
155
|
findAllText: true,
|
|
156
|
+
replaceText: true,
|
|
157
|
+
applyFormattingOperation: true,
|
|
150
158
|
getReviewWorkSnapshot: true,
|
|
151
159
|
getSuggestionsSnapshot: true,
|
|
152
160
|
acceptChange: true,
|
|
153
161
|
rejectChange: true,
|
|
154
162
|
resolveComment: true,
|
|
163
|
+
getCommentThreadForChange: true,
|
|
164
|
+
ensureCommentThreadForChange: true,
|
|
165
|
+
addReplyToChange: true,
|
|
155
166
|
queryScopes: true,
|
|
156
167
|
getWorkflowMarkupSnapshot: true,
|
|
157
168
|
getInteractionGuardSnapshot: 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);
|
|
@@ -58,7 +58,9 @@ import {
|
|
|
58
58
|
} from "./text-commands.ts";
|
|
59
59
|
import type {
|
|
60
60
|
BlockNode,
|
|
61
|
+
InlineNode,
|
|
61
62
|
MutableCanonicalDocument,
|
|
63
|
+
ParagraphNode,
|
|
62
64
|
RevisionRecord as CanonicalRevisionRecord,
|
|
63
65
|
} from "../../model/canonical-document.ts";
|
|
64
66
|
import { remapCommentThreads } from "../../review/store/comment-remapping.ts";
|
|
@@ -2314,39 +2316,93 @@ function isSingleParagraphSuggestingRange(
|
|
|
2314
2316
|
to: number,
|
|
2315
2317
|
): boolean {
|
|
2316
2318
|
const ranges: Array<{ start: number; end: number }> = [];
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
+
collectSuggestingParagraphRanges(
|
|
2320
|
+
document.content.children,
|
|
2321
|
+
0,
|
|
2322
|
+
ranges,
|
|
2323
|
+
true,
|
|
2324
|
+
);
|
|
2325
|
+
|
|
2326
|
+
return ranges.some((range) => from >= range.start && to <= range.end);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function collectSuggestingParagraphRanges(
|
|
2330
|
+
blocks: readonly BlockNode[],
|
|
2331
|
+
startCursor: number,
|
|
2332
|
+
output: Array<{ start: number; end: number }>,
|
|
2333
|
+
addRootParagraphBoundaries: boolean,
|
|
2334
|
+
): number {
|
|
2335
|
+
let cursor = startCursor;
|
|
2336
|
+
|
|
2337
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
2338
|
+
const block = blocks[index];
|
|
2339
|
+
if (!block) {
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2319
2342
|
|
|
2320
|
-
for (const block of document.content.children) {
|
|
2321
2343
|
if (block.type === "paragraph") {
|
|
2322
|
-
if (previousWasParagraph) {
|
|
2323
|
-
cursor += 1;
|
|
2324
|
-
}
|
|
2325
2344
|
const start = cursor;
|
|
2326
|
-
cursor += block
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2345
|
+
cursor += paragraphLogicalLength(block);
|
|
2346
|
+
output.push({ start, end: cursor });
|
|
2347
|
+
} else if (block.type === "table") {
|
|
2348
|
+
for (const row of block.rows) {
|
|
2349
|
+
for (const cell of row.cells) {
|
|
2350
|
+
cursor = collectSuggestingParagraphRanges(
|
|
2351
|
+
cell.children,
|
|
2352
|
+
cursor,
|
|
2353
|
+
output,
|
|
2354
|
+
false,
|
|
2355
|
+
);
|
|
2337
2356
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2357
|
+
}
|
|
2358
|
+
} else if (block.type === "sdt") {
|
|
2359
|
+
cursor = collectSuggestingParagraphRanges(
|
|
2360
|
+
block.children,
|
|
2361
|
+
cursor,
|
|
2362
|
+
output,
|
|
2363
|
+
false,
|
|
2364
|
+
);
|
|
2365
|
+
} else {
|
|
2366
|
+
cursor += 1;
|
|
2343
2367
|
}
|
|
2344
2368
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2369
|
+
if (
|
|
2370
|
+
addRootParagraphBoundaries &&
|
|
2371
|
+
index < blocks.length - 1 &&
|
|
2372
|
+
blocks[index + 1]?.type === "paragraph"
|
|
2373
|
+
) {
|
|
2374
|
+
cursor += 1;
|
|
2375
|
+
}
|
|
2347
2376
|
}
|
|
2348
2377
|
|
|
2349
|
-
return
|
|
2378
|
+
return cursor;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function paragraphLogicalLength(paragraph: ParagraphNode): number {
|
|
2382
|
+
return paragraph.children.reduce<number>(
|
|
2383
|
+
(size, child) => size + inlineLogicalLength(child),
|
|
2384
|
+
0,
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function inlineLogicalLength(node: InlineNode): number {
|
|
2389
|
+
switch (node.type) {
|
|
2390
|
+
case "text":
|
|
2391
|
+
return node.text.length;
|
|
2392
|
+
case "hyperlink":
|
|
2393
|
+
case "field":
|
|
2394
|
+
return node.children.reduce<number>(
|
|
2395
|
+
(size, child) => size + inlineLogicalLength(child as InlineNode),
|
|
2396
|
+
0,
|
|
2397
|
+
);
|
|
2398
|
+
case "bookmark_start":
|
|
2399
|
+
case "bookmark_end":
|
|
2400
|
+
case "scope_marker_start":
|
|
2401
|
+
case "scope_marker_end":
|
|
2402
|
+
return 0;
|
|
2403
|
+
default:
|
|
2404
|
+
return 1;
|
|
2405
|
+
}
|
|
2350
2406
|
}
|
|
2351
2407
|
|
|
2352
2408
|
function applySuggestingInsert(
|
|
@@ -839,6 +839,21 @@ export function normalizeCommentThreadRecord(value: unknown): CommentThreadRecor
|
|
|
839
839
|
typeof record.metadata.rootParaId === "string"
|
|
840
840
|
? record.metadata.rootParaId
|
|
841
841
|
: undefined,
|
|
842
|
+
linkedRevisionId:
|
|
843
|
+
typeof record.metadata.linkedRevisionId === "string"
|
|
844
|
+
? record.metadata.linkedRevisionId
|
|
845
|
+
: undefined,
|
|
846
|
+
detachedReason:
|
|
847
|
+
record.metadata.detachedReason === "incomplete-markers" ||
|
|
848
|
+
record.metadata.detachedReason === "multi-paragraph" ||
|
|
849
|
+
record.metadata.detachedReason === "opaque-region" ||
|
|
850
|
+
record.metadata.detachedReason === "revision-overlap"
|
|
851
|
+
? record.metadata.detachedReason
|
|
852
|
+
: undefined,
|
|
853
|
+
actionabilityNote:
|
|
854
|
+
typeof record.metadata.actionabilityNote === "string"
|
|
855
|
+
? record.metadata.actionabilityNote
|
|
856
|
+
: undefined,
|
|
842
857
|
}
|
|
843
858
|
: undefined,
|
|
844
859
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HeaderFooterVariant } from "../../model/canonical-document.ts";
|
|
2
|
+
|
|
3
|
+
interface HeaderFooterReferenceElement {
|
|
4
|
+
attributes: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ParsedHeaderFooterReferenceAttributes {
|
|
8
|
+
variant: HeaderFooterVariant;
|
|
9
|
+
relationshipId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readHeaderFooterReferenceAttributes(
|
|
13
|
+
element: HeaderFooterReferenceElement,
|
|
14
|
+
): ParsedHeaderFooterReferenceAttributes | undefined {
|
|
15
|
+
const relationshipId =
|
|
16
|
+
element.attributes["r:id"] ??
|
|
17
|
+
element.attributes["r:Id"] ??
|
|
18
|
+
element.attributes.id ??
|
|
19
|
+
element.attributes.Id;
|
|
20
|
+
if (!relationshipId) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
variant: toHeaderFooterVariant(element.attributes["w:type"] ?? element.attributes.type),
|
|
26
|
+
relationshipId,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toHeaderFooterVariant(raw: string | undefined): HeaderFooterVariant {
|
|
31
|
+
if (raw === "first") {
|
|
32
|
+
return "first";
|
|
33
|
+
}
|
|
34
|
+
if (raw === "even") {
|
|
35
|
+
return "even";
|
|
36
|
+
}
|
|
37
|
+
return "default";
|
|
38
|
+
}
|