-
Notifications
You must be signed in to change notification settings - Fork 61
fix: constant tab height, [new] escape key, post create buffer #1210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -4,6 +4,7 @@ | |||||||||||
import sys | ||||||||||||
import termios | ||||||||||||
import tty | ||||||||||||
import select | ||||||||||||
from datetime import datetime | ||||||||||||
from typing import Any | ||||||||||||
|
||||||||||||
|
@@ -179,37 +180,65 @@ def _display_header(self): | |||||||||||
print() | ||||||||||||
|
||||||||||||
def _display_agent_list(self): | ||||||||||||
"""Display the list of agent runs.""" | ||||||||||||
"""Display the list of agent runs, fixed to 10 lines of main content.""" | ||||||||||||
if not self.agent_runs: | ||||||||||||
print("No agent runs found.") | ||||||||||||
self._pad_to_lines(1) | ||||||||||||
return | ||||||||||||
|
||||||||||||
for i, agent_run in enumerate(self.agent_runs): | ||||||||||||
# Determine how many extra lines the inline action menu will print (if open) | ||||||||||||
menu_lines = 0 | ||||||||||||
if self.show_action_menu and 0 <= self.selected_index < len(self.agent_runs): | ||||||||||||
selected_run = self.agent_runs[self.selected_index] | ||||||||||||
github_prs = selected_run.get("github_pull_requests", []) | ||||||||||||
options_count = 1 # "open in web" | ||||||||||||
if github_prs: | ||||||||||||
options_count += 1 # "pull locally" | ||||||||||||
if github_prs and github_prs[0].get("url"): | ||||||||||||
options_count += 1 # "open PR" | ||||||||||||
menu_lines = options_count + 1 # +1 for the hint line | ||||||||||||
|
||||||||||||
# We want total printed lines (rows + menu) to be 10 | ||||||||||||
window_size = max(1, 10 - menu_lines) | ||||||||||||
|
||||||||||||
total = len(self.agent_runs) | ||||||||||||
if total <= window_size: | ||||||||||||
start = 0 | ||||||||||||
end = total | ||||||||||||
else: | ||||||||||||
start = max(0, min(self.selected_index - window_size // 2, total - window_size)) | ||||||||||||
end = start + window_size | ||||||||||||
|
||||||||||||
printed_rows = 0 | ||||||||||||
for i in range(start, end): | ||||||||||||
agent_run = self.agent_runs[i] | ||||||||||||
# Highlight selected item | ||||||||||||
prefix = "→ " if i == self.selected_index and not self.show_action_menu else " " | ||||||||||||
|
||||||||||||
status = self._format_status(agent_run.get("status", "Unknown"), agent_run) | ||||||||||||
created = self._format_date(agent_run.get("created_at", "Unknown")) | ||||||||||||
summary = agent_run.get("summary", "No summary") or "No summary" | ||||||||||||
|
||||||||||||
# No need to truncate summary as much since we removed the URL column | ||||||||||||
if len(summary) > 60: | ||||||||||||
summary = summary[:57] + "..." | ||||||||||||
|
||||||||||||
# Color coding: indigo blue for selected, darker gray for others (but keep status colors) | ||||||||||||
if i == self.selected_index and not self.show_action_menu: | ||||||||||||
# Blue timestamp and summary for selected row, but preserve status colors | ||||||||||||
line = f"\033[34m{prefix}{created:<10}\033[0m {status} \033[34m{summary}\033[0m" | ||||||||||||
else: | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic bug: Padding logic pads too many lines when list is empty
Suggested change
|
||||||||||||
# Gray text for non-selected rows, but preserve status colors | ||||||||||||
line = f"\033[90m{prefix}{created:<10}\033[0m {status} \033[90m{summary}\033[0m" | ||||||||||||
|
||||||||||||
print(line) | ||||||||||||
printed_rows += 1 | ||||||||||||
|
||||||||||||
# Show action menu right below the selected row if it's expanded | ||||||||||||
if i == self.selected_index and self.show_action_menu: | ||||||||||||
self._display_inline_action_menu(agent_run) | ||||||||||||
|
||||||||||||
# If fewer than needed to reach 10 lines, pad blank lines | ||||||||||||
total_printed = printed_rows + menu_lines | ||||||||||||
if total_printed < 10: | ||||||||||||
self._pad_to_lines(total_printed) | ||||||||||||
|
||||||||||||
def _display_new_tab(self): | ||||||||||||
"""Display the new agent creation interface.""" | ||||||||||||
print("Create new background agent (Claude Code):") | ||||||||||||
|
@@ -249,6 +278,9 @@ def _display_new_tab(self): | |||||||||||
print(border_style + "└" + "─" * (box_width - 2) + "┘" + reset) | ||||||||||||
print() | ||||||||||||
|
||||||||||||
# The new tab main content area should be a fixed 10 lines | ||||||||||||
self._pad_to_lines(6) | ||||||||||||
|
||||||||||||
def _create_background_agent(self, prompt: str): | ||||||||||||
"""Create a background agent run.""" | ||||||||||||
if not self.token or not self.org_id: | ||||||||||||
|
@@ -298,33 +330,36 @@ def _create_background_agent(self, prompt: str): | |||||||||||
|
||||||||||||
def _show_post_creation_menu(self, web_url: str): | ||||||||||||
"""Show menu after successful agent creation.""" | ||||||||||||
print("\nWhat would you like to do next?") | ||||||||||||
print() | ||||||||||||
from codegen.cli.utils.inplace_print import inplace_print | ||||||||||||
|
||||||||||||
print("\nWhat would you like to do next?") | ||||||||||||
options = ["open in web preview", "go to recents"] | ||||||||||||
selected = 0 | ||||||||||||
prev_lines = 0 | ||||||||||||
|
||||||||||||
while True: | ||||||||||||
# Clear previous menu display and move cursor up | ||||||||||||
for i in range(len(options) + 2): | ||||||||||||
print("\033[K") # Clear line | ||||||||||||
print(f"\033[{len(options) + 2}A", end="") # Move cursor up | ||||||||||||
|
||||||||||||
def build_lines(): | ||||||||||||
menu_lines = [] | ||||||||||||
# Options | ||||||||||||
for i, option in enumerate(options): | ||||||||||||
if i == selected: | ||||||||||||
print(f" \033[34m→ {option}\033[0m") | ||||||||||||
menu_lines.append(f" \033[34m→ {option}\033[0m") | ||||||||||||
else: | ||||||||||||
print(f" \033[90m {option}\033[0m") | ||||||||||||
menu_lines.append(f" \033[90m {option}\033[0m") | ||||||||||||
# Hint line last | ||||||||||||
menu_lines.append("\033[90m[Enter] select • [↑↓] navigate • [Esc] back to new tab\033[0m") | ||||||||||||
return menu_lines | ||||||||||||
|
||||||||||||
print("\n\033[90m[Enter] select • [↑↓] navigate • [Esc] back to new tab\033[0m") | ||||||||||||
# Initial render | ||||||||||||
prev_lines = inplace_print(build_lines(), prev_lines) | ||||||||||||
|
||||||||||||
# Get input | ||||||||||||
while True: | ||||||||||||
key = self._get_char() | ||||||||||||
|
||||||||||||
if key == "\x1b[A" or key.lower() == "w": # Up arrow or W | ||||||||||||
selected = max(0, selected - 1) | ||||||||||||
selected = (selected - 1) % len(options) | ||||||||||||
prev_lines = inplace_print(build_lines(), prev_lines) | ||||||||||||
elif key == "\x1b[B" or key.lower() == "s": # Down arrow or S | ||||||||||||
selected = min(len(options) - 1, selected + 1) | ||||||||||||
selected = (selected + 1) % len(options) | ||||||||||||
prev_lines = inplace_print(build_lines(), prev_lines) | ||||||||||||
elif key == "\r" or key == "\n": # Enter - select option | ||||||||||||
if selected == 0: # open in web preview | ||||||||||||
try: | ||||||||||||
|
@@ -340,6 +375,8 @@ def _show_post_creation_menu(self, web_url: str): | |||||||||||
self._load_agent_runs() # Refresh the data | ||||||||||||
break | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic bug: Selecting "go to recents" doesn’t switch to the recents tab
Suggested change
|
||||||||||||
elif key == "\x1b": # Esc - back to new tab | ||||||||||||
self.current_tab = 1 # 'new' tab index | ||||||||||||
self.input_mode = True | ||||||||||||
break | ||||||||||||
|
||||||||||||
def _display_web_tab(self): | ||||||||||||
|
@@ -353,6 +390,8 @@ def _display_web_tab(self): | |||||||||||
print(f" \033[34m→ Open Web ({display_url})\033[0m") | ||||||||||||
print() | ||||||||||||
print("Press Enter to open the web interface in your browser.") | ||||||||||||
# The web tab main content area should be a fixed 10 lines | ||||||||||||
self._pad_to_lines(5) | ||||||||||||
|
||||||||||||
def _pull_agent_branch(self, agent_id: str): | ||||||||||||
"""Pull the PR branch for an agent run locally.""" | ||||||||||||
|
@@ -386,6 +425,11 @@ def _display_content(self): | |||||||||||
elif self.current_tab == 2: # web | ||||||||||||
self._display_web_tab() | ||||||||||||
|
||||||||||||
def _pad_to_lines(self, lines_printed: int, target: int = 10): | ||||||||||||
"""Pad the main content area with blank lines to reach a fixed height.""" | ||||||||||||
for _ in range(max(0, target - lines_printed)): | ||||||||||||
print() | ||||||||||||
|
||||||||||||
def _display_inline_action_menu(self, agent_run: dict): | ||||||||||||
"""Display action menu inline below the selected row.""" | ||||||||||||
agent_id = agent_run.get("id", "unknown") | ||||||||||||
|
@@ -432,8 +476,15 @@ def _get_char(self): | |||||||||||
|
||||||||||||
# Handle escape sequences (arrow keys) | ||||||||||||
if ch == "\x1b": # ESC | ||||||||||||
# Peek for additional bytes to distinguish bare ESC vs sequences | ||||||||||||
ready, _, _ = select.select([sys.stdin], [], [], 0.03) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic bug: Incomplete escape sequence returns "ESC[" which no handler recognizes
Suggested change
|
||||||||||||
if not ready: | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Robustness:
Suggested change
|
||||||||||||
return "\x1b" # bare Esc | ||||||||||||
ch2 = sys.stdin.read(1) | ||||||||||||
if ch2 == "[": | ||||||||||||
ready2, _, _ = select.select([sys.stdin], [], [], 0.03) | ||||||||||||
if not ready2: | ||||||||||||
return "\x1b[" | ||||||||||||
ch3 = sys.stdin.read(1) | ||||||||||||
return f"\x1b[{ch3}" | ||||||||||||
else: | ||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,27 @@ | ||||||||||||||||||
import sys | ||||||||||||||||||
from typing import Iterable | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
def inplace_print(lines: Iterable[str], prev_lines_rendered: int) -> int: | ||||||||||||||||||
"""Redraw a small block of text in-place without scrolling. | ||||||||||||||||||
Args: | ||||||||||||||||||
lines: The lines to render (each should NOT include a trailing newline) | ||||||||||||||||||
prev_lines_rendered: How many lines were rendered in the previous frame. Pass 0 on first call. | ||||||||||||||||||
Returns: | ||||||||||||||||||
The number of lines rendered this call. Use as prev_lines_rendered on the next call. | ||||||||||||||||||
""" | ||||||||||||||||||
# Move cursor up to the start of the previous block (if any) | ||||||||||||||||||
if prev_lines_rendered > 0: | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Terminal control bug: Using CSI F moves to column 1; use Cursor Up (A) to preserve column
Suggested change
|
||||||||||||||||||
sys.stdout.write(f"\x1b[{prev_lines_rendered}F") # Cursor up N lines | ||||||||||||||||||
|
||||||||||||||||||
# Rewrite each line, clearing it first to avoid remnants from previous content | ||||||||||||||||||
count = 0 | ||||||||||||||||||
for line in lines: | ||||||||||||||||||
sys.stdout.write("\x1b[2K\r") # Clear entire line and return carriage | ||||||||||||||||||
sys.stdout.write(f"{line}\n") | ||||||||||||||||||
count += 1 | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Logic bug: inplace_print leaves stale lines when new frame has fewer lines
Suggested change
|
||||||||||||||||||
|
||||||||||||||||||
sys.stdout.flush() | ||||||||||||||||||
return count |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic bug: Off-by-one highlighting when list is scrolled
prefix
comparesi
againstself.selected_index
, buti
is the sliced-window index whileself.selected_index
is absolute, so the selected row loses its arrow whenstart > 0
.