@@ -116,41 +116,21 @@ async function execCommand(
116116 authToken,
117117 } ) ;
118118 const ws = await openPtyWebSocket ( wsUrl , authToken ) ;
119- await refreshPtySessionAfterAttach (
120- ws ,
121- baseUrl ,
122- sessionName ,
123- 80 ,
124- 24 ,
125- authToken ,
126- ) ;
127- ws . send ( command + "\n" ) ;
128-
129- return new Promise < void > ( ( resolve , reject ) => {
130- const releaseOnce = createPtySessionReleaser (
131- baseUrl ,
132- sessionName ,
133- authToken ,
134- ) ;
135- const disposeInterruptSignals = registerPtyInterruptHandlers (
136- ws ,
137- releaseOnce ,
138- ) ;
139-
140- let timeoutId : ReturnType < typeof setTimeout > | undefined ;
141- if ( PTY_EXEC_TIMEOUT_MS > 0 ) {
142- timeoutId = setTimeout ( ( ) => {
143- releaseOnce ( ) ;
144- ws . close ( ) ;
145- } , PTY_EXEC_TIMEOUT_MS ) ;
146- }
147119
120+ const releaseOnce = createPtySessionReleaser ( baseUrl , sessionName , authToken ) ;
121+ const disposeInterruptSignals = registerPtyInterruptHandlers ( ws , releaseOnce ) ;
122+
123+ let timeoutId : ReturnType < typeof setTimeout > | undefined ;
124+
125+ const completion = new Promise < void > ( ( resolve , reject ) => {
148126 const finish = ( ) => {
149127 if ( timeoutId !== undefined ) clearTimeout ( timeoutId ) ;
150128 releaseOnce ( ) ;
151129 disposeInterruptSignals ( ) ;
152130 } ;
153131
132+ // Attach listeners synchronously *before* any await — otherwise messages
133+ // emitted between ws open and listener registration are dropped.
154134 ws . on ( "message" , ( data : WebSocket . RawData ) => {
155135 writePtyStreamToStdout ( data ) ;
156136 } ) ;
@@ -164,7 +144,29 @@ async function execCommand(
164144 finish ( ) ;
165145 reject ( err ) ;
166146 } ) ;
147+
148+ if ( PTY_EXEC_TIMEOUT_MS > 0 ) {
149+ timeoutId = setTimeout ( ( ) => {
150+ releaseOnce ( ) ;
151+ ws . close ( ) ;
152+ } , PTY_EXEC_TIMEOUT_MS ) ;
153+ }
167154 } ) ;
155+
156+ await refreshPtySessionAfterAttach (
157+ ws ,
158+ baseUrl ,
159+ sessionName ,
160+ 80 ,
161+ 24 ,
162+ authToken ,
163+ ) ;
164+ // Send the command followed by `exit` so the shell terminates and the
165+ // server closes the WebSocket. Without this, the session would stay open
166+ // after the command finished and the CLI would hang.
167+ ws . send ( command + "\nexit\n" ) ;
168+
169+ return completion ;
168170}
169171
170172async function interactiveSession (
@@ -181,23 +183,26 @@ async function interactiveSession(
181183 authToken,
182184 } ) ;
183185 const ws = await openPtyWebSocket ( wsUrl , authToken ) ;
184- await refreshPtySessionAfterAttach (
186+
187+ // Attach IO listeners before the refresh round-trip so server output
188+ // emitted during the ptyControl HTTP call is not dropped.
189+ const releaseOnce = createPtySessionReleaser ( baseUrl , sessionName , authToken ) ;
190+ const { dispose, done } = startPtyIoSession (
185191 ws ,
186192 baseUrl ,
187193 sessionName ,
188- cols ,
189- rows ,
190194 authToken ,
191195 ) ;
196+ const disposeSignals = registerPtyInterruptHandlers ( ws , releaseOnce ) ;
192197
193- const releaseOnce = createPtySessionReleaser ( baseUrl , sessionName , authToken ) ;
194- const { dispose, done } = startPtyIoSession (
198+ await refreshPtySessionAfterAttach (
195199 ws ,
196200 baseUrl ,
197201 sessionName ,
202+ cols ,
203+ rows ,
198204 authToken ,
199205 ) ;
200- const disposeSignals = registerPtyInterruptHandlers ( ws , releaseOnce ) ;
201206
202207 try {
203208 await done ;
0 commit comments