Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import 'prismjs/components/prism-python'
import { cn } from '@/lib/core/utils/cn'
import { extractTextContent } from '@/lib/core/utils/react-node-text'
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
import { useAutoScroll } from '@/hooks/use-auto-scroll'
import { useScrollAnchor } from '@/hooks/use-scroll-anchor'
import { DataTable } from './data-table'
import { ZoomablePreview } from './zoomable-preview'

Expand Down Expand Up @@ -866,7 +866,10 @@ const MarkdownPreview = memo(function MarkdownPreview({
onCheckboxToggle?: (checkboxIndex: number, checked: boolean) => void
}) {
const { push: navigate } = useRouter()
const { ref: autoScrollRef } = useAutoScroll(isStreaming && !disableAutoScroll)
const { ref: autoScrollRef, spacerRef } = useScrollAnchor(
isStreaming && !disableAutoScroll,
content
)

const contentRef = useRef(content)
contentRef.current = content
Expand Down Expand Up @@ -921,16 +924,13 @@ const MarkdownPreview = memo(function MarkdownPreview({
>
{markdownContent}
</Streamdown>
<div ref={spacerRef} aria-hidden />
</div>
)

return (
<NavigateCtx.Provider value={navigate}>
{onCheckboxToggle ? (
<MarkdownCheckboxCtx.Provider value={ctxValue}>{body}</MarkdownCheckboxCtx.Provider>
) : (
body
)}
<MarkdownCheckboxCtx.Provider value={ctxValue}>{body}</MarkdownCheckboxCtx.Provider>
</NavigateCtx.Provider>
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,99 @@ describe('syncTextEditorContentState — streaming finalize shortcuts', () => {
expect(next.content).toBe('v2')
})
})

describe('syncTextEditorContentState — inter-session content shrink (replace mode)', () => {
it('replaces long linger content with a short first chunk from a new session', () => {
const lingerState = streaming(
'a very long document with many paragraphs',
'a very long document with many paragraphs',
''
)
const next = syncTextEditorContentState(lingerState, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: 'short',
streamingMode: 'replace',
})
expect(next.phase).toBe('streaming')
expect(next.content).toBe('short')
expect(next.lastStreamedContent).toBe('short')
})

it('correctly transitions to the new chunk even when it is a single character', () => {
const lingerState = streaming(
'full document\nmany lines\nof content',
'full document\nmany lines\nof content',
''
)
const next = syncTextEditorContentState(lingerState, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: '#',
streamingMode: 'replace',
})
expect(next.phase).toBe('streaming')
expect(next.content).toBe('#')
})

it('does not finalize early when the new short chunk happens to equal savedContent', () => {
const lingerState = streaming('long content', 'long content', 'old saved')
const next = syncTextEditorContentState(lingerState, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: '',
streamingMode: 'replace',
})
expect(next.phase).toBe('streaming')
expect(next.content).toBe('')
})

it('stays streaming across multiple growing chunks after the shrink', () => {
const lingerState = streaming('final long document', 'final long document', '')

const chunk1 = syncTextEditorContentState(lingerState, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: '# New',
streamingMode: 'replace',
})
expect(chunk1.phase).toBe('streaming')
expect(chunk1.content).toBe('# New')

const chunk2 = syncTextEditorContentState(chunk1, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: '# New Section\n\nSome text',
streamingMode: 'replace',
})
expect(chunk2.phase).toBe('streaming')
expect(chunk2.content).toBe('# New Section\n\nSome text')

const chunk3 = syncTextEditorContentState(chunk2, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: '# New Section\n\nSome text that is now longer than the original',
streamingMode: 'replace',
})
expect(chunk3.phase).toBe('streaming')
expect(chunk3.content).toBe('# New Section\n\nSome text that is now longer than the original')
})

it('synthetic file (canReconcile=false) finalizes with current content when streaming ends', () => {
const finalChunk = streaming(
'# Complete Document\n\nAll done.',
'# Complete Document\n\nAll done.',
''
)
const next = syncTextEditorContentState(finalChunk, {
canReconcileToFetchedContent: false,
fetchedContent: undefined,
streamingContent: undefined,
streamingMode: 'replace',
})
expect(next.phase).toBe('ready')
expect(next.content).toBe('# Complete Document\n\nAll done.')
expect(next.savedContent).toBe('# Complete Document\n\nAll done.')
expect(next.lastStreamedContent).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ function shouldShowStreamingFilePanel(
previewSession: FilePreviewSession | null | undefined,
active: MothershipResource | null
): boolean {
if (!previewSession || previewSession.status === 'complete' || !active) return false
if (!previewSession || !hasRenderableFilePreviewContent(previewSession) || !active) return false
if (active.id === 'streaming-file') return true
if (active.type !== 'file') return false
if (active.id && previewSession.fileId === active.id) {
return hasRenderableFilePreviewContent(previewSession)
return true
}
return false
}
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3402,7 +3402,11 @@ export function useChat(
if (parentToolCallId) {
subagentByParentToolCallId.delete(parentToolCallId)
}
if (previewSessionRef.current && !activePreviewSessionIdRef.current) {
if (
previewSessionRef.current &&
(!activePreviewSessionIdRef.current ||
previewSessionRef.current.status === 'complete')
) {
const lastFileResource = resourcesRef.current.find(
(r) => r.type === 'file' && r.id !== 'streaming-file'
)
Expand Down
Loading
Loading