From b89ea201faf37a3f5fbe4bc0df4012073c2246de Mon Sep 17 00:00:00 2001 From: Nikhil Date: Fri, 8 May 2026 18:26:31 -0700 Subject: [PATCH] fix: allow pasted images in agent text box Fixes #150 --- .../apps/agent/lib/attachments.test.ts | 96 +++++++++++++++++++ .../apps/agent/lib/attachments.ts | 5 +- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 packages/browseros-agent/apps/agent/lib/attachments.test.ts diff --git a/packages/browseros-agent/apps/agent/lib/attachments.test.ts b/packages/browseros-agent/apps/agent/lib/attachments.test.ts new file mode 100644 index 000000000..ceda85ba2 --- /dev/null +++ b/packages/browseros-agent/apps/agent/lib/attachments.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'bun:test' +import { stageAttachment } from './attachments' + +function restoreGlobal(name: string, value: unknown) { + if (value === undefined) { + Reflect.deleteProperty(globalThis, name) + return + } + Reflect.set(globalThis, name, value) +} + +describe('stageAttachment', () => { + it('stages pasted clipboard images that do not have a filename', async () => { + const file = new File([new Uint8Array([1, 2, 3])], '', { + type: 'image/png', + }) + + const result = await stageAttachment(file) + + expect(result.ok).toBe(true) + if (!result.ok) throw new Error(result.error.message) + expect(result.attachment.kind).toBe('image') + expect(result.attachment.name).toBe('image') + expect(result.attachment.mediaType).toBe('image/png') + expect(result.attachment.dataUrl).toStartWith('data:image/png;base64,') + expect(result.attachment.payload).toMatchObject({ + kind: 'image', + mediaType: 'image/png', + dataUrl: result.attachment.dataUrl, + }) + }) + + it('uses the recompressed blob media type for large pasted images', async () => { + const originalCreateImageBitmap = Reflect.get( + globalThis, + 'createImageBitmap', + ) + const originalOffscreenCanvas = Reflect.get(globalThis, 'OffscreenCanvas') + const originalHTMLCanvasElement = Reflect.get( + globalThis, + 'HTMLCanvasElement', + ) + + class FakeOffscreenCanvas { + width: number + height: number + + constructor(width: number, height: number) { + this.width = width + this.height = height + } + + getContext() { + return { + drawImage() {}, + } + } + + async convertToBlob(options: { type?: string }) { + return new Blob([new Uint8Array([9, 8, 7])], { + type: options.type ?? 'image/jpeg', + }) + } + } + + try { + Reflect.set(globalThis, 'createImageBitmap', async () => ({ + width: 4096, + height: 2048, + close() {}, + })) + Reflect.set(globalThis, 'OffscreenCanvas', FakeOffscreenCanvas) + Reflect.set(globalThis, 'HTMLCanvasElement', class HTMLCanvasElement {}) + + const file = new File([new Uint8Array(2 * 1024 * 1024)], 'shot.png', { + type: 'image/png', + }) + + const result = await stageAttachment(file) + + expect(result.ok).toBe(true) + if (!result.ok) throw new Error(result.error.message) + expect(result.attachment.mediaType).toBe('image/jpeg') + expect(result.attachment.dataUrl).toStartWith('data:image/jpeg;base64,') + expect(result.attachment.payload).toMatchObject({ + kind: 'image', + mediaType: 'image/jpeg', + dataUrl: result.attachment.dataUrl, + }) + } finally { + restoreGlobal('createImageBitmap', originalCreateImageBitmap) + restoreGlobal('OffscreenCanvas', originalOffscreenCanvas) + restoreGlobal('HTMLCanvasElement', originalHTMLCanvasElement) + } + }) +}) diff --git a/packages/browseros-agent/apps/agent/lib/attachments.ts b/packages/browseros-agent/apps/agent/lib/attachments.ts index ea915805c..7c462216d 100644 --- a/packages/browseros-agent/apps/agent/lib/attachments.ts +++ b/packages/browseros-agent/apps/agent/lib/attachments.ts @@ -100,6 +100,7 @@ export async function stageAttachment( try { const compressed = await compressImageIfNeeded(file) const dataUrl = await readAsDataUrl(compressed) + const encodedMediaType = compressed.type || mediaType // Rough byte ceiling — `data:image/png;base64,...` doubles size with // base64. Reject early so we never POST something the route will 400. if (dataUrl.length > MAX_IMAGE_BYTES * 2) { @@ -118,12 +119,12 @@ export async function stageAttachment( attachment: { id: makeId(), kind: 'image', - mediaType, + mediaType: encodedMediaType, name: file.name || 'image', dataUrl, payload: { kind: 'image', - mediaType, + mediaType: encodedMediaType, dataUrl, name: file.name || undefined, },