Models that interleave their reasoning_content and content token streams
(Kimi/DeepSeek/GLM-style routes) emit text -> reasoning -> text deltas
within a single tool-bounded segment. Appending each delta as its own
part shredded one sentence into "Let me" / Thinking / "verify the file",
with a Thinking disclosure wedged mid-sentence.
Coalesce streaming deltas into the most recent same-type part within the
current segment (bounded by any non-streaming part, e.g. a tool call).
The opposite streaming channel is transparent, so a reasoning burst
between two content deltas no longer opens a fresh text part, while a
real tool call still starts a new segment and preserves narration order.
Data-layer only; the renderer already groups consecutive reasoning.
A GUI app launched from Explorer inherits the environment block captured at
login, so a HERMES_HOME set via 'setx' AFTER login is invisible in process.env
even though the CLI (a fresh shell) sees it. The desktop then silently fell
back to %LOCALAPPDATA%\hermes and reported 'No inference provider configured'
despite a valid configured home (#45471).
resolveHermesHome() now consults the live HKCU\Environment registry value on
Windows before the LOCALAPPDATA default. New windows-user-env.cjs helper parses
'reg query' output, expands %VAR% refs, and fails safe (returns null off-Windows,
on spawn error, or empty value). The registry value is normalized through the
same normalizeHermesHomeRoot() path as the env var for consistency.
Co-authored-by: jeffrobodie-glitch <jeffrobodie@gmail.com>
When a desktop/dashboard session had no agent built yet and the user explicitly
picked a provider in the model picker, config.set('model', ...) would first try
to initialize the agent from the (possibly broken) config default provider —
failing before the user's explicit switch could take effect, trapping them on a
misconfigured default.
config.set now pre-parses the model flags: if an explicit --provider is present
and no agent exists yet, it skips the default-provider agent build and routes
straight through _apply_model_switch with the explicit provider. _apply_model_switch
gained a parsed_flags passthrough (avoids double-parsing) and only falls back to
resolve_runtime_provider(requested=None) when no explicit provider was given.
The desktop hook now sends config.set instead of slash.exec for active-session
model changes, so errors from the selected provider surface to the user instead
of being swallowed.
Co-authored-by: rodboev <rod.boev@gmail.com>
The /desktop page is deprecated and redirects to the home page. The
landing page for the desktop app is now simply
https://hermes-agent.nousresearch.com/. Update all docs and the
Docusaurus nav/footer links accordingly.
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
electron-builder 26.8.x can stage an Electron.app without its
Contents/MacOS/Electron binary, then fail renaming it to Hermes:
ENOENT: no such file or directory, rename .../MacOS/Electron -> .../MacOS/Hermes
This breaks `npm run pack` and the installer desktop stage before a
launchable Hermes.app exists.
- Point build.electronDist at the already-installed Electron dist so
electron-builder reuses it instead of re-unpacking from cache.
- Add a darwin-only prebuilder patch that restores the missing main
binary from the runtime dist before the rename. Idempotent (marker
guard), soft-fails on shape mismatch, survives node_modules reinstall.
Co-authored-by: ChasLui <chaslui@outlook.com>
Asserts ensureGatewayProfile keeps $connection in lockstep with the active
profile's backend: activating a remote pool profile flips mode to remote,
returning to default resyncs to local, a failed descriptor fetch leaves the
prior connection intact, and a same-profile activation doesn't churn it.
Regression coverage for #46651.
The renderer's $connection seeds from the PRIMARY (window) backend at boot and
otherwise only refreshes on a sleep/wake reconnect. Activating a background
profile (ensureGatewayProfile) pointed the live gateway + REST at that profile's
backend but never updated $connection, so its `mode` stayed stuck on the
primary. With a local primary and a remote pool profile active, every code path
that branches on local-vs-remote misfired: image attachments went out via the
path-based `image.attach` instead of `image.attach_bytes`, handing the remote
gateway a client-only Windows path it can't resolve ("image not found: C:\..."),
and the /api/fs/* file browser and /api/media fetches targeted the wrong
machine.
Resync $connection from the now-active profile's descriptor right after the
gateway swap, so the remote-aware paths follow the live backend. Best-effort: a
failed descriptor fetch leaves the prior connection intact for boot/reconnect to
resync. Single-profile users are unaffected (the same-profile fast path never
runs the swap).
Fixes#46651
Upgrade the Vite/esbuild surfaces that kept web, ui-tui, and the bootstrap installer on vulnerable esbuild versions, regenerate the root lockfile, and preserve intentional package+lock dependency edits during update lockfile cleanup.
The collapsed-pane hover-reveal trigger strip (14px wide, 6px edge
gutter) overlapped the neighboring scroller's 8px .scrollbar-dt
scrollbar, which sits flush with the window edge when the rail panes
are collapsed. Hovering the scrollbar revealed the file browser over
it, and clicks on the overlapped band hit the trigger instead of the
scrollbar thumb.
Widen the edge gutter to calc(0.5rem + 2px) so the strip clears the
scrollbar (rem-coupled to the .scrollbar-dt width) while still
covering the OS window-resize grab area inset.
Part of #44140 (item 2).
Co-authored-by: AIalliAI <285906080+AIalliAI@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Show a shimmering "Summarizing thread" label during auto-compaction, skip
the post-turn hydrate when compaction fired so the live transcript does not
collapse to the stored summary-only session.
Auto-compression rewrites history mid-turn, which made long threads look
like they reset. Re-tag the gateway lifecycle status as compacting and
surface it in the desktop thread loading indicators.
* fix(desktop): clarify enter-to-send and top-align choice radios
Match the composer keyboard contract in clarify freeform answers and align choice-row radio dots to the start of wrapped labels.
* fix(desktop): clarify loading spinner until request is ready
Hold the clarify panel on a centered Loader2 until clarify.request arrives instead of showing disabled choices or a loading-question stub.
* refactor(desktop): dedupe clarify shell and drop stale ready gates
Extract the shared clarify panel wrapper and remove disabled-state checks that loading already makes unreachable.
The turn-end sound is a notification concern, not an appearance one — relocate
the variant picker + preview from the Appearance tab to the Notifications tab
(its i18n keys move from settings.appearance to settings.notifications with it).
Adds a native OS notification system (Electron Notification, routed cross-OS)
distinct from the in-app toast feed. Before this, one hardcoded cue existed
(message.complete while document.hidden) with no settings or event coverage.
- Engine (store/native-notifications.ts): localStorage-backed prefs (master
switch + per-kind toggles) and a gated dispatcher over five kinds — approval,
input, turnDone, turnError, backgroundDone — with a 1s per-(kind,session)
self-evicting throttle.
- Gating: "backgrounded" = document.hidden OR !document.hasFocus(), so an
alt-tabbed window still counts as away. Completion kinds fire only when
backgrounded and for the active session (no spam from a busy gateway);
attention kinds (approval/input) also break through for off-screen sessions.
- Wired into real event sites (use-message-stream.ts): message.complete, error,
approval/clarify/sudo/secret.request; backgroundDone from composer-status at
the running -> exited transition.
- Click focuses the window and jumps to the originating session; approval
notifications carry Approve/Reject buttons that resolve in place over
approval.respond, mirroring the in-app Run/Reject bar.
- Settings: new Notifications panel (master + per-kind switches, test button
with real OS-result feedback). Full i18n (en/ja/zh/zh-hant).
* feat(desktop): add curated completion sound bank for turn completion
Replace the prior haptic-only completion cue with a curated Web Audio completion sound flow, defaulting to the minimal two-note comfort preset while keeping alternate presets available for quick iteration. Play the cue on every message completion event (including background sessions) so turn-end feedback is consistent across active and non-active chats.
* refactor(desktop): drop done1 byte sample from completion bank
Keep the curated Web Audio presets only; the embedded sample added bulk without shipping as the default cue.
* feat(desktop): expand completion sounds and add Appearance picker
Add fourteen synthesized turn-end presets with preview in settings, persisted variant selection, and softer default mixing for late-night use.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(desktop): dedupe completion-sound resolver, trim audio comments
Make the store the single source of truth for the variant default + range
validation and have the sound lib import it (one-way lib→store edge, no
cycle), instead of two divergent copies. Extract the shared white-noise
buffer used by the air/whoosh voices and cut the synth comments down to
why-only notes.
---------
Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): jump-to-approval pill for off-screen approvals
A blocked approval's only response surface is the inline Run/Reject bar on
the pending tool row. When that row is scrolled out of view the session looks
stalled with no visible action. Surface a composer-anchored "Approval needed"
pill only when an approval is pending AND its inline bar is scrolled away;
clicking scrolls the bar back into view. Preserves the deliberate inline (not
modal) approval design — the pill never duplicates the approve/reject controls.
The inline bar mirrors its own viewport visibility via IntersectionObserver
(tracks scroll/resize/layout) and registers a scroll-into-view handler the pill
fires, mirroring the existing thread-scroll jump-button bridge.
Supersedes #45828.
* fix(desktop): morph jump-to-bottom into approval prompt; drop scroll bridge
Collapse the separate "jump to approval" pill into the existing
scroll-to-bottom control: when scrolled away from the bottom while an approval
is pending, it relabels to "Approval needed". A parked approval's inline
Run/Reject bar is always the bottom-most content, so the existing
scroll-to-bottom action lands the user right on it — one control, no collision.
This also fixes the layout corruption from the first cut: the pill called
native el.scrollIntoView(), which scrolls every scrollable ancestor including
the overflow:hidden chat shell containers. Those have no scrollbar to scroll
back and don't remount on session switch, so the composer stayed shoved and
the breakage persisted across sessions. Reusing requestScrollToBottom() (the
use-stick-to-bottom path) only touches the one designated scroll container.
Removes the now-unused approval-scroll store + IntersectionObserver wiring.
Large paste and Ctrl+A → Delete froze the composer for seconds — both routed
through Chromium's contenteditable editing pipeline (~O(n²) on multiline DOM).
- insertPlainTextAtCaret: Range + text/<br> fragment (paste path)
- deleteSelectionInEditor: range.deleteContents for non-collapsed Backspace/Delete
- Shared composerSelectionRange helper; both flush via flushEditorToDraft
Profiled live (47 KB / 122 paragraphs): paste 4474 ms → 13 ms; select-delete
1304 ms → 4 ms. Collapsed-caret deletes still native.
* fix(desktop): accept slash command on space at command stage
Pressing space on a no-arg slash command (e.g. /hermes-agent) fell
through to the arg-completion stage and dead-ended on "No matches"
instead of inserting the directive. Space now mirrors Tab/Enter while
the command name is still being typed: no-arg commands commit the chip,
arg-taking commands expand to their options step.
* fix(desktop): suppress arg popover for no-arg slash commands
Committing a no-arg command (`/hermes-agent `) re-detected the chip+space
as an arg query and re-opened the popover on "No matches". The arg-stage
menu now only opens when the command actually takes args.
* fix(desktop): polish slash arg completion (space/tab/click + typed args)
Unify Enter/Tab/Space accept of the highlighted item at both the command
and arg stages: no-arg commands commit a chip, arg commands expand to
options, and an arg option commits the full `/cmd arg` chip. A fully-typed
arg (which the backend completer drops from suggestions) now commits on
Space/Tab via the verbatim text instead of dead-ending, and the "No
matches" empty state is suppressed past a command's name. Space stays
slash-only so @ mentions keep a literal space.
The native desktop approval bar deliberately omits the command because the
pending tool row "already shows it" — but that row only renders a single
truncated line, and a pending row can't be expanded (it has no result yet). So
the full command was only reachable by opening the "Always allow" dropdown,
reading the modal, cancelling, then clicking Run — 4-5 clicks just to see what
you're approving.
Add a "Command" toggle to the approval bar that reveals the full
`request.command` inline (reusing the dialog's pre styling), default collapsed.
Approving a long command is now "expand, Run". Gated on a non-empty command so
zero-command approvals are unaffected.
* feat(cli): add --safe-mode troubleshooting flag
Inspired by Claude Code v2.1.169 (June 2026): run Hermes with all
customizations disabled to isolate setup problems from product bugs.
--safe-mode implies --ignore-user-config and --ignore-rules, and
additionally skips plugin discovery (hermes_cli/plugins.py) and MCP
server loading (tools/mcp_tool.py) via the internal HERMES_SAFE_MODE
env bridge.
* fix(desktop): keep composer usable during reconnect
Right-click → Color flashed open then closed: on dismiss the context menu
refocuses its trigger, which doubles as the popover anchor, so the picker
read it as a focus-outside event and closed itself. Suppress the menu's
close auto-focus so the picker survives. Long-press already worked since
it bypasses the menu lifecycle.
The image-generate tool showed a placeholder, then the model echoed a
(often different) image inline in its prose — a second, jarring copy in
the wrong place, dimmed as tool scaffolding, with a misplaced download
button.
Now the generated image lives only in the tool slot:
- Strip every embedded image/media link from the assistant prose of a
message that produced an image (the model frequently restates the
remote URL while the result holds the local path), preserving the
agent's words. Applied on hydration, live deltas, and completion.
- One stable frame sized from the aspect_ratio arg up front, so the
diffusion placeholder and the decoded image share the same box and
crossfade with no layout shift; the box derives its height from the
true ratio on load (no letterboxing).
- Exempt generated images from the tool-block dim-until-hover rule.
- Extract a shared useImageDownload hook + ImageLightbox so the tool
image and markdown images share one implementation.
"Session busy" (4009) is the gateway's concurrency guard, not a user-facing
error. The queue already covers the deliberate "type while busy" case, so
the only leak was a submit racing the settle edge. Generalize the rewind
path's busy-retry into a shared `withSessionBusyRetry` and wrap every
`prompt.submit` (fresh send, session-resume resubmit, and rewind) so a
transient busy is ridden out within a bounded deadline and the call lands
silently. The fromQueue swallow stays as a backstop for the pathological
>deadline case.
A queued drain firing on the settle edge can race a not-yet-wound-down
turn and get a transient 4009 "session busy". Previously that appended a
red "session busy" error bubble (and toast) per attempt. For fromQueue
submits, swallow the busy error: release busy, keep the entry queued, and
let the composer's bounded auto-drain retry on the next idle.
A prompt typed mid-turn ("ghost bubble") could stick forever and never
send when the backend restarted/reconnected during the turn. Two fragile
assumptions in the composer queue drain caused it:
1. Drain fired ONLY on an observed busy true→false edge. A remount/
reconnect resets `previousBusyRef` to the current busy value, so the
settle edge is swallowed and the queue never drains. Replace
`shouldAutoDrainOnSettle` with the edge-independent `shouldAutoDrain`
(idle + non-empty), driven on the settle edge, on mount/reconnect, and
after a re-key. The drain lock still serializes sends.
2. The queue is keyed by `queueSessionKey || sessionId`. When a backend
resume mints a new runtime session id for the same conversation, the
entry strands under the dead key. Pass the *stable* stored id as
`queueSessionKey` so the composer can tell runtime churn from a real
session switch, and `migrateQueuedPrompts` re-keys pending entries on a
runtime-id change only (never on a deliberate switch).
Also make the drain resilient to a thrown/rejected onSubmit (e.g. a stale-
session 404): the entry stays queued and is retried on the next idle, with
a per-entry attempt cap (MAX_AUTO_DRAIN_ATTEMPTS) to avoid spin-loops and a
quiet toast once it gives up. A manual send clears the backoff.
Tests: composer-queue covers edge-free drain + re-key migration;
use-prompt-actions covers rejected-drain-keeps-entry + idle retry sends.
The diffusion placeholder read `--dt-*` tokens via
`getComputedStyle().getPropertyValue()`, but those resolve through `var()`
chains into `color-mix(in srgb, …)` — returned verbatim and unparseable, so
every token fell to a hardcoded light fallback (white card). In dark mode the
placeholder rendered as a white square.
Resolve each token through a throwaway probe element's `color` so the browser
computes it to a concrete color, and teach `parseColor` Chromium's
`color(srgb r g b / a)` serialization. Re-resolve on theme repaint via a
MutationObserver rather than per animation frame.
- Resume: fire the REST transcript prefetch and the session.resume RPC in
parallel, and skip the redundant message conversion + reconciliation
when the prefetch already hydrated the transcript.
- Haptics: web-haptics builds its AudioContext lazily on first trigger,
paying the ~850ms CoreAudio spin-up on the first streamStart haptic as
the first token paints. Open/close a throwaway context at idle so the
real one connects to an already-warm audio service.
Re-parsing the full message markdown every reveal frame is O(N^2) over a
long answer and dominated stream CPU.
- Throttle useSmoothReveal commits to ~1 frame (REVEAL_MIN_COMMIT_MS).
- Memoize block parsing with an LRU keyed on source text so only changed
blocks re-parse.
- Replace Streamdown's full-text parseIncompleteMarkdown with a
tail-bounded remend: scan to the last top-level boundary outside
fences/math and repair only the trailing open block. New remend-tail.ts
is proven render-equivalent to full remend at every streaming prefix
(remend-tail.test.ts), minus an intentional, documented divergence on
cross-block dangling openers.