Skip to content

feat: built-in attachment input with picker, drag-and-drop, and paste - #31

Merged
divyanshub024 merged 17 commits into
mainfrom
dv/attachment-input
Aug 29, 2026
Merged

feat: built-in attachment input with picker, drag-and-drop, and paste#31
divyanshub024 merged 17 commits into
mainfrom
dv/attachment-input

Conversation

@divyanshub024

@divyanshub024divyanshub024 commented Aug 26, 2026

Copy link
Copy Markdown
Member

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 same FlowAttachmentOptions, all reporting refusals through onAttachmentRejected:

  • Pick: FlowComposer.onAttachmentsPicked renders the attach button and opens the platform's file dialog. onAttach stays as the escape hatch when the host wants its own gallery sheet or camera; passing both asserts.
  • Drop (web): FlowChatView.onAttachmentsDropped takes the whole surface and raises a frosted gradient invitation while a file is over it; FlowComposer.onAttachmentsDropped scopes the same drop to the card, which lights up instead. Wiring both is fine, the innermost target under the pointer wins. FlowDropTarget exposes the detection for any other subtree, and dropActive stays writable for desktop hosts with their own detection.
  • Paste (web): FlowComposer.onAttachmentsPasted takes 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 bytes and mimeType, so hosts can upload what was attached. The example now sends images to Gemini as inlineData, 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 while image is null and the full-screen preview on tap.

New dependency, argued: file_selector is flutter.dev-published and endorsed on all six platforms. Unlike image_picker it 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: FlowImagePart joins the sealed FlowMessagePart, so downstream exhaustive switches need a case. FlowShimmerText becomes a StatelessWidget (the sweep moved to FlowShimmerSweep). FlowComposer.onSend can now fire with empty text when attachments are pending. All called out in the changelog.

Screenshots

BeforeAfter

How this was verified

  • Full Chat (surface drop, paste, picker, drop variant pinned for the docs embed), Composer (card-scoped drop, picker, paste), Streaming Message (image variant), Attachments stages, on Chrome in both themes.
  • Example app on macOS debug and release (the Release entitlement is the one everyone forgets; both are set), sending picked and dropped images to Gemini for real.
  • Playground compiles under dart2js and dart2wasm; docs site builds; pana holds 160/160 with the new dependency.
  • The drop treatment was rendered in both themes at 2x and inspected against the Figma frames; label sits on titleSmallEmphasised with a 48 glyph.
  • Not verifiable here: Windows/Linux dialogs, Android IME rich content (needs real Gboard), Safari/Firefox dragleave ordering.

Checklist

  • flutter analyze lib and flutter analyze in example/ and playground/ are clean
  • dart format . applied
  • Exercised in the playground — with a stage demo added or updated if this is a new component or variant
  • Any new entry under dependencies: in pubspec.yaml is flutter.dev-published, forces no configuration on hosts that never use the feature, and is argued in this PR
  • Nothing model-facing — no prompts, schemas, or provider/network calls
  • New public API is exported from lib/flow_ui.dart and documented in docs/ and the README table
  • CHANGELOG.md updated for user-facing changes, with breaking changes called out
  • PR title follows conventional commits (feat:, 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 shared FlowAttachmentOptions / onAttachmentRejected. Hosts still own pending state via attachments; picked/dropped items now include bytes and mimeType for upload. attachmentsEnabled gates every path without unwiring callbacks; onAttach remains for custom pickers.

Composer & chat UXerrorMessage adds 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; FlowImagePart adds large-format generated images with a shimmer placeholder until image lands. Drop overlay is redrawn (blur + gradient); preview backdrop matches it and frosted taps dismiss.

Breaking — sealed FlowMessagePart gains FlowImagePart (exhaustive switches need a case); FlowShimmerText is now a StatelessWidget. Adds file_selector (macOS file entitlement when attaching). Docs, example (Gemini inline images + image model), and FlowDropTarget for custom drop zones.

Reviewed by Cursor Bugbot for commit 691d539. Bugbot is set up for automated code reviews on this repo. Configure here.

…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.
Comment threadlib/src/utils/flow_attachment_intake.dart Outdated
Comment threadlib/src/widgets/flow_composer.dart
…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.
Comment threadlib/src/utils/flow_attachment_intake.dart Outdated
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.
Comment threadlib/src/widgets/flow_message.dart Outdated
Comment threadexample/lib/gemini_api.dart
Comment threadexample/lib/main.dart
# Conflicts:
#	CHANGELOG.md
#	CLAUDE.md
#	example/pubspec.lock
#	playground/macos/Runner/Release.entitlements
#	playground/pubspec.lock
Comment threaddocs/src/content/docs/getting-started.mdx Outdated
Comment threadlib/src/widgets/flow_composer.dart
- 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.
rahulbisht25
rahulbisht25 previously approved these changes Aug 28, 2026
…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.
Comment threadlib/src/widgets/flow_composer.dart
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.
Comment threadlib/src/widgets/flow_chat_view.dart Outdated
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.

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment threadlib/src/utils/flow_file_drop_web.dart
…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.
@divyanshub024
divyanshub024 merged commit 73929cf into mainAug 29, 2026
5 checks passed
@divyanshub024
divyanshub024 deleted the dv/attachment-input branch August 29, 2026 17:38
Sign up for freeto 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.

feat: attachment input, the attach affordance and drop target

2 participants

@divyanshub024@rahulbisht25