Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -859,4 +859,60 @@ describe('uploadComposerAttachment remote read failures', () => {
)
).rejects.toThrow('ENOENT: no such file')
})

it('hosted web client attaches a non-image file by path only (no client read, no data_url)', async () => {
// The web SPA has no local disk: the .md was already uploaded to
// ~/.hermes/uploads by the web shim, so we attach by PATH and let the gateway
// read it. The shim's readFileDataUrl only caches image previews, so for a
// .md it returns '' — the old code then threw "Could not read <name>" even
// though the upload had succeeded.
const win = window as unknown as { __HERMES_WEB_CLIENT__?: boolean }
win.__HERMES_WEB_CLIENT__ = true
const readFileDataUrl = vi.fn(async () => '') // web-shim cache miss for non-images

try {
Object.defineProperty(window, 'hermesDesktop', {
configurable: true,
value: { readFileDataUrl }
})

const calls: { method: string; params?: Record<string, unknown> }[] = []
const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
calls.push({ method, params })
if (method === 'file.attach') {
return {
attached: true,
ref_text: '@file:.hermes/desktop-attachments/notes.md',
uploaded: true
} as never
}
return {} as never
})

const result = await uploadComposerAttachment(
{
id: 'file:notes',
kind: 'file',
label: 'notes.md',
path: '/home/hermes/.hermes/uploads/20260613/071447-79fff2b5-notes.md'
},
{ remote: true, requestGateway, sessionId: RUNTIME_SESSION_ID }
)

// Attaches by path; never reads bytes client-side; sends NO data_url.
expect(readFileDataUrl).not.toHaveBeenCalled()
expect(calls).toHaveLength(1)
expect(calls[0]?.method).toBe('file.attach')
expect(calls[0]?.params).toMatchObject({
session_id: RUNTIME_SESSION_ID,
path: '/home/hermes/.hermes/uploads/20260613/071447-79fff2b5-notes.md',
name: 'notes.md'
})
expect(calls[0]?.params).not.toHaveProperty('data_url')
expect(result.refText).toBe('@file:.hermes/desktop-attachments/notes.md')
expect(result.attachedSessionId).toBe(RUNTIME_SESSION_ID)
} finally {
delete win.__HERMES_WEB_CLIENT__
}
})
})
21 changes: 20 additions & 1 deletion apps/desktop/src/app/session/hooks/use-prompt-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ async function readFileDataUrlForAttach(filePath: string): Promise<string | null
return dataUrl || null
}

// The hosted web SPA (lib/web-shim.ts) sets `window.__HERMES_WEB_CLIENT__`. That
// build runs in a plain browser with NO local filesystem: every file path it
// holds is already gateway-resident — non-image drops are uploaded to
// ~/.hermes/uploads via the web shim, and in-app refs are workspace paths. So it
// cannot (and need not) read file bytes client-side for a path attach; the
// gateway's file.attach resolves the path directly. Reading would hit the web
// shim's image-only data-URL cache and wrongly fail for .md/text/pdf.
function isHostedWebClient(): boolean {
return Boolean((window as unknown as { __HERMES_WEB_CLIENT__?: boolean }).__HERMES_WEB_CLIENT__)
}

// The readFileDataUrl IPC base64-loads the whole file into memory and is
// hard-capped (DATA_URL_READ_MAX_BYTES, 16 MB) in electron/hardening.cjs, which
// rejects with a raw "file is too large (N bytes; limit M bytes)" string. In
Expand Down Expand Up @@ -235,7 +246,15 @@ export async function uploadComposerAttachment(
// Non-image file.
let dataUrl: string | null = null

if (remote) {
// Native (Electron) remote mode reads the file's bytes here because the path
// lives on the CLIENT's disk, invisible to the gateway — they ride along as a
// data_url. The hosted web client is the opposite: it has no local disk and
// the file is ALREADY on the gateway (uploaded to ~/.hermes/uploads, or an
// in-app workspace ref), so attach by path only and let the gateway read it.
// Reading here would hit the web shim's image-only cache and throw
// "Could not read <name>" for every .md/text/pdf even though the upload that
// produced this attachment already succeeded.
if (remote && !isHostedWebClient()) {
try {
dataUrl = await readFileDataUrlForAttach(path)
} catch (err) {
Expand Down
Loading