@@ -21,9 +21,10 @@ import {
2121 Stop ,
2222} from "phosphor-react-native" ;
2323import { useFeatureFlag } from "posthog-react-native" ;
24- import { useCallback , useEffect , useMemo , useState } from "react" ;
24+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2525import {
2626 ActivityIndicator ,
27+ Alert ,
2728 Keyboard ,
2829 Pressable ,
2930 ScrollView ,
@@ -37,7 +38,11 @@ import { useThemeColors } from "@/lib/theme";
3738import type { MessagingMode } from "../stores/messagingModeStore" ;
3839import { AgentConfigControls } from "./AgentConfigControls" ;
3940import { AttachmentSheet } from "./attachments/AttachmentSheet" ;
40- import { AttachmentsBar } from "./attachments/AttachmentsBar" ;
41+ import {
42+ type AttachmentStatus ,
43+ AttachmentsBar ,
44+ } from "./attachments/AttachmentsBar" ;
45+ import { attachmentPreparer } from "./attachments/buildCloudPrompt" ;
4146import {
4247 captureFromCamera ,
4348 pickDocument ,
@@ -122,6 +127,9 @@ export function TaskChatComposer({
122127 const modelConfigOption = getModelConfigOption ( configOptions ) ;
123128 const [ message , setMessage ] = useState ( ( ) => initialMessage ?? "" ) ;
124129 const [ attachments , setAttachments ] = useState < PendingAttachment [ ] > ( [ ] ) ;
130+ const [ attachmentStatus , setAttachmentStatus ] = useState <
131+ Record < string , AttachmentStatus >
132+ > ( { } ) ;
125133 const [ attachmentSheetOpen , setAttachmentSheetOpen ] = useState ( false ) ;
126134
127135 // Mirror composer state into refs so a failed send can read the current
@@ -132,6 +140,48 @@ export function TaskChatComposer({
132140 attachmentsRef . current = attachments ;
133141 const submissionRef = useRef ( 0 ) ;
134142
143+ const clearStatus = useCallback ( ( id : string ) => {
144+ setAttachmentStatus ( ( prev ) => {
145+ if ( ! ( id in prev ) ) return prev ;
146+ const { [ id ] : _dropped , ...rest } = prev ;
147+ return rest ;
148+ } ) ;
149+ } , [ ] ) ;
150+
151+ // Encode eagerly so oversized/unsupported files fail at attach, not send.
152+ const beginPreparing = useCallback (
153+ ( att : PendingAttachment ) => {
154+ setAttachmentStatus ( ( prev ) => ( { ...prev , [ att . id ] : "preparing" } ) ) ;
155+ attachmentPreparer . prepare ( att ) . then (
156+ ( ) => {
157+ if ( attachmentsRef . current . some ( ( a ) => a . id === att . id ) ) {
158+ clearStatus ( att . id ) ;
159+ }
160+ } ,
161+ ( error : unknown ) => {
162+ if ( ! attachmentsRef . current . some ( ( a ) => a . id === att . id ) ) return ;
163+ setAttachmentStatus ( ( prev ) => ( { ...prev , [ att . id ] : "error" } ) ) ;
164+ Alert . alert (
165+ "Attachment can't be sent" ,
166+ error instanceof Error
167+ ? error . message
168+ : "This file couldn't be prepared. Remove it and try another." ,
169+ ) ;
170+ } ,
171+ ) ;
172+ } ,
173+ [ clearStatus ] ,
174+ ) ;
175+
176+ const loadAttachments = useCallback (
177+ ( next : PendingAttachment [ ] ) => {
178+ setAttachments ( next ) ;
179+ setAttachmentStatus ( { } ) ;
180+ for ( const att of next ) beginPreparing ( att ) ;
181+ } ,
182+ [ beginPreparing ] ,
183+ ) ;
184+
135185 useEffect ( ( ) => {
136186 if ( ! initialMessage ) return ;
137187 setMessage ( initialMessage ) ;
@@ -140,8 +190,17 @@ export function TaskChatComposer({
140190 useEffect ( ( ) => {
141191 if ( ! restoredDraft ) return ;
142192 setMessage ( restoredDraft . text ) ;
143- setAttachments ( restoredDraft . attachments ) ;
144- } , [ restoredDraft ] ) ;
193+ loadAttachments ( restoredDraft . attachments ) ;
194+ } , [ restoredDraft , loadAttachments ] ) ;
195+
196+ useEffect (
197+ ( ) => ( ) => {
198+ for ( const att of attachmentsRef . current ) {
199+ attachmentPreparer . forget ( att . id ) ;
200+ }
201+ } ,
202+ [ ] ,
203+ ) ;
145204
146205 useEffect ( ( ) => {
147206 if ( ! hasLiveConfig ) return ;
@@ -174,9 +233,12 @@ export function TaskChatComposer({
174233 const isTranscribing = status === "transcribing" ;
175234
176235 const hasContent = ! isComposerEmpty ( { text : message , attachments } ) ;
236+ const statuses = Object . values ( attachmentStatus ) ;
237+ const attachmentsPreparing = statuses . includes ( "preparing" ) ;
238+ const sendBlocked = attachmentsPreparing || statuses . includes ( "error" ) ;
177239 const primaryAction = resolveComposerPrimaryAction ( {
178240 hasContent,
179- disabled,
241+ disabled : disabled || sendBlocked ,
180242 isRecording,
181243 isTranscribing,
182244 canStop : ! isUserTurn && ! ! onStop ,
@@ -187,7 +249,7 @@ export function TaskChatComposer({
187249
188250 const applyContent = ( content : ComposerContent ) => {
189251 setMessage ( content . text ) ;
190- setAttachments ( content . attachments ) ;
252+ loadAttachments ( content . attachments ) ;
191253 } ;
192254
193255 const handleSend = ( ) => {
@@ -214,14 +276,19 @@ export function TaskChatComposer({
214276 ) => {
215277 try {
216278 const att = await picker ( ) ;
217- if ( att ) setAttachments ( ( prev ) => [ ...prev , att ] ) ;
279+ if ( att ) {
280+ setAttachments ( ( prev ) => [ ...prev , att ] ) ;
281+ beginPreparing ( att ) ;
282+ }
218283 } catch ( err ) {
219284 log . error ( "Failed to pick attachment" , err ) ;
220285 }
221286 } ;
222287
223288 const removeAttachment = ( id : string ) => {
224289 setAttachments ( ( prev ) => prev . filter ( ( a ) => a . id !== id ) ) ;
290+ attachmentPreparer . forget ( id ) ;
291+ clearStatus ( id ) ;
225292 } ;
226293
227294 const handleMicPress = async ( ) => {
@@ -282,6 +349,7 @@ export function TaskChatComposer({
282349 < AttachmentsBar
283350 attachments = { attachments }
284351 onRemove = { removeAttachment }
352+ statuses = { attachmentStatus }
285353 />
286354 < TextInput
287355 className = "px-4 pt-3.5 pb-3 text-[15px] text-gray-12"
@@ -368,20 +436,22 @@ export function TaskChatComposer({
368436 canSend ? handleSend : showStop ? handleStop : handleMicPress
369437 }
370438 onLongPress = { handleMicLongPress }
371- disabled = { isTranscribing || disabled }
439+ disabled = { isTranscribing || disabled || sendBlocked }
372440 className = { `h-9 w-9 items-center justify-center rounded-lg ${
373441 canSend ? "bg-gray-12" : "bg-gray-3"
374442 } `}
375443 >
376- { isTranscribing ? (
444+ { isTranscribing || attachmentsPreparing ? (
377445 < ActivityIndicator
378446 size = "small"
379447 color = { themeColors . gray [ 12 ] }
380448 />
381- ) : canSend ? (
449+ ) : canSend || sendBlocked ? (
382450 < ArrowUp
383451 size = { 18 }
384- color = { themeColors . background }
452+ color = {
453+ canSend ? themeColors . background : themeColors . gray [ 9 ]
454+ }
385455 weight = "bold"
386456 />
387457 ) : isRecording || showStop ? (
0 commit comments