Commit Graph

4447 Commits

Author SHA1 Message Date
Teknium 92e0de0ac4 feat(vault): Desktop, TUI and CLI surfaces for password-manager unlock
Desktop
- Settings → Credential Vault gains a "Password managers" section: per-manager
  toggle (disabled with a hint when the CLI isn't installed), Locked/Unlocked
  pill, Unlock (masked master-password dialog → vault.unlock) and Lock.
  Items from a manager show a source badge instead of a delete button.
- Mid-turn vault.unlock.request renders a masked card in the chat (same
  contract as the secret/sudo cards: dismiss = keep locked, late answers
  tolerated, blocks the composer, badges background sessions).
- i18n parity en/ar/ja/zh/zh-hant.

Ink TUI (hermes --tui): vault.unlock.request/expire overlay via MaskedPrompt;
Esc keeps the manager locked.

CLI: `hermes vault sources [--enable|--disable NAME]`; `hermes vault list`
shows the source column and names enabled-but-locked managers.

Docs: credential-vault.md covers managers, per-session unlock, and the
headless (cron/webhook/API/-q) no-prompt posture.
2026-09-10 10:35:07 -07:00
Teknium cde0ad0edd feat: agent signs into sites from an encrypted local vault (CLI, browser fill, Desktop Settings)
Consolidated re-apply of #96988 onto current main. Ported from
Merit-Systems/OpenInstinct (MIT) opaque-handle autofill design: the model
sees vault handles + login metadata, the password is resolved and filled
server-side over the supervised CDP socket, and filled values are scrubbed
from every browser tool result by an unconditional redaction registry.

Rebase adaptations to the Sep-2026 facade/sibling layout:
- toolsets: one _HERMES_CORE_TOOLS entry (the browser toolset derives from it)
- hermes_cli/main.py: vault parser registered via the subcommand owner table
- file_safety: vault/ joins the _READ_DENIED_DIRS credential-dir table
- redact: registry scrub runs before the redact_secrets early-return
- browser_vault_tool: _run_browser_command now lives in browser_tool_session
2026-09-10 10:35:07 -07:00
Erosika 2954fc7c55 fix(desktop): keep the relay deliver timeout beside its call
relay-deliver-budget.test.ts reads relay.ts and expects RELAY_DELIVER_TIMEOUT_MS within 400 characters of the bot_relay.deliver call. The comment above the new from_connection parameter pushed it past that window. The parameter names say what the gateway does with them.
2026-09-10 10:27:07 -07:00
Erosika 3db4defcc1 fix(bot-mode): qualify a relayed author from the local connection too
delivery_turn_author kept the bare bot:<profile> id when the sender's connection was the Desktop's own "local", so a DM relayed from that machine collided with the recipient's profile of the same name. A relayed DM always crosses gateways, so the connection id is now part of the id whenever the Desktop sends one, and only the direct message_agent path in tools/bot_mode_dm.py stays bare. The relay.ts and session_auto_continue.py comments added earlier are cut to one line each.
2026-09-10 10:27:07 -07:00
Erosika d3c8bacbe3 fix(bot-mode): qualify relayed authors with the sender's connection id
A relayed DM stamped bot:<profile> on the recipient turn, so an ops profile on another machine and the local ops profile shared one author id. The Desktop now forwards from_connection with each bot_relay.deliver, and delivery_turn_author builds bot:<connection>/<profile> for it while the Desktop's own gateway ("local") keeps the bare id. An api author object accepts an optional origin string that yields the same shape.
2026-09-10 10:27:07 -07:00
Erosika 16d15869ba feat(bot-mode): carry the sender through local and relay deliveries
a bot dm arrived as an ordinary user message. the only trace of the sender
was the "Message from" text prefix, which the model reads and nothing else
does. the recipient's memory provider saw its own configured user.

message_agent now passes the sender as {"id": "bot:<profile>", "name":
<handle>, "is_bot": true} to the delivery runner (--author <json>), which sets
HERMES_TURN_AUTHOR on the recipient one-shot only. the -Q turn reads it and
passes turn_author into run_conversation. the desktop relay forwards the
envelope's from_profile/from_handle to bot_relay.deliver, which sets the same
variable on its delivery turn. the runner drops any inherited author first so
a delivery without one stays unattributed. the text prefix is unchanged.
2026-09-10 10:27:07 -07:00
Erosika b1915cb02d fix(bot-mode): keep the relay waiter watching past the Desktop deliver deadline
The sender-side waiter gave up at 900s while the Desktop held bot_relay.deliver open for 1500s, so a turn finishing between minute 15 and minute 25 wrote a reply nobody read. REPLY_WAIT_SECONDS now rebuilds the Desktop budget from the same numbers and waits 60s past it. The two turn constants move into tools/bot_relay.py so the gateway handler and the waiter share one definition.
2026-09-10 10:18:27 -07:00
hermes-seaeye[bot] 8defaaad48 fmt(js): npm run fix on merge (#107504)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 16:49:07 +00:00
fangliquanflq 0596631bf3 fix(desktop): scope tour responses to active session 2026-09-10 09:40:01 -07:00
fangliquanflq 3c823f40b5 fix(preview): scope action responses to active session 2026-09-10 09:40:01 -07:00
Teknium 50ea107d9d fix(desktop): read an auto-TTS reply aloud once when the HUD is open
With `voice.auto_tts` on and HUD mode active, a reply could be spoken
twice: the HUD renderer and the hidden app window both claim
`speak:<messageId>` through `hermes:ambient:claim`, and main collapsed
the two claims with the same 1 s deduper it uses for the turn-end beep.
The app window under the HUD is throttled by Chromium, so its transcript
subscription fires well past 1 s, finds the key pruned, and is told it
owns the cue. Fixes #99717.

The unit was wrong, not the window: a spoken reply is minutes of audio
keyed by a durable message id. `createAmbientClaimArbiter` keeps the
tick-sized window for `sound:*` and holds `speak:*` claims for a
10-minute TTL. No HUD special-casing: the same race exists between any
two windows showing the same chat, and the arbiter fixes them all.

Diagnosis credit: #99810 (@liuhao1024) and #100289 (@shivamjg101),
whose TTL framing this follows.
2026-09-10 09:38:07 -07:00
Konstantin Khlopkov 62081b87b7 fix(desktop): give the Hyprland HUD float/pin promotion room for a lagging j/clients
The first map on Hyprland can leave the HUD tiled when `j/clients` does not
list the new client within the 8 × 50 ms the promotion loop allowed; the
window then stays a stretched transparent tile (#103091). Widen the budget to
24 × 100 ms. The loop returns on the first successful float+pin, so a prompt
compositor pays nothing extra.

Salvaged from #103116. Its second half (re-promote from focusWindow's
re-show branch) is dropped: the HUD is destroyed and respawned on every
toggle and is not minimizable, so that branch never runs for it.
2026-09-10 09:37:47 -07:00
Teknium 7617a91d44 test(desktop): trim HUD band tests to the two invariants
Keep the row-shape contract (a TurnRow carrying aui_message-group measures;
clearance/background-resume spacers do not) and drop the two cases that
only re-assert the selector's shape.
2026-09-10 09:37:30 -07:00
KoNit-K 84a07e7683 fix(desktop): match HUD transcript rows with aui_message-group slot
TurnRow roots now carry data-slot="aui_message-group", so the band's
*:not([data-slot]) selector measured an empty short session as 0px.
Include message-group rows while keeping the slot-less "show earlier" path.

Fixes #107050

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 09:37:30 -07:00
jonny 3868ee9a12 Merge pull request #107239 from KoNit-K/fix/extension-titlebar-hide-fixed-clusters
fix(desktop): hide fixed titlebar clusters on contributed full pages
2026-09-10 17:55:11 +02:00
hermes-seaeye[bot] 110736c0bc fmt(js): npm run fix on merge (#107454)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 15:26:17 +00:00
Teknium 248ff2d3e8 feat(desktop): one row per plugin; desktop halves are app-level copies, never profile-scoped
Capabilities → Plugins is now a single table: one row per PACKAGE, with a
Desktop column (this app) and an Agent column (the selected profile). A
package with both halves is one row, never two; the kind badge is inferred
from what it ships (plugin.yaml → agent, plugin.js → desktop).

The desktop half of a unified agent+desktop package no longer loads from the
profile-shaped `plugins/<name>/desktop/` folder. Electron copies that half
into `~/.hermes/desktop-plugins/<name>/` beside a `.hermes-package.json`
marker (package name, source, origin repo/sha) and keeps it in sync: newer
source → re-copy, package uninstalled → copy removed, hand-installed
standalone folder of the same name → never overwritten. The renderer scans
exactly one root, so a pane can never appear, disappear, or re-scope when the
user switches profiles — the same switch reads the same value everywhere.

Why a copy rather than scanning every profile: two profiles can carry the
same package at different SHAs; a scan has to pick one silently. One copy,
one source of truth, stamped with where it came from.

- plugin-packages.ts: pure merge of desktop records + agent rows → rows
- plugins.manage list reports `has_desktop_half` so the pairing is explicit
- Install dialog (local backend): the desktop half is materialised from the
  installed package instead of cloning a second standalone copy; remote
  backends keep the separate clone. Target path now names the real profile
  folder for non-default profiles.
- "Install here": a desktop half whose agent half is missing in the selected
  profile pre-fills the dialog from the marker's origin; disabled with an
  explanation for hand-copied folders with no origin.
- Profile selector moves into the Agent column header; hidden with 1 profile
- Rescan/Update reconcile the copies BEFORE rescanning (ordering bug)
- Drop the dead `agentPluginsRoot` IPC; docs updated (desktop.md, SDK,
  bot-mode.md, hermes-desktop-plugins reference)

Live-dogfooded on a headless Electron with two profiles and a real
file:// git package: install both halves, profile switch ×3, Install here
into the second profile, v2 update via `hermes plugins update` → chip text
changes on Rescan, uninstall from both profiles → copy and row gone, broken
plugin row, cold restart, sash drag/reset, legacy Settings → Plugins
redirect.
2026-09-10 08:19:55 -07:00
Teknium ff93f0ee9b fix(desktop): desktop plugins are app-level, not per profile; catalog gets a drag sash; Accent Picker moves to its own repo
Three things Teknium hit on the merged Plugins page:

1. Desktop plugins vanished on profile switch. `fs-ipc.ts::localPluginsRoot`
   resolved `<HERMES_HOME>/profiles/<active>/desktop-plugins` for a named
   Desktop profile, so a disk-installed plugin only existed under the profile
   it was installed from. Desktop plugins extend the app, not an agent; the
   root is now `<HERMES_HOME>/desktop-plugins` regardless of profile, gateway
   or remote machine (`electron/desktop-plugins-root.ts`), with a one-time
   migration that lifts any per-profile folders into the app root (root copy
   wins on collision). Agent-plugin and logs roots stay profile-scoped.

   On the page, the profile selector now renders INSIDE the framed Agent
   plugins block as its header, and Desktop plugins sit outside that frame,
   so the scope boundary is visible without reading the blurb.

2. The catalog viewport could not be resized. It gets the same top-edge drag
   sash as the Skills hub picker (persisted height, double-click resets,
   clamped so the lists above keep real height; iframe pointer-events off
   while dragging).

3. Accent Picker, a theme-authoring toy shipped off by default, is removed
   from the bundled set and published as a standalone desktop plugin at
   https://github.com/NousResearch/hermes-desktop-accent-picker (same source,
   esbuild-bundled plugin.js importing only the three loader-resolvable
   specifiers). Docs point there.
2026-09-10 08:19:55 -07:00
Teknium 8092f0e8ee fix(desktop): leaving HUD mode always gives the app window back (macOS)
Symptom (Discord/X report, macOS): exit HUD mode and the small HUD window
stays on screen while the main window never regains focus; the toggle now
reads the wrong state, so there is no in-app way out.

Three defects in the HUD lifecycle, all in the main process:

- closeHudWindow() detached the window's 'closed' listener before calling
  win.close(). close() is a REQUEST that waits on the renderer's unload
  handlers, so a HUD renderer that never answered left an always-on-top
  window with no teardown owner and no close broadcast. The 'closed'
  handler now stays attached and is the single teardown path; a bounded
  requestHudClose() (electron/hud-close.ts) destroys the window after a
  1.5 s grace if the renderer never lets go.

- ⌘W inside the HUD was routed to the MAIN renderer's close-active-tab
  (installPreviewShortcut is wired on every chat window), so it closed a
  tab in the hidden app window and left the HUD up. ⌘W in the HUD now
  runs closeHudWindow(), the same path as the exit button.

- restoreMainWindowFromHud() was a bare show() and only armed when the
  main window was isVisible() at HUD-open time. On macOS isVisible() is
  false for an occluded window (Electron folds occlusionState in), so
  the app never came back when the HUD was opened over another app, and
  show() alone does not make the window key. The restore now arms on a
  live main window and routes through focusWindow() (restore → show →
  focus). Closes #88513 (credit: PR #90489's diagnosis).

Renderer: the handoff now also requests composer focus on the close
broadcast, so the app window gets the keyboard, not just the session.

Docs: ⌘/Ctrl+W added to the HUD exit list.
2026-09-10 08:19:13 -07:00
Teknium 67764dc086 test(desktop): poll the autosave test with vi.waitFor under fake timers
config-settings.test.tsx runs vi.useFakeTimers({ shouldAdvanceTime: true })
and then polled with RTL's waitFor, whose interval/timeout also run on the
faked clock. On a loaded CI runner the interplay of real-time advance and
the faked poll interval starved the poll and the file timed out at 15s
(seen on a PR that never touched this file). vi.waitFor advances the fake
timers on every check, which is what the test needs.
2026-09-10 05:09:15 -07:00
Teknium b6bf9700a9 test(desktop): type the hoisted requestGateway mock so the pin test typechecks
CI runs tsc on the test tree; the hoisted vi.fn had a zero-arg signature so
mockImplementation with a (method) callback was rejected.
2026-09-10 05:09:15 -07:00
Teknium 6d89c1afde feat(plugins): pin a plugin to a commit from Desktop, and show pins in every list
Only the CLI could install a plugin at an exact commit (`--ref <sha>`); the
Desktop "Install from Git" dialog, the TUI gateway `plugins.manage install`
method and the dashboard `/agent-plugins/install` endpoint all called
`_install_plugin_core` without a ref, so a team that wanted everyone on the
same private-plugin commit had to leave the app for a terminal.

- `dashboard_install_plugin(ref=)` threads the pin to `_install_plugin_core`;
  the 40-hex validation and HEAD verification are unchanged. The gateway
  method and dashboard body accept `ref`.
- Desktop dialog gains an optional "Pin to commit" field (custom sources only,
  client-side 40-hex check disables Install on anything shorter).
- `plugins.manage list` rows carry `pinned_sha`; the Desktop plugins tab shows
  a `pinned @ <sha8>` badge and `hermes plugins list` prints `git pinned@<sha8>`
  in Source, so a team can eyeball that everyone runs the same commit.
2026-09-10 05:09:15 -07:00
hermes-seaeye[bot] 1603a073e4 fmt(js): npm run fix on merge (#107293)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 11:04:48 +00:00
686f6c61 f44b0fd342 fix(desktop): keep model picker on the selected provider
Treat the Desktop catalog selection as a (provider, model) pair so a
shared model id on OpenRouter cannot rewrite a custom or first-party
pick after refresh or keyboard highlight.
2026-09-10 03:58:52 -07:00
hermes-seaeye[bot] 16a408534c fmt(js): npm run fix on merge (#107255)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 09:55:33 +00:00
Teknium d5495e762c fix(desktop): messaging restart banner lives in the detail column; drop duplicate toast CTA
Live pass showed the banner spanning list + detail and the save toast carrying a
second 'Restart gateway' button beside the banner's 'Restart now'. Render the
banner inside DetailColumn (content width, above the platform header) and let the
toast stay a plain confirmation. Also hide the redundant 'Messaging gateway
stopped' setup pill when the state pill already says exactly that.
2026-09-10 02:48:21 -07:00
Teknium 52dea2c7d9 feat(desktop): Telegram QR quick setup + persistent restart banner on Messaging
Port the dashboard Channels flow into the Desktop Messaging page:

- Telegram "Quick setup": the Nous pairing service mints a bot; the page shows
  the QR / deep link, polls until Telegram confirms, pre-fills the detected
  owner id into the allowlist, and applies. The backend writes the token +
  allowlist into the scoped profile and restarts the gateway; the page watches
  the restart child's exit so a failed restart surfaces instead of a silent
  "gateway stopped".
- Restart-needed banner: every credential save / clear / enable toggle keeps a
  banner with "Restart now" up until a restart actually completes. Toasts
  vanish; the credential still only loads on the next gateway start.
- runGatewayRestart now resolves a boolean (never rejected before, so the
  Webhooks page's catch path was dead and its banner cleared on failure).
- New API helpers carry the page's profile scope on every pairing call so
  start/status/apply hit the same backend that holds the pairing in memory.
- i18n keys in all six locales; `qrcode` added to the desktop deps (already
  pinned identically in web/).
2026-09-10 02:48:21 -07:00
hermes-seaeye[bot] 5172f22df2 fmt(js): npm run fix on merge (#107244)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 09:37:09 +00:00
Teknium 61afcde8f9 refactor(desktop): one Plugins surface — Capabilities → Plugins owns agent + desktop plugins, install, and the catalog
Plugins were split across two pages that each showed half the picture:
Settings → Plugins listed desktop plugins plus "Install from Git" and a
pointer saying agent plugins live elsewhere; Capabilities → Plugins listed
agent plugins plus the catalog picker but knew nothing about desktop
plugins. A user asking "what extends my Hermes and where do I add more?"
had to visit both and still could not see the whole set in one place.

Capabilities → Plugins is now THE plugins page:

- Agent plugins section (scoped to the profile selector) with the
  "Install from Git" button in its header — installs target the scoped
  profile, not whichever one is active.
- Desktop plugins section beneath it (same for every profile), with the
  folder/rescan controls and the "agent half missing here" drift chip,
  whose repair also lands in the SCOPED profile.
- The catalog picker underneath, unchanged.

Settings → Plugins is removed. `/settings?tab=plugins[&plugin=…]` and the
existing `?tab=mcp` redirect share one table (`settings/moved-tabs.ts`) so
old bookmarks and palette links land on the same row on the new page.
Command palette: plugins moved from the Settings group to the Capabilities
group; installed-plugin rows deep-link to `/skills?tab=plugins&plugin=…`.
Dead `settings.plugins.agent.*` and `settings.nav.plugins` i18n keys dropped;
docs and in-code pointers say Capabilities → Plugins.
2026-09-10 02:30:50 -07:00
KoNit-K 564895fea8 fix(desktop): keep plugin titlebar slots on extension pages
Hiding the app clusters on contributed full pages must not unmount titleBar.left/center/right; overlays still return null.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 17:27:06 +08:00
KoNit-K 3f044f9887 fix(desktop): hide fixed titlebar clusters on contributed full pages
Plugin ROUTES_AREA pages own their titlebar chrome; the app clusters were sitting on top of it because only overlays hid them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 17:24:01 +08:00
Teknium 2554a16a1d fix(desktop/settings): let searchable-select popovers size to content, not the shrink-wrapped trigger
The Timezone field (and the voice/model ComboboxInput) pinned the popover to
--radix-popover-trigger-width. In the settings grid the trigger shrink-wraps to
its current value (~90-120px), so every IANA row was clipped to "Africa/A…" and
the picker was unusable by eye even though selection itself worked. Use min-w so
the list keeps its own width and only grows to cover a wider trigger.

Live-verified on a sandboxed Electron instance over CDP: popover 123px -> 288px,
rows no longer overflow; click, type+Enter and autosave to config.yaml all land.
2026-09-10 01:04:59 -07:00
brooklyn! 6e07eb4838 docs(desktop): define panel titlebar and sticky backdrop behavior 2026-09-10 01:14:30 -05:00
brooklyn! e7c819a7e1 fix(desktop): mask text behind sticky user messages 2026-09-10 01:14:30 -05:00
brooklyn! bbe212de9b feat(desktop): keep panel tabs in the titlebar 2026-09-10 01:14:30 -05:00
hermes-seaeye[bot] cfdbbb6e35 fmt(js): npm run fix on merge (#107102)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 04:49:08 +00:00
hermes-seaeye[bot] 425b00174d fmt(js): npm run fix on merge (#107101)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 04:43:55 +00:00
hermes-seaeye[bot] 7cdc6a7338 fmt(js): npm run fix on merge (#107097)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 04:38:38 +00:00
brooklyn! 78052fe437 fix(desktop): keep notification IPC types independent of the renderer
The Electron composite project imported src/global.d.ts and followed its renderer-only dependencies. Share the notification payload from a dependency-free Electron module instead; all desktop typechecks and lint now pass.
2026-09-09 23:37:48 -05:00
brooklyn! 85571c16b6 fix(desktop): reveal the notification session without replacing its tab
Resolve missing runtime mappings through the shared mirror, dismiss covering routes, and preserve Bot tab ownership. Includes the mapping fix from https://github.com/NousResearch/hermes-agent/pull/100405 (b9e66c4a485dc353238fab8c7a07aa16107d11b7).

Co-authored-by: Nikita Simakov <everm1nd.mail@gmail.com>
2026-09-09 23:37:48 -05:00
brooklyn! 7107458d96 fix(desktop): route native notifications back to their source window
Capture durable focus identity before runtime bindings disappear, retain native handlers, and keep runtime approval actions and plugin callbacks on their source renderer.
2026-09-09 23:37:48 -05:00
Brian Sweatt f557829bff fix(desktop): retain notification handlers across banner dismissal
Adapted from https://github.com/NousResearch/hermes-agent/pull/65897 (a6fb6fe063e235c33b41212bd9cdaea4f9dcb1dc). Keep handlers across close, release on consumption or failure, and dismiss expired notifications before releasing them.

Co-authored-by: brooklyn! <brooklyn.bb.nicholson@gmail.com>
2026-09-09 23:37:48 -05:00
outdog-hwh a245d9aaa4 fix(desktop): resolve native notification action indexes
Adapted from https://github.com/NousResearch/hermes-agent/pull/51893 (a70b0991b2d72796479d63532ccf0fac15b17ba0).

Co-authored-by: brooklyn! <brooklyn.bb.nicholson@gmail.com>
2026-09-09 23:37:48 -05:00
brooklyn! c7676fc6b7 docs(desktop): explain radio artist lookups and privacy 2026-09-09 23:32:55 -05:00
brooklyn! 983f0c2870 feat(desktop): link radio credits directly to artist profiles 2026-09-09 23:32:55 -05:00
brooklyn! a6102b8d80 docs(desktop): document Radio controls and privacy 2026-09-09 22:57:27 -05:00
brooklyn! 3f53719c1d feat(desktop): ship Radio as an opt-in SDK plugin 2026-09-09 22:57:27 -05:00
hermes-seaeye[bot] 1675f1f2c2 fmt(js): npm run fix on merge (#107057)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-10 03:02:40 +00:00
brooklyn! b5c0a7ebe2 test(desktop): cover clarify submit shortcuts and input guards 2026-09-09 21:57:01 -05:00
brooklyn! ff7a1e8953 fix(desktop): restore Cmd/Ctrl+Enter in clarify forms 2026-09-09 21:57:01 -05:00