@@ -85,10 +85,10 @@ interface RichMarkdownEditorProps {
8585 */
8686 streamIsIncremental ?: boolean
8787 /**
88- * The agent edit operation driving the stream, when known (`create`/`append`/`update`/`patch`). Used
89- * only to relax the "must extend" gate for `patch ` (which legitimately replaces a mid-document region):
90- * every other operation's snapshot must extend what's shown, so a base-less `append` fragment can't
91- * reconcile the live doc to a wipe .
88+ * The agent edit operation driving the stream, when known (`create`/`append`/`update`/`patch`). In the
89+ * collaborative path it decides only whether to stream mid-flight: an `update ` (from-scratch rewrite) is
90+ * HELD until settle so the open doc doesn't collapse to a partial result, while `append`/`patch`/`create`
91+ * apply each frame .
9292 */
9393 streamOperation ?: string
9494 disableStreamingAutoScroll ?: boolean
@@ -339,11 +339,11 @@ export function LoadedRichMarkdownEditor({
339339 streamingAtMountRef . current ? null : splitFrontmatter ( content ) . body
340340 )
341341 /**
342- * The body the AGENT last streamed into the collaborative doc — the extend-gate baseline for the collab
343- * streaming path. Written ONLY at stream start (snapshotting the pre-stream base) and by the streaming
344- * tick, never by `onUpdate`, so a concurrent PEER edit (which does clobber { @link lastSyncedBodyRef} via
345- * `onUpdate`) can't make the agent's growing snapshot stop prefixing the shown body and stall the stream.
346- * Reset to `null` on settle so the next stream re-captures its own baseline .
342+ * The body the AGENT last applied into the collaborative doc — a dedup guard for the collab streaming
343+ * tick, so an unchanged frame skips a redundant shadow reconcile/reparse. Written ONLY by the streaming
344+ * tick ( never by `onUpdate`), and reset to `null` on settle for the next stream. It is NOT a string-prefix
345+ * baseline: the mid-stream hold is decided by operation (`update` waits for settle), not by comparing the
346+ * raw preview against the editor's canonical markdown .
347347 */
348348 const lastStreamedBodyRef = useRef < string | null > ( null )
349349 const onChangeRef = useRef ( onChange )
@@ -872,13 +872,10 @@ export function LoadedRichMarkdownEditor({
872872 // re-runs and applies once it lands (the read-only placeholder shows the base content meanwhile —
873873 // see `showPlaceholder`).
874874 if ( ! collabReady ) return
875- // Open the stream's shadow on the FIRST ready frame — BEFORE the extend gate — so its shadow
876- // captures the pre-stream base (immune to later peer edits) even for an `update` whose every frame
877- // is gated out until settle. Snapshot that base as the agent's private extend-gate baseline.
878- if ( agentStreamSessionRef . current === null ) {
879- agentStreamSessionRef . current = beginAgentStream ( editor )
880- lastStreamedBodyRef . current = lastSyncedBodyRef . current
881- }
875+ // Open the stream's shadow on the FIRST ready frame so it captures the pre-stream base (immune to
876+ // later peer edits) — including for an `update`, whose frames are all held until settle, so settle
877+ // still has a shadow through which to apply the final rewrite.
878+ agentStreamSessionRef . current ??= beginAgentStream ( editor )
882879 const session = agentStreamSessionRef . current
883880 const body = splitFrontmatter ( content ) . body
884881 if ( body === lastStreamedBodyRef . current ) return
@@ -890,15 +887,11 @@ export function LoadedRichMarkdownEditor({
890887 streamRafRef . current = null
891888 return
892889 }
893- const shownBody = lastStreamedBodyRef . current
894- const extendsShown = shownBody === null || pending . startsWith ( shownBody )
895- // Every snapshot except a mid-document `patch` must EXTEND what the AGENT last streamed: a
896- // from-scratch rebuild (`create`/`update`) is only revealed as it grows, and an `append`
897- // snapshot that doesn't extend the base is a base-less fragment (the server emits one before the
898- // base loads) which would reconcile the seeded doc down to a wipe. Only `patch` replaces a
899- // mid-region. The baseline is the agent's own last frame (not the editor body), so a concurrent
900- // peer edit can't make the growing snapshot stop prefixing it and stall the stream.
901- if ( ! extendsShown && streamOperationRef . current !== 'patch' ) {
890+ // Hold a from-scratch rewrite (`update`) until settle so the open doc doesn't collapse to a
891+ // partial rewrite mid-stream (matching `main`). `append`/`patch`/`create` apply each frame — the
892+ // shadow reconcile is peer-safe, and base-less `append` fragments no longer reach the client (the
893+ // server fail-closes them), so there is nothing here to string-prefix or wipe-guard against.
894+ if ( streamOperationRef . current === 'update' ) {
902895 streamRafRef . current = null
903896 return
904897 }
@@ -929,11 +922,10 @@ export function LoadedRichMarkdownEditor({
929922 cancelAnimationFrame ( streamRafRef . current )
930923 streamRafRef . current = null
931924 }
932- // Settle: apply the FINAL body once so the Y.Doc exactly equals the streamed result — even when
933- // every mid-stream frame was gated out (an `update` rewrite never extends the base) or the stream
934- // finished before the seed, cases where no frame applied. Reuse the stream's shadow when it exists
935- // (seeded from the pre-stream base, so peer edits survive); otherwise open one on demand. The
936- // durable server write then lands as a noop diff.
925+ // Settle: apply the FINAL body once so the Y.Doc exactly equals the streamed result — even when no
926+ // frame applied mid-stream (an `update` is held until settle, or the stream finished before the
927+ // seed). Reuse the stream's shadow when it exists (seeded from the pre-stream base, so peer edits
928+ // survive); otherwise open one on demand. The durable server write then lands as a noop diff.
937929 if ( wasStreamingRef . current && collabReady ) {
938930 wasStreamingRef . current = false
939931 const finalBody = splitFrontmatter ( content ) . body
0 commit comments