@@ -65,7 +65,15 @@ interface ResourceContentProps {
6565 workspaceId : string
6666 resource : MothershipResource
6767 previewMode ?: PreviewMode
68- streamingFile ?: { fileName : string ; fileId ?: string ; content : string } | null
68+ streamingFile ?: {
69+ toolCallId ?: string
70+ fileName : string
71+ fileId ?: string
72+ targetKind ?: 'new_file' | 'file_id'
73+ operation ?: string
74+ edit ?: Record < string , unknown >
75+ content : string
76+ } | null
6977 genericResourceData ?: GenericResourceData
7078}
7179
@@ -87,11 +95,10 @@ export const ResourceContent = memo(function ResourceContent({
8795
8896 const streamOperation = useMemo ( ( ) => {
8997 if ( ! streamingFile ) return undefined
90- const m = streamingFile . content . match ( / " o p e r a t i o n " \s * : \s * " ( \w + ) " / )
91- return m ?. [ 1 ]
98+ return streamingFile . operation
9299 } , [ streamingFile ] )
93100
94- const isWriteStream = streamOperation === 'write '
101+ const isWriteStream = streamOperation === 'create' || streamOperation === 'append '
95102 const isPatchStream = streamOperation === 'patch'
96103 const isUpdateStream = streamOperation === 'update'
97104
@@ -113,24 +120,36 @@ export const ResourceContent = memo(function ResourceContent({
113120 isSourceMime
114121 )
115122
123+ // #region agent log
124+ if ( streamingFile ) {
125+ fetch ( 'http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06' , { method :'POST' , headers :{ 'Content-Type' :'application/json' , 'X-Debug-Session-Id' :'6f10b0' } , body :JSON . stringify ( { sessionId :'6f10b0' , location :'resource-content.tsx:streaming-context' , message :'streaming state' , data :{ resourceId :resource . id , resourceType :resource . type , streamOp :streamOperation , isPatch :isPatchStream , isWrite :isWriteStream , isUpdate :isUpdateStream , hasActiveFileRecord :! ! activeFileRecord , hasFetchedContent :! ! fetchedFileContent , fetchedContentLen :fetchedFileContent ?. length , streamingFileContentLen :streamingFile . content . length , streamingFileName :streamingFile . fileName , streamingFileMode :isWriteStream ?'append' :'replace' } , timestamp :Date . now ( ) } ) } ) . catch ( ( ) => { } ) ;
126+ }
127+ // #endregion
116128 const streamingExtractedContent = useMemo ( ( ) => {
117129 if ( ! streamingFile ) return undefined
118- const raw = streamingFile . content
119-
120- // Do not guess. Until the operation key has streamed in, we don't know
121- // whether the payload should append, replace, or splice into the file.
122- // Rendering early here can show content at the end of the file and then
123- // "snap" to the right place once the operation/mode becomes known.
124130 if ( ! streamOperation ) return undefined
125131
126132 if ( isPatchStream ) {
127- if ( ! fetchedFileContent ) return undefined
128- return extractPatchPreview ( raw , fetchedFileContent )
133+ if ( ! fetchedFileContent ) {
134+ // #region agent log
135+ fetch ( 'http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06' , { method :'POST' , headers :{ 'Content-Type' :'application/json' , 'X-Debug-Session-Id' :'6f10b0' } , body :JSON . stringify ( { sessionId :'6f10b0' , location :'resource-content.tsx:patch-no-fetched' , message :'patch but no fetchedFileContent' , data :{ resourceId :resource . id , activeFileRecordId :activeFileRecord ?. id } , timestamp :Date . now ( ) , hypothesisId :'H1' } ) } ) . catch ( ( ) => { } ) ;
136+ // #endregion
137+ return undefined
138+ }
139+ const patchResult = extractPatchPreview ( streamingFile , fetchedFileContent )
140+ // #region agent log
141+ fetch ( 'http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06' , { method :'POST' , headers :{ 'Content-Type' :'application/json' , 'X-Debug-Session-Id' :'6f10b0' } , body :JSON . stringify ( { sessionId :'6f10b0' , location :'resource-content.tsx:patch-result' , message :'extractPatchPreview result' , data :{ hasPatchResult :! ! patchResult , patchResultLen :patchResult ?. length , fetchedLen :fetchedFileContent . length , contentPreview :streamingFile . content . slice ( 0 , 200 ) , edit :streamingFile . edit } , timestamp :Date . now ( ) , hypothesisId :'H4' } ) } ) . catch ( ( ) => { } ) ;
142+ // #endregion
143+ return patchResult
129144 }
130145
131- const extracted = extractFileContent ( raw )
146+ const extracted = streamingFile . content
132147 if ( extracted . length === 0 ) return undefined
133148
149+ // #region agent log
150+ fetch ( 'http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06' , { method :'POST' , headers :{ 'Content-Type' :'application/json' , 'X-Debug-Session-Id' :'6f10b0' } , body :JSON . stringify ( { sessionId :'6f10b0' , location :'resource-content.tsx:write-update-content' , message :'extracted content for write/update' , data :{ streamOp :streamOperation , extractedLen :extracted . length , extractedPreview :extracted . slice ( 0 , 150 ) } , timestamp :Date . now ( ) , hypothesisId :'H2' } ) } ) . catch ( ( ) => { } ) ;
151+ // #endregion
152+
134153 if ( isUpdateStream ) return extracted
135154 if ( isWriteStream ) return extracted
136155
@@ -160,6 +179,15 @@ export const ResourceContent = memo(function ResourceContent({
160179 const streamingFileMode : 'append' | 'replace' =
161180 isWriteStream ? 'append' : 'replace'
162181
182+ // For existing file resources (not streaming-file), only pass streaming
183+ // content for patch operations where the preview splices new content into
184+ // the displayed file. Update operations re-stream the entire file from
185+ // scratch which causes visual duplication of already-visible content.
186+ const embeddedStreamingContent =
187+ resource . id !== 'streaming-file' && isUpdateStream
188+ ? undefined
189+ : streamingExtractedContent
190+
163191 if ( streamingFile && resource . id === 'streaming-file' ) {
164192 return (
165193 < div className = 'flex h-full flex-col overflow-hidden' >
@@ -192,7 +220,7 @@ export const ResourceContent = memo(function ResourceContent({
192220 workspaceId = { workspaceId }
193221 fileId = { resource . id }
194222 previewMode = { previewMode }
195- streamingContent = { streamingExtractedContent }
223+ streamingContent = { embeddedStreamingContent }
196224 streamingMode = { streamingFileMode }
197225 />
198226 )
@@ -587,65 +615,6 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
587615 )
588616}
589617
590- function extractFileContent ( raw : string ) : string {
591- const marker = '"content":'
592- const idx = raw . indexOf ( marker )
593- if ( idx === - 1 ) return ''
594- const rest = raw . slice ( idx + marker . length ) . trimStart ( )
595- if ( ! rest . startsWith ( '"' ) ) return rest
596-
597- // Walk the JSON string value to find the unescaped closing quote.
598- // While streaming, the closing quote may not have arrived yet — in that
599- // case we treat everything received so far as the content (no trim).
600- let end = - 1
601- for ( let i = 1 ; i < rest . length ; i ++ ) {
602- if ( rest [ i ] === '\\' ) {
603- i ++ // skip escaped character
604- continue
605- }
606- if ( rest [ i ] === '"' ) {
607- end = i
608- break
609- }
610- }
611-
612- const inner = end === - 1 ? rest . slice ( 1 ) : rest . slice ( 1 , end )
613- return inner
614- . replace ( / \\ n / g, '\n' )
615- . replace ( / \\ t / g, '\t' )
616- . replace ( / \\ r / g, '\r' )
617- . replace ( / \\ " / g, '"' )
618- . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) => String . fromCharCode ( Number . parseInt ( hex , 16 ) ) )
619- . replace ( / \\ \\ / g, '\\' )
620- }
621-
622- function extractJsonString ( raw : string , key : string ) : string | undefined {
623- const pattern = new RegExp ( `"${ key } "\\s*:\\s*"` )
624- const m = pattern . exec ( raw )
625- if ( ! m ) return undefined
626- const start = m . index + m [ 0 ] . length
627- let end = - 1
628- for ( let i = start ; i < raw . length ; i ++ ) {
629- if ( raw [ i ] === '\\' ) {
630- i ++
631- continue
632- }
633- if ( raw [ i ] === '"' ) {
634- end = i
635- break
636- }
637- }
638- if ( end === - 1 ) return undefined
639- return raw
640- . slice ( start , end )
641- . replace ( / \\ n / g, '\n' )
642- . replace ( / \\ t / g, '\t' )
643- . replace ( / \\ r / g, '\r' )
644- . replace ( / \\ " / g, '"' )
645- . replace ( / \\ u ( [ 0 - 9 a - f A - F ] { 4 } ) / g, ( _ , hex ) => String . fromCharCode ( Number . parseInt ( hex , 16 ) ) )
646- . replace ( / \\ \\ / g, '\\' )
647- }
648-
649618function findAnchorIndex ( lines : string [ ] , anchor : string , occurrence = 1 , afterIndex = - 1 ) : number {
650619 const trimmed = anchor . trim ( )
651620 let count = 0
@@ -658,24 +627,46 @@ function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterI
658627 return - 1
659628}
660629
661- function extractPatchPreview ( raw : string , existingContent : string ) : string | undefined {
662- const mode = extractJsonString ( raw , 'mode' )
663- if ( ! mode ) return undefined
664-
630+ function extractPatchPreview (
631+ streamingFile : {
632+ content : string
633+ edit ?: Record < string , unknown >
634+ } ,
635+ existingContent : string
636+ ) : string | undefined {
637+ const edit = streamingFile . edit ?? { }
638+ const strategy = typeof edit . strategy === 'string' ? edit . strategy : undefined
665639 const lines = existingContent . split ( '\n' )
666- const occurrenceMatch = raw . match ( / " o c c u r r e n c e " \s * : \s * ( \d + ) / )
667- const occurrence = occurrenceMatch ? Number . parseInt ( occurrenceMatch [ 1 ] , 10 ) : 1
640+ const occurrence =
641+ typeof edit . occurrence === 'number' && Number . isFinite ( edit . occurrence )
642+ ? edit . occurrence
643+ : 1
644+
645+ if ( strategy === 'search_replace' ) {
646+ const search = typeof edit . search === 'string' ? edit . search : ''
647+ if ( ! search ) return undefined
648+ const replace = streamingFile . content
649+ if ( ( edit . replaceAll as boolean | undefined ) === true ) {
650+ return existingContent . split ( search ) . join ( replace )
651+ }
652+ const firstIdx = existingContent . indexOf ( search )
653+ if ( firstIdx === - 1 ) return undefined
654+ return existingContent . slice ( 0 , firstIdx ) + replace + existingContent . slice ( firstIdx + search . length )
655+ }
656+
657+ const mode = typeof edit . mode === 'string' ? edit . mode : undefined
658+ if ( ! mode ) return undefined
668659
669660 if ( mode === 'replace_between' ) {
670- const beforeAnchor = extractJsonString ( raw , ' before_anchor' )
671- const afterAnchor = extractJsonString ( raw , ' after_anchor' )
661+ const beforeAnchor = typeof edit . before_anchor === 'string' ? edit . before_anchor : undefined
662+ const afterAnchor = typeof edit . after_anchor === 'string' ? edit . after_anchor : undefined
672663 if ( ! beforeAnchor || ! afterAnchor ) return undefined
673664
674665 const beforeIdx = findAnchorIndex ( lines , beforeAnchor , occurrence )
675666 const afterIdx = findAnchorIndex ( lines , afterAnchor , occurrence , beforeIdx )
676667 if ( beforeIdx === - 1 || afterIdx === - 1 || afterIdx <= beforeIdx ) return undefined
677668
678- const newContent = extractFileContent ( raw )
669+ const newContent = streamingFile . content
679670 const spliced = [
680671 ...lines . slice ( 0 , beforeIdx + 1 ) ,
681672 ...( newContent . length > 0 ? newContent . split ( '\n' ) : [ ] ) ,
@@ -685,13 +676,13 @@ function extractPatchPreview(raw: string, existingContent: string): string | und
685676 }
686677
687678 if ( mode === 'insert_after' ) {
688- const anchor = extractJsonString ( raw , ' anchor' )
679+ const anchor = typeof edit . anchor === 'string' ? edit . anchor : undefined
689680 if ( ! anchor ) return undefined
690681
691682 const anchorIdx = findAnchorIndex ( lines , anchor , occurrence )
692683 if ( anchorIdx === - 1 ) return undefined
693684
694- const newContent = extractFileContent ( raw )
685+ const newContent = streamingFile . content
695686 const spliced = [
696687 ...lines . slice ( 0 , anchorIdx + 1 ) ,
697688 ...( newContent . length > 0 ? newContent . split ( '\n' ) : [ ] ) ,
@@ -701,8 +692,8 @@ function extractPatchPreview(raw: string, existingContent: string): string | und
701692 }
702693
703694 if ( mode === 'delete_between' ) {
704- const startAnchor = extractJsonString ( raw , ' start_anchor' )
705- const endAnchor = extractJsonString ( raw , ' end_anchor' )
695+ const startAnchor = typeof edit . start_anchor === 'string' ? edit . start_anchor : undefined
696+ const endAnchor = typeof edit . end_anchor === 'string' ? edit . end_anchor : undefined
706697 if ( ! startAnchor || ! endAnchor ) return undefined
707698
708699 const startIdx = findAnchorIndex ( lines , startAnchor , occurrence )
0 commit comments