diff --git a/contributors/emails/glitchbunny0@proton.me b/contributors/emails/glitchbunny0@proton.me new file mode 100644 index 0000000000..5f679947b3 --- /dev/null +++ b/contributors/emails/glitchbunny0@proton.me @@ -0,0 +1,2 @@ +glitchbunny0 +# PR #58957 salvage diff --git a/contributors/emails/harsha@usethread.io b/contributors/emails/harsha@usethread.io new file mode 100644 index 0000000000..78f9244a8c --- /dev/null +++ b/contributors/emails/harsha@usethread.io @@ -0,0 +1,2 @@ +harshmoney123 +# PR #100805 salvage diff --git a/tests/hermes_cli/test_terminal_notify.py b/tests/hermes_cli/test_terminal_notify.py new file mode 100644 index 0000000000..fce8b90d5e --- /dev/null +++ b/tests/hermes_cli/test_terminal_notify.py @@ -0,0 +1,50 @@ +"""display.bell_on_prompt / bell_on_complete also drive OSC 9 + Warp OSC 777 via _ring_bell.""" + +import json + +from cli import HermesCLI +from hermes_cli import terminal_notify + +_WARP_OK = { + "TERM_PROGRAM": "WarpTerminal", + "WARP_CLI_AGENT_PROTOCOL_VERSION": "1", + "WARP_CLIENT_VERSION": "v0.2026.08.01.00.00.stable_01", +} + + +def _ring(monkeypatch, *, flag_on, env, **kwargs): + for key in _WARP_OK: + monkeypatch.delenv(key, raising=False) + for key, value in env.items(): + monkeypatch.setenv(key, value) + written = [] + monkeypatch.setattr(terminal_notify, "_write_tty", written.append) + cli = HermesCLI.__new__(HermesCLI) + cli.bell_on_prompt = flag_on + cli.session_id = "sess-1" + cli._ring_bell(prompt=True, **kwargs) + return "".join(written) + + +def test_osc9_body_emitted_and_sanitized_only_when_flag_on(monkeypatch): + out = _ring(monkeypatch, flag_on=True, env={}, context="approval\x1b\x07\x00\x7f!") + assert out == "\x1b]9;Hermes: approval!\x07" + assert _ring(monkeypatch, flag_on=False, env={}, context="approval") == "" + + +def test_warp_osc777_only_under_supported_warp_build(monkeypatch): + out = _ring(monkeypatch, flag_on=True, env=_WARP_OK, context="approval", detail="rm -rf build") + prefix = "\x1b]777;notify;warp://cli-agent;" + assert out.count(prefix) == 1 + payload = json.loads(out.split(prefix, 1)[1].rstrip("\x07")) + assert payload["agent"] == "hermes" + assert payload["event"] == "permission_request" + assert payload["summary"] == "rm -rf build" + assert payload["session_id"] == "sess-1" + assert payload["v"] == 1 + # Broken build (advertises the protocol var but can't render) → OSC 9 only. + broken = dict(_WARP_OK, WARP_CLIENT_VERSION="v0.2026.03.25.08.24.stable_05") + assert prefix not in _ring(monkeypatch, flag_on=True, env=broken, context="approval") + # Not Warp at all → OSC 9 only. + not_warp = dict(_WARP_OK, TERM_PROGRAM="ghostty") + assert prefix not in _ring(monkeypatch, flag_on=True, env=not_warp, context="approval") diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 6eca90e672..bc4d42ef01 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1910,6 +1910,10 @@ display: resume_display: full # full (show previous messages on resume) | minimal (one-liner only) bell_on_complete: false # Play terminal bell when agent finishes (great for long tasks) bell_on_prompt: false # Play terminal bell when a blocking prompt opens (clarify, approval, sudo password, secret capture) — works over SSH + # Both bell flags also emit an OSC 9 desktop notification (Ghostty, iTerm2, Kitty, WezTerm raise an OS + # notification; other terminals ignore it) and, inside Warp (TERM_PROGRAM=WarpTerminal with the CLI-agent + # protocol advertised), a warp://cli-agent OSC 777 event (`stop` on completion, `permission_request` on + # blocking prompts) so Warp's tab status and notification mailbox track Hermes. No extra keys needed. 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