fix(desktop): persist dropped image bytes before attach

This commit is contained in:
Gille
2026-08-11 17:16:03 -06:00
committed by brooklyn!
parent 486f4ace20
commit edb33be511
2 changed files with 83 additions and 2 deletions
@@ -1,5 +1,7 @@
import { act, renderHook } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ComposerAttachment } from '@/store/composer'
import { $connection } from '@/store/session'
import {
@@ -7,7 +9,8 @@ import {
type DroppedFile,
extractDroppedFiles,
HERMES_PATHS_MIME,
partitionDroppedFiles
partitionDroppedFiles,
useComposerActions
} from './use-composer-actions'
// A Finder/Explorer drop carries a native File handle; an in-app drag (project
@@ -244,3 +247,74 @@ describe('attachmentPreviewDataUrl', () => {
await expect(attachmentPreviewDataUrl('/home/gateway/shot.png')).resolves.toBe(REMOTE_PREVIEW)
})
})
describe('useComposerActions native image drops', () => {
afterEach(() => {
Reflect.deleteProperty(window, 'hermesDesktop')
vi.unstubAllGlobals()
vi.clearAllMocks()
})
it('copies dropped screenshot bytes before trusting a transient macOS path', async () => {
const transientPath =
'/var/folders/x7/example/T/TemporaryItems/NSIRD_screencaptureui_4roSuW/Screen Shot 2026-08-11.png'
const durablePath = '/Users/test/Library/Application Support/Hermes/composer-images/composer_saved.png'
const previewUrl = 'data:image/png;base64,c2NyZWVuc2hvdA=='
const screenshot = new File([new Uint8Array([1, 2, 3])], 'Screen Shot 2026-08-11.png', {
type: 'image/png'
})
const saveImageBuffer = vi.fn(async () => durablePath)
const readFileDataUrl = vi.fn(async (path: string) => {
if (path === transientPath) {
throw new Error('temporary screenshot path disappeared')
}
return previewUrl
})
const add = vi.fn<(attachment: ComposerAttachment) => void>()
Object.defineProperty(window, 'hermesDesktop', {
configurable: true,
value: {
readFileDataUrl,
saveImageBuffer
}
})
const { result } = renderHook(() =>
useComposerActions({
activeSessionId: null,
currentCwd: '/Users/test/project',
requestGateway: vi.fn(),
scope: {
add,
remove: vi.fn(() => null),
target: 'test-composer'
}
})
)
let attached = false
await act(async () => {
attached = await result.current.attachDroppedItems([{ file: screenshot, path: transientPath }])
})
expect(attached).toBe(true)
expect(saveImageBuffer).toHaveBeenCalledOnce()
expect(readFileDataUrl).toHaveBeenCalledWith(durablePath)
expect(readFileDataUrl).not.toHaveBeenCalledWith(transientPath)
expect(add).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'image',
path: durablePath,
previewUrl
})
)
})
})
@@ -644,7 +644,14 @@ export function useComposerActions({
const isImage = file.type.startsWith('image/') || isImagePath(file.name) || (filePath && isImagePath(filePath))
if (isImage) {
if ((filePath && (await attachImagePath(filePath))) || (await attachImageBlob(file))) {
// Finder may expose a dropped screenshot through a short-lived
// TemporaryItems/NSIRD_screencaptureui path even when the visible
// file has already landed on Desktop. Reading that path for the
// preview can succeed, then image.attach fails after macOS removes
// it before submit. Persist the File bytes into Desktop's durable
// composer-image cache first; keep the native path as a compatibility
// fallback for older shells that cannot save the buffer.
if ((await attachImageBlob(file)) || (filePath && (await attachImagePath(filePath)))) {
attached = true
continue