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:
teknium1
2026-09-12 19:18:24 -07:00
committed by Teknium
parent 4f03480787
commit 08a2e7dbcc
8 changed files with 16 additions and 87 deletions
+3 -3
View File
@@ -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(
-33
View File
@@ -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``."""
-3
View File
@@ -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",)),
+1 -1
View File
@@ -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
-1
View File
@@ -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",
+10 -44
View File
@@ -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()
+1 -2
View File
@@ -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.
+1
View File
@@ -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)