Skip to content

Commit 90a79c4

Browse files
authored
fix: constant tab height, [new] escape key, post create buffer (#1210)
# Motivation <!-- Why is this change necessary? --> # Content <!-- Please include a summary of the change --> # Testing <!-- How was the change tested? --> # Please check the following before marking your PR as ready for review - [ ] I have added tests for my changes - [ ] I have updated the documentation or added new documentation as needed
1 parent 96fe679 commit 90a79c4

File tree

2 files changed

+99
-21
lines changed

2 files changed

+99
-21
lines changed

src/codegen/cli/tui/app.py

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import termios
66
import tty
7+
import select
78
from datetime import datetime
89
from 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("\nWhat would you like to do next?")
302-
print()
333+
from codegen.cli.utils.inplace_print import inplace_print
303334

335+
print("\nWhat 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:
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import sys
2+
from typing import Iterable
3+
4+
5+
def inplace_print(lines: Iterable[str], prev_lines_rendered: int) -> int:
6+
"""Redraw a small block of text in-place without scrolling.
7+
8+
Args:
9+
lines: The lines to render (each should NOT include a trailing newline)
10+
prev_lines_rendered: How many lines were rendered in the previous frame. Pass 0 on first call.
11+
12+
Returns:
13+
The number of lines rendered this call. Use as prev_lines_rendered on the next call.
14+
"""
15+
# Move cursor up to the start of the previous block (if any)
16+
if prev_lines_rendered > 0:
17+
sys.stdout.write(f"\x1b[{prev_lines_rendered}F") # Cursor up N lines
18+
19+
# Rewrite each line, clearing it first to avoid remnants from previous content
20+
count = 0
21+
for line in lines:
22+
sys.stdout.write("\x1b[2K\r") # Clear entire line and return carriage
23+
sys.stdout.write(f"{line}\n")
24+
count += 1
25+
26+
sys.stdout.flush()
27+
return count

0 commit comments

Comments
 (0)