@@ -32,6 +32,10 @@ import {
3232} from "@/features/tasks/composer/options" ;
3333import { TaskChatComposer } from "@/features/tasks/composer/TaskChatComposer" ;
3434import { taskKeys } from "@/features/tasks/hooks/useTasks" ;
35+ import {
36+ pendingTaskPromptStoreApi ,
37+ usePendingTaskPrompt ,
38+ } from "@/features/tasks/stores/pendingTaskPromptStore" ;
3539import { useTaskSessionStore } from "@/features/tasks/stores/taskSessionStore" ;
3640import { useTaskStore } from "@/features/tasks/stores/taskStore" ;
3741import type { Task } from "@/features/tasks/types" ;
@@ -92,6 +96,30 @@ export default function TaskDetailScreen() {
9296
9397 const session = taskId ? getSessionForTask ( taskId ) : undefined ;
9498
99+ // Optimistic echo set by the new-task screen (or the terminal-resume path
100+ // below) so the user's prompt appears in the thread immediately, before
101+ // the live session catches up.
102+ const optimisticPrompt = usePendingTaskPrompt ( taskId ) ;
103+
104+ // Clear the echo once the canonical user_message_chunk with matching text
105+ // arrives via SSE — `TaskSessionView` also dedups visually, but clearing
106+ // the store frees it for the next submit. Only events with `ts >= setAt`
107+ // qualify so a text-identical historical turn (e.g. resubmitting
108+ // "Continue") doesn't drop the echo before the real copy lands.
109+ useEffect ( ( ) => {
110+ if ( ! taskId || ! optimisticPrompt ) return ;
111+ const matched = session ?. events . some (
112+ ( e ) =>
113+ e . type === "session_update" &&
114+ e . notification ?. update ?. sessionUpdate === "user_message_chunk" &&
115+ e . notification . update . content ?. text === optimisticPrompt . promptText &&
116+ ( e . ts ?? 0 ) >= optimisticPrompt . setAt ,
117+ ) ;
118+ if ( matched ) {
119+ pendingTaskPromptStoreApi . clear ( taskId ) ;
120+ }
121+ } , [ taskId , optimisticPrompt , session ?. events ] ) ;
122+
95123 // Per-task composer pill values. Persisted in taskStore so reopening the
96124 // task keeps the user's choices; defaults fall back to the same constants
97125 // the new-task composer uses.
@@ -217,6 +245,19 @@ export default function TaskDetailScreen() {
217245 const handleSendAfterTerminal = useCallback (
218246 async ( text : string , attachments : PendingAttachment [ ] ) => {
219247 if ( ! taskId || ! task ) return ;
248+ // Optimistically echo into the chat before tearing down the old session
249+ // and waiting for the resume run's SSE stream to come up.
250+ const echoAttachments = attachments . map ( ( a ) => ( {
251+ kind : a . kind ,
252+ uri : a . uri ,
253+ fileName : a . fileName ,
254+ mimeType : a . mimeType ,
255+ } ) ) ;
256+ pendingTaskPromptStoreApi . set ( taskId , {
257+ promptText : text ,
258+ attachments : echoAttachments . length > 0 ? echoAttachments : undefined ,
259+ setAt : Date . now ( ) ,
260+ } ) ;
220261 try {
221262 setRetrying ( true ) ;
222263 disconnectFromTask ( taskId ) ;
@@ -242,6 +283,7 @@ export default function TaskDetailScreen() {
242283 updateTaskInCache ( updatedTask ) ;
243284 } catch ( err ) {
244285 log . error ( "Failed to send after terminal" , err ) ;
286+ pendingTaskPromptStoreApi . clear ( taskId ) ;
245287 setRetrying ( false ) ;
246288 Alert . alert (
247289 "Failed to send" ,
@@ -390,7 +432,10 @@ export default function TaskDetailScreen() {
390432 ! ! session &&
391433 session . status === "connecting" &&
392434 session . events . length === 0 ;
393- const showLoading = loading || isHistoryLoading ;
435+ // Suppress the full-screen overlay when we have an optimistic prompt to
436+ // show — the user just submitted and seeing their own text + a connecting
437+ // indicator is friendlier than a blank spinner.
438+ const showLoading = ( loading || isHistoryLoading ) && ! optimisticPrompt ;
394439 const showAutomationContext =
395440 fromAutomation === "1" || task ?. origin_product === "automation" ;
396441 const automationContextLabel =
@@ -473,6 +518,15 @@ export default function TaskDetailScreen() {
473518 }
474519 onOpenTask = { handleOpenTask }
475520 onSendPermissionResponse = { handleSendPermissionResponse }
521+ optimisticUserMessage = {
522+ optimisticPrompt
523+ ? {
524+ text : optimisticPrompt . promptText ,
525+ attachments : optimisticPrompt . attachments ,
526+ setAt : optimisticPrompt . setAt ,
527+ }
528+ : undefined
529+ }
476530 contentContainerStyle = { {
477531 paddingTop : 8 ,
478532 paddingBottom :
0 commit comments