Skip to content

fix(web): separate image attachments from message text - #5177

Draft
maria-rcks wants to merge 6 commits into
pingdotgg:mainfrom
maria-rcks:agent/hide-image-only-prompt
Draft

fix(web): separate image attachments from message text#5177
maria-rcks wants to merge 6 commits into
pingdotgg:mainfrom
maria-rcks:agent/hide-image-only-prompt

Conversation

@maria-rcks

@maria-rcks maria-rcks commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

What changed

  • Render normal user image attachments in a dedicated component above the text bubble.
  • Omit the text bubble entirely for attachment-only messages.
  • Hide the provider-facing image-only fallback prompt from the timeline.
  • Use familiar count-specific gallery layouts: two equal squares, one large square with two stacked squares for three images, and a balanced 2×2 square grid for larger sets.
  • Collapse sets larger than four behind a +N overlay while keeping every attachment available in the image lightbox.

Why

Images were rendered inside the same padded message box as text, and image-only turns exposed an internal provider instruction as user-authored content. Larger attachment sets also consumed unnecessary vertical space and flattened every preview into the same shallow proportion.

Impact

Images and captions now have clear visual separation, image-only turns show only their attachments, and larger sets remain compact without losing access to any image. The gallery changes with image count while keeping square crops and a balanced outer shape. This applies to the web client and the desktop app that wraps it; mobile does not use this web timeline.

Screenshots

Before After
Before: images rendered individually inside the message bubble After: balanced square image gallery above a separate text bubble

Validation

  • vp test run apps/web/src/components/chat/MessagesTimeline.test.tsx — 18 tests passed
  • vp run typecheck from apps/web
  • vp lint on the changed files
  • Visually verified the balanced gallery against the isolated PR fixture at a 1000×700 viewport

Generated with GPT-5.6 Sol via the Codex harness.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 29a4165c-3ca4-4bd3-83c6-34289287a9c0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Aug 1, 2026
@@ -0,0 +1,20 @@
export const IMAGE_ONLY_BOOTSTRAP_PROMPT =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium lib/userMessage.ts:1

resolveUserMessageDisplayText hides a user's message by returning "" whenever the trimmed text exactly equals IMAGE_ONLY_BOOTSTRAP_PROMPT (or its Ultrathink: variant). A user who intentionally types this sentence alongside an image attachment will have their message silently replaced with an empty display string, losing their genuine caption. This happens because the function uses a user-readable prompt string as the sole sentinel to distinguish generated fallback text from user-authored text. Consider using a collision-resistant internal marker or persisted metadata instead, so genuine user input is never mistaken for a synthetic bootstrap prompt.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/lib/userMessage.ts around line 1:

`resolveUserMessageDisplayText` hides a user's message by returning `""` whenever the trimmed text exactly equals `IMAGE_ONLY_BOOTSTRAP_PROMPT` (or its `Ultrathink:` variant). A user who intentionally types this sentence alongside an image attachment will have their message silently replaced with an empty display string, losing their genuine caption. This happens because the function uses a user-readable prompt string as the sole sentinel to distinguish generated fallback text from user-authored text. Consider using a collision-resistant internal marker or persisted metadata instead, so genuine user input is never mistaken for a synthetic bootstrap prompt.

@maria-rcks maria-rcks closed this Aug 1, 2026
@maria-rcks maria-rcks changed the title fix(web): hide image-only fallback prompt fix(web): separate image attachments from message text Aug 1, 2026
@maria-rcks maria-rcks reopened this Aug 1, 2026
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Aug 1, 2026
data-user-message-attachments="true"
data-user-message-attachments-collapsed={collapsedImageCount > 0 ? "true" : "false"}
>
{visibleImages.map((image, index) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High chat/UserMessageAttachments.tsx:38

With more than four attachments, if the fourth item has no previewUrl, the +N collapsed-tile overlay and click target are not rendered — they exist only inside the image.previewUrl branch. Previewable attachments after index 3 are hidden by slice(0, MAX_VISIBLE_IMAGES), so with four non-previewable files followed by a previewable image, the image becomes completely inaccessible. Render the collapsed overlay and expand action independently of whether the fourth item itself has a preview URL (for example, by selecting a previewable image for that tile).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/UserMessageAttachments.tsx around line 38:

With more than four attachments, if the fourth item has no `previewUrl`, the `+N` collapsed-tile overlay and click target are not rendered — they exist only inside the `image.previewUrl` branch. Previewable attachments after index 3 are hidden by `slice(0, MAX_VISIBLE_IMAGES)`, so with four non-previewable files followed by a previewable image, the image becomes completely inaccessible. Render the collapsed overlay and expand action independently of whether the fourth item itself has a preview URL (for example, by selecting a previewable image for that tile).

: `Preview ${image.name}`
}
onClick={() => {
const preview = buildExpandedImagePreview(props.images, image.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High chat/UserMessageAttachments.tsx:58

Overflow attachments without a previewUrl are permanently inaccessible. When there are more than four images, the last visible tile shows +N, but buildExpandedImagePreview filters out any attachment lacking a previewUrl. If an overflowed attachment has no preview URL, it never appears in the expanded gallery, so its fallback name is never shown and there is no way to see it. Consider passing the full props.images array (including non-previewable items) to the expanded preview, or surfacing non-previewable overflow attachments by name outside the image gallery.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/chat/UserMessageAttachments.tsx around line 58:

Overflow attachments without a `previewUrl` are permanently inaccessible. When there are more than four images, the last visible tile shows `+N`, but `buildExpandedImagePreview` filters out any attachment lacking a `previewUrl`. If an overflowed attachment has no preview URL, it never appears in the expanded gallery, so its fallback name is never shown and there is no way to see it. Consider passing the full `props.images` array (including non-previewable items) to the expanded preview, or surfacing non-previewable overflow attachments by name outside the image gallery.

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

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant