fix: allow pasted images in agent text box

Fixes #150
This commit is contained in:
Nikhil
2026-05-08 18:26:31 -07:00
committed by GitHub
parent 4e405681a7
commit b89ea201fa
2 changed files with 99 additions and 2 deletions

View File

@@ -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)
}
})
})

View File

@@ -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,
},