Skip to content

Commit 49868aa

Browse files
committed
refactor(file-viewer): drop dead streamingMode/append path, align naming, cover autosave
The streaming engine only ever runs in 'replace' mode (the only runtime callers pass it); the 'append' branch of resolveStreamingEditorContent was unreachable. Remove streamingMode + the StreamingMode type and thread it out of the 6 components that forwarded it — nextContent is now simply the streamed snapshot, behavior-identical on the live path. Rename for codebase semantics: the boolean prop streaming -> isStreaming, EditorKeymap -> RichMarkdownKeymap, the highlight PluginKey KEY -> HIGHLIGHT_PLUGIN_KEY. Add a defensive isEditable guard to the markdown paste handler (parity with the image handler; read-only must never mutate). Add a dependency-free useAutosave test suite (debounce, min-display window, no-data-loss when an edit lands mid-save, error/no-retry, Cmd+S flush, streaming-disabled lock, unmount flush).
1 parent 63a4f67 commit 49868aa

12 files changed

Lines changed: 288 additions & 138 deletions

File tree

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ import dynamic from 'next/dynamic'
66
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
77
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
88
import { useWorkspaceFileBinary, useWorkspaceFileContent } from '@/hooks/queries/workspace-files'
9-
import { resolveFileCategory } from './file-category'
10-
import type { StreamingMode } from './text-editor-state'
11-
import { useDocPreviewBinary } from './use-doc-preview-binary'
12-
13-
export type { StreamingMode } from './text-editor-state'
14-
159
import { CsvTablePreview } from './csv-table-preview'
1610
import { DocxPreview } from './docx-preview'
11+
import { resolveFileCategory } from './file-category'
1712
import { ImagePreview } from './image-preview'
1813
import type { PdfDocumentSource } from './pdf-viewer'
1914
import { PptxPreview } from './pptx-preview'
@@ -26,6 +21,7 @@ import {
2621
resolvePreviewError,
2722
} from './preview-shared'
2823
import { TextEditor } from './text-editor'
24+
import { useDocPreviewBinary } from './use-doc-preview-binary'
2925
import { XlsxPreview } from './xlsx-preview'
3026

3127
const PdfViewerCore = dynamic(() => import('./pdf-viewer').then((m) => m.PdfViewerCore), {
@@ -95,7 +91,6 @@ interface FileViewerProps {
9591
onSaveStatusChange?: (status: 'idle' | 'saving' | 'saved' | 'error') => void
9692
saveRef?: React.MutableRefObject<(() => Promise<void>) | null>
9793
streamingContent?: string
98-
streamingMode?: StreamingMode
9994
disableStreamingAutoScroll?: boolean
10095
previewContextKey?: string
10196
}
@@ -111,7 +106,6 @@ export function FileViewer({
111106
onSaveStatusChange,
112107
saveRef,
113108
streamingContent,
114-
streamingMode,
115109
disableStreamingAutoScroll = false,
116110
previewContextKey,
117111
}: FileViewerProps) {
@@ -153,7 +147,6 @@ export function FileViewer({
153147
onSaveStatusChange={onSaveStatusChange}
154148
saveRef={saveRef}
155149
streamingContent={streamingContent}
156-
streamingMode={streamingMode}
157150
disableStreamingAutoScroll={disableStreamingAutoScroll}
158151
previewContextKey={previewContextKey}
159152
/>
@@ -171,7 +164,6 @@ export function FileViewer({
171164
onSaveStatusChange={onSaveStatusChange}
172165
saveRef={saveRef}
173166
streamingContent={streamingContent}
174-
streamingMode={streamingMode}
175167
disableStreamingAutoScroll={disableStreamingAutoScroll}
176168
previewContextKey={previewContextKey}
177169
/>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import 'prismjs/components/prism-ruby'
2323
import 'prismjs/components/prism-rust'
2424
import { detectLanguage } from './detect-language'
2525

26-
const KEY = new PluginKey('codeBlockHighlight')
26+
const HIGHLIGHT_PLUGIN_KEY = new PluginKey('codeBlockHighlight')
2727

2828
function tokenClasses(token: Token): string {
2929
const classes = ['token', token.type]
@@ -113,7 +113,7 @@ export const CodeBlockHighlight = Extension.create({
113113
addProseMirrorPlugins() {
114114
return [
115115
new Plugin({
116-
key: KEY,
116+
key: HIGHLIGHT_PLUGIN_KEY,
117117
state: {
118118
init: (_, { doc }) => buildDecorations(doc),
119119
apply: (tr, current) => {
@@ -124,7 +124,7 @@ export const CodeBlockHighlight = Extension.create({
124124
},
125125
props: {
126126
decorations(state) {
127-
return KEY.getState(state)
127+
return HIGHLIGHT_PLUGIN_KEY.getState(state)
128128
},
129129
},
130130
}),

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import StarterKit from '@tiptap/starter-kit'
1414
import { CodeBlockWithLanguage, MarkdownCodeBlock } from './code-block'
1515
import { CodeBlockHighlight } from './code-highlight'
1616
import { MarkdownImage, ResizableImage } from './image'
17-
import { EditorKeymap } from './keymap'
17+
import { RichMarkdownKeymap } from './keymap'
1818
import { MarkdownLinkInputRule } from './link-input-rule'
1919
import { MarkdownPaste } from './markdown-paste'
2020
import { SlashCommand } from './slash-command/slash-command'
@@ -106,7 +106,7 @@ export function createMarkdownEditorExtensions({
106106
...createMarkdownContentExtensions({ nodeViews: true }),
107107
CodeBlockHighlight,
108108
SlashCommand,
109-
EditorKeymap,
109+
RichMarkdownKeymap,
110110
MarkdownPaste,
111111
Placeholder.configure({ placeholder }),
112112
]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function selectAdjacentLeaf(editor: Editor, direction: 'up' | 'down'): boolean {
3333
* same scoped behavior as a code editor.
3434
* - **ArrowUp/ArrowDown** select an adjacent divider or image (see {@link selectAdjacentLeaf}).
3535
*/
36-
export const EditorKeymap = Extension.create({
36+
export const RichMarkdownKeymap = Extension.create({
3737
name: 'richMarkdownKeymap',
3838
priority: 1000,
3939

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const MarkdownPaste = Extension.create({
3535
new Plugin({
3636
props: {
3737
handlePaste: (_view, event) => {
38+
if (!editor.isEditable) return false
3839
if (editor.isActive('codeBlock')) return false
3940
const html = event.clipboardData?.getData('text/html')
4041
if (html) return false

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

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
88
import { useUploadWorkspaceFile } from '@/hooks/queries/workspace-files'
99
import type { SaveStatus } from '@/hooks/use-autosave'
1010
import { PreviewLoadingFrame } from '../preview-shared'
11-
import type { StreamingMode } from '../text-editor-state'
1211
import { useEditableFileContent } from '../use-editable-file-content'
1312
import { createMarkdownEditorExtensions } from './extensions'
1413
import { extractImageFiles } from './image-paste'
@@ -36,7 +35,6 @@ interface RichMarkdownEditorProps {
3635
onSaveStatusChange?: (status: SaveStatus) => void
3736
saveRef?: React.MutableRefObject<(() => Promise<void>) | null>
3837
streamingContent?: string
39-
streamingMode?: StreamingMode
4038
disableStreamingAutoScroll?: boolean
4139
previewContextKey?: string
4240
}
@@ -62,7 +60,6 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({
6260
onSaveStatusChange,
6361
saveRef,
6462
streamingContent,
65-
streamingMode,
6663
disableStreamingAutoScroll = false,
6764
previewContextKey,
6865
}: RichMarkdownEditorProps) {
@@ -78,7 +75,6 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({
7875
workspaceId,
7976
canEdit,
8077
streamingContent,
81-
streamingMode,
8278
onDirtyChange,
8379
onSaveStatusChange,
8480
saveRef,
@@ -101,7 +97,7 @@ export const RichMarkdownEditor = memo(function RichMarkdownEditor({
10197
file={file}
10298
workspaceId={workspaceId}
10399
content={content}
104-
streaming={isStreamInteractionLocked}
100+
isStreaming={isStreamInteractionLocked}
105101
canEdit={canEdit}
106102
autoFocus={autoFocus}
107103
disableStreamingAutoScroll={disableStreamingAutoScroll}
@@ -117,7 +113,7 @@ interface LoadedRichMarkdownEditorProps {
117113
/** The live content from the engine — grows as the agent streams, then settles to the saved doc. */
118114
content: string
119115
/** True while agent output is streaming in: the editor renders it read-only and syncs each chunk. */
120-
streaming: boolean
116+
isStreaming: boolean
121117
canEdit: boolean
122118
autoFocus?: boolean
123119
disableStreamingAutoScroll?: boolean
@@ -143,7 +139,7 @@ function lockSettled(content: string): SettledContent {
143139

144140
/**
145141
* The single TipTap editor for a markdown file — the only surface the user ever sees. While agent
146-
* output streams in ({@link streaming}) it renders that content read-only and re-syncs each chunk;
142+
* output streams in ({@link isStreaming}) it renders that content read-only and re-syncs each chunk;
147143
* when the stream settles it locks the round-trip verdict + frontmatter on the final content and
148144
* hands control to the user. A file opened outside a stream skips straight to that editable state via
149145
* the initial-content model (no imperative sync). Frontmatter is held aside and re-applied on every
@@ -153,7 +149,7 @@ export function LoadedRichMarkdownEditor({
153149
file,
154150
workspaceId,
155151
content,
156-
streaming,
152+
isStreaming,
157153
canEdit,
158154
autoFocus,
159155
disableStreamingAutoScroll,
@@ -162,15 +158,15 @@ export function LoadedRichMarkdownEditor({
162158
}: LoadedRichMarkdownEditorProps) {
163159
// Whether this editor mounted mid-stream. If so it starts empty + read-only and syncs the streamed
164160
// content until the stream settles; otherwise it uses the plain create-time initial-content model.
165-
const streamingAtMountRef = useRef(streaming)
161+
const streamingAtMountRef = useRef(isStreaming)
166162

167163
// The verdict + frontmatter locked via {@link lockSettled} — at mount for a settled file, or at the
168164
// moment a stream settles (in the effect below). Null until then; reads default to read-only.
169165
const settledRef = useRef<SettledContent | null>(null)
170166
if (!streamingAtMountRef.current && settledRef.current === null) {
171167
settledRef.current = lockSettled(content)
172168
}
173-
const isEditable = canEdit && !streaming && (settledRef.current?.verdict ?? false)
169+
const isEditable = canEdit && !isStreaming && (settledRef.current?.verdict ?? false)
174170

175171
// The body that seeds the editor at create time. Empty when streaming — the sync effect pushes the
176172
// streamed body in via setContent (this ref is never written again).
@@ -278,7 +274,7 @@ export function LoadedRichMarkdownEditor({
278274
const lastSyncedBodyRef = useRef<string | null>(null)
279275
useEffect(() => {
280276
if (!editor) return
281-
if (streaming) {
277+
if (isStreaming) {
282278
const body = splitFrontmatter(content).body
283279
if (body === lastSyncedBodyRef.current) return
284280
lastSyncedBodyRef.current = body
@@ -303,7 +299,7 @@ export function LoadedRichMarkdownEditor({
303299
return
304300
}
305301
editor.setEditable(canEdit && settledRef.current.verdict)
306-
}, [editor, content, streaming, canEdit, autoFocus, disableStreamingAutoScroll])
302+
}, [editor, content, isStreaming, canEdit, autoFocus, disableStreamingAutoScroll])
307303

308304
return (
309305
<div

0 commit comments

Comments
 (0)