@@ -26,6 +26,7 @@ import {
2626 getInteractionAvailability ,
2727 getLivePendingApprovals ,
2828 isSessionTurnStopping ,
29+ refusedSendReason ,
2930 type TurnViewModel ,
3031} from "@agenta/chat/model"
3132import { getSessionTurnId } from "@agenta/chat/state"
@@ -51,13 +52,19 @@ import type {RichChatInputHandle} from "@agenta/ui/rich-chat-input"
5152import { isAltChord } from "@agenta/ui/shortcuts"
5253import { Button } from "@agenta/ui/ui"
5354import { useQueryClient } from "@tanstack/react-query"
55+ import type { FileUIPart } from "ai"
5456import { useAtomValue , useSetAtom , useStore } from "jotai"
5557
5658import { ContentRail } from "@/components/ContentRail"
5759import { ScreenScaffold } from "@/components/ScreenScaffold"
5860
5961import { useProjectPermission } from "../context/useProjectPermission"
60- import { failPendingTaskAtom , pendingTasksAtom , sendPendingTaskAtom } from "../home/pendingTask"
62+ import {
63+ failPendingTaskAtom ,
64+ keepFailedSendAtom ,
65+ pendingTasksAtom ,
66+ sendPendingTaskAtom ,
67+ } from "../home/pendingTask"
6168import { AppShell } from "../nav/AppShell"
6269import { livenessQueryKey , useLivenessUpdatedAt } from "../sessions/useLivenessPoll"
6370import { useWalletSummary } from "../wallet/useWalletSummary"
@@ -179,6 +186,34 @@ export const LiveConversation = ({
179186 const registerLocalSession = useSetAtom ( registerLocalSessionAtom )
180187 const markLocalSessionAccepted = useSetAtom ( markLocalSessionAcceptedAtom )
181188 const dropUnacceptedLocalSession = useSetAtom ( dropUnacceptedLocalSessionAtom )
189+ // A send outlives this screen. One that fails after the user left has no composer to go back
190+ // to, so its session stays listed and the message waits as a failed task for their return.
191+ const keepFailedSend = useSetAtom ( keepFailedSendAtom )
192+ const onScreenRef = useRef ( false )
193+ useEffect ( ( ) => {
194+ onScreenRef . current = true
195+ return ( ) => {
196+ onScreenRef . current = false
197+ }
198+ } , [ ] )
199+ const sendFailed = useCallback (
200+ ( message : { text : string ; fileParts ?: FileUIPart [ ] } , error ?: unknown ) => {
201+ if ( onScreenRef . current ) {
202+ dropUnacceptedLocalSession ( sessionId )
203+ return
204+ }
205+ keepFailedSend ( {
206+ sessionId,
207+ task : {
208+ agentId : agentId ?? entityId ,
209+ text : message . text ,
210+ parts : message . fileParts ,
211+ failureReason : refusedSendReason ( error ) ?? undefined ,
212+ } ,
213+ } )
214+ } ,
215+ [ agentId , dropUnacceptedLocalSession , entityId , keepFailedSend , sessionId ] ,
216+ )
182217 const followCommitRef = useRef < ( revisionId : string ) => void > ( ( ) => undefined )
183218 const conversation = useAgentConversation ( {
184219 entityId,
@@ -189,7 +224,7 @@ export const LiveConversation = ({
189224 sharedReaderLivenessUpdatedAt : livenessUpdatedAt ,
190225 restoreRefusedSend,
191226 onSendAccepted : ( ) => markLocalSessionAccepted ( sessionId ) ,
192- onSendFailed : ( ) => dropUnacceptedLocalSession ( sessionId ) ,
227+ onSendFailed : sendFailed ,
193228 } )
194229 const canEditSecrets = useProjectPermission ( projectId , "edit_secret" )
195230 const store = useStore ( )
@@ -356,17 +391,17 @@ export const LiveConversation = ({
356391 await sendToConversation ( input )
357392 } catch ( error ) {
358393 // The durable path reports this through `onSendFailed` too; this covers the rest.
359- dropUnacceptedLocalSession ( sessionId )
394+ sendFailed ( { text : input . text , fileParts : input . parts } , error )
360395 throw error
361396 }
362397 } ,
363398 [
364399 agentId ,
365400 buildKitReadyAtom ,
366- dropUnacceptedLocalSession ,
367401 firstTurn ,
368402 projectId ,
369403 registerLocalSession ,
404+ sendFailed ,
370405 sendToConversation ,
371406 sessionId ,
372407 store ,
0 commit comments