Commit Graph

23805 Commits

Author SHA1 Message Date
Brooklyn Nicholson 93b50ea0bb test+docs(tour): cover the engine and document the API
Ten behavior tests for target discovery, stable-selector ordering, step
paging, recovery hints, and the self-containment contract the preview
injection depends on. Docs cover data-tour markup and the curated-tour
entry point alongside the tool itself.
2026-08-19 00:52:54 -05:00
Brooklyn Nicholson 23d88c2b0e feat(tools): tour — let the agent walk a user through the UI
One generic tool in the desktop_ui toolset: discover what is on screen,
highlight an element with narration, or hand the user a paged tour. No tour
content lives in the code — the agent authors each one live, which is what
makes 'how does this work?' answerable as a walkthrough instead of a wall
of text.

Rides the existing blocking-prompt bridge (tour.request/.respond) like
read_preview, so it works on every connection topology.
2026-08-19 00:52:54 -05:00
Brooklyn Nicholson 0d572e063b feat(desktop): answer tour.request from the renderer
Translates the wire payload into a normalized action and dynamic-imports
the engine, keeping driver.js off the boot path. Active session only — a
background turn must never paint an overlay over what the user is looking
at.
2026-08-19 00:48:32 -05:00
Brooklyn Nicholson ec69940537 feat(desktop): run tours inside the preview pane
The guest page is out-of-process, so the first action injects a
self-contained bundle over executeJavaScript — the driver.js IIFE, its
stylesheet, and the engine source — parked on window globals so later
actions reuse the live instance. Injection is idempotent and vanishes with
the page, so a navigation resets the tour.

This is what lets a tour walk through any web app open in the in-app
browser, not just Hermes itself.
2026-08-19 00:47:58 -05:00
Brooklyn Nicholson 018176915b feat(desktop): tour engine — highlight and narrate any DOM
A surface-agnostic walkthrough engine plus the API that drives it.
collectTourTargets scans any document for addressable elements and marks
each selector stable (identity-based, survives a re-render) or positional;
runTourEngine turns one action into a driver.js highlight, a multi-step
tour, or a step change.

Both are written self-contained — no imports, no closures — so the same
source runs in the renderer and, stringified, inside a webview guest page.
Popovers are repainted from the app's own theme tokens, so tours follow
every theme and custom skin.

The named verbs (startTour, showTourStep, nextTourStep, …) are the public
API: the agent tool is one caller, and a feature can ship its own curated
tour through the same entry.
2026-08-19 00:47:58 -05:00
Brooklyn Nicholson 5dc20eb319 build(desktop): add driver.js for guided tours
Pure-ESM, MIT, ~5KB gzipped, no runtime deps. Excluded from optimizeDeps:
it only enters the graph through a dynamic import, so letting the scanner
discover it at first use prebundles the ?raw IIFE as a module (breaking the
raw-text transform) and forces a mid-session page reload.
2026-08-19 00:47:58 -05:00
emozilla 5d3c15aaa7 restore profile switching 2026-08-18 22:44:03 -07:00
Teknium 4903993cdd test: enforce -q/--query-file exclusivity at parse time
The subprocess-based exclusivity test invoked hermes_cli.main in the CI
environment where startup exits 1 before the manual guard runs. Enforce
the conflict in argparse itself (mutually exclusive group, exit 2 at parse
time) and test the parser directly; the manual guard stays for programmatic
namespace fills.
2026-08-18 22:31:32 -07:00
Teknium 1190825652 fix(bot-mode): DM protocol no longer shell-interpolates message bodies
The Bot Mode teammate-DM protocol told agents to inline the message into a
double-quoted shell argument: quotes truncated the body and $(...)/backticks
executed on the sender's machine. The protocol now writes the message to a
temp file and delivers it via a new 'hermes chat --query-file' flag (or '-'
for stdin); 'hermes peer dm' already accepted stdin and the peer recipe now
uses it. No shell pass touches the body at any point.

Supersedes the tool-based approach in #89077 — same bug, fixed with a CLI
flag + protocol rewrite instead of a new model tool.

Co-authored-by: mehmetkr-31 <mehmetkr-31@users.noreply.github.com>
2026-08-18 22:31:32 -07:00
hermes-seaeye[bot] 292d0df04d fmt(js): npm run fix on merge (#89759)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 05:11:57 +00:00
David Dudok de Wit 9ed738fa0c fix(desktop): stabilize gateway settings interactions 2026-08-18 22:03:25 -07:00
David Dudok de Wit c41ec9c588 refactor(desktop): scope gateway context to Sessions 2026-08-18 22:03:25 -07:00
David Dudok de Wit e999df46fa fix(desktop): keep gateway search shortcut local 2026-08-18 22:03:25 -07:00
David Dudok de Wit 6e9e12075c refactor(desktop): keep gateway details contextual 2026-08-18 22:03:25 -07:00
David Dudok de Wit 2c6dcf91c7 feat(desktop): keep large gateway registries searchable 2026-08-18 22:03:25 -07:00
David Dudok de Wit 0a36fa7fbd fix(desktop): keep remote bot identity visible 2026-08-18 22:03:25 -07:00
David Dudok de Wit 72fc5ab7ca fix(desktop): use gateway terminology in connection UI 2026-08-18 22:03:25 -07:00
David Dudok de Wit 3095ac54db docs(desktop): clarify connection settings copy 2026-08-18 22:03:25 -07:00
David Dudok de Wit 2228bc7d88 feat(desktop): remember the last used source 2026-08-18 22:03:25 -07:00
David Dudok de Wit ccd30f955c feat(desktop): distinguish sources from profiles 2026-08-18 22:03:25 -07:00
David Dudok de Wit 0ab0ed54ff feat(desktop): clarify active and default connections 2026-08-18 22:03:25 -07:00
David Dudok de Wit 4ea516b8d4 docs(desktop): explain multi-source session scoping 2026-08-18 22:03:25 -07:00
David Dudok de Wit fa73b45d34 fix(desktop): isolate state across source switches 2026-08-18 22:03:25 -07:00
David Dudok de Wit 0ca7443824 feat(desktop): switch sources from the Sessions rail 2026-08-18 22:03:25 -07:00
David Dudok de Wit 7245b022db feat(desktop): route REST through registered sources 2026-08-18 22:03:25 -07:00
amanning3390 8e5f55f83b feat(hermes-bots): add a collapsible group activity view
Bot Mode group rooms only showed a single "is thinking…" line while bots
ran, and nothing after a turn settled or failed — no way to see what the
room did without reading the whole transcript.

- Runtime-only, bounded activity feed per room (GROUP_ACTIVITY_LIMIT),
  recording truthful turn events: queued, working, replied, passed,
  timed-out, failed, cancelled, settled, delivered.
- Every event is tagged with the room epoch it belongs to; the view shows
  only the CURRENT run, so a superseding send (or a rename that re-keys
  the room) can never surface stale activity.
- Quiet disclosure in the room header: collapsed by default, the collapsed
  row shows the latest event summary; expanding lists the current run's
  events newest-first with per-state glyphs and tones.
- Never persisted and never hydrated — the transcript stays the only
  durable record, so activity cannot be replayed as history.

Tests: 8 new behavioral + source-contract cases in
tests/group-activity.test.mjs (settled arc, failed turn, supersede/cancel,
epoch filtering, bounded feed, runtime-only guarantee, labels, disclosure
a11y contract).
2026-08-18 22:03:02 -07:00
pierrenode 589bee99cf fix(bot-mode): never resubmit into a still-running stranded group member
runGroupChatRounds' responder selection had no awareness of the stranded/
harvest state the previous round's harvest pass just confirmed. A member
whose turn timed out (marked stranded) but is STILL genuinely running
could be re-selected as a responder in a later round of the same
invocation — resolveGroupResponders has no busy filter, and a member's
watermark is bumped past the stranded timeout regardless of outcome, so a
fresh delta re-qualifies them.

Re-selecting them fires another prompt.submit into their live session.
tui_gateway's _handle_busy_submit treats that as a normal busy mid-turn
prompt: by default it either redirects the live turn in place or, for
older agents, hard-interrupts it and queues the new text as the next
turn. Either way the member's original in-flight work — exactly what the
stranded/harvest mechanism exists to protect — gets abandoned or killed,
undermining the "never lost, just late" guarantee.

Filter responders against the room's current stranded map (freshly
confirmed by this round's own harvest pass) before selecting who speaks.
A member with a live stranded marker is skipped; the next harvest pass
picks their reply up once it actually lands.

Added a regression test exercising runGroupChatRounds end-to-end with a
member confirmed still-busy: without the guard the round loop resubmits
into their session (asserted via prompt.submit call count); with the
guard it never does, and the marker survives untouched. Mutation-verified:
temporarily reverted the filter and confirmed the new test fails
(2 !== 0) fast, without a real wall-clock wait.

Full hermes-bots plugin suite: 246/246 pass (47 files). node --check
clean on both changed files.
2026-08-18 22:02:32 -07:00
Teknium a6bada232c feat(mcp): per-server oauth.user_agent for token-endpoint requests (#75576)
Some authorization servers and WAFs reject httpx's default User-Agent on
the OAuth token endpoint. mcp_servers.<name>.oauth.user_agent now stamps a
custom User-Agent onto the two token-endpoint requests (authorization-code
exchange and refresh) on both provider construction paths. Opt-in,
per-server, token requests only — never MCP traffic or discovery, and no
other headers are configurable. Empty/null/non-string values are ignored.

Completes the second half of #75576 (the CIMD half landed via #89566).
2026-08-18 21:57:53 -07:00
Teknium d440891204 test(desktop): update canonical-chat-identity expectation for salvaged PR #89031 2026-08-18 21:52:41 -07:00
marketing2981 ed47d41dc9 fix(desktop): bot navigation no longer forces the all-profiles sidebar on
Every host.openSession call in the Bot Mode plugin omitted
keepAllProfilesScope, so the SDK applied its default and flipped
$showAllProfiles back on whenever the target session belonged to a
different profile than the live gateway (sdk/index.ts:
options.keepAllProfilesScope !== false => setShowAllProfiles(true)).

For anyone running more than one profile this silently undid the sidebar
profile filter: narrow Sessions to one profile, click any other bot, and
the unified all-profiles list came back.

Bot navigation is an explicit context switch into that bot's profile, so
pass keepAllProfilesScope: false at every openSession call site (4 on
current main after the plugin.js refactor consolidated the original 7).

Salvaged from PR #89031 onto current main; includes contributor mapping.
2026-08-18 21:52:41 -07:00
Teknium af3373f7c8 test: follow-up for salvaged PR #89020 — focused-highlight contract includes the !activeGroup guard 2026-08-18 21:52:11 -07:00
Shawn Wang 80f080b6ed fix(bot-mode): highlight selected group chat
Signed-off-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
2026-08-18 21:52:11 -07:00
chelsealong 3809f740c6 fix(bot-mode): reseed $selectedBot on register so re-enabling never leaves it stale
nanostores' .listen() never replays the current value the way .subscribe()
does, so the $focusedBotProfile listener in register() only kept
$selectedBot current from the moment it was attached. A disable -> profile
switch -> re-enable cycle (Settings > Plugins) left $selectedBot pointed at
whichever bot was active before the plugin was disabled, so the roster
highlight fallback and Routines scoping could start from a stale bot.

Extract the sync into bindProfileSync(), which reseeds $selectedBot from
the profile store's current value before attaching the listener. This runs
on every register() call, so re-enabling the plugin always starts in sync.

Salvaged from PR #89637 (the pane-precedence portion was superseded on main
by the $focusedBotProfile design; this residual reseed gap remained).
Regression test mimics real nanostores get/listen semantics and fails
without the reseed.

Fixes-residual-of: #89625
2026-08-18 21:51:36 -07:00
Jaime Henriques 35078e849b docs: surface Bot Mode in docs landing paths 2026-08-18 21:51:09 -07:00
David Dudok de Wit 00e9a32454 fix(desktop): keep Bot Mode routines docked 2026-08-18 21:50:56 -07:00
Jay 104ec97595 fix(bot-mode): group room preview shows the bot handle, not @default
`botHandle()` exists so that, per its own comment, "the word 'default'
never surfaces in the UI" — it presents the primary profile as `hermes`.
The roster rows, mention resolution and the group-chat prompt all route
through it. Two preview paths did not, and rendered the raw profile name:

- `GroupRow`'s room preview line built `@${last.from?.name}`, so a group
  room read `@default: …` while the bot answers to `@hermes`.
- `previewKind()` returned the raw captured name from the bot-to-bot
  delivery prefix, so the `🤖 @<name>` badge and its tooltip could show
  `@default` too.

The mismatch is presentation-only, but it reads as a routing bug: the
room says the message came from `@default` while `@default` is not a
handle the mention resolver accepts, so users reasonably conclude
bot-to-bot addressing is broken when it is working correctly.

Both paths now map through `botHandle()`. `GroupRow` passes the matching
member so a bot with a custom handle keeps it; `previewKind` maps the
lowercased sender name, which leaves every non-primary profile unchanged.

Tests: the primary profile resolves to `hermes` and a named profile keeps
its own handle (behavioural, in the existing previewKind suite), plus a
source-shape assertion for the render path matching that file's
convention. Both new assertions fail against the pre-fix source.

Fixes #89484
2026-08-18 21:50:31 -07:00
Teknium cb0fd836ac feat: Bot Mode blob avatars gain 4 new silhouettes via blobatar 2.0.0
blobatar 0.2.0 -> 2.0.0 (gen2). Ten silhouettes instead of six:
capsule, triangle, hexagon and droplet join round, organic, boxy,
nub, cloud and sun. BLOB_KIND_TRAIT repinned to gen2 band centers
(empirically verified against the published package: every pinned
value resolves to its named silhouette across seeds). The avatar
picker derives from BLOB_KINDS, so the new shapes appear there
automatically. Note: gen2 remaps most unpinned seed->face mappings
by design (upstream generation change).
2026-08-18 21:45:29 -07:00
hermes-seaeye[bot] 395c70d616 fmt(js): npm run fix on merge (#89703)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 04:05:24 +00:00
ethernet 0b879298a7 feat(desktop): browser-bar controls for the address and the page
The browser bar gets an open-in-external-browser glyph that opens
the tab's live address. The address field gets a copy control on its
right edge, with the same pre-faded inline appearance as the
code-block copy button. Copy always takes the address the field
shows: on a remote gateway, that is the reach-resolved address. The
page verbs (copy URL, open externally, console, DevTools) live here
and not in the guest context menu, which keeps the node-scoped
tools.
2026-08-18 23:53:07 -04:00
ethernet 1314a539c2 refactor(desktop): custom translated context menus across the app
One renderer coordinator owns every right-click and replaces the
native Electron menus. Menus are assembled from what the click
landed on:

- Links and images get open/copy/save sections; chat links add the
  reach-aware resolved-URL copy on remote gateways.
- Editables get spell-check suggestions (async-appended when
  Chromium's facts arrive from main), cut/copy/paste, and select
  all. Cut and copy need a selection; paste needs a non-empty
  clipboard; select all needs field content. The verbs show their
  accelerators instead of icons and dispatch a frame after the menu
  closes, so the radix focus trap cannot steal the target. Select
  all runs renderer-side, scoped to the field, because main's
  selectAll acts on the focused frame and could grab the transcript.
- Terminals answer through registered xterm handles; the read-only
  agent terminal hides paste.
- The in-app browser guest builds the same menus from the webview
  tag's context-menu event: Chromium's editFlags gate the edit
  verbs, spell-check rides the event, and Inspect element closes
  every menu. Coordinates arrive as window-relative device pixels,
  so the handler divides by the window zoom factor; guest edit
  commands focus the webview first, because they act on the focused
  webContents.
- Bare app chrome falls back to the window verbs.

Labels come from the locale files in all five languages. Main keeps
thin IPC verbs: edit commands, copy-image-at-gesture, spell-check
actions, and dictionary-add for guests (the tag has no session API).
The e2e spec exercises the real focus trap; it is blocked today by
the gateway-checking stall that also fails e2e/chat.spec.ts.
2026-08-18 23:53:07 -04:00
Teknium 5dd15872a6 fix: never evict pinned CIMD sockets from the callback reservation FIFO
The _MAX_RESERVED_SOCKETS cap applied to pinned CIMD sockets too, so under
heavy concurrency an ephemeral-reservation churn could close a parked pinned
socket before _wait_for_callback adopted it, silently reopening the
port-stealing window the pin exists to prevent (#22161). Eviction now skips
the pinned range; it is already bounded by _CIMD_PORTS.

Follow-up to the #84050 salvage.
2026-08-18 20:03:18 -07:00
rob-maron 0b588cb3a4 MCP CIMD auth 2026-08-18 20:03:18 -07:00
Teknium bdd0a79c6a fix(goals): /goal resume actually restarts work after budget exhaustion
After a standing goal auto-paused on turn-budget exhaustion, every
surface's /goal resume handler only flipped the persisted state back to
active (and reset turns_used) and rendered an acknowledgement — nothing
re-entered the conversation loop, so the goal sat idle until the user
sent another ordinary message.

Fix the whole class by scheduling the canonical
GoalManager.next_continuation_prompt() through each surface's existing
input path after a successful resume:

- Desktop/TUI (tui_gateway/methods_tools.py command.dispatch): return a
  sendable {type: "send"} dispatch with the continuation as the message,
  a "Continuing now" notice, and display "/goal resume" so the
  transcript shows the concise invocation instead of the model-facing
  scaffolding. No-goal keeps the exec response.
- Classic CLI (hermes_cli/cli_commands_mixin.py): put the continuation
  on _pending_input, same as the /goal <text> kickoff.
- Messaging gateway (gateway/slash_commands.py): enqueue a continuation
  MessageEvent through the adapter FIFO — the same path the post-turn
  judge uses — so queued real user messages preempt naturally and the
  pause/clear stale-continuation cleanup recognizes it.

Also correct the now-misleading gateway.goal.resumed copy ("Send any
message to continue…") across all 17 locale files.

Regression tests cover exact budget exhaustion → resume on the real CLI
handler, the real gateway handler (including the
_is_goal_continuation_event guard contract), and the TUI
command.dispatch boundary; verified each fails on the pre-fix code.

Fixes #75362
2026-08-18 19:51:52 -07:00
Teknium 51013e9ae0 fix(desktop): /goal clear removes the Goal paused card immediately
`/goal clear` (and pause/resume/status) can come back from the gateway as
a TYPED `{ type: "exec" }` command dispatch instead of the plain
`{ output }` slash.exec shape. The typed exec/plugin branch in
use-prompt-actions/slash.ts rendered the output ("✓ Goal cleared.") and
returned immediately — it never reached the goal-store sync that the
plain-output path runs (`applyGoalStatusText`). The composer status stack
therefore kept showing the stale "Goal paused" card, with the old goal
text, until the chat was left and reopened (which re-hydrates via
`refreshSessionGoal`).

Fix: in the typed exec/plugin dispatch branch, when the command is `goal`,
mirror the dispatch output into the goal store via
`applyGoalStatusText(sessionId, output)` before rendering — exactly what
the plain-output path already does. This covers the whole sibling class
(clear/pause/resume/status/done) since the store's text parser already
understands every /goal output shape; set (`send` dispatch notice) was
already handled.

Tests:
- use-prompt-actions/index.test.tsx: typed exec `✓ Goal cleared.` removes
  the session's goal entry immediately (#80348), and typed exec
  `▶ Goal resumed:` flips a paused card back to active.
- store/goals.test.ts: `✓ Goal cleared.` output clears a paused goal.

Fixes #80348
2026-08-18 19:51:41 -07:00
Teknium 23c1c9815f fix(desktop): Bots sidebar highlight and Cronjobs tile now follow the chat on screen
The Bots roster highlight and the Routines (Cronjobs) tile were keyed off
host.state.profile — the gateway socket's home. Tab/tile focus moves without
swapping the socket, so opening one bot's chat while the socket was homed on
another highlighted the wrong bot and showed the wrong bot's cronjobs
(community report: Newsanalyst chat open, Hermes highlighted).

- sdk: new host.state.focusedSessionProfile — owner profile of the focused
  chat, resolved from the focused stored session's row stamp via
  rememberedSessionProfile() (same ladder as remembered navigation and the
  HUD), with the gateway profile as the draft/uncached fallback.
- hermes-bots: $focusedBotProfile = focusedSessionProfile || profile
  (feature-detected; older desktops keep prior behavior). BotRow highlight,
  RoutinesPane scope, and the $selectedBot tracker use it. Turn-busy 'work'
  mood stays keyed to the socket-home profile (only it can be mid-turn).
- tests: SDK atom behavior (vitest) + plugin source-shape suite; prewarm
  harness stubs gain the new atom.
- docs: SDK page + hermes-agent skill reference list the new atom.
2026-08-18 19:51:13 -07:00
Brooklyn Nicholson 74f99af470 feat(desktop): agent-applied layout presets — apply_layout joins the desktop_ui toolset
The agent could reveal single panes (focus_pane) but had no way to arrange
the workspace as one act. apply_layout closes that gap: a desktop_ui tool
that emits layout.apply over the existing bridge, resolved in the renderer
against the layouts contribution registry — the same list the layout picker
reads — so core presets (default/focus/terminal-deck/quad), plugin presets,
and user-saved presets are all addressable by id. Active session only, same
as pane.reveal: a background turn never rearranges the user's desktop.
2026-08-18 21:06:18 -05:00
hermes-seaeye[bot] b7bed2419e fmt(js): npm run fix on merge (#89619)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-19 01:35:26 +00:00
ethernet 66bb77cbf9 docs(clarify): advertise the questions batch in the tool description
The `questions` parameter had a full description, but the top-level
tool description still described three single-question modes and never
mentioned batching. The model decides how to call a tool from that
description, so it kept asking one question per call.

The description now states that 2-5 independent questions can go in
one call and that one batched call is preferred over a chain of
single-question calls. The parameter description also tells the model
to put a short batch title in the still-required top-level question.
Two schema tests pin the contract: the description names the batch
capability, and the questions parameter stays optional with the
MAX_QUESTIONS cap.
2026-08-18 21:28:53 -04:00
ethernet d9f98fe071 feat(tui): restore the earlier answer when a batch question is re-visited
Tab or Shift-Tab onto an answered question now restores its state, the
same model as the CLI panel. A choice answer puts the cursor back on
its row. An answer that matches no choice was typed via Other, so the
cursor lands on the Other row with the text staged in the input —
Enter then edits the earlier text instead of starting blank. The
restore logic lives in a pure helper (clarifyBatchRevisitState) with
direct tests, because the prompt component has no keystroke harness.
2026-08-18 21:28:53 -04:00
ethernet 9a9015dfa8 test(desktop): batch clarify E2E spec and mock trigger
The mock server gains a batch clarify trigger that scripts a
two-question clarify turn. The scripted turn fires only while the
conversation has no tool result, so the answered batch falls through
to the canned reply instead of a repeat of the quiz.

The spec runs the real chain from composer to renderer and asserts
one batch card, the staged-answers confirm gate, and the settled
card. The local harness cannot boot the packaged app in this
environment (the pre-existing chat spec fails the same way), so the
proof for this spec is the CI run.
2026-08-18 21:28:53 -04:00