feat(desktop+gateway): remote media relay — attach images/PDFs & display gateway images over the network#41203
feat(desktop+gateway): remote media relay — attach images/PDFs & display gateway images over the network#41203teknium1 wants to merge 1 commit into
Conversation
…splay gateway images over the network
Desktop connected to a remote gateway can now attach images and PDFs and
display agent-written images. Previously the desktop passed a LOCAL file path
to image.attach; on a remote gateway that path doesn't exist, so the image was
silently dropped ("skipped unreadable path") and the vision model never saw it.
The reverse direction was also broken — images the agent wrote on the gateway
rendered as dead links in the remote client.
Gateway (tui_gateway/server.py):
- image.attach_bytes: base64 byte upload written into the gateway's own images
dir and queued via the existing native-image-attach pipeline. Magic-byte
extension sniffing, data-URL prefix + whitespace tolerance, 25 MB cap,
structured error codes. Accepts content_base64/filename (canonical) and
data/ext (older-desktop aliases).
- pdf.attach: renders each page to PNG via pdftoppm (poppler-utils) at 150 DPI
and queues the pages as images; 50 MB / 25-page caps. Accepts host path or
base64 upload.
- Shared helpers (_decode_attach_base64, _sniff_image_ext, _queue_attached_image)
so the two methods and the existing image.attach don't duplicate logic.
Gateway (hermes_cli/web_server.py):
- GET /api/media: returns a gateway-local image as a base64 data URL so remote
clients can display it. Auth-gated like every /api route, extension
allowlist + size cap, AND confined to the gateway's own media roots
(images/screenshots/cache, resolved symlink-safe) so an authed caller can't
read image-extension files anywhere on disk.
Desktop (apps/desktop):
- syncImageAttachmentsForSubmit uploads bytes via image.attach_bytes when the
connection mode is 'remote'; the local fast path is unchanged.
- media.ts gains isRemoteGateway() + gatewayMediaDataUrl(); directive-text and
markdown-text fetch images over /api/media in remote mode.
Consolidates the competing remote-media PRs (#38876, #40317, #21908, #39437)
into one coherent implementation, taking the strongest parts of each and adding
shared-helper cleanup plus the /api/media root-confinement hardening on top.
The per-profile gateway switching from #38876 is intentionally left out as a
separable feature. TUI file uploads (#40492) remain a separate surface.
Tested: 11 new tui_gateway tests + 5 /api/media endpoint tests + desktop
media.remote unit tests; full tui_gateway + web_server suites green (472
passed); tsc -b clean; E2E verified the full attach→disk→queue and
gateway-path→data-URL display round-trip plus the out-of-root security block.
Co-authored-by: Max Mitcham <maxmitcham@mac.home>
Co-authored-by: Justlrnal4 <Justlrnal4@users.noreply.github.com>
Co-authored-by: Chris Cook <ccook@nvms.com>
Co-authored-by: Thomas Paquette <thomas.paquette@gmail.com>
🔎 Lint report:
|
|
Already landed on main as 16786f3 (same commit title), with teknium's and the four co-authors' authorship preserved via Co-authored-by. The PR stayed open only because it merged as a re-authored squash rather than through the GitHub button, and its branch sits on a months-old main (hence the conflict flag). Verified the whole round-trip is live on main: |
Finder/OS drops became `@file:/Users/...` refs that only resolve when the gateway shares the local disk, so on a remote gateway non-image files (PDF/CSV/Markdown/...) never reached the agent. Route OS drops through the file.attach / image.attach_bytes upload pipeline — in-app project-tree and gutter drags stay inline workspace-relative refs — across every drop surface: the conversation area, the composer form, the contenteditable input, and the message-edit composer (which still reproduced the bug). Also: - upload dropped files eagerly when a session exists, so the card shows a spinner instead of stalling the send (images stay submit-time to avoid racing their thumbnail write); - round the attachment card and drop the monospace detail; - render image previews from the bytes we already hold, so a pasted/dropped screenshot shows its thumbnail and previews even when its only on-disk copy is a transient path (the data URL is not persisted to localStorage). Supersedes #38615, #41203. Co-authored-by: LeonSGP <154585401+LeonSGP43@users.noreply.github.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Finder/OS drops became `@file:/Users/...` refs that only resolve when the gateway shares the local disk, so on a remote gateway non-image files (PDF/CSV/Markdown/...) never reached the agent. Route OS drops through the file.attach / image.attach_bytes upload pipeline — in-app project-tree and gutter drags stay inline workspace-relative refs — across every drop surface: the conversation area, the composer form, the contenteditable input, and the message-edit composer (which still reproduced the bug). Also: - upload dropped files eagerly when a session exists, so the card shows a spinner instead of stalling the send (images stay submit-time to avoid racing their thumbnail write); - round the attachment card and drop the monospace detail; - render image previews from the bytes we already hold, so a pasted/dropped screenshot shows its thumbnail and previews even when its only on-disk copy is a transient path (the data URL is not persisted to localStorage). Supersedes NousResearch#38615, NousResearch#41203. Co-authored-by: LeonSGP <154585401+LeonSGP43@users.noreply.github.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Finder/OS drops became `@file:/Users/...` refs that only resolve when the gateway shares the local disk, so on a remote gateway non-image files (PDF/CSV/Markdown/...) never reached the agent. Route OS drops through the file.attach / image.attach_bytes upload pipeline — in-app project-tree and gutter drags stay inline workspace-relative refs — across every drop surface: the conversation area, the composer form, the contenteditable input, and the message-edit composer (which still reproduced the bug). Also: - upload dropped files eagerly when a session exists, so the card shows a spinner instead of stalling the send (images stay submit-time to avoid racing their thumbnail write); - round the attachment card and drop the monospace detail; - render image previews from the bytes we already hold, so a pasted/dropped screenshot shows its thumbnail and previews even when its only on-disk copy is a transient path (the data URL is not persisted to localStorage). Supersedes NousResearch#38615, NousResearch#41203. Co-authored-by: LeonSGP <154585401+LeonSGP43@users.noreply.github.com> Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
Summary
Desktop connected to a remote gateway can now attach images and PDFs and display agent-written images — the whole remote-media round-trip works for the first time.
Previously the desktop passed a local file path to
image.attach. On a remote gateway (VPS) that path doesn't exist, so the upload was silently dropped ("skipped unreadable path") and the vision model never saw the image. The reverse direction was broken too: images the agent wrote on the gateway rendered as dead links in the remote client.This consolidates the competing remote-media PRs (#38876, #40317, #21908, #39437) into one coherent implementation, taking the strongest part of each and adding shared-helper cleanup + a security hardening of
/api/mediaon top.Changes
Gateway —
tui_gateway/server.pyimage.attach_bytes: base64 byte upload, written into the gateway's ownimages/dir and queued via the existing native-image-attach pipeline. Magic-byte extension sniffing, data-URL prefix + whitespace tolerance, 25 MB cap, structured error codes. Acceptscontent_base64/filename(canonical) anddata/ext(older-desktop aliases).pdf.attach: renders each page to PNG viapdftoppm(poppler-utils) at 150 DPI and queues the pages as images; 50 MB / 25-page caps. Accepts hostpathor base64 upload._decode_attach_base64,_sniff_image_ext,_queue_attached_image) so the two new methods and the existingimage.attachdon't duplicate logic.Gateway —
hermes_cli/web_server.pyGET /api/media: returns a gateway-local image as a base64 data URL so remote clients can display it. Auth-gated like every/apiroute, extension allowlist + size cap, and confined to the gateway's own media roots (images/screenshots/cache, resolved symlink-safe) so an authed caller can't read image-extension files anywhere on disk.Desktop —
apps/desktopsyncImageAttachmentsForSubmituploads bytes viaimage.attach_byteswhen the connection mode is'remote'; the local fast path is unchanged.media.tsgainsisRemoteGateway()+gatewayMediaDataUrl();directive-textandmarkdown-textfetch images over/api/mediain remote mode.scripts/release.py— AUTHOR_MAP entries for the salvaged contributors.What's intentionally out of scope
Validation
/api/mediaas data URL/api/mediascopetui_gatewaytests + 5/api/mediaendpoint tests + desktopmedia.remoteunit tests.tui_gateway+web_serversuites green: 472 passed.tsc -bclean on the desktop app.Credit
Consolidates the work of @maxtrigify (#38876), @Justlrnal4 (#40317), @ccook1963 (#21908), and @RyTsYdUp (#39437), preserved via
Co-authored-bytrailers on the commit. Each PR will be closed pointing here.Infographic