From d8cb73b4ab2b3c766185fd654100175cffd7bfb3 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 22:00:31 -0500 Subject: [PATCH] fix(desktop): open a composer image attachment in the lightbox, not the rail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking an attached image routed it through normalizeOrLocalPreviewTarget and into the right rail, where it rendered as a small contained inside a pane built for reading and editing files. The bytes were already in hand as a data URL, so the rail's whole read/edit/reload apparatus was doing nothing for a picture the user just wanted to look at. Route images with a resolved thumbnail to ImageLightbox — the same overlay the thread uses — so the attachment previews at full size with the download action, and drop the dataUrl/previewKind graft that only existed to make the rail render bytes it shouldn't have been handed. Non-image attachments, and images whose thumbnail never resolved, still fall through to the rail unchanged. --- .../src/app/chat/composer/attachments.tsx | 163 ++++++++++-------- 1 file changed, 92 insertions(+), 71 deletions(-) diff --git a/apps/desktop/src/app/chat/composer/attachments.tsx b/apps/desktop/src/app/chat/composer/attachments.tsx index f50f74be6683..cb42dc9f1c99 100644 --- a/apps/desktop/src/app/chat/composer/attachments.tsx +++ b/apps/desktop/src/app/chat/composer/attachments.tsx @@ -1,8 +1,11 @@ import { useStore } from '@nanostores/react' +import { useState } from 'react' import { useSessionView } from '@/app/chat/session-view' +import { ImageLightbox } from '@/components/chat/zoomable-image' import { Codicon } from '@/components/ui/codicon' import { Tip } from '@/components/ui/tooltip' +import { useImageDownload } from '@/hooks/use-image-download' import { useI18n } from '@/i18n' import { AlertCircle, FileText, FolderOpen, ImageIcon, Link, Loader2, Terminal } from '@/lib/icons' import { normalizeOrLocalPreviewTarget } from '@/lib/local-preview' @@ -38,12 +41,25 @@ function AttachmentPill({ attachment, onRemove }: { attachment: ComposerAttachme const hasUploadError = attachment.uploadState === 'error' const canPreview = attachment.kind !== 'folder' && attachment.kind !== 'terminal' && !isUploading const detail = attachment.detail && attachment.detail !== attachment.label ? attachment.detail : undefined + // An attached image already holds its full bytes as a data URL, so it belongs + // in the same lightbox the thread uses. The rail is for files you read or + // edit — not a picture you just want to look at. Images that never resolved a + // thumbnail still fall through to the rail rather than dead-clicking. + const lightboxSrc = attachment.kind === 'image' && !isUploading ? attachment.previewUrl : undefined + const [lightboxOpen, setLightboxOpen] = useState(false) + const { download, saving } = useImageDownload(lightboxSrc) - async function openAttachmentPreview() { + async function openAttachment() { if (!canPreview) { return } + if (lightboxSrc) { + setLightboxOpen(true) + + return + } + const rawTarget = attachment.path || attachment.detail || @@ -64,85 +80,90 @@ function AttachmentPill({ attachment, onRemove }: { attachment: ComposerAttachme throw new Error(c.couldNotPreview(attachment.label)) } - // We already hold the image bytes (the card thumbnail) — render those - // directly so a screenshot/clipboard image previews even when its only - // on-disk copy is a transient path the renderer can't re-read. - const withBytes = - attachment.kind === 'image' && attachment.previewUrl - ? { ...preview, dataUrl: attachment.previewUrl, previewKind: 'image' as const } - : preview - - openPreview(withBytes, 'manual') + openPreview(preview, 'manual') } catch (error) { notifyError(error, c.previewUnavailable) } } return ( - -
- - {onRemove && ( + <> + +
- )} -
-
+ {onRemove && ( + + )} +
+
+ {lightboxSrc && ( + + )} + ) }