@@ -22,6 +22,7 @@ type FileReadImagePayload = {
2222 mime : string ;
2323 url : string ;
2424 size : number ;
25+ version : string ;
2526 isTextBacked : boolean ;
2627} ;
2728
@@ -120,6 +121,7 @@ export function useCodeEditorActions() {
120121 mime : data . mime ,
121122 url : data . url ,
122123 size : data . size ,
124+ version : data . version ,
123125 isTextBacked : data . isTextBacked ,
124126 externalState : undefined ,
125127 } ;
@@ -131,6 +133,15 @@ export function useCodeEditorActions() {
131133 [ dispatch , setOpenFiles , workspaceId ]
132134 ) ;
133135
136+ const loadTextBackedImageContent = useCallback ( async ( url : string ) => {
137+ const response = await fetch ( url , { credentials : "include" } ) ;
138+ if ( ! response . ok ) {
139+ throw new Error ( `Failed to fetch text-backed image bytes: ${ response . status } ` ) ;
140+ }
141+
142+ return response . text ( ) ;
143+ } , [ ] ) ;
144+
134145 const handleSave = useCallback ( async ( ) => {
135146 if ( ! workspaceId || ! currentFile || currentFile . kind !== "text" || isSaving ) {
136147 return ;
@@ -298,8 +309,93 @@ export function useCodeEditorActions() {
298309 continue ;
299310 }
300311
312+ if (
313+ file . kind === "text" &&
314+ file . viewingTextBackedImageAsText === true &&
315+ nextData . kind === "image" &&
316+ nextData . isTextBacked
317+ ) {
318+ if ( file . isDirty ) {
319+ setOpenFiles ( ( prev ) => {
320+ const existing = prev [ path ] ;
321+ if ( ! existing || existing . kind !== "text" ) {
322+ return prev ;
323+ }
324+
325+ return {
326+ ...prev ,
327+ [ path ] : {
328+ ...existing ,
329+ externalState : "modified" ,
330+ } ,
331+ } ;
332+ } ) ;
333+
334+ if ( activeFilePath === path ) {
335+ setExternalStatus ( {
336+ path,
337+ status : "modified" ,
338+ } ) ;
339+ }
340+ continue ;
341+ }
342+
343+ try {
344+ const content = await loadTextBackedImageContent ( nextData . url ) ;
345+ if ( cancelled ) {
346+ return ;
347+ }
348+
349+ setOpenFiles ( ( prev ) => {
350+ const existing = prev [ path ] ;
351+ if ( ! existing || existing . kind !== "text" ) {
352+ return prev ;
353+ }
354+
355+ return {
356+ ...prev ,
357+ [ path ] : {
358+ ...existing ,
359+ content,
360+ isDirty : false ,
361+ externalState : undefined ,
362+ viewingTextBackedImageAsText : true ,
363+ } ,
364+ } ;
365+ } ) ;
366+
367+ if ( activeFilePath === path ) {
368+ setExternalStatus ( ( current ) => ( current ?. path === path ? null : current ) ) ;
369+ }
370+ } catch ( error ) {
371+ console . error ( "Failed to refresh text-backed image bytes:" , error ) ;
372+ setOpenFiles ( ( prev ) => {
373+ const existing = prev [ path ] ;
374+ if ( ! existing || existing . kind !== "text" ) {
375+ return prev ;
376+ }
377+
378+ return {
379+ ...prev ,
380+ [ path ] : {
381+ ...existing ,
382+ externalState : "modified" ,
383+ } ,
384+ } ;
385+ } ) ;
386+
387+ if ( activeFilePath === path ) {
388+ setExternalStatus ( {
389+ path,
390+ status : "modified" ,
391+ } ) ;
392+ }
393+ }
394+ continue ;
395+ }
396+
301397 if ( file . kind === "image" && nextData . kind === "image" ) {
302- if ( file . url === nextData . url && file . size === nextData . size ) {
398+ if ( file . version === nextData . version && file . size === nextData . size ) {
303399 continue ;
304400 }
305401
@@ -311,6 +407,7 @@ export function useCodeEditorActions() {
311407 mime : nextData . mime ,
312408 url : nextData . url ,
313409 size : nextData . size ,
410+ version : nextData . version ,
314411 isTextBacked : nextData . isTextBacked ,
315412 externalState : undefined ,
316413 } ,
@@ -337,7 +434,15 @@ export function useCodeEditorActions() {
337434 return ( ) => {
338435 cancelled = true ;
339436 } ;
340- } , [ activeFilePath , dispatch , editorRefreshToken , openFiles , setOpenFiles , workspaceId ] ) ;
437+ } , [
438+ activeFilePath ,
439+ dispatch ,
440+ editorRefreshToken ,
441+ loadTextBackedImageContent ,
442+ openFiles ,
443+ setOpenFiles ,
444+ workspaceId ,
445+ ] ) ;
341446
342447 const handleClose = useCallback ( ( ) => {
343448 if ( ! workspaceId ) {
0 commit comments