Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 93 additions & 26 deletions packages/opencode/src/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,89 @@ export namespace File {
export type Node = z.infer<typeof Node>

export const Content = z
.object({
content: z.string(),
diff: z.string().optional(),
patch: z
.object({
oldFileName: z.string(),
newFileName: z.string(),
oldHeader: z.string().optional(),
newHeader: z.string().optional(),
hunks: z.array(
z.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
}),
),
index: z.string().optional(),
})
.optional(),
})
.discriminatedUnion("type", [
z.object({
type: z.literal("text"),
content: z.string(),
diff: z.string().optional(),
patch: z
.object({
oldFileName: z.string(),
newFileName: z.string(),
oldHeader: z.string().optional(),
newHeader: z.string().optional(),
hunks: z.array(
z.object({
oldStart: z.number(),
oldLines: z.number(),
newStart: z.number(),
newLines: z.number(),
lines: z.array(z.string()),
}),
),
index: z.string().optional(),
})
.optional(),
}),
z.object({
type: z.literal("binary"),
content: z.string(),
mimeType: z.string(),
}),
])
.meta({
ref: "FileContent",
})
export type Content = z.infer<typeof Content>

function getMimeType(filepath: string): string {
const ext = path.extname(filepath).toLowerCase()
const mimeTypes: Record<string, string> = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".bmp": "image/bmp",
".webp": "image/webp",
".ico": "image/x-icon",
".svg": "image/svg+xml",
".zip": "application/zip",
".tar": "application/x-tar",
".gz": "application/gzip",
".pdf": "application/pdf",
".wasm": "application/wasm",
".exe": "application/x-msdownload",
".dll": "application/x-msdownload",
".so": "application/x-sharedlib",
}
return mimeTypes[ext] || "application/octet-stream"
}

async function isBinaryFile(filepath: string, file: Bun.BunFile): Promise<boolean> {
const ext = path.extname(filepath).toLowerCase()

if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".ico"].includes(ext)) {
return true
}

if ([".zip", ".tar", ".gz", ".exe", ".dll", ".so", ".pdf", ".wasm"].includes(ext)) {
return true
}

const stat = await file.stat()
if (stat.size === 0) return false

const bufferSize = Math.min(512, stat.size)
const buffer = await file.arrayBuffer()
const bytes = new Uint8Array(buffer.slice(0, bufferSize))

for (let i = 0; i < bytes.length; i++) {
if (bytes[i] === 0) return true
}

return false
}

export const Event = {
Edited: Bus.event(
"file.edited",
Expand Down Expand Up @@ -188,14 +244,25 @@ export namespace File {
}))
}

export async function read(file: string) {
export async function read(file: string): Promise<Content> {
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)

const isBinary = await isBinaryFile(full, bunFile)

if (isBinary) {
const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0))
const content = Buffer.from(buffer).toString("base64")
return { type: "binary", content, mimeType: getMimeType(full) }
}

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()
Expand All @@ -206,10 +273,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) {
Expand Down
43 changes: 25 additions & 18 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,24 +950,31 @@ export type FileNode = {
ignored: boolean
}

export type FileContent = {
content: string
diff?: string
patch?: {
oldFileName: string
newFileName: string
oldHeader?: string
newHeader?: string
hunks: Array<{
oldStart: number
oldLines: number
newStart: number
newLines: number
lines: Array<string>
}>
index?: string
}
}
export type FileContent =
| {
type: "text"
content: string
diff?: string
patch?: {
oldFileName: string
newFileName: string
oldHeader?: string
newHeader?: string
hunks: Array<{
oldStart: number
oldLines: number
newStart: number
newLines: number
lines: Array<string>
}>
index?: string
}
}
| {
type: "binary"
content: string
mimeType: string
}

export type File = {
path: string
Expand Down
12 changes: 11 additions & 1 deletion packages/web/src/content/docs/sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ const result = await client.session.prompt({
| `find.text({ query })` | Search for text in files | Array of match objects with `path`, `lines`, `line_number`, `absolute_offset`, `submatches` |
| `find.files({ query })` | Find files by name | `string[]` (file paths) |
| `find.symbols({ query })` | Find workspace symbols | <a href={typesUrl}><code>Symbol[]</code></a> |
| `file.read({ query })` | Read a file | `{ type: "raw" \| "patch", content: string }` |
| `file.read({ query })` | Read a file | <a href={typesUrl}><code>FileContent</code></a> |
| `file.status({ query? })` | Get status for tracked files | <a href={typesUrl}><code>File[]</code></a> |

---
Expand All @@ -282,6 +282,16 @@ const files = await client.find.files({
const content = await client.file.read({
query: { path: "src/index.ts" },
})

// Handle text vs binary content
if (content.type === "binary") {
const dataUrl = `data:${content.mimeType};base64,${content.content}`
// For images, you can render <img src={dataUrl} /> in a browser context
} else {
// Text content with optional diff/patch metadata
console.log(content.content)
if (content.diff) console.log("Has diff available")
}
```

---
Expand Down