@@ -4,6 +4,7 @@ import type {
44 ResolvedKeybindingsConfig ,
55 ScopedThreadRef ,
66} from "@t3tools/contracts" ;
7+ import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview" ;
78import { VirtualizedFile , type SelectedLineRange } from "@pierre/diffs" ;
89import { Editor } from "@pierre/diffs/editor" ;
910import { EditorProvider , File , type FileOptions , Virtualizer } from "@pierre/diffs/react" ;
@@ -16,6 +17,7 @@ import * as Schema from "effect/Schema";
1617import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
1718
1819import { isBrowserPreviewFile , openFileInPreview } from "~/browser/openFileInPreview" ;
20+ import { useAssetUrlState } from "~/assets/assetUrls" ;
1921import ChatMarkdown from "~/components/ChatMarkdown" ;
2022import { OpenInPicker } from "~/components/chat/OpenInPicker" ;
2123import { useClientSettings } from "~/hooks/useSettings" ;
@@ -113,6 +115,43 @@ const FILE_LINK_REVEAL_UNSAFE_CSS = `
113115` ;
114116type FilePostRender = NonNullable < FileOptions < unknown > [ "onPostRender" ] > ;
115117
118+ function WorkspaceImagePreview ( props : {
119+ readonly environmentId : EnvironmentId ;
120+ readonly threadRef : ScopedThreadRef ;
121+ readonly absolutePath : string ;
122+ readonly alt : string ;
123+ } ) {
124+ const assetUrl = useAssetUrlState ( props . environmentId , {
125+ _tag : "workspace-file" ,
126+ threadId : props . threadRef . threadId ,
127+ path : props . absolutePath ,
128+ } ) ;
129+ const [ failedUrl , setFailedUrl ] = useState < string | null > ( null ) ;
130+
131+ if ( assetUrl . _tag === "Failure" || ( assetUrl . _tag === "Success" && failedUrl === assetUrl . url ) ) {
132+ return (
133+ < div className = "flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive" >
134+ Unable to load workspace image.
135+ </ div >
136+ ) ;
137+ }
138+
139+ return assetUrl . _tag === "Success" ? (
140+ < div className = "flex min-h-0 flex-1 items-center justify-center overflow-auto p-4" >
141+ < img
142+ className = "max-h-full max-w-full object-contain"
143+ src = { assetUrl . url }
144+ alt = { props . alt }
145+ onError = { ( ) => setFailedUrl ( assetUrl . url ) }
146+ />
147+ </ div >
148+ ) : (
149+ < div className = "flex min-h-0 flex-1 items-center justify-center text-muted-foreground" >
150+ < LoaderCircle className = "size-5 animate-spin" />
151+ </ div >
152+ ) ;
153+ }
154+
116155function clampFileLine ( contents : string , requestedLine : number ) : number {
117156 let lineCount = 1 ;
118157 for ( let index = 0 ; index < contents . length ; index += 1 ) {
@@ -630,7 +669,8 @@ export default function FilePreviewPanel({
630669 const openPreview = useAtomCommand ( previewEnvironment . open , {
631670 reportFailure : false ,
632671 } ) ;
633- const file = useProjectFileQuery ( environmentId , cwd , relativePath ) ;
672+ const isImage = relativePath !== null && isWorkspaceImagePreviewPath ( relativePath ) ;
673+ const file = useProjectFileQuery ( environmentId , cwd , relativePath , ! isImage ) ;
634674 const [ explorerOpen , setExplorerOpen ] = useState ( initialExplorerOpen ) ;
635675 const [ markdownView , setMarkdownView ] = useState < {
636676 path : string | null ;
@@ -818,7 +858,15 @@ export default function FilePreviewPanel({
818858 relativePath ? "flex" : "hidden" ,
819859 ) }
820860 >
821- { relativePath && file . error && file . data === null ? (
861+ { relativePath && isImage && absolutePath ? (
862+ < WorkspaceImagePreview
863+ key = { absolutePath }
864+ environmentId = { environmentId }
865+ threadRef = { threadRef }
866+ absolutePath = { absolutePath }
867+ alt = { relativePath }
868+ />
869+ ) : relativePath && file . error && file . data === null ? (
822870 < div className = "flex min-h-0 flex-1 items-center justify-center px-6 text-center text-xs leading-relaxed text-destructive" >
823871 { file . error }
824872 </ div >
0 commit comments