From a99bd3aa2c0f7100d0bcbfa4a11d818b7c753661 Mon Sep 17 00:00:00 2001 From: kcrommett <523952+kcrommett@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:52:39 -0700 Subject: [PATCH] tweak: adjust file api to encode images (#3292) --- packages/opencode/src/file/index.ts | 67 +++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index e5023f0d..1478de86 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,6 +1,7 @@ import z from "zod/v4" import { Bus } from "../bus" import { $ } from "bun" +import type { BunFile } from "bun" import { formatPatch, structuredPatch } from "diff" import path from "path" import fs from "fs" @@ -41,6 +42,7 @@ export namespace File { export const Content = z .object({ + type: z.literal("text"), content: z.string(), diff: z.string().optional(), patch: z @@ -61,12 +63,53 @@ export namespace File { index: z.string().optional(), }) .optional(), + encoding: z.literal("base64").optional(), + mimeType: z.string().optional(), }) .meta({ ref: "FileContent", }) export type Content = z.infer + async function shouldEncode(file: BunFile): Promise { + const type = file.type?.toLowerCase() + if (!type) return false + + if (type.startsWith("text/")) return false + if (type.includes("charset=")) return false + + 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 + + if (type === "application/octet-stream") return true + + const bins = [ + "zip", + "gzip", + "bzip", + "compressed", + "binary", + "stream", + "pdf", + "msword", + "powerpoint", + "excel", + "ogg", + "exe", + "dmg", + "iso", + "rar", + ] + if (bins.some((mark) => sub.includes(mark))) return true + + return false + } + export const Event = { Edited: Bus.event( "file.edited", @@ -188,14 +231,30 @@ export namespace File { })) } - export async function read(file: string) { + export async function read(file: string): Promise { using _ = log.time("read", { file }) const project = Instance.project const full = path.join(Instance.directory, file) - const content = await Bun.file(full) + const bunFile = Bun.file(full) + + if (!(await bunFile.exists())) { + return { type: "text", content: "" } + } + + const encode = await shouldEncode(bunFile) + + 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" } + } + + const content = await bunFile .text() .catch(() => "") .then((x) => x.trim()) + if (project.vcs === "git") { let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text() if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text() @@ -206,10 +265,10 @@ export namespace File { ignoreWhitespace: true, }) const diff = formatPatch(patch) - return { content, patch, diff } + return { type: "text", content, patch, diff } } } - return { content } + return { type: "text", content } } export async function list(dir?: string) {