11'use client'
22
3- import { memo , useEffect , useRef } from 'react'
3+ import { memo , useEffect , useRef , useState } from 'react'
44import type { JSONContent } from '@tiptap/core'
55import type { Editor } from '@tiptap/react'
66import { EditorContent , useEditor } from '@tiptap/react'
@@ -158,28 +158,24 @@ export function LoadedRichMarkdownEditor({
158158 onChange,
159159 onSaveShortcut,
160160} : LoadedRichMarkdownEditorProps ) {
161- // Whether this editor mounted mid-stream. If so it starts empty + read-only and syncs the streamed
162- // content until the stream settles; otherwise it uses the plain create-time initial-content model.
161+ // Whether this editor mounted mid-stream — if so it starts empty and syncs streamed chunks until settle.
163162 const streamingAtMountRef = useRef ( isStreaming )
164163
165- // The verdict + frontmatter locked via {@link lockSettled} — at mount for a settled file, or at the
166- // moment a stream settles (in the effect below). Null until then; reads default to read-only.
164+ // Verdict + frontmatter locked once via {@link lockSettled} ( at mount when settled, else when the
165+ // stream settles below); null until then reads as read-only.
167166 const settledRef = useRef < SettledContent | null > ( null )
168167 if ( ! streamingAtMountRef . current && settledRef . current === null ) {
169168 settledRef . current = lockSettled ( content )
170169 }
171170 const isEditable = canEdit && ! isStreaming && ( settledRef . current ?. verdict ?? false )
172171
173- // The parsed doc that seeds the editor at create time — chunked-parsed (linear) rather than handed
174- // to the editor as a raw markdown string (whose parse is ~O(n²)). Empty when streaming: the sync
175- // effect pushes the streamed body in via setContent (this ref is never written again).
176- const initialContentRef = useRef < JSONContent | string > (
172+ // Seed the editor with the chunked-parsed doc (linear vs the editor's ~O(n²) markdown parse), computed
173+ // once via lazy state init — `useRef(parseMarkdownToDoc(...))` would re-parse the whole body every render.
174+ const [ initialContent ] = useState < JSONContent | string > ( ( ) =>
177175 streamingAtMountRef . current ? '' : parseMarkdownToDoc ( splitFrontmatter ( content ) . body )
178176 )
179- // The frontmatter re-attached on every change. Empty until the content settles (the editor never
180- // displays frontmatter, so a streamed doc simply shows its body). Re-derived in the settle effect
181- // on each stream→settle, so a repeat stream re-attaches the settled doc's frontmatter, never a
182- // stale one.
177+ // Frontmatter held aside and re-attached on every change (the editor never shows it); re-derived per
178+ // stream→settle in the settle effect, so a repeat stream uses the new doc's frontmatter, not a stale one.
183179 const frontmatterRef = useRef ( settledRef . current ?. frontmatter ?? '' )
184180 const onChangeRef = useRef ( onChange )
185181 onChangeRef . current = onChange
@@ -229,7 +225,7 @@ export function LoadedRichMarkdownEditor({
229225 autofocus : streamingAtMountRef . current ? false : autoFocus ? 'end' : false ,
230226 immediatelyRender : false ,
231227 shouldRerenderOnTransaction : false ,
232- content : initialContentRef . current ,
228+ content : initialContent ,
233229 editorProps : {
234230 attributes : { class : 'rich-markdown-prose' } ,
235231 handleKeyDown : ( _view , event ) => {
@@ -273,19 +269,15 @@ export function LoadedRichMarkdownEditor({
273269 } )
274270 editorInstanceRef . current = editor
275271
276- // Stream content into the editor (read-only) until it settles, then lock the verdict + frontmatter
277- // and hand control to the user. After the hand-off, only `canEdit` changes touch the editor — the
278- // editor owns the content, so there is no sync that could clobber a user edit.
272+ // Stream content in read-only until it settles, then lock the verdict + frontmatter and hand off; after
273+ // that only `canEdit` touches the editor (it owns the content, so no sync can clobber a user edit).
279274 const lastSyncedBodyRef = useRef < string | null > ( null )
280- // Whether the editor was streaming on the previous effect run, so the settle branch can re-lock on
281- // each stream→settle transition. An agent can edit the same file more than once within a chat, and
282- // `previewContextKey` (the chat id) keeps this instance mounted across those edits — so the verdict
283- // + frontmatter must be re-derived per stream, not frozen on the first settled snapshot.
275+ // Tracks whether the previous run was streaming so the settle branch re-locks on every stream→settle:
276+ // one instance can receive several agent edits in a chat (kept mounted by `previewContextKey`), so the
277+ // verdict/frontmatter must follow the latest stream, not the first settled snapshot.
284278 const wasStreamingRef = useRef ( streamingAtMountRef . current )
285- // Coalesce streaming chunk-syncs to one re-parse per animation frame. A fast-streaming agent emits
286- // many chunks per frame; without this each one re-parses the whole accumulating markdown
287- // (`@tiptap/markdown`'s parse is superlinear), saturating the main thread. The editor is read-only
288- // while streaming, so only the latest frame's content needs to render.
279+ // Coalesce streamed chunks to one re-parse per animation frame — a fast agent emits many per frame and
280+ // each would re-parse the whole accumulating body. Read-only while streaming, so only the latest renders.
289281 const pendingStreamBodyRef = useRef < string | null > ( null )
290282 const streamRafRef = useRef < number | null > ( null )
291283 useEffect ( ( ) => {
@@ -312,16 +304,14 @@ export function LoadedRichMarkdownEditor({
312304 } )
313305 return
314306 }
315- // A streamed frame scheduled just before settle must not land afterward and clobber the final
316- // content, so drop it before settling.
307+ // Drop a frame scheduled just before settle so it can't land afterward and clobber the final content.
317308 if ( streamRafRef . current !== null ) {
318309 cancelAnimationFrame ( streamRafRef . current )
319310 streamRafRef . current = null
320311 }
321- // Settle: lock the verdict + frontmatter on the freshly-settled content. Re-lock on the initial
322- // settle and on every later stream→settle, so a repeat agent edit gates editability + frontmatter
323- // on the NEW content rather than a stale pre-stream snapshot. User edits never re-derive (they
324- // keep `isStreaming`/`wasStreamingRef` false), preserving the don't-strand-edits rule.
312+ // Settle: re-lock the verdict + frontmatter on the freshly-settled content — on the first settle and
313+ // every later stream→settle, so a repeat agent edit gates on the NEW content, not a stale snapshot.
314+ // User edits never reach here (`isStreaming`/`wasStreamingRef` stay false), preserving don't-strand-edits.
325315 const isInitialSettle = settledRef . current === null
326316 if ( isInitialSettle || wasStreamingRef . current ) {
327317 wasStreamingRef . current = false
0 commit comments