Files
hermes-agent/tools
Teknium f66a929a6b fix(desktop): render approval/sudo/secret prompts so tools stop silently timing out (#38578)
* fix(desktop): render approval/sudo/secret prompts so tools stop silently timing out

The desktop app's gateway event handler (use-message-stream.ts) handled
clarify.request but had no case for approval.request, sudo.request, or
secret.request. When a tool needed approval, the gateway emitted
approval.request and blocked the agent thread in _await_gateway_decision()
for up to 5 min (approvals.gateway_timeout); the desktop dropped the unknown
event, never showed a dialog, then the agent returned BLOCKED. No prompt,
just a stall then a block.

The Ink TUI already handles all three (createGatewayEventHandler.ts); this
brings the Electron app to parity.

- store/prompts.ts: approval/sudo/secret atoms (+ request-id-guarded clears)
- components/prompt-overlays.tsx: Radix dialogs; close/Esc maps to refusal so
  silence is never mistaken for consent (parity with TUI Esc->deny)
- use-message-stream.ts: wire the three *.request cases; clearAllPrompts on
  message.complete so an overlay can't outlive its turn
- chat-messages.ts: GatewayEventPayload gains command/description/env_var/prompt
- mount PromptOverlays in the chat shell

* feat(desktop): inline tool-call approval bar (Cursor-style "Run")

Render dangerous-command / execute_code approval inline on the pending
tool row instead of as a modal. Binding is positional: the desktop
tool.start payload carries no structured args, but approval.request only
fires from the terminal/execute_code guards and the agent blocks on one
approval at a time, so the single pending row of those tools is the one
that raised it. Command/description text comes from $approvalRequest.

Drops ApprovalDialog from PromptOverlays (sudo/secret stay modal).

* style(desktop): make inline approval bar match Cursor's command card

Drop the amber alert styling for a neutral elevated card: command on a
terminal-prefixed row up top, a divided footer with the muted description
on the left and right-aligned controls — a ghost "Reject" (Esc) plus a
split primary "Run" (⌘⏎) whose chevron opens "Allow this session" /
"Always allow" / "Reject". Wire ⌘/Ctrl+Enter → Run and Esc → Reject to
match Cursor's accept/skip bindings, guarded against double-send via the
$approvalRequest atom.

* style(desktop): shrink inline approval to a tiny Cursor-style button strip

The running tool row already shows the command, so drop the whole card +
command echo + description band. What's left is a compact strip under the
row: a small split "Run ⌘⏎" button (chevron → Allow this session / Always
allow / Reject) and a ghost "Reject Esc", indented to sit under the row's
title text.

* style(desktop): drop the loud blue Run button for a quiet outlined control

Swap the primary (blue) Run for a subtle outlined split control — neutral
border, transparent fill, hover-accent — so the approval strip reads as
quiet inline affordance rather than a big CTA. Reject stays ghost.

* style(desktop): make Run a soft primary badge

Tint the Run split control with the primary color as a badge (bg-primary/10,
primary text, primary/25 border, rounded-md, hover primary/15) instead of a
solid CTA or a neutral outline.

* style(desktop): slim the approval chevron and space out Reject

The chevron button had ballooned because dropping the size prop fell back
to the big default size (h-9 + has-svg px-3). Pin size=xs everywhere and
give the chevron a tight w-5/px-0. Bump the gap between the Run badge and
Reject (gap-2.5) and loosen Reject's internal spacing.

* feat(desktop): confirm before "Always allow" persists an approval

"Always allow" writes the matched pattern to ~/.hermes/config.yaml and
suppresses the prompt in every future session — too consequential to fire
straight from a menu click. Route it through a confirm dialog that names
the pattern + command and the file it touches. The dialog owns the
keyboard while open so Esc closes it instead of denying the approval.

* fix(gateway): make sudo + secret prompts actually fire in the desktop

Tek's PR added the sudo/secret overlays and callback wiring, but neither
reached the live path:

- Sudo: the sudo password callback is thread-local (terminal_tool
  _callback_tls), and _wire_callbacks runs on the agent-build thread, not
  the turn thread that executes tools. At command time the callback was
  missing, so terminal sudo fell through to /dev/tty and hung the headless
  gateway. Re-wire callbacks at the top of the prompt-submit turn thread.

- Secret: skills_tool short-circuited to the "secret entry unsupported"
  hint for any gateway surface, before invoking the callback. Interactive
  surfaces (desktop/TUI) register a secret-capture callback that routes to
  the secret.request overlay; only short-circuit when no callback exists,
  so messaging still gets the hint but the desktop prompts.

* docs(desktop): drop Cursor references from approval comments

* docs(desktop): drop Cursor reference from prompt-overlays comment

* fix(skills): gate in-band secret capture on HERMES_INTERACTIVE, not callback presence

The desktop/sudo PR switched the gateway secret-capture short-circuit from
"any gateway surface" to "gateway surface with no callback registered". That
made a messaging gateway (telegram/discord/...) attempt interactive in-band
secret capture whenever any callback happened to be registered, instead of
returning the safe "setup unsupported" hint — and broke
test_gateway_still_loads_skill_but_returns_setup_guidance.

Discriminate on HERMES_INTERACTIVE instead: the desktop app / TUI set it in
_enable_gateway_prompts (alongside registering the secret.request callback),
while messaging platforms never do. This is the same flag tools/approval.py
uses to tell an interactive surface from a messaging one, so messaging keeps
the hint and desktop/TUI still prompt.

---------

Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-06-04 01:53:51 +00:00
..