44import sys
55import termios
66import tty
7+ import select
78from datetime import datetime
89from typing import Any
910
@@ -179,37 +180,65 @@ def _display_header(self):
179180 print ()
180181
181182 def _display_agent_list (self ):
182- """Display the list of agent runs."""
183+ """Display the list of agent runs, fixed to 10 lines of main content ."""
183184 if not self .agent_runs :
184185 print ("No agent runs found." )
186+ self ._pad_to_lines (1 )
185187 return
186188
187- for i , agent_run in enumerate (self .agent_runs ):
189+ # Determine how many extra lines the inline action menu will print (if open)
190+ menu_lines = 0
191+ if self .show_action_menu and 0 <= self .selected_index < len (self .agent_runs ):
192+ selected_run = self .agent_runs [self .selected_index ]
193+ github_prs = selected_run .get ("github_pull_requests" , [])
194+ options_count = 1 # "open in web"
195+ if github_prs :
196+ options_count += 1 # "pull locally"
197+ if github_prs and github_prs [0 ].get ("url" ):
198+ options_count += 1 # "open PR"
199+ menu_lines = options_count + 1 # +1 for the hint line
200+
201+ # We want total printed lines (rows + menu) to be 10
202+ window_size = max (1 , 10 - menu_lines )
203+
204+ total = len (self .agent_runs )
205+ if total <= window_size :
206+ start = 0
207+ end = total
208+ else :
209+ start = max (0 , min (self .selected_index - window_size // 2 , total - window_size ))
210+ end = start + window_size
211+
212+ printed_rows = 0
213+ for i in range (start , end ):
214+ agent_run = self .agent_runs [i ]
188215 # Highlight selected item
189216 prefix = "→ " if i == self .selected_index and not self .show_action_menu else " "
190217
191218 status = self ._format_status (agent_run .get ("status" , "Unknown" ), agent_run )
192219 created = self ._format_date (agent_run .get ("created_at" , "Unknown" ))
193220 summary = agent_run .get ("summary" , "No summary" ) or "No summary"
194221
195- # No need to truncate summary as much since we removed the URL column
196222 if len (summary ) > 60 :
197223 summary = summary [:57 ] + "..."
198224
199- # Color coding: indigo blue for selected, darker gray for others (but keep status colors)
200225 if i == self .selected_index and not self .show_action_menu :
201- # Blue timestamp and summary for selected row, but preserve status colors
202226 line = f"\033 [34m{ prefix } { created :<10} \033 [0m { status } \033 [34m{ summary } \033 [0m"
203227 else :
204- # Gray text for non-selected rows, but preserve status colors
205228 line = f"\033 [90m{ prefix } { created :<10} \033 [0m { status } \033 [90m{ summary } \033 [0m"
206229
207230 print (line )
231+ printed_rows += 1
208232
209233 # Show action menu right below the selected row if it's expanded
210234 if i == self .selected_index and self .show_action_menu :
211235 self ._display_inline_action_menu (agent_run )
212236
237+ # If fewer than needed to reach 10 lines, pad blank lines
238+ total_printed = printed_rows + menu_lines
239+ if total_printed < 10 :
240+ self ._pad_to_lines (total_printed )
241+
213242 def _display_new_tab (self ):
214243 """Display the new agent creation interface."""
215244 print ("Create new background agent (Claude Code):" )
@@ -249,6 +278,9 @@ def _display_new_tab(self):
249278 print (border_style + "└" + "─" * (box_width - 2 ) + "┘" + reset )
250279 print ()
251280
281+ # The new tab main content area should be a fixed 10 lines
282+ self ._pad_to_lines (6 )
283+
252284 def _create_background_agent (self , prompt : str ):
253285 """Create a background agent run."""
254286 if not self .token or not self .org_id :
@@ -298,33 +330,36 @@ def _create_background_agent(self, prompt: str):
298330
299331 def _show_post_creation_menu (self , web_url : str ):
300332 """Show menu after successful agent creation."""
301- print ("\n What would you like to do next?" )
302- print ()
333+ from codegen .cli .utils .inplace_print import inplace_print
303334
335+ print ("\n What would you like to do next?" )
304336 options = ["open in web preview" , "go to recents" ]
305337 selected = 0
338+ prev_lines = 0
306339
307- while True :
308- # Clear previous menu display and move cursor up
309- for i in range (len (options ) + 2 ):
310- print ("\033 [K" ) # Clear line
311- print (f"\033 [{ len (options ) + 2 } A" , end = "" ) # Move cursor up
312-
340+ def build_lines ():
341+ menu_lines = []
342+ # Options
313343 for i , option in enumerate (options ):
314344 if i == selected :
315- print (f" \033 [34m→ { option } \033 [0m" )
345+ menu_lines . append (f" \033 [34m→ { option } \033 [0m" )
316346 else :
317- print (f" \033 [90m { option } \033 [0m" )
347+ menu_lines .append (f" \033 [90m { option } \033 [0m" )
348+ # Hint line last
349+ menu_lines .append ("\033 [90m[Enter] select • [↑↓] navigate • [Esc] back to new tab\033 [0m" )
350+ return menu_lines
318351
319- print ("\n \033 [90m[Enter] select • [↑↓] navigate • [Esc] back to new tab\033 [0m" )
352+ # Initial render
353+ prev_lines = inplace_print (build_lines (), prev_lines )
320354
321- # Get input
355+ while True :
322356 key = self ._get_char ()
323-
324357 if key == "\x1b [A" or key .lower () == "w" : # Up arrow or W
325- selected = max (0 , selected - 1 )
358+ selected = (selected - 1 ) % len (options )
359+ prev_lines = inplace_print (build_lines (), prev_lines )
326360 elif key == "\x1b [B" or key .lower () == "s" : # Down arrow or S
327- selected = min (len (options ) - 1 , selected + 1 )
361+ selected = (selected + 1 ) % len (options )
362+ prev_lines = inplace_print (build_lines (), prev_lines )
328363 elif key == "\r " or key == "\n " : # Enter - select option
329364 if selected == 0 : # open in web preview
330365 try :
@@ -340,6 +375,8 @@ def _show_post_creation_menu(self, web_url: str):
340375 self ._load_agent_runs () # Refresh the data
341376 break
342377 elif key == "\x1b " : # Esc - back to new tab
378+ self .current_tab = 1 # 'new' tab index
379+ self .input_mode = True
343380 break
344381
345382 def _display_web_tab (self ):
@@ -353,6 +390,8 @@ def _display_web_tab(self):
353390 print (f" \033 [34m→ Open Web ({ display_url } )\033 [0m" )
354391 print ()
355392 print ("Press Enter to open the web interface in your browser." )
393+ # The web tab main content area should be a fixed 10 lines
394+ self ._pad_to_lines (5 )
356395
357396 def _pull_agent_branch (self , agent_id : str ):
358397 """Pull the PR branch for an agent run locally."""
@@ -386,6 +425,11 @@ def _display_content(self):
386425 elif self .current_tab == 2 : # web
387426 self ._display_web_tab ()
388427
428+ def _pad_to_lines (self , lines_printed : int , target : int = 10 ):
429+ """Pad the main content area with blank lines to reach a fixed height."""
430+ for _ in range (max (0 , target - lines_printed )):
431+ print ()
432+
389433 def _display_inline_action_menu (self , agent_run : dict ):
390434 """Display action menu inline below the selected row."""
391435 agent_id = agent_run .get ("id" , "unknown" )
@@ -432,8 +476,15 @@ def _get_char(self):
432476
433477 # Handle escape sequences (arrow keys)
434478 if ch == "\x1b " : # ESC
479+ # Peek for additional bytes to distinguish bare ESC vs sequences
480+ ready , _ , _ = select .select ([sys .stdin ], [], [], 0.03 )
481+ if not ready :
482+ return "\x1b " # bare Esc
435483 ch2 = sys .stdin .read (1 )
436484 if ch2 == "[" :
485+ ready2 , _ , _ = select .select ([sys .stdin ], [], [], 0.03 )
486+ if not ready2 :
487+ return "\x1b ["
437488 ch3 = sys .stdin .read (1 )
438489 return f"\x1b [{ ch3 } "
439490 else :
0 commit comments