632078bca7
Extend _ring_bell() so display.bell_on_prompt / bell_on_complete also emit terminal-native desktop notifications from the same six call sites (clarify, clarify batch, approval incl. computer_use, sudo password, secret capture, turn complete). No new config keys. - OSC 9 (ESC ] 9 ; body BEL): Ghostty / iTerm2 / Kitty / WezTerm raise an OS notification; unknown terminals drop it. Body is "Hermes: <context>" with C0 controls and DEL stripped. Written to /dev/tty (prompt_toolkit's stdout wrapper can buffer/strip raw escapes) with a sys.stdout fallback. - Warp OSC 777 warp://cli-agent (agent "hermes", event permission_request / stop, compact JSON mirroring build-payload.sh). Gated on TERM_PROGRAM=WarpTerminal + WARP_CLI_AGENT_PROTOCOL_VERSION + the should-use-structured.sh broken-build floor (stable/preview builds at or before v0.2026.03.25.08.24.*_05 rejected). Never raises. Salvages #58957 and #100805. Co-authored-by: glitchbunny0 <glitchbunny0@proton.me> Co-authored-by: harsha-usethread <harsha@usethread.io>
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""Terminal-native desktop notifications: OSC 9 and Warp's OSC 777 CLI-agent protocol.
|
|
|
|
Both emitters ride on the existing ``display.bell_on_prompt`` /
|
|
``display.bell_on_complete`` flags (see ``cli._ring_bell``) — no extra config.
|
|
|
|
- **OSC 9** (``ESC ] 9 ; <body> BEL``): Ghostty, iTerm2, Kitty and WezTerm
|
|
raise an OS notification; terminals that don't know the sequence drop it.
|
|
- **OSC 777** (``ESC ] 777 ; notify ; warp://cli-agent ; <json> BEL``): Warp's
|
|
structured CLI-agent protocol (tab status + notification mailbox). Only sent
|
|
when Warp advertises support and the build is newer than the last release
|
|
that set the protocol var without being able to render the payload.
|
|
|
|
Sequences are written to ``/dev/tty`` because prompt_toolkit's stdout wrapper
|
|
can buffer or strip raw escapes; when ``/dev/tty`` can't be opened (Windows,
|
|
no controlling terminal) they fall back to ``sys.stdout``. Never raises.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
_C0_AND_DEL = re.compile(r"[\x00-\x1f\x7f]")
|
|
_WARP_PROTOCOL_VERSION = 1
|
|
# Last Warp release per channel that set WARP_CLI_AGENT_PROTOCOL_VERSION but
|
|
# could not render structured payloads (Warp's reference agent plugin,
|
|
# should-use-structured.sh). Bash compares these lexicographically; so do we.
|
|
_WARP_LAST_BROKEN = {
|
|
"stable": "v0.2026.03.25.08.24.stable_05",
|
|
"preview": "v0.2026.03.25.08.24.preview_05",
|
|
}
|
|
|
|
|
|
def _write_tty(seq: str) -> None:
|
|
"""Write raw escapes to /dev/tty, falling back to sys.stdout. Never raises."""
|
|
try:
|
|
with open("/dev/tty", "w", encoding="utf-8") as tty:
|
|
tty.write(seq)
|
|
return
|
|
except OSError:
|
|
pass
|
|
try:
|
|
sys.stdout.write(seq)
|
|
sys.stdout.flush()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def osc9(body: str) -> str:
|
|
"""OSC 9 sequence with C0 controls and DEL stripped from the body."""
|
|
return f"\x1b]9;{_C0_AND_DEL.sub('', body)}\x07"
|
|
|
|
|
|
def warp_supported(env=None) -> bool:
|
|
"""True when running in a Warp build that can render OSC 777 agent payloads."""
|
|
env = os.environ if env is None else env
|
|
if env.get("TERM_PROGRAM") != "WarpTerminal" or not env.get("WARP_CLI_AGENT_PROTOCOL_VERSION"):
|
|
return False
|
|
client = env.get("WARP_CLIENT_VERSION", "")
|
|
if not client:
|
|
return False
|
|
for channel, last_broken in _WARP_LAST_BROKEN.items():
|
|
if channel in client and client <= last_broken:
|
|
return False
|
|
return True
|
|
|
|
|
|
def warp_osc777(event: str, detail: str, session_id: str = "") -> str:
|
|
"""OSC 777 ``warp://cli-agent`` notification; ``event`` is ``stop`` or ``permission_request``.
|
|
|
|
Payload mirrors the reference plugin's build-payload.sh: common fields plus
|
|
``summary`` (permission_request) or ``response`` (stop), truncated to 200.
|
|
"""
|
|
try:
|
|
advertised = int(os.environ.get("WARP_CLI_AGENT_PROTOCOL_VERSION", "1"))
|
|
except ValueError:
|
|
advertised = 1
|
|
cwd = os.getcwd()
|
|
payload = {
|
|
"v": min(advertised, _WARP_PROTOCOL_VERSION),
|
|
"agent": "hermes",
|
|
"event": event,
|
|
"session_id": session_id,
|
|
"cwd": cwd,
|
|
"project": os.path.basename(cwd),
|
|
}
|
|
payload["summary" if event == "permission_request" else "response"] = detail[:200]
|
|
return f"\x1b]777;notify;warp://cli-agent;{json.dumps(payload, separators=(',', ':'))}\x07"
|
|
|
|
|
|
def notify(context: str, *, prompt: bool, session_id: str = "", detail: str = "") -> None:
|
|
"""Emit OSC 9 (plus Warp OSC 777 when supported) for a blocking prompt or turn end."""
|
|
seq = osc9(f"Hermes: {context}")
|
|
if warp_supported():
|
|
event = "permission_request" if prompt else "stop"
|
|
seq += warp_osc777(event, detail or context, session_id)
|
|
_write_tty(seq)
|