fix(agent): the desktop's tools reach it on remote and cloud backends too

The pane, in-app browser, and reaction tools were gated on HERMES_DESKTOP=1 —
an env var set only on backends Electron spawns itself (local and SSH). A
desktop client connected to a plain URL gateway or Hermes Cloud lost all six:
they were stripped from the schema before the model saw them, on the same
backend whose platform hint was telling it "you are chatting inside the Hermes
desktop app". open_preview, read_preview, read_terminal, close_terminal,
focus_pane, and react_to_message were all silently absent.

The client is not the host. Capability now resolves from the session's own
source, which session.create already carries:

- The six tools move into a `desktop_ui` toolset, off _HERMES_CORE_TOOLS so no
  other platform pays their schema.
- _gui_surface_toolsets(platform) folds `desktop_ui` (and the existing
  `project` tools) into the GUI gateway's resolution when the session's
  platform is the desktop app — the same answer on every topology.
- check_fn drops the env probe. It kept the one thing that is genuinely a
  per-process/user fact: react_to_message's display.message_reactions opt-in,
  which the desktop mirrors onto whichever gateway it is connected to.

react_to_message was doubly broken: it read that toggle behind the env gate, so
even a local-backend user's Settings toggle could not reach a remote session.

The embedded terminal pane keeps working correctly the other way round: it runs
`hermes --tui` against a desktop-spawned backend, and a tui-sourced session
gets no GUI tools even though HERMES_DESKTOP=1 is set on that process.
This commit is contained in:
Brooklyn Nicholson
2026-08-06 20:27:00 -05:00
committed by brooklyn!
parent fe3a1cad6e
commit 7ad9ace2cc
15 changed files with 271 additions and 113 deletions
+9 -10
View File
@@ -4,9 +4,9 @@
The conversational counterpart to the user's tapback: the same reaction store,
the same one-per-author semantics, just written with ``author="agent"``.
Gated on ``HERMES_DESKTOP`` (like the other GUI affordances) so it costs nothing
on every other surface — the platform adapters already expose reactions through
``send_message(action="react")``, and this is the desktop's equivalent.
Lives in the ``desktop_ui`` toolset (like the other GUI affordances) so it costs
nothing on every other surface — the platform adapters already expose reactions
through ``send_message(action="react")``, and this is the desktop's equivalent.
Defaults to the message that triggered this turn (the photon precedent: the
model shouldn't have to thread row ids through tool calls), and emits
@@ -90,14 +90,13 @@ def react_to_message_tool(emoji: str, message_row_id=None, messages_back=None) -
def check_react_requirements() -> bool:
"""Desktop GUI only, and opt-in.
"""Opt-in feature flag — surface eligibility is the toolset's job.
HERMES_DESKTOP is set on the gateway the app spawns; the feature itself is
off by default and enabled from Settings → Appearance (the desktop mirrors
the toggle into ``display.message_reactions``).
``desktop_ui`` already restricts this to GUI sessions. What's left is the
user's own toggle (Settings → Appearance), which the desktop mirrors into
``display.message_reactions`` on the CONNECTED gateway's config — so this
reads the right config whether that gateway is local, SSH, URL, or cloud.
"""
if not env_var_enabled("HERMES_DESKTOP"):
return False
try:
from hermes_cli.config import load_config_readonly
@@ -155,7 +154,7 @@ REACT_TO_MESSAGE_SCHEMA = {
registry.register(
name="react_to_message",
toolset="terminal",
toolset="desktop_ui",
schema=REACT_TO_MESSAGE_SCHEMA,
handler=lambda args, **kw: react_to_message_tool(
emoji=args.get("emoji", ""),