Skip to content

Commit cf84e05

Browse files
improvement(path): append, patch snapshot based streaming (#5161)
1 parent cb17207 commit cf84e05

3 files changed

Lines changed: 36 additions & 3 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ interface FileViewerProps {
9292
saveRef?: React.MutableRefObject<(() => Promise<void>) | null>
9393
streamingContent?: string
9494
isAgentEditing?: boolean
95+
streamIsIncremental?: boolean
9596
disableStreamingAutoScroll?: boolean
9697
previewContextKey?: string
9798
}
@@ -108,6 +109,7 @@ export function FileViewer({
108109
saveRef,
109110
streamingContent,
110111
isAgentEditing,
112+
streamIsIncremental,
111113
disableStreamingAutoScroll = false,
112114
previewContextKey,
113115
}: FileViewerProps) {
@@ -150,6 +152,7 @@ export function FileViewer({
150152
saveRef={saveRef}
151153
streamingContent={streamingContent}
152154
isAgentEditing={isAgentEditing}
155+
streamIsIncremental={streamIsIncremental}
153156
disableStreamingAutoScroll={disableStreamingAutoScroll}
154157
previewContextKey={previewContextKey}
155158
/>

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ interface RichMarkdownEditorProps {
4545
saveRef?: React.MutableRefObject<(() => Promise<void>) | null>
4646
streamingContent?: string
4747
isAgentEditing?: boolean
48+
/**
49+
* True when the stream delivers complete full-file snapshots (an `append`/`patch` edit built on the
50+
* existing file) rather than a from-scratch rebuild (`create`/`update`). Incremental snapshots are
51+
* applied live; a rebuild is only revealed while it extends what's shown (see the streaming tick).
52+
*/
53+
streamIsIncremental?: boolean
4854
disableStreamingAutoScroll?: boolean
4955
previewContextKey?: string
5056
}
@@ -60,6 +66,7 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({
6066
saveRef,
6167
streamingContent,
6268
isAgentEditing,
69+
streamIsIncremental,
6370
disableStreamingAutoScroll = false,
6471
previewContextKey,
6572
}: RichMarkdownEditorProps) {
@@ -101,6 +108,7 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({
101108
isStreaming={isStreamInteractionLocked}
102109
canEdit={canEdit}
103110
autoFocus={autoFocus}
111+
streamIsIncremental={streamIsIncremental}
104112
disableStreamingAutoScroll={disableStreamingAutoScroll}
105113
onChange={setDraftContent}
106114
onSaveShortcut={saveImmediately}
@@ -117,6 +125,8 @@ interface LoadedRichMarkdownEditorProps {
117125
isStreaming: boolean
118126
canEdit: boolean
119127
autoFocus?: boolean
128+
/** See {@link RichMarkdownEditorProps.streamIsIncremental}. */
129+
streamIsIncremental?: boolean
120130
disableStreamingAutoScroll?: boolean
121131
onChange: (markdown: string) => void
122132
onSaveShortcut: () => Promise<void>
@@ -140,6 +150,7 @@ export function LoadedRichMarkdownEditor({
140150
isStreaming,
141151
canEdit,
142152
autoFocus,
153+
streamIsIncremental,
143154
disableStreamingAutoScroll,
144155
onChange,
145156
onSaveShortcut,
@@ -160,8 +171,9 @@ export function LoadedRichMarkdownEditor({
160171
)
161172
/**
162173
* The body currently shown in the editor: seeded from a settled mount, updated on local edits (via
163-
* onUpdate) and on each streamed sync. The streaming tick reveals a chunk only when it extends this,
164-
* so an agent rewrite holds the current content instead of collapsing to a partial result.
174+
* onUpdate) and on each streamed sync. Incremental edits (append/patch) stream complete snapshots and
175+
* always apply; a from-scratch rebuild (create/update) only applies while it still extends this, so a
176+
* rewrite holds the current content instead of collapsing to a partial result.
165177
*/
166178
const lastSyncedBodyRef = useRef<string | null>(
167179
streamingAtMountRef.current ? null : splitFrontmatter(content).body
@@ -170,6 +182,10 @@ export function LoadedRichMarkdownEditor({
170182
onChangeRef.current = onChange
171183
const onSaveShortcutRef = useRef(onSaveShortcut)
172184
onSaveShortcutRef.current = onSaveShortcut
185+
// Read in the RAF tick so an already-scheduled tick still sees the latest edit kind (it can change
186+
// between sessions within one turn, e.g. an append followed by a rewrite).
187+
const streamIsIncrementalRef = useRef(streamIsIncremental)
188+
streamIsIncrementalRef.current = streamIsIncremental
173189
const router = useRouter()
174190
const routerRef = useRef(router)
175191
routerRef.current = router
@@ -300,7 +316,11 @@ export function LoadedRichMarkdownEditor({
300316
}
301317
const shownBody = lastSyncedBodyRef.current
302318
const extendsShown = shownBody === null || pending.startsWith(shownBody)
303-
if (!extendsShown) {
319+
// Incremental edits (append/patch) arrive as complete full-file snapshots, so each is applied
320+
// live — ProseMirror diffs the localized change in place (mid-doc rewrite, insertion, delete).
321+
// A rebuild (create/update streamed from scratch) only extends while revealing from empty; once
322+
// a chunk would collapse the established document it is held until settle, avoiding the flicker.
323+
if (!streamIsIncrementalRef.current && !extendsShown) {
304324
streamRafRef.current = null
305325
return
306326
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ export const ResourceContent = memo(function ResourceContent({
167167
}, [workspaceId, streamFileName])
168168

169169
const disableStreamingAutoScroll = previewSession?.operation === 'patch'
170+
// `append`/`patch` stream complete full-file snapshots (built on the existing file), so the editor
171+
// applies each live. `create`/`update` are streamed from scratch and would collapse an open doc, so
172+
// the editor holds until settle. See the rich-markdown streaming tick.
173+
const streamIsIncremental =
174+
previewSession?.operation === 'append' || previewSession?.operation === 'patch'
170175
const isTextPreview =
171176
!!previewSession && resolveFileCategory(null, previewSession.fileName) === 'text-editable'
172177
// Feed streamed content only while actively streaming. On completion the session keeps
@@ -195,6 +200,7 @@ export const ResourceContent = memo(function ResourceContent({
195200
previewMode={previewMode ?? 'preview'}
196201
streamingContent={textStreamingContent}
197202
isAgentEditing={isAgentEditing}
203+
streamIsIncremental={streamIsIncremental}
198204
disableStreamingAutoScroll={disableStreamingAutoScroll}
199205
previewContextKey={previewContextKey}
200206
/>
@@ -218,6 +224,7 @@ export const ResourceContent = memo(function ResourceContent({
218224
previewSession?.fileId === resource.id ? textStreamingContent : undefined
219225
}
220226
isAgentEditing={isAgentEditing}
227+
streamIsIncremental={streamIsIncremental}
221228
disableStreamingAutoScroll={disableStreamingAutoScroll}
222229
previewContextKey={previewContextKey}
223230
/>
@@ -604,6 +611,7 @@ interface EmbeddedFileProps {
604611
previewMode?: PreviewMode
605612
streamingContent?: string
606613
isAgentEditing?: boolean
614+
streamIsIncremental?: boolean
607615
disableStreamingAutoScroll?: boolean
608616
previewContextKey?: string
609617
}
@@ -615,6 +623,7 @@ function EmbeddedFile({
615623
previewMode,
616624
streamingContent,
617625
isAgentEditing,
626+
streamIsIncremental,
618627
disableStreamingAutoScroll = false,
619628
previewContextKey,
620629
}: EmbeddedFileProps) {
@@ -657,6 +666,7 @@ function EmbeddedFile({
657666
previewMode={previewMode}
658667
streamingContent={streamingContent}
659668
isAgentEditing={isAgentEditing}
669+
streamIsIncremental={streamIsIncremental}
660670
disableStreamingAutoScroll={disableStreamingAutoScroll}
661671
previewContextKey={previewContextKey}
662672
/>

0 commit comments

Comments
 (0)