1515
1616from centml .sdk import PodStatus
1717from centml .sdk .shell .exceptions import NoPodAvailableError , PodNotFoundError
18- from centml .sdk .shell .renderer import pyte_extract_text , render_dirty
1918
2019BEGIN_MARKER = "__CENTML_BEGIN_5f3a__"
2120END_MARKER = "__CENTML_END_5f3a__"
@@ -74,20 +73,19 @@ def resolve_pod(cclient, deployment_id, pod_name=None) -> Tuple[str, Optional[st
7473 return running_pods [0 ], warning
7574
7675
77- async def forward_io (ws , screen , stream , shutdown ):
76+ async def forward_io (ws , term_size , shutdown ):
7877 """Bidirectional forwarding between local stdin/stdout and WebSocket.
7978
80- Output flows through a pyte terminal emulator so that cursor
81- addressing, line wrapping, and colors are rendered correctly
82- regardless of the remote PTY dimensions.
79+ Output is written directly to stdout (the local terminal is in raw
80+ mode, so ANSI sequences from the remote PTY are rendered natively).
8381
8482 The platform API proxy sends a close frame (code=1000) when the
8583 remote shell exits, so _read_ws terminates via ConnectionClosed.
8684
8785 Args:
8886 ws: WebSocket connection.
89- screen: pyte.Screen instance sized to the local terminal.
90- stream: pyte.Stream attached to *screen* .
87+ term_size: Mutable list ``[cols, rows]`` kept up-to-date by the
88+ SIGWINCH handler in ``interactive_session`` .
9189 shutdown: asyncio.Event set by signal handlers to request exit.
9290
9391 Returns:
@@ -104,11 +102,11 @@ async def _read_ws():
104102 msg = json .loads (raw_msg )
105103 data = msg .get ("data" , "" )
106104 if data :
107- stream . feed (data .replace ("\n " , "\r \n " ))
108- render_dirty ( screen , sys .stdout .buffer )
105+ sys . stdout . buffer . write (data .replace ("\n " , "\r \n " ). encode ( "utf-8" , errors = "replace " ))
106+ sys .stdout .buffer . flush ( )
109107 elif msg .get ("error" ):
110- stream . feed (f"Error: { msg ['error' ]} \r \n " )
111- render_dirty ( screen , sys .stdout .buffer )
108+ sys . stderr . buffer . write (f"Error: { msg ['error' ]} \r \n " . encode () )
109+ sys .stderr .buffer . flush ( )
112110 except websockets .ConnectionClosed :
113111 return
114112
@@ -135,8 +133,8 @@ def _on_stdin_ready():
135133 {
136134 "operation" : "stdin" ,
137135 "data" : data .decode ("utf-8" , errors = "replace" ),
138- "rows" : screen . lines ,
139- "cols" : screen . columns ,
136+ "rows" : term_size [ 1 ] ,
137+ "cols" : term_size [ 0 ] ,
140138 }
141139 )
142140 )
@@ -183,13 +181,7 @@ async def interactive_session(ws_url, token):
183181 try :
184182 tty .setraw (fd )
185183 cols , rows = shutil .get_terminal_size ()
186-
187- screen = pyte .Screen (cols , rows )
188- stream = pyte .Stream (screen )
189-
190- # Switch to alternate screen buffer (disables scrollback) and clear.
191- sys .stdout .buffer .write (b"\033 [?1049h\033 [2J\033 [H" )
192- sys .stdout .buffer .flush ()
184+ term_size = [cols , rows ]
193185
194186 shutdown = asyncio .Event ()
195187 loop .add_signal_handler (signal .SIGTERM , shutdown .set )
@@ -201,15 +193,14 @@ async def interactive_session(ws_url, token):
201193
202194 def _send_resize ():
203195 c , r = shutil .get_terminal_size ()
204- screen .resize (r , c )
205- screen .dirty .update (range (r ))
196+ term_size [0 ], term_size [1 ] = c , r
206197 asyncio .ensure_future (ws .send (json .dumps ({"operation" : "resize" , "rows" : r , "cols" : c })))
207198
208199 loop .add_signal_handler (signal .SIGWINCH , _send_resize )
209200
210201 await ws .send (json .dumps ({"operation" : "resize" , "rows" : rows , "cols" : cols }))
211202 try :
212- exit_code = await forward_io (ws , screen , stream , shutdown )
203+ exit_code = await forward_io (ws , term_size , shutdown )
213204 finally :
214205 loop .remove_signal_handler (signal .SIGWINCH )
215206
@@ -219,9 +210,17 @@ def _send_resize():
219210 loop .remove_signal_handler (signal .SIGTERM )
220211 loop .remove_signal_handler (signal .SIGHUP )
221212 termios .tcsetattr (fd , termios .TCSADRAIN , old_settings )
222- # Leave alternate screen buffer, restore cursor and attributes.
223- sys .stdout .buffer .write (b"\033 [?1049l\033 [?25h\033 [0m" )
224- sys .stdout .buffer .flush ()
213+
214+
215+ def _pyte_extract_text (line_stream , line_screen , text ):
216+ """Feed text through a single-row pyte screen and return visible characters.
217+
218+ More robust than regex ANSI stripping: pyte interprets all VT100/VT220
219+ sequences including OSC, cursor repositioning, and truecolor escapes.
220+ """
221+ line_screen .reset ()
222+ line_stream .feed (text )
223+ return "" .join (line_screen .buffer [0 ][col ].data for col in range (line_screen .columns )).rstrip ()
225224
226225
227226async def exec_session (ws_url , token , command ):
@@ -265,7 +264,7 @@ async def exec_session(ws_url, token, command):
265264 buffer += msg ["data" ]
266265 while "\n " in buffer :
267266 line , buffer = buffer .split ("\n " , 1 )
268- clean = pyte_extract_text (line_stream , line_screen , line .rstrip ("\r " ))
267+ clean = _pyte_extract_text (line_stream , line_screen , line .rstrip ("\r " ))
269268 if BEGIN_MARKER in clean :
270269 is_capturing = True
271270 continue
0 commit comments