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