feat(chat): right-click Copy image on transcript pictures - #482
feat(chat): right-click Copy image on transcript pictures#482Adam-Dalloul wants to merge 3 commits into
Conversation
4519a30 to
320c672
Compare
|
Thanks for this — the premise is right, and I like that you modelled it on I ran the gates on your branch: Two things I'd like fixed before merge, plus a few smaller ones. 1. WebKit will reject the JPEG/WebP/GIF path (macOS desktop)
The PNG path is fine as written (nothing suspends before the write), but this fix makes both paths safe and is what WebKit documents: const png =
sourceType === "image/png"
? Promise.resolve(new Blob([bytes as BlobPart], { type: "image/png" }))
: rasterToPngBlob(bytes, sourceType)
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })])
2. In the server / Docker web mode, Copy image is always offered and always failsServed over plain HTTP on a LAN (not loopback — So Better still for that case: render the children with a bare Also worth fixing before merge
Non-blocking notes
Happy to take another look once the clipboard write ordering and the capability gate are in. |
… works The rasterized path awaited an image decode and a canvas encode before it called `clipboard.write()`. WebKit only honours a write issued inside the user gesture, so on the desktop app — which is WKWebView — copying a JPEG, webp or gif spent the transient activation on the decode and then failed with NotAllowedError. `ClipboardItem` takes a pending Blob, so the write now goes out synchronously and the browser awaits the raster itself. Two copies in a row also land in the order they were asked for now, rather than the order their rasters happened to finish in. `write()` reports a rejected representation as an error of its own, so the real reason is captured on the way past and rethrown in its place. That observer is attached before the write rather than chained into it: the promise then always has a handler, so a write that fails first for an unrelated reason can't leave the raster rejection unhandled. Served over plain HTTP on a LAN, neither `ClipboardItem` nor `clipboard.write` exists — that is why `installClipboardFallback` is there, and it only backfills `writeText`. The row was offered anyway and could only ever end in an error toast. `canCopyImageToClipboard` was already exported for this and unused; it now decides whether the menu is built at all. Where it isn't, the trigger still shields the transcript menu from the event but stops short of preventing the default, so the browser's own image menu takes over — and its Copy image has no secure-context requirement. The nine non-English locales carried the English strings. They are translated, and the typed error's developer text no longer reaches a toast: it maps to a message of its own instead of being interpolated raw. The rest is what the extra wrapper cost. `ImageActions` takes a className and puts it on the trigger, so the styled box is the flex item its parent laid out again — with its shrink-0, and without a stray line box under the image. The copy/download pair moved into `useImageActions`, so the menu, the hover button and the preview dialog report success and failure the same way instead of drifting across three copies. Right-clicking the blown-up preview opens that same menu rather than silently copying, through a render prop that keeps ui/ free of message-specific imports. Also: the raster released its object URL on every exit but one. Assistant markdown images are still Streamdown's own; copying those needs a URL fetch path and is left alone here.
|
Pushed the fixes onto this branch ( The two blockers
The rest
Tests — Green on One thing left alone deliberately: assistant markdown images are Streamdown's own component, and copying those means a URL fetch with CORS to think about — a separate change rather than something to bolt on here. |
Right-clicking a picture in a sent (or generated) message only opened the conversation menu (Copy text / export / …). There was Download on hover, but no Copy image.
This adds a nested image menu, same bubbling contract as file-badge actions:
ClipboardItem(JPEG/webp/gif are converted so Chrome/Tauri accept the write)The conversation menu still appears when you right-click anywhere else in the transcript.
No secrets. One concern.