Skip to content

feat(web): show image thumbnails in composer attachment chips#511

Open
GeT-LeFt wants to merge 4 commits intotiann:mainfrom
GeT-LeFt:feat/composer-image-thumbnails
Open

feat(web): show image thumbnails in composer attachment chips#511
GeT-LeFt wants to merge 4 commits intotiann:mainfrom
GeT-LeFt:feat/composer-image-thumbnails

Conversation

@GeT-LeFt
Copy link
Copy Markdown
Contributor

Summary

Image attachments in the composer render as a 64px thumbnail (max-width 120px) instead of a plain filename chip. The schema already has `previewUrl` and sent-message attachments already display thumbnails — this aligns the composer stage with that behavior.

  • Click the thumbnail to open a fullscreen preview via the shared `ImageLightbox`.
  • Upload state is shown as a dark overlay with a spinner on top of the thumbnail (previously the entire chip was hidden until upload finished, so the user lost sight of what they selected).
  • Filename appears as a small gradient label along the bottom edge.
  • Remove button is repositioned as a floating top-right button, visible on hover.

Non-image attachments and error state are unchanged.

Dependencies

This PR depends on #509, which introduces the shared `ImageLightbox` component. The diff shown here will include #509's changes until that one lands — rebasing onto `main` after #509 is merged will leave this PR as a single-file, ~40-line change.

Closes #510

Test plan

  • `bun run typecheck` in `web/` passes.
  • Verified non-image attachments (e.g. `.pdf`, `.txt`) and the error state still render as text chips.
  • Verified clicking a thumbnail opens the lightbox and ESC / backdrop close it.

Tool results (Read, Bash, Markdown, Generic views) currently render only
text content — any base64 image blocks returned by tools are dropped.
This means screenshots read by the Read tool, image output from bash
commands, etc. are invisible in the chat.

Changes:
- Add ImageLightbox component with Portal overlay, ESC/backdrop close,
  and open-in-new-tab action.
- Extract and render base64 image blocks in tool result content.
- Integrate ResultImages into Read/Bash/Markdown/Generic result views.
- Add click-to-preview for user-uploaded image attachments.
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review mode: initial

Findings

  • [Major] The new composer thumbnail upload state is currently unreachable. web/src/components/AssistantChat/AttachmentItem.tsx:42 treats previewUrl as the image/non-image switch, but previewUrl is only assigned after upload completion in web/src/lib/attachmentAdapter.ts:93-107, so the running overlay path never renders.
  • [Major] web/src/components/ImageLightbox.tsx:43 opens user-controlled previewUrl values with window.open(src, '_blank') and keeps window.opener attached for non-data: URLs. previewUrl is accepted as an arbitrary string in shared/src/schemas.ts:147 and passed through in web/src/chat/normalizeUser.ts:23, so this adds a reverse-tabnabbing path.

Summary

  • Review mode: initial
  • 2 issues found in the latest head diff. Residual gap: no automated coverage was added for the new composer-thumbnail path or the lightbox open-in-new-tab path.

Testing

  • Not run (automation): bun is not available in this runner (/bin/bash: bun: command not found).

HAPI Bot

const isError = status.type === 'incomplete'
const [lightboxOpen, setLightboxOpen] = useState(false)

const previewUrl = (attachment as Record<string, unknown>).previewUrl as string | undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] isImage now depends on previewUrl, but previewUrl is only added after the adapter switches the attachment to requires-action (web/src/lib/attachmentAdapter.ts:93-107). During both running yields there is still no preview URL, so this new thumbnail + spinner state never appears while the upload is in progress.

Suggested fix:

const previewUrl =
    isImageMimeType(contentType) && file.size <= MAX_PREVIEW_BYTES
        ? await fileToDataUrl(file)
        : undefined

yield {
    id,
    type: 'file',
    name: file.name,
    contentType,
    file,
    status: { type: 'running', reason: 'uploading', progress: 0 },
    previewUrl
}

Comment thread web/src/components/ImageLightbox.tsx Outdated
title="在新标签页打开"
onClick={(e) => {
e.stopPropagation()
window.open(src, '_blank')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] src is ultimately user-controlled attachment metadata (shared/src/schemas.ts:147, web/src/chat/normalizeUser.ts:23). Opening it with window.open(src, '_blank') leaves window.opener available for non-data: URLs, which lets the opened page navigate the HAPI tab.

Suggested fix:

const newWindow = window.open(src, '_blank', 'noopener,noreferrer')
if (newWindow) newWindow.opener = null

huchenxi added 3 commits April 22, 2026 14:59
The Read tool view used a ternary that swapped file content for images
when a result contained image blocks, so mixed text+image payloads
(e.g. Read on a notebook with embedded images) lost the textual output.

Render the CodeBlock unconditionally and append ResultImages when
images are present, mirroring how Bash / Markdown / Generic views
already handle the same extraction.
Image attachments in the composer currently display as plain filename
text (same chip style as non-image files), even though previewUrl is
available. This hides useful context from the user before they send
the message.

Render images as thumbnails instead:
- 64px thumbnail with object-cover, max-width 120px, click to open the
  shared ImageLightbox
- Uploading state shown as a dark overlay with a spinner on top of the
  thumbnail (previously the chip was hidden entirely until upload
  finished)
- Filename gradient label along the bottom edge
- Remove button repositioned as a floating circular button in the
  top-right, visible on hover

Non-image attachments and error state are unchanged.
- attachmentAdapter.ts: generate previewUrl for images before the initial
  'running' yield so the composer thumbnail and upload overlay are
  visible during upload, not only after completion.
- ImageLightbox.tsx: pass 'noopener,noreferrer' to window.open for the
  'open in new tab' action to prevent reverse tabnabbing via the
  user-controlled previewUrl.
@GeT-LeFt GeT-LeFt force-pushed the feat/composer-image-thumbnails branch from 3cf7d6b to 03abe29 Compare April 22, 2026 07:03
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review mode: follow-up after new commits

Findings

  • [Major] Hover-only remove action regresses the mobile composer flow — group-hover:opacity-100 on the new image-chip remove button leaves it invisible on touch-first surfaces, so selected images are easy to add but hard to undo before send. Evidence: web/src/components/AssistantChat/AttachmentItem.tsx:61.
    Suggested fix:
    <AttachmentPrimitive.Remove
        className="absolute right-1 top-1 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white/80 opacity-100 transition-opacity group-focus-within:opacity-100 md:opacity-0 md:group-hover:opacity-100 hover:bg-black/80 hover:text-white"
        aria-label="Remove attachment"
        title="Remove attachment"
    >
  • [Minor] New lightbox controls bypass the existing locale system — the added title strings are hardcoded in Chinese, so English sessions now get untranslated tooltips. Evidence: web/src/components/ImageLightbox.tsx:40, web/src/components/ImageLightbox.tsx:54.
    Suggested fix:
    import { useTranslation } from '@/lib/use-translation'
    
    const { t } = useTranslation()
    
    <button title={t('imageLightbox.openInNewTab')} aria-label={t('imageLightbox.openInNewTab')} />
    <button title={t('button.close')} aria-label={t('button.close')} />

Summary

  • Review mode: follow-up after new commits
  • 2 issues found in the latest head diff.
  • Residual risk: the new touch removal path and localized lightbox controls have no automated coverage in this diff.

Testing

  • Not run (automation): bun is not available in this runner.

HAPI Bot

</div>
)}
<AttachmentPrimitive.Remove
className="absolute -right-1 -top-1 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white/80 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-black/80 hover:text-white"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] group-hover:opacity-100 makes the new remove affordance hover-only, so on touch devices the image chip has no visible undo action before send. That is a regression for the PWA/phone flow this composer is used in.

Suggested fix:

<AttachmentPrimitive.Remove
    className="absolute right-1 top-1 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-black/60 text-white/80 opacity-100 transition-opacity group-focus-within:opacity-100 md:opacity-0 md:group-hover:opacity-100 hover:bg-black/80 hover:text-white"
    aria-label="Remove attachment"
    title="Remove attachment"
>

<div className="fixed right-4 top-4 z-[101] flex items-center gap-2">
<button
className="rounded-lg bg-white/10 p-2 text-white/80 backdrop-blur-sm transition-colors hover:bg-white/20 hover:text-white"
title="在新标签页打开"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MINOR] These new tooltip strings bypass the existing locale tables, so English sessions now show Chinese-only UI here.

Suggested fix:

import { useTranslation } from '@/lib/use-translation'

const { t } = useTranslation()

<button title={t('imageLightbox.openInNewTab')} aria-label={t('imageLightbox.openInNewTab')} />
<button title={t('button.close')} aria-label={t('button.close')} />

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Enhancement] Show image thumbnails in composer attachment chips

1 participant