@beyondwork/docx-react-component 1.0.87 → 1.0.89
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/v3/_runtime-handle.ts +5 -0
- package/src/api/v3/ai/replacement.ts +82 -0
- package/src/api/v3/runtime/content.ts +3 -0
- package/src/api/v3/runtime/formatting.ts +64 -0
- package/src/core/commands/formatting-commands.ts +107 -0
- package/src/core/state/text-transaction.ts +11 -4
- package/src/runtime/document-runtime.ts +293 -27
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +12 -3
- package/src/runtime/scopes/audit-bundle.ts +2 -2
- package/src/runtime/scopes/compiler-service.ts +70 -0
- package/src/runtime/scopes/formatting/apply.ts +262 -0
- package/src/runtime/scopes/index.ts +12 -0
- package/src/runtime/scopes/replacement/propose.ts +2 -0
- package/src/runtime/scopes/scope-kinds/paragraph.ts +1 -0
- package/src/runtime/scopes/semantic-scope-types.ts +48 -4
- package/src/runtime/scopes/workflow-overlap.ts +9 -11
- package/src/shell/session-bootstrap.ts +1 -0
- package/src/ui/WordReviewEditor.tsx +277 -28
- package/src/ui/editor-command-bag.ts +11 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/headless/chrome-registry.ts +6 -6
- package/src/ui/headless/role-action-sets.ts +4 -10
- package/src/ui/headless/selection-tool-resolver.ts +11 -0
- package/src/ui-tailwind/chrome/editor-action-registry.ts +1 -1
- package/src/ui-tailwind/chrome/tw-context-band.tsx +7 -7
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +13 -18
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +8 -5
- package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +100 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +0 -40
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +9 -7
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +17 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +6 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +17 -7
- package/src/ui-tailwind/editor-surface/preserve-position.ts +61 -11
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +20 -5
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +52 -6
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +5 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +5 -0
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +32 -38
- package/src/ui-tailwind/review-workspace/types.ts +2 -0
- package/src/ui-tailwind/theme/editor-theme.css +25 -12
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +13 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +6 -15
- package/src/ui-tailwind/tw-review-workspace.tsx +28 -18
- package/src/ui-tailwind/workflow-scope-layers.ts +70 -0
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
MAIN_STORY_TARGET,
|
|
125
125
|
storyTargetsEqual,
|
|
126
126
|
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
127
|
+
type TransactionMapping,
|
|
127
128
|
} from "../core/selection/mapping.ts";
|
|
128
129
|
import {
|
|
129
130
|
toInternalAnchorProjection,
|
|
@@ -190,6 +191,7 @@ import {
|
|
|
190
191
|
} from "../core/commands/add-scope.ts";
|
|
191
192
|
import {
|
|
192
193
|
applyFormattingOperationToDocument,
|
|
194
|
+
applyTextMarkOperationToDocumentRange,
|
|
193
195
|
type FormattingOperation,
|
|
194
196
|
} from "../core/commands/formatting-commands.ts";
|
|
195
197
|
import { resolveActiveParagraphIndex } from "../core/commands/paragraph-layout-commands.ts";
|
|
@@ -449,6 +451,12 @@ export interface DocumentRuntime {
|
|
|
449
451
|
* UI paths.
|
|
450
452
|
*/
|
|
451
453
|
applyScopeReplacement(plan: RuntimeOperationPlan): void;
|
|
454
|
+
/**
|
|
455
|
+
* Layer-08 scoped formatting dispatch. Returns true when at least one
|
|
456
|
+
* document.replace transaction was committed, which lets the compiler
|
|
457
|
+
* suppress no-op audits.
|
|
458
|
+
*/
|
|
459
|
+
applyScopeFormatting(plan: RuntimeOperationPlan): boolean;
|
|
452
460
|
dispatch(command: EditorCommand): void;
|
|
453
461
|
/**
|
|
454
462
|
* Apply a command received from a remote collaborator. The command
|
|
@@ -1174,6 +1182,8 @@ export function createDocumentRuntime(
|
|
|
1174
1182
|
// to a single microtask-deferred emit. Declared early because emit() (which
|
|
1175
1183
|
// checks this flag) runs during construction.
|
|
1176
1184
|
let analyticsEmitScheduled = false;
|
|
1185
|
+
let analyticsEmitScheduleMode: "none" | "microtask" | "idle" = "none";
|
|
1186
|
+
let deferNextContextAnalyticsEmit = false;
|
|
1177
1187
|
|
|
1178
1188
|
// V6c — heading fingerprint for TOC auto-invalidation. Compared in notify();
|
|
1179
1189
|
// a mismatch schedules a microtask refresh of TOC fields.
|
|
@@ -1380,6 +1390,8 @@ export function createDocumentRuntime(
|
|
|
1380
1390
|
let viewportBlockRanges: readonly { start: number; end: number }[] | null = null;
|
|
1381
1391
|
/** Serialized fingerprint of the active ranges — used as the idempotency key for `refreshSurfaceOnly`. */
|
|
1382
1392
|
let viewportRangesKey: string = serializeViewportRanges(null);
|
|
1393
|
+
const EDITING_CORRIDOR_BLOCK_RADIUS = 8;
|
|
1394
|
+
const EDITING_CORRIDOR_MIN_BLOCKS = 24;
|
|
1383
1395
|
|
|
1384
1396
|
function applyViewportRanges(
|
|
1385
1397
|
incoming: readonly { start: number; end: number }[] | null,
|
|
@@ -1567,19 +1579,35 @@ export function createDocumentRuntime(
|
|
|
1567
1579
|
function getCachedSurface(
|
|
1568
1580
|
document: CanonicalDocumentEnvelope,
|
|
1569
1581
|
nextActiveStory: EditorStoryTarget,
|
|
1582
|
+
options: {
|
|
1583
|
+
viewportBlockRangesOverride?: readonly { start: number; end: number }[] | null;
|
|
1584
|
+
enrichCulledPlaceholders?: boolean;
|
|
1585
|
+
} = {},
|
|
1570
1586
|
): RuntimeRenderSnapshot["surface"] {
|
|
1571
1587
|
const activeStoryKey = storyTargetKey(nextActiveStory);
|
|
1588
|
+
const surfaceViewportRanges =
|
|
1589
|
+
"viewportBlockRangesOverride" in options
|
|
1590
|
+
? options.viewportBlockRangesOverride ?? null
|
|
1591
|
+
: viewportBlockRanges;
|
|
1592
|
+
const surfaceViewportRangesKey =
|
|
1593
|
+
"viewportBlockRangesOverride" in options
|
|
1594
|
+
? serializeViewportRanges(surfaceViewportRanges)
|
|
1595
|
+
: viewportRangesKey;
|
|
1596
|
+
const surfaceCacheKey =
|
|
1597
|
+
options.enrichCulledPlaceholders === false
|
|
1598
|
+
? `${surfaceViewportRangesKey}|raw-placeholders`
|
|
1599
|
+
: surfaceViewportRangesKey;
|
|
1572
1600
|
if (
|
|
1573
1601
|
cachedSurface &&
|
|
1574
1602
|
cachedSurface.revisionToken === state.revisionToken &&
|
|
1575
1603
|
cachedSurface.activeStoryKey === activeStoryKey &&
|
|
1576
|
-
cachedSurface.viewportRangesKey ===
|
|
1604
|
+
cachedSurface.viewportRangesKey === surfaceCacheKey
|
|
1577
1605
|
) {
|
|
1578
1606
|
return cachedSurface.snapshot;
|
|
1579
1607
|
}
|
|
1580
1608
|
|
|
1581
1609
|
const snapshot = createEditorSurfaceSnapshot(document, state.selection, nextActiveStory, {
|
|
1582
|
-
viewportBlockRanges,
|
|
1610
|
+
viewportBlockRanges: surfaceViewportRanges,
|
|
1583
1611
|
...(effectiveMarkupModeProvider
|
|
1584
1612
|
? { getEffectiveMarkupMode: effectiveMarkupModeProvider }
|
|
1585
1613
|
: {}),
|
|
@@ -1605,20 +1633,57 @@ export function createDocumentRuntime(
|
|
|
1605
1633
|
//
|
|
1606
1634
|
// No-op on pre-pagination calls (engine not yet ready → empty map)
|
|
1607
1635
|
// and on the inert facet.
|
|
1608
|
-
const enrichedSnapshot =
|
|
1636
|
+
const enrichedSnapshot =
|
|
1637
|
+
options.enrichCulledPlaceholders === false
|
|
1638
|
+
? snapshot
|
|
1639
|
+
: enrichCulledPlaceholdersWithHeights(snapshot);
|
|
1609
1640
|
cachedSurface = {
|
|
1610
1641
|
revisionToken: state.revisionToken,
|
|
1611
1642
|
activeStoryKey,
|
|
1612
|
-
viewportRangesKey,
|
|
1643
|
+
viewportRangesKey: surfaceCacheKey,
|
|
1613
1644
|
snapshot: enrichedSnapshot,
|
|
1614
1645
|
};
|
|
1615
1646
|
// Keep the scroll-path fingerprint in lockstep so a subsequent
|
|
1616
1647
|
// `maybeRefreshSurfaceForViewport` sees the freshly-built snapshot
|
|
1617
1648
|
// and short-circuits instead of paying a redundant projection.
|
|
1618
|
-
cachedSurfaceFingerprint = `${state.revisionToken}|${activeStoryKey}|${
|
|
1649
|
+
cachedSurfaceFingerprint = `${state.revisionToken}|${activeStoryKey}|${surfaceCacheKey}|${String(state.selection.anchor)}:${String(state.selection.head)}`;
|
|
1619
1650
|
return enrichedSnapshot;
|
|
1620
1651
|
}
|
|
1621
1652
|
|
|
1653
|
+
function getLocalTextCommitViewportRanges(
|
|
1654
|
+
previousSurface: RuntimeRenderSnapshot["surface"] | undefined,
|
|
1655
|
+
): readonly { start: number; end: number }[] | null {
|
|
1656
|
+
if (!previousSurface || previousSurface.blocks.length < EDITING_CORRIDOR_MIN_BLOCKS) {
|
|
1657
|
+
return viewportBlockRanges;
|
|
1658
|
+
}
|
|
1659
|
+
const caretBlockIndex = findSurfaceBlockIndexForSelection(previousSurface, state.selection);
|
|
1660
|
+
if (caretBlockIndex === -1) {
|
|
1661
|
+
return viewportBlockRanges;
|
|
1662
|
+
}
|
|
1663
|
+
const corridor = {
|
|
1664
|
+
start: Math.max(0, caretBlockIndex - EDITING_CORRIDOR_BLOCK_RADIUS),
|
|
1665
|
+
end: Math.min(previousSurface.blocks.length, caretBlockIndex + EDITING_CORRIDOR_BLOCK_RADIUS + 1),
|
|
1666
|
+
};
|
|
1667
|
+
return normalizeViewportRanges([...(viewportBlockRanges ?? []), corridor]);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function findSurfaceBlockIndexForSelection(
|
|
1671
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
1672
|
+
selection: EditorState["selection"],
|
|
1673
|
+
): number {
|
|
1674
|
+
if (!surface) return -1;
|
|
1675
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
1676
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
1677
|
+
for (let index = 0; index < surface.blocks.length; index += 1) {
|
|
1678
|
+
const block = surface.blocks[index];
|
|
1679
|
+
if (!block) continue;
|
|
1680
|
+
if (to >= block.from && from <= block.to) {
|
|
1681
|
+
return index;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return -1;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1622
1687
|
function enrichCulledPlaceholdersWithHeights(
|
|
1623
1688
|
snapshot: EditorSurfaceSnapshot,
|
|
1624
1689
|
): EditorSurfaceSnapshot {
|
|
@@ -2469,6 +2534,57 @@ export function createDocumentRuntime(
|
|
|
2469
2534
|
return snapshot;
|
|
2470
2535
|
}
|
|
2471
2536
|
|
|
2537
|
+
function refreshLocalTextCommitSnapshot(
|
|
2538
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
2539
|
+
mapping: TransactionMapping,
|
|
2540
|
+
): RuntimeRenderSnapshot {
|
|
2541
|
+
perfCounters.increment("refresh.localTextEquivalent");
|
|
2542
|
+
return {
|
|
2543
|
+
...cachedRenderSnapshot,
|
|
2544
|
+
documentId: state.documentId,
|
|
2545
|
+
sessionId: state.sessionId,
|
|
2546
|
+
sourceLabel: state.sourceLabel,
|
|
2547
|
+
revisionToken: state.revisionToken,
|
|
2548
|
+
isReady: state.phase === "ready",
|
|
2549
|
+
isDirty: state.isDirty,
|
|
2550
|
+
readOnly: state.readOnly,
|
|
2551
|
+
documentMode: viewState.documentMode,
|
|
2552
|
+
selection: toPublicSelectionSnapshot(state.selection, activeStory),
|
|
2553
|
+
activeStory,
|
|
2554
|
+
documentStats: updateDocumentStatsForLocalTextCommit(
|
|
2555
|
+
cachedRenderSnapshot.documentStats,
|
|
2556
|
+
mapping,
|
|
2557
|
+
),
|
|
2558
|
+
warnings: cachedRenderSnapshot.warnings,
|
|
2559
|
+
fatalError: state.fatalError ? toPublicError(state.fatalError) : undefined,
|
|
2560
|
+
commandState: {
|
|
2561
|
+
canUndo: history.past.length > 0,
|
|
2562
|
+
canRedo: history.future.length > 0,
|
|
2563
|
+
readOnly: state.readOnly,
|
|
2564
|
+
},
|
|
2565
|
+
surface,
|
|
2566
|
+
protectionSnapshot,
|
|
2567
|
+
grabbedObjectId: grabState.objectId,
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
function updateDocumentStatsForLocalTextCommit(
|
|
2572
|
+
previousStats: RuntimeRenderSnapshot["documentStats"],
|
|
2573
|
+
mapping: TransactionMapping,
|
|
2574
|
+
): RuntimeRenderSnapshot["documentStats"] {
|
|
2575
|
+
if (!previousStats) {
|
|
2576
|
+
return toPublicDocumentStats(state);
|
|
2577
|
+
}
|
|
2578
|
+
let delta = 0;
|
|
2579
|
+
for (const step of mapping.steps) {
|
|
2580
|
+
delta += step.insertSize - Math.max(0, step.to - step.from);
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
...previousStats,
|
|
2584
|
+
storyLength: Math.max(0, previousStats.storyLength + delta),
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2472
2588
|
/**
|
|
2473
2589
|
* Fingerprint over the inputs that the viewport-refresh fast-path knows
|
|
2474
2590
|
* about. Used to short-circuit {@link maybeRefreshSurfaceForViewport}
|
|
@@ -3275,6 +3391,7 @@ export function createDocumentRuntime(
|
|
|
3275
3391
|
{
|
|
3276
3392
|
type: "text.insert",
|
|
3277
3393
|
text: step.text,
|
|
3394
|
+
...(step.formatting ? { formatting: step.formatting } : {}),
|
|
3278
3395
|
origin: createOrigin("api", timestamp),
|
|
3279
3396
|
},
|
|
3280
3397
|
{
|
|
@@ -3349,6 +3466,7 @@ export function createDocumentRuntime(
|
|
|
3349
3466
|
{
|
|
3350
3467
|
type: "text.insert",
|
|
3351
3468
|
text: step.text,
|
|
3469
|
+
...(step.formatting ? { formatting: step.formatting } : {}),
|
|
3352
3470
|
origin: createOrigin("api", timestamp),
|
|
3353
3471
|
},
|
|
3354
3472
|
{
|
|
@@ -3397,6 +3515,48 @@ export function createDocumentRuntime(
|
|
|
3397
3515
|
}
|
|
3398
3516
|
}
|
|
3399
3517
|
},
|
|
3518
|
+
applyScopeFormatting(plan: RuntimeOperationPlan) {
|
|
3519
|
+
let changed = false;
|
|
3520
|
+
for (const step of plan.steps) {
|
|
3521
|
+
if (
|
|
3522
|
+
step.kind !== "formatting-apply" ||
|
|
3523
|
+
!step.range ||
|
|
3524
|
+
!step.formattingAction
|
|
3525
|
+
) {
|
|
3526
|
+
continue;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
const operation =
|
|
3530
|
+
step.formattingAction.kind === "clear-mark"
|
|
3531
|
+
? {
|
|
3532
|
+
type: "clear-mark" as const,
|
|
3533
|
+
mark: step.formattingAction.mark,
|
|
3534
|
+
}
|
|
3535
|
+
: {
|
|
3536
|
+
type: "set-mark" as const,
|
|
3537
|
+
mark: step.formattingAction.mark,
|
|
3538
|
+
};
|
|
3539
|
+
const result = applyTextMarkOperationToDocumentRange(
|
|
3540
|
+
state.document,
|
|
3541
|
+
step.range,
|
|
3542
|
+
operation,
|
|
3543
|
+
);
|
|
3544
|
+
if (!result.changed) {
|
|
3545
|
+
continue;
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const timestamp = clock();
|
|
3549
|
+
this.dispatch({
|
|
3550
|
+
type: "document.replace",
|
|
3551
|
+
document: { ...result.document, updatedAt: timestamp },
|
|
3552
|
+
mapping: createEmptyMapping(),
|
|
3553
|
+
selection: toInternalSelectionSnapshot(result.selection),
|
|
3554
|
+
origin: createOrigin("api", timestamp),
|
|
3555
|
+
});
|
|
3556
|
+
changed = true;
|
|
3557
|
+
}
|
|
3558
|
+
return changed;
|
|
3559
|
+
},
|
|
3400
3560
|
insertFragment(fragment, target) {
|
|
3401
3561
|
// I2 Tier B Slice 1 — dispatch `fragment.insert` against the active story. The
|
|
3402
3562
|
// runtime command handler routes into `applyFragmentInsert` (structure-ops).
|
|
@@ -4892,14 +5052,52 @@ export function createDocumentRuntime(
|
|
|
4892
5052
|
history.future = [];
|
|
4893
5053
|
}
|
|
4894
5054
|
|
|
4895
|
-
applyTransactionToState(transaction);
|
|
5055
|
+
applyTransactionToState(transaction, { allowLocalTextFastPath: true });
|
|
4896
5056
|
}
|
|
4897
5057
|
|
|
4898
5058
|
function commitRemote(transaction: EditorTransaction): void {
|
|
4899
5059
|
applyTransactionToState(transaction);
|
|
4900
5060
|
}
|
|
4901
5061
|
|
|
4902
|
-
function
|
|
5062
|
+
function shouldUseLocalTextCommitSnapshot(
|
|
5063
|
+
previous: EditorState,
|
|
5064
|
+
next: EditorState,
|
|
5065
|
+
transaction: EditorTransaction,
|
|
5066
|
+
effects: EditorTransaction["effects"],
|
|
5067
|
+
): boolean {
|
|
5068
|
+
if (activeStory.kind !== "main") return false;
|
|
5069
|
+
if (!transaction.markDirty || transaction.mapping.steps.length === 0) {
|
|
5070
|
+
return false;
|
|
5071
|
+
}
|
|
5072
|
+
if (transaction.mapping.metadata?.invalidatesStructures) {
|
|
5073
|
+
return false;
|
|
5074
|
+
}
|
|
5075
|
+
if (previous.document.subParts !== next.document.subParts) {
|
|
5076
|
+
return false;
|
|
5077
|
+
}
|
|
5078
|
+
if (effects.warningsAdded.length > 0 || effects.warningsCleared.length > 0) {
|
|
5079
|
+
return false;
|
|
5080
|
+
}
|
|
5081
|
+
if ((effects.transientWarnings?.length ?? 0) > 0) {
|
|
5082
|
+
return false;
|
|
5083
|
+
}
|
|
5084
|
+
return (
|
|
5085
|
+
!effects.commentAdded &&
|
|
5086
|
+
!effects.commentResolved &&
|
|
5087
|
+
!effects.commentReopened &&
|
|
5088
|
+
!effects.commentReplyAdded &&
|
|
5089
|
+
!effects.commentBodyEdited &&
|
|
5090
|
+
!effects.changeAccepted &&
|
|
5091
|
+
!effects.changeRejected &&
|
|
5092
|
+
!effects.revisionAuthored &&
|
|
5093
|
+
!effects.commandBlocked
|
|
5094
|
+
);
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
function applyTransactionToState(
|
|
5098
|
+
transaction: EditorTransaction,
|
|
5099
|
+
options: { allowLocalTextFastPath?: boolean } = {},
|
|
5100
|
+
): void {
|
|
4903
5101
|
// Pure-no-op short-circuit: when a reducer skipped without emitting any
|
|
4904
5102
|
// observable effect AND the selection is identical, skip finalizeState
|
|
4905
5103
|
// / invalidate / refresh / notify entirely. This keeps hot paths (e.g.
|
|
@@ -4941,7 +5139,9 @@ export function createDocumentRuntime(
|
|
|
4941
5139
|
const previous = state;
|
|
4942
5140
|
|
|
4943
5141
|
const tApply0 = performance.now();
|
|
5142
|
+
const tProtection0 = performance.now();
|
|
4944
5143
|
protectionSnapshot = remapProtectionSnapshot(protectionSnapshot, transaction.mapping);
|
|
5144
|
+
perfCounters.increment("commit.protectionRemap.us", Math.round((performance.now() - tProtection0) * 1000));
|
|
4945
5145
|
const tFinalize0 = performance.now();
|
|
4946
5146
|
state = finalizeState(transaction.nextState, transaction.markDirty, clock());
|
|
4947
5147
|
perfCounters.increment("commit.finalizeState.us", Math.round((performance.now() - tFinalize0) * 1000));
|
|
@@ -4949,8 +5149,10 @@ export function createDocumentRuntime(
|
|
|
4949
5149
|
// Re-sync marker-backed scope IDs against the new document; the
|
|
4950
5150
|
// store handles set computation + telemetry. `replaceOverlay(current, doc)`
|
|
4951
5151
|
// is idempotent on state and re-derives the marker-backed set.
|
|
5152
|
+
const tOverlay0 = performance.now();
|
|
4952
5153
|
overlayStore.replaceOverlay(overlayStore.getOverlay(), state.document);
|
|
4953
5154
|
const detachedWorkflowScopeWarnings = syncDetachedWorkflowScopeWarningsInState();
|
|
5155
|
+
perfCounters.increment("commit.overlaySync.us", Math.round((performance.now() - tOverlay0) * 1000));
|
|
4954
5156
|
|
|
4955
5157
|
const tInvalidate0 = performance.now();
|
|
4956
5158
|
if (transaction.markDirty && transaction.mapping.steps.length > 0) {
|
|
@@ -4974,12 +5176,37 @@ export function createDocumentRuntime(
|
|
|
4974
5176
|
}
|
|
4975
5177
|
perfCounters.increment("commit.invalidate.us", Math.round((performance.now() - tInvalidate0) * 1000));
|
|
4976
5178
|
|
|
5179
|
+
const notifyEffects: EditorTransaction["effects"] = {
|
|
5180
|
+
...transaction.effects,
|
|
5181
|
+
warningsAdded: [
|
|
5182
|
+
...transaction.effects.warningsAdded,
|
|
5183
|
+
...detachedWorkflowScopeWarnings.added,
|
|
5184
|
+
],
|
|
5185
|
+
warningsCleared: [
|
|
5186
|
+
...transaction.effects.warningsCleared,
|
|
5187
|
+
...detachedWorkflowScopeWarnings.cleared,
|
|
5188
|
+
],
|
|
5189
|
+
};
|
|
5190
|
+
|
|
5191
|
+
const tClassify0 = performance.now();
|
|
5192
|
+
const useLocalTextCommitSnapshot =
|
|
5193
|
+
options.allowLocalTextFastPath === true &&
|
|
5194
|
+
shouldUseLocalTextCommitSnapshot(
|
|
5195
|
+
previous,
|
|
5196
|
+
state,
|
|
5197
|
+
transaction,
|
|
5198
|
+
notifyEffects,
|
|
5199
|
+
);
|
|
5200
|
+
perfCounters.increment("commit.refreshClassify.us", Math.round((performance.now() - tClassify0) * 1000));
|
|
5201
|
+
|
|
4977
5202
|
// I5: validate post-mutation selection against the new document bound.
|
|
4978
5203
|
// First call to getCachedSurface() here computes the post-mutation surface
|
|
4979
5204
|
// snapshot (cache miss — revisionToken just changed in finalizeState).
|
|
4980
|
-
//
|
|
4981
|
-
//
|
|
4982
|
-
//
|
|
5205
|
+
// For local text commits, this is an editing-corridor surface (current
|
|
5206
|
+
// block plus a small halo) so the immediate typing path keeps visible
|
|
5207
|
+
// truth hot without rebuilding offscreen table/comment markup. Structural,
|
|
5208
|
+
// review, and warning commits still use the full current viewport surface
|
|
5209
|
+
// and fall back to refreshRenderSnapshot().
|
|
4983
5210
|
//
|
|
4984
5211
|
// Wired ONLY at this chokepoint. The other 12 `cachedRenderSnapshot =
|
|
4985
5212
|
// refreshRenderSnapshot()` sites in this file are intentionally skipped:
|
|
@@ -4999,7 +5226,13 @@ export function createDocumentRuntime(
|
|
|
4999
5226
|
// The return type of getCachedSurface is `RuntimeRenderSnapshot["surface"]`
|
|
5000
5227
|
// which is optional in the public API for shape-only reasons — the helper
|
|
5001
5228
|
// itself always returns a defined snapshot.
|
|
5002
|
-
const
|
|
5229
|
+
const tValidation0 = performance.now();
|
|
5230
|
+
const surfaceForValidation = useLocalTextCommitSnapshot
|
|
5231
|
+
? getCachedSurface(state.document, activeStory, {
|
|
5232
|
+
viewportBlockRangesOverride: getLocalTextCommitViewportRanges(cachedRenderSnapshot.surface),
|
|
5233
|
+
enrichCulledPlaceholders: false,
|
|
5234
|
+
})!
|
|
5235
|
+
: getCachedSurface(state.document, activeStory)!;
|
|
5003
5236
|
const validationOptions = state.selection.activeRange.kind === "node"
|
|
5004
5237
|
? {
|
|
5005
5238
|
isValidNodeTarget: createSurfaceNodeSelectionProbe(surfaceForValidation),
|
|
@@ -5015,26 +5248,24 @@ export function createDocumentRuntime(
|
|
|
5015
5248
|
state = { ...state, selection: validatedSelection };
|
|
5016
5249
|
storySelections.set(storyTargetKey(activeStory), state.selection);
|
|
5017
5250
|
}
|
|
5251
|
+
perfCounters.increment("commit.selectionValidation.us", Math.round((performance.now() - tValidation0) * 1000));
|
|
5018
5252
|
|
|
5019
5253
|
const tRefresh0 = performance.now();
|
|
5020
|
-
cachedRenderSnapshot =
|
|
5254
|
+
cachedRenderSnapshot = useLocalTextCommitSnapshot
|
|
5255
|
+
? refreshLocalTextCommitSnapshot(surfaceForValidation, transaction.mapping)
|
|
5256
|
+
: refreshRenderSnapshot();
|
|
5021
5257
|
perfCounters.increment("commit.refresh.us", Math.round((performance.now() - tRefresh0) * 1000));
|
|
5022
5258
|
|
|
5023
5259
|
const tNotify0 = performance.now();
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
...transaction
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
...transaction.effects.warningsCleared,
|
|
5034
|
-
...detachedWorkflowScopeWarnings.cleared,
|
|
5035
|
-
],
|
|
5036
|
-
},
|
|
5037
|
-
});
|
|
5260
|
+
deferNextContextAnalyticsEmit = useLocalTextCommitSnapshot;
|
|
5261
|
+
try {
|
|
5262
|
+
notify(previous, state, {
|
|
5263
|
+
...transaction,
|
|
5264
|
+
effects: notifyEffects,
|
|
5265
|
+
});
|
|
5266
|
+
} finally {
|
|
5267
|
+
deferNextContextAnalyticsEmit = false;
|
|
5268
|
+
}
|
|
5038
5269
|
perfCounters.increment("commit.notify.us", Math.round((performance.now() - tNotify0) * 1000));
|
|
5039
5270
|
perfCounters.increment("commit.total.us", Math.round((performance.now() - tApply0) * 1000));
|
|
5040
5271
|
emitStageToken(telemetryBus, "commit", "commit.apply.complete", {
|
|
@@ -5608,23 +5839,58 @@ export function createDocumentRuntime(
|
|
|
5608
5839
|
// synchronous call stack — one per commit. (Flag declared above near
|
|
5609
5840
|
// perfCounters so it is initialized before construction-time emits run.)
|
|
5610
5841
|
function scheduleContextAnalyticsEmit(): void {
|
|
5842
|
+
const shouldDefer = deferNextContextAnalyticsEmit;
|
|
5843
|
+
deferNextContextAnalyticsEmit = false;
|
|
5611
5844
|
if (analyticsEmitScheduled) {
|
|
5845
|
+
if (shouldDefer && analyticsEmitScheduleMode === "microtask") {
|
|
5846
|
+
analyticsEmitScheduleMode = "idle";
|
|
5847
|
+
perfCounters.increment("emit.contextAnalytics.deferred");
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5612
5850
|
perfCounters.increment("emit.contextAnalytics.coalesced");
|
|
5613
5851
|
return;
|
|
5614
5852
|
}
|
|
5615
5853
|
analyticsEmitScheduled = true;
|
|
5616
|
-
|
|
5854
|
+
analyticsEmitScheduleMode = shouldDefer ? "idle" : "microtask";
|
|
5855
|
+
const run = () => {
|
|
5617
5856
|
// Reset BEFORE the emit so any synchronous re-entrant emits triggered
|
|
5618
5857
|
// by listener callbacks schedule a fresh second microtask (no lost
|
|
5619
5858
|
// updates). Reversing this order would drop bursts originating in
|
|
5620
5859
|
// listeners.
|
|
5621
5860
|
analyticsEmitScheduled = false;
|
|
5861
|
+
analyticsEmitScheduleMode = "none";
|
|
5622
5862
|
const t = performance.now();
|
|
5623
5863
|
emitContextAnalyticsChanged();
|
|
5624
5864
|
perfCounters.increment("emit.contextAnalytics.us", Math.round((performance.now() - t) * 1000));
|
|
5865
|
+
};
|
|
5866
|
+
if (shouldDefer) {
|
|
5867
|
+
perfCounters.increment("emit.contextAnalytics.deferred");
|
|
5868
|
+
scheduleIdleContextAnalytics(run);
|
|
5869
|
+
return;
|
|
5870
|
+
}
|
|
5871
|
+
queueMicrotask(() => {
|
|
5872
|
+
if (analyticsEmitScheduleMode === "idle") {
|
|
5873
|
+
scheduleIdleContextAnalytics(run);
|
|
5874
|
+
return;
|
|
5875
|
+
}
|
|
5876
|
+
run();
|
|
5625
5877
|
});
|
|
5626
5878
|
}
|
|
5627
5879
|
|
|
5880
|
+
function scheduleIdleContextAnalytics(callback: () => void): void {
|
|
5881
|
+
const requestIdle = (globalThis as {
|
|
5882
|
+
requestIdleCallback?: (
|
|
5883
|
+
cb: () => void,
|
|
5884
|
+
options?: { timeout?: number },
|
|
5885
|
+
) => number;
|
|
5886
|
+
}).requestIdleCallback;
|
|
5887
|
+
if (typeof requestIdle === "function") {
|
|
5888
|
+
requestIdle(callback, { timeout: 250 });
|
|
5889
|
+
return;
|
|
5890
|
+
}
|
|
5891
|
+
setTimeout(callback, 0);
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5628
5894
|
// V6c — TOC auto-refresh scheduler. Mirrors scheduleContextAnalyticsEmit's
|
|
5629
5895
|
// microtask-coalesce shape. Bursts of heading edits within one synchronous
|
|
5630
5896
|
// call stack collapse to a single rebuild + a single toc_auto_refreshed
|
|
@@ -62,8 +62,8 @@ import {
|
|
|
62
62
|
} from "./preservation-boundary.ts";
|
|
63
63
|
import { resolveScopeRange } from "./scope-range.ts";
|
|
64
64
|
import type {
|
|
65
|
-
ReplacementOperationKind,
|
|
66
65
|
ReplacementScope,
|
|
66
|
+
ScopeActionOperationKind,
|
|
67
67
|
SemanticScope,
|
|
68
68
|
ValidationApproval,
|
|
69
69
|
ValidationIssue,
|
|
@@ -85,7 +85,7 @@ export interface ScopeValidationRuntime {
|
|
|
85
85
|
|
|
86
86
|
export interface ComposeScopeValidationInputs {
|
|
87
87
|
readonly scope: SemanticScope;
|
|
88
|
-
readonly operation:
|
|
88
|
+
readonly operation: ScopeActionOperationKind;
|
|
89
89
|
readonly proposedContent: ReplacementScope["proposedContent"];
|
|
90
90
|
readonly runtime: ScopeValidationRuntime;
|
|
91
91
|
/**
|
|
@@ -119,6 +119,11 @@ export interface ComposeScopeValidationInputs {
|
|
|
119
119
|
* here; the compile step consumes them to drive per-step behavior.
|
|
120
120
|
*/
|
|
121
121
|
readonly preservePolicy?: ReplacementScope["preserve"];
|
|
122
|
+
/**
|
|
123
|
+
* Formatting-only actions do not destroy opaque fragments or nested scope
|
|
124
|
+
* markers, so they bypass the replacement preservation-boundary check.
|
|
125
|
+
*/
|
|
126
|
+
readonly skipPreservation?: boolean;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/**
|
|
@@ -127,9 +132,12 @@ export interface ComposeScopeValidationInputs {
|
|
|
127
132
|
* entry in the 37-op policy matrix.
|
|
128
133
|
*/
|
|
129
134
|
function inferActionId(
|
|
130
|
-
operation:
|
|
135
|
+
operation: ScopeActionOperationKind,
|
|
131
136
|
content: ReplacementScope["proposedContent"],
|
|
132
137
|
): AIAction {
|
|
138
|
+
if (operation === "formatting") {
|
|
139
|
+
return "fix_formatting";
|
|
140
|
+
}
|
|
133
141
|
switch (operation) {
|
|
134
142
|
case "replace":
|
|
135
143
|
return content.kind === "text" ? "rewrite_paragraph" : "generate_text";
|
|
@@ -287,6 +295,7 @@ function collectPreservationVerdict(
|
|
|
287
295
|
warnings: ValidationIssue[],
|
|
288
296
|
): void {
|
|
289
297
|
const { document, scope, positionMap } = inputs;
|
|
298
|
+
if (inputs.skipPreservation === true) return;
|
|
290
299
|
if (!document) return;
|
|
291
300
|
const pm = positionMap ?? buildScopePositionMap(document);
|
|
292
301
|
const range = inputs.enumeratedScope
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import type { TelemetryBus } from "../debug/telemetry-bus.ts";
|
|
12
12
|
import type {
|
|
13
|
-
ReplacementScope,
|
|
14
13
|
RuntimeOperationPlan,
|
|
15
14
|
ScopeActionAudit,
|
|
15
|
+
ScopeActionProposal,
|
|
16
16
|
SemanticScope,
|
|
17
17
|
ValidationResult,
|
|
18
18
|
} from "./semantic-scope-types.ts";
|
|
@@ -24,7 +24,7 @@ export interface EmitScopeActionAuditInputs {
|
|
|
24
24
|
readonly documentHashBefore: string;
|
|
25
25
|
readonly documentHashAfter?: string;
|
|
26
26
|
readonly targetScopeSnapshot: SemanticScope;
|
|
27
|
-
readonly proposed:
|
|
27
|
+
readonly proposed: ScopeActionProposal;
|
|
28
28
|
readonly plan: RuntimeOperationPlan;
|
|
29
29
|
readonly validation: ValidationResult;
|
|
30
30
|
readonly emittedAtUtc: string;
|