diff --git a/cli.py b/cli.py index e9019ca4f2f..4d84fe96f73 100755 --- a/cli.py +++ b/cli.py @@ -1244,6 +1244,10 @@ class HermesCLI: self._command_running = False self._command_status = "" + # Background task tracking: {task_id: threading.Thread} + self._background_tasks: Dict[str, threading.Thread] = {} + self._background_task_counter = 0 + def _invalidate(self, min_interval: float = 0.25) -> None: """Throttled UI repaint — prevents terminal blinking on slow/SSH connections.""" import time as _time @@ -2825,6 +2829,8 @@ class HermesCLI: self._reload_mcp() elif cmd_lower.startswith("/rollback"): self._handle_rollback_command(cmd_original) + elif cmd_lower.startswith("/background"): + self._handle_background_command(cmd_original) elif cmd_lower.startswith("/skin"): self._handle_skin_command(cmd_original) else: @@ -2869,6 +2875,113 @@ class HermesCLI: return True + def _handle_background_command(self, cmd: str): + """Handle /background — run a prompt in a separate background session. + + Spawns a new AIAgent in a background thread with its own session. + When it completes, prints the result to the CLI without modifying + the active session's conversation history. + """ + parts = cmd.strip().split(maxsplit=1) + if len(parts) < 2 or not parts[1].strip(): + _cprint(" Usage: /background ") + _cprint(" Example: /background Summarize the top HN stories today") + _cprint(" The task runs in a separate session and results display here when done.") + return + + prompt = parts[1].strip() + self._background_task_counter += 1 + task_num = self._background_task_counter + task_id = f"bg_{datetime.now().strftime('%H%M%S')}_{uuid.uuid4().hex[:6]}" + + # Make sure we have valid credentials + if not self._ensure_runtime_credentials(): + _cprint(" (>_<) Cannot start background task: no valid credentials.") + return + + _cprint(f" 🔄 Background task #{task_num} started: \"{prompt[:60]}{'...' if len(prompt) > 60 else ''}\"") + _cprint(f" Task ID: {task_id}") + _cprint(f" You can continue chatting — results will appear when done.\n") + + def run_background(): + try: + bg_agent = AIAgent( + model=self.model, + api_key=self.api_key, + base_url=self.base_url, + provider=self.provider, + api_mode=self.api_mode, + max_iterations=self.max_turns, + enabled_toolsets=self.enabled_toolsets, + quiet_mode=True, + verbose_logging=False, + session_id=task_id, + platform="cli", + session_db=self._session_db, + reasoning_config=self.reasoning_config, + providers_allowed=self._providers_only, + providers_ignored=self._providers_ignore, + providers_order=self._providers_order, + provider_sort=self._provider_sort, + provider_require_parameters=self._provider_require_params, + provider_data_collection=self._provider_data_collection, + fallback_model=self._fallback_model, + ) + + result = bg_agent.run_conversation( + user_message=prompt, + task_id=task_id, + ) + + response = result.get("final_response", "") if result else "" + if not response and result and result.get("error"): + response = f"Error: {result['error']}" + + # Display result in the CLI (thread-safe via patch_stdout) + print() + _cprint(f"{_GOLD}{'─' * 40}{_RST}") + _cprint(f" ✅ Background task #{task_num} complete") + _cprint(f" Prompt: \"{prompt[:60]}{'...' if len(prompt) > 60 else ''}\"") + _cprint(f"{_GOLD}{'─' * 40}{_RST}") + if response: + try: + from hermes_cli.skin_engine import get_active_skin + _skin = get_active_skin() + label = _skin.get_branding("response_label", "⚕ Hermes") + _resp_color = _skin.get_color("response_border", "#CD7F32") + except Exception: + label = "⚕ Hermes" + _resp_color = "#CD7F32" + + _chat_console = ChatConsole() + _chat_console.print(Panel( + response, + title=f"[bold]{label} (background #{task_num})[/bold]", + title_align="left", + border_style=_resp_color, + box=rich_box.HORIZONTALS, + padding=(1, 2), + )) + else: + _cprint(" (No response generated)") + + # Play bell if enabled + if self.bell_on_complete: + sys.stdout.write("\a") + sys.stdout.flush() + + except Exception as e: + print() + _cprint(f" ❌ Background task #{task_num} failed: {e}") + finally: + self._background_tasks.pop(task_id, None) + if self._app: + self._invalidate(min_interval=0) + + thread = threading.Thread(target=run_background, daemon=True, name=f"bg-task-{task_id}") + self._background_tasks[task_id] = thread + thread.start() + def _handle_skin_command(self, cmd: str): """Handle /skin [name] — show or change the display skin.""" try: