refactor(cli): vim mode is a config key only — drop the /vim slash command
Maintainer ruling: no new slash command for this. `display.vim_mode: true` in config.yaml enables vi keybindings in the composer at startup; the NORMAL/INSERT/REPLACE status-bar label stays. Removes the CommandDef, the handler, its dispatch-table entry and slash-command docs; documents the key under Display Settings.
This commit is contained in:
@@ -2947,8 +2947,8 @@ class HermesCLI(CLIProcessNotificationsMixin, CLIAgentSetupMixin, CLICommandsMix
|
||||
|
||||
self._status_bar_visible = _status_bar_visible_from_display_config(CLI_CONFIG.get("display"))
|
||||
self._battery_visible = bool(CLI_CONFIG["display"].get("battery", False))
|
||||
# Vi/vim editing mode for the input composer (toggled via /vim, persisted to
|
||||
# display.vim_mode). Off by default: prompt_toolkit's standard emacs bindings.
|
||||
# Vi/vim editing mode for the input composer (display.vim_mode, config-only).
|
||||
# Off by default: prompt_toolkit's standard emacs bindings.
|
||||
self._vim_mode = bool(CLI_CONFIG["display"].get("vim_mode", False))
|
||||
# Hide rules + status bar until the next input after a resize, so SIGWINCH cannot
|
||||
# stamp a fresh status bar over one the terminal just reflowed into scrollback.
|
||||
@@ -3763,7 +3763,7 @@ class HermesCLI(CLIProcessNotificationsMixin, CLIAgentSetupMixin, CLICommandsMix
|
||||
if _STEADY_CURSOR is not None:
|
||||
extra_kw["cursor"] = _STEADY_CURSOR
|
||||
if EditingMode is not None:
|
||||
# Vi editing mode when display.vim_mode is on (toggled at runtime by /vim).
|
||||
# Vi editing mode when display.vim_mode is on.
|
||||
# EMACS is prompt_toolkit's own default, so non-opted-in behaviour is unchanged.
|
||||
extra_kw["editing_mode"] = EditingMode.VI if self._vim_mode else EditingMode.EMACS
|
||||
return Application(
|
||||
|
||||
@@ -97,39 +97,6 @@ class CLIStatusBarMixin:
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _handle_vim_command(self, cmd_original: str) -> None:
|
||||
"""``/vim`` toggles vi keybindings in the composer, ``/vim on|off`` sets, ``/vim status``
|
||||
reports. Persisted to ``display.vim_mode``; applied to the live prompt_toolkit
|
||||
Application immediately, no restart needed."""
|
||||
from cli import save_config_value
|
||||
from prompt_toolkit.enums import EditingMode
|
||||
parts = (cmd_original or "").split()
|
||||
arg = parts[1].strip().lower() if len(parts) > 1 else ""
|
||||
|
||||
if arg in ("status", "show"):
|
||||
self._console_print(f" Vim mode {'on' if self._vim_mode else 'off'}")
|
||||
return
|
||||
|
||||
if arg in ("on", "true", "yes"):
|
||||
target = True
|
||||
elif arg in ("off", "false", "no"):
|
||||
target = False
|
||||
elif arg in ("", "toggle"):
|
||||
target = not self._vim_mode
|
||||
else:
|
||||
self._console_print(" Usage: /vim [on|off|status]")
|
||||
return
|
||||
|
||||
self._vim_mode = target
|
||||
save_config_value("display.vim_mode", target)
|
||||
app = getattr(self, "_app", None)
|
||||
if app is not None:
|
||||
app.editing_mode = EditingMode.VI if target else EditingMode.EMACS
|
||||
if target:
|
||||
self._console_print(" Vim mode on — Esc for NORMAL, i to insert")
|
||||
else:
|
||||
self._console_print(" Vim mode off — standard keybindings")
|
||||
|
||||
def _handle_battery_command(self, cmd_original: str) -> None:
|
||||
"""``/battery`` toggles, ``/battery on|off`` sets, ``/battery status`` reports the
|
||||
setting plus a live reading. Persisted to ``display.battery``."""
|
||||
|
||||
@@ -163,9 +163,6 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
||||
CommandDef("battery", "Toggle a color-coded battery indicator in the status bar",
|
||||
"Configuration", cli_only=True, args_hint="[on|off|status]",
|
||||
subcommands=("on", "off", "status")),
|
||||
CommandDef("vim", "Toggle vim keybindings in the input composer",
|
||||
"Configuration", cli_only=True, args_hint="[on|off|status]",
|
||||
subcommands=("on", "off", "status")),
|
||||
CommandDef("timestamps", "Toggle [HH:MM] timestamps on messages and /history", "Configuration",
|
||||
cli_only=True, args_hint="[on|off|status]",
|
||||
subcommands=("on", "off", "status"), aliases=("ts",)),
|
||||
|
||||
@@ -837,7 +837,7 @@ DEFAULT_CONFIG = {
|
||||
# fights terminal auto-scroll in non-fullscreen mode.
|
||||
# See #45592.
|
||||
"cli_refresh_interval": 1.0,
|
||||
# Vi/vim keybindings in the CLI input composer (toggled by /vim).
|
||||
# Vi/vim keybindings in the CLI input composer (config-only, no slash command).
|
||||
# Off by default, preserving prompt_toolkit's standard emacs bindings.
|
||||
"vim_mode": False,
|
||||
"user_message_preview": { # CLI: submitted user-message lines echoed to scrollback
|
||||
|
||||
@@ -18,7 +18,6 @@ OLD_CHAIN_COMMANDS = [
|
||||
"blueprint", "curator", "kanban", "skills", "learn", "init", "memory",
|
||||
"platforms", "status", "context", "egress", "statusbar", "diff", "battery",
|
||||
"timestamps", "verbose", "focus", "footer", "yolo", "approvals", "reasoning",
|
||||
"vim",
|
||||
"fast", "compress", "usage", "subscription", "topup", "insights", "copy",
|
||||
"debug", "update", "version", "paste", "image", "reload", "reload-mcp",
|
||||
"reload-skills", "bundles", "browser", "plugins", "rollback", "snapshot",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Tests for the /vim CLI command and display.vim_mode config handling."""
|
||||
"""display.vim_mode: config-only vi keybindings for the CLI composer."""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from prompt_toolkit.enums import EditingMode
|
||||
|
||||
@@ -13,48 +12,6 @@ def _import_cli():
|
||||
return cli_mod
|
||||
|
||||
|
||||
class TestHandleVimCommand(unittest.TestCase):
|
||||
"""/vim toggles vi editing mode, persists it, and applies it live."""
|
||||
|
||||
def _make_cli(self, vim_mode=False, app=None):
|
||||
return SimpleNamespace(
|
||||
_vim_mode=vim_mode,
|
||||
_app=app,
|
||||
_console_print=lambda *a, **k: None,
|
||||
)
|
||||
|
||||
def test_toggle_persists_and_applies_to_running_app(self):
|
||||
cli_mod = _import_cli()
|
||||
app = SimpleNamespace(editing_mode=EditingMode.EMACS)
|
||||
stub = self._make_cli(vim_mode=False, app=app)
|
||||
|
||||
with patch.object(cli_mod, "save_config_value") as mock_save:
|
||||
cli_mod.HermesCLI._handle_vim_command(stub, "/vim")
|
||||
self.assertTrue(stub._vim_mode)
|
||||
self.assertEqual(app.editing_mode, EditingMode.VI)
|
||||
mock_save.assert_called_once_with("display.vim_mode", True)
|
||||
|
||||
with patch.object(cli_mod, "save_config_value") as mock_save:
|
||||
cli_mod.HermesCLI._handle_vim_command(stub, "/vim off")
|
||||
self.assertFalse(stub._vim_mode)
|
||||
self.assertEqual(app.editing_mode, EditingMode.EMACS)
|
||||
mock_save.assert_called_once_with("display.vim_mode", False)
|
||||
|
||||
def test_status_and_invalid_args_change_nothing(self):
|
||||
cli_mod = _import_cli()
|
||||
printed = []
|
||||
stub = self._make_cli(vim_mode=True)
|
||||
stub._console_print = lambda msg: printed.append(str(msg))
|
||||
|
||||
with patch.object(cli_mod, "save_config_value") as mock_save:
|
||||
cli_mod.HermesCLI._handle_vim_command(stub, "/vim status")
|
||||
cli_mod.HermesCLI._handle_vim_command(stub, "/vim sideways")
|
||||
|
||||
mock_save.assert_not_called()
|
||||
self.assertTrue(stub._vim_mode)
|
||||
self.assertIn("Usage", " ".join(printed))
|
||||
|
||||
|
||||
class TestVimModeLabel(unittest.TestCase):
|
||||
"""The status-bar label reflects the live vi input mode; off/no-app yields empty."""
|
||||
|
||||
@@ -76,5 +33,14 @@ class TestVimModeLabel(unittest.TestCase):
|
||||
self.assertEqual(cli_mod.HermesCLI._vim_mode_label(stub), "REPLACE")
|
||||
|
||||
|
||||
class TestNoSlashCommand(unittest.TestCase):
|
||||
"""vim_mode is a config key only: no /vim command is registered."""
|
||||
|
||||
def test_vim_not_in_command_registry(self):
|
||||
from hermes_cli.commands import COMMAND_REGISTRY
|
||||
|
||||
self.assertNotIn("vim", {c.name for c in COMMAND_REGISTRY})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -88,7 +88,6 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
|
||||
| `/import <archive.tar.gz> [--name <name>]` | **CLI only.** Install a profile archive as a new profile, inferring the name from the archive unless `--name` is given. Refuses to overwrite an existing profile and cannot import as `default`. Creates a shell wrapper when the name is free. See [Export and import a profile file](../user-guide/profile-distributions.md#export-and-import-a-profile-file). |
|
||||
| `/statusbar` (alias: `/sb`) | Toggle the context/model status bar on or off |
|
||||
| `/battery [on\|off\|status]` | Toggle a color-coded battery read-out as the first status-bar element (off by default; no-op without a battery). |
|
||||
| `/vim [on\|off\|status]` | Toggle vi/vim keybindings in the input composer (off by default). The live NORMAL/INSERT/REPLACE mode shows at the right of the status bar; persisted to `display.vim_mode`. |
|
||||
| `/voice [on\|off\|tts\|status]` | Toggle CLI voice mode and spoken playback. Recording uses `voice.record_key` (default: `Ctrl+B`). |
|
||||
| `/yolo` | Toggle YOLO mode — skip all dangerous command approval prompts. |
|
||||
| `/approvals [manual\|smart\|off]` | Show or set the persistent dangerous-command approval mode. |
|
||||
@@ -308,7 +307,7 @@ The messaging gateway supports the following built-in commands inside Telegram,
|
||||
|
||||
## Notes
|
||||
|
||||
- `/skin`, `/snapshot`, `/export`, `/import`, `/reload`, `/tools`, `/toolsets`, `/browser`, `/config`, `/cron`, `/platforms`, `/paste`, `/image`, `/statusbar`, `/battery`, `/vim`, `/focus`, `/plugins`, `/indicator`, `/wake`, `/journey`, `/redraw`, `/clear`, `/history`, `/save`, `/copy`, `/handoff`, `/prompt`, `/pet`, `/hatch`, `/timestamps`, `/subscription`, and `/quit` are **CLI-only** commands.
|
||||
- `/skin`, `/snapshot`, `/export`, `/import`, `/reload`, `/tools`, `/toolsets`, `/browser`, `/config`, `/cron`, `/platforms`, `/paste`, `/image`, `/statusbar`, `/battery`, `/focus`, `/plugins`, `/indicator`, `/wake`, `/journey`, `/redraw`, `/clear`, `/history`, `/save`, `/copy`, `/handoff`, `/prompt`, `/pet`, `/hatch`, `/timestamps`, `/subscription`, and `/quit` are **CLI-only** commands.
|
||||
- `/skills` is **CLI-only for search/browse/install**; its write-approval review subcommands (`pending`, `approve`, `reject`, `diff`, `approval`) also work on messaging platforms when `skills.write_approval` is on. `/memory` works on **both** surfaces.
|
||||
- `/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.
|
||||
|
||||
@@ -1993,6 +1993,7 @@ display:
|
||||
show_reasoning: true # Show model reasoning/thinking above each response (default: true; toggle with /reasoning show|hide)
|
||||
streaming: false # Stream tokens to terminal as they arrive (real-time output)
|
||||
show_cost: false # Show estimated $ cost in the CLI status bar
|
||||
vim_mode: false # CLI only: vi/vim keybindings in the input composer (Esc → NORMAL, i → INSERT). The live NORMAL/INSERT/REPLACE mode shows at the right of the status bar. Config-only, read at startup.
|
||||
timestamps: false # When true, prefixes user and assistant labels with timestamps in the CLI / TUI transcript
|
||||
timestamp_format: "%H:%M" # strftime format for those timestamps (e.g. "%b-%d %H:%M" for month-day)
|
||||
tool_preview_length: 0 # Max chars for tool call previews (0 = no limit, show full paths/commands)
|
||||
|
||||
Reference in New Issue
Block a user