feat(mobile): preview HEIC images and share session attachments - #5426
Merged
Conversation
Start the upload for every image addition in addCandidates so the chip reaches uploaded before send. Disable the new-session Start button while an image uploads on the remote target branch too.
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (5 files)
Previous Review Summaries (6 snapshots, latest commit cabbb9d)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit cabbb9d)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit 5ace8a8)Status: No Issues Found | Recommendation: Merge Files Reviewed (4 files)
Previous review (commit 6ff5262)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (7 files)
Fix these issues in Kilo Cloud Previous review (commit 17e3cfe)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit 3a3e7d8)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (17 files)
Fix these issues in Kilo Cloud Previous review (commit b491040)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (17 files)
Reviewed by grok-4.6 · Input: 53.1K · Output: 7.2K · Cached: 318.2K Review guidance: REVIEW.md from base branch |
pandemicsyn
approved these changes
Aug 21, 2026
# Conflicts: # apps/mobile/src/components/agents/file-part-renderer.mounted.test.tsx # apps/mobile/src/components/agents/file-part-renderer.tsx
The share handler read viewerVisible and preview from its render closure after the share await, so a failure that landed once the viewer or sheet had closed set state on an unmounted surface and was silently dropped. Track the current visibility in refs and read those in the catch branch so the failure surfaces as a toast instead.
Disable Send while an image uploads, matching the new-session start gate. Render the image viewer decode fallback in white on the black canvas for WCAG contrast. Move the file preview share error out of the scroll view so it stays visible after scroll.
Render both inline share errors through AccessibleStatus so VoiceOver and TalkBack announce the failure, keeping the current visual styling.
Add accessibilityState busy to the SheetHeader Share pressable so VoiceOver announces a share in progress instead of a dimmed button, matching the image viewer.
Decouple the Stop-vs-Send decision from the upload state. The row now chooses Stop only when there is no sendable content, so an in-flight upload no longer swaps a streaming session's Send button for Stop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
HEIC and HEIF photos now attach and count as images. A camera photo with no file name receives an automatic name.
Photos upload as soon as you add them to a message. You see the upload progress and the finished state before sending; a temporary failure shows a retry, and a rejected file shows a permanent error.
The Send button stays disabled while a photo upload is in progress. While the agent is generating, the composer shows Stop when there is nothing to send and Send when there is text or an attachment.
When a photo cannot be displayed, the attachment preview and the full-screen viewer show a placeholder icon instead of an empty area.
The full-screen photo viewer and the text preview sheet now include a Share button. A failed share shows its message inside the open screen.
Image chips now begin uploading the moment they are added, instead of waiting for send. The upload hook starts one upload per image candidate after the chip list commits and leaves document chips deferred to the send path; the new-session screen adds the upload-in-progress flag to its remote-start gate. A remote spawn therefore cannot start while an image upload is pending, and the send path uploads only the remaining document chips.
Files
apps/mobile/src/lib/agent-attachments/use-agent-attachment-upload.ts— starts an upload for each image candidate after the chip list commits; documents stay deferred touploadPending.apps/mobile/src/app/(app)/agent-chat/new.tsx— addsattachments.isUploadingto the remote-spawn start gate.Send is disabled while an attachment upload is in flight, and the Stop-versus-Send choice no longer depends on the upload state. The composer control projection gains an
isUploadinginput and ahasSendableContentoutput, andcanSendnow also requires that no upload is pending; the input row picks Stop versus Send from streaming plus sendable content instead of fromcanSend. During streaming, a composer with no text and no sendable attachment shows Stop, and one with text or a sendable attachment shows Send.Files
apps/mobile/src/components/agents/chat-composer-input-state.ts— adds theisUploadinginput and thehasSendableContentoutput;canSendnow also requires!isUploading.apps/mobile/src/components/agents/chat-composer-input-row.tsx— adds thehasSendableContentprop and switches the Stop/Send branch from!canSendto!hasSendableContent.apps/mobile/src/components/agents/chat-composer.tsx— feedsupload.isUploadinginto the control state andhasSendableContentinto the input row.HEIC and HEIF files now classify as images. The extension-to-MIME table and the image-extension set both gain
heicandheif, so these assets take the existing metadata-strip and JPEG re-encode path. The picker keeps a non-empty picker filename and, when the picker omits the name, synthesizes one from the URI extension, then the MIME subtype, thenimage.png.Files
apps/mobile/src/lib/agent-attachments/constants.ts— addsheicandheifto the extension-to-MIME table.apps/mobile/src/lib/agent-attachments/validate.ts— addsheicandheifto the image-extension set.apps/mobile/src/components/agents/attachment-picker.ts— keeps the picker filename when non-empty; otherwise synthesizesimage.<ext>from the URI extension, then the MIME subtype, thenimage.png.Session-page image and text previews now expose a Share action. The renderer passes its share handler into the image viewer and the preview sheet, and the sheet header gains an optional
onShareprop with asharingbusy state, so both surfaces show a Share button. A share failure renders inline inside the open viewer or sheet, outside the scroll view, and as a toast only when neither is open.Files
apps/mobile/src/components/agents/file-part-renderer.tsx— passes the share handler, busy state, and error message into the viewer and preview sheet; renders the error inline when a modal is open and as a toast otherwise.apps/mobile/src/components/sheet-header.tsx— adds an optional share button to the header with a busy accessibility state.Image chips and the full-screen viewer now show a centered fallback icon when an image fails to decode, and the viewer announces its share error through the accessibility status component. Each surface tracks a decode-failure state and renders an
AlertCircleplaceholder instead of a blank frame; the viewer uses a white icon on the black backdrop and resets the state on reopen or on a new image.Files
apps/mobile/src/components/agents/attachment-preview-strip.tsx— tracks a thumbnail decode failure and renders a centeredAlertCirclefallback.apps/mobile/src/components/image-viewer-modal.tsx— tracks a viewer decode failure, renders a whiteAlertCirclefallback on the black backdrop, resets it on reopen or new image, and renders the share error throughAccessibleStatusinstead of a plain text.Tests: 10 files changed —
attachment-picker.test.ts,attachment-preview-strip.mounted.test.tsx,chat-composer-input-row.mounted.test.tsx,chat-composer-input-state.test.ts,file-part-renderer.mounted.test.tsx,image-viewer-modal.mounted.test.tsx(new),sheet-header.mounted.test.tsx(new),strip-image-metadata.test.ts,use-agent-attachment-upload.test.ts,validate.test.ts.Generated: none.
Verification
Verification ran two cases on iOS.
Visual Changes
Session page attachment previews (image and Markdown entry points). The user now sees the attached image rendered as a tappable preview and the attached Markdown file as a chip inside the session transcript. In the picture, the red sample.png preview and the notes.md chip sit in the first message bubble, above the read-only session banner. Disagreement with the handoff: the handoff labels this still "image viewer with share action", but the picture shows the transcript after the viewer closed; the viewer's Share button and the native share sheet are not in the frame (the S1 case log proves them).
Markdown preview sheet. No picture shows this screen. The preview sheet with its Done and Share notes.md actions never opened in any round, so no still shows the change. The only related still,
s2-r2-blocked.png, shows the R2 attachment-download blocker ("could not be retrieved"), not the preview sheet.Reviewer Notes
No human steps are needed before or after merge.
E2E skip: the Markdown-preview-share case could not run — the seed produced an unresolvable attachment URL, then the sandbox R2 attachment download failed on a misconfigured R2 endpoint. Test-environment limitation, not a product defect; the changed unit tests cover it.