|  | 
| 6 | 6 | import tty | 
| 7 | 7 | from datetime import datetime | 
| 8 | 8 | from typing import Any | 
|  | 9 | +import threading | 
|  | 10 | +import time | 
| 9 | 11 | 
 | 
| 10 | 12 | import requests | 
| 11 | 13 | import typer | 
| @@ -35,14 +37,66 @@ def __init__(self): | 
| 35 | 37 |         self.tabs = ["recents", "new", "web"] | 
| 36 | 38 |         self.current_tab = 0 | 
| 37 | 39 | 
 | 
|  | 40 | +        # Refresh state | 
|  | 41 | +        self.is_refreshing = False | 
|  | 42 | +        self._auto_refresh_interval_seconds = 10 | 
|  | 43 | +        self._refresh_lock = threading.Lock() | 
|  | 44 | + | 
| 38 | 45 |         # New tab state | 
| 39 | 46 |         self.prompt_input = "" | 
|  | 47 | + | 
| 40 | 48 |         self.cursor_position = 0 | 
| 41 | 49 |         self.input_mode = False  # When true, we're typing in the input box | 
| 42 | 50 | 
 | 
| 43 | 51 |         # Set up signal handler for Ctrl+C | 
| 44 | 52 |         signal.signal(signal.SIGINT, self._signal_handler) | 
| 45 | 53 | 
 | 
|  | 54 | +        # Start background auto-refresh thread (daemon) | 
|  | 55 | +        self._auto_refresh_thread = threading.Thread(target=self._auto_refresh_loop, daemon=True) | 
|  | 56 | +        self._auto_refresh_thread.start() | 
|  | 57 | + | 
|  | 58 | +    def _auto_refresh_loop(self): | 
|  | 59 | +        """Background loop to auto-refresh recents tab every interval.""" | 
|  | 60 | +        while True: | 
|  | 61 | +            # Sleep first so we don't immediately spam a refresh on start | 
|  | 62 | +            time.sleep(self._auto_refresh_interval_seconds) | 
|  | 63 | + | 
|  | 64 | +            if not self.running: | 
|  | 65 | +                break | 
|  | 66 | + | 
|  | 67 | +            # Only refresh when on recents tab and not currently refreshing | 
|  | 68 | +            if self.current_tab == 0 and not self.is_refreshing: | 
|  | 69 | +                # Try background refresh; if lock is busy, skip this tick | 
|  | 70 | +                acquired = self._refresh_lock.acquire(blocking=False) | 
|  | 71 | +                if not acquired: | 
|  | 72 | +                    continue | 
|  | 73 | +                try: | 
|  | 74 | +                    # Double-check state after acquiring lock | 
|  | 75 | +                    if self.running and self.current_tab == 0 and not self.is_refreshing: | 
|  | 76 | +                        self._background_refresh() | 
|  | 77 | +                finally: | 
|  | 78 | +                    self._refresh_lock.release() | 
|  | 79 | + | 
|  | 80 | +    def _background_refresh(self): | 
|  | 81 | +        """Refresh data without disrupting selection/menu state; redraw if still on recents.""" | 
|  | 82 | +        self.is_refreshing = True | 
|  | 83 | +        # Do not redraw immediately to reduce flicker; header shows indicator on next paint | 
|  | 84 | + | 
|  | 85 | +        previous_index = self.selected_index | 
|  | 86 | +        try: | 
|  | 87 | +            if self._load_agent_runs(): | 
|  | 88 | +                # Preserve selection but clamp to new list bounds | 
|  | 89 | +                if self.agent_runs: | 
|  | 90 | +                    self.selected_index = max(0, min(previous_index, len(self.agent_runs) - 1)) | 
|  | 91 | +                else: | 
|  | 92 | +                    self.selected_index = 0 | 
|  | 93 | +        finally: | 
|  | 94 | +            self.is_refreshing = False | 
|  | 95 | + | 
|  | 96 | +        # Redraw only if still on recents and app running | 
|  | 97 | +        if self.running and self.current_tab == 0: | 
|  | 98 | +            self._clear_and_redraw() | 
|  | 99 | + | 
| 46 | 100 |     def _get_webapp_domain(self) -> str: | 
| 47 | 101 |         """Get the webapp domain based on environment.""" | 
| 48 | 102 |         return get_domain() | 
| @@ -72,6 +126,10 @@ def _format_status_line(self, left_text: str) -> str: | 
| 72 | 126 |         instructions_line = f"\033[90m{left_text}\033[0m" | 
| 73 | 127 |         org_line = f"{purple_color}• {org_name}{reset_color}" | 
| 74 | 128 | 
 | 
|  | 129 | +        # Append a subtle refresh indicator when a refresh is in progress | 
|  | 130 | +        if getattr(self, "is_refreshing", False): | 
|  | 131 | +            org_line += "  \033[90m■ Refreshing…\033[0m" | 
|  | 132 | + | 
| 75 | 133 |         return f"{instructions_line}\n{org_line}" | 
| 76 | 134 | 
 | 
| 77 | 135 |     def _load_agent_runs(self) -> bool: | 
| @@ -685,9 +743,17 @@ def _open_agent_details(self): | 
| 685 | 743 | 
 | 
| 686 | 744 |     def _refresh(self): | 
| 687 | 745 |         """Refresh the agent runs list.""" | 
|  | 746 | +        # Indicate refresh and redraw immediately so the user sees it | 
|  | 747 | +        self.is_refreshing = True | 
|  | 748 | +        self._clear_and_redraw() | 
|  | 749 | + | 
| 688 | 750 |         if self._load_agent_runs(): | 
| 689 | 751 |             self.selected_index = 0  # Reset selection | 
| 690 | 752 | 
 | 
|  | 753 | +        # Clear refresh indicator and redraw with updated data | 
|  | 754 | +        self.is_refreshing = False | 
|  | 755 | +        self._clear_and_redraw() | 
|  | 756 | + | 
| 691 | 757 |     def _clear_and_redraw(self): | 
| 692 | 758 |         """Clear screen and redraw everything.""" | 
| 693 | 759 |         # Move cursor to top and clear screen from cursor down | 
|  | 
0 commit comments