11import z from "zod/v4"
22import { Bus } from "../bus"
33import { $ } from "bun"
4+ import type { BunFile } from "bun"
45import { formatPatch , structuredPatch } from "diff"
56import path from "path"
67import fs from "fs"
@@ -41,6 +42,7 @@ export namespace File {
4142
4243 export const Content = z
4344 . object ( {
45+ type : z . literal ( "text" ) ,
4446 content : z . string ( ) ,
4547 diff : z . string ( ) . optional ( ) ,
4648 patch : z
@@ -61,12 +63,53 @@ export namespace File {
6163 index : z . string ( ) . optional ( ) ,
6264 } )
6365 . optional ( ) ,
66+ encoding : z . literal ( "base64" ) . optional ( ) ,
67+ mimeType : z . string ( ) . optional ( ) ,
6468 } )
6569 . meta ( {
6670 ref : "FileContent" ,
6771 } )
6872 export type Content = z . infer < typeof Content >
6973
74+ async function shouldEncode ( file : BunFile ) : Promise < boolean > {
75+ const type = file . type ?. toLowerCase ( )
76+ if ( ! type ) return false
77+
78+ if ( type . startsWith ( "text/" ) ) return false
79+ if ( type . includes ( "charset=" ) ) return false
80+
81+ const parts = type . split ( "/" , 2 )
82+ const top = parts [ 0 ]
83+ const rest = parts [ 1 ] ?? ""
84+ const sub = rest . split ( ";" , 1 ) [ 0 ]
85+
86+ const tops = [ "image" , "audio" , "video" , "font" , "model" , "multipart" ]
87+ if ( tops . includes ( top ) ) return true
88+
89+ if ( type === "application/octet-stream" ) return true
90+
91+ const bins = [
92+ "zip" ,
93+ "gzip" ,
94+ "bzip" ,
95+ "compressed" ,
96+ "binary" ,
97+ "stream" ,
98+ "pdf" ,
99+ "msword" ,
100+ "powerpoint" ,
101+ "excel" ,
102+ "ogg" ,
103+ "exe" ,
104+ "dmg" ,
105+ "iso" ,
106+ "rar" ,
107+ ]
108+ if ( bins . some ( ( mark ) => sub . includes ( mark ) ) ) return true
109+
110+ return false
111+ }
112+
70113 export const Event = {
71114 Edited : Bus . event (
72115 "file.edited" ,
@@ -188,14 +231,30 @@ export namespace File {
188231 } ) )
189232 }
190233
191- export async function read ( file : string ) {
234+ export async function read ( file : string ) : Promise < Content > {
192235 using _ = log . time ( "read" , { file } )
193236 const project = Instance . project
194237 const full = path . join ( Instance . directory , file )
195- const content = await Bun . file ( full )
238+ const bunFile = Bun . file ( full )
239+
240+ if ( ! ( await bunFile . exists ( ) ) ) {
241+ return { type : "text" , content : "" }
242+ }
243+
244+ const encode = await shouldEncode ( bunFile )
245+
246+ if ( encode ) {
247+ const buffer = await bunFile . arrayBuffer ( ) . catch ( ( ) => new ArrayBuffer ( 0 ) )
248+ const content = Buffer . from ( buffer ) . toString ( "base64" )
249+ const mimeType = bunFile . type || "application/octet-stream"
250+ return { type : "text" , content, mimeType, encoding : "base64" }
251+ }
252+
253+ const content = await bunFile
196254 . text ( )
197255 . catch ( ( ) => "" )
198256 . then ( ( x ) => x . trim ( ) )
257+
199258 if ( project . vcs === "git" ) {
200259 let diff = await $ `git diff ${ file } ` . cwd ( Instance . directory ) . quiet ( ) . nothrow ( ) . text ( )
201260 if ( ! diff . trim ( ) ) diff = await $ `git diff --staged ${ file } ` . cwd ( Instance . directory ) . quiet ( ) . nothrow ( ) . text ( )
@@ -206,10 +265,10 @@ export namespace File {
206265 ignoreWhitespace : true ,
207266 } )
208267 const diff = formatPatch ( patch )
209- return { content, patch, diff }
268+ return { type : "text" , content, patch, diff }
210269 }
211270 }
212- return { content }
271+ return { type : "text" , content }
213272 }
214273
215274 export async function list ( dir ?: string ) {
0 commit comments