Uh oh!
There was an error while loading. Please reload this page.
feat: built-in attachment input with picker, drag-and-drop, and paste - #31
Conversation
…aste The composer picks its own files now: onAttachmentsPicked opens the platform dialog through file_selector (flutter.dev-published, no host configuration except the macOS file-read entitlement), and hands back FlowAttachments already read, decoded, and carrying their bytes and MIME type for upload. FlowChatView.onAttachmentsDropped and FlowComposer.onAttachmentsDropped take web drag-and-drop at two scopes, with a frosted gradient invitation on the surface and a lit card on the composer; onAttachmentsPasted takes Ctrl+V images. All three decode through FlowAttachmentOptions and report refusals through onAttachmentRejected. onAttach stays as the host-picks escape hatch and dropActive as the desktop override, since drop and paste have no SDK path off the web. Closes#25
FlowImagePart renders an AI-generated picture as large-format content, unlike FlowAttachmentPart's tiles: a null image draws a shimmering block at the part's aspect ratio until the host re-renders with the provider set, and tapping the landed picture opens the full-screen preview. The sweep animation moves out of FlowShimmerText into FlowShimmerSweep so both surfaces share it. Adding a subtype to the sealed FlowMessagePart is source-breaking for exhaustive switches downstream; called out in the changelog.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…ing file A group naming only uniform type identifiers used to defer to blanket acceptance, which let any file through an accept list that also had checkable groups. UTIs are now judged through a root-type table (public.image, public.movie, com.adobe.pdf and the like), and deferral is left only for identifiers the table does not know. With allowMultiple false the intake took the first file offered, so a refused one ahead of an acceptable one consumed the slot and, on paste, suppressed the browser's own paste with nothing attached. The slot now goes to the first file that passes, with the refused ones still reported.
Uh oh!
There was an error while loading. Please reload this page.
Attachments in a user turn now sit above the bubble in a row from the trailing edge, each image a 116 square tile, cover-cropped, under an outlineVariant hairline that strengthens to outline on hover; files keep their pill tiles. A picture with no caption draws no bubble at all, and assistant turns are unchanged. FlowMessageStyle gains attachmentCardColor, attachmentCardBorderColor and attachmentCardHoverBorderColor. The playground's Message stage gains a With image variant.
Two tap targets, one inside the other: a tap on the picture is absorbed and a tap anywhere else on the frosted page closes, as the close button and Escape already did. The image is centred inside the viewer so its box is the painted picture rather than the whole viewport, which is what makes the two places distinct.
streamReply yields typed deltas, text fragments or a whole picture, and asks for image output only on the image models, since a text model refuses the request. The screen offers Gemini 3.1 Flash Image in the selector and renders the reply through FlowImagePart: a generating block from the first delta, the picture at its measured aspect ratio once the bytes land, the slot dropped if the model answers in words alone.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
# Conflicts: # CHANGELOG.md # CLAUDE.md # example/pubspec.lock # playground/macos/Runner/Release.entitlements # playground/pubspec.lock
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- public.audiovisual-content is Apple's parent for audio as well as video, so the UTI table maps each identifier to a list of MIME prefixes and that one accepts both; an audio file dropped against a group naming it was refused as unsupportedType. - A lifted sent image drew previewImage, which for the package's own attachments is bounded for the full-screen viewer, so a 116 tile decoded the picture at 2048. It now draws the plain thumbnail wrapped at tile resolution, with the same guard the attachment tiles use. - A disabled composer now refuses drops as it already refused picks, pastes and removals; before, a drag still lit the card and delivered. - FlowImagePart gains bytes and mimeType, the pair FlowAttachment carries. Without them the example dropped generated pictures from the history: a follow-up turn sent only the text, and an image-only turn was skipped entirely. - A measurement landing after the stream failed replaced the turn's parts and dropped the error card's message; it now stands down once the turn has failed. - The install docs claimed nothing outside flutter.dev and no Android permission. google_fonts is a dependency and its fetch needs INTERNET and macOS network.client on every app.
…ant on hover The rest ink now matches the pending strip's tiles, so a picture keeps the same hairline before and after it is sent.
# Conflicts: # CHANGELOG.md # lib/src/widgets/flow_composer.dart # lib/src/widgets/flow_message.dart
…ough edges showFlowAttachmentPicker is public: the function behind the composer's attach button, callable from a host's own menu. It never throws — a dialog that cannot open is reported as unreadable under an empty name — and must be called synchronously from the gesture on the web. The playground and the example route 'Add Files or Photos' through it and drop the separate paperclip; the paperclip API stays. Picked, dropped and pasted images now carry their type as kind, so the tile draws the JPG pill the design shows. The composer's pending strip scrolls its tiles under the card's inset, so an overflowing strip cuts the last tile at the edge as the cue that there is more — no counter. The preview's close button sits on an opaque surfaceBright disc with an outlineVariant hairline and the theme's shadow, and grows to 44 on touch platforms; the translucent wash it had vanished over a dark photo in the light theme and a light one in the dark. Refused files are announced by a host-side notice that fades after four seconds, in the playground and the example; the composer stage gets one too. The pinned drop treatment moves from Full Chat to a third Attachments variant, with the docs embed following.
The site loads Google Sans from Google Fonts since #29, which deleted the script that copied these out of the package but not the copies it had already made. Nothing references them; the OFL goes with them.
…design FlowComposer.errorMessage raises the design's tab above the card: the error wash with a warning glyph and the host's line, left-aligned, wrapping when long, growing in from the card's edge, and staying until its cross (onErrorDismiss) or the host clears it. errorIcon swaps the drawn glyph; FlowComposerStyle.errorBackgroundColor and errorForegroundColor recolour it. The playground and the example use it for refused files in place of a host-side notice that faded on a timer. The drop treatment takes the design's values: a vertical wash from surfaceBright at 40% to surface at 80% over a 12 blur, replacing the 72-94% surface frost; dropBlurSigma goes with it, unreleased and with nothing left to control. The attachment preview's backdrop adopts the same frost so the two read as one. Picked, dropped and pasted images no longer carry a kind pill; files without a picture keep theirs. The example seeds an image model's turn with the generating block so the shimmer shows before the bytes land, rather than the thinking indicator.
Uh oh!
There was an error while loading. Please reload this page.
Presence of a callback says a way in is wired; this flag on FlowComposer and FlowChatView says it is available — off for a plan tier, a model that takes no images, a thread that has closed. False stops the attach button, both drops, paste and keyboard media at once, leaves the handlers in place, and keeps pending attachments in the strip and removable, since they are the host's state.
Uh oh!
There was an error while loading. Please reload this page.
attachmentsEnabled false, and the composer's enabled false, passed a null onDropped into FlowDropTarget, which unregistered the web listener — the only thing calling preventDefault inside the surface. A file dropped on a chat that had merely switched attachments off then let the browser navigate the tab to it. The target now takes an enabled flag of its own: off keeps the zone registered, raises no treatment, delivers nothing, and shows the browser's refusing cursor instead of promising a copy.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 97cc379. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
…rror ink clears AA _targetAt picked the smallest rectangle under the pointer regardless of whether it was on, so a composer with attachments off shadowed a chat view that still took drops: refusing cursor, no treatment, nothing delivered. It now prefers the innermost enabled target and falls back to a disabled one only where nothing enabled contains the point — there, claiming the drop to swallow it still beats the browser navigating. The light preset's onErrorContainer is #B54141, a darker cut of the error accent: #C14A4A on its own 6% wash over surface came to 4.2:1, under WCAG AA at label sizes, so the composer's error banner and a failed user bubble were hard to read. The new ink clears 4.8:1 and still reads as the same red; the dark preset was already clear.
Puts onErrorContainer back to #C14A4A in the light preset, as the retune set it, and drops the changelog and CLAUDE.md notes that described the darker cut. The banner's 4.2:1 on its wash stands as a palette decision rather than a code fix.
Uh oh!
There was an error while loading. Please reload this page.

Summary
The composer can now get files itself instead of only displaying what the host gathered. Closes#25.
Three ways in, all handing back
FlowAttachments already read and decoded, all held to the sameFlowAttachmentOptions, all reporting refusals throughonAttachmentRejected:FlowComposer.onAttachmentsPickedrenders the attach button and opens the platform's file dialog.onAttachstays as the escape hatch when the host wants its own gallery sheet or camera; passing both asserts.FlowChatView.onAttachmentsDroppedtakes the whole surface and raises a frosted gradient invitation while a file is over it;FlowComposer.onAttachmentsDroppedscopes the same drop to the card, which lights up instead. Wiring both is fine, the innermost target under the pointer wins.FlowDropTargetexposes the detection for any other subtree, anddropActivestays writable for desktop hosts with their own detection.FlowComposer.onAttachmentsPastedtakes Ctrl+V images while the field has focus. Text paste is untouched, and the browser default is only suppressed once a file will actually be taken, so a rejected image cannot eat a text paste.Attachments the package produced carry
bytesandmimeType, so hosts can upload what was attached. The example now sends images to Gemini asinlineData, and a pending attachment arms the send button on its own since a picture with no caption is a message.The second commit adds
FlowImagePart: an AI-generated picture as large-format content, with a shimmering generating state whileimageis null and the full-screen preview on tap.New dependency, argued:
file_selectoris flutter.dev-published and endorsed on all six platforms. Unlikeimage_pickerit writes no permission, no FileProvider and no Play-services entry into a host's Android manifest and needs no iOS plist strings; its Android manifest is empty. The one cost is a macOS entitlement (com.apple.security.files.user-selected.read-only), documented in getting-started, the composer docs, and the dartdoc. Hosts that never wire an attach callback configure nothing.Breaking:
FlowImagePartjoins the sealedFlowMessagePart, so downstream exhaustive switches need a case.FlowShimmerTextbecomes aStatelessWidget(the sweep moved toFlowShimmerSweep).FlowComposer.onSendcan now fire with empty text when attachments are pending. All called out in the changelog.Screenshots
How this was verified
panaholds 160/160 with the new dependency.titleSmallEmphasisedwith a 48 glyph.dragleaveordering.Checklist
flutter analyze libandflutter analyzeinexample/andplayground/are cleandart format .applieddependencies:inpubspec.yamlis flutter.dev-published, forces no configuration on hosts that never use the feature, and is argued in this PRlib/flow_ui.dartand documented indocs/and the README tableCHANGELOG.mdupdated for user-facing changes, with breaking changes called outfeat:,fix:,refactor:,docs:,chore:)Note
Medium Risk
Large public API and breaking sealed-part/shimmer changes affect downstream apps; attachment bytes stay in memory until send, and web-only drop/paste behavior must be understood on other platforms.
Overview
End-to-end attachment intake — the package can open the platform file dialog (
showFlowAttachmentPicker,onAttachmentsPicked), accept web drops on the chat surface or composer card, and web image paste into the field, all through sharedFlowAttachmentOptions/onAttachmentRejected. Hosts still own pending state viaattachments; picked/dropped items now includebytesandmimeTypefor upload.attachmentsEnabledgates every path without unwiring callbacks;onAttachremains for custom pickers.Composer & chat UX —
errorMessageadds the design error tab above the card; send can fire with empty text when attachments are pending. User turns lift sent images above the bubble;FlowImagePartadds large-format generated images with a shimmer placeholder untilimagelands. Drop overlay is redrawn (blur + gradient); preview backdrop matches it and frosted taps dismiss.Breaking — sealed
FlowMessagePartgainsFlowImagePart(exhaustive switches need a case);FlowShimmerTextis now aStatelessWidget. Addsfile_selector(macOS file entitlement when attaching). Docs, example (Gemini inline images + image model), andFlowDropTargetfor custom drop zones.Reviewed by Cursor Bugbot for commit 691d539. Bugbot is set up for automated code reviews on this repo. Configure here.