-
Notifications
You must be signed in to change notification settings - Fork 63
fix: constant tab height, [new] escape key, post create buffer #1210
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: | ||||||||||||
|
Contributor
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 | ||||||||||||
|
Contributor
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) | ||||||||||||
|
Contributor
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: | ||||||||||||
|
Contributor
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: | ||||||||||||||||||
|
Contributor
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 | ||||||||||||||||||
|
Contributor
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
prefixcomparesiagainstself.selected_index, butiis the sliced-window index whileself.selected_indexis absolute, so the selected row loses its arrow whenstart > 0.