@@ -9,6 +9,10 @@ import { type InquirerReadline } from '@inquirer/type';
99import { withHooks , effectScheduler } from './hook-engine.ts' ;
1010import { AbortPromptError , CancelPromptError , ExitPromptError } from './errors.ts' ;
1111
12+ // Capture the real setImmediate at module load time so it works even when test
13+ // frameworks mock timers with vi.useFakeTimers() or similar.
14+ const nativeSetImmediate = globalThis . setImmediate ;
15+
1216type ViewFunction < Value , Config > = (
1317 config : Prettify < Config > ,
1418 done : ( value : Value ) => void ,
@@ -49,6 +53,11 @@ export function createPrompt<Value, Config>(
4953 const output = new MuteStream ( ) ;
5054 output . pipe ( context . output ?? process . stdout ) ;
5155
56+ // Pre-mute the output so that readline doesn't echo stale keystrokes
57+ // to the terminal before the first render. ScreenManager will unmute/mute
58+ // the output around each render call as needed.
59+ output . mute ( ) ;
60+
5261 const rl = readline . createInterface ( {
5362 terminal : true ,
5463 input,
@@ -85,14 +94,6 @@ export function createPrompt<Value, Config>(
8594 rl . on ( 'SIGINT' , sigint ) ;
8695 cleanups . add ( ( ) => rl . removeListener ( 'SIGINT' , sigint ) ) ;
8796
88- // Re-renders only happen when the state change; but the readline cursor could change position
89- // and that also requires a re-render (and a manual one because we mute the streams).
90- // We set the listener after the initial workLoop to avoid a double render if render triggered
91- // by a state change sets the cursor to the right position.
92- const checkCursorPos = ( ) => screen . checkCursorPos ( ) ;
93- rl . input . on ( 'keypress' , checkCursorPos ) ;
94- cleanups . add ( ( ) => rl . input . removeListener ( 'keypress' , checkCursorPos ) ) ;
95-
9697 return withHooks ( rl , ( cycle ) => {
9798 // The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
9899 // triggers after the process is done (which happens after timeouts are done triggering.)
@@ -101,30 +102,57 @@ export function createPrompt<Value, Config>(
101102 rl . on ( 'close' , hooksCleanup ) ;
102103 cleanups . add ( ( ) => rl . removeListener ( 'close' , hooksCleanup ) ) ;
103104
104- cycle ( ( ) => {
105- try {
106- const nextView = view ( config , ( value ) => {
107- setImmediate ( ( ) => resolve ( value ) ) ;
108- } ) ;
109-
110- // Typescript won't allow this, but not all users rely on typescript.
111- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
112- if ( nextView === undefined ) {
113- const callerFilename = callSites [ 1 ] ?. getFileName ( ) ;
114- throw new Error (
115- `Prompt functions must return a string.\n at ${ callerFilename } ` ,
116- ) ;
105+ const startCycle = ( ) => {
106+ // Re-renders only happen when the state change; but the readline cursor could
107+ // change position and that also requires a re-render (and a manual one because
108+ // we mute the streams). We set the listener after the initial workLoop to avoid
109+ // a double render if render triggered by a state change sets the cursor to the
110+ // right position.
111+ const checkCursorPos = ( ) => screen . checkCursorPos ( ) ;
112+ rl . input . on ( 'keypress' , checkCursorPos ) ;
113+ cleanups . add ( ( ) => rl . input . removeListener ( 'keypress' , checkCursorPos ) ) ;
114+
115+ cycle ( ( ) => {
116+ try {
117+ const nextView = view ( config , ( value ) => {
118+ setImmediate ( ( ) => resolve ( value ) ) ;
119+ } ) ;
120+
121+ // Typescript won't allow this, but not all users rely on typescript.
122+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
123+ if ( nextView === undefined ) {
124+ const callerFilename = callSites [ 1 ] ?. getFileName ( ) ;
125+ throw new Error (
126+ `Prompt functions must return a string.\n at ${ callerFilename } ` ,
127+ ) ;
128+ }
129+
130+ const [ content , bottomContent ] =
131+ typeof nextView === 'string' ? [ nextView ] : nextView ;
132+ screen . render ( content , bottomContent ) ;
133+
134+ effectScheduler . run ( ) ;
135+ } catch ( error : unknown ) {
136+ reject ( error ) ;
117137 }
118-
119- const [ content , bottomContent ] =
120- typeof nextView === 'string' ? [ nextView ] : nextView ;
121- screen . render ( content , bottomContent ) ;
122-
123- effectScheduler . run ( ) ;
124- } catch ( error : unknown ) {
125- reject ( error ) ;
126- }
127- } ) ;
138+ } ) ;
139+ } ;
140+
141+ // Proper Readable streams (like process.stdin) may have OS-level buffered
142+ // data that arrives in the poll phase when readline resumes the stream.
143+ // Deferring the first render by one setImmediate tick (check phase, after
144+ // poll) lets that stale data flow through readline harmlessly—no keypress
145+ // handlers are registered yet and the output is muted, so the stale
146+ // keystrokes are silently discarded.
147+ // Old-style streams (like MuteStream) have no such buffering, so the
148+ // render cycle starts immediately.
149+ //
150+ // @see https://github.com/SBoudrias/Inquirer.js/issues/1303
151+ if ( 'readableFlowing' in input ) {
152+ nativeSetImmediate ( startCycle ) ;
153+ } else {
154+ startCycle ( ) ;
155+ }
128156
129157 return Object . assign (
130158 promise
0 commit comments