Skip to content

Commit e35f0ec

Browse files
author
Theodore Li
committed
Fix file streaming
1 parent a57c8cb commit e35f0ec

3 files changed

Lines changed: 86 additions & 19 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const ResourceContent = memo(function ResourceContent({
8080
[workspaceId, streamFileName]
8181
)
8282

83-
if (streamingFile) {
83+
if (streamingFile && resource.id === 'streaming-file') {
8484
return (
8585
<div className='flex h-full flex-col overflow-hidden'>
8686
<FileViewer
@@ -105,6 +105,9 @@ export const ResourceContent = memo(function ResourceContent({
105105
workspaceId={workspaceId}
106106
fileId={resource.id}
107107
previewMode={previewMode}
108+
streamingContent={
109+
streamingFile ? extractFileContent(streamingFile.content) : undefined
110+
}
108111
/>
109112
)
110113

@@ -379,9 +382,10 @@ interface EmbeddedFileProps {
379382
workspaceId: string
380383
fileId: string
381384
previewMode?: PreviewMode
385+
streamingContent?: string
382386
}
383387

384-
function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {
388+
function EmbeddedFile({ workspaceId, fileId, previewMode, streamingContent }: EmbeddedFileProps) {
385389
const { data: files = [], isLoading, isFetching } = useWorkspaceFiles(workspaceId)
386390
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
387391

@@ -409,6 +413,7 @@ function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {
409413
workspaceId={workspaceId}
410414
canEdit={true}
411415
previewMode={previewMode}
416+
streamingContent={streamingContent}
412417
/>
413418
</div>
414419
)

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,40 @@ const PREVIEW_CYCLE: Record<PreviewMode, PreviewMode> = {
1717
preview: 'editor',
1818
} as const
1919

20+
function streamFileBasename(name: string): string {
21+
const n = name.replace(/\\/g, '/').trim()
22+
const parts = n.split('/').filter(Boolean)
23+
return parts.length ? parts[parts.length - 1]! : n
24+
}
25+
26+
function fileTitlesEquivalent(streamFileName: string, resourceTitle: string): boolean {
27+
return streamFileBasename(streamFileName) === streamFileBasename(resourceTitle)
28+
}
29+
30+
/**
31+
* Whether the active resource should show the in-progress file_write stream.
32+
* The synthetic `streaming-file` tab always shows it; a real file tab shows it when
33+
* the streamed `fileName` matches that resource (so users who stay on the open file see live text).
34+
*/
35+
function streamReferencesFileId(raw: string, fileId: string): boolean {
36+
if (!fileId) return false
37+
const escaped = fileId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
38+
return new RegExp(`"fileId"\\s*:\\s*"${escaped}"`).test(raw)
39+
}
40+
41+
function shouldShowStreamingFilePanel(
42+
streamingFile: { fileName: string; content: string } | null | undefined,
43+
active: MothershipResource | null
44+
): boolean {
45+
if (!streamingFile || !active) return false
46+
if (active.id === 'streaming-file') return true
47+
if (active.type !== 'file') return false
48+
const fn = streamingFile.fileName.trim()
49+
if (fn && fileTitlesEquivalent(fn, active.title)) return true
50+
if (active.id && streamReferencesFileId(streamingFile.content, active.id)) return true
51+
return false
52+
}
53+
2054
interface MothershipViewProps {
2155
workspaceId: string
2256
chatId?: string
@@ -52,6 +86,11 @@ export const MothershipView = memo(
5286
) {
5387
const active = resources.find((r) => r.id === activeResourceId) ?? resources[0] ?? null
5488

89+
const streamingForActive =
90+
streamingFile && active && shouldShowStreamingFilePanel(streamingFile, active)
91+
? streamingFile
92+
: undefined
93+
5594
const [previewMode, setPreviewMode] = useState<PreviewMode>('preview')
5695
const [prevActiveId, setPrevActiveId] = useState<string | null | undefined>(active?.id)
5796
const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), [])
@@ -97,7 +136,7 @@ export const MothershipView = memo(
97136
workspaceId={workspaceId}
98137
resource={active}
99138
previewMode={isActivePreviewable ? previewMode : undefined}
100-
streamingFile={active.id === 'streaming-file' ? streamingFile : undefined}
139+
streamingFile={streamingForActive}
101140
/>
102141
) : (
103142
<div className='flex h-full items-center justify-center text-[14px] text-[var(--text-muted)]'>

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -764,24 +764,37 @@ export function useChat(
764764
const delta = typeof parsed.data === 'string' ? parsed.data : ''
765765
if (!id || !delta) break
766766

767-
if (activeSubagent === 'file_write') {
768-
const prev = streamingFileRef.current
769-
if (prev) {
770-
const raw = prev.content + delta
771-
let fileName = prev.fileName
772-
if (!fileName) {
773-
const m = raw.match(/"fileName"\s*:\s*"([^"]+)"/)
774-
if (m) {
775-
fileName = m[1]
776-
setResources((rs) =>
777-
rs.map((r) => (r.id === 'streaming-file' ? { ...r, title: fileName } : r))
767+
const toolName =
768+
typeof parsed.toolName === 'string' ? parsed.toolName : ''
769+
const streamWorkspaceFile =
770+
activeSubagent === 'file_write' || toolName === 'workspace_file'
771+
772+
if (streamWorkspaceFile) {
773+
let prev = streamingFileRef.current
774+
if (!prev) {
775+
prev = { fileName: '', content: '' }
776+
streamingFileRef.current = prev
777+
setStreamingFile(prev)
778+
if (toolName === 'workspace_file' && activeSubagent !== 'file_write') {
779+
addResource({ type: 'file', id: 'streaming-file', title: 'Writing file...' })
780+
}
781+
}
782+
const raw = prev.content + delta
783+
let fileName = prev.fileName
784+
if (!fileName) {
785+
const m = raw.match(/"fileName"\s*:\s*"([^"]+)"/)
786+
if (m) {
787+
fileName = m[1]
788+
setResources((rs) =>
789+
rs.map((r) =>
790+
r.id === 'streaming-file' ? { ...r, title: fileName } : r
778791
)
779-
}
792+
)
780793
}
781-
const next = { fileName, content: raw }
782-
streamingFileRef.current = next
783-
setStreamingFile(next)
784794
}
795+
const next = { fileName, content: raw }
796+
streamingFileRef.current = next
797+
setStreamingFile(next)
785798
}
786799

787800
const idx = toolMap.get(id)
@@ -875,6 +888,12 @@ export function useChat(
875888
}
876889

877890
onToolResultRef.current?.(tc.name, tc.status === 'success', tc.result?.output)
891+
892+
if (tc.name === 'workspace_file') {
893+
setStreamingFile(null)
894+
streamingFileRef.current = null
895+
setResources((rs) => rs.filter((r) => r.id !== 'streaming-file'))
896+
}
878897
}
879898

880899
break
@@ -973,7 +992,11 @@ export function useChat(
973992
activeSubagent = name
974993
blocks.push({ type: 'subagent', content: name })
975994
if (name === 'file_write') {
976-
setStreamingFile({ fileName: '', content: '' })
995+
const emptyFile = { fileName: '', content: '' }
996+
// Ref must be updated synchronously: tool_call_delta can arrive before React
997+
// re-renders after setStreamingFile, and the handler only appends when prev exists.
998+
streamingFileRef.current = emptyFile
999+
setStreamingFile(emptyFile)
9771000
addResource({ type: 'file', id: 'streaming-file', title: 'Writing file...' })
9781001
}
9791002
flush()

0 commit comments

Comments
 (0)