Skip to content

fix(desktop): read composer image preview locally in remote mode - #167

Merged
Kyzcreig merged 1 commit into
mainfrom
fix/desktop-remote-image-paste-preview
Jul 2, 2026
Merged

Kyzcreig merged 1 commit into
mainfrom
fix/desktop-remote-image-paste-preview

Conversation

@Kyzcreig

@Kyzcreig Kyzcreig commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Problem

When the desktop app runs against a remote gateway (thin client on one Mac driving a hermes dashboard on another), pasting an image into the composer pops "Image preview failed" / "file not found" every time.

Root cause

A pasted/clipboard/dropped/picked composer image is written to the client's local Electron userData/composer-images/ dir (saveImageBuffer / saveClipboardImage) — even in remote mode, because that's the only place the clipboard bytes exist. But the preview readback (attachImagePath) went through readDesktopFileDataUrl(), which in remote mode routes to the backend's GET /api/fs/read-data-url. The backend is a different machine and has no such file → 404 File not found → the "Image preview failed" toast.

Verified live: the pasted PNG exists on the client Mac's disk; the exact path does not exist on the backend Mac, and _fs_regular_file raises 404 "File not found" for it.

The attachment itself was unaffected — submit uploads the bytes via image.attach_bytes, which already reads the local path through the Electron bridge (readImageForRemoteAttach → window.hermesDesktop.readFileDataUrl). Only the thumbnail preview was broken.

Fix

Add readComposerImagePreview() which reads the preview from the local Electron bridge first, falling back to the remote-aware read only when the local bridge is unavailable or can't read the path (e.g. a path dragged from the remote file tree). This mirrors what the submit-time upload already does, and keeps local-mode behavior identical.

Tests

3 new cases in desktop-fs.test.ts:

  • remote mode reads the local bridge and never touches backend FS REST (the regression guard)
  • local mode still reads the local bridge
  • remote-file-tree path falls back to the backend read when the local read throws

apps/desktop typecheck, eslint, and prettier all clean; the two touched test files are green (22 tests).

@greptile-apps

greptile-apps Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR fixes a "Image preview failed" regression in remote (thin-client) mode by introducing readComposerImagePreview(), which reads composer images from the local Electron bridge first and falls back to the remote-aware readDesktopFileDataUrl() only when the local read throws.

  • desktop-fs.ts: Adds readComposerImagePreview() — tries window.hermesDesktop.readFileDataUrl (local bridge) first; swallows errors and falls through to the backend FS REST only for paths that don't exist on the client (e.g. dragged from the remote file tree).
  • use-composer-actions.ts: Replaces the single readDesktopFileDataUrl call in attachImagePath with the new readComposerImagePreview.
  • desktop-fs.test.ts: Adds 3 targeted regression tests covering remote-local, local-local, and the ENOENT fallback case.

Confidence Score: 5/5

Safe to merge. The change is a narrow, targeted fix with no impact on submit-time attachment behaviour.

The fix is minimal and well-scoped: one new function in desktop-fs.ts, one call-site update, and three regression tests that specifically guard the broken code path. The local-first read correctly mirrors what the submit-time attachment path already did. All existing tests continue to pass, and the fallback to the remote-aware read preserves functionality for paths that originate from the remote file tree. No shared state, no side-effects outside the preview thumbnail path.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/lib/desktop-fs.ts Adds readComposerImagePreview() with local-first read and silent error fallback. Logic is sound; one subtle edge: a falsy (empty-string) return from the local bridge also triggers the fallback, which in remote mode issues an unnecessary backend request.
apps/desktop/src/app/chat/hooks/use-composer-actions.ts Single call-site swap from readDesktopFileDataUrl to readComposerImagePreview; existing try/catch error-handling is preserved and still shows the Image preview failed toast on failure.
apps/desktop/src/lib/desktop-fs.test.ts Adds a focused describe block with 3 new tests; all key paths (remote-local, local-local, ENOENT fallback) are covered. The no-bridge case and empty-string-return case are not explicitly tested but are not regression risks.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[attachImagePath called\nwith filePath] --> B[readComposerImagePreview]
    B --> C{window.hermesDesktop\n.readFileDataUrl available?}
    C -- No --> F[readDesktopFileDataUrl]
    C -- Yes --> D[localReader path]
    D -- Returns non-empty string --> E[Return local data URL\n✓ Preview shown]
    D -- Returns empty string --> F
    D -- Throws e.g. ENOENT --> F
    F --> G{Remote mode?}
    G -- No --> H[bridge.readFileDataUrl path\nLocal Electron IPC]
    G -- Yes --> I[GET /api/fs/read-data-url\nBackend REST]
    H --> J[Return data URL]
    I --> J
    H -- Throws --> K[Error propagates to caller]
    I -- 404 Not Found --> K
    K --> L[notifyError\nImage preview failed toast]
    J --> M[addComposerAttachment\nwith previewUrl]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[attachImagePath called\nwith filePath] --> B[readComposerImagePreview]
    B --> C{window.hermesDesktop\n.readFileDataUrl available?}
    C -- No --> F[readDesktopFileDataUrl]
    C -- Yes --> D[localReader path]
    D -- Returns non-empty string --> E[Return local data URL\n✓ Preview shown]
    D -- Returns empty string --> F
    D -- Throws e.g. ENOENT --> F
    F --> G{Remote mode?}
    G -- No --> H[bridge.readFileDataUrl path\nLocal Electron IPC]
    G -- Yes --> I[GET /api/fs/read-data-url\nBackend REST]
    H --> J[Return data URL]
    I --> J
    H -- Throws --> K[Error propagates to caller]
    I -- 404 Not Found --> K
    K --> L[notifyError\nImage preview failed toast]
    J --> M[addComposerAttachment\nwith previewUrl]
Loading

Reviews (2): Last reviewed commit: "fix(desktop): read composer image previe..." | Re-trigger Greptile

A pasted/clipboard/dropped/picked composer image is written to the
CLIENT's local Electron userData dir (saveImageBuffer / saveClipboardImage)
even when the app runs against a REMOTE gateway. The preview readback,
however, went through readDesktopFileDataUrl(), which in remote mode routes
to the backend's GET /api/fs/read-data-url. The backend is a different
machine and has no such file, so it returns 404 "File not found" — surfaced
to the user as the "Image preview failed" toast on every paste.

The attachment itself was unaffected (submit uploads the bytes via
image.attach_bytes, which already reads the local path through the Electron
bridge) — only the thumbnail preview was broken.

Fix: add readComposerImagePreview(), which reads the preview from the LOCAL
Electron bridge first and only falls back to the remote-aware read when the
local bridge is unavailable or can't read the path (e.g. a path dragged from
the remote file tree). This mirrors what the submit-time upload already does
and keeps the local-mode behavior identical.

Tests: 3 new cases in desktop-fs.test.ts assert remote-mode preview reads
the local bridge and never touches the backend FS REST, plus the
remote-file-tree fallback path.
@Kyzcreig
Kyzcreig force-pushed the fix/desktop-remote-image-paste-preview branch from 6ff24ce to aa3df2c Compare July 2, 2026 05:11
@Kyzcreig
Kyzcreig merged commit 1184b02 into main Jul 2, 2026
24 checks passed
@Kyzcreig
Kyzcreig deleted the fix/desktop-remote-image-paste-preview branch July 2, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant