fix(desktop): read attachment previews local-first in remote mode
attachImagePath fetched its thumbnail through readDesktopFileDataUrl, which in remote mode routes every read to the gateway fs bridge. Paperclip picks, clipboard saves, and OS drops always produce paths on the LOCAL machine, so the gateway read 404s — toasting "image preview failed" and dropping the thumbnail even though the attach itself works (upload reads local bytes via the Electron bridge). Read the local bridge first and fall back to the remote facade, which still serves in-app drags from the remote project tree. Local mode is unchanged (the facade already reads locally there). Follow-up to #56572, which restored the remote paperclip picker and made this path reachable from the picker as well.
This commit is contained in:
parent
c19bfb50ad
commit
fe82b3a774
2 changed files with 91 additions and 2 deletions
|
|
@ -1,6 +1,14 @@
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { type DroppedFile, extractDroppedFiles, HERMES_PATHS_MIME, partitionDroppedFiles } from './use-composer-actions'
|
||||
import { $connection } from '@/store/session'
|
||||
|
||||
import {
|
||||
attachmentPreviewDataUrl,
|
||||
type DroppedFile,
|
||||
extractDroppedFiles,
|
||||
HERMES_PATHS_MIME,
|
||||
partitionDroppedFiles
|
||||
} from './use-composer-actions'
|
||||
|
||||
// A Finder/Explorer drop carries a native File handle; an in-app drag (project
|
||||
// tree, gutter line ref) is path-only. The split decides whether a drop becomes
|
||||
|
|
@ -178,3 +186,61 @@ describe('extractDroppedFiles', () => {
|
|||
expect(result[0]?.isDirectory).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('attachmentPreviewDataUrl', () => {
|
||||
const LOCAL_PREVIEW = 'data:image/png;base64,bG9jYWw='
|
||||
const REMOTE_PREVIEW = 'data:image/png;base64,cmVtb3Rl'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.clearAllMocks()
|
||||
$connection.set(null)
|
||||
})
|
||||
|
||||
it('reads a local path via the local bridge even in remote mode (paperclip/paste/OS drop)', async () => {
|
||||
const readFileDataUrl = vi.fn(async () => LOCAL_PREVIEW)
|
||||
const api = vi.fn()
|
||||
|
||||
vi.stubGlobal('window', { hermesDesktop: { api, readFileDataUrl } })
|
||||
$connection.set({ mode: 'remote' } as never)
|
||||
|
||||
await expect(attachmentPreviewDataUrl('/Users/me/Pictures/pic.png')).resolves.toBe(LOCAL_PREVIEW)
|
||||
|
||||
expect(readFileDataUrl).toHaveBeenCalledWith('/Users/me/Pictures/pic.png')
|
||||
expect(api).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the remote fs bridge when the path is not on this machine (project-tree drag)', async () => {
|
||||
const readFileDataUrl = vi.fn(async () => {
|
||||
throw new Error('ENOENT')
|
||||
})
|
||||
|
||||
const api = vi.fn(async ({ path }: { path: string }) => {
|
||||
if (path.startsWith('/api/fs/read-data-url?')) {
|
||||
return { dataUrl: REMOTE_PREVIEW }
|
||||
}
|
||||
|
||||
throw new Error(`unexpected path ${path}`)
|
||||
})
|
||||
|
||||
vi.stubGlobal('window', { hermesDesktop: { api, readFileDataUrl } })
|
||||
$connection.set({ mode: 'remote' } as never)
|
||||
|
||||
await expect(attachmentPreviewDataUrl('/home/gateway/shot.png')).resolves.toBe(REMOTE_PREVIEW)
|
||||
|
||||
expect(api).toHaveBeenCalledWith({
|
||||
path: '/api/fs/read-data-url?path=%2Fhome%2Fgateway%2Fshot.png'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back when the local bridge returns an empty read', async () => {
|
||||
const readFileDataUrl = vi.fn(async () => '')
|
||||
|
||||
const api = vi.fn(async () => ({ dataUrl: REMOTE_PREVIEW }))
|
||||
|
||||
vi.stubGlobal('window', { hermesDesktop: { api, readFileDataUrl } })
|
||||
$connection.set({ mode: 'remote' } as never)
|
||||
|
||||
await expect(attachmentPreviewDataUrl('/home/gateway/shot.png')).resolves.toBe(REMOTE_PREVIEW)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -39,6 +39,29 @@ export function isImagePath(filePath: string): boolean {
|
|||
return IMAGE_EXTENSION_PATTERN.test(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an attachment's thumbnail preview, local disk first. Paperclip picks,
|
||||
* clipboard saves, and OS drops always hand us paths on THIS machine — the
|
||||
* remote-routed fs facade would 404 them against the gateway and toast a bogus
|
||||
* "preview failed" even though the attach itself works (upload reads local
|
||||
* bytes too). In-app drags from the remote project tree are the opposite case:
|
||||
* the local read fails there, so fall back to the facade (remote fs bridge).
|
||||
* In local mode the facade IS the local bridge, so this stays a single read.
|
||||
*/
|
||||
export async function attachmentPreviewDataUrl(filePath: string): Promise<string> {
|
||||
try {
|
||||
const local = await window.hermesDesktop?.readFileDataUrl?.(filePath)
|
||||
|
||||
if (local) {
|
||||
return local
|
||||
}
|
||||
} catch {
|
||||
// Not on this machine (or unreadable locally) — try the gateway.
|
||||
}
|
||||
|
||||
return readDesktopFileDataUrl(filePath)
|
||||
}
|
||||
|
||||
export interface DroppedFile {
|
||||
/** Browser-native File handle. Absent for in-app drags (e.g. project tree). */
|
||||
file?: File
|
||||
|
|
@ -367,7 +390,7 @@ export function useComposerActions({ activeSessionId, currentCwd, requestGateway
|
|||
attachToMain(baseAttachment)
|
||||
|
||||
try {
|
||||
const previewUrl = await readDesktopFileDataUrl(filePath)
|
||||
const previewUrl = await attachmentPreviewDataUrl(filePath)
|
||||
|
||||
if (previewUrl) {
|
||||
addComposerAttachment({ ...baseAttachment, previewUrl })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue