improvement(path): append, patch snapshot based streaming (#5161)

This commit is contained in:
Vikhyath Mondreti
2026-06-20 19:09:34 -07:00
committed by GitHub
parent cb17207447
commit cf84e051cd
3 changed files with 36 additions and 3 deletions
@@ -92,6 +92,7 @@ interface FileViewerProps {
saveRef?: React.MutableRefObject<(() => Promise<void>) | 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}
/>
@@ -45,6 +45,12 @@ interface RichMarkdownEditorProps {
saveRef?: React.MutableRefObject<(() => Promise<void>) | 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<void>
@@ -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<string | null>(
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
}
@@ -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}
/>