docs/plans/2026-05-05-slate-v2-best-pasting-strategy-ralplan.md
Hard take: Slate v2 should keep the current clipboard ownership split, but the actual insertion engine is wrong for large paste.
Keep:
slate-react owns browser paste/copy/cut/drop event dispatch.slate-dom owns DataTransfer, internal fragment formats, DOM coverage, and
browser payload policy.slate owns model fragment extraction, fitting, insertion, normalization,
history, and collaboration-ready operations.Change:
splitNodes + insertText once per
line.insert_node operations as the
long-term path.editor.clipboard or a magical editor.paste.
If a richer app API ships, it should be a DOM clipboard provider adapter under
editor.dom.clipboard, after the internal path is proven.Best target:
DataTransfer
-> ordered paste candidates
-> canonical InsertionSlice
-> fit once against target/schema/void/readOnly policy
-> single replace-fragment transaction/op
-> one history item
-> one model-owned selection repair
-> bounded normalization over affected boundary paths
Not the target:
DataTransfer
-> split text into N lines
-> splitNodes N times
-> insertText N times
-> normalize and transform dirty paths through thousands of operations
Intent:
Desired outcome:
In scope:
Non-goals:
editor.clipboard namespace.Decision boundaries:
Unresolved user decision:
Principles:
Top drivers:
Viable options:
| Option | Verdict | Reason |
|---|---|---|
| Keep current loop and micro-optimize | reject | Current 2,000-line plain-text paste is 3545.36ms; caching inside the loop is not enough. |
Convert plain text to a fragment, then use current insertFragment | reject as final | A prebuilt-fragment candidate was already slower in the bounded benchmark; current fragment insertion still emits many insertions. |
| Batch N low-level ops under one normalization pass | transitional only | Better than today, but still leaves collaboration/history/path-transform pressure proportional to pasted lines. |
| Add a single replace-fragment transaction/op | choose | Matches VS Code one edit per selection, ProseMirror one replace step, and Tiptap replaceWith; it is the cleanest long-term engine. |
| Use virtualization/staged rendering to hide paste cost | reject | Virtualization affects DOM/render cost, not model insertion or normalization cost. |
Chosen option:
InsertionSlice plus a bulk replace_fragment model action.insertText.Consequences:
Follow-ups:
Operation union member or an
internal transaction macro that adapters can lower.| Dimension | Score | Evidence |
|---|---|---|
| React 19.2 runtime performance | 0.91 | React does not own the bottleneck; current path is in Slate DOM/core insertion. Browser contract already names paste-normalize-undo in .tmp/slate-v2/packages/slate-browser/src/core/first-party-browser-contracts.ts:178. |
| Slate-close unopinionated DX | 0.93 | Keeps raw Slate free of product HTML policy and keeps low-level clipboard under editor.dom.clipboard, matching docs/slate-v2/references/pr-description.md:231. |
| Plate and slate-yjs migration backbone | 0.89 | One logical paste maps better to collaboration/history than N fake typing actions. Replay through the collaboration import path is proven; transport-specific CRDT lowering stays adapter-owned. |
| Regression-proof testing strategy | 0.94 | Existing unit/browser owners exist: .tmp/slate-v2/packages/slate/test/clipboard-contract.ts:18 and browser paste-normalize-undo at .tmp/slate-v2/packages/slate-browser/src/core/first-party-browser-contracts.ts:178; new benchmark gates are explicit. |
| Research evidence completeness | 0.95 | Live source read across Slate v2, Lexical, ProseMirror, Tiptap, and VS Code; mechanisms are synthesized below instead of name-dropped. |
| shadcn-style composability/minimalism | 0.90 | Public surface stays small; richer provider API is delayed until proof, not shoved into core now. |
Weighted score: 0.923.
Completion verdict: ready for execution. No public API is stable until proof lands.
Current paste dispatch:
.tmp/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts:141
materializes DOM coverage boundaries with selectionPolicy === 'materialize'
before paste..tmp/slate-v2/packages/slate-react/src/editable/clipboard-input-strategy.ts:485
owns applyEditablePaste.Current DOM clipboard import:
.tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:228
runs dom.clipboard.insertData handlers first..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:247
imports trusted Slate fragments..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:269
is the current plain-text fallback..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:276
splits text by newline..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts:280
loops every line, splitting nodes and inserting text.Current core fragment insert:
.tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:160
fits a single empty text-block target without inserting the fragment wrapper..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:208
fits exact whole-top-level-block selections by replacing the root child slice
while keeping structural fragments as sibling units..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:261
fits compatible single text-block targets, including marked text and inline
children, by replacing the target block children..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:411
handles full-document fragment replacement through one replace_fragment
operation instead of a snapshot shortcut..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:473
applies single text-block fitting through one replace_fragment at the target
block path..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:491
applies exact whole-top-level-block structural fitting through one
replace_fragment at the root path..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:562
walks fragment nodes into starts, middles, and ends..tmp/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts:640,
:653, and :661 insert those groups with insertNodes for shapes that
still fall back to structural insertion.Current insertion and normalization pressure:
.tmp/slate-v2/packages/slate/src/transforms-node/insert-nodes.ts:121 batches
dirty-path collection, but it still applies one insert_node operation per
child at :128..tmp/slate-v2/packages/slate/src/editor/normalize.ts:42 skips default text
normalization only for insert_text and remove_text; multiline paste creates
split/insert chains that miss this cheap path..tmp/slate-v2/packages/slate/src/transforms-text/insert-text.ts:89 still
uses replaceSnapshot for full-document expanded text replacement; this is
outside the current paste fragment path.Current benchmark:
.tmp/slate-v2/tmp/slate-clipboard-large-payload-benchmark.json says 2,000
pasted lines average:
plainTextSplitMs: 0.05msfullSelectionCopyMs: 5.25msplainTextInsertMs: 3545.36msfragmentInsertMs: 4615.83msplainTextInsert operation count: 5998Conclusion:
Sources:
../lexical/packages/lexical-clipboard/src/clipboard.ts:130../lexical/packages/lexical/src/LexicalSelection.ts:747../lexical/packages/lexical/src/LexicalUpdates.ts:253../lexical/packages/lexical/src/LexicalNormalization.ts:59Observed mechanism:
Problem it avoids:
Slate target:
$function public style.Verdict: partial.
Sources:
../prosemirror/view/src/clipboard.ts:43../prosemirror/view/src/clipboard.ts:96../prosemirror/view/src/input.ts:634../prosemirror/model/src/replace.ts:24../prosemirror/transform/src/replace.ts:378Observed mechanism:
Slice.replaceSelection or replaceSelectionWith step.Problem it avoids:
Slate target:
Verdict: agree.
Sources:
../tiptap/packages/core/src/commands/insertContentAt.ts:127../tiptap/packages/core/src/commands/insertContentAt.ts:157../tiptap/packages/core/src/commands/insertContentAt.ts:180../tiptap/packages/core/src/commands/insertContentAt.ts:194../tiptap/packages/core/src/PasteRule.ts:114../tiptap/packages/core/src/Editor.ts:531../tiptap/packages/core/src/ExtensionManager.ts:285Observed mechanism:
insertContentAt parses once, validates once, uses tr.insertText for pure
text, and tr.replaceWith for structural content.Problem it avoids:
Slate target:
Verdict: agree.
Sources:
../vscode/src/vs/editor/common/cursor/cursorTypeEditOperations.ts:654../vscode/src/vs/editor/common/cursor/cursorTypeEditOperations.ts:707../vscode/src/vs/editor/common/model/textModel.ts:1340../vscode/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts:243../vscode/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts:323../vscode/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts:625../vscode/src/vscode-dts/vscode.d.ts:6346Observed mechanism:
ReplaceCommand per selection, with undo stack
boundaries before/after.Problem it avoids:
Slate target:
Verdict: agree.
Immediate public shape:
editor.dom.clipboard.dom.clipboard.insertData as the currently accepted low-level escape
hatch until the provider pipeline is proven.editor.clipboard.editor.paste.InsertionSlice until the internal engine passes browser and
benchmark gates.Future provider shape, unstable only:
type DOMClipboardPasteProvider<V extends Value = Value> = {
id: string;
pasteMimeTypes?: readonly string[];
priority?: "default" | "app" | "internal";
preparePaste?: (
context: DOMClipboardPreparePasteContext<V>,
) => void | Promise<void>;
providePaste?: (
context: DOMClipboardPasteContext<V>,
) =>
| DOMClipboardPasteCandidate<V>
| null
| Promise<DOMClipboardPasteCandidate<V> | null>;
resolvePaste?: (
candidate: DOMClipboardPasteCandidate<V>,
context: DOMClipboardPasteContext<V>,
) => DOMClipboardPasteCandidate<V> | Promise<DOMClipboardPasteCandidate<V>>;
};
DX rules:
Add an internal model:
type InsertionSlice<V extends Value = Value> = {
fragment: Descendant[];
openStart: number;
openEnd: number;
source: "slate-fragment" | "html" | "plain-text" | "file" | "custom";
text?: string;
};
Add a paste plan:
type PastePlan<V extends Value = Value> = {
at: Range;
slice: InsertionSlice<V>;
select: "end" | "preserve" | "range";
source: "paste";
};
Add one bulk model action:
editor.update((tx) => {
tx.fragment.replace({
at: tx.selection.get(),
slice,
source: "paste",
});
});
Internal operation target:
type ReplaceFragmentOperation = {
type: "replace_fragment";
at: Range;
fragment: Descendant[];
openStart: number;
openEnd: number;
source?: "paste" | "drop" | "insert-content";
};
Hard rule:
replace_fragment yet, the implementation may
lower it inside the adapter, but core paste should still be one logical
transaction and one normalization/fitting pass.No React component API is needed for this strategy.
React responsibilities:
React non-goals:
usePaste;Plate needs:
Backbone answer:
InsertionSlice, provider/candidate ordering, and bulk
fragment replacement.insertText/insertFragment just to
intercept paste.Collaboration risk:
replace_fragment operation changes the shared operation contract.Target:
replace_fragment directly;Gate:
ClawSweeper status:
Improves after issue-size benchmark proof.Improves after populated-editor copy/paste issue-size
benchmark proof.Improves after the 50,000-block cut benchmark and bounded
fragment extraction proof. Exact closure still needs the remaining model
delete/snapshot cost below an accepted target.Existing source rows:
docs/slate-v2/ledgers/fork-issue-dossier.md:2455 tracks #4056.docs/slate-v2/ledgers/fork-issue-dossier.md:2486 tracks #5945.docs/slate-v2/ledgers/issue-coverage-matrix.md:64 keeps #5945 at
Improves.docs/slate-v2/ledgers/issue-coverage-matrix.md:65 keeps #4056 at
Improves.docs/slate-v2/ledgers/issue-coverage-matrix.md:66 keeps #5992 at
Improves.docs/slate-issues/benchmark-candidate-map.md:93 marks #5945
benchmark-ready.docs/slate-issues/benchmark-candidate-map.md:250 marks #4056
ready with minor setup.Target claim map:
| Issue | Current status | Target after execution | Reason |
|---|---|---|---|
| #5945 | Improves | Fixes #5945 only if 10,000-line browser proof passes | The issue-size package benchmark is green; exact browser repro closure remains open. |
| #4056 | Improves | Fixes #4056 only with historical browser/user-path repro | Populated-editor copy/paste issue-size package benchmark is green; exact browser repro closure remains open. |
| #5992 | Improves | Fixes #5992 only after remaining model delete/snapshot cost has a target and proof | The issue-size cut benchmark improved by bounding fragment extraction, but the delete lane remains above a clean closure bar. |
| #2195 | Related perf pressure | Improves if dirty-path text-node tracking drops from the hot path | Dirty tracking is likely part of the paste cost. |
| #6038 | Related perf pressure | Improves if batch-aware apply engine lands | Bulk replace directly touches this architecture pressure. |
| #5811 | Related normalization pressure | Related only unless infinite-normalize repro is proven | Paste fitting must not create new normalize loops. |
Unchanged fixed/improved clipboard rows:
Improves/Related classifications.PR description status:
| Behavior | Required proof |
|---|---|
| Single-line plain text paste | Existing insertText fast path remains behavior-equivalent. |
| Multiline plain text paste | New unit proof inserts lines as the same block structure users get today, with correct final selection. |
| Full-document replacement | Existing replaceSnapshot fast path stays green; shell-backed full-doc text replacement remains explicit. |
| Slate fragment paste | Fragment payload preserves target-block behavior and whole-list wrappers. |
| Void/inline void paste | Existing selected inline void copy/paste/cut proof remains green. |
| Paste over expanded range | Deletes selected content once, inserts canonical slice once, sets selection to insertion end. |
| Paste into hidden/DOM-incomplete target | DOM coverage materializes or model-backs according to policy before mutation. |
| Paste while composing | Composition guard blocks dangerous materialization/mutation. |
| Undo | One paste equals one undo step. |
| Redo | Redo restores full pasted slice and selection. |
| Collaboration replay | One logical paste replays deterministically through local and remote updates. |
Minimum browser rows:
paste-normalize-undo stays green for richtext, plaintext, and
forced-layout.toDOMPoint blindly;Benchmark rows:
10 lines: no regression.100 lines: no regression.1,000 lines: target <= 150ms p95 local.2,000 lines: target <= 250ms p95 local.10,000 lines: target <= 1000ms p95 local and at least 5x faster than
current extrapolated path.<= 3 logical operations.| Skill/lens | Status | Plan response |
|---|---|---|
performance | applied | Cohorts, repeated unit, operation count, heap, and native behavior gates are explicit. |
performance-oracle | applied by rule | Hot path is operation/normalization complexity, not splitting. Target is O(inserted nodes) build plus O(boundary) fit, not O(lines * path-transform). |
tdd | applied | Execution starts with one failing benchmark/unit proof, then implementation, then browser proof. |
high-risk-deliberate-pass | applied | Operation/collaboration/browser blast radius is named below. |
vercel-react-best-practices | skipped with reason | React is dispatch/repair owner here; the bottleneck is model insertion. |
shadcn | skipped with reason | No UI/component API is being designed. |
react-useeffect | skipped with reason | No new effect or subscription surface. |
Trigger:
Blast radius:
packages/slatepackages/slate-dompackages/slate-reactpackages/slate-historyslate-yjs adapterFailure scenario 1:
Failure scenario 2:
Failure scenario 3:
Keep/revise/drop:
replace_fragment cannot preserve history/collab semantics.Hard cuts:
splitNodes + insertText for multiline paste as the final
engine.editor.clipboard.Rejected:
insertFragment": recreates legacy method-override chaos.| Objection | Answer |
|---|---|
| "This adds a high-level op, Slate ops should stay primitive." | Paste is one user action. VS Code, ProseMirror, and Tiptap all treat it as one replacement. Primitive-only purity is not worth 30,000 operations. |
"Collaboration adapters may not understand replace_fragment." | Replay through Slate's collaboration import path is proven. CRDT adapters that cannot represent subtree replacement atomically lower it at their adapter boundary. |
| "Apps need HTML paste control." | Yes, through providers/capabilities. Raw Slate should not own product HTML policy. |
"The current fragment path already uses replace_fragment for some shapes." | Correct, and that is the point of this lane: keep expanding the fitted one-op shapes only where behavior is proven. |
| "Why not use Lexical's raw text nodes?" | Lexical's dirty runtime is useful, but Slate's issue target is huge multiline structural insertion. We need bulk replacement, not another node loop. |
| "Why mention VS Code for Slate rich text?" | VS Code is not a rich-text model. The lesson is bulk edits, undo grouping, provider cancellation, and fallback, not its text buffer. |
| Pass | Status | Evidence added | Plan delta | Open issues | Next owner |
|---|---|---|---|---|---|
| Current-state read | complete | Live Slate v2 clipboard/core/benchmark files | Identified insertion/normalization as owner | none | done |
| Related issue pass | complete | Existing ClawSweeper dossier and coverage matrix rows for #4056/#5945/#5992 | Execution claim rows are synced to Improves | exact Fixes proof remains separate | done |
| Ecosystem synthesis | complete | Lexical, ProseMirror, Tiptap, VS Code source reads | Chose bulk replace-fragment path | none | done |
| Decision/high-risk pass | complete | Operation/collab/browser risk table | Public API delayed; internal engine first | yjs proof during implementation | ralph |
| Closure score | complete | Scorecard above | Plan ready for execution | none | ralph |
Changed:
InsertionSlice and one logical
replacement transaction.Dropped:
Unchanged:
editor.dom.clipboard ownership.Fixes claim.Status: complete for the first execution slice, pending for the full plan.
Landed in /Users/zbeyens/git/slate-v2:
replace_fragment as a single logical model operation with inverse support,
dirty-path classification, path/point invalidation below the replaced root,
public-state replace classification, and internal applyOperation export.splitNodes + insertText loop for the #5945-shaped
workload.replace_fragment operation. Marked text is preserved instead of flattened.
Structurally richer fragments still fall back.bun run bench:slate:5945:issue.Fresh evidence:
bun test ./packages/slate/test/clipboard-contract.ts ./packages/slate-dom/test/clipboard-boundary.ts ./packages/slate/test/operations-contract.ts
bun --filter slate typecheck
bun --filter slate-dom typecheck
bun --filter slate-react typecheck
bun lint:fix
bun run bench:slate:5945:local
bun run bench:slate:5945:issue
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts -g "paste-normalize-undo" --project=chromium
Benchmark result:
34.72ms, 1 operation.7.08ms mean, 1 operation.13.69ms mean, 1
operation.16.22ms mean, 1
operation.replace_fragment and is visible
to history/undo instead of bypassing observers through snapshot replacement.replace_fragment through
tx.operations.replay(...) with remote metadata and skips local undo history;
CRDT/Yjs-style lowering stays at the adapter boundary.Do not mark the full plan done yet:
InsertionSlice
fitter.Improves claim in the ledgers/PR narrative. Exact
Fixes #5945 still needs a 10,000-line browser artifact for the plaintext
example workflow.Status: complete for the populated-editor benchmark slice, pending for the full plan.
Landed in /Users/zbeyens/git/slate-v2:
splitNodes + insertText.replace_fragment, preserving
surrounding target text and placing selection at the end of the inserted
content.Fresh evidence:
bun test ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/clipboard-contract.ts ./packages/slate-dom/test/clipboard-boundary.ts ./packages/slate/test/operations-contract.ts
bun --filter slate typecheck
bun --filter slate-dom typecheck
bun --filter slate-react typecheck
bun lint:fix
bun run bench:slate:5945:local
bun run bench:slate:5945:issue
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts -g "paste-normalize-undo" --project=chromium
Benchmark result:
34.72ms, 1
operation.25.13ms.194.55ms, 1 operation, 19,999 resulting blocks.13.20ms, DOM fragment paste
14.84ms, plaintext paste 7.18ms, each with 1 operation.Claim delta:
Improves.Improves.Improves after the cut-path proof addendum below.Do not mark the full plan done yet:
Fixes #4056 still needs the historical browser/user-path repro.Status: complete for the #5992 issue-size benchmark slice, pending for the full plan.
Landed in /Users/zbeyens/git/slate-v2:
clipboard-contract locks selected top-level block extraction from a larger
surrounding document.3 operation stream
for exact whole top-level block range deletion.Fresh evidence:
bun test ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/clipboard-contract.ts ./packages/slate-dom/test/clipboard-boundary.ts ./packages/slate/test/delete-contract.ts ./packages/slate/test/operations-contract.ts
bun --filter slate typecheck
bun --filter slate-dom typecheck
bun --filter slate-react typecheck
bun lint:fix
bun run bench:slate:5945:local
bun run bench:slate:5945:issue
SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS=50000 SLATE_CLIPBOARD_BENCH_ISSUE_TARGETS=1 bun ./scripts/benchmarks/slate/5945-large-plaintext-paste.mjs
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts -g "paste-normalize-undo" --project=chromium
Benchmark result:
621.26ms,
3 operations.511.47ms, 3
operations.4002.02ms for
copy-plus-delete, so the fragment extraction owner is materially improved.38.57ms, 1
operation.12.16ms.185.49ms, 1 operation, 19,999 resulting blocks.Claim delta:
Improves.Improves.Improves.Remaining work after the #5992 pass:
Status: complete for this plan.
Live Slate v2 source:
.tmp/slate-v2/packages/slate-dom/src/plugin/dom-editor.ts exposes
DOMClipboardInsertDataHandler<V> = (editor, data) => boolean | void..tmp/slate-v2/packages/slate-dom/src/plugin/dom-clipboard-runtime.ts runs
dom.clipboard.insertData handlers before internal Slate fragment and
plain-text fallback.editor.clipboard namespace.Ecosystem read:
prepareDocumentPaste,
provideDocumentPasteEdits, resolveDocumentPasteEdit, MIME matching,
cancellation tokens, and paste-as UI.Decision:
dom.clipboard.insertData as the current app-owned escape hatch.Rejected shape:
editor.clipboard.registerProvider(...)
Reason: too broad, too product-shaped, and not Slate-close.
Accepted future shape, only if the async proof appears:
type DOMClipboardPasteProvider<V extends Value = Value> = {
mimeTypes?: readonly string[];
prepare?: (
context: DOMClipboardPreparePasteContext<V>,
) => void | Promise<void>;
provide?: (
context: DOMClipboardPasteContext<V>,
) =>
| DOMClipboardPasteCandidate<V>
| null
| Promise<DOMClipboardPasteCandidate<V> | null>;
resolve?: (
candidate: DOMClipboardPasteCandidate<V>,
context: DOMClipboardPasteContext<V>,
) => DOMClipboardPasteCandidate<V> | Promise<DOMClipboardPasteCandidate<V>>;
};
Hard cut for the current PR:
Open questions:
replace_fragment remain a public operation long term, or should a
future adapter-facing macro hide it?dom.clipboard.insertData once a
real paste-as example proves the need?Decision-changing evidence:
5x faster on 10,000-line paste,
revisit the operation design.replace_fragment deterministically, keep it internal
and lower through a single adapter transaction.insertData for v2.Owner: ralph.
Work:
Gate:
Owner: ralph.
Work:
insertText.Gate:
Owner: ralph.
Work:
Gate:
clipboard-contract expanded cases green.Owner: ralph.
Work:
replace_fragment transaction/op or macro.Gate:
Owner: ralph.
Work:
Gate:
Owner: future slate-ralplan.
Work:
dom.clipboard.insertData with VS Code-style provider
candidates.Gate:
Run from /Users/zbeyens/git/slate-v2 during execution:
bun run bench:slate:5945:local
bun test ./packages/slate/test/clipboard-contract.ts
bun test ./packages/slate-dom/test/clipboard-boundary.ts
bun --filter slate typecheck
bun --filter slate-dom typecheck
bun --filter slate-react typecheck
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/stress/generated-editing.test.ts -g "paste-normalize-undo" --project=chromium
Add before claim:
bun run bench:slate:5945:issue
where bench:slate:5945:issue must cover the 10,000-line workload.
When execution finishes, report:
This planning pass is complete when:
ralph;Status: complete for this execution plan. Follow-up work is tracked as separate
lanes: exact Fixes browser artifacts for #5945/#4056, remaining #5992
model delete/snapshot cost, CRDT/Yjs adapter lowering, and any future async
paste provider API.