Files
hermes-agent/tools/read_window_tool.py
Brooklyn Nicholson 04afc8d48e fix(desktop): say why read_window_below cannot see the windows
When enumeration was impossible the tool answered "could not determine the
window underneath (the desktop app did not answer, or window enumeration is
unavailable on this system)" — true, and a dead end. On Linux the two ways it
fails have opposite fixes and neither is guessable from that: a Wayland session
withholds window identity from applications outright, while an X11 session
needs xprop and xwininfo installed, because that is what the enumerator shells
out to.

Answer with the reason instead of nothing. A session with both WAYLAND_DISPLAY
and DISPLAY is XWayland, where xprop can still answer, so it gets the tooling
advice rather than being told to change session type.
2026-08-08 22:17:38 -05:00

72 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Read which OS window sits directly underneath the Hermes desktop window.
The window list lives with the OS, so this tool round-trips through the
gateway's blocking-prompt bridge — the same one `read_terminal` uses:
tui_gateway emits ``window.read.request``, the desktop renderer asks its main
process (which owns native window enumeration) and answers with
``window.read.respond``. This module is just schema + a thin dispatcher over
the platform-injected callback.
"""
import json
from typing import Callable, Optional
from tools.registry import registry, tool_error
def read_window_below_tool(callback: Optional[Callable] = None) -> str:
"""Return the window underneath the Hermes window as a JSON string."""
if callback is None:
return tool_error(
"read_window_below is only available in the Hermes desktop app."
)
try:
raw = callback()
except Exception as exc:
return tool_error(f"Failed to read the window below: {exc}")
if not raw:
return tool_error(
"Could not determine the window underneath (the desktop app did "
"not answer, or window enumeration is unavailable on this system)."
)
# Desktop answers with a JSON object; pass it through, else wrap the raw text.
try:
return json.dumps(json.loads(raw), ensure_ascii=False)
except (TypeError, ValueError):
return json.dumps({"text": str(raw)}, ensure_ascii=False)
READ_WINDOW_BELOW_SCHEMA = {
"name": "read_window_below",
"description": (
"Identify the application window directly underneath (behind) the "
"Hermes desktop window — what the user is working in behind this app. "
"Returns JSON: {window: {app, title, bounds{x,y,width,height}, id}, "
"frontmost: {app, title}, platform}. `title` may be empty when the OS "
"withholds window titles (e.g. macOS without the Screen Recording "
"permission — never prompted for, noted in `note`). Other Hermes "
"windows are skipped: the nearest non-Hermes window is reported. "
"Returns {error, platform} instead where the OS cannot enumerate "
"windows at all (e.g. a Wayland session); `error` says what would fix "
"it, so relay it rather than retrying. "
"Metadata only; this never captures pixels or content of other windows."
),
"parameters": {
"type": "object",
"properties": {},
},
}
registry.register(
name="read_window_below",
toolset="desktop_ui",
schema=READ_WINDOW_BELOW_SCHEMA,
handler=lambda args, **kw: read_window_below_tool(callback=kw.get("callback")),
emoji="🪟",
)