summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAlex Yaroshuk <[email protected]>2026-02-01 23:40:33 +0800
committerGitHub <[email protected]>2026-02-01 09:40:33 -0600
commit23c803707d2ee1ac71bbb9420272c45139947382 (patch)
treebf448912d6c0d1e16f9ce2396a731e052c527fe8
parentb51005ec4af0dc99e4cdbfb42055581bee584fa4 (diff)
downloadopencode-23c803707d2ee1ac71bbb9420272c45139947382.tar.gz
opencode-23c803707d2ee1ac71bbb9420272c45139947382.zip
fix(app): binary file handling in file view (#11312)
-rw-r--r--packages/app/src/i18n/ar.ts1
-rw-r--r--packages/app/src/i18n/br.ts1
-rw-r--r--packages/app/src/i18n/da.ts1
-rw-r--r--packages/app/src/i18n/de.ts1
-rw-r--r--packages/app/src/i18n/en.ts1
-rw-r--r--packages/app/src/i18n/es.ts1
-rw-r--r--packages/app/src/i18n/fr.ts1
-rw-r--r--packages/app/src/i18n/ja.ts1
-rw-r--r--packages/app/src/i18n/ko.ts1
-rw-r--r--packages/app/src/i18n/no.ts1
-rw-r--r--packages/app/src/i18n/pl.ts1
-rw-r--r--packages/app/src/i18n/ru.ts1
-rw-r--r--packages/app/src/i18n/th.ts1
-rw-r--r--packages/app/src/i18n/zh.ts1
-rw-r--r--packages/app/src/i18n/zht.ts1
-rw-r--r--packages/app/src/pages/session.tsx14
-rw-r--r--packages/opencode/src/file/index.ts212
-rw-r--r--packages/sdk/js/src/gen/types.gen.ts2
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts2
19 files changed, 221 insertions, 24 deletions
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() {
</Show>
</div>
</Match>
+ <Match when={state()?.loaded && isBinary()}>
+ <div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
+ <Mark class="w-14 opacity-10" />
+ <div class="flex flex-col gap-2 max-w-md">
+ <div class="text-14-semibold text-text-strong truncate">
+ {path()?.split("/").pop()}
+ </div>
+ <div class="text-14-regular text-text-weak">
+ {language.t("session.files.binaryContent")}
+ </div>
+ </div>
+ </div>
+ </Match>
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
<Match when={state()?.loading}>
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
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<typeof Content>
+ 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<string, string> = {
+ 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<boolean> {
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?: {