diff --git a/agent/side_question.py b/agent/side_question.py new file mode 100644 index 0000000000..79bc7fce3f --- /dev/null +++ b/agent/side_question.py @@ -0,0 +1,159 @@ +"""Context-aware side questions (``/btw``). + +``/btw `` answers a quick question ABOUT the current conversation +without interrupting it: a one-shot auxiliary LLM call receives a read-only +transcript snapshot plus the question, and the answer is delivered alongside +the live session. The live conversation history is never touched — no +synthetic turns, no role-alternation risk, no prompt-cache invalidation. + +This is deliberately different from ``/bg`` (``/background``'s successor), +which spawns a fresh, contextless agent session for independent work. + +Model selection rides the standard auxiliary plumbing +(:func:`agent.auxiliary_client.call_llm` via :func:`agent.oneshot.run_oneshot`): +pass ``main_runtime`` to inherit the live session's provider/model; users can +override per-task via ``auxiliary.side_question.provider`` / ``.model`` in +config.yaml. +""" + +import logging +from typing import Any, Dict, List, Optional + +logger = logging.getLogger(__name__) + +# Free-form auxiliary task name — resolvable via auxiliary.side_question.* in +# config.yaml, falls back main-model-first like every other aux task. +SIDE_QUESTION_TASK = "side_question" + +# Per-message and total character budgets for the transcript snapshot. The +# snapshot is rendered to plain text (never replayed as raw provider messages) +# so assistant tool_calls entries can't trip provider-side validation on a +# tools-less one-shot request. +_PER_MESSAGE_CHAR_CAP = 2000 +_TRANSCRIPT_CHAR_BUDGET = 24000 + +_INSTRUCTIONS = ( + "You are the same AI assistant that is currently working inside the " + "conversation transcribed below. The user has asked a quick SIDE question " + "with /btw while the main work continues.\n" + "Rules:\n" + "- Answer ONLY the side question. Do not continue, redo, or critique the " + "main task.\n" + "- Use the transcript as your primary context; it is a snapshot and may " + "not include the very latest activity.\n" + "- If the transcript does not contain enough information to answer, say " + "so plainly instead of guessing.\n" + "- Be concise and direct." +) + + +def _msg_text(msg: Dict[str, Any]) -> str: + """Best-effort plain text from a provider-format message content field.""" + content = msg.get("content") + if isinstance(content, str): + return content + if isinstance(content, list): + parts = [] + for block in content: + if isinstance(block, dict): + text = block.get("text") + if isinstance(text, str): + parts.append(text) + return "\n".join(parts) + return "" + + +def render_history_for_side_question( + history: Optional[List[Dict[str, Any]]], + char_budget: int = _TRANSCRIPT_CHAR_BUDGET, +) -> str: + """Render a conversation snapshot as a plain-text transcript. + + Keeps the most recent messages that fit ``char_budget``, newest-biased + (older context is what gets dropped). Tool calls are summarized by name; + tool results are included truncated so "what did that command output" + style questions remain answerable. + """ + lines: List[str] = [] + for msg in history or []: + if not isinstance(msg, dict): + continue + role = msg.get("role") + text = _msg_text(msg).strip() + if role == "system": + continue # system prompt is not needed and can be huge + if role == "user": + if text: + lines.append(f"USER: {text[:_PER_MESSAGE_CHAR_CAP]}") + elif role == "assistant": + tool_calls = msg.get("tool_calls") or [] + if tool_calls: + names = [ + (tc.get("function") or {}).get("name", "?") + for tc in tool_calls + if isinstance(tc, dict) + ] + lines.append(f"ASSISTANT [called tools: {', '.join(names)}]") + if text: + lines.append(f"ASSISTANT: {text[:_PER_MESSAGE_CHAR_CAP]}") + elif role == "tool": + if text: + lines.append(f"TOOL RESULT: {text[:_PER_MESSAGE_CHAR_CAP]}") + + # Newest-biased fit: walk from the end until the budget is spent. + kept: List[str] = [] + used = 0 + for line in reversed(lines): + cost = len(line) + 1 + if used + cost > char_budget and kept: + break + kept.append(line) + used += cost + kept.reverse() + + if not kept: + return "(no prior conversation)" + prefix = "" + if len(kept) < len(lines): + prefix = "[...older conversation omitted...]\n" + return prefix + "\n".join(kept) + + +def answer_side_question( + question: str, + history: Optional[List[Dict[str, Any]]], + *, + main_runtime: Optional[Dict[str, Any]] = None, + max_tokens: int = 2048, + temperature: Optional[float] = 0.3, + timeout: float = 180.0, +) -> str: + """Answer ``question`` against a snapshot of ``history``. + + Returns the model's text answer. Raises whatever the auxiliary client + raises (RuntimeError on no provider, etc.) — callers surface the error + on their own UI. + """ + from agent.oneshot import run_oneshot + + question = (question or "").strip() + if not question: + raise ValueError("answer_side_question requires a non-empty question") + + transcript = render_history_for_side_question(history) + user_input = ( + "Conversation transcript (snapshot):\n" + "-----\n" + f"{transcript}\n" + "-----\n\n" + f"Side question: {question}" + ) + return run_oneshot( + instructions=_INSTRUCTIONS, + user_input=user_input, + task=SIDE_QUESTION_TASK, + max_tokens=max_tokens, + temperature=temperature, + timeout=timeout, + main_runtime=main_runtime, + ) diff --git a/apps/desktop/src/lib/desktop-slash-commands.test.ts b/apps/desktop/src/lib/desktop-slash-commands.test.ts index 3001b103aa..cd39fcce9d 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.test.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.test.ts @@ -49,7 +49,8 @@ const REGISTRY_CATALOG = registryCatalog( '/agents': null, '/steer': 'text', '/stop': null, - '/background': 'text', + '/bg': 'text', + '/btw': 'text', '/debug': null, '/goal': 'mixed', '/personality': 'options', @@ -61,7 +62,7 @@ const REGISTRY_CATALOG = registryCatalog( '/loop': 'mixed', '/lcm': 'text' }, - { '/tasks': '/agents', '/bg': '/background', '/q': '/queue', '/proactive': '/loop' } + { '/tasks': '/agents', '/background': '/bg', '/q': '/queue', '/proactive': '/loop' } ) describe('desktop slash command curation', () => { @@ -93,9 +94,11 @@ describe('desktop slash command curation', () => { it('treats registry and plugin commands as exec when the catalog says so', () => { expect(resolveDesktopCommand('/refine')?.argumentMode).toBe('text') expect(isDesktopSlashSuggestion('/refine')).toBe(true) - expect(isDesktopSlashSuggestion('/bg')).toBe(false) - expect(isDesktopSlashCommand('/background')).toBe(true) - expect(desktopSlashCommandArgumentMode('/background')).toBe('text') + expect(isDesktopSlashSuggestion('/background')).toBe(false) + expect(isDesktopSlashCommand('/bg')).toBe(true) + expect(desktopSlashCommandArgumentMode('/bg')).toBe('text') + expect(isDesktopSlashCommand('/btw')).toBe(true) + expect(desktopSlashCommandArgumentMode('/btw')).toBe('text') expect(resolveDesktopCommand('/lcm')?.surface).toEqual({ kind: 'exec' }) expect(desktopSlashCommandArgumentMode('/lcm')).toBe('text') }) @@ -230,7 +233,8 @@ describe('desktop slash command curation', () => { it('still routes commands without dedicated RPCs through exec()', () => { const execNames = [ - '/background', + '/bg', + '/btw', '/debug', '/goal', '/personality', diff --git a/cli.py b/cli.py index 732baaf387..f85f8405f2 100644 --- a/cli.py +++ b/cli.py @@ -6396,7 +6396,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): except Exception: pass - # Count live /background tasks. The dict entry is removed in the + # Count live /bg tasks. The dict entry is removed in the # task thread's finally block, so len() reflects truly-running tasks. # len() on a CPython dict is atomic; safe to read without a lock. try: @@ -11947,20 +11947,20 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): def _should_handle_background_command_inline( self, text: str, has_images: bool = False ) -> bool: - """Return True when /background should be dispatched while the agent runs. + """Return True when /bg or /btw should be dispatched while the agent runs. - Same queue problem /steer had. ``/background`` (``/bg``, ``/btw``) - exists to start independent work *without* waiting for the current - turn, but a slash command typed while the agent is busy goes into - ``_pending_input``, and ``process_loop`` is blocked inside - ``self.chat()`` for the whole run. The background task therefore only - starts once the foreground turn has finished, which is the one moment - it was not needed. + Same queue problem /steer had. ``/bg`` exists to start independent + work *without* waiting for the current turn, and ``/btw`` exists to + answer a side question about the in-flight conversation, but a slash + command typed while the agent is busy goes into ``_pending_input``, + and ``process_loop`` is blocked inside ``self.chat()`` for the whole + run. The side task would therefore only start once the foreground + turn has finished, which is the one moment it was not needed. - The command's own ``CommandDef`` already declares + Both commands' ``CommandDef`` entries already declare ``busy_policy="dispatch"``; the gateway honours that, the classic CLI never consulted it. Dispatching inline on the UI thread starts the - background session immediately and leaves the foreground turn running + side session immediately and leaves the foreground turn running untouched: no interrupt, no steer. """ if not text or has_images or not _looks_like_slash_command(text): @@ -11971,7 +11971,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): from hermes_cli.commands import resolve_command base = text.split(None, 1)[0].lower().lstrip('/') cmd = resolve_command(base) - return bool(cmd and cmd.name == "background") + return bool(cmd and cmd.name in ("bg", "btw")) except Exception: return False @@ -12575,8 +12575,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self._handle_agents_command() elif canonical == "journey": self._handle_journey_command(cmd_original) - elif canonical == "background": + elif canonical == "bg": self._handle_background_command(cmd_original) + elif canonical == "btw": + self._handle_btw_command(cmd_original) elif canonical == "queue": # Extract prompt after "/queue " or "/q " parts = cmd_original.split(None, 1) @@ -18213,12 +18215,12 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): event.app.invalidate() return - # Same treatment for /background (/bg, /btw) while the agent is - # running. Queuing it defeats the entire point of the command: - # process_loop is blocked inside self.chat(), so the background - # task would only start once the foreground turn it was meant to - # run alongside has already finished (#75221). The foreground - # turn is left alone: no interrupt, no steer. + # Same treatment for /bg and /btw while the agent is + # running. Queuing them defeats the entire point of the + # commands: process_loop is blocked inside self.chat(), so the + # side task would only start once the foreground turn it was + # meant to run alongside has already finished (#75221). The + # foreground turn is left alone: no interrupt, no steer. if self._should_handle_background_command_inline( text, has_images=has_images ): diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index d05c5265e7..fede9adf49 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -6297,7 +6297,7 @@ class BasePlatformAdapter(ABC): return # Other bypass commands (/approve, /deny, /status, - # /background, /restart) just need direct dispatch — they + # /bg, /restart) just need direct dispatch — they # don't cancel the running task. logger.debug( "[%s] Command '/%s' bypassing active-session guard for %s", diff --git a/gateway/platforms/yuanbao.py b/gateway/platforms/yuanbao.py index 4e9949e402..d36c177794 100644 --- a/gateway/platforms/yuanbao.py +++ b/gateway/platforms/yuanbao.py @@ -1774,7 +1774,7 @@ class OwnerCommandMiddleware(InboundMiddleware): # Slash command allowlist that bot owner can execute in group without @Bot ALLOWLIST: frozenset = frozenset({ "/new", "/reset", "/retry", "/undo", "/stop", - "/approve", "/deny", "/background", "/bg", + "/approve", "/deny", "/bg", "/btw", "/queue", "/q", }) diff --git a/gateway/relay/command_manifest.py b/gateway/relay/command_manifest.py index 36d7a8fcd7..2a906311e9 100644 --- a/gateway/relay/command_manifest.py +++ b/gateway/relay/command_manifest.py @@ -138,8 +138,13 @@ def build_relay_command_manifest() -> List[Dict[str, Any]]: "options": [_opt("text", "The prompt to queue")], }, { - "name": "background", - "description": "Run a prompt in the background", + "name": "bg", + "description": "Run a prompt in a separate background session", "options": [_opt("text", "The prompt to run")], }, + { + "name": "btw", + "description": "Ask a side question about the current conversation", + "options": [_opt("text", "The question to answer")], + }, ] diff --git a/gateway/run.py b/gateway/run.py index 8ce292a548..08c379fbc9 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -17201,7 +17201,8 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew "deny": self._handle_deny_command, "pause": self._handle_pause_command, "agents": self._handle_agents_command, - "background": self._handle_background_command, + "bg": self._handle_background_command, + "btw": self._handle_btw_command, "kanban": self._handle_kanban_command, "subgoal": self._handle_subgoal_command, "heartbeat": self._handle_heartbeat_command, @@ -18587,9 +18588,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if canonical == "diff": return await self._handle_diff_command(event) - if canonical == "background": + if canonical == "bg": return await self._handle_background_command(event) + if canonical == "btw": + return await self._handle_btw_command(event) + if canonical == "queue": queue_payload = event.get_command_args().strip() if not queue_payload: diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 88078335f7..f8b2d854f4 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -3609,7 +3609,7 @@ class GatewaySlashCommandsMixin: return f"```diff\n{diff}{note}\n```" async def _handle_background_command(self, event: MessageEvent) -> str: - """Handle /background — run a prompt in a separate background session. + """Handle /bg — run a prompt in a separate background session. Spawns a new AIAgent in a background thread with its own session. When it completes, sends the result back to the same chat without @@ -3645,6 +3645,81 @@ class GatewaySlashCommandsMixin: preview = prompt[:60] + ("..." if len(prompt) > 60 else "") return t("gateway.background.started", preview=preview, task_id=task_id) + async def _handle_btw_command(self, event: MessageEvent) -> str: + """Handle /btw — answer a side question about this conversation. + + Snapshots the session transcript and answers the question with a + one-shot auxiliary LLM call (main model by default) — the live + session's history is never touched, so role alternation and the + prompt cache stay intact and the current turn keeps running. The + answer is delivered to the chat when ready. + + Deliberately different from /bg, which spawns a fresh contextless + agent session for independent work. + """ + question = event.get_command_args().strip() + if not question: + return t("gateway.btw.usage") + + source = event.source + session_entry = await self.async_session_store.get_or_create_session(source) + history = await self.async_session_store.load_transcript(session_entry.session_id) + if not history: + return t("gateway.btw.no_history") + + try: + model, runtime_kwargs = self._resolve_session_agent_runtime( + source=source, + ) + except Exception: + model, runtime_kwargs = None, {} + if not runtime_kwargs.get("api_key"): + return t("gateway.btw.no_provider") + + main_runtime = { + "model": model, + "provider": runtime_kwargs.get("provider"), + "base_url": runtime_kwargs.get("base_url"), + "api_key": runtime_kwargs.get("api_key"), + "api_mode": runtime_kwargs.get("api_mode"), + } + history_snapshot = list(history) + event_message_id = self._reply_anchor_for_event(event) + _thread_metadata = self._thread_metadata_for_source(source, event_message_id) + adapter = self._adapter_for_source(source) + preview = question[:60] + ("..." if len(question) > 60 else "") + + async def _run_side_question() -> None: + from agent.side_question import answer_side_question + try: + answer = await asyncio.to_thread( + answer_side_question, + question, + history_snapshot, + main_runtime=main_runtime, + ) + except Exception as e: + logger.warning("/btw side question failed: %s", e) + if adapter is not None: + await adapter.send( + source.chat_id, + t("gateway.btw.failed", preview=preview, error=str(e)), + metadata=_thread_metadata, + ) + return + if adapter is not None: + await adapter.send( + source.chat_id, + t("gateway.btw.answer", preview=preview, answer=answer or ""), + metadata=_thread_metadata, + ) + + _task = asyncio.create_task(_run_side_question()) + self._background_tasks.add(_task) + _task.add_done_callback(self._background_tasks.discard) + + return t("gateway.btw.started", preview=preview) + def _save_gateway_config_key(self, key_path: str, value) -> bool: """Save a dot-separated key to config.yaml (shared by /reasoning, /fast and their interactive pickers).""" diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index 3ee64cd682..9d41538fd6 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -2203,7 +2203,7 @@ class CLICommandsMixin: save_config_value(f"{subsystem}.write_approval", bool(enabled)) def _handle_background_command(self, cmd: str): - """Handle /background — run a prompt in a separate background session. + """Handle /bg — 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 @@ -2212,8 +2212,9 @@ class CLICommandsMixin: from cli import AIAgent, ChatConsole, _accent_hex, _cprint, _maybe_remap_for_light_mode, _render_final_assistant_content, set_approval_callback, set_secret_capture_callback, set_sudo_password_callback 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(" Usage: /bg ") + _cprint(" Example: /bg Summarize the top HN stories today") + _cprint(" (For a side question about this conversation, use /btw .)") _cprint(" The task runs in a separate session and results display here when done.") return @@ -2357,6 +2358,98 @@ class CLICommandsMixin: self._background_tasks[task_id] = thread thread.start() + def _handle_btw_command(self, cmd: str): + """Handle /btw — answer a side question about this conversation. + + Snapshots the live conversation history and asks a one-shot auxiliary + LLM call (same model as the session by default) to answer the question + against that snapshot. The live session is never touched: no history + mutation, no role-alternation risk, no prompt-cache invalidation. The + current turn keeps running; the answer prints when ready. + """ + from cli import ChatConsole, _accent_hex, _cprint + + parts = cmd.strip().split(maxsplit=1) + if len(parts) < 2 or not parts[1].strip(): + _cprint(" Usage: /btw ") + _cprint(" Example: /btw which file was that error in?") + _cprint(" Answers a quick question about this conversation without interrupting it.") + _cprint(" (For an independent background task, use /bg .)") + return + + question = parts[1].strip() + + if not self._ensure_runtime_credentials(): + _cprint(" (>_<) Cannot answer side question: no valid credentials.") + return + + # Snapshot NOW, on the UI thread — the foreground turn keeps appending + # to conversation_history while the worker runs. + history_snapshot = list(self.conversation_history or []) + turn_route = self._resolve_turn_agent_config(question) + main_runtime = { + "model": turn_route["model"], + "provider": turn_route["runtime"].get("provider"), + "base_url": turn_route["runtime"].get("base_url"), + "api_key": turn_route["runtime"].get("api_key"), + "api_mode": turn_route["runtime"].get("api_mode"), + } + + preview = question[:60] + ("..." if len(question) > 60 else "") + _cprint(f" 💬 Side question: \"{preview}\"") + _cprint(" Answering from a snapshot of this conversation — the current work continues.\n") + + def run_side_question(): + try: + from agent.side_question import answer_side_question + answer = answer_side_question( + question, + history_snapshot, + main_runtime=main_runtime, + ) + if self._app: + self._app.invalidate() + time.sleep(0.05) + print() + ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]") + _cprint(f" 💬 /btw: \"{preview}\"") + ChatConsole().print(f"[{_accent_hex()}]{'─' * 40}[/]") + if answer: + from cli import _maybe_remap_for_light_mode, _render_final_assistant_content + try: + from hermes_cli.skin_engine import get_active_skin + _skin = get_active_skin() + label = _skin.get_branding("response_label", "⚕ Hermes") + _resp_color = _maybe_remap_for_light_mode(_skin.get_color("response_border", "#CD7F32")) + _resp_text = _maybe_remap_for_light_mode(_skin.get_color("banner_text", "#FFF8DC")) + except Exception: + label = "⚕ Hermes" + _resp_color = "#CD7F32" + _resp_text = "#FFF8DC" + ChatConsole().print(Panel( + _render_final_assistant_content(answer, mode=self.final_response_markdown), + title=f"[{_resp_color} bold]{label} (btw)[/]", + title_align="left", + border_style=_resp_color, + style=_resp_text, + box=rich_box.HORIZONTALS, + padding=(1, 4), + width=self._scrollback_box_width(), + )) + else: + _cprint(" (No answer generated)") + except Exception as e: + if self._app: + self._app.invalidate() + time.sleep(0.05) + print() + _cprint(f" ❌ /btw failed: {e}") + finally: + if self._app: + self._invalidate(min_interval=0) + + threading.Thread(target=run_side_question, daemon=True, name="btw-side-question").start() + def _handle_bundles_command(self, cmd: str) -> None: """In-session ``/bundles`` — show installed skill bundles. diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 0a5b7677c5..9619072473 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -197,8 +197,10 @@ COMMAND_REGISTRY: list[CommandDef] = [ CommandDef("deny", "Deny a pending dangerous command (optionally with a reason)", "Session", gateway_only=True, args_hint="[all] [reason]", busy_policy="dispatch", desktop="messaging"), - CommandDef("background", "Run a prompt in the background", "Session", - aliases=("bg", "btw"), args_hint="", busy_policy="dispatch"), + CommandDef("bg", "Run a prompt in a separate background session", "Session", + args_hint="", busy_policy="dispatch"), + CommandDef("btw", "Ask a side question about the current conversation without interrupting it", "Session", + args_hint="", busy_policy="dispatch"), CommandDef("agents", "Show active agents and running tasks", "Session", aliases=("tasks",), busy_policy="dispatch"), CommandDef("journey", "Open the learning journey timeline", @@ -507,7 +509,7 @@ HELP_SESSION_SUBGROUPS: dict[str, tuple[str, ...]] = { "compress", "compact", "context", "ctx", "status", ), "Background & Automation": ( - "background", "bg", "btw", "agents", "tasks", "queue", "q", "steer", + "bg", "btw", "agents", "tasks", "queue", "q", "steer", "goal", "subgoal", "heartbeat", "hb", "refine", "loop", "proactive", "moa", "journey", "learning", "memory-graph", ), @@ -719,7 +721,7 @@ def telegram_bot_commands() -> list[tuple[str, str]]: underscores. Aliases are skipped -- Telegram shows one menu entry per canonical command. - Built-in commands that require arguments (e.g. /queue, /steer, /background) + Built-in commands that require arguments (e.g. /queue, /steer, /bg) are **included** because their handlers return usage text when selected without a payload, making them discoverable via autocomplete. @@ -775,7 +777,8 @@ _TELEGRAM_MENU_PRIORITY = ( "deny", "queue", "steer", - "background", + "bg", + "btw", # Lower-priority but still useful operational built-ins. "reasoning", "usage", @@ -1365,7 +1368,9 @@ _SLACK_RESERVED_COMMANDS = frozenset({ # would otherwise get, and the Telegram-parity test fails when a canonical # gets clamped ("reset" was unpinned for exactly that — /new keeps its # native slot, the alias spelling stays reachable via /hermes reset). -_SLACK_PRIORITY_ALIASES = ("btw", "bg") +# (Currently empty: /bg and /btw were promoted from aliases of /background +# to canonical commands, so they win first-pass slots on their own.) +_SLACK_PRIORITY_ALIASES: tuple[str, ...] = () # Canonical commands intentionally NOT given a native Slack slash slot. Slack # caps apps at 50 slash commands and the registry is at that ceiling; rather @@ -1435,7 +1440,7 @@ def slack_native_slashes() -> list[tuple[str, str, str]]: first-class slash and not a ``/hermes `` subcommand. Both canonical names and aliases are included so users can type any - documented form (e.g. ``/background``, ``/bg``, and ``/btw`` all work). + documented form; aliases are surfaced alongside canonical names. Plugin-registered slash commands are included too. Commands whose sanitized name collides with a Slack built-in @@ -1538,7 +1543,7 @@ def slack_subcommand_map() -> dict[str, str]: """Return subcommand -> /command mapping for Slack /hermes handler. Maps both canonical names and aliases so /hermes bg do stuff works - the same as /hermes background do stuff. + the same as /hermes bg do stuff. Plugin-registered slash commands are included so ``/hermes `` routes through the plugin handler. diff --git a/hermes_cli/tips.py b/hermes_cli/tips.py index b2c8a041ab..1723365d3b 100644 --- a/hermes_cli/tips.py +++ b/hermes_cli/tips.py @@ -10,7 +10,7 @@ import random TIPS = [ # --- Slash Commands --- - "/background (alias /bg or /btw) runs a task in a separate session while your current one stays free.", + "/bg runs a task in a separate session while your current one stays free; /btw answers a side question about this conversation without interrupting it.", "/branch forks the current session so you can explore a different direction without losing progress.", "/compress manually compresses conversation context when things get long.", "/rollback lists filesystem checkpoints — restore files the agent modified to any prior state.", diff --git a/locales/af.yaml b/locales/af.yaml index 6e35b39352..a2e6727dd9 100644 --- a/locales/af.yaml +++ b/locales/af.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Opdragte goedgekeur (patroon permanent goedgekeur) ({count} opdragte). Die agent gaan voort..." background: - usage: "Gebruik: /background \nVoorbeeld: /background Som vandag se top HN-stories op\n\nVoer die prompt in 'n aparte sessie uit. Jy kan aanhou gesels — die resultaat verskyn hier wanneer dit klaar is." + usage: "Gebruik: /bg \nVoorbeeld: /bg Som vandag se top HN-stories op\n\nVoer die prompt in 'n aparte sessie uit. Jy kan aanhou gesels — die resultaat verskyn hier wanneer dit klaar is." started: "🔄 Agtergrondtaak begin: \"{preview}\"\nTaak-ID: {task_id}\nJy kan aanhou gesels — resultate verskyn hier wanneer dit klaar is." + btw: + usage: "Gebruik: /btw \nVoorbeeld: /btw in watter lêer was daardie fout?\n\nBeantwoord 'n vinnige newevraag oor hierdie gesprek sonder om dit te onderbreek. Vir 'n onafhanklike agtergrondtaak, gebruik /bg ." + no_history: "Nog geen gesprek nie — stuur jou vraag eerder as 'n gewone boodskap." + no_provider: "❌ Kan nie newevraag beantwoord nie: geen verskaffer-geloofsbriewe opgestel nie." + started: "💬 Newevraag: \"{preview}\"\nBeantwoord vanaf 'n momentopname van hierdie gesprek — die huidige werk gaan voort." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw het misluk: \"{preview}\"\n{error}" + branch: db_unavailable: "Sessie-databasis is nie beskikbaar nie." no_conversation: "Geen gesprek om te vertak nie — stuur eers 'n boodskap." diff --git a/locales/ar.yaml b/locales/ar.yaml index c158ca8e90..bc74699b45 100644 --- a/locales/ar.yaml +++ b/locales/ar.yaml @@ -91,9 +91,17 @@ gateway: always_plural: "✅ تمت الموافقة على الأوامر (النمط مُوافق عليه دائمًا) ({count} أوامر). الوكيل يستأنف..." background: - usage: "الاستخدام: /background \nمثال: /background لخّص أهم قصص HN اليوم\n\nيشغّل الموجِّه في جلسة منفصلة. يمكنك متابعة المحادثة — ستظهر النتيجة هنا عند الانتهاء." + usage: "الاستخدام: /bg \nمثال: /bg لخّص أهم قصص HN اليوم\n\nيشغّل الموجِّه في جلسة منفصلة. يمكنك متابعة المحادثة — ستظهر النتيجة هنا عند الانتهاء." started: "🔄 بدأت مهمة خلفية: \"{preview}\"\nمعرّف المهمة: {task_id}\nيمكنك متابعة المحادثة — ستظهر النتائج عند الانتهاء." + btw: + usage: "الاستخدام: /btw <سؤال>\nمثال: /btw في أي ملف كان ذلك الخطأ؟\n\nيجيب عن سؤال جانبي سريع حول هذه المحادثة دون مقاطعتها. لمهمة خلفية مستقلة استخدم /bg ." + no_history: "لا توجد محادثة بعد — أرسل سؤالك كرسالة عادية بدلاً من ذلك." + no_provider: "❌ تعذّرت الإجابة عن السؤال الجانبي: لم يتم إعداد بيانات اعتماد مزوّد." + started: "💬 سؤال جانبي: \"{preview}\"\nتتم الإجابة من لقطة لهذه المحادثة — يستمر العمل الحالي." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ فشل /btw: \"{preview}\"\n{error}" + branch: db_unavailable: "قاعدة بيانات الجلسات غير متاحة." no_conversation: "لا توجد محادثة للتفريع — أرسل رسالة أولًا." diff --git a/locales/de.yaml b/locales/de.yaml index a4b008811b..d01642d671 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Befehle genehmigt (Muster dauerhaft genehmigt) ({count} Befehle). Der Agent wird fortgesetzt..." background: - usage: "Verwendung: /background \nBeispiel: /background Fasse die Top-HN-Storys von heute zusammen\n\nFührt den Prompt in einer separaten Sitzung aus. Sie können weiter chatten — das Ergebnis erscheint hier, wenn es fertig ist." + usage: "Verwendung: /bg \nBeispiel: /bg Fasse die Top-HN-Storys von heute zusammen\n\nFührt den Prompt in einer separaten Sitzung aus. Sie können weiter chatten — das Ergebnis erscheint hier, wenn es fertig ist." started: "🔄 Hintergrund-Aufgabe gestartet: \"{preview}\"\nAufgaben-ID: {task_id}\nSie können weiter chatten — die Ergebnisse erscheinen hier, wenn sie fertig sind." + btw: + usage: "Verwendung: /btw \nBeispiel: /btw in welcher Datei war dieser Fehler?\n\nBeantwortet eine kurze Nebenfrage zu dieser Konversation, ohne sie zu unterbrechen. Für eine unabhängige Hintergrundaufgabe /bg verwenden." + no_history: "Noch keine Konversation — senden Sie Ihre Frage stattdessen als normale Nachricht." + no_provider: "❌ Nebenfrage kann nicht beantwortet werden: keine Anbieter-Zugangsdaten konfiguriert." + started: "💬 Nebenfrage: \"{preview}\"\nAntwort aus einem Schnappschuss dieser Konversation — die aktuelle Arbeit läuft weiter." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw fehlgeschlagen: \"{preview}\"\n{error}" + branch: db_unavailable: "Sitzungsdatenbank nicht verfügbar." no_conversation: "Keine Konversation zum Verzweigen — senden Sie zuerst eine Nachricht." diff --git a/locales/en.yaml b/locales/en.yaml index 6cd29ad63e..834e5a0547 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -83,9 +83,17 @@ gateway: always_plural: "✅ Commands approved (pattern approved permanently) ({count} commands). The agent is resuming..." background: - usage: "Usage: /background \nExample: /background Summarize the top HN stories today\n\nRuns the prompt in a separate session. You can keep chatting — the result will appear here when done." + usage: "Usage: /bg \nExample: /bg Summarize the top HN stories today\n\nRuns the prompt in a separate session. You can keep chatting — the result will appear here when done." started: "🔄 Background task started: \"{preview}\"\nTask ID: {task_id}\nYou can keep chatting — results will appear when done." + btw: + usage: "Usage: /btw \nExample: /btw which file was that error in?\n\nAnswers a quick side question about this conversation without interrupting it. For an independent background task, use /bg ." + no_history: "No conversation yet — send your question as a normal message instead." + no_provider: "❌ Cannot answer side question: no provider credentials configured." + started: "💬 Side question: \"{preview}\"\nAnswering from a snapshot of this conversation — the current work continues." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw failed: \"{preview}\"\n{error}" + branch: db_unavailable: "Session database not available." no_conversation: "No conversation to branch — send a message first." diff --git a/locales/es.yaml b/locales/es.yaml index 59d7232e80..c90d1c3d04 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Comandos aprobados (patrón aprobado permanentemente) ({count} comandos). El agente se está reanudando..." background: - usage: "Uso: /background \nEjemplo: /background Resume las principales historias de HN de hoy\n\nEjecuta el prompt en una sesión separada. Puedes seguir chateando — el resultado aparecerá aquí cuando termine." + usage: "Uso: /bg \nEjemplo: /bg Resume las principales historias de HN de hoy\n\nEjecuta el prompt en una sesión separada. Puedes seguir chateando — el resultado aparecerá aquí cuando termine." started: "🔄 Tarea en segundo plano iniciada: \"{preview}\"\nID de tarea: {task_id}\nPuedes seguir chateando — los resultados aparecerán aquí cuando terminen." + btw: + usage: "Uso: /btw \nEjemplo: /btw ¿en qué archivo estaba ese error?\n\nResponde una pregunta rápida sobre esta conversación sin interrumpirla. Para una tarea independiente en segundo plano, usa /bg ." + no_history: "Aún no hay conversación — envía tu pregunta como un mensaje normal." + no_provider: "❌ No se puede responder la pregunta: no hay credenciales de proveedor configuradas." + started: "💬 Pregunta aparte: \"{preview}\"\nRespondiendo desde una instantánea de esta conversación — el trabajo actual continúa." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw falló: \"{preview}\"\n{error}" + branch: db_unavailable: "Base de datos de sesiones no disponible." no_conversation: "No hay conversación para ramificar — envía un mensaje primero." diff --git a/locales/fr.yaml b/locales/fr.yaml index 477d3f3146..abd3e284b7 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Commandes approuvées (modèle approuvé de manière permanente) ({count} commandes). L'agent reprend..." background: - usage: "Usage : /background \nExemple : /background Résume les meilleures histoires HN d'aujourd'hui\n\nExécute le prompt dans une session séparée. Vous pouvez continuer à discuter — le résultat apparaîtra ici une fois terminé." + usage: "Usage : /bg \nExemple : /bg Résume les meilleures histoires HN d'aujourd'hui\n\nExécute le prompt dans une session séparée. Vous pouvez continuer à discuter — le résultat apparaîtra ici une fois terminé." started: "🔄 Tâche d'arrière-plan démarrée : « {preview} »\nID de tâche : {task_id}\nVous pouvez continuer à discuter — les résultats apparaîtront ici une fois terminés." + btw: + usage: "Usage : /btw \nExemple : /btw dans quel fichier était cette erreur ?\n\nRépond à une question rapide sur cette conversation sans l'interrompre. Pour une tâche d'arrière-plan indépendante, utilisez /bg ." + no_history: "Pas encore de conversation — envoyez plutôt votre question comme un message normal." + no_provider: "❌ Impossible de répondre : aucun identifiant de fournisseur configuré." + started: "💬 Question annexe : « {preview} »\nRéponse à partir d'un instantané de cette conversation — le travail en cours continue." + answer: "💬 /btw : « {preview} »\n\n{answer}" + failed: "❌ /btw a échoué : « {preview} »\n{error}" + branch: db_unavailable: "Base de données des sessions indisponible." no_conversation: "Aucune conversation à brancher — envoyez d'abord un message." diff --git a/locales/ga.yaml b/locales/ga.yaml index 3734823226..daff6c5b08 100644 --- a/locales/ga.yaml +++ b/locales/ga.yaml @@ -72,9 +72,17 @@ gateway: always_plural: "✅ Orduithe ceadaithe (patrún ceadaithe go buan) ({count} ordú). Tá an gníomhaire ag atosú..." background: - usage: "Úsáid: /background \nSampla: /background Déan achoimre ar phríomhscéalta HN inniu\n\nRitheann an leid i seisiún ar leith. Is féidir leat leanúint leis an gcomhrá — taispeánfar an toradh anseo nuair a bheidh sé críochnaithe." + usage: "Úsáid: /bg \nSampla: /bg Déan achoimre ar phríomhscéalta HN inniu\n\nRitheann an leid i seisiún ar leith. Is féidir leat leanúint leis an gcomhrá — taispeánfar an toradh anseo nuair a bheidh sé críochnaithe." started: "🔄 Tasc cúlra tosaithe: \"{preview}\"\nAitheantas an tasc: {task_id}\nIs féidir leat leanúint leis an gcomhrá — taispeánfar na torthaí nuair a bheidh sé críochnaithe." + btw: + usage: "Úsáid: /btw \nSampla: /btw cén comhad ina raibh an earráid sin?\n\nFreagraíonn ceist thaobhach ghasta faoin gcomhrá seo gan cur isteach air. Le haghaidh tasc cúlra neamhspleách, úsáid /bg ." + no_history: "Níl aon chomhrá ann fós — seol do cheist mar ghnáth-theachtaireacht ina ionad." + no_provider: "❌ Ní féidir an cheist a fhreagairt: níl dintiúir sholáthraí cumraithe." + started: "💬 Ceist thaobhach: \"{preview}\"\nÁ freagairt ó léargas ar an gcomhrá seo — leanann an obair reatha ar aghaidh." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ Theip ar /btw: \"{preview}\"\n{error}" + branch: db_unavailable: "Níl bunachar sonraí na seisiún ar fáil." no_conversation: "Níl aon chomhrá le brainseáil — seol teachtaireacht ar dtús." diff --git a/locales/hu.yaml b/locales/hu.yaml index e529dfe3b4..d69f9da511 100644 --- a/locales/hu.yaml +++ b/locales/hu.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Parancsok jóváhagyva (minta véglegesen jóváhagyva) ({count} parancs). Az ügynök folytatja..." background: - usage: "Használat: /background \nPélda: /background Foglald össze a mai legjobb HN sztorikat\n\nKülön munkamenetben futtatja a promptot. Folytathatod a beszélgetést — az eredmény itt jelenik meg, amint elkészül." + usage: "Használat: /bg \nPélda: /bg Foglald össze a mai legjobb HN sztorikat\n\nKülön munkamenetben futtatja a promptot. Folytathatod a beszélgetést — az eredmény itt jelenik meg, amint elkészül." started: "🔄 Háttérfeladat elindítva: \"{preview}\"\nFeladatazonosító: {task_id}\nFolytathatod a beszélgetést — az eredmények itt jelennek meg, amint elkészülnek." + btw: + usage: "Használat: /btw \nPélda: /btw melyik fájlban volt az a hiba?\n\nGyors mellékkérdésre válaszol erről a beszélgetésről anélkül, hogy megszakítaná. Független háttérfeladathoz használd a /bg parancsot." + no_history: "Még nincs beszélgetés — küldd el a kérdésed normál üzenetként." + no_provider: "❌ A mellékkérdés nem válaszolható meg: nincs szolgáltatói hitelesítő adat beállítva." + started: "💬 Mellékkérdés: \"{preview}\"\nA beszélgetés pillanatképéből válaszolunk — a jelenlegi munka folytatódik." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ A /btw sikertelen: \"{preview}\"\n{error}" + branch: db_unavailable: "A munkamenet-adatbázis nem érhető el." no_conversation: "Nincs elágaztatható beszélgetés — küldj előbb egy üzenetet." diff --git a/locales/it.yaml b/locales/it.yaml index 5ceef400f1..7b2da1c8f6 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Comandi approvati (modello approvato in modo permanente) ({count} comandi). L'agente sta riprendendo..." background: - usage: "Uso: /background \nEsempio: /background Riassumi le principali notizie di HN di oggi\n\nEsegue il prompt in una sessione separata. Puoi continuare a chattare — il risultato apparirà qui al termine." + usage: "Uso: /bg \nEsempio: /bg Riassumi le principali notizie di HN di oggi\n\nEsegue il prompt in una sessione separata. Puoi continuare a chattare — il risultato apparirà qui al termine." started: "🔄 Attività in background avviata: \"{preview}\"\nID attività: {task_id}\nPuoi continuare a chattare — i risultati appariranno al termine." + btw: + usage: "Uso: /btw \nEsempio: /btw in quale file era quell'errore?\n\nRisponde a una rapida domanda su questa conversazione senza interromperla. Per un'attività in background indipendente usa /bg ." + no_history: "Nessuna conversazione ancora — invia la domanda come messaggio normale." + no_provider: "❌ Impossibile rispondere: nessuna credenziale del provider configurata." + started: "💬 Domanda a margine: \"{preview}\"\nRispondo da uno snapshot di questa conversazione — il lavoro attuale continua." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw non riuscito: \"{preview}\"\n{error}" + branch: db_unavailable: "Database delle sessioni non disponibile." no_conversation: "Nessuna conversazione da diramare — invia prima un messaggio." diff --git a/locales/ja.yaml b/locales/ja.yaml index 5d252d17ee..ab7c840b44 100644 --- a/locales/ja.yaml +++ b/locales/ja.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ コマンドを承認しました (パターンを永続的に許可) ({count} 件)。エージェントを再開しています..." background: - usage: "使い方: /background <プロンプト>\n例: /background 今日の HN トップ記事を要約して\n\nプロンプトを別のセッションで実行します。チャットを続けられます — 完了したらここに結果が表示されます。" + usage: "使い方: /bg <プロンプト>\n例: /bg 今日の HN トップ記事を要約して\n\nプロンプトを別のセッションで実行します。チャットを続けられます — 完了したらここに結果が表示されます。" started: "🔄 バックグラウンドタスクを開始しました: 「{preview}」\nタスク ID: {task_id}\nチャットを続けられます — 完了したらここに結果が表示されます。" + btw: + usage: "使い方: /btw <質問>\n例: /btw あのエラーはどのファイルだった?\n\nこの会話について、作業を中断せずにちょっとした質問に答えます。独立したバックグラウンドタスクには /bg <プロンプト> を使ってください。" + no_history: "まだ会話がありません — 質問は通常のメッセージとして送ってください。" + no_provider: "❌ 質問に回答できません: プロバイダーの認証情報が設定されていません。" + started: "💬 サイド質問: 「{preview}」\nこの会話のスナップショットから回答します — 現在の作業は継続します。" + answer: "💬 /btw: 「{preview}」\n\n{answer}" + failed: "❌ /btw が失敗しました: 「{preview}」\n{error}" + branch: db_unavailable: "セッションデータベースは利用できません。" no_conversation: "分岐する会話がありません — まずメッセージを送信してください。" diff --git a/locales/ko.yaml b/locales/ko.yaml index efdbb75469..f038b52a83 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ 명령이 승인되었습니다 (패턴 영구 승인됨) ({count}개). 에이전트가 재개됩니다..." background: - usage: "사용법: /background \n예시: /background 오늘 HN 인기 글을 요약해줘\n\n프롬프트를 별도 세션에서 실행합니다. 계속 대화할 수 있으며, 완료되면 결과가 여기에 표시됩니다." + usage: "사용법: /bg \n예시: /bg 오늘 HN 인기 글을 요약해줘\n\n프롬프트를 별도 세션에서 실행합니다. 계속 대화할 수 있으며, 완료되면 결과가 여기에 표시됩니다." started: "🔄 백그라운드 작업이 시작되었습니다: \"{preview}\"\n작업 ID: {task_id}\n계속 대화하실 수 있습니다 — 완료되면 결과가 여기에 표시됩니다." + btw: + usage: "사용법: /btw <질문>\n예시: /btw 그 오류가 어느 파일에 있었지?\n\n현재 작업을 중단하지 않고 이 대화에 대한 간단한 질문에 답합니다. 독립적인 백그라운드 작업은 /bg 를 사용하세요." + no_history: "아직 대화가 없습니다 — 질문을 일반 메시지로 보내주세요." + no_provider: "❌ 질문에 답할 수 없습니다: 공급자 자격 증명이 설정되지 않았습니다." + started: "💬 사이드 질문: \"{preview}\"\n이 대화의 스냅샷을 바탕으로 답변합니다 — 현재 작업은 계속됩니다." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw 실패: \"{preview}\"\n{error}" + branch: db_unavailable: "세션 데이터베이스를 사용할 수 없습니다." no_conversation: "분기할 대화가 없습니다 — 먼저 메시지를 보내주세요." diff --git a/locales/pt.yaml b/locales/pt.yaml index 3a45b18f4a..4e2f4a34c6 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Comandos aprovados (padrão aprovado permanentemente) ({count} comandos). O agente está a retomar..." background: - usage: "Uso: /background \nExemplo: /background Resume as principais histórias do HN de hoje\n\nExecuta o prompt numa sessão separada. Podes continuar a conversar — o resultado aparecerá aqui quando estiver concluído." + usage: "Uso: /bg \nExemplo: /bg Resume as principais histórias do HN de hoje\n\nExecuta o prompt numa sessão separada. Podes continuar a conversar — o resultado aparecerá aqui quando estiver concluído." started: "🔄 Tarefa em segundo plano iniciada: \"{preview}\"\nID da tarefa: {task_id}\nPodes continuar a conversar — os resultados aparecerão aqui quando estiverem prontos." + btw: + usage: "Uso: /btw \nExemplo: /btw em que ficheiro estava aquele erro?\n\nResponde a uma pergunta rápida sobre esta conversa sem a interromper. Para uma tarefa independente em segundo plano, usa /bg ." + no_history: "Ainda não há conversa — envia a tua pergunta como mensagem normal." + no_provider: "❌ Não é possível responder: nenhuma credencial de fornecedor configurada." + started: "💬 Pergunta à parte: \"{preview}\"\nA responder a partir de um instantâneo desta conversa — o trabalho atual continua." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw falhou: \"{preview}\"\n{error}" + branch: db_unavailable: "Base de dados de sessões indisponível." no_conversation: "Não há conversa para ramificar — envia uma mensagem primeiro." diff --git a/locales/ru.yaml b/locales/ru.yaml index 0e7d011908..e033d0668d 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Команды одобрены (шаблон одобрен навсегда) ({count} команд). Агент возобновляет работу..." background: - usage: "Использование: /background <запрос>\nПример: /background Сделай сводку лучших историй с HN сегодня\n\nЗапускает запрос в отдельном сеансе. Можно продолжить общение — результат появится здесь по завершении." + usage: "Использование: /bg <запрос>\nПример: /bg Сделай сводку лучших историй с HN сегодня\n\nЗапускает запрос в отдельном сеансе. Можно продолжить общение — результат появится здесь по завершении." started: "🔄 Фоновая задача запущена: «{preview}»\nID задачи: {task_id}\nМожно продолжить общение — результаты появятся здесь по завершении." + btw: + usage: "Использование: /btw <вопрос>\nПример: /btw в каком файле была та ошибка?\n\nОтвечает на быстрый попутный вопрос об этом разговоре, не прерывая его. Для независимой фоновой задачи используйте /bg <запрос>." + no_history: "Разговора ещё нет — отправьте вопрос обычным сообщением." + no_provider: "❌ Не удалось ответить: учётные данные провайдера не настроены." + started: "💬 Попутный вопрос: «{preview}»\nОтвечаю по снимку этого разговора — текущая работа продолжается." + answer: "💬 /btw: «{preview}»\n\n{answer}" + failed: "❌ /btw не удался: «{preview}»\n{error}" + branch: db_unavailable: "База данных сеансов недоступна." no_conversation: "Нет беседы для ответвления — сначала отправьте сообщение." diff --git a/locales/tr.yaml b/locales/tr.yaml index 55101e49fc..3dabe13b65 100644 --- a/locales/tr.yaml +++ b/locales/tr.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Komutlar onaylandı (desen kalıcı olarak onaylandı) ({count} komut). Ajan devam ediyor..." background: - usage: "Kullanım: /background \nÖrnek: /background Bugünün öne çıkan HN haberlerini özetle\n\nİstemi ayrı bir oturumda çalıştırır. Sohbete devam edebilirsin — sonuç tamamlandığında burada görünecek." + usage: "Kullanım: /bg \nÖrnek: /bg Bugünün öne çıkan HN haberlerini özetle\n\nİstemi ayrı bir oturumda çalıştırır. Sohbete devam edebilirsin — sonuç tamamlandığında burada görünecek." started: "🔄 Arka plan görevi başlatıldı: \"{preview}\"\nGörev kimliği: {task_id}\nSohbete devam edebilirsin — sonuçlar tamamlandığında burada görünecek." + btw: + usage: "Kullanım: /btw \nÖrnek: /btw o hata hangi dosyadaydı?\n\nBu konuşma hakkında hızlı bir yan soruyu, konuşmayı kesmeden yanıtlar. Bağımsız bir arka plan görevi için /bg kullan." + no_history: "Henüz konuşma yok — sorunu normal bir mesaj olarak gönder." + no_provider: "❌ Yan soru yanıtlanamıyor: sağlayıcı kimlik bilgileri yapılandırılmamış." + started: "💬 Yan soru: \"{preview}\"\nBu konuşmanın anlık görüntüsünden yanıtlanıyor — mevcut çalışma devam ediyor." + answer: "💬 /btw: \"{preview}\"\n\n{answer}" + failed: "❌ /btw başarısız oldu: \"{preview}\"\n{error}" + branch: db_unavailable: "Oturum veritabanı kullanılamıyor." no_conversation: "Dallandırılacak konuşma yok — önce bir mesaj gönderin." diff --git a/locales/uk.yaml b/locales/uk.yaml index 44b0639040..d9907b006a 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ Команди схвалено (шаблон схвалено назавжди) ({count} команд). Агент відновлює роботу…" background: - usage: "Використання: /background <запит>\nПриклад: /background Підсумуй найкращі історії з HN сьогодні\n\nЗапускає запит в окремому сеансі. Можна продовжити спілкування — результат з'явиться тут після завершення." + usage: "Використання: /bg <запит>\nПриклад: /bg Підсумуй найкращі історії з HN сьогодні\n\nЗапускає запит в окремому сеансі. Можна продовжити спілкування — результат з'явиться тут після завершення." started: "🔄 Фонове завдання запущено: «{preview}»\nID завдання: {task_id}\nМожна продовжити спілкування — результати з'являться тут після завершення." + btw: + usage: "Використання: /btw <питання>\nПриклад: /btw у якому файлі була та помилка?\n\nВідповідає на швидке побіжне питання про цю розмову, не перериваючи її. Для незалежного фонового завдання використовуйте /bg <запит>." + no_history: "Розмови ще немає — надішліть питання звичайним повідомленням." + no_provider: "❌ Не вдалося відповісти: облікові дані провайдера не налаштовано." + started: "💬 Побіжне питання: «{preview}»\nВідповідаю за знімком цієї розмови — поточна робота триває." + answer: "💬 /btw: «{preview}»\n\n{answer}" + failed: "❌ /btw не вдалося: «{preview}»\n{error}" + branch: db_unavailable: "База даних сеансів недоступна." no_conversation: "Немає розмови для розгалуження — спочатку надішліть повідомлення." diff --git a/locales/zh-hant.yaml b/locales/zh-hant.yaml index 8d9dc19fa7..2a9520fca3 100644 --- a/locales/zh-hant.yaml +++ b/locales/zh-hant.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ 指令已批准(永久允許該模式)({count} 條指令)。代理正在恢復…" background: - usage: "用法:/background <提示>\n範例:/background 摘要今天 HN 上的熱門故事\n\n在獨立工作階段中執行該提示。你可以繼續聊天 — 完成後結果將顯示於此。" + usage: "用法:/bg <提示>\n範例:/bg 摘要今天 HN 上的熱門故事\n\n在獨立工作階段中執行該提示。你可以繼續聊天 — 完成後結果將顯示於此。" started: "🔄 背景任務已啟動:「{preview}」\n任務 ID:{task_id}\n你可以繼續聊天 — 完成後結果將顯示於此。" + btw: + usage: "用法:/btw <問題>\n範例:/btw 那個錯誤在哪個檔案?\n\n在不打斷目前工作的情況下,回答關於此對話的快速問題。若要執行獨立的背景任務,請使用 /bg <提示>。" + no_history: "尚無對話 — 請將問題作為一般訊息傳送。" + no_provider: "❌ 無法回答問題:未設定供應商憑證。" + started: "💬 順帶一問:「{preview}」\n正根據此對話的快照回答 — 目前的工作繼續進行。" + answer: "💬 /btw:「{preview}」\n\n{answer}" + failed: "❌ /btw 失敗:「{preview}」\n{error}" + branch: db_unavailable: "工作階段資料庫無法使用。" no_conversation: "沒有可分支的對話 — 請先傳送一則訊息。" diff --git a/locales/zh.yaml b/locales/zh.yaml index b991506ced..696623cfc8 100644 --- a/locales/zh.yaml +++ b/locales/zh.yaml @@ -68,9 +68,17 @@ gateway: always_plural: "✅ 命令已批准(永久允许该模式)({count} 条命令)。代理正在恢复…" background: - usage: "用法:/background <提示>\n示例:/background 总结今天 HN 上热门的故事\n\n在独立会话中运行该提示。你可以继续聊天 — 结果完成后将在此显示。" + usage: "用法:/bg <提示>\n示例:/bg 总结今天 HN 上热门的故事\n\n在独立会话中运行该提示。你可以继续聊天 — 结果完成后将在此显示。" started: "🔄 后台任务已启动:「{preview}」\n任务 ID:{task_id}\n你可以继续聊天 — 完成后结果将在此显示。" + btw: + usage: "用法:/btw <问题>\n示例:/btw 那个错误在哪个文件?\n\n在不打断当前工作的情况下,回答关于此对话的快速问题。要运行独立的后台任务,请使用 /bg <提示>。" + no_history: "还没有对话 — 请将问题作为普通消息发送。" + no_provider: "❌ 无法回答问题:未配置提供商凭据。" + started: "💬 顺便一问:「{preview}」\n正根据此对话的快照回答 — 当前工作继续进行。" + answer: "💬 /btw:「{preview}」\n\n{answer}" + failed: "❌ /btw 失败:「{preview}」\n{error}" + branch: db_unavailable: "会话数据库不可用。" no_conversation: "没有可分支的对话 — 请先发送一条消息。" diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index c01031cf33..fcd846b52e 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -5148,7 +5148,7 @@ class DiscordAdapter(BasePlatformAdapter): # historically ran with NO authorization check — bypassing every gate # ``on_message`` enforces (DISCORD_ALLOWED_USERS, DISCORD_ALLOWED_ROLES, # DISCORD_ALLOWED_CHANNELS, DISCORD_IGNORED_CHANNELS). Any guild member - # could invoke ``/background``, ``/restart``, ``/sethome``, etc. as the + # could invoke ``/bg``, ``/restart``, ``/sethome``, etc. as the # operator. ``_check_slash_authorization`` mirrors the on_message gates # one-for-one so the slash surface honors the same trust boundary. # @@ -6007,10 +6007,15 @@ class DiscordAdapter(BasePlatformAdapter): async def slash_queue(interaction: discord.Interaction, prompt: str): await self._run_simple_slash(interaction, f"/queue {prompt}", "Queued for the next turn.") - @tree.command(name="background", description="Run a prompt in the background") + @tree.command(name="bg", description="Run a prompt in a separate background session") @discord.app_commands.describe(prompt="The prompt to run in the background") async def slash_background(interaction: discord.Interaction, prompt: str): - await self._run_simple_slash(interaction, f"/background {prompt}", "Background task started~") + await self._run_simple_slash(interaction, f"/bg {prompt}", "Background task started~") + + @tree.command(name="btw", description="Ask a side question about the current conversation") + @discord.app_commands.describe(question="The side question to answer without interrupting") + async def slash_btw(interaction: discord.Interaction, question: str): + await self._run_simple_slash(interaction, f"/btw {question}", "Side question dispatched~") # ── Auto-register any gateway-available commands not yet on the tree ── # This ensures new commands added to COMMAND_REGISTRY in diff --git a/tests/agent/test_side_question.py b/tests/agent/test_side_question.py new file mode 100644 index 0000000000..4bcc4c3c1f --- /dev/null +++ b/tests/agent/test_side_question.py @@ -0,0 +1,92 @@ +"""Tests for agent/side_question.py — the /btw context-aware side question engine.""" + +from unittest.mock import patch + +from agent.side_question import ( + SIDE_QUESTION_TASK, + answer_side_question, + render_history_for_side_question, +) + + +class TestRenderHistory: + def test_empty_history(self): + assert render_history_for_side_question([]) == "(no prior conversation)" + assert render_history_for_side_question(None) == "(no prior conversation)" + + def test_basic_roles(self): + history = [ + {"role": "system", "content": "SYSTEM PROMPT — must not appear"}, + {"role": "user", "content": "fix the bug in foo.py"}, + { + "role": "assistant", + "content": "Looking now.", + "tool_calls": [ + {"function": {"name": "read_file"}}, + {"function": {"name": "patch"}}, + ], + }, + {"role": "tool", "content": "Traceback: ValueError in foo.py line 3"}, + {"role": "assistant", "content": "Fixed it."}, + ] + out = render_history_for_side_question(history) + assert "SYSTEM PROMPT" not in out + assert "USER: fix the bug in foo.py" in out + assert "ASSISTANT [called tools: read_file, patch]" in out + assert "TOOL RESULT: Traceback: ValueError in foo.py line 3" in out + assert "ASSISTANT: Fixed it." in out + + def test_structured_content_blocks(self): + history = [ + {"role": "user", "content": [{"type": "text", "text": "hello there"}]}, + ] + out = render_history_for_side_question(history) + assert "USER: hello there" in out + + def test_newest_biased_truncation(self): + history = [ + {"role": "user", "content": f"message number {i} " + "x" * 400} + for i in range(200) + ] + out = render_history_for_side_question(history, char_budget=3000) + # Newest messages survive; oldest are dropped with a marker. + assert "message number 199" in out + assert "message number 0 " not in out + assert out.startswith("[...older conversation omitted...]") + assert len(out) < 4000 + + def test_non_dict_entries_ignored(self): + out = render_history_for_side_question(["garbage", None, 42, {"role": "user", "content": "hi"}]) + assert "USER: hi" in out + + +class TestAnswerSideQuestion: + def test_empty_question_raises(self): + try: + answer_side_question(" ", []) + except ValueError: + pass + else: + raise AssertionError("expected ValueError for empty question") + + def test_calls_oneshot_with_snapshot_and_task(self): + captured = {} + + def fake_run_oneshot(**kwargs): + captured.update(kwargs) + return "the error was in foo.py" + + history = [{"role": "user", "content": "run the tests"}] + runtime = {"model": "m", "provider": "p", "base_url": "u", "api_key": "k", "api_mode": "chat_completions"} + with patch("agent.oneshot.run_oneshot", side_effect=fake_run_oneshot): + answer = answer_side_question( + "which file had the error?", history, main_runtime=runtime + ) + + assert answer == "the error was in foo.py" + assert captured["task"] == SIDE_QUESTION_TASK + assert captured["main_runtime"] is runtime + assert "USER: run the tests" in captured["user_input"] + assert "Side question: which file had the error?" in captured["user_input"] + # The instructions steer the model to answer only the side question. + assert "side" in captured["instructions"].lower() diff --git a/tests/cli/test_cli_approval_ui.py b/tests/cli/test_cli_approval_ui.py index 081a94bac8..3ba323c147 100644 --- a/tests/cli/test_cli_approval_ui.py +++ b/tests/cli/test_cli_approval_ui.py @@ -249,7 +249,7 @@ class TestCliApprovalUi: patch.object(cli_module, "_cprint"), \ patch.object(cli_module, "ChatConsole") as chat_console: chat_console.return_value.print = MagicMock() - cli._handle_background_command("/btw check weather") + cli._handle_background_command("/bg check weather") # Join the worker thread deterministically rather than polling a # wall-clock deadline — under load the thread's finally-block pop diff --git a/tests/cli/test_cli_background_busy_path.py b/tests/cli/test_cli_background_busy_path.py index 96e627761d..9af07b0da1 100644 --- a/tests/cli/test_cli_background_busy_path.py +++ b/tests/cli/test_cli_background_busy_path.py @@ -1,8 +1,8 @@ -"""Regression tests for classic-CLI mid-run /background dispatch. +"""Regression tests for classic-CLI mid-run /bg and /btw dispatch. Background ---------- -``/background`` (``/bg``, ``/btw``) exists to start independent work while +``/bg`` (formerly ``/background``) exists to start independent work while the current turn keeps running. Typed while the agent was busy it went into ``self._pending_input`` like ordinary input, and ``process_loop`` is blocked inside ``self.chat()`` for the whole run, so the background task only started @@ -72,15 +72,23 @@ class TestBackgroundInlineDetector: cli = _make_cli() cli._agent_running = True assert cli._should_handle_background_command_inline( - "/background inspect the test failures" + "/bg inspect the test failures" ) is True - def test_detects_both_aliases(self): + def test_detects_both_commands(self): cli = _make_cli() cli._agent_running = True assert cli._should_handle_background_command_inline("/bg do work") is True assert cli._should_handle_background_command_inline("/btw do work") is True + def test_background_alias_still_resolves_to_bg(self): + """The retired /background spelling no longer resolves to a command.""" + cli = _make_cli() + cli._agent_running = True + assert cli._should_handle_background_command_inline( + "/background do work" + ) is False + def test_ignores_background_when_agent_idle(self): """Idle input falls through to the normal process_loop dispatch.""" cli = _make_cli() @@ -117,16 +125,16 @@ class TestBackgroundInlineDetector: class TestBackgroundBusyPolicyContract: """The registry already declares the intent this detector implements.""" - def test_background_declares_dispatch_while_busy(self): + def test_bg_and_btw_declare_dispatch_while_busy(self): from hermes_cli.commands import resolve_command - cmd = resolve_command("background") - assert cmd is not None - assert cmd.busy_policy == "dispatch" + for name in ("bg", "btw"): + cmd = resolve_command(name) + assert cmd is not None + assert cmd.name == name + assert cmd.busy_policy == "dispatch" - def test_aliases_resolve_to_background(self): + def test_background_name_is_retired(self): from hermes_cli.commands import resolve_command - for alias in ("bg", "btw"): - cmd = resolve_command(alias) - assert cmd is not None and cmd.name == "background" + assert resolve_command("background") is None diff --git a/tests/gateway/test_background_command.py b/tests/gateway/test_background_command.py index 65a98deecd..c137542285 100644 --- a/tests/gateway/test_background_command.py +++ b/tests/gateway/test_background_command.py @@ -1,4 +1,4 @@ -"""Tests for /background gateway slash command. +"""Tests for /bg gateway slash command. Tests the _handle_background_command handler (run a prompt in a separate background session) across gateway messenger platforms. @@ -14,7 +14,7 @@ from gateway.platforms.base import MessageEvent from gateway.session import SessionSource -def _make_event(text="/background", platform=Platform.TELEGRAM, +def _make_event(text="/bg", platform=Platform.TELEGRAM, user_id="12345", chat_id="67890"): """Build a MessageEvent for testing.""" source = SessionSource( @@ -62,26 +62,18 @@ class TestHandleBackgroundCommand: @pytest.mark.asyncio async def test_no_prompt_shows_usage(self): - """Running /background with no prompt shows usage.""" - runner = _make_runner() - event = _make_event(text="/background") - result = await runner._handle_background_command(event) - assert "Usage:" in result - assert "/background" in result - - @pytest.mark.asyncio - async def test_bg_alias_no_prompt_shows_usage(self): """Running /bg with no prompt shows usage.""" runner = _make_runner() event = _make_event(text="/bg") result = await runner._handle_background_command(event) assert "Usage:" in result + assert "/bg" in result @pytest.mark.asyncio async def test_empty_prompt_shows_usage(self): - """Running /background with only whitespace shows usage.""" + """Running /bg with only whitespace shows usage.""" runner = _make_runner() - event = _make_event(text="/background ") + event = _make_event(text="/bg ") result = await runner._handle_background_command(event) assert "Usage:" in result @@ -172,44 +164,136 @@ class TestRunBackgroundTask: # --------------------------------------------------------------------------- -# /background in help and known_commands +# /bg in help and known_commands # --------------------------------------------------------------------------- class TestBackgroundInHelp: - """Verify /background appears in help text and known commands.""" + """Verify /bg and /btw appear in help text and known commands.""" @pytest.mark.asyncio - async def test_background_in_help_output(self): - """The /help output includes /background.""" + async def test_bg_and_btw_in_help_output(self): + """The /help output includes /bg and /btw.""" runner = _make_runner() event = _make_event(text="/help") result = await runner._handle_help_command(event) - assert "/background" in result + assert "/bg" in result + assert "/btw" in result # --------------------------------------------------------------------------- -# CLI /background command definition +# CLI /bg command definition # --------------------------------------------------------------------------- class TestBackgroundInCLICommands: - """Verify /background is registered in the CLI command system.""" + """Verify /bg and /btw are registered in the CLI command system.""" - def test_background_autocompletes(self): - """The /background command appears in autocomplete results.""" + def test_bg_autocompletes(self): + """The /bg and /btw commands appear in autocomplete results.""" pytest.importorskip("prompt_toolkit") from hermes_cli.commands import SlashCommandCompleter from prompt_toolkit.document import Document completer = SlashCommandCompleter() - doc = Document("backgro") # Partial match + doc = Document("bg") # Partial match completions = list(completer.get_completions(doc, None)) # Text doesn't start with / so no completions assert len(completions) == 0 - doc = Document("/backgro") # With slash prefix + doc = Document("/bg") # With slash prefix completions = list(completer.get_completions(doc, None)) cmd_displays = [str(c.display) for c in completions] - assert any("/background" in d for d in cmd_displays) + assert any("/bg" in d for d in cmd_displays) + + doc = Document("/btw") + completions = list(completer.get_completions(doc, None)) + cmd_displays = [str(c.display) for c in completions] + assert any("/btw" in d for d in cmd_displays) + + +# --------------------------------------------------------------------------- +# _handle_btw_command +# --------------------------------------------------------------------------- + + +class TestHandleBtwCommand: + """Tests for GatewayRunner._handle_btw_command (context-aware side question).""" + + @pytest.mark.asyncio + async def test_no_question_shows_usage(self): + runner = _make_runner() + event = _make_event(text="/btw") + result = await runner._handle_btw_command(event) + assert "Usage:" in result + assert "/btw" in result + + @pytest.mark.asyncio + async def test_no_history_reports_no_conversation(self): + runner = _make_runner() + store = AsyncMock() + store.get_or_create_session.return_value = MagicMock(session_id="s1") + store.load_transcript.return_value = [] + store._store = runner.session_store + runner._async_session_store = store + event = _make_event(text="/btw what did we do?") + result = await runner._handle_btw_command(event) + assert "conversation" in result.lower() + + @pytest.mark.asyncio + async def test_dispatches_side_question_and_sends_answer(self): + runner = _make_runner() + store = AsyncMock() + store.get_or_create_session.return_value = MagicMock(session_id="s1") + store.load_transcript.return_value = [ + {"role": "user", "content": "fix foo.py"}, + {"role": "assistant", "content": "done"}, + ] + store._store = runner.session_store + runner._async_session_store = store + runner._resolve_session_agent_runtime = MagicMock( + return_value=("test-model", {"api_key": "k", "provider": "p", + "base_url": "u", "api_mode": "chat_completions"}) + ) + runner._reply_anchor_for_event = MagicMock(return_value=None) + runner._thread_metadata_for_source = MagicMock(return_value=None) + mock_adapter = AsyncMock() + runner._adapter_for_source = MagicMock(return_value=mock_adapter) + + event = _make_event(text="/btw which file was that?") + + with patch("agent.side_question.answer_side_question", + return_value="it was foo.py") as mock_answer: + result = await runner._handle_btw_command(event) + # Ack returned immediately, worker task registered. + assert "which file was that?" in result + # Drain the fire-and-forget task. + for task in list(runner._background_tasks): + await task + + # Snapshot + question reached the engine; live history untouched. + args, kwargs = mock_answer.call_args + assert args[0] == "which file was that?" + assert args[1][0]["content"] == "fix foo.py" + assert kwargs["main_runtime"]["model"] == "test-model" + + # The answer was delivered to the chat. + mock_adapter.send.assert_called_once() + sent_text = mock_adapter.send.call_args[0][1] + assert "it was foo.py" in sent_text + + @pytest.mark.asyncio + async def test_no_credentials_reports_error(self): + runner = _make_runner() + store = AsyncMock() + store.get_or_create_session.return_value = MagicMock(session_id="s1") + store.load_transcript.return_value = [{"role": "user", "content": "hi"}] + store._store = runner.session_store + runner._async_session_store = store + runner._resolve_session_agent_runtime = MagicMock( + return_value=(None, {"api_key": None}) + ) + event = _make_event(text="/btw what?") + result = await runner._handle_btw_command(event) + assert "❌" in result diff --git a/tests/gateway/test_command_bypass_active_session.py b/tests/gateway/test_command_bypass_active_session.py index 693d1ed3ef..20b91c25e1 100644 --- a/tests/gateway/test_command_bypass_active_session.py +++ b/tests/gateway/test_command_bypass_active_session.py @@ -186,18 +186,34 @@ class TestCommandBypassActiveSession: @pytest.mark.asyncio async def test_background_bypasses_guard(self): - """/background must bypass so it spawns a parallel task, not an interrupt.""" + """/bg must bypass so it spawns a parallel task, not an interrupt.""" adapter = _make_adapter() sk = _session_key() adapter._active_sessions[sk] = asyncio.Event() - await adapter.handle_message(_make_event("/background summarize HN")) + await adapter.handle_message(_make_event("/bg summarize HN")) assert sk not in adapter._pending_messages, ( - "/background was queued as a pending message instead of being dispatched" + "/bg was queued as a pending message instead of being dispatched" ) - assert any("handled:background" in r for r in adapter.sent_responses), ( - "/background response was not sent back to the user" + assert any("handled:bg" in r for r in adapter.sent_responses), ( + "/bg response was not sent back to the user" + ) + + @pytest.mark.asyncio + async def test_btw_bypasses_guard(self): + """/btw must bypass so the side question dispatches mid-run.""" + adapter = _make_adapter() + sk = _session_key() + adapter._active_sessions[sk] = asyncio.Event() + + await adapter.handle_message(_make_event("/btw which file was that?")) + + assert sk not in adapter._pending_messages, ( + "/btw was queued as a pending message instead of being dispatched" + ) + assert any("handled:btw" in r for r in adapter.sent_responses), ( + "/btw response was not sent back to the user" ) @pytest.mark.asyncio diff --git a/tests/gateway/test_discord_slash_auth.py b/tests/gateway/test_discord_slash_auth.py index b78d31e75d..6e2fc27d45 100644 --- a/tests/gateway/test_discord_slash_auth.py +++ b/tests/gateway/test_discord_slash_auth.py @@ -3,7 +3,7 @@ Slash invocations (``_run_simple_slash``, ``_handle_thread_create_slash``) historically bypassed every gate ``on_message`` enforces — DISCORD_ALLOWED_USERS, DISCORD_ALLOWED_ROLES, DISCORD_ALLOWED_CHANNELS, DISCORD_IGNORED_CHANNELS. -Any guild member could invoke ``/background``, ``/restart``, etc. as the +Any guild member could invoke ``/bg``, ``/restart``, etc. as the operator. ``_check_slash_authorization`` mirrors all four gates one-for-one. These tests pin the security-correct behavior so the bypass cannot regress. @@ -213,7 +213,7 @@ async def test_no_allowlist_allows_with_gateway_allow_all(adapter, monkeypatch): async def test_allowed_user_passes(adapter): adapter._allowed_user_ids = {"100200300"} interaction = _make_interaction("100200300") - assert await adapter._check_slash_authorization(interaction, "/background hi") is True + assert await adapter._check_slash_authorization(interaction, "/bg hi") is True interaction.response.send_message.assert_not_awaited() @@ -256,7 +256,7 @@ async def test_channel_not_in_allowlist_rejected(adapter, monkeypatch, caplog): monkeypatch.setenv("DISCORD_ALLOWED_CHANNELS", "1111,2222") interaction = _make_interaction("100200300", channel_id=9999) with caplog.at_level(logging.WARNING): - assert await adapter._check_slash_authorization(interaction, "/background hi") is False + assert await adapter._check_slash_authorization(interaction, "/bg hi") is False assert any("DISCORD_ALLOWED_CHANNELS" in r.message for r in caplog.records) @@ -284,7 +284,7 @@ async def test_unauthorized_attempt_notifies_telegram(adapter): adapter._allowed_user_ids = {"100200300"} interaction = _make_interaction("999999999") - await adapter._check_slash_authorization(interaction, "/background hi") + await adapter._check_slash_authorization(interaction, "/bg hi") # Notify is fire-and-forget — let the scheduled task run. await asyncio.sleep(0) @@ -295,7 +295,7 @@ async def test_unauthorized_attempt_notifies_telegram(adapter): assert chat_id == "987654321" assert "Unauthorized" in msg assert "999999999" in msg - assert "/background hi" in msg + assert "/bg hi" in msg assert "DISCORD_ALLOWED_USERS" in msg diff --git a/tests/gateway/test_multiplex_background_task_scope.py b/tests/gateway/test_multiplex_background_task_scope.py index f6e1125e55..bf5c70e724 100644 --- a/tests/gateway/test_multiplex_background_task_scope.py +++ b/tests/gateway/test_multiplex_background_task_scope.py @@ -1,6 +1,6 @@ """Regression: background tasks respect profile secret scope when multiplexing. -Issue #60726: /background spawns _run_background_task as a fire-and-forget +Issue #60726: /bg spawns _run_background_task as a fire-and-forget asyncio task with no profile scope, so _resolve_session_agent_runtime()'s credential reads raise UnscopedSecretError when multiplex_profiles is on. The fix wraps the task body in _profile_runtime_scope, mirroring _run_agent. diff --git a/tests/gateway/test_slash_access_dispatch.py b/tests/gateway/test_slash_access_dispatch.py index 0c30c03976..2b765f004d 100644 --- a/tests/gateway/test_slash_access_dispatch.py +++ b/tests/gateway/test_slash_access_dispatch.py @@ -238,7 +238,7 @@ async def test_admin_runs_quick_command_when_gating_enabled(): # Running-agent fast-path gating — admin/user split must hold even when an # agent is already running. The fast-path block in _handle_message dispatches # /stop, /restart, /new, /steer, /model, /approve, /deny, /agents, -# /background, /kanban, /goal, /yolo, /verbose, /footer, /help, /commands, +# /bg, /btw, /kanban, /goal, /yolo, /verbose, /footer, /help, /commands, # /profile, /update directly without going through the cold dispatch site. # We must apply the gate there too — otherwise non-admins could bypass # gating just because an agent happens to be busy. diff --git a/tests/hermes_cli/test_busy_policy_invariants.py b/tests/hermes_cli/test_busy_policy_invariants.py index 75e1117751..db2fc0c264 100644 --- a/tests/hermes_cli/test_busy_policy_invariants.py +++ b/tests/hermes_cli/test_busy_policy_invariants.py @@ -23,7 +23,8 @@ _HISTORICAL_BYPASS_NAMES = frozenset( { "agents", "approve", - "background", + "bg", + "btw", "commands", "deny", "help", diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index 501a9d200d..e7fb446a21 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -176,11 +176,14 @@ class TestGatewayHelpLines: assert not re.search(pattern, joined), \ f"cli_only command /{cmd.name} should not be in gateway help" - def test_includes_alias_note_for_bg(self): + def test_bg_and_btw_are_separate_commands(self): lines = gateway_help_lines() + joined = "\n".join(lines) + assert "`/bg" in joined + assert "`/btw" in joined + # The retired /background canonical name must be gone. bg_line = [l for l in lines if "/background" in l] - assert len(bg_line) == 1 - assert "/bg" in bg_line[0] + assert not bg_line class TestTelegramBotCommands: @@ -198,11 +201,12 @@ class TestTelegramBotCommands: def test_includes_builtin_commands_with_required_args(self): - """Built-in arg-taking commands (e.g. /queue, /steer, /background) + """Built-in arg-taking commands (e.g. /queue, /steer, /bg, /btw) are now included because their handlers return usage text when invoked without arguments — issue #24312.""" names = {name for name, _ in telegram_bot_commands()} - assert "background" in names + assert "bg" in names + assert "btw" in names assert "queue" in names assert "steer" in names diff --git a/tui_gateway/methods_prompt.py b/tui_gateway/methods_prompt.py index 7e2f816133..43a80ca715 100644 --- a/tui_gateway/methods_prompt.py +++ b/tui_gateway/methods_prompt.py @@ -1380,6 +1380,76 @@ def _(rid, params: dict) -> dict: return _ok(rid, {"task_id": task_id}) +@method("prompt.btw") +def _(rid, params: dict) -> dict: + """Answer a side question about the session without touching its history. + + Snapshots the live conversation (in-flight ``_session_messages`` when a + turn is running, else the persisted ``session["history"]``) and runs a + one-shot auxiliary LLM call against it (``agent/side_question.py``). The + session's history, role alternation, and prompt cache are untouched; the + answer arrives as a ``btw.complete`` event. + """ + session, err = _sess(params, rid) + if err: + return err + text, parent = params.get("text", ""), params.get("session_id", "") + if not text: + return _err(rid, 4012, "text required") + task_id = f"btw_{uuid.uuid4().hex[:6]}" + + agent = session.get("agent") + snapshot = list( + getattr(agent, "_session_messages", None) + or session.get("history") + or [] + ) + main_runtime = { + "model": getattr(agent, "model", None), + "provider": getattr(agent, "provider", None), + "base_url": getattr(agent, "base_url", None), + "api_key": getattr(agent, "api_key", None), + "api_mode": getattr(agent, "api_mode", None), + } + + def run(): + session_tokens = _set_session_context(task_id, cwd=_session_cwd(session)) + try: + from agent.side_question import answer_side_question + + _profile_home_str = session.get("profile_home") + home_token = ( + set_hermes_home_override(_profile_home_str) + if _profile_home_str + else None + ) + try: + answer = answer_side_question( + text, + snapshot, + main_runtime=main_runtime, + ) + finally: + if home_token is not None: + reset_hermes_home_override(home_token) + _emit( + "btw.complete", + parent, + {"task_id": task_id, "question": text, "text": answer or ""}, + ) + except Exception as e: + _emit( + "btw.complete", + parent, + {"task_id": task_id, "question": text, "text": f"error: {e}"}, + ) + finally: + _clear_session_context(session_tokens) + + threading.Thread(target=run, daemon=True).start() + return _ok(rid, {"task_id": task_id}) + + @method("preview.restart") def _(rid, params: dict) -> dict: session, err = _sess(params, rid) diff --git a/ui-tui/src/__tests__/slashParity.test.ts b/ui-tui/src/__tests__/slashParity.test.ts index 0b6a6149ff..f54fa629a6 100644 --- a/ui-tui/src/__tests__/slashParity.test.ts +++ b/ui-tui/src/__tests__/slashParity.test.ts @@ -16,7 +16,8 @@ interface CommandRegistryLoad { const NATIVE_MUTATING_COMMANDS = new Set(['browser', 'busy', 'fast', 'reload-mcp', 'rollback', 'stop']) const MUTATING_COMMANDS = [ - 'background', + 'bg', + 'btw', 'branch', 'browser', 'busy', diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 86a988ed40..a8ebbe56bd 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -1295,6 +1295,10 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: dropBgTask(ev.payload.task_id) sys(`[bg ${ev.payload.task_id}] ${ev.payload.text}`) + return + case 'btw.complete': + sys(`[btw${ev.payload.question ? ` "${ev.payload.question}"` : ''}] ${ev.payload.text}`) + return case 'review.summary': { // Self-improvement background review emitted a persistent summary diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index bc072ec380..c920fb5f58 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -78,12 +78,12 @@ const reasoningConfigPayload = (arg: string, sid: string) => { export const sessionCommands: SlashCommand[] = [ { - aliases: ['bg', 'btw'], + aliases: ['background'], help: 'launch a background prompt', - name: 'background', + name: 'bg', run: (arg, ctx) => { if (!arg) { - return ctx.transcript.sys('/background ') + return ctx.transcript.sys('/bg ') } ctx.gateway.rpc('prompt.background', { session_id: ctx.sid, text: arg }).then( @@ -99,6 +99,26 @@ export const sessionCommands: SlashCommand[] = [ } }, + { + help: 'ask a side question about this conversation', + name: 'btw', + run: (arg, ctx) => { + if (!arg) { + return ctx.transcript.sys('/btw ') + } + + ctx.gateway.rpc('prompt.btw', { session_id: ctx.sid, text: arg }).then( + ctx.guarded(r => { + if (!r.task_id) { + return + } + + ctx.transcript.sys(`btw ${r.task_id} — answering from a conversation snapshot`) + }) + ) + } + }, + { help: 'change or show model', name: 'model', diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 8d13e0f3f2..73ddee0a17 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -729,6 +729,7 @@ export type GatewayEvent = | { payload: { env_var: string; prompt: string; request_id: string }; session_id?: string; type: 'secret.request' } | { payload: { request_id: string }; session_id?: string; type: 'secret.expire' | 'sudo.expire' } | { payload: { task_id: string; text: string }; session_id?: string; type: 'background.complete' } + | { payload: { question?: string; task_id: string; text: string }; session_id?: string; type: 'btw.complete' } | { payload?: { text?: string }; session_id?: string; type: 'review.summary' } | { payload: SubagentEventPayload; session_id?: string; type: 'subagent.spawn_requested' } | { payload: SubagentEventPayload; session_id?: string; type: 'subagent.start' } diff --git a/website/docs/reference/cli-symbols.md b/website/docs/reference/cli-symbols.md index 6876e87af0..c3487f8e39 100644 --- a/website/docs/reference/cli-symbols.md +++ b/website/docs/reference/cli-symbols.md @@ -42,7 +42,7 @@ The single line at the bottom of the TUI. Segments appear only when relevant and | `⏱` | Per-prompt elapsed time while the turn runs, e.g. `⏱ 12s/3m 45s` (turn time / session time). | | `⏲` | The same timer, frozen after the turn completes. | | `cmp N` | The session has been auto-compressed N times. | -| `▶ N` | N `/background` tasks currently running. | +| `▶ N` | N `/bg` tasks currently running. | | `⚠ YOLO` | YOLO mode is on (auto-approval). Also shown in the startup banner. | | `⛓ N` | N subagents currently active. | | `↩ resumes when subagent finishes` | Reassurance shown while you are idle but delegated work is still in flight — the result returns on its own. | diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index 6afc8f04cf..ba43b6e212 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -64,7 +64,8 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in | `/status` | Show session info — model, provider, profile, session ID, working directory, title, created/updated timestamps, token totals, agent-running state — followed by a local **Session recap** block (recent user/assistant turn counts, tool result count, top tools used, last few files touched, the latest user prompt, and the latest assistant reply). The recap is computed locally from the in-memory conversation; no LLM call, no prompt-cache impact. | | `/context [all]` (alias: `/ctx`) | Visual context-window breakdown. On the CLI/TUI: a 5×20 glyph block grid (each cell ≈ 1% of the model window) plus an estimated per-category table — system prompt, tool definitions, rules, skills index, MCP, subagents, memory, conversation — versus free space. On messaging platforms: a usage gauge with auto-compression threshold/headroom, compression stats, cumulative throughput, and the same category table in plain text. `/context all` appends per-skill and per-toolset cost listings (index cost vs SKILL.md load cost; schema tokens per toolset). Read-only and computed locally — no LLM call, no prompt-cache impact. | | `/agents` (alias: `/tasks`) | Show active agents and running tasks across the current session. | -| `/background ` (alias: `/bg`, `/btw`) | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). | +| `/bg ` | Run a prompt in a separate background session. The agent processes your prompt independently — your current session stays free for other work. Results appear as a panel when the task finishes. See [CLI Background Sessions](/user-guide/cli#background-sessions). | +| `/btw ` | Ask a quick side question **about the current conversation** without interrupting it. A one-shot auxiliary LLM call answers from a read-only snapshot of the transcript — the live session's history and prompt cache are untouched, and the current turn keeps running. For independent work with a fresh context, use `/bg`. | | `/branch [name]` (alias: `/fork`) | Branch the current session (explore a different path) | | `/worktree [new [name]\|list]` | **CLI only.** Inspect or create isolated git worktrees mid-session (inspired by Copilot CLI's `/worktree new`). Bare `/worktree` shows the active worktree; `/worktree list` lists the repo's worktrees; `/worktree new [name]` creates a worktree under `.worktrees/` (branched from the freshly-fetched remote tip, honoring `worktree_sync`) and retargets the session's terminal and file tools into it. Named trees use your name (`hermes/` branch); unnamed ones get a random `hermes-`. On exit the tree is kept only if it has unpushed commits — same lifecycle as `hermes -w`. See [Git Worktrees](/user-guide/git-worktrees). | | `/handoff ` | **CLI only.** Hand the current session off to a messaging platform (Telegram, Discord, Slack, WhatsApp, Signal, Matrix). The gateway picks it up immediately, creates a fresh thread on platforms that support threads (Telegram topics, Discord text-channel threads, Slack message-anchored threads), re-binds the destination to your CLI session_id so the full role-aware transcript replays, and forges a synthetic user turn so the agent confirms it's working in the new place. Your CLI exits cleanly on success with a `/resume` hint; resume locally any time with `/resume `. Refused mid-turn. Requires the gateway to be running and a home channel configured for the target platform (`/sethome` from the destination chat). See [Cross-Platform Handoff](/user-guide/sessions#cross-platform-handoff). | @@ -250,7 +251,8 @@ The messaging gateway supports the following built-in commands inside Telegram, | `/voice [on\|off\|tts\|join\|channel\|leave\|status]` | Control spoken replies in chat. `join`/`channel`/`leave` manage Discord voice-channel mode. | | `/rollback [number]` | List or restore filesystem checkpoints. | | `/diff [staged\|all\|session] [--stat]` | Show git changes in the working directory (fenced and truncated to platform message limits). `session` shows the cumulative diff of everything Hermes changed; `--stat` shows just the summary. | -| `/background <prompt>` | Run a prompt in a separate background session. Results are delivered back to the same chat when the task finishes. See [Messaging Background Sessions](/user-guide/messaging/#background-sessions). | +| `/bg <prompt>` | Run a prompt in a separate background session. Results are delivered back to the same chat when the task finishes. See [Messaging Background Sessions](/user-guide/messaging/#background-sessions). | +| `/btw <question>` | Ask a side question about the current conversation without interrupting it. Answered from a transcript snapshot; the answer is sent to the chat when ready. | | `/queue <prompt>` (alias: `/q`) | Queue a prompt for the next turn without interrupting the current one. | | `/steer <prompt>` | Inject a message after the next tool call without interrupting — the model picks it up on its next iteration rather than as a new turn. | | `/goal <text>` | Set a standing goal Hermes works toward across turns — our take on the Ralph loop. A judge model checks after each turn; if not done, Hermes auto-continues until it is, you pause/clear it, or the turn budget (default 20) is hit. Subcommands: `/goal status`, `/goal pause`, `/goal resume`, `/goal clear`. Safe to run mid-agent for status/pause/clear; setting a new goal requires `/stop` first. See [Persistent Goals](/user-guide/features/goals). | @@ -295,7 +297,7 @@ The messaging gateway supports the following built-in commands inside Telegram, - `/verbose` is **CLI-only by default**, but can be enabled for messaging platforms by setting `display.tool_progress_command: true` in `config.yaml`. When enabled, it cycles the `display.tool_progress` mode and saves to config. - `/focus` and `/verbose` share one suppression path (`display.tool_progress`), so they can never contradict each other: `/focus on` pins tool progress to `off` and stashes your mode under `display.focus_saved_tool_progress`; `/focus off` restores it; cycling `/verbose` while focus is on takes the mode back and clears the focus badge. Focus view is display-only — it never changes conversation history, the system prompt, or anything sent to the model, so it has zero prompt-cache impact. - `/sethome`, `/restart`, `/approve`, `/deny`, `/topic`, `/platform`, and `/commands` are **messaging-only** commands. -- `/status`, `/egress`, `/version`, `/whoami`, `/background`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/diff`, `/debug`, `/fast`, `/approvals`, `/footer`, `/curator`, `/kanban`, `/topup`, `/suggestions`, `/blueprint`, `/learn`, `/init`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway. +- `/status`, `/egress`, `/version`, `/whoami`, `/bg`, `/btw`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/diff`, `/debug`, `/fast`, `/approvals`, `/footer`, `/curator`, `/kanban`, `/topup`, `/suggestions`, `/blueprint`, `/learn`, `/init`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway. - `/voice join`, `/voice channel`, and `/voice leave` are only meaningful on Discord. - In the TUI, `/sessions` shows live sessions in the current TUI process. Use `/resume [name]` or `hermes --tui --resume <id-or-title>` for saved or closed transcripts. diff --git a/website/docs/user-guide/cli.md b/website/docs/user-guide/cli.md index 08a9401468..36a6cedf3f 100644 --- a/website/docs/user-guide/cli.md +++ b/website/docs/user-guide/cli.md @@ -136,7 +136,7 @@ A persistent status bar sits above the input area, updating in real time: | Context bar | Visual fill indicator with color-coded thresholds | | Cost | Estimated session cost (or `n/a` for unknown/zero-priced models) | | 🗜️ N | **Context compression count** — how many times the running session has been auto-compressed. Appears once the first compression fires. | -| ▶ N | **Active background tasks** — how many `/background` prompts are still running in the current session. Appears whenever at least one task is in flight. | +| ▶ N | **Active background tasks** — how many `/bg` prompts are still running in the current session. Appears whenever at least one task is in flight. | | Duration | Elapsed session time | | Session title | Once the session has a title, it appears as a gold badge pinned to the far-right edge. Long titles truncate before displacing the essential model and context fields. | | ⚠ YOLO | **YOLO mode warning** — shown whenever `HERMES_YOLO_MODE` is on (either `hermes --yolo` at launch or `/yolo` toggled mid-session). Mirrors the banner-line warning so you can't forget you're in auto-approve mode. | @@ -213,7 +213,8 @@ Common examples: | `/model` | Show or change the current model | | `/tools` | List currently available tools | | `/skills browse` | Browse the skills hub and official optional skills | -| `/background <prompt>` | Run a prompt in a separate background session | +| `/bg <prompt>` | Run a prompt in a separate background session | +| `/btw <question>` | Ask a side question about the current conversation without interrupting it | | `/skin` | Show or switch the active CLI skin | | `/voice on` | Enable CLI voice mode (press `Ctrl+B` to record) | | `/voice tts` | Toggle spoken playback for Hermes replies | @@ -487,7 +488,7 @@ When compression triggers, middle turns are summarized while the first 3 and las Run a prompt in a separate background session while continuing to use the CLI for other work: ``` -/background Analyze the logs in /var/log and summarize any errors from today +/bg Analyze the logs in /var/log and summarize any errors from today ``` Hermes immediately confirms the task and gives you back the prompt: @@ -499,7 +500,7 @@ Hermes immediately confirms the task and gives you back the prompt: ### How It Works -Each `/background` prompt spawns a **completely separate agent session** in a daemon thread: +Each `/bg` prompt spawns a **completely separate agent session** in a daemon thread: - **Isolated conversation** — the background agent has no knowledge of your current session's history. It receives only the prompt you provide. - **Same configuration** — the background agent inherits your model, provider, toolsets, reasoning settings, and fallback model from the current session. @@ -523,8 +524,8 @@ If the task fails, you'll see an error notification instead. If `display.bell_on ### Use Cases -- **Long-running research** — "/background research the latest developments in quantum error correction" while you work on code -- **File processing** — "/background analyze all Python files in this repo and list any security issues" while you continue a conversation +- **Long-running research** — "/bg research the latest developments in quantum error correction" while you work on code +- **File processing** — "/bg analyze all Python files in this repo and list any security issues" while you continue a conversation - **Parallel investigations** — start multiple background tasks to explore different angles simultaneously :::info diff --git a/website/docs/user-guide/messaging/discord.md b/website/docs/user-guide/messaging/discord.md index f6da7e0c4d..609ab43918 100644 --- a/website/docs/user-guide/messaging/discord.md +++ b/website/docs/user-guide/messaging/discord.md @@ -648,7 +648,7 @@ Hermes automatically registers installed skills as **native Discord Application - Each skill becomes a Discord slash command (e.g., `/code-review`, `/ascii-art`) - Skills accept an optional `args` string parameter - Discord has a limit of 100 application commands per bot — if you have more skills than available slots, extra skills are skipped with a warning in the logs -- Skills are registered during bot startup alongside built-in commands like `/model`, `/reset`, and `/background` +- Skills are registered during bot startup alongside built-in commands like `/model`, `/reset`, and `/bg` No extra configuration is needed — any skill installed via `hermes skills install` is automatically registered as a Discord slash command on the next gateway restart. diff --git a/website/docs/user-guide/messaging/index.md b/website/docs/user-guide/messaging/index.md index fb6098e003..ef25303025 100644 --- a/website/docs/user-guide/messaging/index.md +++ b/website/docs/user-guide/messaging/index.md @@ -210,7 +210,8 @@ platform network disconnect as an event-loop failure. | `/reasoning [level\|show\|hide]` | Change reasoning effort or toggle reasoning display | | `/voice [on\|off\|tts\|join\|leave\|status]` | Control messaging voice replies and Discord voice-channel behavior | | `/rollback [number]` | List or restore filesystem checkpoints | -| `/background <prompt>` | Run a prompt in a separate background session | +| `/bg <prompt>` | Run a prompt in a separate background session | +| `/btw <question>` | Ask a side question about the current conversation without interrupting it | | `/reload-mcp` | Reload MCP servers from config | | `/update` | Update Hermes Agent to the latest version | | `/help` | Show available commands | @@ -495,7 +496,7 @@ When enabled, the bot sends status messages as it works: Run a prompt in a separate background session so the agent works on it independently while your main chat stays responsive: ``` -/background Check all servers in the cluster and report any that are down +/bg Check all servers in the cluster and report any that are down ``` Hermes confirms immediately: @@ -507,7 +508,7 @@ Hermes confirms immediately: ### How It Works -Each `/background` prompt spawns a **separate agent instance** that runs asynchronously: +Each `/bg` prompt spawns a **separate agent instance** that runs asynchronously: - **Isolated session** — the background agent has its own session with its own conversation history. It has no knowledge of your current chat context and receives only the prompt you provide. - **Same configuration** — inherits your model, provider, toolsets, reasoning settings, and provider routing from the current gateway setup. @@ -539,10 +540,10 @@ HERMES_BACKGROUND_NOTIFICATIONS=result ### Use Cases -- **Server monitoring** — "/background Check the health of all services and alert me if anything is down" -- **Long builds** — "/background Build and deploy the staging environment" while you continue chatting -- **Research tasks** — "/background Research competitor pricing and summarize in a table" -- **File operations** — "/background Organize the photos in ~/Downloads by date into folders" +- **Server monitoring** — "/bg Check the health of all services and alert me if anything is down" +- **Long builds** — "/bg Build and deploy the staging environment" while you continue chatting +- **Research tasks** — "/bg Research competitor pricing and summarize in a table" +- **File operations** — "/bg Organize the photos in ~/Downloads by date into folders" :::tip Background tasks on messaging platforms are fire-and-forget — you don't need to wait or check on them. Results arrive in the same chat automatically when the task finishes. diff --git a/website/docs/user-guide/messaging/matrix.md b/website/docs/user-guide/messaging/matrix.md index c5406c87b0..39124393ed 100644 --- a/website/docs/user-guide/messaging/matrix.md +++ b/website/docs/user-guide/messaging/matrix.md @@ -553,7 +553,7 @@ To find a Room ID: in Element, go to the room → **Settings** → **Advanced** Hermes supports the same gateway commands in Matrix that it supports on other messaging platforms, including `/commands`, `/model`, `/stop`, `/queue`, -`/steer`, `/goal`, `/subgoal`, `/background`, `/bg`, `/btw`, `/tasks`, and +`/steer`, `/goal`, `/subgoal`, `/bg`, `/btw`, `/tasks`, and `/yolo`. Some Matrix clients reserve leading `/` for local client commands and may not diff --git a/website/docs/user-guide/messaging/slack.md b/website/docs/user-guide/messaging/slack.md index d8c20f36f1..4bc66f5a30 100644 --- a/website/docs/user-guide/messaging/slack.md +++ b/website/docs/user-guide/messaging/slack.md @@ -301,7 +301,7 @@ Then in Slack: ### Legacy `/hermes <subcommand>` still works For backward compatibility with older manifests, you can still type -`/hermes btw run the tests` — Hermes routes it the same way as `/btw +`/hermes bg run the tests` — Hermes routes it the same way as `/bg run the tests`. Free-form questions also work: `/hermes what's the weather?` is treated as a regular message. diff --git a/website/docs/user-guide/messaging/telegram.md b/website/docs/user-guide/messaging/telegram.md index 713f14958b..9aa51384bb 100644 --- a/website/docs/user-guide/messaging/telegram.md +++ b/website/docs/user-guide/messaging/telegram.md @@ -839,7 +839,7 @@ Shows the current topic's binding: session title, session ID, and hints for `/ne - The General (pinned top) topic in a forum-enabled DM is treated as the root lobby, regardless of whether Telegram delivers its messages with `message_thread_id=1` or with no thread_id - Root-lobby reminders are rate-limited to one message per 30 seconds per chat — a user who forgets topic mode is on and types ten prompts in the root won't get ten replies - BotFather setup screenshots are rate-limited to one send per 5 minutes per chat — repeated `/topic` attempts while Threads Settings are still disabled won't re-upload the same image -- `/background <prompt>` started inside a topic delivers its result back to the same topic; background sessions don't trigger auto-rename of the owning topic +- `/bg <prompt>` started inside a topic delivers its result back to the same topic; background sessions don't trigger auto-rename of the owning topic - `/topic` itself is gated by the bot's user authorization check — unauthorized DMs get a refusal instead of activation ### Disabling multi-session mode diff --git a/website/docs/user-guide/messaging/yuanbao.md b/website/docs/user-guide/messaging/yuanbao.md index a7414f8852..5ba3b56a70 100644 --- a/website/docs/user-guide/messaging/yuanbao.md +++ b/website/docs/user-guide/messaging/yuanbao.md @@ -323,7 +323,7 @@ Results are delivered to your home channel. Run long operations without blocking the conversation: ``` -/background Analyze all files in the archive +/bg Analyze all files in the archive ``` ### Cross-Platform Messages diff --git a/website/docs/user-guide/tui.md b/website/docs/user-guide/tui.md index c15ee3f6ed..04724657f4 100644 --- a/website/docs/user-guide/tui.md +++ b/website/docs/user-guide/tui.md @@ -216,7 +216,7 @@ The status line also shows: - **Working directory with git branch** — `~/projects/hermes-agent (docs/two-week-gap-sweep)`. The branch suffix updates when you `git checkout` in a side terminal (mtime-cached) so the TUI reflects your actual active branch, not whatever it was at launch. - **Per-prompt elapsed time** — `⏱ 12s/3m 45s` while the turn is running (live), frozen to `⏲ 32s / 3m 45s` after the turn completes. First number is time since last user message; second is total session duration. Resets on every new prompt. - **`🗜️ N`** — number of times the running session has been auto-compressed. Appears once the first compression fires. -- **`▶ N`** — number of `/background` tasks currently running in this session. Appears whenever at least one task is in flight. +- **`▶ N`** — number of `/bg` tasks currently running in this session. Appears whenever at least one task is in flight. - **`⚠ YOLO`** — visible warning whenever YOLO mode is on (`hermes --yolo`, `/yolo`, or `HERMES_YOLO_MODE=1`). The same badge also appears in the startup banner so you cannot launch an auto-approving session without noticing. ## Configuration diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/slash-commands.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/slash-commands.md index be7e1ca69a..4af5a9f45c 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/slash-commands.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/slash-commands.md @@ -56,7 +56,8 @@ Hermes 有两个斜杠命令入口,均由 `hermes_cli/commands.py` 中的中 | `/redraw` | 强制完整重绘 UI(在 tmux 调整大小、鼠标选择产生残影等导致终端错位后恢复)。 | | `/status` | 显示会话信息——模型、提供商、profile、会话 ID、工作目录、标题、创建/更新时间戳、token 总量、agent 运行状态——随后显示本地**会话摘要**块(近期用户/助手轮次数、工具结果数、最常用工具、最近访问的文件、最新用户 prompt 和最新助手回复)。摘要从内存中的对话本地计算,不调用 LLM,不影响 prompt 缓存。 | | `/agents`(别名:`/tasks`) | 显示当前会话中的活动 agent 和运行中的任务。 | -| `/background <prompt>`(别名:`/bg`、`/btw`) | 在独立的后台会话中运行 prompt。agent 独立处理你的 prompt——当前会话保持空闲可继续其他工作。任务完成后结果以面板形式显示。见 [CLI 后台会话](/user-guide/cli#background-sessions)。 | +| `/bg <prompt>` | 在独立的后台会话中运行 prompt。agent 独立处理你的 prompt——当前会话保持空闲可继续其他工作。任务完成后结果以面板形式显示。见 [CLI 后台会话](/user-guide/cli#background-sessions)。 | +| `/btw <question>` | 在不打断当前工作的情况下,就**当前对话**提出一个快速的顺带问题。由一次一次性辅助 LLM 调用根据对话快照作答——实时会话的历史和提示缓存不受影响,当前回合继续运行。需要全新上下文的独立任务请使用 `/bg`。 | | `/branch [name]`(别名:`/fork`) | 分支当前会话(探索不同路径) | | `/handoff <platform>` | **仅限 CLI。** 将当前会话移交给消息平台(Telegram、Discord、Slack、WhatsApp、Signal、Matrix)。gateway 立即接管,在支持线程的平台上创建新线程(Telegram 话题、Discord 文字频道线程、Slack 消息锚定线程),将目标重新绑定到你的 CLI session_id 以重放完整的角色感知转录,并伪造一条合成用户轮次让 agent 确认已在新位置工作。成功后 CLI 干净退出并提示 `/resume`;随时可用 `/resume <title>` 在本地恢复。轮次进行中拒绝执行。需要 gateway 正在运行且目标平台已配置 home 频道(从目标聊天中执行 `/sethome`)。见 [跨平台移交](/user-guide/sessions#cross-platform-handoff)。 | @@ -220,7 +221,8 @@ hermes config set model.aliases.grok x-ai/grok-4 | `/reasoning [level\|show\|hide]` | 更改推理力度或切换推理显示。 | | `/voice [on\|off\|tts\|join\|channel\|leave\|status]` | 控制聊天中的语音回复。`join`/`channel`/`leave` 管理 Discord 语音频道模式。 | | `/rollback [number]` | 列出或恢复文件系统检查点。 | -| `/background <prompt>` | 在独立的后台会话中运行 prompt。任务完成后结果投递回同一聊天。见 [消息平台后台会话](/user-guide/messaging/#background-sessions)。 | +| `/bg <prompt>` | 在独立的后台会话中运行 prompt。任务完成后结果投递回同一聊天。见 [消息平台后台会话](/user-guide/messaging/#background-sessions)。 | +| `/btw <question>` | 在不打断当前对话的情况下,就当前对话提出顺带问题。根据对话快照作答;答案就绪后发送到聊天中。 | | `/queue <prompt>`(别名:`/q`) | 将 prompt 加入队列等待下一轮处理,不中断当前轮次。 | | `/steer <prompt>` | 在下一次工具调用后注入一条消息,不中断——模型在下一次迭代时获取,而非作为新轮次。 | | `/goal <text>` | 设置一个持续目标,Hermes 将跨轮次持续推进——这是我们对 Ralph loop 的实现。裁判模型在每轮后检查;若未完成,Hermes 自动继续,直到完成、你暂停/清除,或达到轮次预算(默认 20)。子命令:`/goal status`、`/goal pause`、`/goal resume`、`/goal clear`。agent 运行中可安全执行 status/pause/clear;设置新目标需先执行 `/stop`。见 [持续目标](/user-guide/features/goals)。 | @@ -249,7 +251,7 @@ hermes config set model.aliases.grok x-ai/grok-4 - `/skills` **仅在搜索/浏览/安装时属于 CLI-only**;其写入审批子命令(`pending`、`approve`、`reject`、`diff`、`approval`)在 `skills.write_approval` 开启时也可在消息平台使用。`/memory` 可在**两个表面**使用。 - `/verbose` **默认仅限 CLI**,但可通过在 `config.yaml` 中设置 `display.tool_progress_command: true` 为消息平台启用。启用后,它会循环切换 `display.tool_progress` 模式并保存到配置。 - `/sethome`、`/update`、`/restart`、`/approve`、`/deny`、`/topic`、`/platform` 和 `/commands` 是**仅限消息平台**的命令。 -- `/status`、`/version`、`/background`、`/queue`、`/steer`、`/voice`、`/reload-mcp`、`/reload-skills`、`/rollback`、`/debug`、`/fast`、`/footer`、`/curator`、`/kanban`、`/credits`、`/suggestions`、`/blueprint`、`/sessions` 和 `/yolo` 在 **CLI 和消息 gateway 中均可使用**。 +- `/status`、`/version`、`/bg`、`/btw`、`/queue`、`/steer`、`/voice`、`/reload-mcp`、`/reload-skills`、`/rollback`、`/debug`、`/fast`、`/footer`、`/curator`、`/kanban`、`/credits`、`/suggestions`、`/blueprint`、`/sessions` 和 `/yolo` 在 **CLI 和消息 gateway 中均可使用**。 - `/voice join`、`/voice channel` 和 `/voice leave` 仅在 Discord 上有意义。 ## 破坏性命令的确认提示 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/cli.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/cli.md index ced238eca2..683d679f89 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/cli.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/cli.md @@ -69,7 +69,7 @@ hermes -w -z "Fix issue #123" # 在 worktree 中以单次查询模式运行 | 上下文进度条 | 带颜色阈值编码的可视填充指示器 | | 费用 | 预估会话费用(未知或零价格模型显示 `n/a`) | | 🗜️ N | **上下文压缩次数**——当前运行会话被自动压缩的次数。首次压缩触发后显示。 | -| ▶ N | **活跃后台任务数**——当前会话中仍在运行的 `/background` prompt(提示词)数量。至少有一个任务进行中时显示。 | +| ▶ N | **活跃后台任务数**——当前会话中仍在运行的 `/bg` prompt(提示词)数量。至少有一个任务进行中时显示。 | | 时长 | 会话已用时间 | | ⚠ YOLO | **YOLO 模式警告**——当 `HERMES_YOLO_MODE` 开启时显示(通过启动时的 `hermes --yolo` 或会话中的 `/yolo` 切换)。与横幅行警告保持同步,确保你不会忘记自己处于自动批准模式。 | @@ -122,7 +122,8 @@ hermes -w -z "Fix issue #123" # 在 worktree 中以单次查询模式运行 | `/model` | 显示或更改当前模型 | | `/tools` | 列出当前可用工具 | | `/skills browse` | 浏览 skill 中心和官方可选 skill | -| `/background <prompt>` | 在独立后台会话中运行一个 prompt | +| `/bg <prompt>` | 在独立后台会话中运行一个 prompt | +| `/btw <question>` | 在不打断当前对话的情况下,就当前对话提出顺带问题 | | `/skin` | 显示或切换当前 CLI 皮肤 | | `/voice on` | 启用 CLI 语音模式(按 `Ctrl+B` 录音) | | `/voice tts` | 切换 Hermes 回复的语音播放 | @@ -383,7 +384,7 @@ auxiliary: 在独立的后台会话中运行 prompt,同时继续使用 CLI 进行其他工作: ``` -/background Analyze the logs in /var/log and summarize any errors from today +/bg Analyze the logs in /var/log and summarize any errors from today ``` Hermes 立即确认任务并将提示符还给你: @@ -395,7 +396,7 @@ Hermes 立即确认任务并将提示符还给你: ### 工作原理 -每个 `/background` prompt 会在守护线程中生成一个**完全独立的 agent 会话**: +每个 `/bg` prompt 会在守护线程中生成一个**完全独立的 agent 会话**: - **隔离对话**——后台 agent 不了解当前会话的历史。它只接收你提供的 prompt。 - **相同配置**——后台 agent 继承当前会话的模型、提供商、工具集、推理设置和回退模型。 @@ -419,8 +420,8 @@ Hermes 立即确认任务并将提示符还给你: ### 使用场景 -- **长时间研究**——"/background research the latest developments in quantum error correction",同时继续编写代码 -- **文件处理**——"/background analyze all Python files in this repo and list any security issues",同时继续对话 +- **长时间研究**——"/bg research the latest developments in quantum error correction",同时继续编写代码 +- **文件处理**——"/bg analyze all Python files in this repo and list any security issues",同时继续对话 - **并行调查**——同时启动多个后台任务,从不同角度探索问题 :::info diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/discord.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/discord.md index 8df87a340e..50798a9a39 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/discord.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/discord.md @@ -624,7 +624,7 @@ Hermes 自动将已安装的技能注册为**原生 Discord 应用命令**。这 - 每个技能成为一个 Discord 斜杠命令(例如 `/code-review`、`/ascii-art`) - 技能接受一个可选的 `args` 字符串参数 - Discord 每个机器人有 100 个应用命令的限制——如果你的技能数量超过可用槽位,多余的技能会被跳过并在日志中显示警告 -- 技能在机器人启动时与内置命令(如 `/model`、`/reset` 和 `/background`)一起注册 +- 技能在机器人启动时与内置命令(如 `/model`、`/reset` 和 `/bg`)一起注册 无需额外配置——通过 `hermes skills install` 安装的任何技能都会在下次网关重启时自动注册为 Discord 斜杠命令。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/index.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/index.md index db2fdc6dfb..a5b70ccaf1 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/index.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/index.md @@ -148,7 +148,8 @@ hermes gateway status --system # 仅 Linux:显式检查系统服务 | `/reasoning [level\|show\|hide]` | 更改推理强度或切换推理显示 | | `/voice [on\|off\|tts\|join\|leave\|status]` | 控制消息语音回复和 Discord 语音频道行为 | | `/rollback [number]` | 列出或恢复文件系统检查点 | -| `/background <prompt>` | 在独立后台会话中运行 prompt(提示词) | +| `/bg <prompt>` | 在独立后台会话中运行 prompt(提示词) | +| `/btw <question>` | 在不打断当前对话的情况下,就当前对话提出顺带问题 | | `/reload-mcp` | 从配置重新加载 MCP 服务器 | | `/update` | 将 Hermes Agent 更新至最新版本 | | `/help` | 显示可用命令 | @@ -317,7 +318,7 @@ display: 在独立的后台会话中运行 prompt,让 agent 独立处理,同时保持主聊天响应: ``` -/background Check all servers in the cluster and report any that are down +/bg Check all servers in the cluster and report any that are down ``` Hermes 立即确认: @@ -329,7 +330,7 @@ Hermes 立即确认: ### 工作原理 -每个 `/background` prompt 会生成一个**独立的 agent 实例**异步运行: +每个 `/bg` prompt 会生成一个**独立的 agent 实例**异步运行: - **隔离会话** — 后台 agent 拥有自己的会话和对话历史。它不了解你当前的聊天上下文,只接收你提供的 prompt。 - **相同配置** — 继承当前网关配置中的模型、提供商、工具集、推理设置和提供商路由。 @@ -361,10 +362,10 @@ HERMES_BACKGROUND_NOTIFICATIONS=result ### 使用场景 -- **服务器监控** — "/background Check the health of all services and alert me if anything is down" -- **长时间构建** — "/background Build and deploy the staging environment",同时继续聊天 -- **研究任务** — "/background Research competitor pricing and summarize in a table" -- **文件操作** — "/background Organize the photos in ~/Downloads by date into folders" +- **服务器监控** — "/bg Check the health of all services and alert me if anything is down" +- **长时间构建** — "/bg Build and deploy the staging environment",同时继续聊天 +- **研究任务** — "/bg Research competitor pricing and summarize in a table" +- **文件操作** — "/bg Organize the photos in ~/Downloads by date into folders" :::tip 消息平台上的后台任务是即发即忘的——你无需等待或主动查询。任务完成后,结果会自动出现在同一聊天中。 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md index 9ebfb0998c..e823deb5f2 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/slack.md @@ -235,7 +235,7 @@ hermes slack manifest --write ### 旧版 `/hermes <子命令>` 仍然有效 -为了向后兼容旧版 manifest,你仍然可以输入 `/hermes btw run the tests`——Hermes 会以与 `/btw run the tests` 相同的方式路由它。自由形式的问题也有效:`/hermes what's the weather?` 会被当作普通消息处理。 +为了向后兼容旧版 manifest,你仍然可以输入 `/hermes bg run the tests`——Hermes 会以与 `/bg run the tests` 相同的方式路由它。自由形式的问题也有效:`/hermes what's the weather?` 会被当作普通消息处理。 ### 在话题(thread)中使用命令(`!cmd` 前缀) diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md index 498618859b..c92acb87e2 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/telegram.md @@ -757,7 +757,7 @@ Hermes 会确认会话标题,并重放最后一条助手消息以提供上下 - 论坛启用私聊中的 General(置顶顶部)话题被视为根大厅,无论 Telegram 是以 `message_thread_id=1` 还是无 thread_id 投递其消息 - 根大厅提醒每个聊天每 30 秒限速一条——忘记话题模式已开启并在根目录输入十条 prompt 的用户不会收到十条回复 - BotFather 设置截图每个聊天每 5 分钟限速一次发送——在 Threads Settings 仍然禁用时重复尝试 `/topic` 不会重复上传同一张图片 -- 在话题内启动的 `/background <prompt>` 会将结果投递回同一话题;后台会话不会触发所属话题的自动重命名 +- 在话题内启动的 `/bg <prompt>` 会将结果投递回同一话题;后台会话不会触发所属话题的自动重命名 - `/topic` 本身受机器人用户授权检查限制——未授权的私聊会收到拒绝而非激活 ### 禁用多会话模式 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/yuanbao.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/yuanbao.md index d49c9e42cf..62b637b5e8 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/yuanbao.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/messaging/yuanbao.md @@ -322,7 +322,7 @@ HERMES_LOG_LEVEL=debug hermes gateway 在不阻塞会话的情况下运行长时间操作: ``` -/background Analyze all files in the archive +/bg Analyze all files in the archive ``` ### 跨平台消息 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md index c18bb063ce..53643659a1 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent.md @@ -260,7 +260,7 @@ hermes uninstall Uninstall Hermes /stop Kill background processes /rollback [N] Restore filesystem checkpoint /snapshot [sub] Create or restore state snapshots of Hermes config/state (CLI) -/background <prompt> Run prompt in background +/bg <prompt> Run prompt in background /queue <prompt> Queue for next turn /steer <prompt> Inject a message after the next tool call without interrupting /agents (/tasks) Show active agents and running tasks diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/tui.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/tui.md index e09fcfced2..c7ac811ef1 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/tui.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/tui.md @@ -172,7 +172,7 @@ TUI 的状态栏实时跟踪 agent 状态: - **工作目录及 git 分支** — `~/projects/hermes-agent (docs/two-week-gap-sweep)`。在旁边的终端执行 `git checkout` 时,分支后缀会更新(mtime 缓存),TUI 反映的是实际活跃分支,而非启动时的分支。 - **每条 prompt 的耗时** — 轮次运行时显示 `⏱ 12s/3m 45s`(实时),轮次完成后冻结为 `⏲ 32s / 3m 45s`。第一个数字是自上次用户消息以来的时间;第二个是会话总时长。每次新 prompt 时重置。 - **`🗜️ N`** — 当前会话被自动压缩的次数。首次压缩触发后显示。 -- **`▶ N`** — 当前会话中正在运行的 `/background` 任务数量。至少有一个任务在执行时显示。 +- **`▶ N`** — 当前会话中正在运行的 `/bg` 任务数量。至少有一个任务在执行时显示。 - **`⚠ YOLO`** — 每当 YOLO 模式开启时(`hermes --yolo`、`/yolo` 或 `HERMES_YOLO_MODE=1`)显示的可见警告。同一徽章也出现在启动 banner 中,确保你不会在未注意到的情况下启动自动审批会话。 ## 配置