Skip to content

Composer: multi-image attachments (image fusion + chat vision) - #9

Merged
1bcMax merged 3 commits into
masterfrom
multi-image-attachments
Jun 3, 2026
Merged

Composer: multi-image attachments (image fusion + chat vision)#9
1bcMax merged 3 commits into
masterfrom
multi-image-attachments

Conversation

@KillerQueen-Z

Copy link
Copy Markdown
Collaborator

Summary

Gateway image2image has accepted multi-image input for a while (image: string | string[], up to 4 for OpenAI / 3 for Google — see blockrun/src/app/api/v1/images/image2image/route.ts:28) but the web composer was hardcoded to a single slot, and clicking + again replaced the picked image instead of appending. This PR wires the composer up to the gateway's real capability.

Behavior

Mode Cap
Music 0 (button never shown)
Video 1 — first attachment used as seed, extras dropped at send time
Image with openai/* 4 (image fusion)
Image with google/* 3 (image fusion)
Image with xai/grok-imagine-image etc. 1
Chat (vision) 4

Cap is derived from maxAttachmentsFor(mode, model) — exported from the chat hook so future panels (e.g. a richer skill marketplace) can read it.

The + button:

  • Accepts multiple files in one OS pick (multiple attribute when cap > 1)
  • Appends to the existing list, never replaces
  • Hides once the cap is reached
  • Shows 2/4 counter in its title attribute

Wire / storage

  • ChatMessage gains images?: string[] for multi; the existing image?: string is kept for back-compat with legacy persisted conversations and assistant single-image outputs.
  • userImages(m) helper flattens both shapes — every reader (vision routing, chat serialization, regenerate, bubble rendering) goes through it so no caller has to branch on the shape.
  • runChatWithTools maps every userImages(m) entry to its own Anthropic image content block.
  • runMedia (image mode) sends image: trimmed — single string when 1 attachment, string[] when 2+. Matches the gateway's accepted shape on both sides of the union.

UI

  • Composer: single attachment keeps the existing .try-attach card visual; 2+ switch to a flex-wrap .try-attach-row strip with individual remove buttons.
  • Message bubble: same — single image uses the existing .try-msg-attach look, 2+ use .try-msg-attach-row for horizontal layout.
  • Zero visual regression for already-existing 1-attachment turns.

Test plan

  • Image mode + GPT Image 2 → attach 1 → + still visible → attach 2 more → counter shows 3/4 → attach 1 more → + disappears at 4/4
  • Image mode + Nano Banana Pro → cap is 3/3 (button hides at 3, not 4)
  • Image mode + Grok Imagine → cap is 1/1 (single anchor, classic behavior)
  • Submit 3-image fusion → gateway accepts (no 400) → result renders
  • Chat mode + Claude Sonnet 4.6 → attach 2 images + "describe both" → both images appear as separate blocks in the model's view (response references both)
  • Video mode → only 1 attach slot
  • Music mode → no + button
  • Existing 1-image conversation (loaded from GCS) → renders with the same .try-msg-attach card as before (back-compat)

KillerQueen-Z and others added 3 commits June 2, 2026 11:49
Gateway image2image already accepted `image: string | string[]` (cap 4
for OpenAI, 3 for Google; see blockrun/src/app/api/v1/images/image2image/
route.ts:28) but the web composer was hardcoded to a single slot —
clicking the + again replaced the picked image instead of appending.

This lets the user attach up to the per-model cap. The + button:
- accepts multiple files in one pick (multi-attribute when cap > 1)
- appends to the existing list, never replaces
- hides once the cap is reached
- shows '2/4' counter in its title attribute

Per-mode/per-model caps via maxAttachmentsFor():
  - music: 0 (button never shown)
  - video: 1 (first attachment used as seed; rest dropped at send time)
  - image: maxImageFusionFor(model) — 4 for openai/*, 3 for google/*, 1 else
  - chat:  4 (matches the highest image cap for UX symmetry)

Storage and wire:
- ChatMessage gains `images?: string[]` for multi; single `image` kept for
  back-compat with legacy persisted conversations + assistant single
  outputs. userImages() helper flattens both shapes for every reader.
- runChatWithTools maps every userImages(m) entry to its own image block
  in the Anthropic /v1/messages content array.
- runMedia(image) sends `image: trimmed` (string when 1, string[] when
  2+); video keeps single-seed behavior (first ref wins).

UI:
- Composer thumbnail strip switches from single .try-attach card to a
  .try-attach-row when there are 2+ images.
- Bubble rendering switches to .try-msg-attach-row above 1 image.
- Single-image rendering paths unchanged (zero visual regression for
  existing 1-attachment turns).
Images staged in one mode (image fusion refs / chat vision) survived a
mode switch and were sent on the next turn — leaking into chat vision or
into a music request that drops them with an empty prompt. Reset
attachments in the existing mode-change block, matching the flyout-reset
pattern from #7.
The .try-attach-row comment states single-attachment turns reuse the
.try-attach card directly, but the composer wrapped every count in the
row, whose 'margin: 0' rule stripped the single thumbnail's spacing.
Gate the row on 2+ items, matching the message-bubble render and the
PR's zero-regression promise for 1-attachment turns.
@1bcMax
1bcMax merged commit c0682bd into master Jun 3, 2026
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.

1 participant