From 4858e26eaa1fff37bc5580a510569f935b2b45b6 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 28 Apr 2026 14:24:50 -0500 Subject: [PATCH] feat(tui): port classic CLI /reload (.env hot-reload) to TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classic CLI exposes ``/reload`` (re-reads ~/.hermes/.env into ``os.environ`` via ``hermes_cli.config.reload_env``) so newly added API keys take effect without restarting the session. The TUI was missing the parity command, so users had to Ctrl+C out and ``hermes --tui`` again whenever they added or rotated a credential. Three small wires: * New ``reload.env`` JSON-RPC method in ``tui_gateway/server.py`` that delegates to ``hermes_cli.config.reload_env`` and returns the count of vars updated. * New ``/reload`` slash command in ``ui-tui/src/app/slash/commands/ops.ts`` matching the existing ``/reload-mcp`` pattern (native RPC, no slash worker). * Drop ``cli_only=True`` from the ``reload`` ``CommandDef`` in ``hermes_cli/commands.py`` so help/menus surface it in the TUI too. ``reload_env`` itself is environment-agnostic. Same caveat as classic CLI: the *currently constructed* agent's credential pool / provider routing does not auto-rebuild. Users who want a brand-new credential resolution should follow with ``/new``. Tests: * New ``test_reload_env_rpc_calls_hermes_cli_reload_env`` confirms RPC delegates and reports the count. * New ``test_reload_env_rpc_surfaces_errors`` confirms exceptions are rendered as JSON-RPC errors. * ``createSlashHandler.test.ts`` slash-parity matrix extended with ``['/reload', 'reload.env', {}]`` so we can't regress the routing. Validation: scripts/run_tests.sh tests/test_tui_gateway_server.py — 92/92. scripts/run_tests.sh tests/hermes_cli/test_commands.py — 128/128. cd ui-tui && npm run type-check — clean; npm test --run — 390/390. --- hermes_cli/commands.py | 3 +- tests/test_tui_gateway_server.py | 36 +++++++++++++++++++ tui_gateway/server.py | 20 +++++++++++ .../src/__tests__/createSlashHandler.test.ts | 1 + ui-tui/src/app/slash/commands/ops.ts | 19 ++++++++++ ui-tui/src/gatewayTypes.ts | 4 +++ 6 files changed, 81 insertions(+), 2 deletions(-) diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 7160c16f9d..b40522a7b3 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -148,8 +148,7 @@ COMMAND_REGISTRY: list[CommandDef] = [ CommandDef("cron", "Manage scheduled tasks", "Tools & Skills", cli_only=True, args_hint="[subcommand]", subcommands=("list", "add", "create", "edit", "pause", "resume", "run", "remove")), - CommandDef("reload", "Reload .env variables into the running session", "Tools & Skills", - cli_only=True), + CommandDef("reload", "Reload .env variables into the running session", "Tools & Skills"), CommandDef("reload-mcp", "Reload MCP servers from config", "Tools & Skills", aliases=("reload_mcp",)), CommandDef("browser", "Connect browser tools to your live Chrome via CDP", "Tools & Skills", diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 1c8eecf060..dacc55df5b 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -3448,3 +3448,39 @@ def test_config_set_indicator_none_keeps_blank_repr(monkeypatch): ) assert "error" in resp assert "unknown indicator: ''" in resp["error"]["message"] + + +# ── reload.env ─────────────────────────────────────────────────────── + + +def test_reload_env_rpc_calls_hermes_cli_reload_env(monkeypatch): + """reload.env mirrors classic CLI's `/reload` — re-reads ~/.hermes/.env + into the gateway process and reports the count of vars updated.""" + calls = {"n": 0} + + def _fake_reload(): + calls["n"] += 1 + return 7 + + fake = types.SimpleNamespace(reload_env=_fake_reload) + with patch.dict(sys.modules, {"hermes_cli.config": fake}): + resp = server.handle_request( + {"id": "1", "method": "reload.env", "params": {}} + ) + + assert resp["result"] == {"updated": 7} + assert calls["n"] == 1 + + +def test_reload_env_rpc_surfaces_errors(monkeypatch): + def _broken(): + raise RuntimeError("env path locked") + + fake = types.SimpleNamespace(reload_env=_broken) + with patch.dict(sys.modules, {"hermes_cli.config": fake}): + resp = server.handle_request( + {"id": "1", "method": "reload.env", "params": {}} + ) + + assert "error" in resp + assert "env path locked" in resp["error"]["message"] diff --git a/tui_gateway/server.py b/tui_gateway/server.py index e5fda15a5c..377ed64a58 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3429,6 +3429,26 @@ def _(rid, params: dict) -> dict: return _err(rid, 5015, str(e)) +@method("reload.env") +def _(rid, params: dict) -> dict: + """Re-read ~/.hermes/.env into the gateway process — TUI parity with + classic CLI's ``/reload`` (cli.py). Newly added API keys take effect + on the next agent call without restarting the TUI. + + The credential pool / provider routing for any *already-constructed* + agent does not auto-rebuild — that's the same behaviour as classic + CLI's ``/reload``. Users who want a brand-new credential resolution + should follow with ``/new``. + """ + try: + from hermes_cli.config import reload_env + + count = reload_env() + return _ok(rid, {"updated": int(count)}) + except Exception as e: + return _err(rid, 5015, str(e)) + + _TUI_HIDDEN: frozenset[str] = frozenset( { "sethome", diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index db5e37347a..3ec340b8a2 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -194,6 +194,7 @@ describe('createSlashHandler', () => { ['/browser status', 'browser.manage', { action: 'status', session_id: null }], ['/browser connect', 'browser.manage', { action: 'connect', session_id: null, url: 'http://127.0.0.1:9222' }], ['/reload-mcp', 'reload.mcp', { session_id: null }], + ['/reload', 'reload.env', {}], ['/stop', 'process.stop', {}], ['/fast status', 'config.get', { key: 'fast', session_id: null }], ['/busy status', 'config.get', { key: 'busy' }], diff --git a/ui-tui/src/app/slash/commands/ops.ts b/ui-tui/src/app/slash/commands/ops.ts index 21cd52b341..7353f6fb4d 100644 --- a/ui-tui/src/app/slash/commands/ops.ts +++ b/ui-tui/src/app/slash/commands/ops.ts @@ -2,6 +2,7 @@ import type { BrowserManageResponse, DelegationPauseResponse, ProcessStopResponse, + ReloadEnvResponse, ReloadMcpResponse, RollbackDiffResponse, RollbackListResponse, @@ -89,6 +90,24 @@ export const opsCommands: SlashCommand[] = [ } }, + { + help: 're-read ~/.hermes/.env into the running gateway (CLI parity)', + name: 'reload', + run: (_arg, ctx) => { + ctx.gateway + .rpc('reload.env', {}) + .then( + ctx.guarded(r => { + const n = Number(r.updated ?? 0) + const noun = n === 1 ? 'var' : 'vars' + + ctx.transcript.sys(`reloaded .env (${n} ${noun} updated)`) + }) + ) + .catch(ctx.guardedErr) + } + }, + { help: 'manage browser CDP connection [connect|disconnect|status]', name: 'browser', diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index 8a518e385e..1f43096340 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -308,6 +308,10 @@ export interface ReloadMcpResponse { status?: string } +export interface ReloadEnvResponse { + updated?: number +} + export interface ProcessStopResponse { killed?: number }