@@ -81,7 +81,6 @@ import {
8181 seedDeploymentShape ,
8282} from '@/lib/core/config/deployment-shape'
8383import { MothershipHandoffStorage } from '@/lib/core/utils/browser-storage'
84- import { normalizeMessage } from '@/lib/mothership/chat/persisted-message'
8584import type { MothershipStreamV1EventEnvelope } from '@/lib/mothership/generated/mothership-stream-v1'
8685import { createSearchResource } from '@/lib/mothership/resources/search'
8786import { getChatResourceSelectionId } from '@/lib/mothership/resources/types'
@@ -117,6 +116,7 @@ interface NetworkState {
117116 abortSettlements : boolean [ ]
118117 abortBodies : CopilotChatAbortBody [ ]
119118 stopBodies : CopilotChatStopBody [ ]
119+ toolInputPadding ?: string
120120 abortTraceparents : Array < string | null >
121121}
122122
@@ -221,11 +221,14 @@ async function fetchStub(input: RequestInfo | URL, init?: RequestInit): Promise<
221221 stream : { streamId } ,
222222 payload : {
223223 phase : 'call' ,
224- executor : 'client' ,
225- mode : 'async' ,
226- toolName : 'run_workflow' ,
224+ executor : state . toolInputPadding ? 'go' : 'client' ,
225+ mode : state . toolInputPadding ? 'sync' : 'async' ,
226+ toolName : state . toolInputPadding ? 'run_code' : 'run_workflow' ,
227227 toolCallId : 'this-chat-tool' ,
228- arguments : { workflowId : 'this-chat-workflow' } ,
228+ arguments : {
229+ workflowId : 'this-chat-workflow' ,
230+ ...( state . toolInputPadding ? { padding : state . toolInputPadding } : { } ) ,
231+ } ,
229232 } ,
230233 }
231234 return new Response (
@@ -751,6 +754,7 @@ describe('useChat remount send recovery', () => {
751754 state . pendingAdmissions . clear ( )
752755 state . abortSettlements = [ ]
753756 state . abortBodies = [ ]
757+ state . toolInputPadding = undefined
754758 state . stopBodies = [ ]
755759 state . abortTraceparents = [ ]
756760 mockRequestJson . mockResolvedValue ( { chats : [ ] } )
@@ -1450,6 +1454,41 @@ describe('useChat remount send recovery', () => {
14501454 }
14511455 )
14521456
1457+ it . each ( [ false , true ] ) (
1458+ 'does not interrupt or send a queued edit before submission (explicit ID: %s)' ,
1459+ async ( explicitId ) => {
1460+ state . postBehavior = 'task'
1461+ const { getResult } = renderUseChatInChat ( 'chat-a' )
1462+ await act ( async ( ) => {
1463+ void getResult ( ) . sendMessage ( 'Original request' )
1464+ } )
1465+ await waitFor ( ( ) => state . postBodies . length === 1 && getResult ( ) . isSending )
1466+ const beforeRender = getResult ( )
1467+ let queuedId = ''
1468+ await act ( async ( ) => {
1469+ void beforeRender . sendMessage ( 'Unfinished correction' )
1470+ queuedId = allQueuedMessages ( ) [ 0 ] . id
1471+ beforeRender . editQueuedMessage ( queuedId )
1472+ void beforeRender . sendNow ( explicitId ? queuedId : undefined )
1473+ } )
1474+ expect ( state . abortBodies ) . toHaveLength ( 0 )
1475+ expect ( state . postBodies ) . toHaveLength ( 1 )
1476+ expect ( allQueuedMessages ( ) ) . toEqual ( [
1477+ expect . objectContaining ( { id : queuedId , content : 'Unfinished correction' } ) ,
1478+ ] )
1479+ expect ( useMothershipQueueStore . getState ( ) . editing [ 'chat-a' ] ) . toBe ( queuedId )
1480+ state . postBehavior = 'hang'
1481+ await act ( async ( ) => {
1482+ void getResult ( ) . sendMessage ( 'Finished correction' )
1483+ void getResult ( ) . sendNow ( )
1484+ } )
1485+ await waitFor ( ( ) => state . postBodies . length === 2 )
1486+ expect ( state . postBodies [ 1 ] . message ) . toBe ( 'Finished correction' )
1487+ expect ( state . abortBodies ) . toHaveLength ( 1 )
1488+ expect ( allQueuedMessages ( ) ) . toHaveLength ( 0 )
1489+ }
1490+ )
1491+
14531492 it ( 'sends the live queue head once without waiting for a render, after Stop settles' , async ( ) => {
14541493 state . postBehavior = 'task'
14551494 const { getResult } = renderUseChatInChat ( 'chat-a' )
@@ -1713,7 +1752,7 @@ describe('useChat remount send recovery', () => {
17131752 }
17141753 } )
17151754
1716- it ( 'preserves a visible workflow watch when Stop persists the partial response ' , async ( ) => {
1755+ it ( 'preserves the visible workflow watch while Stop sends only identifiers ' , async ( ) => {
17171756 state . postBehavior = 'task'
17181757 const { getResult } = renderUseChatInChat ( 'chat-a' )
17191758 await act ( async ( ) => {
@@ -1730,20 +1769,43 @@ describe('useChat remount send recovery', () => {
17301769 await getResult ( ) . stopGeneration ( )
17311770 } )
17321771 expect ( state . stopBodies ) . toHaveLength ( 1 )
1733- const saved = state . stopBodies [ 0 ]
1734- const restored = normalizeMessage ( {
1735- id : 'saved-assistant' ,
1736- role : 'assistant' ,
1737- content : saved . content ,
1738- contentBlocks : saved . contentBlocks ,
1739- } )
1740- expect ( restored . contentBlocks ?. find ( ( block ) => block . type === 'task' ) ?. task ) . toEqual ( {
1741- taskId : 'watch-1' ,
1742- kind : 'workflow_run' ,
1743- status : 'pending' ,
1744- target : { workflowId : 'workflow-1' , executionId : 'watched-execution' } ,
1745- note : 'Check the completed invoice run' ,
1772+ expect ( state . stopBodies [ 0 ] ) . toEqual ( {
1773+ chatId : 'chat-a' ,
1774+ streamId : state . postBodies [ 0 ] . userMessageId ,
1775+ } )
1776+ const task = getResult ( )
1777+ . messages . flatMap ( ( message ) => message . contentBlocks ?? [ ] )
1778+ . find ( ( block ) => block . type === 'task' ) ?. task
1779+ expect ( task ?. taskId ) . toBe ( 'watch-1' )
1780+ } )
1781+
1782+ it ( 'sends a queued correction after stopping with more than 10 MiB of tool input' , async ( ) => {
1783+ state . postBehavior = 'tool'
1784+ state . toolInputPadding = 'x' . repeat ( 11 * 1024 * 1024 )
1785+ const { getResult } = renderUseChatInChat ( 'chat-a' )
1786+ await act ( async ( ) => {
1787+ void getResult ( ) . sendMessage ( 'Start working' )
17461788 } )
1789+ await waitFor ( ( ) =>
1790+ expect (
1791+ getResult ( ) . messages . some ( ( message ) =>
1792+ message . contentBlocks ?. some ( ( block ) => block . toolCall ?. id === 'this-chat-tool' )
1793+ )
1794+ ) . toBe ( true )
1795+ )
1796+ state . postBehavior = 'hang'
1797+ await act ( async ( ) => {
1798+ await getResult ( ) . sendMessage ( 'Use the correction' )
1799+ void getResult ( ) . sendNow ( )
1800+ } )
1801+ await waitFor ( ( ) => expect ( state . postBodies ) . toHaveLength ( 2 ) )
1802+ expect ( state . postBodies [ 1 ] . message ) . toBe ( 'Use the correction' )
1803+ expect ( state . stopBodies ) . toHaveLength ( 1 )
1804+ expect ( state . stopBodies [ 0 ] ) . not . toHaveProperty ( 'content' )
1805+ expect ( state . stopBodies [ 0 ] ) . not . toHaveProperty ( 'contentBlocks' )
1806+ expect ( new TextEncoder ( ) . encode ( JSON . stringify ( state . stopBodies [ 0 ] ) ) . length ) . toBeLessThan ( 1024 )
1807+ expect ( allQueuedMessages ( ) ) . toHaveLength ( 0 )
1808+ expect ( getResult ( ) . error ) . toBeNull ( )
17471809 } )
17481810
17491811 it . each ( [ false , true ] ) (
0 commit comments