Skip to content

Commit 6e8bd21

Browse files
committed
fix(file-viewer): re-lock round-trip verdict + frontmatter on each stream settle
LoadedRichMarkdownEditor stays mounted across multiple agent edits to the same file within a chat (previewContextKey is the chat id), but the settle effect only locked settledRef when it was null — so a second stream into the same instance kept editability and frontmatter tied to the first settled snapshot. A repeat edit that is round-trip-unsafe would stay editable, and saves would re-attach the stale frontmatter. Track wasStreaming and re-derive the verdict + frontmatter on every stream->settle transition (user edits never re-derive, preserving the don't-strand-edits rule). Verified red/green in the e2e streaming harness.
1 parent 49868aa commit 6e8bd21

1 file changed

Lines changed: 20 additions & 6 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,10 @@ export function LoadedRichMarkdownEditor({
172172
// streamed body in via setContent (this ref is never written again).
173173
const initialBodyRef = useRef(streamingAtMountRef.current ? '' : splitFrontmatter(content).body)
174174
// The frontmatter re-attached on every change. Empty until the content settles (the editor never
175-
// displays frontmatter, so a streamed doc simply shows its body).
176-
const frontmatterRef = useRef('')
177-
frontmatterRef.current = settledRef.current?.frontmatter ?? ''
175+
// displays frontmatter, so a streamed doc simply shows its body). Re-derived in the settle effect
176+
// on each stream→settle, so a repeat stream re-attaches the settled doc's frontmatter, never a
177+
// stale one.
178+
const frontmatterRef = useRef(settledRef.current?.frontmatter ?? '')
178179
const onChangeRef = useRef(onChange)
179180
onChangeRef.current = onChange
180181
const onSaveShortcutRef = useRef(onSaveShortcut)
@@ -272,9 +273,15 @@ export function LoadedRichMarkdownEditor({
272273
// and hand control to the user. After the hand-off, only `canEdit` changes touch the editor — the
273274
// editor owns the content, so there is no sync that could clobber a user edit.
274275
const lastSyncedBodyRef = useRef<string | null>(null)
276+
// Whether the editor was streaming on the previous effect run, so the settle branch can re-lock on
277+
// each stream→settle transition. An agent can edit the same file more than once within a chat, and
278+
// `previewContextKey` (the chat id) keeps this instance mounted across those edits — so the verdict
279+
// + frontmatter must be re-derived per stream, not frozen on the first settled snapshot.
280+
const wasStreamingRef = useRef(streamingAtMountRef.current)
275281
useEffect(() => {
276282
if (!editor) return
277283
if (isStreaming) {
284+
wasStreamingRef.current = true
278285
const body = splitFrontmatter(content).body
279286
if (body === lastSyncedBodyRef.current) return
280287
lastSyncedBodyRef.current = body
@@ -285,8 +292,15 @@ export function LoadedRichMarkdownEditor({
285292
if (!disableStreamingAutoScroll && el && pinnedToBottom) el.scrollTop = el.scrollHeight
286293
return
287294
}
288-
if (settledRef.current === null) {
295+
// Settle: lock the verdict + frontmatter on the freshly-settled content. Re-lock on the initial
296+
// settle and on every later stream→settle, so a repeat agent edit gates editability + frontmatter
297+
// on the NEW content rather than a stale pre-stream snapshot. User edits never re-derive (they
298+
// keep `isStreaming`/`wasStreamingRef` false), preserving the don't-strand-edits rule.
299+
const isInitialSettle = settledRef.current === null
300+
if (isInitialSettle || wasStreamingRef.current) {
301+
wasStreamingRef.current = false
289302
settledRef.current = lockSettled(content)
303+
frontmatterRef.current = settledRef.current.frontmatter
290304
// Re-seed only if the settled body differs from the last streamed chunk — it usually doesn't,
291305
// and an extra setContent would needlessly rebuild the doc and drop selection/scroll.
292306
const body = splitFrontmatter(content).body
@@ -295,10 +309,10 @@ export function LoadedRichMarkdownEditor({
295309
editor.commands.setContent(body, { contentType: 'markdown', emitUpdate: false })
296310
}
297311
editor.setEditable(canEdit && settledRef.current.verdict)
298-
if (autoFocus) editor.commands.focus('end')
312+
if (isInitialSettle && autoFocus) editor.commands.focus('end')
299313
return
300314
}
301-
editor.setEditable(canEdit && settledRef.current.verdict)
315+
if (settledRef.current) editor.setEditable(canEdit && settledRef.current.verdict)
302316
}, [editor, content, isStreaming, canEdit, autoFocus, disableStreamingAutoScroll])
303317

304318
return (

0 commit comments

Comments
 (0)