Commit Graph

5640 Commits

Author SHA1 Message Date
teknium1
54c55307e3 test(desktop-e2e): never run a local e2e against the developer's real Hermes
buildAppEnv sandboxed HERMES_HOME only. Desktop now attaches to the host's
running hermes serve (one backend per host) and profile roots are
HOME-anchored, so on a machine running Hermes a local spec either attached
to that backend or listed the real profiles, and its sends went through
the developer's real model and state.db instead of the mock provider.

Set HERMES_DESKTOP_ISOLATED_BACKEND=1 (the documented private-backend
escape hatch) and HOME=<sandbox root>. CI has neither a running backend
nor real profiles, so it never took this path.
2026-09-23 02:28:38 -07:00
teknium1
680d613a01 fix(desktop): live-reload only loopback dev pages, one loopback predicate
Follow-up to the salvaged fix. Narrow the workspace live-reload gate to
loopback (full 127/8, ::1, 0.0.0.0, localhost) and drop `.local`: mDNS
names are other devices on the LAN (homeassistant.local), the same
"unrelated page loses its state on every agent file edit" class the fix
closes. The pane's error copy already had its own loopback regex for the
remote-gateway hint; both call sites now share isLoopbackPreviewUrl.

Tests trimmed to one pane invariant (reloads a loopback page, stops once
the tab browses elsewhere: the live page decides, not the tab's original
address) plus the predicate table.
2026-09-23 02:28:17 -07:00
fangliquanflq
8206d89b98 fix(desktop): preserve external preview page state 2026-09-23 02:28:17 -07:00
hermes-seaeye[bot]
a27b13056e fmt(js): npm run fix on merge (#120048)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-23 09:02:52 +00:00
teknium1
71b61884a4 fix(desktop): deliver a gateway event once even when two sockets to one backend carry it
The backend stamps `seq` once per event before its transport fan-out, so the
same frame arriving on two of the renderer's sockets to one process carries
the same (session_id, seq); `JsonRpcGateway` is per socket and cannot see the
other copy. `JsonRpcGateway.dispatchEvent` now tags each event with the
`replay_epoch` its socket adopted from `gateway.ready` (per process, not per
socket), and both fan-ins in `use-gateway-boot.ts` (primary + registry
secondaries) pass through one `createGatewayEventDedupe()` gate keyed by
(epoch, session_id, seq) before any store runs.

A duplicate is the same key within 30 s. Not "seq not above the highest
seen": the backend restarts a session's counter at 1 when the session leaves
its 64-session replay ring, and a high-water mark would swallow the whole
restart of any short session. Seq-less / session-less events always pass.
Bounded: 2048 seqs per session, 256 sessions LRU.

Live, with the router fix reverted so two sockets still join the chat: the
DOM never doubled a word (0/38 samples vs 4/40 on main) and the doubled
"reply was cut short" card is back to one.

Closes #120007. Part of #120005.
2026-09-23 01:56:12 -07:00
teknium1
0bb539b472 fix(desktop): session calls for a profile on the shared host backend ride the primary socket
Since #118246 one local `hermes serve` serves every profile, and main marks
those routes `sharedPrimary: true`. `requestGatewayForProfile` honoured the
flag, but the session-owner family (`requestGatewayForAgent`, retain/open/
ensure) went through `isAttachedSharedRemote`, whose `primaryConnectionMode
=== 'local'` early return (#113956, written while every local profile had
its own pooled child) sent every non-default local profile to a registry
secondary: a SECOND WebSocket to the SAME backend process. The backend joins
any socket that sends a session call to the chat's transport fan-out, so the
renderer received every event twice — "HelloHello from from the the mock
mock…" while streaming, duplicate interim bubbles, doubled error cards
(#120005, #119131, #119566, #119540, #118934).

The predicate is now `ridesPrimaryBackend`: it asks main for the route on
every call and reuses the primary socket (with the `profile` param) when the
descriptor carries `sharedRemote` (#96493) OR `sharedPrimary` (#118246).
#101416 stays fixed: when the probe fails on a LOCAL primary we never fall
back to the primary (a pooled profile's chat must not be minted under the
primary's pid), and a pooled descriptor (HERMES_DESKTOP_ISOLATED_BACKEND=1)
still opens its own secondary.

Closes #120006. Part of #120005.
2026-09-23 01:56:12 -07:00
teknium1
4a2bd7406e fix(desktop): models added by a plugin or catalog update no longer start hidden in the picker
The Edit Models store persisted only an allowlist of visible provider::model keys.
Once a provider had any stored key it was skipped by the default expansion, so a
model that appeared later (plugin update shipping a new route, catalog refresh,
new release) was absent from the list and rendered switched off. The store could
not tell "hidden on purpose" from "never seen".

Persist a `known` snapshot (hermes.desktop.known-models) beside the allowlist,
recorded whenever the user persists a choice. A curated provider now admits
models absent from the snapshot through the same curated default rule
(featured list / top-N); a provider the user hid outright stays hidden, new
models included. Stores written before the snapshot existed adopt the catalog
as judged the first time it loads, so existing hide choices are honoured
verbatim and only later arrivals count as new.
2026-09-23 01:35:20 -07:00
alt-glitch
5f47c35d37 fix: a connector row says "Waiting for your browser…" only after the user opens its link
The connect-first handoff (D85) has the build chat call manage_connections before
anything else, and the backend watcher mints each app's link on its first pass. That
moves the row to `initiated`, which the card drew as the spinner plus "Waiting for
your browser…" although nobody had clicked Connect and no browser had opened. Try
again had the same gap: the fresh link is minted, not opened.

The card now remembers which rows the user opened from it. A minted link the user has
not opened shows as not connected with the Connect button; clicking Connect opens it
and shows the waiting cue; Try again clears it until the new link is opened.
2026-09-23 11:54:42 +05:30
alt-glitch
e458619f23 fix: the first build is named and seeded from the welcome chat's stored reply, not its streamed copy
On Windows (run 3, 2026-09-23) the renderer's streamed copy of the setup
reply repeated its own chunks. The handoff card read its task and brief from
that copy, so the build session was titled "Set up mySet up my games an…" and
opened with "Set up Sid up Sid's PC's PC for gaming for gaming…", cut at 240
chars. The backend's stored reply held the clean directive.

The card now reads the handoff directive from session.history (the persisted
reply) and uses those values; an unreachable history falls back to the
rendered attrs. It also waits for the whole reply to stop running: `locked`
tracks only the text part, which a later part settles mid-reply.
2026-09-23 11:46:41 +05:30
hermes-seaeye[bot]
c0d7294769 fmt(js): npm run fix on merge (#119817)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-23 04:02:02 +00:00
alt-glitch
6b9214b08e fix: the build session's runbook names each installed plugin's skill by its loadable name
Live run: the first default turn called skill_view("blender") and got
"Failed to load skill: blender". A plugin skill registers under its
qualified name (`agent-plugin-<key>:<skill>`), and the runbook carried only
the catalog name, so the model guessed the short one. The install card's row
already reports the qualified name; it now rides the settled outcome into
the runbook, which tells the build agent to load it by that exact name.
2026-09-23 09:26:07 +05:30
alt-glitch
d41f079e4e feat: Blender and NVIDIA each get a recommended first task
Sid's ruling after walking the flow: both plugin families carry a pill.
"Help me make something in Blender" is offered everywhere (the catalog runs
Blender on all three platforms). "Set up my games and streaming" (NVIDIA App
+ Broadcast) is offered on a Windows PC with an NVIDIA GPU, where it leads,
since those plugins are Windows-only. Each task runs the install beat for
its own plugins.
2026-09-23 09:26:07 +05:30
alt-glitch
d06ada6b0b fix: the connectors card paints its rows at once and keeps them across remounts
Live run: the card showed no chips for 3 to 5 seconds, drew them, lost them,
and drew them again. connectors.list takes 6.7 to 8 s on this Mac, and the
catalog read another 6.7 s cold. The hook read once per MOUNT, and the card
remounts on every transcript rebuild (each hidden submit, each turn end), so
each remount threw the rows away and waited out another round trip.

Both reads now live in the query cache keyed by the guide session, and the
kickoff prefetches them when the guide session opens (created or adopted),
two turns before the card needs them. A remount paints the cached rows.
2026-09-23 09:26:07 +05:30
alt-glitch
6ec517c475 feat: onboarding offers catalog plugins beside connectors and installs the picked ones before handoff
NS-960. One card, one group ("connectors"): the curated catalog plugins
this OS runs lead the hosted connectors (D1, D4). A plugin whose app is
absent is greyed with the reason and stays pickable (D5). Picks land in
answers.plugins and ride the existing [setup] note to the guide.

The guide's runbook gains the install beat as the last thing before the
handoff card (D2, D3): narrow the picks to what the chosen task needs, then
ONE manage_catalog install call. A new suggested first task sits beside the
existing ones and installs its plugins through that same beat: "Set up my
games and streaming" (NVIDIA App + Broadcast) on a Windows PC with an NVIDIA
GPU, "Help me make something in Blender" everywhere else.

When the guide's install card settles, each plugin row's outcome
(installed / failed / skipped, whatever the user did) is written into the
answers (D6). The build session's runbook names what is ready, what was
offered and not installed, and what was picked but not offered, and tells
the build agent never to install. profiles.remember_onboarding records the
picked plugin names in the default profile's memory, next to the connectors.
2026-09-23 09:26:07 +05:30
alt-glitch
5c215ffa21 feat: catalog marks onboarding plugins; catalog rows carry the app's presence
The onboarding card needs to list catalog plugins beside the hosted
connectors (NS-960 D1, D4) and grey a plugin whose app is absent (D5).
The catalog had no curated flag, and the manage_catalog row left
app_state empty.

- `onboarding: true` and `title` on catalog entries (loader, validator,
  docs); set on blender, nvidia-app and nvidia-broadcast.
- hermes_cli/plugin_catalog_presence.py reads the plugin.json at the
  catalog's pinned commit once per pin and judges its app declaration with
  the hermes_platform resolver the installer and the Plugins-tab pill use.
  No declaration or an unreadable one is `unknown`, never `present`.
- `plugins.manage action=onboarding` lists the curated entries this OS
  runs (platform mismatch is the only exclusion) with app_state and the
  sentence the card greys the row with.
- manage_catalog plugin rows now carry app_state and the catalog title.
- A live entry that differs from the in-tree entry at the same pin (new
  metadata) now follows the same newer-catalog rule as a new pin, so a
  checkout that adds `onboarding` is not masked by a published doc that
  predates it.
2026-09-23 09:26:07 +05:30
hermes-seaeye[bot]
731e99d15d fmt(js): npm run fix on merge (#119739)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-23 02:21:22 +00:00
brooklyn!
40f29e280c test(desktop): wait for the endpoint form before editing it 2026-09-22 21:15:22 -05:00
brooklyn!
7eb0ae1a60 fix(desktop): keep multi-gateway navigation available in Simple mode 2026-09-22 21:15:22 -05:00
brooklyn!
9867a75c1e fix(desktop): load saved gateways independently of optional chrome 2026-09-22 21:15:22 -05:00
hermes-seaeye[bot]
d3b25b52ad fmt(js): npm run fix on merge (#119649)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 23:49:00 +00:00
Siddharth Balyan
9039b690fc feat: installed plugins' MCP tools and skills are live in every open chat, no Connect-now (#119644)
Installing a plugin from any surface now makes its MCP servers and skills
usable in every open chat of that profile on the chat's next turn. There is
no Connect-now button, no /reload-mcp, no relaunch.

- hermes_cli/plugins_activation.py::load_and_go_live: after the forced plugin
  rescan, connect the plugin's portable MCP servers one by one
  (register_mcp_servers, the connector flow's call), then refresh that
  profile's open chats and queue a turn note listing the servers, their
  tools and the plugin's skills. activation gains live_now; deferred keeps
  only Python tools and prompt sections.
- MCP tools are deferred behind tool_search / tool_call, so appending them
  (preserve_prefix) leaves the model-facing tool array and the cached prompt
  prefix unchanged; the note rides the existing one-shot turn-note channel,
  so the system prompt stays byte-stable. /reload-mcp keeps its consent gate:
  it is a full rebuild.
- tui_gateway/methods_tools.py: one session walk (_refresh_live_sessions)
  shared by reload.mcp and plugin activation, filtered to the plugin's
  profile home.
- hermes plugins install/enable (another process) asks the running Desktop /
  dashboard backend to do the in-process half through the new
  POST /api/dashboard/agent-plugins/activate, found via the host rendezvous
  record and its session token.
- Desktop: the Connect-now toast and its strings are removed in all six
  locales; the install toast says what went live ("3 tools connected ·
  skill X ready") and warns per server that did not connect.
- Contract: PluginActivation.live_now; regenerated TS/OpenRPC.
- register_skill docstring: plugin skills are listed by skills_list.
2026-09-23 05:11:44 +05:30
Siddharth Balyan
8365cae594 Catalog install card: plugin and skill rows with an Advanced install modal (#119633)
* feat(desktop): catalog install card rows for plugin and skill targets

manage_catalog opens the same connection operation manage_connections does,
with rows of kind plugin/skill. The renderer collapsed every non-connector
kind to 'mcp', so those rows would have drawn as MCP servers with no name,
purpose or provenance.

- Contract: ConnectionTargetKind gains plugin/skill; ConnectionOperationTarget
  types the catalog fields agreed in CATALOG-ROW-CONTRACT.md (display,
  description, tier, platforms, repo, sha, subdir, scan, requirements,
  has_desktop_half, target_profile, app_state, skill).
- Store: kinds map through a lookup; catalog rows carry a typed CatalogEntry.
- CatalogRow: kind glyph, name, kind/tier/platform pills, one-line purpose,
  then Install / Advanced / Skip. Installing shows the backend's per-row
  detail; installed shows "N tools · show names"; failed shows the reason
  and Try again (approved again for that row only); skipped stays quiet.
- Advanced: a centred dialog with the Plugins-tab controls (halves, target
  profile, enable, force, pin) plus read-only source and scan. Its Install
  sends the values in the answer env; the row's Install sends env null.
- A missed tool.start restores the card on a manage_catalog row.

* fix(desktop): catalog row uses the app's sliding progress and one pill style

The installing row drew a full-width pulsing line, which reads as a finished
rule, not work in progress; it now uses the Progress primitive's sliding
indeterminate block (reduced motion honoured there). The kind pill was
all-caps monospace next to two sentence-case pills; all three now share one
shape, matching the V1 offer artboard, and the platform pill stays the one
coloured note. The Advanced modal's Source and Security tables share a fixed
label column, and requirements render as prose, not code.

* fix(desktop): a skill's Advanced shows only the controls a skill has

A skill has no agent/desktop halves, no enable step and no commit pin, so
its Advanced dialog offered three controls that do nothing. It now shows the
target profile, force reinstall, source, security and credentials, and sends
only target_profile, force and the credential values. The preparing line
takes the card's width instead of the whole chat column.
2026-09-23 05:11:35 +05:30
brooklyn!
28aceb3451 style(desktop): align stability changes with local conventions 2026-09-22 17:36:39 -05:00
brooklyn!
a87310a343 fix(desktop): reconcile refreshed transcripts by acknowledged occurrence 2026-09-22 17:36:39 -05:00
brooklyn!
36b4f7cc2d fix(gateway): acknowledge persisted prompt and reply identities 2026-09-22 17:36:39 -05:00
brooklyn!
d96004856f fix(desktop): keep native selections intact while unwrapping composer blocks 2026-09-22 17:36:39 -05:00
Hermes
b4d75f123b fix(desktop): restore the caret when normalization removes its anchor
The #100302 sweep guard keeps the caret's own empty text node alive, but
normalizeComposerEditorDom has three more paths that can delete the node a
live caret is anchored in: the phantom tail-block removal, the trailing-br
removal, and the block unwrap. Per the DOM spec a boundary re-anchors in the
removed node's parent, so the caret reads as "valid" while its position has
drifted (the #88621 class: editor stays activeElement, printable keys stop
producing input events, clicking restores typing).

Normalization is text-preserving, so this snapshots the caret once — both as
the boundary (node, offset) and as a composerPlainText offset, the same units
the undo/redo caret save already uses — and re-establishes it via
placeCaretAtOffset only when the snapshotted anchor node no longer exists in
the editor. Hidden keep-alive composers are excluded: the snapshot is null
unless the selection is a single collapsed range inside this editor, so a
background repaint never steals the document selection the visible composer
owns. Skipped when the editor does not hold focus (document.activeElement).

The sweep guard from the previous commit stays: skipping the node is cheaper
than a remove-then-restore round-trip and keeps the caret node identity
stable through the empty-litter case.

Tests: the new invariant (caret restored when its anchor block is removed)
fails on the base commit without this change; a second test pins that a
still-valid selection passes through untouched. Composer suite 461/461,
tsc/eslint/prettier clean.

(cherry picked from commit 741265a88499c13f6a3086133067ece9ad8aa619)
2026-09-22 17:36:39 -05:00
Ahmett101
79e8aab91c fix(desktop): preserve composer caret during normalization
Avoid removing Chromium's live collapsed selection container while sweeping zero-length composer text-node litter.\n\nCloses #100302.

(cherry picked from commit 9dc30faccb18ef0bc70f82795dd0882455d87552)
2026-09-22 17:36:39 -05:00
Siddharth Balyan
aaf0fd9273 The last four MCP enabled-flag readers use the shared reader (ACP, hermes tools picker, desktop connector card, health sweep) (#119601)
* fix(mcp): ACP, `hermes tools` and the desktop connector card use the one enabled reader

#119567 left four readers of `mcp_servers.<name>.enabled` on their own rules.
@webtecnica's #119560 found the ACP one:

- `acp_adapter/session.py::_make_agent` used `is not False`, so an ACP
  session kept a server with `enabled: "false"` or `enabled: 0` in its
  toolset while the MCP client skipped it.
- `hermes tools` MCP picker (`tools_config_mcp._configure_mcp_tools_interactive`)
  read `enabled: 0` as on and crashed on a non-dict entry.
- Desktop connector card (`connectors/data/join.ts`) and the MCP health sweep
  (`store/mcp-health.ts`) used `enabled !== false`.

All four now call `mcp_server_enabled` (Python) or `serverEnabled` (renderer),
which share one case table.

Co-authored-by: webtecnica <webtecnica@gmail.com>

* chore: retrigger CI (zero-job dispatch failure, auto-heal)

---------

Co-authored-by: webtecnica <webtecnica@gmail.com>
2026-09-22 22:24:31 +00:00
hermes-seaeye[bot]
400f11bd35 fmt(js): npm run fix on merge (#119581)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 22:08:11 +00:00
Siddharth Balyan
3e00a356a4 fix(mcp): one reader for mcp_servers.<name>.enabled (#119567)
The `enabled` key had four parsers. The MCP client (`_parse_boolish`) read
`enabled: 0` as on; the toolset resolver and editor (`_parse_enabled_flag`)
read it as off. The server list (`summarize_server`, `/api/mcp/servers`) read
any non-`False` value as on, so `enabled: "false"` showed on while the agent
skipped it. The catalog and `hermes mcp list` accepted only true/1/yes, so
`enabled: on` showed off while the server ran.

`tools/mcp_tool_common.py::mcp_server_enabled` is now the only reader, and
every surface calls it. `_parse_boolish` treats YAML numbers by truthiness
(0 off, other numbers on). Everything else keeps the client's semantics:
the off words are off, absent / null / junk stay on, with the existing
warning for junk.

The desktop MCP page mirrors the rule in `serverEnabled`
(`apps/desktop/src/lib/mcp-servers.ts`). One case table
(`mcp-enabled-cases.json`) drives the Python invariant test and the vitest
test, so the page and the runtime cannot drift apart again.
2026-09-22 22:00:24 +00:00
hermes-seaeye[bot]
ec21bd7674 fmt(js): npm run fix on merge (#119573)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 21:50:23 +00:00
brooklyn!
8adb99fabd test(desktop): fix mode-memory compiler and lint checks 2026-09-22 16:44:24 -05:00
brooklyn!
c21df8e7eb fix(desktop): keep onboarding's temporary layout out of mode memory 2026-09-22 16:44:24 -05:00
brooklyn!
ff7642c268 fix(desktop): preserve live pane guests across layout changes 2026-09-22 16:44:24 -05:00
brooklyn!
68f75db5e5 fix(desktop): restore independent Simple and Advanced layouts 2026-09-22 16:44:24 -05:00
hermes-seaeye[bot]
4c61eaa06f fmt(js): npm run fix on merge (#119476)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 20:01:52 +00:00
Siddharth Balyan
3c6c132366 Setup profile is minted by the backend and found by role, not by name (#119456)
* feat: setup profile is minted by the backend and found by role, not by name

The guided onboarding runs in a profile the desktop used to create itself
(profiles.create with a soul, "already exists" treated as success) and
recognise by the literal "hermes-setup". The upcoming setup toolset grants
catalog installs to that profile, so the marker that grants it must be
written only by the backend.

- profile.yaml carries `role: setup`; read_profile_meta / write_profile_meta /
  ProfileInfo know it; profiles.list and GET /api/profiles report it.
- hermes_cli/setup_profile.py: ensure (find by role, adopt a pre-role
  hermes-setup dir, else clone default + soul + role) and reset (soul,
  memories, skills back to the created state, in place). The soul text moves
  here from the renderer.
- tui_gateway/methods_onboarding.py: onboarding.ensure_setup_profile and
  onboarding.reset_setup_profile. Neither takes a name; profiles.create and
  profiles.configure already reject `role` (unknown key, 4000).
- Copies never inherit the role: --clone-all, profile import, and a
  distribution that ships profile.yaml drop it.
- setup.status for a named profile reports `ready` once the boot bootstrap
  settled. Since one host backend serves every profile (#118246) the
  desktop's setup-profile probe lands on this branch, which never set
  `ready`, and the kickoff waited forever.
- Desktop: SETUP_PROFILE, ensureSetupProfile(profiles.create) and
  composeSetupSoul are gone. store/setup-profile.ts holds the name the
  backend returned (or the roster's role row after a relaunch); kickoff,
  handoff and the build card use it. The dev reset calls the reset RPC.

* fix: write the setup soul as bytes so Windows keeps \n line endings

* refactor(desktop): drop the renderer's setup-profile store; the backend is the only owner

Kickoff reads the name straight from onboarding.ensure_setup_profile and records it on
$setupSession, which every later step already carries. The handoff recovery check reads
the roster row's role. No renderer module holds a setup-profile name or a fallback lookup.
2026-09-23 01:25:04 +05:30
hermes-seaeye[bot]
6c4536aed2 fmt(js): npm run fix on merge (#119356)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 17:45:19 +00:00
Siddharth Balyan
834580309d feat(desktop): after a plugin install, offer "Connect now" for its MCP servers instead of a gateway restart (#119349)
The success toast after a Desktop plugin install said "Restart the gateway for the plugin to take
effect" with a button that restarts the messaging gateway. The Desktop chat talks to `hermes serve`,
a different process, so the button did nothing for the plugin's MCP servers and the user relaunched
the whole app to get them. Observed live on the x64 Windows desktop installing nvidia-app.

`plugins.manage install` now returns `activation.deferred.mcp_servers` (the plugin's servers not yet
connected) and `gateway_reloaded` (#119266), and the backend rescans plugins on the install path, so a
follow-up `reload.mcp` connects them in place. The toast now branches on that:

  deferred.mcp_servers non-empty  → "<name> installed. Its MCP server is not connected yet." + Connect now
                                    → reload.mcp {confirm: true, session_id?} → plugin list refresh
  gateway_reloaded                → "<name> installed and active." (no button)
  otherwise                       → the existing restart toast, unchanged (native plugin the gateway missed)

`installAgentPlugin` surfaces the two fields as `deferredMcpServers` / `gatewayReloaded`. New i18n keys
in every locale. Two behaviour tests: deferred servers → Connect now fires reload.mcp with confirm:true;
no activation → restart toast still fires.

Verified on main a53b42ddea: install nvidia-app from the catalog, then `/reload-mcp now` (the same RPC
the button calls) → `MCP server 'nvidia-app' (HTTP): registered 12 tool(s)` 0.5 s later, no relaunch.
2026-09-22 23:08:13 +05:30
hermes-seaeye[bot]
42c1a93417 fmt(js): npm run fix on merge (#119335)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 17:22:52 +00:00
hermes-seaeye[bot]
f78ba6a700 fmt(js): npm run fix on merge (#119328)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-22 17:16:27 +00:00
brooklyn!
a53b42ddea fix(desktop): a layout says what is on screen
Applying a preset opens every pane it places and closes the ones it lists
as `resting`, through the panes' own stores so ⌃`/⌘J/⌘G/⌘B stay truthful.
The per-pane `revealOnPreset` flag is gone; a saved deck remembers which
of its panes were closed.

Basic was `sessions | workspace`, and picking it produced Focus — the
apply adopts every omitted pane back in as workspace tabs, so Basic →
Focus → Basic converged on one shape (in Advanced too). Basic is now
sessions + chat with the terminal resting as a rail under the chat and
review/files resting in a right column; Focus keeps files/review as tabs
behind the chat with the same rail (a terminal tab covered the
conversation, and a reveal on apply fronted it). Simple's two presets are
Basic and its mirror.

The mirror exposed a positional bug on main: `$fileBrowserOpen` collapsed
"the right edge" whatever sat there, so a resting file tree folded the
mirrored sidebar away — flip Default with ⌘\ and ⌘J hides the sessions
column. An edge now belongs to the pane on it (`sidebarSide()` /
`fileBrowserSide()` follow `$panesFlipped`; the tree-side bindings pick
their owner the same way). Sessions gains an opener to mirror its closer.
In Simple a closed terminal hides, rail and all (`bindToolPaneCollapse(…,
$rail)`); ⌃` is the door for the session.
2026-09-22 12:08:37 -05:00
brooklyn!
ddeed59afd feat(desktop): Interface mode in the layout editor, Settings, ⌘K and onboarding
The layout editor gets an Interface mode section above the templates —
Simple "For talking to Hermes. Sidebar and chat; no terminal, file or diff
panes." / Advanced "For developers. Terminal, files, diffs, statusbar and
layouts, as you set them." In Simple the shelf is Sidebar left / Sidebar
right and there is nothing to arrange or save. Settings → Appearance →
Window & layout carries the same control; rows whose value Simple decides
say so. ⌘K "Simple mode" and the rebindable `view.toggleSimpleMode` are the
guaranteed way back. First launch: Basic applies Simple, Elite Advanced.
Six locales, docs, and a Playwright round trip.
2026-09-22 12:08:37 -05:00
brooklyn!
107c67cad2 feat(desktop): tier the chrome so Simple hides the instrumentation
Simple: no statusbar, no profile rail (unless a second profile makes it
the only way to switch), terminal / files / review resting, product tool
view, inline diffs folded, thinking collapsed, session rows down to title,
preview and time. Titlebar keeps sidebar, Settings and the layout editor —
`TITLEBAR_FIXED_TOOLS` is the one table the buttons and the width
reservation both read, so a hidden tool releases its space. Sidebar rows
carry their own `tier` (Artifacts and Scheduled jobs are Advanced;
Capabilities and Messaging are how anyone sets Hermes up) and the list
filters once — nothing is passed down. The status stack keeps Todos,
Goal, Queue and preview chips; Subagents and Background processes are
Advanced, and the 5s subagent poll stops with them while the one-shot
hydrate stays. Edit mode arranges what Simple shows: force-revealing
toggle-hidden panes is an Advanced behaviour.

Hidden surfaces do not mount — no pane bodies, no xterm, no statusbar
polling — verified live over CDP.

Also in tree-split: a capped all-fixed run whose remaining tracks are
end-placed chrome anchors to its edge, so a bottom terminal whose column
⌘J folded stays at the bottom instead of floating to the top.
2026-09-22 12:08:37 -05:00
brooklyn!
41d4124fff feat(desktop): interface mode resolver behind the presentation stores
Two modes. Advanced is the app as it is — `policy.advanced` is `{}`, so
every read falls through to the user's own atom and nothing is written on
an existing install. Simple shadows a table of preferences:

  effective(surface) = sessionReveal ?? policy[mode] ?? userPreference

Each preference store wraps its atom in `modeBound` and exports the
effective atom under the name everything already reads, so no consumer
learns the word "mode". A toggle pressed while shadowed lands in a
session layer that a restart or mode change clears — a mode is a default,
not a lock, and a reveal can never leak into the other mode's preference.
Inside `asLayoutIntent` a shadowed write is dropped instead: a layout pick
opening panes is not "show me this".

Tiers are the list-shaped half: an item tagged with a mode shows only
there, untagged shows everywhere, and the list filters once with
`shownInMode`. `$showsAdvancedChrome` is the same answer for single
elements.
2026-09-22 12:08:37 -05:00
teknium1
cc02316660 fix: portable MCP server names in the activation summary are the mcp.json names (post-#119263) 2026-09-22 09:50:22 -07:00
teknium1
21d0b12958 feat(plugins): late-loaded plugins wire their platform handlers live (#87770)
A plugin that finished loading after an adapter connected never got its platform
handlers (slash commands, button callbacks, inbound transforms) registered until a
gateway restart, silently. Three pieces, one seam shared by every surface:

1. Discovery listener: PluginManager.on_plugin_loaded(cb) fires from INSIDE
   discover_and_load for the plugins a sweep newly loaded (diff of the loaded set),
   with a per-plugin activation summary (hermes_cli/plugins_activation.py):
   activated_now {gateway_commands, gateway_transforms, hooks, callbacks} vs
   deferred {tools, prompt, mcp_servers}. Every mid-run load path now performs a real
   discover_plugins(force=True): CLI install/enable (via the gateway), Desktop/TUI
   plugins.manage install/toggle/update, dashboard REST install, tool-triggered
   force re-discovery, the new `reload-plugins` control-socket verb. A non-forced
   discover_plugins() short-circuits on _discovered, which is why reload.mcp after
   a mid-run install used to reload the OLD server set.
2. Idempotent re-wire: BasePlatformAdapter.rewire_plugin_handlers() runs only
   factories not yet wired on the live native client (keyed (plugin, qualname);
   a force reload hands back new function objects). Telegram hoists late handlers
   ahead of core's catch-all filters.COMMAND / CallbackQueryHandler (PTB dispatches
   the first match per group) and re-wires on the transient-init rebuild; Slack
   dedupes register_slack_action_handler per AsyncApp. The gateway runner
   subscribes per served profile and re-wires on the loop.
3. Scope limit + honest messaging: handlers only. Tools/prompt stay deferred to
   the next session (prompt-cache invariant), MCP servers to mcp.reload; the CLI
   hint and plugins.manage results (activation, gateway_reloaded,
   restart_required only when no gateway answered) say exactly that.
2026-09-22 09:50:22 -07:00
Siddharth Balyan
d826001ae5 fix(desktop): plugin install no longer fails its own wire contract (#119254)
`dashboard_install_plugin` has returned `python_dependencies` in its ok payload since 96e8a23222,
but `PluginsManageResult` (extra=forbid) was never given the field. Every Desktop install therefore
logged `result of 'plugins.manage' violates its wire contract` and the client never saw a clean
install result. Add the field; regenerate the TS/OpenRPC contracts.

The existing install test now sends the installer's real ok payload key for key, so a payload/contract
drift fails under HERMES_TEST_ISOLATION instead of only in a user's errors.log. Red on main, green here.

Live repro: x64 desktop, main 571ceec006, `hermes://plugin/install?repo=NousResearch/hermes-nvidia`
→ errors.log 21:08:42 "python_dependencies Extra inputs are not permitted".
2026-09-22 21:49:57 +05:30
Siddharth Balyan
d1ce67c67a fix(desktop): the connector dialog's verb and switch sit in its header (#119128)
* fix(desktop): the connector dialog's verb and switch sit in its header

The primary verb (Install, Authenticate, Connect, Reconnect) and the
server or app switch move to the header's right cluster, so a dialog
that only carried a button in its lead band loses the band and gets
squarer. Stop waiting stays next to its sentence in the lead.

An app whose sign-in expired or broke keeps the whole-app switch, so it
can be turned off; before, the switch showed only for a live connection
and an expired account could neither be switched off nor removed. When
Nous refuses to remove a sign-in, the confirm dialog now says so and
points at the switch. The tool-rule switches stay hidden until the
connection is live, not merely present. The usage footer is one line.

* fix(desktop): a squarer connector dialog; connector reads retry while the gateway opens

The dialog is 32rem wide instead of 42rem. A read that fails with
"Hermes gateway is not connected" retries with backoff up to five
times; before, a policy read that raced the socket at startup stayed
failed and the tool rules read as unchangeable until Retry.

* feat(desktop): one picker says where a paired app runs

A catalog app that Nous also runs showed two rows under "How Hermes
reaches <App>", a Connect button on one and a switch on the other, and
nothing said which one was in use. The section is now "Where <App>
runs" with one segmented control, Managed or On this device, and only
the chosen way's own control under it. Picking a segment makes that way
the one in use: it turns the other way off, and turns a ready way on.
The tool list follows the picker; the dialog's own hosted/local tabs
are gone. Paired cards always open the managed dialog, which now also
carries the local server's usage line and Advanced section.

* fix(desktop): one control on the paired app's device row; the search clear button sits at the row's edge

Under the picker, the On this device row carried a switch next to its
button. The picker already decides whether the server runs, so the row
now has its state sentence and one button: Install, Turn back on when
the server is off, Authenticate when it needs a sign-in, nothing when
it runs. The unused switch row component is gone.

The Connectors search input takes the row's width, so the clear button
sits at the right edge instead of hugging the typed text.
2026-09-22 20:40:00 +05:30