4
4
import sys
5
5
import termios
6
6
import tty
7
+ import select
7
8
from datetime import datetime
8
9
from typing import Any
9
10
@@ -179,37 +180,65 @@ def _display_header(self):
179
180
print ()
180
181
181
182
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 ."""
183
184
if not self .agent_runs :
184
185
print ("No agent runs found." )
186
+ self ._pad_to_lines (1 )
185
187
return
186
188
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 ]
188
215
# Highlight selected item
189
216
prefix = "→ " if i == self .selected_index and not self .show_action_menu else " "
190
217
191
218
status = self ._format_status (agent_run .get ("status" , "Unknown" ), agent_run )
192
219
created = self ._format_date (agent_run .get ("created_at" , "Unknown" ))
193
220
summary = agent_run .get ("summary" , "No summary" ) or "No summary"
194
221
195
- # No need to truncate summary as much since we removed the URL column
196
222
if len (summary ) > 60 :
197
223
summary = summary [:57 ] + "..."
198
224
199
- # Color coding: indigo blue for selected, darker gray for others (but keep status colors)
200
225
if i == self .selected_index and not self .show_action_menu :
201
- # Blue timestamp and summary for selected row, but preserve status colors
202
226
line = f"\033 [34m{ prefix } { created :<10} \033 [0m { status } \033 [34m{ summary } \033 [0m"
203
227
else :
204
- # Gray text for non-selected rows, but preserve status colors
205
228
line = f"\033 [90m{ prefix } { created :<10} \033 [0m { status } \033 [90m{ summary } \033 [0m"
206
229
207
230
print (line )
231
+ printed_rows += 1
208
232
209
233
# Show action menu right below the selected row if it's expanded
210
234
if i == self .selected_index and self .show_action_menu :
211
235
self ._display_inline_action_menu (agent_run )
212
236
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
+
213
242
def _display_new_tab (self ):
214
243
"""Display the new agent creation interface."""
215
244
print ("Create new background agent (Claude Code):" )
@@ -249,6 +278,9 @@ def _display_new_tab(self):
249
278
print (border_style + "└" + "─" * (box_width - 2 ) + "┘" + reset )
250
279
print ()
251
280
281
+ # The new tab main content area should be a fixed 10 lines
282
+ self ._pad_to_lines (6 )
283
+
252
284
def _create_background_agent (self , prompt : str ):
253
285
"""Create a background agent run."""
254
286
if not self .token or not self .org_id :
@@ -298,33 +330,36 @@ def _create_background_agent(self, prompt: str):
298
330
299
331
def _show_post_creation_menu (self , web_url : str ):
300
332
"""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
303
334
335
+ print ("\n What would you like to do next?" )
304
336
options = ["open in web preview" , "go to recents" ]
305
337
selected = 0
338
+ prev_lines = 0
306
339
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
313
343
for i , option in enumerate (options ):
314
344
if i == selected :
315
- print (f" \033 [34m→ { option } \033 [0m" )
345
+ menu_lines . append (f" \033 [34m→ { option } \033 [0m" )
316
346
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
318
351
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 )
320
354
321
- # Get input
355
+ while True :
322
356
key = self ._get_char ()
323
-
324
357
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 )
326
360
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 )
328
363
elif key == "\r " or key == "\n " : # Enter - select option
329
364
if selected == 0 : # open in web preview
330
365
try :
@@ -340,6 +375,8 @@ def _show_post_creation_menu(self, web_url: str):
340
375
self ._load_agent_runs () # Refresh the data
341
376
break
342
377
elif key == "\x1b " : # Esc - back to new tab
378
+ self .current_tab = 1 # 'new' tab index
379
+ self .input_mode = True
343
380
break
344
381
345
382
def _display_web_tab (self ):
@@ -353,6 +390,8 @@ def _display_web_tab(self):
353
390
print (f" \033 [34m→ Open Web ({ display_url } )\033 [0m" )
354
391
print ()
355
392
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 )
356
395
357
396
def _pull_agent_branch (self , agent_id : str ):
358
397
"""Pull the PR branch for an agent run locally."""
@@ -386,6 +425,11 @@ def _display_content(self):
386
425
elif self .current_tab == 2 : # web
387
426
self ._display_web_tab ()
388
427
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
+
389
433
def _display_inline_action_menu (self , agent_run : dict ):
390
434
"""Display action menu inline below the selected row."""
391
435
agent_id = agent_run .get ("id" , "unknown" )
@@ -432,8 +476,15 @@ def _get_char(self):
432
476
433
477
# Handle escape sequences (arrow keys)
434
478
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
435
483
ch2 = sys .stdin .read (1 )
436
484
if ch2 == "[" :
485
+ ready2 , _ , _ = select .select ([sys .stdin ], [], [], 0.03 )
486
+ if not ready2 :
487
+ return "\x1b ["
437
488
ch3 = sys .stdin .read (1 )
438
489
return f"\x1b [{ ch3 } "
439
490
else :
0 commit comments