11import test from 'ava' ;
22import { Text } from 'ink' ;
33import React from 'react' ;
4+ import { DELAY_COMMAND_COMPLETE_MS } from '@/constants' ;
5+ import { useUserMessageQueue } from '@/hooks/useUserMessageQueue' ;
46import stripAnsi from 'strip-ansi' ;
57import type { Message } from '@/types' ;
68import { renderWithTheme } from '../../test-utils/render-with-theme.js' ;
@@ -43,6 +45,13 @@ interface Overrides {
4345 setPendingPlanProceed ?: ( v : string | null ) => void ;
4446 handleMessageSubmit ?: ( message : string ) => Promise < void > ;
4547 currentSessionId ?: string | null ;
48+ toolManager ?: unknown ;
49+ queuedMessages ?: Array < { id : string ; message : string ; displayValue : string } > ;
50+ handleUserSubmit ?: ( message : string ) => Promise < void > ;
51+ drainNextMessage ?: (
52+ dispatch : ( message : { id : string ; message : string ; displayValue : string } ) =>
53+ boolean | Promise < boolean > ,
54+ ) => boolean | Promise < boolean > ;
4655}
4756
4857function makeProps ( o : Overrides = { } ) {
@@ -51,6 +60,7 @@ function makeProps(o: Overrides = {}) {
5160
5261 const appState = {
5362 client : o . client ?? null ,
63+ toolManager : o . toolManager ?? null ,
5464 messages : o . messages ?? [ ] ,
5565 currentModel : 'mock-model' ,
5666 currentProvider : 'mock' ,
@@ -143,16 +153,16 @@ function makeProps(o: Overrides = {}) {
143153 pendingToolConfirmation : null ,
144154 handleToolConfirmation : noop ,
145155 handleQuestionAnswer : noop ,
146- handleUserSubmit : noopAsync ,
156+ handleUserSubmit : o . handleUserSubmit ?? noopAsync ,
147157 userMessageQueue : {
148- queuedMessages : [ ] ,
158+ queuedMessages : o . queuedMessages ?? [ ] ,
149159 enqueueMessage : ( ) => ( {
150160 id : 'queued-test' ,
151161 message : '' ,
152162 displayValue : '' ,
153163 } ) ,
154164 removeMessage : noop ,
155- drainNextMessage : ( ) => false ,
165+ drainNextMessage : o . drainNextMessage ?? ( ( ) => false ) ,
156166 } ,
157167 handleIdeSelect : noop ,
158168 } as never ;
@@ -163,6 +173,166 @@ test('renders without crashing in default state', t => {
163173 t . truthy ( lastFrame ( ) ) ;
164174} ) ;
165175
176+ test ( 'does not drain queued prompts while a turn is generating' , async t => {
177+ let submitted = false ;
178+ const { unmount} = renderWithTheme (
179+ < InteractiveApp
180+ { ...makeProps ( {
181+ startChat : true ,
182+ client : { } ,
183+ toolManager : { } ,
184+ isGenerating : true ,
185+ isConversationComplete : true ,
186+ queuedMessages : [
187+ { id : 'queued-1' , message : 'queued prompt' , displayValue : 'queued prompt' } ,
188+ ] ,
189+ handleUserSubmit : async ( ) => {
190+ submitted = true ;
191+ } ,
192+ } ) }
193+ /> ,
194+ ) ;
195+
196+ await new Promise ( resolve => setTimeout ( resolve , 25 ) ) ;
197+ t . false ( submitted ) ;
198+ unmount ( ) ;
199+ } ) ;
200+
201+ test ( 'does not drain queued prompts while a modal mode is active' , async t => {
202+ let submitted = false ;
203+ const { unmount} = renderWithTheme (
204+ < InteractiveApp
205+ { ...makeProps ( {
206+ startChat : true ,
207+ client : { } ,
208+ toolManager : { } ,
209+ activeMode : 'model' ,
210+ isConversationComplete : true ,
211+ queuedMessages : [
212+ { id : 'queued-1' , message : 'queued prompt' , displayValue : 'queued prompt' } ,
213+ ] ,
214+ handleUserSubmit : async ( ) => {
215+ submitted = true ;
216+ } ,
217+ } ) }
218+ /> ,
219+ ) ;
220+
221+ await new Promise ( resolve => setTimeout ( resolve , 25 ) ) ;
222+ t . false ( submitted ) ;
223+ unmount ( ) ;
224+ } ) ;
225+
226+ test ( 'does not drain queued prompts while plan review is active' , async t => {
227+ let submitted = false ;
228+ const { unmount} = renderWithTheme (
229+ < InteractiveApp
230+ { ...makeProps ( {
231+ startChat : true ,
232+ client : { } ,
233+ toolManager : { } ,
234+ planReviewState : { show : true , originalMessage : 'make a plan' } ,
235+ isConversationComplete : true ,
236+ queuedMessages : [
237+ { id : 'queued-1' , message : 'queued prompt' , displayValue : 'queued prompt' } ,
238+ ] ,
239+ handleUserSubmit : async ( ) => {
240+ submitted = true ;
241+ } ,
242+ } ) }
243+ /> ,
244+ ) ;
245+
246+ await new Promise ( resolve => setTimeout ( resolve , 25 ) ) ;
247+ t . false ( submitted ) ;
248+ unmount ( ) ;
249+ } ) ;
250+
251+ test ( 'drains every queued prompt after each dispatched turn returns to idle' , async t => {
252+ const submitted : string [ ] = [ ] ;
253+
254+ const QueueDrainHarness = ( ) => {
255+ const userMessageQueue = useUserMessageQueue ( ) ;
256+ const [ isConversationComplete , setIsConversationComplete ] =
257+ React . useState ( true ) ;
258+
259+ React . useEffect ( ( ) => {
260+ userMessageQueue . enqueueMessage ( { message : 'first' , displayValue : 'first' } ) ;
261+ userMessageQueue . enqueueMessage ( { message : 'second' , displayValue : 'second' } ) ;
262+ } , [ userMessageQueue . enqueueMessage ] ) ;
263+
264+ return (
265+ < InteractiveApp
266+ { ...makeProps ( {
267+ startChat : true ,
268+ client : { } ,
269+ toolManager : { } ,
270+ isConversationComplete,
271+ handleUserSubmit : async message => {
272+ submitted . push ( message ) ;
273+ setIsConversationComplete ( false ) ;
274+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
275+ setIsConversationComplete ( true ) ;
276+ } ,
277+ } ) }
278+ userMessageQueue = { userMessageQueue }
279+ />
280+ ) ;
281+ } ;
282+
283+ const { unmount} = renderWithTheme ( < QueueDrainHarness /> ) ;
284+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
285+ t . deepEqual ( submitted , [ 'first' , 'second' ] ) ;
286+ unmount ( ) ;
287+ } ) ;
288+
289+ test ( 'drains a prompt after delayed command completion when the app is idle' , async t => {
290+ const submitted : string [ ] = [ ] ;
291+
292+ const DelayedCommandHarness = ( ) => {
293+ const userMessageQueue = useUserMessageQueue ( ) ;
294+ const [ isToolExecuting , setIsToolExecuting ] = React . useState ( true ) ;
295+ const [ isConversationComplete , setIsConversationComplete ] =
296+ React . useState ( false ) ;
297+
298+ React . useEffect ( ( ) => {
299+ userMessageQueue . enqueueMessage ( {
300+ message : 'after compact' ,
301+ displayValue : 'after compact' ,
302+ } ) ;
303+ const timeout = setTimeout ( ( ) => {
304+ setIsToolExecuting ( false ) ;
305+ setIsConversationComplete ( true ) ;
306+ } , DELAY_COMMAND_COMPLETE_MS ) ;
307+
308+ return ( ) => clearTimeout ( timeout ) ;
309+ } , [ userMessageQueue . enqueueMessage ] ) ;
310+
311+ return (
312+ < InteractiveApp
313+ { ...makeProps ( {
314+ startChat : true ,
315+ client : { } ,
316+ toolManager : { } ,
317+ isToolExecuting,
318+ isConversationComplete,
319+ handleUserSubmit : async message => {
320+ submitted . push ( message ) ;
321+ } ,
322+ } ) }
323+ userMessageQueue = { userMessageQueue }
324+ />
325+ ) ;
326+ } ;
327+
328+ const { unmount} = renderWithTheme ( < DelayedCommandHarness /> ) ;
329+ await new Promise ( resolve =>
330+ setTimeout ( resolve , DELAY_COMMAND_COMPLETE_MS + 40 ) ,
331+ ) ;
332+ t . deepEqual ( submitted , [ 'after compact' ] ) ;
333+ unmount ( ) ;
334+ } ) ;
335+
166336test ( 'renders the static-component marker through ChatHistory' , t => {
167337 const { lastFrame} = renderWithTheme (
168338 < InteractiveApp { ...makeProps ( { startChat : true } ) } /> ,
0 commit comments