diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 8ca05cdfe..80179144a 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -432,6 +432,7 @@ export const dict = { "session.review.noChanges": "لا توجد تغييرات", "session.files.selectToOpen": "اختر ملفًا لفتحه", "session.files.all": "كل الملفات", + "session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)", "session.messages.renderEarlier": "عرض الرسائل السابقة", "session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...", "session.messages.loadEarlier": "تحميل الرسائل السابقة", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index ad0772cd8..c874a4376 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -433,6 +433,7 @@ export const dict = { "session.review.noChanges": "Sem alterações", "session.files.selectToOpen": "Selecione um arquivo para abrir", "session.files.all": "Todos os arquivos", + "session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)", "session.messages.renderEarlier": "Renderizar mensagens anteriores", "session.messages.loadingEarlier": "Carregando mensagens anteriores...", "session.messages.loadEarlier": "Carregar mensagens anteriores", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 031d92d4b..555990a9c 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -434,6 +434,7 @@ export const dict = { "session.review.noChanges": "Ingen ændringer", "session.files.selectToOpen": "Vælg en fil at åbne", "session.files.all": "Alle filer", + "session.files.binaryContent": "Binær fil (indhold kan ikke vises)", "session.messages.renderEarlier": "Vis tidligere beskeder", "session.messages.loadingEarlier": "Indlæser tidligere beskeder...", "session.messages.loadEarlier": "Indlæs tidligere beskeder", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 9febfcff1..e56081c90 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -442,6 +442,7 @@ export const dict = { "session.review.noChanges": "Keine Änderungen", "session.files.selectToOpen": "Datei zum Öffnen auswählen", "session.files.all": "Alle Dateien", + "session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)", "session.messages.renderEarlier": "Frühere Nachrichten rendern", "session.messages.loadingEarlier": "Lade frühere Nachrichten...", "session.messages.loadEarlier": "Frühere Nachrichten laden", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index a6a50506a..4254860ac 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -441,6 +441,7 @@ export const dict = { "session.files.selectToOpen": "Select a file to open", "session.files.all": "All files", + "session.files.binaryContent": "Binary file (content cannot be displayed)", "session.messages.renderEarlier": "Render earlier messages", "session.messages.loadingEarlier": "Loading earlier messages...", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index ee75a143d..e928f03ce 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -436,6 +436,7 @@ export const dict = { "session.review.noChanges": "Sin cambios", "session.files.selectToOpen": "Selecciona un archivo para abrir", "session.files.all": "Todos los archivos", + "session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)", "session.messages.renderEarlier": "Renderizar mensajes anteriores", "session.messages.loadingEarlier": "Cargando mensajes anteriores...", "session.messages.loadEarlier": "Cargar mensajes anteriores", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index f0652a981..31000cd17 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -441,6 +441,7 @@ export const dict = { "session.review.noChanges": "Aucune modification", "session.files.selectToOpen": "Sélectionnez un fichier à ouvrir", "session.files.all": "Tous les fichiers", + "session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)", "session.messages.renderEarlier": "Afficher les messages précédents", "session.messages.loadingEarlier": "Chargement des messages précédents...", "session.messages.loadEarlier": "Charger les messages précédents", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index ffe536814..80efc5c2a 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -433,6 +433,7 @@ export const dict = { "session.review.noChanges": "変更なし", "session.files.selectToOpen": "開くファイルを選択", "session.files.all": "すべてのファイル", + "session.files.binaryContent": "バイナリファイル(内容を表示できません)", "session.messages.renderEarlier": "以前のメッセージを表示", "session.messages.loadingEarlier": "以前のメッセージを読み込み中...", "session.messages.loadEarlier": "以前のメッセージを読み込む", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 6c30e0123..014092d07 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -435,6 +435,7 @@ export const dict = { "session.review.noChanges": "변경 없음", "session.files.selectToOpen": "열 파일을 선택하세요", "session.files.all": "모든 파일", + "session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)", "session.messages.renderEarlier": "이전 메시지 렌더링", "session.messages.loadingEarlier": "이전 메시지 로드 중...", "session.messages.loadEarlier": "이전 메시지 로드", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 132c0b6c1..400ce37d3 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -436,6 +436,7 @@ export const dict = { "session.review.noChanges": "Ingen endringer", "session.files.selectToOpen": "Velg en fil å åpne", "session.files.all": "Alle filer", + "session.files.binaryContent": "Binær fil (innhold kan ikke vises)", "session.messages.renderEarlier": "Vis tidligere meldinger", "session.messages.loadingEarlier": "Laster inn tidligere meldinger...", "session.messages.loadEarlier": "Last inn tidligere meldinger", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index efed3eeb1..5a0580982 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -435,6 +435,7 @@ export const dict = { "session.review.noChanges": "Brak zmian", "session.files.selectToOpen": "Wybierz plik do otwarcia", "session.files.all": "Wszystkie pliki", + "session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)", "session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości", "session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...", "session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 0728c4a34..4277368f5 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -437,6 +437,7 @@ export const dict = { "session.review.noChanges": "Нет изменений", "session.files.selectToOpen": "Выберите файл, чтобы открыть", "session.files.all": "Все файлы", + "session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)", "session.messages.renderEarlier": "Показать предыдущие сообщения", "session.messages.loadingEarlier": "Загрузка предыдущих сообщений...", "session.messages.loadEarlier": "Загрузить предыдущие сообщения", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index f8a646f55..e2eabd7ad 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -438,6 +438,7 @@ export const dict = { "session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด", "session.files.all": "ไฟล์ทั้งหมด", + "session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)", "session.messages.renderEarlier": "แสดงข้อความก่อนหน้า", "session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 2266c109b..118e03ce4 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -434,6 +434,7 @@ export const dict = { "session.review.noChanges": "无更改", "session.files.selectToOpen": "选择要打开的文件", "session.files.all": "所有文件", + "session.files.binaryContent": "二进制文件(无法显示内容)", "session.messages.renderEarlier": "显示更早的消息", "session.messages.loadingEarlier": "正在加载更早的消息...", "session.messages.loadEarlier": "加载更早的消息", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 30837e56f..45a789df4 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -431,6 +431,7 @@ export const dict = { "session.review.noChanges": "沒有變更", "session.files.selectToOpen": "選取要開啟的檔案", "session.files.all": "所有檔案", + "session.files.binaryContent": "二進位檔案(無法顯示內容)", "session.messages.renderEarlier": "顯示更早的訊息", "session.messages.loadingEarlier": "正在載入更早的訊息...", "session.messages.loadEarlier": "載入更早的訊息", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index b346fa692..d3e74072a 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -2342,6 +2342,7 @@ export default function Page() { const c = state()?.content return c?.mimeType === "image/svg+xml" }) + const isBinary = createMemo(() => state()?.content?.type === "binary") const svgContent = createMemo(() => { if (!isSvg()) return const c = state()?.content @@ -2794,6 +2795,19 @@ export default function Page() { + + + + + + {path()?.split("/").pop()} + + + {language.t("session.files.binaryContent")} + + + + {renderCode(contents(), "pb-40")} {language.t("common.loading")}... diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index dfa6356a2..32465015e 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -44,7 +44,7 @@ export namespace File { export const Content = z .object({ - type: z.literal("text"), + type: z.enum(["text", "binary"]), content: z.string(), diff: z.string().optional(), patch: z @@ -73,6 +73,174 @@ export namespace File { }) export type Content = z.infer + const binaryExtensions = new Set([ + "exe", + "dll", + "pdb", + "bin", + "so", + "dylib", + "o", + "a", + "lib", + "wav", + "mp3", + "ogg", + "oga", + "ogv", + "ogx", + "flac", + "aac", + "wma", + "m4a", + "weba", + "mp4", + "avi", + "mov", + "wmv", + "flv", + "webm", + "mkv", + "zip", + "tar", + "gz", + "gzip", + "bz", + "bz2", + "bzip", + "bzip2", + "7z", + "rar", + "xz", + "lz", + "z", + "pdf", + "doc", + "docx", + "ppt", + "pptx", + "xls", + "xlsx", + "dmg", + "iso", + "img", + "vmdk", + "ttf", + "otf", + "woff", + "woff2", + "eot", + "sqlite", + "db", + "mdb", + "apk", + "ipa", + "aab", + "xapk", + "app", + "pkg", + "deb", + "rpm", + "snap", + "flatpak", + "appimage", + "msi", + "msp", + "jar", + "war", + "ear", + "class", + "kotlin_module", + "dex", + "vdex", + "odex", + "oat", + "art", + "wasm", + "wat", + "bc", + "ll", + "s", + "ko", + "sys", + "drv", + "efi", + "rom", + "com", + "bat", + "cmd", + "ps1", + "sh", + "bash", + "zsh", + "fish", + ]) + + const imageExtensions = new Set([ + "png", + "jpg", + "jpeg", + "gif", + "bmp", + "webp", + "ico", + "tif", + "tiff", + "svg", + "svgz", + "avif", + "apng", + "jxl", + "heic", + "heif", + "raw", + "cr2", + "nef", + "arw", + "dng", + "orf", + "raf", + "pef", + "x3f", + ]) + + function isImageByExtension(filepath: string): boolean { + const ext = path.extname(filepath).toLowerCase().slice(1) + return imageExtensions.has(ext) + } + + function getImageMimeType(filepath: string): string { + const ext = path.extname(filepath).toLowerCase().slice(1) + const mimeTypes: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + bmp: "image/bmp", + webp: "image/webp", + ico: "image/x-icon", + tif: "image/tiff", + tiff: "image/tiff", + svg: "image/svg+xml", + svgz: "image/svg+xml", + avif: "image/avif", + apng: "image/apng", + jxl: "image/jxl", + heic: "image/heic", + heif: "image/heif", + } + return mimeTypes[ext] || "image/" + ext + } + + function isBinaryByExtension(filepath: string): boolean { + const ext = path.extname(filepath).toLowerCase().slice(1) + return binaryExtensions.has(ext) + } + + function isImage(mimeType: string): boolean { + return mimeType.startsWith("image/") + } + async function shouldEncode(file: BunFile): Promise { const type = file.type?.toLowerCase() log.info("shouldEncode", { type }) @@ -83,30 +251,10 @@ export namespace File { const parts = type.split("/", 2) const top = parts[0] - const rest = parts[1] ?? "" - const sub = rest.split(";", 1)[0] const tops = ["image", "audio", "video", "font", "model", "multipart"] if (tops.includes(top)) return true - const bins = [ - "zip", - "gzip", - "bzip", - "compressed", - "binary", - "pdf", - "msword", - "powerpoint", - "excel", - "ogg", - "exe", - "dmg", - "iso", - "rar", - ] - if (bins.some((mark) => sub.includes(mark))) return true - return false } @@ -287,6 +435,22 @@ export namespace File { throw new Error(`Access denied: path escapes project directory`) } + // Fast path: check extension before any filesystem operations + if (isImageByExtension(file)) { + const bunFile = Bun.file(full) + if (await bunFile.exists()) { + const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0)) + const content = Buffer.from(buffer).toString("base64") + const mimeType = getImageMimeType(file) + return { type: "text", content, mimeType, encoding: "base64" } + } + return { type: "text", content: "" } + } + + if (isBinaryByExtension(file)) { + return { type: "binary", content: "" } + } + const bunFile = Bun.file(full) if (!(await bunFile.exists())) { @@ -294,11 +458,15 @@ export namespace File { } const encode = await shouldEncode(bunFile) + const mimeType = bunFile.type || "application/octet-stream" + + if (encode && !isImage(mimeType)) { + return { type: "binary", content: "", mimeType } + } if (encode) { const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0)) const content = Buffer.from(buffer).toString("base64") - const mimeType = bunFile.type || "application/octet-stream" return { type: "text", content, mimeType, encoding: "base64" } } diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index ca13e5e93..8eefe5bfe 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1554,7 +1554,7 @@ export type FileNode = { } export type FileContent = { - type: "text" + type: "text" | "binary" content: string diff?: string patch?: { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cb2f58677..e992b27bd 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2042,7 +2042,7 @@ export type FileNode = { } export type FileContent = { - type: "text" + type: "text" | "binary" content: string diff?: string patch?: {