Files
hermes-agent/tools/terminal_tool_macos_open.py
Brooklyn Nicholson 01b21dbc8e fix(terminal): report the open outcome from the final observed frontmost state
The ladder now ends by observing frontmost after the settle and re-assert:
the tool output says whether the opened document actually ended up in front,
and when another application holds focus it names that application — a focus
race can no longer read as plain success (#95261 requirement 2). The
not-front branch gets one extra activate+0.4s so the report reads the state
after a last raise attempt, not before it.
2026-10-02 22:42:55 -05:00

195 lines
8.1 KiB
Python

"""macOS ``open`` frontmost raise-ladder for the terminal tool.
Split out of ``tools/terminal_tool.py`` (PR #95286, Jackal991): a pure,
deterministic string rewrite — no subprocess calls at transform time — that
appends a verified raise-ladder to file-opening ``open`` invocations so the
opened document lands in front of the Hermes window instead of behind it.
"""
import platform
import shlex
# ---------------------------------------------------------------------------
# macOS `open` frontmost raise-ladder
#
# On macOS, when the agent opens a file via a tool call (e.g. `open -a Preview
# file.pdf` or `open file.pdf`), the command returns exit 0 and genuinely
# succeeds — the document loads and a window is created — but the window opens
# BEHIND the Hermes desktop window (Hermes is typically maximised, so the file
# is completely hidden). Because the exit code is 0, the agent reports success
# while the user sees nothing happen.
#
# Root cause: the tool shell runs as a child process of the Electron app; macOS
# does not let a non-frontmost process hand activation to another app, so the
# implicit activation that normally accompanies `open` is silently ignored.
# `osascript -e 'tell application "X" to activate'` alone is NOT reliable (it
# also returns 0 while focus lands elsewhere, e.g. a racing Chrome).
#
# Fix: VERIFY, don't trust the exit code. After an `open`, check `frontmost`
# and escalate via a raise-ladder, then settle ~0.8s, re-check, and re-assert
# once. Each rung is verified before moving on; the report comes from the final
# observed state, never from "a rung returned 0".
# ---------------------------------------------------------------------------
# `open` flags that do NOT open a file in a new frontmost window and must never
# be transformed (conservative: only transform when it clearly opens a file).
_MACOS_OPEN_NO_TRANSFORM_FLAGS = frozenset(
{"-R", "-e", "-t", "-f", "-W", "-n", "-g", "-h", "-b", "-D"}
)
# Shell splice that breaks out of a single-quoted osascript argument so the
# shell expands $_H_APP (handles app names containing spaces, e.g. "Google
# Chrome"). Renders as: " ' " $_H_APP " ' "
_APP_SPLICE = '"\'"$_H_APP"\'"'
def _osascript_app(script: str) -> str:
"""Wrap an AppleScript snippet in an `osascript -e` invocation.
``script`` may reference the target app via the ``_APP_SPLICE`` shell
splice (``$_H_APP``). Errors are suppressed so a swallowed AppleEvent
simply falls through to the next rung of the ladder.
"""
return f"osascript -e '{script}' 2>/dev/null"
def _build_macos_open_raise_ladder(app: str | None, file: str) -> str:
"""Build the shell raise-ladder appended after a macOS ``open`` command.
The ladder verifies the target app is frontmost and escalates through
progressively more forceful activation mechanisms, then settles ~0.8s,
re-checks ``frontmost``, and re-asserts once to absorb a queued activation
from another app. Each rung is verified before moving on.
Args:
app: The target application name (from ``open -a <App>``), or None for
a bare ``open <file>`` where the app is resolved at runtime from
the frontmost process.
file: The file path that was opened.
"""
if app is not None:
app_assign = f"_H_APP={shlex.quote(app)}"
else:
# Bare `open <file>`: the target app is unknown at transform time, so
# resolve it at runtime from whichever process is frontmost after the
# open (the app that actually received the document).
app_assign = (
'_H_APP="$(osascript -e \'tell application "System Events" to get '
'name of first application process whose frontmost is true\' '
'2>/dev/null)"'
)
file_q = shlex.quote(file)
activate = _osascript_app("tell application " + _APP_SPLICE + " to activate")
frontmost = _osascript_app(
'tell application "System Events" to get frontmost of application process '
+ _APP_SPLICE
)
unminimise = _osascript_app(
'tell application "System Events" to set visible of every process to true'
)
# lsappinfo is a different subsystem and works even when AppleEvents are
# swallowed. `find` returns the app's ASN; `setfront` raises it.
asn = 'lsappinfo find "$_H_APP" 2>/dev/null | awk \'{print $1}\''
setfront = '[ -n "$_H_ASN" ] && lsappinfo setfront "$_H_ASN" 2>/dev/null'
# Re-issuing `open -a <App> <file>` on the already-open document is treated
# by macOS as pure activation.
reopen = f"open -a \"$_H_APP\" {file_q} 2>/dev/null"
# Outcome reporting from the FINAL observed frontmost state. The two
# branches are deliberately distinct so a focus race can never read as
# success: the tool output tells the agent (and the user) whether the
# opened document actually ended up in front, and if not, which
# application holds focus. ``file_q`` is a self-contained quoted token, so
# the messages survive paths with spaces or shell metacharacters.
frontmost_name = (
"osascript -e 'tell application \"System Events\" to get name of "
"first application process whose frontmost is true' 2>/dev/null"
)
report = (
f'if {frontmost}; then echo open: {file_q} is now frontmost; '
f'else _H_FOCUS=$({frontmost_name}); '
f'echo open: {file_q} opened but another application holds focus; '
f'echo "frontmost: $_H_FOCUS"; fi'
)
return (
f"{app_assign}; "
f"if ! {frontmost}; then "
f"{activate}; sleep 0.2; "
f"if ! {frontmost}; then "
f"_H_ASN=$({asn}); {setfront}; sleep 0.2; "
f"if ! {frontmost}; then "
f"{reopen}; sleep 0.2; {unminimise}; {activate}; "
f"fi; "
f"fi; "
f"fi; "
f"sleep 0.8; "
f"if ! {frontmost}; then {activate}; sleep 0.4; fi; "
f"{report}"
)
def _transform_macos_open_command(command: str | None, system: str | None = None) -> str | None:
"""Append a frontmost raise-ladder to a macOS ``open`` command.
On macOS, ``open <file>`` / ``open -a <App> <file>`` returns exit 0 and
genuinely opens the document, but the window can land BEHIND the Hermes
desktop window (Hermes is typically maximised), so the user sees nothing
happen while the agent reports success. This transform appends a verified
raise-ladder that brings the opened app to the front.
The transform is a pure, deterministic string rewrite (no subprocess calls
at transform time) so it is unit-testable on any OS by passing ``system``
as data (``"Darwin"`` to exercise the ladder, ``"Linux"`` to prove the
no-op). The actual osascript/lsappinfo calls happen at runtime inside the
rewritten shell command.
Conservative: only transforms a bare ``open <file>`` or
``open -a <App> <file>``. Never transforms ``open`` with no args, the
non-file-opening flags (``-R``/``-e``/``-t``/``-f``/``-W``/``-n``/``-g``/
``-h``/``-b``/``-D``), ``open -a <App>`` with no file, compound commands,
or any non-``open`` command. The original ``open`` runs exactly as written.
"""
if (system if system is not None else platform.system()) != "Darwin":
return command
if command is None:
return command
try:
tokens = shlex.split(command)
except ValueError:
return command
if not tokens or tokens[0] != "open":
return command
args = tokens[1:]
if not args:
return command
first = args[0]
if first.startswith("-"):
if first in _MACOS_OPEN_NO_TRANSFORM_FLAGS:
return command
if first == "-a":
# open -a <App> <file>
if len(args) < 3:
return command
app, file = args[1], args[2]
if file.startswith("-"):
return command
if len(tokens) != 4:
return command
else:
return command
else:
# bare open <file>
file = first
app = None
if len(tokens) != 2:
return command
ladder = _build_macos_open_raise_ladder(app, file)
return f"{command}; {ladder}"