From 386731698bca1d3f36f008bb299308ee2931d04a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 20:45:29 -0500 Subject: [PATCH 01/11] Revert "chore: generate" This reverts commit 38e454011915c76e88a938fd61dd6d6d0c13f355. --- packages/opencode/src/session/prompt.ts | 36 ++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5414eba2e5..30446552cf 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1389,26 +1389,26 @@ NOTE: At any point in time through this workflow you should feel free to ask the return { info, parts } }, Effect.scoped) - const prompt: (input: PromptInput) => Effect.Effect = Effect.fn("SessionPrompt.prompt")( - function* (input: PromptInput) { - const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) - yield* revert.cleanup(session) - const message = yield* createUserMessage(input) - yield* sessions.touch(input.sessionID) + const prompt: (input: PromptInput) => Effect.Effect = Effect.fn("SessionPrompt.prompt")(function* ( + input: PromptInput, + ) { + const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) + yield* revert.cleanup(session) + const message = yield* createUserMessage(input) + yield* sessions.touch(input.sessionID) - const permissions: Permission.Ruleset = [] - for (const [t, enabled] of Object.entries(input.tools ?? {})) { - permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) - } - if (permissions.length > 0) { - session.permission = permissions - yield* sessions.setPermission({ sessionID: session.id, permission: permissions }) - } + const permissions: Permission.Ruleset = [] + for (const [t, enabled] of Object.entries(input.tools ?? {})) { + permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) + } + if (permissions.length > 0) { + session.permission = permissions + yield* sessions.setPermission({ sessionID: session.id, permission: permissions }) + } - if (input.noReply === true) return message - return yield* loop({ sessionID: input.sessionID }) - }, - ) + if (input.noReply === true) return message + return yield* loop({ sessionID: input.sessionID }) + }) const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) { const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user") From ab51197da0ce78b9a5d7f798d3fa3888e460e7d2 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 20:45:30 -0500 Subject: [PATCH 02/11] Revert "disable image resizing" This reverts commit 4100fcbd171951d214ec616d59a4ff2b5a2814a5. --- packages/opencode/src/session/prompt.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 30446552cf..934427f569 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -43,6 +43,7 @@ import { Shell } from "@/shell/shell" import { ShellID } from "@/tool/shell/id" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "@/tool/truncate" +import { Image } from "@/image/image" import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util/process" import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect" @@ -80,10 +81,10 @@ const elog = EffectLogger.create({ service: "session.prompt" }) export interface Interface { readonly cancel: (sessionID: SessionID) => Effect.Effect - readonly prompt: (input: PromptInput) => Effect.Effect + readonly prompt: (input: PromptInput) => Effect.Effect readonly loop: (input: LoopInput) => Effect.Effect readonly shell: (input: ShellInput) => Effect.Effect - readonly command: (input: CommandInput) => Effect.Effect + readonly command: (input: CommandInput) => Effect.Effect readonly resolvePromptParts: (template: string) => Effect.Effect } @@ -108,6 +109,7 @@ export const layer = Layer.effect( const lsp = yield* LSP.Service const registry = yield* ToolRegistry.Service const truncate = yield* Truncate.Service + const image = yield* Image.Service const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const scope = yield* Scope.Scope const instruction = yield* Instruction.Service @@ -124,7 +126,7 @@ export const layer = Layer.effect( return { cancel: (sessionID: SessionID) => cancel(sessionID), resolvePromptParts: (template: string) => resolvePromptParts(template), - prompt: (input: PromptInput) => prompt(input), + prompt: (input: PromptInput) => prompt(input).pipe(Effect.catch(Effect.die)), } satisfies TaskPromptOps }) @@ -1290,7 +1292,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the { message: info, parts: resolvedParts }, ) - const parts = resolvedParts + const parts = yield* Effect.forEach(resolvedParts, (part) => + part.type === "file" && part.mime.startsWith("image/") ? image.normalize(part) : Effect.succeed(part), + ) const parsed = MessageV2.Info.zod.safeParse(info) if (!parsed.success) { @@ -1389,9 +1393,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the return { info, parts } }, Effect.scoped) - const prompt: (input: PromptInput) => Effect.Effect = Effect.fn("SessionPrompt.prompt")(function* ( - input: PromptInput, - ) { + const prompt: (input: PromptInput) => Effect.Effect = Effect.fn( + "SessionPrompt.prompt", + )(function* (input: PromptInput) { const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) yield* revert.cleanup(session) const message = yield* createUserMessage(input) @@ -1809,6 +1813,7 @@ export const defaultLayer = Layer.suspend(() => Layer.provide(Session.defaultLayer), Layer.provide(SessionRevert.defaultLayer), Layer.provide(SessionSummary.defaultLayer), + Layer.provide(Image.defaultLayer), Layer.provide( Layer.mergeAll( Agent.defaultLayer, From 3ac090f5d5bf4d717990a43b9b11910348a57385 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 21:30:07 -0500 Subject: [PATCH 03/11] fix image handling in single binary builds --- packages/opencode/script/build.ts | 12 +++++++++++- packages/opencode/src/image/image.ts | 19 +++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 2f2edb4ff5..f449c4b822 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -188,6 +188,10 @@ for (const item of targets) { const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js") const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath) + const photonWasm = path.relative( + dir, + fileURLToPath(import.meta.resolve("@silvia-odwyer/photon-node/photon_rs_bg.wasm")), + ).replaceAll("\\", "/") const workerPath = "./src/cli/cmd/tui/worker.ts" // Use platform-specific bunfs root path based on target OS @@ -214,7 +218,13 @@ for (const item of targets) { windows: {}, }, files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}, - entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])], + entrypoints: [ + "./src/index.ts", + parserWorker, + workerPath, + photonWasm, + ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []), + ], define: { OPENCODE_VERSION: `'${Script.version}'`, OPENCODE_MIGRATIONS: JSON.stringify(migrations), diff --git a/packages/opencode/src/image/image.ts b/packages/opencode/src/image/image.ts index 2115e19198..35973ba97c 100644 --- a/packages/opencode/src/image/image.ts +++ b/packages/opencode/src/image/image.ts @@ -1,6 +1,7 @@ import { Config } from "@/config/config" import type { MessageV2 } from "@/session/message-v2" import * as Log from "@opencode-ai/core/util/log" +import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" } import { Context, Effect, Layer, Schema } from "effect" const MAX_BASE64_BYTES = 4.5 * 1024 * 1024 @@ -61,13 +62,12 @@ export const layer = Layer.effect( const loadPhoton = yield* Effect.cached( Effect.promise(async () => { try { - const photonWasm = (await import("@silvia-odwyer/photon-node/photon_rs_bg.wasm", { with: { type: "file" } })) - .default // Patched photon-node reads this during module init so Bun compiled binaries use the embedded wasm path. ;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH = photonWasm return await import("@silvia-odwyer/photon-node") - } catch { + } catch (error) { + log.warn("failed to load photon", { error }) return null } }), @@ -86,16 +86,23 @@ export const layer = Layer.effect( const base64 = input.url.slice(input.url.indexOf(";base64,") + ";base64,".length) const photon = yield* loadPhoton - if (!photon) return yield* new PhotonUnavailableError() + if (!photon) { + if (Buffer.byteLength(base64, "utf8") <= info.maxBase64Bytes) return input + return yield* new PhotonUnavailableError() + } const decoded = yield* Effect.sync(() => { try { return photon.PhotonImage.new_from_byteslice(Buffer.from(base64, "base64")) - } catch { + } catch (error) { + log.warn("failed to decode image", { error }) return undefined } }) - if (!decoded) return yield* new DecodeError() + if (!decoded) { + if (Buffer.byteLength(base64, "utf8") <= info.maxBase64Bytes) return input + return yield* new DecodeError() + } try { const originalWidth = decoded.get_width() From 8b196e859e8dd66b118de5c99a633994e16f10d7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 21:39:58 -0500 Subject: [PATCH 04/11] handle unavailable image resizer gracefully --- packages/opencode/src/session/processor.ts | 10 +++++++++- packages/opencode/src/session/prompt.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 579c4cc42c..19796bd9f4 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -404,7 +404,15 @@ export const layer: Layer.Layer< ) const normalized = yield* Effect.forEach(toolAttachments, (attachment) => attachment.mime.startsWith("image/") - ? image.normalize(attachment).pipe(Effect.exit) + ? image + .normalize(attachment) + .pipe( + Effect.catchIf( + (error) => error instanceof Image.PhotonUnavailableError, + () => Effect.succeed(attachment), + ), + Effect.exit, + ) : Effect.succeed(Exit.succeed(attachment)), ) const omitted = normalized.filter(Exit.isFailure).length diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 934427f569..0b60749642 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1293,7 +1293,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the ) const parts = yield* Effect.forEach(resolvedParts, (part) => - part.type === "file" && part.mime.startsWith("image/") ? image.normalize(part) : Effect.succeed(part), + part.type === "file" && part.mime.startsWith("image/") + ? image.normalize(part).pipe( + Effect.catchIf( + (error) => error instanceof Image.PhotonUnavailableError, + () => Effect.succeed(part), + ), + ) + : Effect.succeed(part), ) const parsed = MessageV2.Info.zod.safeParse(info) From 93bed80ac2ae6e3ac180ca0f0ca120a3ea0e7b7a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 21:57:28 -0500 Subject: [PATCH 05/11] avoid loading image resizer for small attachments --- packages/opencode/src/image/image.ts | 31 ++++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/image/image.ts b/packages/opencode/src/image/image.ts index 35973ba97c..25fc5be75d 100644 --- a/packages/opencode/src/image/image.ts +++ b/packages/opencode/src/image/image.ts @@ -85,24 +85,19 @@ export const layer = Layer.effect( return yield* new InvalidDataUrlError({ url: input.url }) const base64 = input.url.slice(input.url.indexOf(";base64,") + ";base64,".length) - const photon = yield* loadPhoton - if (!photon) { - if (Buffer.byteLength(base64, "utf8") <= info.maxBase64Bytes) return input - return yield* new PhotonUnavailableError() - } + const bytes = Buffer.byteLength(base64, "utf8") + if (bytes <= info.maxBase64Bytes) return input - const decoded = yield* Effect.sync(() => { - try { - return photon.PhotonImage.new_from_byteslice(Buffer.from(base64, "base64")) - } catch (error) { + const photon = yield* loadPhoton + if (!photon) return yield* new PhotonUnavailableError() + + const decoded = yield* Effect.try({ + try: () => photon.PhotonImage.new_from_byteslice(Buffer.from(base64, "base64")), + catch: (error) => { log.warn("failed to decode image", { error }) - return undefined - } + return new DecodeError() + }, }) - if (!decoded) { - if (Buffer.byteLength(base64, "utf8") <= info.maxBase64Bytes) return input - return yield* new DecodeError() - } try { const originalWidth = decoded.get_width() @@ -110,12 +105,12 @@ export const layer = Layer.effect( if ( originalWidth <= info.maxWidth && originalHeight <= info.maxHeight && - Buffer.byteLength(base64, "utf8") <= info.maxBase64Bytes + bytes <= info.maxBase64Bytes ) return input if (!info.autoResize) return yield* new SizeError({ - bytes: Buffer.byteLength(base64, "utf8"), + bytes, max: info.maxBase64Bytes, width: originalWidth, height: originalHeight, @@ -166,7 +161,7 @@ export const layer = Layer.effect( } return yield* new SizeError({ - bytes: Buffer.byteLength(base64, "utf8"), + bytes, max: info.maxBase64Bytes, width: originalWidth, height: originalHeight, From 7f0b5dcbeeceb8c656dbeaa78ffdb624d69f069b Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 22:12:15 -0500 Subject: [PATCH 06/11] rename unavailable image resizer error --- packages/opencode/src/image/image.ts | 10 +++++----- packages/opencode/src/session/processor.ts | 2 +- packages/opencode/src/session/prompt.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/image/image.ts b/packages/opencode/src/image/image.ts index 25fc5be75d..bc87f91661 100644 --- a/packages/opencode/src/image/image.ts +++ b/packages/opencode/src/image/image.ts @@ -11,12 +11,12 @@ const AUTO_RESIZE = true const JPEG_QUALITIES = [80, 85, 70, 55, 40] const log = Log.create({ service: "image" }) -export class PhotonUnavailableError extends Schema.TaggedErrorClass()( - "ImagePhotonUnavailableError", +export class ResizerUnavailableError extends Schema.TaggedErrorClass()( + "ImageResizerUnavailableError", {}, ) { override get message() { - return "Photon image processor is unavailable" + return "Image resizer is unavailable" } } @@ -47,7 +47,7 @@ export class SizeError extends Schema.TaggedErrorClass()("ImageSizeEr } } -export type Error = PhotonUnavailableError | InvalidDataUrlError | DecodeError | SizeError +export type Error = ResizerUnavailableError | InvalidDataUrlError | DecodeError | SizeError export interface Interface { readonly normalize: (input: MessageV2.FilePart) => Effect.Effect @@ -89,7 +89,7 @@ export const layer = Layer.effect( if (bytes <= info.maxBase64Bytes) return input const photon = yield* loadPhoton - if (!photon) return yield* new PhotonUnavailableError() + if (!photon) return yield* new ResizerUnavailableError() const decoded = yield* Effect.try({ try: () => photon.PhotonImage.new_from_byteslice(Buffer.from(base64, "base64")), diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 19796bd9f4..2b6637fe14 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -408,7 +408,7 @@ export const layer: Layer.Layer< .normalize(attachment) .pipe( Effect.catchIf( - (error) => error instanceof Image.PhotonUnavailableError, + (error) => error instanceof Image.ResizerUnavailableError, () => Effect.succeed(attachment), ), Effect.exit, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0b60749642..5dd0d372fc 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1296,7 +1296,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the part.type === "file" && part.mime.startsWith("image/") ? image.normalize(part).pipe( Effect.catchIf( - (error) => error instanceof Image.PhotonUnavailableError, + (error) => error instanceof Image.ResizerUnavailableError, () => Effect.succeed(part), ), ) From 7be017016cc96f4b305ce8537844d9d090cab8bd Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 23:18:54 -0500 Subject: [PATCH 07/11] simplify code --- packages/opencode/script/build.ts | 12 +----------- packages/opencode/src/image/image.ts | 21 +++++++++------------ 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index f449c4b822..2f2edb4ff5 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -188,10 +188,6 @@ for (const item of targets) { const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js") const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js") const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath) - const photonWasm = path.relative( - dir, - fileURLToPath(import.meta.resolve("@silvia-odwyer/photon-node/photon_rs_bg.wasm")), - ).replaceAll("\\", "/") const workerPath = "./src/cli/cmd/tui/worker.ts" // Use platform-specific bunfs root path based on target OS @@ -218,13 +214,7 @@ for (const item of targets) { windows: {}, }, files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}, - entrypoints: [ - "./src/index.ts", - parserWorker, - workerPath, - photonWasm, - ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []), - ], + entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])], define: { OPENCODE_VERSION: `'${Script.version}'`, OPENCODE_MIGRATIONS: JSON.stringify(migrations), diff --git a/packages/opencode/src/image/image.ts b/packages/opencode/src/image/image.ts index bc87f91661..aec8e2b1ff 100644 --- a/packages/opencode/src/image/image.ts +++ b/packages/opencode/src/image/image.ts @@ -60,17 +60,15 @@ export const layer = Layer.effect( Effect.gen(function* () { const config = yield* Config.Service const loadPhoton = yield* Effect.cached( - Effect.promise(async () => { - try { - // Patched photon-node reads this during module init so Bun compiled binaries use the embedded wasm path. - ;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH = - photonWasm - return await import("@silvia-odwyer/photon-node") - } catch (error) { - log.warn("failed to load photon", { error }) - return null - } - }), + Effect.sync(() => { + // Patched photon-node reads this during module init so Bun compiled binaries use the embedded wasm path. + ;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH = + photonWasm + }).pipe( + Effect.andThen(() => Effect.tryPromise(() => import("@silvia-odwyer/photon-node"))), + Effect.tapError((error) => Effect.sync(() => log.warn("failed to load photon", { error }))), + Effect.mapError(() => new ResizerUnavailableError()), + ), ) const normalize = Effect.fn("Image.normalize")(function* (input: MessageV2.FilePart) { @@ -89,7 +87,6 @@ export const layer = Layer.effect( if (bytes <= info.maxBase64Bytes) return input const photon = yield* loadPhoton - if (!photon) return yield* new ResizerUnavailableError() const decoded = yield* Effect.try({ try: () => photon.PhotonImage.new_from_byteslice(Buffer.from(base64, "base64")), From 296be393b5e45dcffec5a3a967386faad67136ee Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 10 May 2026 23:50:58 -0500 Subject: [PATCH 08/11] fix, rm early return --- packages/opencode/src/image/image.ts | 7 +------ packages/opencode/test/image/image.test.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/image/image.ts b/packages/opencode/src/image/image.ts index aec8e2b1ff..05830989a1 100644 --- a/packages/opencode/src/image/image.ts +++ b/packages/opencode/src/image/image.ts @@ -84,7 +84,6 @@ export const layer = Layer.effect( const base64 = input.url.slice(input.url.indexOf(";base64,") + ";base64,".length) const bytes = Buffer.byteLength(base64, "utf8") - if (bytes <= info.maxBase64Bytes) return input const photon = yield* loadPhoton @@ -99,11 +98,7 @@ export const layer = Layer.effect( try { const originalWidth = decoded.get_width() const originalHeight = decoded.get_height() - if ( - originalWidth <= info.maxWidth && - originalHeight <= info.maxHeight && - bytes <= info.maxBase64Bytes - ) + if (originalWidth <= info.maxWidth && originalHeight <= info.maxHeight && bytes <= info.maxBase64Bytes) return input if (!info.autoResize) return yield* new SizeError({ diff --git a/packages/opencode/test/image/image.test.ts b/packages/opencode/test/image/image.test.ts index bf5c0b3948..27b1326812 100644 --- a/packages/opencode/test/image/image.test.ts +++ b/packages/opencode/test/image/image.test.ts @@ -57,6 +57,23 @@ describe("Image", () => { }), ) + it.effect("resizes images that fit the byte limit but exceed dimension limits", () => + Effect.gen(function* () { + const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node")) + const source = new photon.PhotonImage(new Uint8Array(Array.from({ length: 9_000 * 4 }, () => 255)), 9_000, 1) + const image = yield* Image.Service + const result = yield* image.normalize(part("image/png", Buffer.from(source.get_bytes()).toString("base64"))) + const resized = photon.PhotonImage.new_from_byteslice( + Buffer.from(result.url.slice(result.url.indexOf(";base64,") + ";base64,".length), "base64"), + ) + + source.free() + expect(resized.get_width()).toBeLessThanOrEqual(2_000) + expect(resized.get_height()).toBeLessThanOrEqual(2_000) + resized.free() + }), + ) + tiny.effect("fails with a typed size error when no resized candidate fits", () => Effect.gen(function* () { const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node")) From 5cb0004eb7c0658ef893ea45995afbc0cafdaf74 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 11 May 2026 00:18:03 -0500 Subject: [PATCH 09/11] fix photon wasm imports in bundled builds --- .../@silvia-odwyer%2Fphoton-node@0.3.4.patch | 277 +++++++++++++++++- 1 file changed, 274 insertions(+), 3 deletions(-) diff --git a/patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch b/patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch index 2e43225562..f7267890ae 100644 --- a/patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch +++ b/patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch @@ -1,11 +1,282 @@ diff --git a/photon_rs.js b/photon_rs.js -index 8f4144d..b83e9a9 100644 +index 8f4144d..29a964a 100644 --- a/photon_rs.js +++ b/photon_rs.js -@@ -4509,7 +4509,8 @@ module.exports.__wbindgen_init_externref_table = function() { - ; +@@ -1,6 +1,7 @@ + + let imports = {}; +-imports['__wbindgen_placeholder__'] = module.exports; ++const __wbindgen_placeholder__ = {}; ++imports['__wbindgen_placeholder__'] = __wbindgen_placeholder__; + let wasm; + const { TextEncoder, TextDecoder } = require(`util`); + +@@ -4272,12 +4273,12 @@ class Rgba { + } + module.exports.Rgba = Rgba; + +-module.exports.__wbg_new_abda76e883ba8a5f = function() { ++__wbindgen_placeholder__.__wbg_new_abda76e883ba8a5f = function() { + const ret = new Error(); + return ret; }; +-module.exports.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + const ret = arg1.stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; +@@ -4285,7 +4286,7 @@ module.exports.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + +-module.exports.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { +@@ -4297,7 +4298,7 @@ module.exports.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + } + }; + +-module.exports.__wbg_instanceof_Window_c4b70662a0d2c5ec = function(arg0) { ++__wbindgen_placeholder__.__wbg_instanceof_Window_c4b70662a0d2c5ec = function(arg0) { + let result; + try { + result = arg0 instanceof Window; +@@ -4308,42 +4309,42 @@ module.exports.__wbg_instanceof_Window_c4b70662a0d2c5ec = function(arg0) { + return ret; + }; + +-module.exports.__wbg_document_e5c1786dea6542e4 = function(arg0) { ++__wbindgen_placeholder__.__wbg_document_e5c1786dea6542e4 = function(arg0) { + const ret = arg0.document; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + +-module.exports.__wbg_body_e70ae6abd01ae584 = function(arg0) { ++__wbindgen_placeholder__.__wbg_body_e70ae6abd01ae584 = function(arg0) { + const ret = arg0.body; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }; + +-module.exports.__wbg_createElement_5d4c76f218b78145 = function() { return handleError(function (arg0, arg1, arg2) { ++__wbindgen_placeholder__.__wbg_createElement_5d4c76f218b78145 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.createElement(getStringFromWasm0(arg1, arg2)); + return ret; + }, arguments) }; + +-module.exports.__wbg_width_4c6f0048d64cf86b = function(arg0) { ++__wbindgen_placeholder__.__wbg_width_4c6f0048d64cf86b = function(arg0) { + const ret = arg0.width; + return ret; + }; + +-module.exports.__wbg_height_21f0d3fd8f753394 = function(arg0) { ++__wbindgen_placeholder__.__wbg_height_21f0d3fd8f753394 = function(arg0) { + const ret = arg0.height; + return ret; + }; + +-module.exports.__wbg_width_79e0847ed5883b03 = function(arg0) { ++__wbindgen_placeholder__.__wbg_width_79e0847ed5883b03 = function(arg0) { + const ret = arg0.width; + return ret; + }; + +-module.exports.__wbg_height_e4e4e4779f8feac0 = function(arg0) { ++__wbindgen_placeholder__.__wbg_height_e4e4e4779f8feac0 = function(arg0) { + const ret = arg0.height; + return ret; + }; + +-module.exports.__wbg_data_fda507064d127f5b = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbg_data_fda507064d127f5b = function(arg0, arg1) { + const ret = arg1.data; + const ptr1 = passArray8ToWasm0(ret, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; +@@ -4351,12 +4352,12 @@ module.exports.__wbg_data_fda507064d127f5b = function(arg0, arg1) { + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + +-module.exports.__wbg_newwithu8clampedarrayandsh_1fddccb3a94a5e05 = function() { return handleError(function (arg0, arg1, arg2, arg3) { ++__wbindgen_placeholder__.__wbg_newwithu8clampedarrayandsh_1fddccb3a94a5e05 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = new ImageData(getClampedArrayU8FromWasm0(arg0, arg1), arg2 >>> 0, arg3 >>> 0); + return ret; + }, arguments) }; + +-module.exports.__wbg_instanceof_CanvasRenderingContext2d_3abbe7ec7af32cae = function(arg0) { ++__wbindgen_placeholder__.__wbg_instanceof_CanvasRenderingContext2d_3abbe7ec7af32cae = function(arg0) { + let result; + try { + result = arg0 instanceof CanvasRenderingContext2D; +@@ -4367,24 +4368,24 @@ module.exports.__wbg_instanceof_CanvasRenderingContext2d_3abbe7ec7af32cae = fun + return ret; + }; + +-module.exports.__wbg_drawImage_fede06db74e39a60 = function() { return handleError(function (arg0, arg1, arg2, arg3) { ++__wbindgen_placeholder__.__wbg_drawImage_fede06db74e39a60 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + arg0.drawImage(arg1, arg2, arg3); + }, arguments) }; + +-module.exports.__wbg_drawImage_f395c8e43c79a909 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { ++__wbindgen_placeholder__.__wbg_drawImage_f395c8e43c79a909 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + arg0.drawImage(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); + }, arguments) }; + +-module.exports.__wbg_getImageData_5e1c242046e6b59e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { ++__wbindgen_placeholder__.__wbg_getImageData_5e1c242046e6b59e = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + const ret = arg0.getImageData(arg1, arg2, arg3, arg4); + return ret; + }, arguments) }; + +-module.exports.__wbg_putImageData_a8b3e177ee06d521 = function() { return handleError(function (arg0, arg1, arg2, arg3) { ++__wbindgen_placeholder__.__wbg_putImageData_a8b3e177ee06d521 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + arg0.putImageData(arg1, arg2, arg3); + }, arguments) }; + +-module.exports.__wbg_instanceof_HtmlCanvasElement_25d964a0dde6717e = function(arg0) { ++__wbindgen_placeholder__.__wbg_instanceof_HtmlCanvasElement_25d964a0dde6717e = function(arg0) { + let result; + try { + result = arg0 instanceof HTMLCanvasElement; +@@ -4395,93 +4396,93 @@ module.exports.__wbg_instanceof_HtmlCanvasElement_25d964a0dde6717e = function(a + return ret; + }; + +-module.exports.__wbg_width_dc225e55343b745e = function(arg0) { ++__wbindgen_placeholder__.__wbg_width_dc225e55343b745e = function(arg0) { + const ret = arg0.width; + return ret; + }; + +-module.exports.__wbg_setwidth_488780db69b08846 = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbg_setwidth_488780db69b08846 = function(arg0, arg1) { + arg0.width = arg1 >>> 0; + }; + +-module.exports.__wbg_height_3a8bec2f3fe71b26 = function(arg0) { ++__wbindgen_placeholder__.__wbg_height_3a8bec2f3fe71b26 = function(arg0) { + const ret = arg0.height; + return ret; + }; + +-module.exports.__wbg_setheight_1761808c18403921 = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbg_setheight_1761808c18403921 = function(arg0, arg1) { + arg0.height = arg1 >>> 0; + }; + +-module.exports.__wbg_getContext_fc99dbd3a9a7e318 = function() { return handleError(function (arg0, arg1, arg2) { ++__wbindgen_placeholder__.__wbg_getContext_fc99dbd3a9a7e318 = function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.getContext(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments) }; + +-module.exports.__wbg_settextContent_f82a86a8df347e1c = function(arg0, arg1, arg2) { ++__wbindgen_placeholder__.__wbg_settextContent_f82a86a8df347e1c = function(arg0, arg1, arg2) { + arg0.textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); + }; + +-module.exports.__wbg_appendChild_fa3b00dade9fc4cf = function() { return handleError(function (arg0, arg1) { ++__wbindgen_placeholder__.__wbg_appendChild_fa3b00dade9fc4cf = function() { return handleError(function (arg0, arg1) { + const ret = arg0.appendChild(arg1); + return ret; + }, arguments) }; + +-module.exports.__wbg_newnoargs_e643855c6572a4a8 = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbg_newnoargs_e643855c6572a4a8 = function(arg0, arg1) { + const ret = new Function(getStringFromWasm0(arg0, arg1)); + return ret; + }; + +-module.exports.__wbg_call_f96b398515635514 = function() { return handleError(function (arg0, arg1) { ++__wbindgen_placeholder__.__wbg_call_f96b398515635514 = function() { return handleError(function (arg0, arg1) { + const ret = arg0.call(arg1); + return ret; + }, arguments) }; + +-module.exports.__wbg_self_b9aad7f1c618bfaf = function() { return handleError(function () { ++__wbindgen_placeholder__.__wbg_self_b9aad7f1c618bfaf = function() { return handleError(function () { + const ret = self.self; + return ret; + }, arguments) }; + +-module.exports.__wbg_window_55e469842c98b086 = function() { return handleError(function () { ++__wbindgen_placeholder__.__wbg_window_55e469842c98b086 = function() { return handleError(function () { + const ret = window.window; + return ret; + }, arguments) }; + +-module.exports.__wbg_globalThis_d0957e302752547e = function() { return handleError(function () { ++__wbindgen_placeholder__.__wbg_globalThis_d0957e302752547e = function() { return handleError(function () { + const ret = globalThis.globalThis; + return ret; + }, arguments) }; + +-module.exports.__wbg_global_ae2f87312b8987fb = function() { return handleError(function () { ++__wbindgen_placeholder__.__wbg_global_ae2f87312b8987fb = function() { return handleError(function () { + const ret = global.global; + return ret; + }, arguments) }; + +-module.exports.__wbindgen_is_undefined = function(arg0) { ++__wbindgen_placeholder__.__wbindgen_is_undefined = function(arg0) { + const ret = arg0 === undefined; + return ret; + }; + +-module.exports.__wbg_buffer_fcbfb6d88b2732e9 = function(arg0) { ++__wbindgen_placeholder__.__wbg_buffer_fcbfb6d88b2732e9 = function(arg0) { + const ret = arg0.buffer; + return ret; + }; + +-module.exports.__wbg_new_bc5d9aad3f9ac80e = function(arg0) { ++__wbindgen_placeholder__.__wbg_new_bc5d9aad3f9ac80e = function(arg0) { + const ret = new Uint8Array(arg0); + return ret; + }; + +-module.exports.__wbg_set_4b3aa8445ac1e91c = function(arg0, arg1, arg2) { ++__wbindgen_placeholder__.__wbg_set_4b3aa8445ac1e91c = function(arg0, arg1, arg2) { + arg0.set(arg1, arg2 >>> 0); + }; + +-module.exports.__wbg_length_d9c4ded7e708c6a1 = function(arg0) { ++__wbindgen_placeholder__.__wbg_length_d9c4ded7e708c6a1 = function(arg0) { + const ret = arg0.length; + return ret; + }; + +-module.exports.__wbindgen_debug_string = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; +@@ -4489,16 +4490,16 @@ module.exports.__wbindgen_debug_string = function(arg0, arg1) { + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + +-module.exports.__wbindgen_throw = function(arg0, arg1) { ++__wbindgen_placeholder__.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + +-module.exports.__wbindgen_memory = function() { ++__wbindgen_placeholder__.__wbindgen_memory = function() { + const ret = wasm.memory; + return ret; + }; + +-module.exports.__wbindgen_init_externref_table = function() { ++__wbindgen_placeholder__.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_2; + const offset = table.grow(4); + table.set(0, undefined); +@@ -4509,7 +4510,8 @@ module.exports.__wbindgen_init_externref_table = function() { + ; + }; + -const path = require('path').join(__dirname, 'photon_rs_bg.wasm'); +// Allow opencode's Bun compiled binary to point photon-node at its embedded wasm asset. +const path = globalThis.__OPENCODE_PHOTON_WASM_PATH || require('path').join(__dirname, 'photon_rs_bg.wasm'); From 57861545eed6c7c77eff183d54a1018e9dfd64cb Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 13 May 2026 11:03:22 -0500 Subject: [PATCH 10/11] fix: resolve bundled photon wasm path --- packages/opencode/src/image/image.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/image/image.ts b/packages/opencode/src/image/image.ts index 05830989a1..90192aca06 100644 --- a/packages/opencode/src/image/image.ts +++ b/packages/opencode/src/image/image.ts @@ -3,6 +3,8 @@ import type { MessageV2 } from "@/session/message-v2" import * as Log from "@opencode-ai/core/util/log" import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" } import { Context, Effect, Layer, Schema } from "effect" +import path from "node:path" +import { fileURLToPath } from "node:url" const MAX_BASE64_BYTES = 4.5 * 1024 * 1024 const MAX_WIDTH = 2000 @@ -63,7 +65,7 @@ export const layer = Layer.effect( Effect.sync(() => { // Patched photon-node reads this during module init so Bun compiled binaries use the embedded wasm path. ;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH = - photonWasm + path.isAbsolute(photonWasm) ? photonWasm : fileURLToPath(new URL(photonWasm, import.meta.url)) }).pipe( Effect.andThen(() => Effect.tryPromise(() => import("@silvia-odwyer/photon-node"))), Effect.tapError((error) => Effect.sync(() => log.warn("failed to load photon", { error }))), From 4fcd6e6640c3621ee60e6f09aba3ec5acdb527ef Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 13 May 2026 11:44:33 -0500 Subject: [PATCH 11/11] docs: document image attachment resizing --- packages/web/src/content/docs/config.mdx | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index ec96069c70..113544fc36 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -385,6 +385,35 @@ You can also configure [local models](/docs/models#local). [Learn more](/docs/mo --- +### Image attachments + +OpenCode normalizes image attachments before sending them to the model. By default, images are resized when they exceed `2000x2000` pixels or `4718592` base64 bytes. + +Configure image attachment limits with the `attachment.image` option: + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "attachment": { + "image": { + "auto_resize": true, + "max_width": 2000, + "max_height": 2000, + "max_base64_bytes": 4718592 + } + } +} +``` + +- `auto_resize` - Resize images that exceed the configured limits before provider requests. Set to `false` to reject oversized images instead. +- `max_width` - Maximum image width in pixels before resizing or rejection. +- `max_height` - Maximum image height in pixels before resizing or rejection. +- `max_base64_bytes` - Maximum encoded image payload size. This is the base64 payload size, not the original file size. + +If an image still cannot fit after resizing, OpenCode omits oversized tool-result images or fails oversized user-provided images with an image size error. + +--- + #### Provider-Specific Options Some providers support additional configuration options beyond the generic `timeout` and `apiKey` settings.