fix(notify): restrict OSC to native-rendering terminals; hint osascript perms
Research finding: terminfo.dev "support" for OSC 9/777 only means the parser consumes the sequence — VS Code/Cursor and Apple Terminal silently drop it without rendering anything (microsoft/vscode#294247, anthropics/claude-code#28338). Emitting OSC there made notifications no-op AND skipped the OS fallback. - _detect_terminal_osc now returns a flavor only for terminals that actually render: iTerm2, Ghostty, kitty, WezTerm. Everything else (VS Code/Cursor, Apple Terminal, unknown) falls through to the osascript path. VS Code/Cursor users wanting click-to-focus can install the "Terminal Notification" extension, which parses the OSC we already emit — documented, not assumed. - Add a one-time WARNING when the osascript fallback runs: on macOS Sequoia+, osascript notifications are attributed to "Script Editor" and silently dropped (exit 0, nothing shown) until the user grants Script Editor notification permission once. The hint spells out the fix so users aren't stuck staring at a no-op.
This commit is contained in:
parent
b59b1cfb12
commit
82b9c44cbd
@ -81,9 +81,11 @@ def test_consume_is_scoped_to_its_session(home, monkeypatch):
|
||||
[
|
||||
({"TERM_PROGRAM": "iTerm.app"}, "osc9"),
|
||||
({"TERM_PROGRAM": "WarpTerminal"}, "osc9"),
|
||||
({"TERM_PROGRAM": "vscode"}, "osc777"),
|
||||
({"KITTY_WINDOW_ID": "1"}, "osc9"),
|
||||
({"WEZTERM_PANE": "0"}, "osc777"),
|
||||
# VS Code / Cursor and Apple Terminal parse but DON'T render OSC
|
||||
# notifications — must fall through to the OS-level path.
|
||||
({"TERM_PROGRAM": "vscode"}, None),
|
||||
({"TERM_PROGRAM": "Apple_Terminal"}, None),
|
||||
({}, None),
|
||||
],
|
||||
@ -152,6 +154,16 @@ def test_desktop_notification_prefers_terminal_over_os(monkeypatch):
|
||||
assert macos_called == []
|
||||
|
||||
|
||||
def test_osascript_permission_hint_fires_once(monkeypatch, caplog):
|
||||
import logging
|
||||
monkeypatch.setattr(notify_utils, "_OSASCRIPT_HINT_SHOWN", False)
|
||||
with caplog.at_level(logging.WARNING, logger=notify_utils.logger.name):
|
||||
notify_utils._osascript_permission_hint_once()
|
||||
notify_utils._osascript_permission_hint_once()
|
||||
hits = [r for r in caplog.records if "Script Editor" in r.getMessage()]
|
||||
assert len(hits) == 1
|
||||
|
||||
|
||||
def test_macos_prefers_terminal_notifier_when_present(monkeypatch):
|
||||
runs = []
|
||||
monkeypatch.setattr(notify_utils.shutil, "which",
|
||||
|
||||
@ -225,10 +225,38 @@ def _show_notification_macos(title: str, message: str) -> None:
|
||||
timeout=5, capture_output=True,
|
||||
)
|
||||
logger.debug("notify: macOS notification sent via osascript")
|
||||
_osascript_permission_hint_once()
|
||||
except Exception as e:
|
||||
logger.debug("notify: osascript notification failed: %s", e)
|
||||
|
||||
|
||||
_OSASCRIPT_HINT_SHOWN = False
|
||||
|
||||
|
||||
def _osascript_permission_hint_once() -> None:
|
||||
"""Warn once that osascript notifications need Script Editor permission.
|
||||
|
||||
On macOS Sequoia+, ``osascript display notification`` is attributed to
|
||||
``com.apple.ScriptEditor2`` and is silently dropped until the user grants
|
||||
Script Editor notification permission — the command still exits 0, so a
|
||||
user sees "nothing happened" with no error. Surface the one-time fix so
|
||||
they aren't stuck. (Native terminals using the OSC path never reach here.)
|
||||
"""
|
||||
global _OSASCRIPT_HINT_SHOWN
|
||||
if _OSASCRIPT_HINT_SHOWN:
|
||||
return
|
||||
_OSASCRIPT_HINT_SHOWN = True
|
||||
logger.warning(
|
||||
"Desktop notifications use osascript on this terminal, which macOS "
|
||||
"delivers as 'Script Editor' — banners are suppressed until you grant "
|
||||
"permission once: run `open -a 'Script Editor'`, execute "
|
||||
"`display notification \"test\" with title \"test\"` inside it, click "
|
||||
"Allow, then check System Settings > Notifications > Script Editor. "
|
||||
"For native banners, run Hermes in iTerm2/Ghostty/kitty/WezTerm, or "
|
||||
"install the VS Code 'Terminal Notification' extension for Cursor."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Terminal-native notifications (OSC escape sequences)
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -253,8 +281,16 @@ def _show_notification_macos(title: str, message: str) -> None:
|
||||
def _detect_terminal_osc() -> Optional[str]:
|
||||
"""Return the OSC notification flavor for the current terminal, or None.
|
||||
|
||||
None means "unknown / unsupported (e.g. Apple Terminal)" — the caller
|
||||
should fall back to an OS-level notifier.
|
||||
Only terminals that actually *render* an OS notification from the escape
|
||||
sequence are listed. terminfo.dev marks many terminals as "supporting"
|
||||
OSC 9/777, but that only means their parser consumes the sequence — VS
|
||||
Code/Cursor and Apple Terminal silently drop it without showing anything
|
||||
(confirmed: microsoft/vscode#294247, anthropics/claude-code#28338). Those
|
||||
return None so the caller falls back to an OS-level notifier (osascript).
|
||||
|
||||
VS Code / Cursor users who want click-to-focus can install the "Terminal
|
||||
Notification" extension (it parses OSC 9/777 from the terminal stream);
|
||||
that's a user-side opt-in, not something we can assume here.
|
||||
"""
|
||||
if os.environ.get("KITTY_WINDOW_ID"):
|
||||
return "osc9" # kitty also speaks the legacy OSC 9
|
||||
@ -264,9 +300,8 @@ def _detect_terminal_osc() -> Optional[str]:
|
||||
return {
|
||||
"iTerm.app": "osc9",
|
||||
"WarpTerminal": "osc9",
|
||||
"Hyper": "osc9",
|
||||
"ghostty": "osc777",
|
||||
"vscode": "osc777", # VS Code AND Cursor integrated terminals
|
||||
"WezTerm": "osc777",
|
||||
}.get(tp)
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user