diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index de013ffa4d..2e1d4d834e 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -92,6 +92,7 @@ interface FileViewerProps { saveRef?: React.MutableRefObject<(() => Promise) | null> streamingContent?: string isAgentEditing?: boolean + streamIsIncremental?: boolean disableStreamingAutoScroll?: boolean previewContextKey?: string } @@ -108,6 +109,7 @@ export function FileViewer({ saveRef, streamingContent, isAgentEditing, + streamIsIncremental, disableStreamingAutoScroll = false, previewContextKey, }: FileViewerProps) { @@ -150,6 +152,7 @@ export function FileViewer({ saveRef={saveRef} streamingContent={streamingContent} isAgentEditing={isAgentEditing} + streamIsIncremental={streamIsIncremental} disableStreamingAutoScroll={disableStreamingAutoScroll} previewContextKey={previewContextKey} /> diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx index afe14867be..2a79be7719 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx @@ -45,6 +45,12 @@ interface RichMarkdownEditorProps { saveRef?: React.MutableRefObject<(() => Promise) | null> streamingContent?: string isAgentEditing?: boolean + /** + * True when the stream delivers complete full-file snapshots (an `append`/`patch` edit built on the + * existing file) rather than a from-scratch rebuild (`create`/`update`). Incremental snapshots are + * applied live; a rebuild is only revealed while it extends what's shown (see the streaming tick). + */ + streamIsIncremental?: boolean disableStreamingAutoScroll?: boolean previewContextKey?: string } @@ -60,6 +66,7 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({ saveRef, streamingContent, isAgentEditing, + streamIsIncremental, disableStreamingAutoScroll = false, previewContextKey, }: RichMarkdownEditorProps) { @@ -101,6 +108,7 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({ isStreaming={isStreamInteractionLocked} canEdit={canEdit} autoFocus={autoFocus} + streamIsIncremental={streamIsIncremental} disableStreamingAutoScroll={disableStreamingAutoScroll} onChange={setDraftContent} onSaveShortcut={saveImmediately} @@ -117,6 +125,8 @@ interface LoadedRichMarkdownEditorProps { isStreaming: boolean canEdit: boolean autoFocus?: boolean + /** See {@link RichMarkdownEditorProps.streamIsIncremental}. */ + streamIsIncremental?: boolean disableStreamingAutoScroll?: boolean onChange: (markdown: string) => void onSaveShortcut: () => Promise @@ -140,6 +150,7 @@ export function LoadedRichMarkdownEditor({ isStreaming, canEdit, autoFocus, + streamIsIncremental, disableStreamingAutoScroll, onChange, onSaveShortcut, @@ -160,8 +171,9 @@ export function LoadedRichMarkdownEditor({ ) /** * The body currently shown in the editor: seeded from a settled mount, updated on local edits (via - * onUpdate) and on each streamed sync. The streaming tick reveals a chunk only when it extends this, - * so an agent rewrite holds the current content instead of collapsing to a partial result. + * onUpdate) and on each streamed sync. Incremental edits (append/patch) stream complete snapshots and + * always apply; a from-scratch rebuild (create/update) only applies while it still extends this, so a + * rewrite holds the current content instead of collapsing to a partial result. */ const lastSyncedBodyRef = useRef( streamingAtMountRef.current ? null : splitFrontmatter(content).body @@ -170,6 +182,10 @@ export function LoadedRichMarkdownEditor({ onChangeRef.current = onChange const onSaveShortcutRef = useRef(onSaveShortcut) onSaveShortcutRef.current = onSaveShortcut + // Read in the RAF tick so an already-scheduled tick still sees the latest edit kind (it can change + // between sessions within one turn, e.g. an append followed by a rewrite). + const streamIsIncrementalRef = useRef(streamIsIncremental) + streamIsIncrementalRef.current = streamIsIncremental const router = useRouter() const routerRef = useRef(router) routerRef.current = router @@ -300,7 +316,11 @@ export function LoadedRichMarkdownEditor({ } const shownBody = lastSyncedBodyRef.current const extendsShown = shownBody === null || pending.startsWith(shownBody) - if (!extendsShown) { + // Incremental edits (append/patch) arrive as complete full-file snapshots, so each is applied + // live — ProseMirror diffs the localized change in place (mid-doc rewrite, insertion, delete). + // A rebuild (create/update streamed from scratch) only extends while revealing from empty; once + // a chunk would collapse the established document it is held until settle, avoiding the flicker. + if (!streamIsIncrementalRef.current && !extendsShown) { streamRafRef.current = null return } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index b91a4e0a44..549aff3d04 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -167,6 +167,11 @@ export const ResourceContent = memo(function ResourceContent({ }, [workspaceId, streamFileName]) const disableStreamingAutoScroll = previewSession?.operation === 'patch' + // `append`/`patch` stream complete full-file snapshots (built on the existing file), so the editor + // applies each live. `create`/`update` are streamed from scratch and would collapse an open doc, so + // the editor holds until settle. See the rich-markdown streaming tick. + const streamIsIncremental = + previewSession?.operation === 'append' || previewSession?.operation === 'patch' const isTextPreview = !!previewSession && resolveFileCategory(null, previewSession.fileName) === 'text-editable' // Feed streamed content only while actively streaming. On completion the session keeps @@ -195,6 +200,7 @@ export const ResourceContent = memo(function ResourceContent({ previewMode={previewMode ?? 'preview'} streamingContent={textStreamingContent} isAgentEditing={isAgentEditing} + streamIsIncremental={streamIsIncremental} disableStreamingAutoScroll={disableStreamingAutoScroll} previewContextKey={previewContextKey} /> @@ -218,6 +224,7 @@ export const ResourceContent = memo(function ResourceContent({ previewSession?.fileId === resource.id ? textStreamingContent : undefined } isAgentEditing={isAgentEditing} + streamIsIncremental={streamIsIncremental} disableStreamingAutoScroll={disableStreamingAutoScroll} previewContextKey={previewContextKey} /> @@ -604,6 +611,7 @@ interface EmbeddedFileProps { previewMode?: PreviewMode streamingContent?: string isAgentEditing?: boolean + streamIsIncremental?: boolean disableStreamingAutoScroll?: boolean previewContextKey?: string } @@ -615,6 +623,7 @@ function EmbeddedFile({ previewMode, streamingContent, isAgentEditing, + streamIsIncremental, disableStreamingAutoScroll = false, previewContextKey, }: EmbeddedFileProps) { @@ -657,6 +666,7 @@ function EmbeddedFile({ previewMode={previewMode} streamingContent={streamingContent} isAgentEditing={isAgentEditing} + streamIsIncremental={streamIsIncremental} disableStreamingAutoScroll={disableStreamingAutoScroll} previewContextKey={previewContextKey} />