Files
hermes-agent/tools/tip_tool.py
Brooklyn Nicholson 0357982696 feat(desktop): make the idle tip rotation opt-in, ungate the tool
The two halves of tips were behind one switch, which meant the app
volunteering commentary at idle shipped on by default. Split them along
the line that matters: the rotation talks unprompted, so it now waits to
be asked for, while an agent tip stays ungated like the tour it mirrors
— Hermes raises one mid-conversation, in answer to something the user
said.

Drops the tool's config gate along with the config key it read. The
renderer mirrored that key with config.set, which has no branch for it
and answered "unknown config key" into a swallowed catch, so the opt-out
never reached the backend in the first place.
2026-08-27 21:50:18 -05:00

116 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Point at something in the Hermes desktop GUI and say one line about it.
The quiet sibling of ``tour``. Same durable ``data-tour`` handles, same
discovery call (``tour(action="targets")``) — but no scrim, no spotlight, and no
Next/Prev. Just an accent-lit bubble with an arrow into whatever the tip is
about, which is the right weight for "that button, there" in the middle of a
sentence.
Fire-and-forget, unlike ``tour``: a tip is not a question, so blocking the turn
on a round-trip would stall the reply it belongs to.
Ungated, also like ``tour``. The desktop's Settings → Appearance switch governs
the app's own idle rotation — the half that talks unprompted — not this, which
Hermes raises mid-conversation in answer to something the user said.
Lives in the ``desktop_ui`` toolset, which the GUI gateway enables only for
desktop-sourced sessions.
"""
import json
from tools import desktop_ui
from tools.registry import registry, tool_error
SIDES = ("top", "right", "bottom", "left")
def tip_tool(text: str, selector: str, title: str = "", side: str = "") -> str:
"""Show one tip bubble anchored to ``selector``."""
text = (text or "").strip()
selector = (selector or "").strip()
if not text:
return tool_error("tip needs text — the one line the bubble says.")
if not selector:
return tool_error(
"tip needs a selector to point at. Call tour(action='targets') to see "
"what's on screen and prefer a target reporting stable: true."
)
if side and side not in SIDES:
return tool_error(f"side must be one of: {', '.join(SIDES)}.")
payload = {"selector": selector, "text": text}
if title:
payload["title"] = title
if side:
payload["side"] = side
try:
ok = desktop_ui.emit("tip.show", payload)
except Exception as exc:
return tool_error(f"Failed to show the tip: {exc}")
if not ok:
return tool_error("tip is only available in the Hermes desktop app.")
return json.dumps({"success": True, "selector": selector}, ensure_ascii=False)
TIP_SCHEMA = {
"name": "tip",
"description": (
"Point at one thing in the Hermes desktop UI with a small accent-lit "
"bubble and an arrow — no dimming, no spotlight, no Next/Prev. Reach "
"for it when a sentence would be clearer with a finger on the thing "
"it's about: 'the model name is a button', 'your files are in here'. "
"Call tour(action='targets') first to see what's on screen and prefer "
"a target reporting `stable: true`; never guess a selector. One tip at "
"a time — a new one replaces the last. Say the same thing in chat as "
"well; the bubble is a pointer, not the message. Use it sparingly: a "
"bubble on every turn is what makes people stop reading them."
),
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The one line the bubble says. Keep it to a sentence.",
},
"selector": {
"type": "string",
"description": (
"CSS selector of the element the arrow points at, from "
"tour(action='targets')."
),
},
"title": {
"type": "string",
"description": "Optional short heading above the text.",
},
"side": {
"type": "string",
"enum": list(SIDES),
"description": "Preferred side of the element. Omit for 'top'; it flips at a screen edge either way.",
},
},
"required": ["text", "selector"],
},
}
registry.register(
name="tip",
toolset="desktop_ui",
schema=TIP_SCHEMA,
handler=lambda args, **kw: tip_tool(
text=args.get("text", ""),
selector=args.get("selector", ""),
title=args.get("title", ""),
side=args.get("side", ""),
),
emoji="💡",
)