From 93f7617406ac54e7a7d7be16d0fecffbcb6ef442 Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Wed, 23 Sep 2026 23:38:21 -0500 Subject: [PATCH] feat(tui): add native terminal mode defaults --- hermes_cli/_parser.py | 4 ++++ hermes_cli/config_defaults.py | 3 +++ hermes_cli/main.py | 3 ++- hermes_cli/main_tui_launch.py | 14 ++++++++++++-- .../test_default_interface_resolution.py | 12 ++++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/hermes_cli/_parser.py b/hermes_cli/_parser.py index dfa4bb3015..4d29b39788 100644 --- a/hermes_cli/_parser.py +++ b/hermes_cli/_parser.py @@ -205,6 +205,8 @@ def _add_top_level_flags(parser: argparse.ArgumentParser) -> None: help="Troubleshooting mode: disable ALL customizations — user config, AGENTS.md/memory injection, plugins, and MCP servers (implies --ignore-user-config and --ignore-rules)") inherited(parser, "--tui", action="store_true", default=False, help="Launch the modern TUI instead of the classic REPL") + inherited(parser, "--native", "--tui-native", dest="tui_native", action="store_true", default=False, + help="With --tui: use native terminal scrollback and disable mouse tracking") inherited(parser, "--cli", action="store_true", default=False, help="Force the classic prompt_toolkit REPL (overrides display.interface=tui)") inherited(parser, "--dev", dest="tui_dev", action="store_true", default=False, @@ -308,6 +310,8 @@ def _build_chat_parser(subparsers) -> argparse.ArgumentParser: help="Session source tag for filtering (default: cli). Use 'tool' for third-party integrations that should not appear in user session lists.") inherited(chat_parser, "--tui", action="store_true", default=SUPPRESS, help="Launch the modern TUI instead of the classic REPL") + inherited(chat_parser, "--native", "--tui-native", dest="tui_native", action="store_true", default=SUPPRESS, + help="Use native terminal scrollback and disable mouse tracking") inherited(chat_parser, "--cli", action="store_true", default=SUPPRESS, help="Force the classic prompt_toolkit REPL (overrides display.interface=tui)") inherited(chat_parser, "--dev", dest="tui_dev", action="store_true", default=SUPPRESS, diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 6a22cf6a78..345ebe1329 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -816,6 +816,9 @@ DEFAULT_CONFIG = { # Interface bare `hermes`/`hermes chat` launches: "cli" (prompt_toolkit REPL) | "tui" (Ink). # Flags win: `--cli` forces the REPL, `--tui` / HERMES_TUI=1 forces the TUI. "interface": "cli", + # Native TUI uses the terminal's primary buffer and scrollback instead of the custom + # alternate-screen viewport. Flags win: `--native` / `--tui-native` and `--cli`. + "tui_native": False, # `hermes --tui` auto-resumes the most recent human-facing session (like `hermes -c`). # HERMES_TUI_RESUME= always wins. "tui_auto_resume_recent": False, diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 92fad1ecbd..f392c6f86f 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -289,7 +289,7 @@ def _wants_tui_early(argv: "list[str] | None" = None) -> bool: argv = sys.argv[1:] if "--cli" in argv: return False - if os.environ.get("HERMES_TUI") == "1" or "--tui" in argv: + if os.environ.get("HERMES_TUI") == "1" or any(flag in argv for flag in ("--tui", "--native", "--tui-native")): return True try: if not (sys.stdin.isatty() and sys.stdout.isatty()): @@ -1841,6 +1841,7 @@ def cmd_chat(args): _launch_tui( passthrough.pop("resume"), tui_dev=getattr(args, "tui_dev", False), + native_mode=True if getattr(args, "tui_native", False) else None, model=getattr(args, "model", None), accept_hooks=getattr(args, "accept_hooks", False), **passthrough, diff --git a/hermes_cli/main_tui_launch.py b/hermes_cli/main_tui_launch.py index 30983fe440..bec2eefb6e 100644 --- a/hermes_cli/main_tui_launch.py +++ b/hermes_cli/main_tui_launch.py @@ -343,7 +343,8 @@ def _setup_tui_worktree() -> dict: def _launch_tui( - resume_session_id: Optional[str] = None, tui_dev: bool = False, model: Optional[str] = None, + resume_session_id: Optional[str] = None, tui_dev: bool = False, native_mode: Optional[bool] = None, + model: Optional[str] = None, provider: Optional[str] = None, toolsets: object = None, skills: object = None, verbose: Optional[bool] = None, quiet: bool = False, query: Optional[str] = None, image: Optional[str] = None, worktree: bool = False, checkpoints: bool = False, @@ -373,6 +374,15 @@ def _launch_tui( os.close(active_session_fd) env["HERMES_TUI_ACTIVE_SESSION_FILE"] = active_session_file env.setdefault("NODE_ENV", "development" if tui_dev else "production") + if native_mode is None: + try: + from hermes_cli.config import load_config + from utils import is_truthy_value + display = load_config().get("display", {}) + native_mode = is_truthy_value(display.get("tui_native", False)) if isinstance(display, dict) else False + except Exception: + native_mode = False + env["HERMES_TUI_NATIVE"] = "1" if native_mode else "0" wt_info = None if worktree: @@ -479,7 +489,7 @@ def _resolve_use_tui(args) -> bool: """ if getattr(args, "cli", False): return False - if getattr(args, "tui", False): + if getattr(args, "tui", False) or getattr(args, "tui_native", False): return True try: if not (sys.stdin.isatty() and sys.stdout.isatty()): diff --git a/tests/hermes_cli/test_default_interface_resolution.py b/tests/hermes_cli/test_default_interface_resolution.py index f5af305e95..287adfb7ac 100644 --- a/tests/hermes_cli/test_default_interface_resolution.py +++ b/tests/hermes_cli/test_default_interface_resolution.py @@ -169,6 +169,11 @@ class TestParserFlags: args = self._parser().parse_args(["chat", "--tui"]) assert args.tui is True + def test_native_flag_at_both_parser_levels(self): + parser = self._parser() + assert parser.parse_args(["--native"]).tui_native is True + assert parser.parse_args(["chat", "--tui-native"]).tui_native is True + def test_cli_and_tui_are_relaunch_inherited(self): from hermes_cli.relaunch import _INHERITED_FLAGS_TABLE @@ -176,6 +181,13 @@ class TestParserFlags: assert "--cli" in inherited assert "--tui" in inherited + def test_native_flag_is_relaunch_inherited(self): + from hermes_cli.relaunch import _INHERITED_FLAGS_TABLE + + inherited = {flag for flag, _takes_value in _INHERITED_FLAGS_TABLE} + assert "--native" in inherited + assert "--tui-native" in inherited + # --------------------------------------------------------------------------- # config default — shipped default preserves classic behavior