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 && ( + + )} + ) }