From ebf2ea584ab2ca37cd70b80b4d8c3bc23604cf47 Mon Sep 17 00:00:00 2001 From: Mibayy Date: Sun, 10 May 2026 22:41:35 -0700 Subject: [PATCH] feat(terminal,cli): docker_extra_args + display.timestamps Two independent opt-in QoL toggles, both off by default. terminal.docker_extra_args: - List of extra flags appended verbatim to docker run after security defaults. Useful for adding capabilities (e.g. --cap-add SETUID) or other docker run options not exposed by existing config keys. - Non-string entries are logged and skipped. - Also available via TERMINAL_DOCKER_EXTRA_ARGS='[...]' env var. display.timestamps: - Appends [HH:MM] to user input bullet and the assistant response box header. Single hub in _format_submitted_user_message_preview() covers both single-line and multi-line user previews; assistant response label gets the timestamp at box-open time. Closes #1569 (timestamps). Co-authored-by: Mibayy --- cli-config.yaml.example | 9 +++++++++ cli.py | 14 ++++++++++++-- hermes_cli/config.py | 2 ++ tools/environments/docker.py | 11 +++++++++++ tools/terminal_tool.py | 4 ++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cli-config.yaml.example b/cli-config.yaml.example index b611b395755..6daceba04a9 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -203,6 +203,12 @@ terminal: # docker_forward_env: # - "GITHUB_TOKEN" # - "NPM_TOKEN" +# # Optional: extra flags passed verbatim to docker run (appended after security defaults). +# # Useful for adding capabilities (e.g. apt installs needing SETUID) or custom options. +# # Example: add a Linux capability not included by default +# # docker_extra_args: +# # - "--cap-add" +# # - "SETUID" # ----------------------------------------------------------------------------- # OPTION 4: Singularity/Apptainer container @@ -947,6 +953,9 @@ display: # false: Wait for the full response before rendering streaming: true + # Show [HH:MM] timestamps on user input and assistant response labels. + # timestamps: false + # ─────────────────────────────────────────────────────────────────────────── # Skin / Theme # ─────────────────────────────────────────────────────────────────────────── diff --git a/cli.py b/cli.py index e54acc1c76c..fd9cc275e8a 100644 --- a/cli.py +++ b/cli.py @@ -2300,6 +2300,8 @@ class HermesCLI: # streaming: stream tokens to the terminal as they arrive (display.streaming in config.yaml) self.streaming_enabled = CLI_CONFIG["display"].get("streaming", False) + # show_timestamps: prefix user and assistant labels with [HH:MM] + self.show_timestamps = CLI_CONFIG["display"].get("timestamps", False) self.final_response_markdown = str( CLI_CONFIG["display"].get("final_response_markdown", "strip") ).strip().lower() or "strip" @@ -3315,9 +3317,13 @@ class HermesCLI: def _format_submitted_user_message_preview(self, user_input: str) -> str: """Format the submitted user-message scrollback preview.""" + ts_suffix = ( + f" [dim]{datetime.now().strftime('%H:%M')}[/]" + if getattr(self, "show_timestamps", False) else "" + ) lines = user_input.split("\n") if len(lines) <= 1: - return f"[bold {_accent_hex()}]●[/] [bold]{_escape(user_input)}[/]" + return f"[bold {_accent_hex()}]●[/] [bold]{_escape(user_input)}[/]{ts_suffix}" first_lines = int(getattr(self, "user_message_preview_first_lines", 2)) last_lines = int(getattr(self, "user_message_preview_last_lines", 2)) @@ -3334,7 +3340,7 @@ class HermesCLI: tail = [] preview_lines = [ - f"[bold {_accent_hex()}]●[/] [bold]{_escape(head[0])}[/]" + f"[bold {_accent_hex()}]●[/] [bold]{_escape(head[0])}[/]{ts_suffix}" ] preview_lines.extend(f"[bold]{_escape(line)}[/]" for line in head[1:]) @@ -3606,6 +3612,8 @@ class HermesCLI: self._stream_text_ansi = f"\033[38;2;{_r};{_g};{_b}m" except (ValueError, IndexError): self._stream_text_ansi = "" + if self.show_timestamps: + label = f"{label} {datetime.now().strftime('%H:%M')}" w = shutil.get_terminal_size().columns fill = w - 2 - len(label) _cprint(f"\n{_ACCENT}╭─{label}{'─' * max(fill - 1, 0)}╮{_RST}") @@ -10162,6 +10170,8 @@ class HermesCLI: _streaming_box_opened = True w = self.console.width label = " ⚕ Hermes " + if self.show_timestamps: + label = f"{label}{datetime.now().strftime('%H:%M')} " fill = w - 2 - len(label) _cprint(f"\n{_ACCENT}╭─{label}{'─' * max(fill - 1, 0)}╮{_RST}") _cprint(f"{_STREAM_PAD}{sentence.rstrip()}") diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3b79da754ba..feeb10892f2 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -579,6 +579,7 @@ DEFAULT_CONFIG = { # Explicit opt-in: mount the host cwd into /workspace for Docker sessions. # Default off because passing host directories into a sandbox weakens isolation. "docker_mount_cwd_to_workspace": False, + "docker_extra_args": [], # Extra flags passed verbatim to docker run # Explicit opt-in: run the Docker container as the host user's uid:gid # (via `--user`). When enabled, files written into bind-mounted dirs # (docker_volumes, the persistent workspace, or the auto-mounted cwd) @@ -901,6 +902,7 @@ DEFAULT_CONFIG = { "bell_on_complete": False, "show_reasoning": False, "streaming": False, + "timestamps": False, # Show [HH:MM] on user and assistant labels "final_response_markdown": "strip", # render | strip | raw # Preserve recent classic CLI output across Ctrl+L, /redraw, and # terminal resize full-screen clears. Disable if a terminal emulator diff --git a/tools/environments/docker.py b/tools/environments/docker.py index 06d8154872c..1cd72ce8552 100644 --- a/tools/environments/docker.py +++ b/tools/environments/docker.py @@ -300,6 +300,7 @@ class DockerEnvironment(BaseEnvironment): host_cwd: str = None, auto_mount_cwd: bool = False, run_as_host_user: bool = False, + extra_args: list = None, ): if cwd == "~": cwd = "/root" @@ -476,6 +477,15 @@ class DockerEnvironment(BaseEnvironment): security_args = _build_security_args(run_as_host_user and bool(user_args)) logger.info(f"Docker volume_args: {volume_args}") + # User-supplied extra docker run flags (docker_extra_args in config.yaml). + # Appended last so they can override defaults if needed. + validated_extra = [] + for arg in (extra_args or []): + if not isinstance(arg, str): + logger.warning("Ignoring non-string docker_extra_args entry: %r", arg) + continue + validated_extra.append(arg) + all_run_args = ( security_args + user_args @@ -483,6 +493,7 @@ class DockerEnvironment(BaseEnvironment): + resource_args + volume_args + env_args + + validated_extra ) logger.info(f"Docker run_args: {all_run_args}") diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 5d6b80c1bc4..3ff22e3f882 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -1087,6 +1087,7 @@ def _get_env_config() -> Dict[str, Any]: "docker_volumes": _parse_env_var("TERMINAL_DOCKER_VOLUMES", "[]", json.loads, "valid JSON"), "docker_env": _parse_env_var("TERMINAL_DOCKER_ENV", "{}", json.loads, "valid JSON"), "docker_run_as_host_user": os.getenv("TERMINAL_DOCKER_RUN_AS_HOST_USER", "false").lower() in ("true", "1", "yes"), + "docker_extra_args": _parse_env_var("TERMINAL_DOCKER_EXTRA_ARGS", "[]", json.loads, "valid JSON"), } @@ -1129,6 +1130,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int, volumes = cc.get("docker_volumes", []) docker_forward_env = cc.get("docker_forward_env", []) docker_env = cc.get("docker_env", {}) + docker_extra_args = cc.get("docker_extra_args", []) if env_type == "local": return _LocalEnvironment(cwd=cwd, timeout=timeout) @@ -1144,6 +1146,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int, forward_env=docker_forward_env, env=docker_env, run_as_host_user=cc.get("docker_run_as_host_user", False), + extra_args=docker_extra_args, ) elif env_type == "singularity": @@ -1792,6 +1795,7 @@ def terminal_tool( "docker_forward_env": config.get("docker_forward_env", []), "docker_env": config.get("docker_env", {}), "docker_run_as_host_user": config.get("docker_run_as_host_user", False), + "docker_extra_args": config.get("docker_extra_args", []), } local_config = None